mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-19 01:26:29 +03:00
* Add oracle harness: verb goldens and function-level vectors
Records stdout/stderr/exit/files for every impeccable verb over a fixed
corpus and replays them against an alternate implementation. Adds a loader
hook that captures per-function call vectors from the pure engine modules.
Prepared with AI assistance (Claude Code).
* Oracle: hook, hook-before-edit, hook-admin cases and goldens
Prepared with AI assistance (Claude Code).
* Add docs/CLI-CONTRACT.md: observable behavior of every impeccable verb
Prepared with AI assistance (Claude Code).
* Oracle: context/doctor/pin/surface-brief/critique/palette/embed/signals/csp/seed/genimg/question cases and goldens
Prepared with AI assistance (Claude Code).
* Oracle: live-mode cases and goldens (roots, inject, wrap, insert, accept, session, manual edits, daemon)
Prepared with AI assistance (Claude Code).
* Oracle: mask the binary path before HOME; export launcher env to the binary
Prepared with AI assistance (Claude Code).
* detect: set process.exitCode instead of exiting after the final write
process.exit() right after a large piped stdout write truncated JSON output
at the pipe buffer boundary; found by the oracle harness. Re-record the six
directory-scan goldens that had captured the truncation.
Prepared with AI assistance (Claude Code).
* Oracle: normalize the hook-admin command in both runtimes' forms and audit chars
Prepared with AI assistance (Claude Code).
* Skill text: invoke the impeccable launcher instead of node scripts
Every `node {{scripts_path}}/<name>.mjs` becomes `{{scripts_path}}/impeccable <verb>`
(context-signals -> signals, hook-admin -> hooks). Setup step 1 drops Node, points
Windows shells without sh at impeccable.cmd, and says the launcher runs a
self-contained binary. allowed-tools follows.
Prepared with AI assistance (Claude Code).
* Scripts dir: replace the Node scripts with the impeccable launcher
skill/scripts keeps command-metadata.json and the page JS; every .mjs entry
point, lib/, and live/ are gone (the binary owns those verbs). Adds the POSIX
launcher, impeccable.cmd, VERSION (copied from the new root ENGINE_VERSION),
scripts/fetch-engine.mjs (bun run fetch:engine) to pull the pinned binary
into skill/scripts/bin/<os>-<arch>/, and gitignores that bin dir.
Prepared with AI assistance (Claude Code).
* Build: ship the launcher instead of bundling the JS engine
readSourceFiles no longer copies cli/engine into the skill; the scripts
payload is the launcher (executable bit preserved through dist, plugin/, and
universal.zip), impeccable.cmd, VERSION (synced from ENGINE_VERSION on every
build), the page JS, and command-metadata.json. Hook manifests call
`<scripts>/impeccable hook` behind an existence guard (Codex adds a
commandWindows sibling calling impeccable.cmd; Cursor runs hook-before-edit;
GitHub keeps the git rev-parse form; Grok mirrors Claude); the Node probe and
systemMessage notice are gone. build:release fetches the pinned engine for
every target (lenient) and stages bin/<os-arch>/ into the dist skill copies
after root harness dirs and plugin/ were synced, so git-delivered trees stay
launcher-only. The detection-rule count check reads the vendored
extension/detector/antipatterns.json and is skipped when absent.
build:browser is a stub; the codex prefix rewrite leaves
`{{scripts_path}}/impeccable` alone.
Prepared with AI assistance (Claude Code).
* CLI: turn the impeccable npm package into a platform-binary shim
cli/engine, cli/lib, and cli/bin/commands are gone; their behavior lives in
the engine binary. cli/bin/cli.js now resolves the binary from IMPECCABLE_BIN,
the @impeccable/cli-<os>-<arch> optional dependency (templates under
cli/platform-packages/, published by the engine release), the
~/.impeccable/bin/<version>/ cache, or a checksum-verified download, and
execs it. package.json drops the engine dependencies and the library
exports; puppeteer moves to devDependencies for the icon scripts.
README.npm.md describes the shim.
Prepared with AI assistance (Claude Code).
* Tests: gate behavior on the oracle and the engine binary
Unit tests of the deleted Node scripts and the JS detector are removed;
their behavior is pinned by tests/oracle goldens (frozen JS behavior plus
reviewed deltas) and the engine's own tests. tests/oracle.test.mjs replays
the corpus against the binary (IMPECCABLE_BIN or skill/scripts/bin/<target>/,
via tests/lib/engine-bin.mjs) and skips cleanly without one; the framework
fixture sweep drives live-inject, live-wrap, and detect-csp through the
binary the same way. record.mjs learns --bin. The function-level vectors
under tests/oracle/vectors/calls are committed as the frozen snapshot they
can no longer be regenerated from. Suites: core trimmed to build and
transformer tests, oracle added to the default run, detector/live reduced to
packaging and reference checks, the live-e2e helper tests move to the opt-in
live-e2e lane pending its retarget, cli-remote-e2e is an empty placeholder.
Prepared with AI assistance (Claude Code).
* Docs: describe the launcher, the engine pin, and the oracle gate
CLAUDE.md gains an Engine binary section (launcher lookup order, ENGINE_VERSION,
untracked binaries, how tests get one, the oracle as behavior gate, what stays
JavaScript) and drops the Node-script and JS-detector descriptions; the CLI
and detection-rule sections point at the shim and the engine repo. README.md
states the skill needs no runtime and lists the launcher-based hook commands;
AGENTS.md follows. CLI-CONTRACT.md's intro notes the scripts it quotes are
the recorded source, not the tree.
Prepared with AI assistance (Claude Code).
* Tests: tighten the hook command guard assertion
Prepared with AI assistance (Claude Code).
* Oracle: re-golden 46 cases for the engine's own command names; record them in DELTAS.md
Prepared with AI assistance (Claude Code).
* Build: ship launcher-only release zips by default
IMPECCABLE_BUNDLE_ENGINE=1 opts in to staging the engine binaries into the
dist skill copies. Bundling every target into every provider copy put
dist/universal.zip near 340 MB, past the 25 MB Cloudflare Pages file cap
that impeccable install downloads through.
Prepared with AI assistance (Claude Code).
* Tests: drive the live-e2e orchestrator through the engine binary
The session, fake-agent loop, steer test, and manual-edit probe spawn
<binary> <verb> (live-server, live, live-inject, live-wrap, live-insert,
live-accept, live-poll, live-complete) resolved by tests/lib/engine-bin.mjs
instead of node skill/scripts/live-*.mjs; the completion typing the agent
imported from the deleted live/completion.mjs is a small local helper. The
live-e2e helper unit tests move back into the default live suite (the steer
loop skips without a binary).
Prepared with AI assistance (Claude Code).
* Tests: run new-work-e2e through the engine's serve-question and generate-image verbs
Prepared with AI assistance (Claude Code).
* Tests: point the skill-behavior harness at the launcher and engine binary
The bash tool exports IMPECCABLE_BIN so the staged skill's launcher runs
without a download; scenarios assert on 'impeccable context' instead of
context.mjs and skip without a binary.
Prepared with AI assistance (Claude Code).
* Tests: note what plugin-e2e validates before and after the generated-output sync
Prepared with AI assistance (Claude Code).
* Oracle: record the engine's 'wasm-unsafe-eval' CSP meta patch as a reviewed delta
Prepared with AI assistance (Claude Code).
* Rebase reconciliation: fold main's post-freeze work into the swapped tree
The rebase onto origin/main brought changes whose JS engine halves left the
tree with the swap. This commit reconciles what survives:
- Suite map: register main's comp-fidelity unit tests (build-phase,
comp-diff, font-match, hero-checks) in the core suite and
live-browser-ignores in the live suite.
- Payload guard: the skill scripts payload now allowlists the comp-fidelity
build pipeline (comp-spec/comp-diff/build-phase/font-match and their libs),
the one Node toolchain that has not moved into the engine.
- Drop skill/scripts/live/project-ignores.mjs, lib/live-path-globs.mjs, and
their test: they import hook-lib/live-inject/impeccable-paths, which the
swap deleted, and their consumer (the JS live server) is the engine now.
- skill text: the comp pipeline's calls to engine verbs (generate-image,
embed-prompt) use the launcher spelling.
- Oracle: re-record 17 detect goldens over the fixture set main changed
(oklch #592, color-mix #578, 1D grid #615, the two comp-fidelity rules)
and record the gap in DELTAS.md; those JS rule changes are not yet ported
to the engine, and the goldens pin its current behavior.
bun run test (oracle included) and bun run build are green on this tree.
AI-assisted change: implemented with Claude Code.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx
* Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job
Byte-identical copies of the engine repo's launchers (engine main
af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is
rejected by the engine-probe handshake instead of hijacking every verb;
impeccable.cmd's download path is rewritten as straight-line goto flow
(the parenthesized blocks expanded %url%/%cached% at parse time, making
it dead code) with certutil sha256 verification and a windows-arm64 ->
x64 asset fallback; the final error points at the release download
instead of npm i -g (npm still serves the 3.x CLI).
ci.yml: the generated-output check no longer diffs the deleted
cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches
the pinned engine (bun run fetch:engine) and replays tests/oracle/
against it. The job is continue-on-error with a loud warning until the
first engine release exists; flipping it to required is a release-time
toggle, documented in the workflow.
Verified here: sh -n on both launcher copies, bun run build green, full
oracle replay against the rebuilt engine binary green (770 pass, 0
fail), and a launcher behavior test proving a fake 3.x CLI on PATH is
skipped while the download + checksum chain completes against a local
file server.
Prepared with AI assistance (Claude Code).
* Oracle: restore detector goldens to post-fix behavior after the engine ports
The Aug 17-31 detector fixes (oklch parsing, color-mix nested hex, 1D grid
pass, comment stripping, root-relative linked stylesheets, URL userinfo
redaction, inert ignore-value refusal) and the comp-fidelity rules
organic-clip-path / buried-raster are ported to the engine. Re-records the
gap-pinning detect goldens from the fixed binary (glow.html included: its
.photo-opaque-grad column now carries the buried-raster finding it was
written for), replays the frozen checkHtmlPatterns call vectors through the
last JS engine state in history (db1462b9^; args untouched, 14 of 101
results moved), and rewrites the DELTAS gap section into the landed-ports
note. Each re-recorded json fixture golden byte-matches that JS state's
output; oracle: 770 pass, 0 fail.
Prepared with AI assistance (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx
* Oracle: pin the Aug 17-31 verb fixes ported to the Rust engine
New cases: hook-session-grok-edit-then-stop (Grok Build camelCase envelope,
end_turn/shutdown/stopHookActive Stop handling, 35ae0733 + bfe634e2 +
3c442af7, #646), hook-session-codex-stop-decision (Codex Stop emits
decision/block, c9e7cd8a, #603), and doctor-order-boot-and-deep (boot and
deep findings keep their established artifact order, 80997663).
Re-recorded goldens whose old bytes froze pre-fix behavior, with a
DELTAS.md entry naming each upstream hash: the Stop finding-cache sync
(3c442af7), the Edit|Write manifests without the retired MultiEdit matcher
(7d5c60d2), and the failWithRollback field order (1f2c3f9d).
Prepared with AI assistance (Claude Code).
* Oracle: drop a duplicated DELTAS section
The verb-fix section landed twice when two porting sessions staged the
same file; keep one copy.
Prepared with AI assistance (Claude Code).
* Oracle: pin the hooks ignore-value inert-entry refusal
Three hadmin-ignore-value-inert-* cases record the engine's port of
be87f5eb (#662) to hooks ignore-value: an exact value for a rule whose
findings can never extract one is refused with the wildcard-plus-file
route (and no config write), while the wildcard scoped form for the same
rule is accepted. Goldens recorded from the engine binary and verified
byte-for-byte against the ea360025 hook-admin.mjs on the same sequences.
No existing golden changes, so no DELTAS entry is owed.
Prepared with AI assistance (Claude Code).
* Launcher: fail closed on a missing download checksum (engine triage C1)
Byte-identical sync of the engine repo's launchers: a freshly downloaded
engine binary now runs only after verifying against its .sha256 sidecar.
A sidecar that cannot be fetched, or a machine with no sha256 tool,
refuses the download instead of exec'ing an unverified binary; the
wget-only path fetches the sidecar too. Binaries already on PATH or in
the cache that pass engine-probe are unaffected.
Prepared with AI assistance (Claude Code).
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx
* Enforce engine-before-skill release order (triage D4)
The launcher, npm shim, and `impeccable install` all resolve the engine
binary for the pinned ENGINE_VERSION, so a skill/CLI release or a rust-swap
merge published ahead of the engine release + platform packages dead-ends
every install path. Add a mechanical guard:
- scripts/check-engine-release.mjs: verifies all five dist binaries +
.sha256 and the five @impeccable/cli-<os>-<arch> npm platform packages
exist for the pinned ENGINE_VERSION; names missing assets, exits non-zero.
Honors IMPECCABLE_DOWNLOAD_BASE.
- release.mjs: hard-fails release:skill and release:cli when assets are
missing; extension is exempt (vendored WASM detector, no engine exec).
- CI engine-release-ready job: runs the check, continue-on-error with a
loud ::warning until the first engine release exists (flip to false then).
- CLAUDE.md Releases: documents the enforced ordering.
Prepared with AI assistance (Claude Code).
* Oracle: re-record the Sep-1 verb fixes ported to the Rust engine
Five fixes landed on main in JS between the swap branch and its rebase and were
ported to the engine; the goldens they touch are re-recorded from the fixed
binary, each engine output first diffed byte-for-byte against the upstream JS on
the same inputs. DELTAS.md documents every case with its upstream hash.
- critique-* (usage/unknown/latest-existing/write-then-read/write-monorepo-child):
the #660 critique close path (identity + fingerprint freshness, ~NNNN
collision suffix, closed flag, close verb, latest --json). Upstream 5211bdf4.
- detect-* (new overused-font fixture cases, dir/scope/no-advisory sweeps):
the #678 overused-font primary-face change (a system stack keeps its system
face, so a Roboto fallback no longer flags). Upstream 2cfd6076.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx
* Tests: fix pre-existing release-guard staging on the swap branch
release.test.mjs was already red on the swap branch: release.mjs imports
check-engine-release.mjs and fetch-engine.mjs (the D4 engine release-order
guard), which the temp work tree never staged, so every dry run failed to
resolve the module instead of exercising the guard. Stage both modules and set
IMPECCABLE_SKIP_ENGINE_CHECK=1 so the guard does not probe the network; this
suite predates the guard and only covers the version/changelog/artifact checks.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx
* oracle: pin E8 stale-hook-manifest detector fallback (context)
Cover the v3-to-launcher upgrade fix (triage E8) recorded from the engine
binary and hand-reviewed:
- context-stale-hook-manifest: a .claude/settings.local.json naming the retired
`node .../hook.mjs` script under the claude-code provider emits
MANUAL_DETECTOR_REQUIRED, because the stale marker no longer counts as an
active hook (its script is gone after the update).
- context-launcher-hook-active: the same manifest in the launcher form still
suppresses MANUAL_DETECTOR_REQUIRED, confirming the launcher marker is
recognized as active.
The only difference between the two goldens is the MANUAL_DETECTOR_REQUIRED
block. No existing golden moved: every other context case runs under the source
provider, whose hook-manifest list is empty, so none of them scan a manifest.
Also null IMPECCABLE_PROVIDER_ID in the case BASE_ENV so a recording machine's
value cannot leak. DELTAS.md records the intentional divergence from JS parity.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaJv2c4oN8wS7Ttq4XRqyx
* Tests: stop two harness hangs from wedging a whole run
Two suites could hang forever and never print a tally, because the one
mechanism that could interrupt the wedged work was missing on both paths.
Hang 1 (bun run test / build-phase.test.mjs): the test's run() helper
spawned every child with spawnSync and no timeout. spawnSync blocks the
test worker's thread, so node's --test-timeout (an event-loop timer)
cannot interrupt a child that wedges (a fork/exec blocked on OS resources
under concurrency, a gate's comp-diff grandchild, or a stray browser
launch). Bound every child with spawnSync timeout + killSignal SIGKILL so
a wedge becomes a fast, named failure the next test survives.
Hang 2 (bun run test:skill-behavior): runTurn called generateText with no
client-side deadline, so a stalled provider stream kept the fetch (and the
whole node process) alive past the per-test timeout, producing no tally.
Attach a real AbortSignal (default 840s, under the 900s per-test cap):
on expiry the fetch aborts, the turn throws, and the scenario
fails-and-continues. The unref'd timer is cleared on completion.
Runner backstops: run-tests.mjs now spawns each command as a detached
process-group leader and enforces a per-suite wall-clock cap that SIGKILLs
the entire group (workers, grandchildren, browsers) on expiry, with
SIGINT/SIGTERM forwarded so Ctrl-C still reaps the tree. The core node
batch gets a finite --test-timeout (180s); skill-behavior gets a 60min
group cap. Env overrides: IMPECCABLE_TEST_WALL_CLOCK_MS,
IMPECCABLE_SKILL_BEHAVIOR_TURN_TIMEOUT_MS,
IMPECCABLE_BUILD_PHASE_RUN_TIMEOUT_MS.
Proof: bun run test green twice (~60s); scoped claude-sonnet-5
skill-behavior sweep terminates with a tally (20 tests, ~32min) where the
840s abort caught a wedged redesign turn and the sweep continued instead
of hanging.
Prepared with AI assistance (Claude Code).
* launcher: export skill-dir env before the IMPECCABLE_BIN exec (sync engine fix)
Prepared with AI assistance (Claude Code).
* Node-free swap: comp-fidelity verbs move to the engine
The four comp-fidelity scripts (comp-spec, comp-diff, font-match, build-phase)
and their six libs are ported into the impeccable-engine binary. This removes
the last Node .mjs from the skill: `git ls-files skill/scripts | grep '\.mjs$'`
now returns nothing.
- reference/new-work.md, reference/visualize.md, and the asset-producer /
finish-reviewer agents now invoke `{{scripts_path}}/impeccable <verb>` instead
of `node <script>.mjs`.
- Deleted the ten ported .mjs and the four JS unit tests that imported them
(their behavior is now covered by the engine's Rust tests and the oracle);
removed those files from scripts/test-suites.mjs.
- Added oracle cases (comp-*, font-match-*, build-phase-*) over a comp-basic
workspace, recorded from the engine binary; the deterministic outputs are
byte-identical to the JS the scripts left behind.
- docs/CLI-CONTRACT.md documents the four verbs, the CDP font rendering, and
the runtime-resolved (never-committed) font-index catalog.
The font-index catalog JSON stays shipped in the skill (data/font-index.json);
the engine resolves it at run time and never vendors it.
Prepared with AI assistance (Claude Code).
* reorg: public plumbing for the in-repo Rust workspace and the two-release flow
The engine binaries move from the impeccable-dist channel to this repo's own
GitHub Releases (tag engine-v<ENGINE_VERSION>), and the closed detector the
engine links arrives as detector-v<DETECTOR_VERSION> releases on the same
repo. This commit wires the public side for that; the crates themselves land
in the next commit.
- Launcher (sh + cmd), npm shim, fetch-engine and check-engine-release now
download from github.com/pbakaus/impeccable/releases/download/engine-v<X>/.
- release.mjs gains `engine`: verifies ENGINE_VERSION against the platform
package pins and the detector release, tags, pushes; release-engine.yml
builds the five targets and publishes. check-detector-release.mjs is the
matching release-order guard (with tests).
- Root Cargo.toml (workspace, lto = false with the reason), rust-toolchain.toml
(exact pin), DETECTOR_VERSION, /target ignored.
- CI: rust + rust-windows jobs and an oracle job that replays the goldens
against a source build, warn-only until the first detector release exists;
ci-test-plan exposes a `rust` output.
- docs/ENGINE.md (the crate map and the closed-detector mechanism) and the
CLAUDE.md engine, release-order and rules sections.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* reorg C: the open Rust runtime joins this repo as one Cargo workspace
The engine no longer lives in a separate repo. `crates/` is a snapshot of the
open crates (foundation, core, common, context, live, hook, skills, comp,
comp-verbs, html, browser, detect, cli) plus `Cargo.lock`, taken as a git
archive of the engine repo at the commit that finished the boundary split.
None of that repo's history comes with it, and none of it should: the closed
half stays private.
The closed half is the rule engine. It ships as a prebuilt native archive per
target, `libimpeccable_detector.a`, published as a `detector-v<X>` GitHub
Release on this repo. `crates/core/build.rs` resolves and links it three ways:
`IMPECCABLE_DETECTOR_LIB=<dir>` for a local detector build, else the
`~/.impeccable/detector/<version>/<target>/` cache, else a download verified
against its `.sha256` sidecar. `crates/core` is a thin shim over a three-symbol
C ABI; nothing above it knows the boundary exists.
What changed versus the engine repo copy:
- Every crate manifest moves from `license-file.workspace` to
`license.workspace` (this workspace declares Apache-2.0), and the workspace
gains the `postcard` dependency the boundary encoding needs.
- The launcher contract test reads `skill/scripts/impeccable{,.cmd}` instead of
a sibling `launcher/` dir, and `engine_binary` downloads from
`github.com/pbakaus/impeccable/releases/download/engine-v<version>/` instead
of the retired dist repo. No oracle golden carried the old URL, so no
re-recording was owed.
- The tests that hunted for a public repo through `IMPECCABLE_PUBLIC_REPO`,
`../impeccable-second` or a hardcoded home directory now resolve the root as
`CARGO_MANIFEST_DIR/../..`, because they are in it. The env var stays as an
override for an out-of-tree checkout.
- The in-page bundle (`detect-antipatterns-browser.js`, 2 MB of generated wasm
glue) is no longer tracked. `crates/core/build.rs` resolves it beside the
archive, hands the path to `impeccable_core::browser::IN_PAGE_BUNDLE_JS`, and
live mode serves that. `scripts/check-detector-release.mjs` now requires it
and its `.sha256` in a detector release.
- The live crate embeds `skill/scripts/live-browser*.js` and
`modern-screenshot.umd.js` directly rather than through vendored copies, so
the binary and the installed skill cannot drift.
- `crates/browser/assets/` (an unused second copy of the bundle) is gone.
- `tests/lib/engine-bin.mjs` also accepts `target/release/impeccable`, so a
plain `cargo build --release -p impeccable` is enough to run `bun run test`.
Verified with the archive from a local detector build: `cargo test --workspace`
267 pass, oracle 795 pass / 0 fail / 0 missing, `bun run build` clean, the
default suite green, and the launcher's `engine-probe` handshake answering
through `skill/scripts/impeccable`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* docs: bring RUNTIME-ENV and PORTING-GUIDE over with the runtime
They describe the binary's environment contract and the parity method every
crate here was ported with; both belong next to the crates now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* core/build.rs: refuse a detector archive built by another rustc, in plain words
The archive links only against the exact rustc that built it; a mismatch
used to surface as pages of undefined std symbols from the linker. The
detector repo now writes rustc-version.txt next to the archive (and ships it
with the release); when it is present, build.rs compares it with its own
compiler and names both versions.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* build:extension: ship the wasm-core extension shell and vendor its detector from the detector release
`bun run build:extension` was broken on this branch: it still imported the
deleted JS engine (cli/engine/registry/antipatterns.mjs,
scripts/lib/browser-detector-bundle.js).
The shipped shell now matches the new design. The content script only
snapshots the DOM; an extension-owned offscreen document runs the
WebAssembly rule core over that snapshot, so the scanned page's CSP no
longer matters. That replaces the old approach of injecting a JS rules
bundle into the page. New files: extension/offscreen/offscreen.html, plus
the "offscreen" permission and a 'wasm-unsafe-eval' extension_pages CSP in
the manifest.
The manifest version stays at 1.3.3. The shell's own manifest carried
2.0.0; feature branches never bump versions, so the bump is a release step.
The five generated detector pieces (core.js, core_bg.wasm, snapshot.js,
overlay.js, antipatterns.json) are vendored at build time into the
gitignored extension/detector/ by the new scripts/lib/detector-bundle.mjs,
which resolves them the same three ways crates/core/build.rs resolves the
native archive: IMPECCABLE_DETECTOR_LIB/extension-detector/, the
~/.impeccable/detector/<DETECTOR_VERSION>/ cache, then a checksum-verified
download of detector-browser-bundle.zip from the detector release.
antipatterns.json is no longer regenerated here.
The zip packaging is unchanged. The Firefox variant still builds so
`web-ext lint` keeps covering the shared shell, but it cannot scan: Gecko
has no chrome.offscreen API. The build prints a one-line warning saying so.
Also here: a referenced-path check that fails the build when the manifest
or the service worker points at a file that is not in extension/, a
resolver unit test wired into the core suite, and the detector rule count
in the READMEs synced to the 61 the vendored registry carries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* oracle: replay byte-for-byte on Linux too
The corpus was recorded on macOS and eight cases failed on ubuntu CI for
reasons that were all environment, not behavior:
- stageWorkspace returns the realpath of the staged dir. macOS's tmpdir is a
symlink and two goldens (context-dir-override, live-accept-source-locked)
had recorded that artifact; both re-recorded, reviewed in DELTAS.md. The
source-locked case now actually exercises the lock it is named for.
- context-lowercase-product-name declares platforms: ['darwin', 'win32'];
run.mjs skips such cases elsewhere and says so in the summary.
- The hook-project workspace's empty provider skill folders (.claude,
.cursor) are now tracked with .gitkeep; git cannot track empty
directories, so a fresh checkout had none and hooks on found nothing to
repair.
- crates/live's read_dir_raw sorts entries by name: the goldens hold the
order macOS returned, Linux returns hash order, and the source-candidate
lists in live-commit output depended on it.
macOS: 795 pass, 0 fail. The Svelte accept cases additionally need the
public repo's node_modules on the machine that runs them (CI now installs
them).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* oracle: mask <HOME> only at path boundaries (a short home like /root ate 'roots.json')
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* oracle: track live-html's dist/generated.html (the root dist/ ignore hid it from CI checkouts)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* release-engine: darwin-x64 builds on macos-14 (macos-13 is retired)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Open the detector: the rule crates join the workspace, the C-ABI goes away
The detector is open source. The rules it ships were already public in this
repo's git history and in every npm tarball of the JS engine, so a closed
binary bought nothing it could keep; the moat is the service (the catalog,
the labs, the review pipeline), not the check functions. Keeping them behind
a prebuilt archive cost a C-ABI, an exact toolchain pin, a build-time
download, a second release to order ahead of every engine release, and a
serde layer that had to serve two encodings.
Deleted
- crates/core/src/ffi.rs, crates/core/build.rs, crates/core/tests/boundary.rs
and the shim modules under src/checks and src/browser.
- crates/foundation/src/boundary.rs and the postcard dependency.
- DETECTOR_VERSION, scripts/check-detector-release.mjs and its test, the
check:detector-release script, the detector gate and
IMPECCABLE_SKIP_DETECTOR_CHECK in scripts/release.mjs.
- scripts/lib/detector-bundle.mjs and tests/detector-bundle.test.mjs (the
vendoring path for the closed browser bundle).
- scripts/build-browser-detector.js and the build:browser script (a stub
since the JS engine left the tree).
- xtask's detector-archive subcommand and its public-repo lookup.
Came back
- crates/core is now the rule logic itself: every check_* / scan_*, the
browser adapters, the visual-contrast decisions. It re-exports foundation
as before, so no consumer changed. Its vectors dispatcher is the union of
both id tables again, and tests/vectors.rs replays the frozen vectors
straight through it.
- crates/wasm and crates/xtask join the workspace. cargo xtask bundle builds
the in-page bundle from browser-bundle/ plus the wasm core, writes
dist/, refreshes the tracked crates/live/assets/detect-antipatterns-
browser.js, and writes extension/detector/. bun run build:extension runs
it instead of downloading.
- crates/live/assets/detect-antipatterns-browser.js is tracked again; live
mode embeds it and serves it as /detect.js.
- Serde is back to plain derives: no is_human_readable branch in
js::json_number, derived Serialize for Rgba and BrowserFinding with their
skip_serializing_if attributes.
- profile.release has lto = "fat" again; rust-toolchain.toml is plain
stable plus the wasm32 target. The rust, rust-windows and oracle CI jobs
lose continue-on-error and can be required.
Verified
- cargo build --workspace --all-targets: clean, no warnings.
- cargo test --workspace: 346 pass, 0 fail (the 8 boundary tests are gone
with the boundary).
- cargo build -p impeccable-wasm --target wasm32-unknown-unknown --release: ok.
- cargo xtask bundle && cargo xtask bundle --check: reproducible; the
regenerated bundle is committed (it differs from the archived one, which
was built with a pinned rustc and lto = false).
- cargo build --release -p impeccable: no linker warnings, 12.5 MB (the
same source at lto = false is 13.1 MB).
- oracle: 795 pass, 0 fail, 0 accepted deltas, 0 missing goldens.
- bun run build, bun run build:extension, web-ext lint (0 errors,
8 warnings), bun run test: 363 + 80 + 1 + 1 + 133 + 180 + 4 pass, 0 fail.
- impeccable detect --no-config --json tests/fixtures/antipatterns: 128.7 ms
median of 5.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* core: doc comments drop the open/closed split
The rule crate and the foundation crate are both Apache-2.0 in one
workspace now, so "open", "closed" and "crosses the boundary" no longer
describe anything. Comments only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Rule packs: downstream crates add rules on all three engines; wasm detect surface
A crate that depends on this workspace can now add rules without forking
it. `impeccable_core::rule_pack::RulePack` (object-safe, Send + Sync +
Debug) carries a pack's registry rows plus three hooks that default to
empty: `check_text` for the text engine, `check_element_dom` and
`check_page_dom` for the browser driver. `impeccable_html::StaticRulePack`
adds `check_document` for the static engine, where the document model
belongs to the html crate and detect cannot name it.
The registry keeps ANTIPATTERNS as the built-in list; `registry::extend`
appends a pack's rows and every lookup consults them after the built-ins,
so a pack can never shadow a built-in id (extend panics on a collision and
is idempotent per slice). `all_antipatterns()` is the built-ins followed by
the registered rows.
Hook order, chosen so built-in output cannot move:
- detect_text: after every matcher, analyzer and the dedupe, before inline
ignores, so `impeccable-disable` waives pack rules like built-in ones.
- detect_html_source: after the element rules, the design-system merge and
the page passes, again before inline ignores. One pack pass per HTML
file: the document hook when set, otherwise the text hook over the raw
source, so a pack implementing both never reports twice.
- collect_browser_findings: the element hook at the end of the per-element
loop through the same disabled-rules filter and group, the page hook
after every built-in page pass with the same el-or-body attribution.
A pack travels on TextOptions / ScanOptions, DetectHtmlOptions
(static_rule_pack plus rule_pack), StaticHtmlEngine, and BrowserConfig
(serde-skipped: a pack is a Rust value, not JSON from the page). The
shipped binary installs none.
`crates/wasm --features detect` exposes the two file engines as JSON
exports for hosts that cannot exec the binary: `detect_text_json` and
`detect_html_source_json`, options `{ inlineIgnores?, designSystem? }`,
returning the findings array `detect --json` prints. `antipatterns_json`
now includes a pack's rows. `set_rule_pack` and `set_static_rule_pack` are
Rust-only, for a crate that links this one as an rlib.
Tests: registry extension and collision in foundation, one test pack per
engine (crates/core, crates/detect, crates/html tests) proving each hook
fires, that the built-in findings are unchanged, and that the waivers and
the disabled-rules list cover pack rules, plus the wasm export shapes.
Workspace tests 346 to 361, oracle 795/0 unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* docs: the cutover checklist under the open design
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* bundle: the page JS and the bundler become a library crate downstream packs can reuse
The in-page bundle, the extension pieces, the registry JSON and the
wasm-pack call were reachable only through `cargo xtask bundle`, which read
`browser-bundle/*.js` from the repo root. A downstream crate that links
impeccable-core + impeccable-wasm with its own rule pack had to copy the
page JS to produce a detector bundle for its module.
They move to `impeccable-bundle` (crates/bundle), which embeds every
`browser-bundle/*.js` with `include_str!` and exposes `in_page_bundle`,
`extension_pieces`, `registry_json`, `check_capture_contract` and
`wasm_pack_build`. Nothing writes files or exits the process; the caller
places the bytes. `registry_json` now reads `all_antipatterns()`, so an
installed pack's rows land in `antipatterns.json` too (no built-in change).
xtask becomes the workspace's caller and writes the same files to the same
places; `cargo xtask bundle` is byte-identical, tracked live asset included.
`IMPECCABLE_BUNDLE_SKIP_WASM_PACK` is the skip switch's new name, the old
`IMPECCABLE_XTASK_SKIP_WASM_PACK` still works.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* The immediate tier moves to the registry, and reaches wasm
The design hook's immediate-tier list is the set of rule ids worth fixing
at the edit site, and a downstream reviewer wants the same set to decide
how loudly a finding is reported. `impeccable-hook` is native-only, so the
list moves to `impeccable_core::registry` (the hook re-exports it) and the
`detect` feature gains `immediate_tier_rules_json()`.
The export is behind `detect`, which the in-page bundle does not build, so
the tracked browser asset is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* docs: Pristine tracks the engine by revision pin, not npm
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* docs: the cutover checklist is maintainer-side, not part of the tree
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: Fix flat type hierarchy false positives (#702)
Upstream sha 84728e9ce4.
The rule now reads rendered semantic roles and the dominant size per role
instead of the raw set of font sizes on the page, and it fires only when
every adjacent role step is under 1.25x.
- crates/core checks::rules gains TYPE_HIERARCHY_SELECTOR / MIN_ROLES /
MIN_STEP_RATIO, typeHierarchyRole, dominantTypeRoleSize and
checkFlatTypeHierarchySamples, the shared half of checks.mjs.
- crates/core browser::page_checks gets checkFlatTypeHierarchyFromDoc over
the Dom trait, with the overlay skip selector checkTypography passes.
- crates/html page.rs gets the same walk over StaticDocument.
- crates/detect drops the source-only analyzer: flat-type-hierarchy leaves
REGEX_ANALYZERS, the text-content analyzers shift to index 1, and
analyzer_rule_id loses its first row.
- crates/html cascade defaults gain contentVisibility, and crates/foundation
registry carries the reworded description.
Goldens re-recorded (the binary now matches origin/main's JS engine on every
one of these fixtures, verified by scanning the shared corpus with both):
glow, icon-tile-stack, layout, modern-color-borders, motion,
named-color-borders, numbered-section-markers, oklch-neon-text,
typography-should-flag, json and text.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: Fix detector URL scans and advisory handling (#709)
Upstream sha fa44839f72.
Advisory handling. `severity` becomes the canonical registry field: the
`advisory` bool leaves `Antipattern`, `advisory_rule_ids` filters on
`severity == "advisory"`, and `derive_advisory_flag` stamps the finding's
`advisory: true` from the effective severity, so a per-finding promotion or
demotion carries the flag. The html and browser engines call it after their
severity override; the detect CLI and the hook accept either spelling; the
driver's serializer and the wasm registry exports derive it the same way.
em-dash-overuse moves from `advisory: true` to `severity: "advisory"`.
URL scans. `expand_joined_url_targets` splits an argv value that is entirely
whitespace-separated URLs and leaves paths with spaces alone. The browser
driver reads the readable linked-stylesheet corpus into the HTML pattern
corpora and resolves a finding's selector with `selector_nodes_for_live_dom`
/ `pseudo_element_host_selector`, so an unresolvable selector drops the
finding instead of keeping it page-level. The CSSOM walk itself is page JS:
`browser-bundle/15-snapshot.js` gains `__snapLinkedStylesheetText` (grouping
rules flattened, container-query probes, effective keyframes) and puts it in
the snapshot as `linkedCss`; `10-probe.js` exposes the same for the in-page
route, and the Dom trait carries `linked_stylesheet_text`.
Also `enclosing_css_selector` blanks comments before hunting the previous
declaration delimiter, and `check_typography` reports the uniquely most-used
family instead of every family over a 15% share.
Verified: `impeccable detect --no-config --json tests/fixtures/antipatterns`
is now byte-identical to `node cli/bin/cli.js` on an origin/main worktree
over the shared corpus (432 findings). The two changed lines in
tests/oracle/vectors/calls/rules.checks/checkHtmlPatterns.jsonl were
re-recorded by running origin/main's `checkHtmlPatterns` over the frozen
args; only the comment-polluted selector changed. Goldens re-recorded for
the advisory partition (config-*, fixture gemini/gpt-tells,
numbered-section-labels, scoped-ignore, shape-assembled-illustration,
color, em-dash-entities) and the help text, each cross-checked against the
JS on origin/main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: stop gray-on-color false positives on Tailwind opacity and JSX (#707)
Upstream sha 32b270f4e8.
`find_solid_chromatic_bg` replaces the bare `bg-<hue>-<n>` match in both
engines: a `bg-blue-500/10` tint is a wash, not a solid fill. The `regex`
crate has no lookahead, so the maximal digit run plus the word boundary is
matched as before and the byte after it is tested for `/`.
The text engine gains the JS-source scanner (`scan_js`) and the scope
helpers on top of it: `containing_markup_tag` keeps a gray text class from
pairing with a background in a sibling tag on the same line, and
`find_ternary_split` / `exclusive_class_scopes` split a `cond ? a : b`
class expression into its arms, recursing into nested ternaries, ignoring
`?.` and `??`, and keeping a common prefix and post-ternary suffix in every
arm. `MatchCtx` now carries the match offset the scope lookup needs.
Verified against origin/main's JS: all eleven cases from the upstream test
file plus a nested / nullish / suffix set produce byte-identical findings on
both engines; they are pinned as Rust unit tests in `regex_matchers` and
`checks::rules`. The shared fixture corpus stays byte-identical.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: resolve unique --target names in monorepos (#706)
Upstream sha 8b326fc81e.
`resolve_target_path` / `find_unique_bare_target` in `crates/context`: a
`--target` that does not exist and reduces to a single path segment under
cwd resolves to the one workspace candidate with that name, so `--target a`
selects `apps/a`. A caller that already absolutized the name against cwd
(live and the other helpers do) takes the same route. Ambiguous or unknown
names still report the miss.
The context CLI resolves the target once and hands the resolved path to
`load_context`, replacing `path_exists_for_target`.
Oracle: four new `context-monorepo-target-bare-*` cases (bare name,
absolutized bare name, unknown name, bare name from a child cwd).
`context-monorepo-target-b-inherits` was re-recorded: resolving the target
before `load_context` changes its `surfaceBriefReason` from `not-found` to
`invalid-target`, which is what origin/main's `context.mjs` prints for the
same run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: Fix Next.js 16 CSP and parent hook discovery (#710)
Upstream sha 672ca29642.
CSP. `detect-csp` recognizes Next.js 16's `proxy.{ts,js,mjs}` request hook
beside `middleware.*`, but only where it sits at a project root or its `src/`
directory: the scan root itself, or a nested directory carrying a Next
project marker (a `next.config.*`, an `app` / `pages` dir, or a `next`
dependency). A same-named helper elsewhere in the tree is not the framework
hook.
Context. `find_git_boundary_root` gives `resolve_project` a git-boundary
notion: an explicit target inside its own repository resolves against that
repository, and an external target resolves against its own root, so caller
context never leaks across the boundary. `hook_manifest_search_roots`
replaces the cwd/projectRoot/repoRoot triple with a walk up from the
project root that stops at the first git boundary, and each root's own hook
lifecycle config is honored before its manifest counts as coverage.
Verified against origin/main's JS: nine `detect-csp` placements and five
hook-discovery scenarios (enclosing harness root, that root disabled,
sibling target, nested git target, markerless nested git target) produce
identical output.
Oracle: five `csp-proxy-*` cases and five `context-hook-*` /
`context-markerless-nested-git-target` cases. Four route-target goldens were
re-recorded because #710 resolves a `/`-prefixed target outside the
workspace; each was cross-checked against origin/main, and
`surface-brief-write-route` has a DELTAS entry for the one wording
difference (an unwritable filesystem root).
`tests/framework-fixtures.test.mjs`'s new proxy-placement block came in from
the merge importing the deleted `detectCsp`; it now drives `detect-csp`
through the binary like the rest of that file.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: fail URL scans when the browser is unavailable (#711)
Upstream sha f2f9958be1e6a4ecb1fbd5ef1ae1b7d9c53e0d24 (Fix: fail URL scans
when the browser is unavailable).
`detect` gains an operational-failure flag. Exit 1 now means at least one
requested target could not be scanned, and it takes precedence over exit 2,
because findings from the targets that did scan do not turn a partial scan
into a complete one. The flag is set by an unreachable path, an unreadable
directory or file in a dir walk, a per-file scan that throws, a URL scan
that throws, and a shared-browser setup failure.
- `walk_dir_reporting` and `build_import_graph_reporting` take a read-error
callback; the plain wrappers stay for callers that do not report. A file
the graph could not read is skipped for the scan too.
- `SharedBrowser::ensure_launched` is the eager half of
`createBrowserDetector()`: the CLI brings the browser up before the loop so
a launch failure prints one `Error:` line and every URL target is skipped,
instead of the lazy launch reporting once per URL.
- The static engine and the text path spell a permission failure the way Node
does (`EACCES: permission denied, open '<path>'`), which is what
`Error: cannot scan <target>: <message>` prints.
- Usage text and docs/CLI-CONTRACT.md carry the exit-status block.
Verified against origin/main's JS: missing target, missing target alongside a
flagging file, unreadable file, unreadable file beside a readable sibling,
unreadable directory, unreadable nested directory, a clean scan, and a
browser-unavailable scan of one and of two URLs all agree on exit code,
stdout and stderr (the browser-not-found wording is the pre-existing
puppeteer-vs-discovery difference).
Oracle: `detect-missing-file` and `detect-missing-file-json` re-recorded at
exit 1, plus new `detect-missing-file-with-findings`,
`detect-unreadable-file-json` and `detect-unreadable-file-in-dir`, each
cross-checked against origin/main. `detect-help` carries the new block.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: OpenCode slash command bridge (#483)
Upstream sha 9736a9f6e9.
OpenCode does not honor `user-invocable: true` on SKILL.md frontmatter, so a
pinned skill never reaches its slash menu. `pin` now writes
`commands/impeccable-<cmd>.md` on the OpenCode command schema instead, and
skips `.opencode` in the SKILL.md loop so no unreachable
`.opencode/skills/<cmd>` is left behind. `unpin` mirrors it, marker-guarded,
and reaches both scopes even when the skill itself is gone.
`find_opencode_commands_dirs` covers the project-local dir when the project
has the skill and the user config dir when Impeccable is installed globally,
resolving that dir the way the CLI does (`OPENCODE_CONFIG_DIR` ->
`XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`).
The build-tooling half of the upstream change (transformers, the OpenCode
command the build generates, `root-commands-sync`) came in with the merge and
needed no port.
Verified against origin/main's pin.mjs across seven scenarios (no harness,
project scope, user scope, a foreign command file, pin then unpin, unpin over
a foreign file, unpin with nothing pinned): identical stdout, identical file
sets, identical file contents apart from the one deliberate difference.
Oracle: five `pin-opencode-*` cases, with a DELTAS entry for the bridge body
naming the launcher rather than `node .../context.mjs`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: Fix Codex skill version metadata (#703)
Upstream sha 482368511a.
Codex's validator rejects unknown top-level keys, so the Codex and `.agents`
skills now carry `version` under the spec-defined `metadata:` map. Both
version readers learn the same parser: `parse_skill_frontmatter_version` in
`crates/context` (the boot update check) and `extract_version` in
`crates/skills` (`getSkillsVersion`). A metadata version wins, a legacy
top-level one still reads, only the map's own indent level counts, tabs count
as two spaces, and a comment line is skipped.
The build-tooling half (`versionInMetadata` on the two providers, the YAML
emitter's nested-object branch) came in with the merge.
Fourteen frontmatter shapes were recorded from origin/main's
`parseSkillFrontmatterVersion` and pinned as unit tests in both crates.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: Fix skill subcommand help handling (#708)
Upstream sha a264199177.
`install`, `link`, `update` and `check` render static help before entering
any operational path, through both the top-level verb and the legacy `skills`
namespace, for `--help` and `-h` alike.
Verified against origin/main's `cli/bin/cli.js`: all six spellings produce
identical text and exit codes.
Oracle: a new `tests/oracle/cases/skills.mjs` with seven help cases. Only the
help paths are pinned there; every other installer path writes into harness
directories or reaches the network.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Oracle: goldens for the three fixtures the merge added
`tests/fixtures/antipatterns/` gained `flat-type-hierarchy.html` (#702) and
`linked-url-patterns.{css,html}` (#709) with the merge, so the corpus
generator produced six `detect-fixture-*` cases with no goldens and the
directory-wide cases (`detect-dir-*`, `detect-scope-*`, `detect-no-advisory-*`)
moved.
Every golden here was recorded from the binary and then cross-checked against
`node cli/bin/cli.js` on an origin/main worktree over the same files: the six
per-fixture cases agree byte for byte in JSON and text, and a full scan of
`tests/fixtures/antipatterns` produces 432 findings identical on both engines
after normalizing the repo path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Port: the installer half of the OpenCode command bridge (#483)
Upstream sha 9736a9f6e9, the part of it that
lives in `cli/bin/commands/skills.mjs` rather than `pin.mjs`.
`copy_provider_commands` mirrors `copy_provider_skills` for a provider's
compiled `commands/` dir: project scope writes `<root>/<configDir>/commands`,
user scope writes the config dir OpenCode actually scans
(`OPENCODE_CONFIG_DIR` -> `XDG_CONFIG_HOME/opencode` -> `~/.config/opencode`),
and a pre-#406 global install at `~/.opencode/commands/` loses exactly the
files just written while siblings, symlinked dirs and home-rooted git repos
are left alone. It runs on install, on the reinstall refresh, on update, and
on link, which is the only path that can deliver the bridge to a linked
install.
`is_up_to_date` now compares the bundle's command files too, so an install
whose skills match but whose bridge is missing or drifted refreshes instead of
reporting success while the slash command stays absent. Only bundle-shipped
files are compared, so a pinned shortcut never affects freshness.
`tests/copy-provider-commands.test.js` arrived with the merge importing the
deleted `cli/bin/commands/skills.mjs`; its scenarios are ported to
`crates/skills/tests/provider_commands_tests.rs` (project scope, the three
user-scope dir resolutions, the legacy migration and its two guards, a
provider with no commands dir, and the four `isUpToDate` command-awareness
cases), and the file is removed and deregistered.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* CI: the first full run on the branch, three fixes
- The oracle harness masks the climb to the root a /-prefixed target
produces (<UP_TO_ROOT>/): the number of `../` is the staged tmpdir's depth
(7 on macOS, 2 on Linux), not the verb's behavior. surface-brief-path-slash
re-recorded.
- Two context test helpers canonicalized their temp dir, which on Windows
yields a \\?\ verbatim path that takes `/` literally; they strip the prefix
like Node's realpathSync. The critique-storage identity test compares
against the platform's own resolved path.
- Every job that drives the binary end to end (live-e2e smoke and full,
accept-cleanup, the DeepSeek sweep, the remote CLI smoke) builds it from
the checkout first; before, they looked for a release that does not exist.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* context tests: the verbatim-prefix strip spells the prefix once
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* context test: derive the snapshot identity from the verb's own resolver
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* context test: JSON-quote the snapshot identity, as the verb does
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* detect test: import resolution against platform-form paths
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* hook test: the stock cache path in the host's path form; Windows CI runs every crate's tests before failing
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: skills tests pass on Windows
The two test temp roots kept `canonicalize`'s `\\?\` verbatim prefix, and the
kernel takes a verbatim path literally, so every `/`-joined path built under
them was an invalid filename. Strip it the way Node's `realpathSync` does.
The manifest, artifact and sibling-binary expectations hard-coded POSIX
separators for paths the product joins with the host's semantics; derive them
from `jsp::join` instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: hook tests pass on Windows
Same verbatim-prefix strip on the test temp roots, plus expectations derived
from the helpers the product uses: cache keys and scan targets from
`jsp::join`, the config path in an admin message from the same relative form
`path.relative` renders, and the footer hints from `quote_command_arg`, which
deliberately switches to the double-quoted Windows form (#476 / #533). The
env lock no longer poisons the sibling tests when one of them fails.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: html oracle goldens compare on Windows
The goldens pin the `<REPO>`-masked fixture path recorded on POSIX. Mask, then
render the remainder with `/` so a Windows checkout's backslashes are not read
as a finding difference. The goldens are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: widen the live read-deadline test's margin
Timing only. The watchdog polls in 50ms steps against a ~15.6ms Windows system
timer while the crate's tests run in parallel, so the later request takes its
turn later there. The bound stays far under the 60s read timeout a
deadline-less read would hold the ticket for, so the test still distinguishes
the fix from the regression.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: the request read deadline was not enforced on Windows
Windows does not unblock a `recv` already parked in the kernel when another
thread calls `shutdown` on the same socket, so the watchdog could not end a
silent connection's read and it held its turnstile place for the whole 60s
header timeout instead of the 10s deadline. Bound the read at the socket too,
which enforces the same deadline everywhere; the watchdog stays as the backstop
for a connection that trickles bytes without ever completing a request. POSIX
behavior is unchanged: the watchdog already closed the socket at the deadline.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: hook tests derive the rest of the host path forms
The test temp helper's `write` returned a `PathBuf::join` result, which keeps
the `/` inside the relative part and so does not match what the hook resolves a
relative target to on Windows. Three more admin messages and the cache-root slug
pinned the POSIX spelling of paths the product renders with the host's
semantics (`path.resolve` also prefixes the current drive there).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: skills test fixtures name USERPROFILE, and the win32 quoted form
`os.homedir()` reads USERPROFILE on Windows, so a fixture home that named only
HOME sent the global installs into the runner's real profile. The Windows hook
command carries the JSON-quoted path, so a host path's backslashes arrive
escaped; derive the expectation instead of pinning the POSIX spelling.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: check the oracle fixtures out with LF
A finding's snippet carries the scanned file's own bytes, and the goldens were
recorded from a POSIX checkout, so a CRLF checkout of a linked stylesheet reads
as a finding difference. The goldens are untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* windows: check the grok global-install manifests as JSON
The Windows hook command carries the JSON-quoted launcher path, so the path's
backslashes are escaped once inside the command and again by the manifest file
itself. Read the manifest as JSON and look for either quoting form instead of
counting escaping layers in a raw substring match.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* npm shim: refuse a download with no verifiable sidecar
The skill launcher and `impeccable install` both fail closed when a
release binary's `.sha256` sidecar cannot be fetched or carries no hash:
they refuse rather than cache an unverified binary. The npm shim did not.
It only compared when a hash was present, so a 404, an empty sidecar, or
a truncated one all wrote the payload straight into
`~/.impeccable/bin/<version>/` and exec'd it.
It now refuses in the same cases, with wording that matches the launcher,
and writes nothing until the hash matches, so a refusal leaves the cache
dir empty. IMPECCABLE_BIN and the optional-dependency lookup are
untouched: neither downloads.
tests/cli-shim.test.mjs runs the real shim against a throwaway HTTP
server and covers missing, empty, and mismatched sidecars, plus the
matching-sidecar and IMPECCABLE_BIN paths. The two refusal cases fail
against the old shim.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Oracle fixture: declare the vite plugin the web workspace imports
`live-workspaces/apps/web/vite.config.js` imports `@vitejs/plugin-react`
but the workspace's package.json listed only `vite`. No oracle case
installs or evaluates that config (the three `live-boot-workspaces-*`
cases stop at root resolution), so the fixture was never wrong at
runtime, only self-contradictory to read. Adding the devDependency keeps
the goldens byte-equal.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Vectors: drop the 12,208 byte-identical repeat lines
The recorder deduplicated by arguments per run, not across runs, so the
frozen call snapshot arrived with 12,208 lines (43% of 28,266) that
repeat an earlier line byte for byte. Every one re-asserts what its first
occurrence already asserts, and `crates/core/tests/vectors.rs` replays
line by line with no count anywhere, so removing them changes nothing it
checks: the replay still reports 8,321 pass, 0 fail.
Duplicates were removed with `awk '!seen[$0]++'`, keeping first
occurrences and file order, and every changed file was checked to equal
that transform of its old contents. No line was added, reordered, or
rewritten, and no vector file gained or lost a distinct call. The tree
drops from 9.2 MB to 5.7 MB.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Fix: restore the live overlay's disabledValues waivers in the engine
The JS engine applied value-level ignore waivers at the tail of
collectBrowserFindings: `_disabledValues` read the entries the live
overlay resolved for the page (skill/scripts/live-browser-ignores.js
sends them as config.disabledValues), and filtered the assembled
findings by the value each one reported, with design-system-color
compared by color value rather than by spelling so a hex waiver
suppressed a finding the browser reported as rgb(...). The Rust port
dropped that stage: `disabledValues` appeared nowhere in the workspace
or in browser-bundle, so a project entry like
[detector]
ignoreValues = [{ rule = "overused-font", value = "geist mono" }]
stopped reaching the overlay. The rules the CLI and the edit hook waive
kept drawing markers and counting toward the badge.
Restore it end to end:
* BrowserConfig gains `disabled_values`, parsed leniently so a
hand-edited __IMPECCABLE_CONFIG__ entry of the wrong shape is dropped
rather than failing the whole config, the way the JS filter did.
* The driver applies the waivers after every pass, so a rule pack's
findings are covered the same way the built-in ones are, honoring the
entries only in extension mode exactly as the JS read them. The
normalizer, the value extractor (including the rule that bounce-easing
without a direct ignoreValue offers no value) and the hex/rgb color
key are ported alongside it.
* collectConfigJson in the in-page bundle and configJson in the
offscreen bundle forward the field. The extension never sends it, so
its behavior is unchanged.
Coverage: two driver unit tests (suppression by font value, by hex
waiver across the rgb spelling, and the extension-mode gate; plus the
config parse and the normalizers), a skipScan test that pins the empty
shape for every stage the core produces, and
crates/wasm/tools/disabled-values-check.mjs, a browser-backed check
ported from the retired tests/detect-antipatterns-browser.test.mjs case
that the swap left without a replacement. Against the previous bundle it
fails on exactly the three waiver assertions and passes the skipScan
one, which is the shape of the regression.
Two related review findings were checked and are not defects. skipScan
is gated on extension mode in both the driver and the bundle, which is
what the JS did (index.mjs#skipScanActive), and the live overlay runs in
extension mode: live-browser.js sets `s.dataset.impeccableExtension` on
the injected /detect.js tag, and the overlay's whole detect toggle
travels over the postMessage loop that 50-scan.js installs only under
EXTENSION_MODE. The visual contrast stage is not leaking either:
collectBrowserFindingsAsync and scan() both consult skipScanActive(),
and the offscreen path skips its visual pass on config.skipScan.
The tracked live asset is regenerated (cargo xtask bundle). The oracle
replays with zero unreviewed differences: the new field defaults empty
and the filter is inert without it, and no CLI path sets extension mode.
AI-assisted change: implemented with Claude Code under maintainer
direction.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Shim test: run from a staged copy and prove the download happened
The three fail-closed cases cleared IMPECCABLE_BIN and pointed
IMPECCABLE_HOME at a temp dir, but locate() prefers an installed
@impeccable/cli-<os>-<arch> before the cache or a download. Those
platform packages ship with every engine release and are a merge
prerequisite, so as soon as one is installed under the repo the cases
would resolve it and go green without fetching anything. Confirmed by
hand: with a platform package staged in node_modules, running the shim
against an unreachable download base still exits 0 from the package.
The shim now runs from a throwaway copy at <tmp>/cli/bin/cli.js beside a
copy of the repo's package.json, with no node_modules on the lookup path
above it, so require.resolve of the platform package fails the way it
does on a machine without the optional dependency. Production code is
unchanged; there is no test-only branch in the shim.
The fixture server also records every request now, and each download case
asserts the asset and sidecar URLs were actually requested, so a future
lookup shortcut fails loudly instead of passing on an untested path. A
sixth case installs a fake platform package next to the staged shim and
asserts the shim prefers it with the server untouched, which pins the
precedence the other cases depend on being absent.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Live: the loader now hands off when the resume is the arrival
The overlay could sit in its generating shader over a DOM that already
held all three variants, and only a page refresh cleared it (#719).
The server's generation preflight runs live-wrap with
--defer-source-write, so the wrapper and every variant reach the DOM in a
single HMR batch. The deferred-wrapper scout is constructed at init and
the variant MutationObserver at Go; observer callbacks run in
construction order, so on that batch the scout resumes first and
resumeSession, not the observer, is the transition into CYCLING. It set
the state and the bar but never called hideShaderOverlay(), so the frozen
capture of the original stayed painted over the variants. It also
reported browser_resumed, which does not count as publication progress,
and then disconnected and re-created the observer, dropping the records
that observer had already queued for the same batch, so variants_ready
never fired at all.
resumeSession now finishes the same transition the observer does (shader
down, inline edit off, insert session finalized, params panel rebuilt)
and reports variants_ready when it already holds every variant. The
deferred scout names itself in the journal as
browser_resumed_deferred_wrapper, so the two resume paths are no longer
indistinguishable.
Wrapper resolution goes through findVariantsWrapper, which prefers a
wrapper that actually holds non-original variants. A target inside a
.map() renders one wrapper per item, and an agent that relocates the
wrapper out of the shared primitive live-wrap scaffolded leaves an empty
one behind; first match could pin either and strand the session at 0/N.
With zero or one match this is the querySelector it replaces.
Tests: waitForCycling now asserts the generating shader is gone once the
bar cycles, across every runtime fixture (it failed on vite8-react-plain
before this change and passes after), marked no-retry so the reload
recovery cannot hide it. Source-shape tests pin the transition, the
variants_ready report, and the wrapper preference.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Live server: stop ends the process, SSE skips the mutation lane
Two Rust-only regressions found while investigating #719, both of which
can leave a tab waiting on a broadcast that never comes.
/stop ran shutdown() but never set shutting_down, and the accept loop
only breaks on that flag or a signal, so a stopped server kept its port
and kept answering while its server.json was already deleted. The next
`impeccable live` then booted a second server on another port and a tab
could reattach to the zombie. Node's shutdown() ended in process.exit(0).
The flag is now set after the response is written, so `stop` still reads
"stopping" instead of a reset connection, and the accept loop (already
non-blocking) exits on its next pass.
GET /events took a turnstile ticket and waited its turn before
registering, even though handle_sse releases that ticket two statements
later and needs no arrival ordering. A peer that stalls mid-request holds
the lane for the whole READ_REQUEST_DEADLINE, so a reconnecting stream
could sit unregistered for up to 10 seconds (measured 9.71s against 0.00s
on Node); broadcast is fire-and-forget, so a `done` landing in that
window reaches an empty client set and is gone. Registering early can
only make a stream see more broadcasts. The one cost is that the
connected frame's activeSessions snapshot may miss a mutation still in
flight, and the browser treats that snapshot as a hint. Preflights still
take a turn: answering those out of order reorders the POSTs the browser
issues behind them.
The route classification moved into releases_ticket_up_front so it can be
unit tested. tests/live-server-leak.test.mjs gains a guard that a stopped
server's pid is gone and its port is free.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Live: the shader teardown can no longer race its own construction
The new cycling assertion caught a real defect on CI: vite8-react-insert
reached CYCLING with #impeccable-live-shader still painted over the page.
showShaderOverlay is async. It appends its canvas synchronously, then
awaits createImageBitmap and finishes the GL setup before it publishes
shaderState. hideShaderOverlay returned early on a null shaderState, so a
teardown that landed inside that window did nothing, and the construction
then published itself over a session that had already left GENERATING,
with no teardown left to run. The scroll tick kept repositioning it,
which is why the CI page.html shows the canvas sized from the capture
rect but styled to the cycling anchor.
Every teardown now bumps a shader epoch before it does anything else, and
a construction pins the epoch it owns and abandons its canvas (releasing
the GL context) at every point past an await and before any publish,
including both bitmap-fallback publishes. A teardown also drops a shader
node that no shaderState owns, so an already-orphaned canvas cannot
survive one.
Reproduced by widening the append-to-publish window: with a 400ms delay
after uiAppend, vite8-react-insert failed with the CI error and the probe
showed the teardown arriving at CYCLING with shaderState still null.
The same run passes with this change, as does a 1500ms window on insert
and plain. Locally that window is about 4ms, which is why it only showed
on a slower runner.
The four remaining setLiveState('CYCLING') sites that did not lower the
loader now do: the SSE done handler (the one route that can reach CYCLING
from GENERATING), the Svelte republish remount, and the two accept
failure recoveries.
The e2e assertion already waits up to 5s for the shader to clear, so it
was never racing a legitimate teardown; it is left as it is.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Live: every active-session wrapper lookup goes through the resolver
Cursor Bugbot on #720: findVariantsWrapper alone was not enough.
resolveBarAnchor, the visible-variant element, mountedParameterCount,
readVisibleVariantFromDOM, showVariantInDOM, the source injection, and
the whole accept path still took the first [data-impeccable-variants]
match, so in the relocated-wrapper case Tune never bound and the bar kept
anchoring to the empty scaffold even after the resume reached CYCLING.
Thirteen call sites now resolve through findVariantsWrapper. The resolver
split in two so a missing id cannot silently widen the lookup to any
session: findVariantsWrapper(sessionId) returns null without an id, and
findAnyVariantsWrapper() is the entry point for the two resume paths that
have no id yet. Both share pickPopulatedVariantsWrapper, which is the old
querySelector whenever there are fewer than two matches.
Discard cleanup now hides every duplicate wrapper rather than the first,
since a target inside a `.map()` renders one per item and hiding one left
the rest of the discarded variants on screen.
What still takes a raw first match is deliberate: bare existence checks,
selector strings for stylesheets and observers (which want to cover every
match), querySelectorAll sweeps, the parsed source document, and the
Svelte component wrapper, which holds no variant children at all. The
source-shape test pins that exact set by name, so a new raw lookup fails
until it is either routed through the resolver or justified there.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Live: a discard releases every wrapper it hid
Bugbot on #720: the non-restoreOriginal discard now hides every matching
wrapper, but the delayed fallback still released only the first
querySelector hit. A target inside a `.map()` renders one wrapper per
item, so the rest stayed at display:none and their original content never
came back on the static and missed-HMR flows that fallback exists for.
The hide, the existence checks, and the release now all speak about the
same set. discardedWrappers(sessionId) is the one place that collects it;
releaseDiscardedStaticWrappers takes the stylesheet down once and
releases each wrapper; releaseDiscardedStaticWrapper drops its sessionId
argument and just unwinds the node it is given. The HMR-ownership
decision still reads the first wrapper, which is fair: duplicates all
render from one source element, so ownership is uniform across them. The
reload branch is unchanged because a reload restores every original at
once.
Covered by a source-shape test rather than an e2e scenario:
hasFrameworkHmrOwnership is true for every React, Vue, and Svelte runtime
fixture, so all of them take the watcher path and none can reach the
static release. The existing framework-ownership guards in the same file
move to the new shape and keep their intent, including the one that says
only non-discard cleanup may blank the wrapper while waiting for HMR.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Release: publish the npm platform packages in one command
bun run release:platform-packages downloads each engine-v<ENGINE_VERSION>
binary with its .sha256 sidecar (required; nothing unverified is
published), stages the package from cli/platform-packages/<target> with
the version stamped, the executable at bin/ and the repo LICENSE, and
runs npm publish --access public. Targets already on the registry are
skipped so a re-run resumes after a partial failure. Preconditions:
package.json pins equal ENGINE_VERSION and npm is logged in.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* release-engine: pin checkout, upload-artifact and download-artifact at v7
The v4 pins target Node 20, which the runner now deprecates and forces
onto Node 24 with a warning on every step. The rest of the workflows
already use v7.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Tests: make the temp-dir helpers unique under a coarse clock
Windows' system clock is coarse enough that two parallel tests could get
the same pid-plus-nanoseconds directory name and then remove each
other's files (rust-windows: close_verb_round_trip_and_ownership,
NotFound). A per-process counter is appended to the name.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
* Tests: declare the temp-dir counter in the hook cache-root tests
The previous commit referenced TMP_SEQ there without defining it.
Co-Authored-By: Claude Code <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vau2X53xGTjjTCXWMVBoNY
---------
Co-authored-by: Claude <noreply@anthropic.com>
1522 lines
227 KiB
JSON
1522 lines
227 KiB
JSON
{"args":["\n\n\n"],"result":false}
|
||
{"args":["\n\n\n\n"],"result":false}
|
||
{"args":[""],"result":false}
|
||
{"args":["Blinking cursor fixture"],"result":false}
|
||
{"args":["\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: monospace, monospace; font-size: 16px; }\n .hero { padding: 40px; }\n .terminal { background: #111; color: #eee; padding: 24px; width: 600px; }\n\n @keyframes blink-anim { 50% { opacity: 0; } }\n @keyframes flicker { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }\n @keyframes caret-blink { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* FLAG: solid block cursor in the hero terminal */\n .flag-block-cursor {\n display: inline-block; width: 10px; height: 18px;\n background: #33d17a; vertical-align: text-bottom;\n animation: blink-anim 1.1s steps(1) infinite;\n }\n\n /* FLAG: glyph cursor, animation name says nothing but keyframes toggle opacity */\n .flag-glyph-cursor { animation: flicker 1s steps(2) infinite; }\n\n /* FLAG: underscore bar cursor */\n .flag-underscore-cursor {\n display: inline-block; width: 12px; height: 3px;\n background: #eee; vertical-align: baseline;\n animation: caret-blink 0.9s steps(1) infinite;\n }\n\n /* PASS: same block cursor shape, but deep below the landing region */\n .pass-below-fold-cursor {\n display: inline-block; width: 10px; height: 18px;\n background: #33d17a;\n animation: blink-anim 1.1s steps(1) infinite;\n }\n\n /* PASS: cursor inside real editable surfaces */\n .pass-editable-cursor, .pass-textbox-cursor {\n display: inline-block; width: 10px; height: 18px;\n background: #444;\n animation: blink-anim 1s steps(1) infinite;\n }\n\n /* PASS: round pulsing dot stays with the pulsing-dot rule */\n .pass-round-dot {\n display: inline-block; width: 8px; height: 8px;\n background: #33d17a; border-radius: 50%;\n animation: blink-anim 2s steps(1) infinite;\n }\n\n /* PASS: spinner animates transform, not visibility */\n .pass-spinner {\n display: inline-block; width: 16px; height: 16px;\n border: 2px solid #999; border-top-color: #333; border-radius: 50%;\n animation: spin 1s linear infinite;\n }\n\n /* PASS: a blinking word is not a cursor shape */\n .pass-blinking-word { animation: flicker 1s steps(2) infinite; }\n\n /* PASS: large blinking panel fails the caret-size gates */\n .pass-blinking-panel {\n display: inline-block; width: 200px; height: 90px;\n background: #222;\n animation: blink-anim 2s steps(1) infinite;\n }\n\n .spacer { height: 1400px; }\n"],"result":false}
|
||
{"args":["\n\n \n\n \n\n \n\n"],"result":false}
|
||
{"args":["\n \n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Terminal-flavored landing hero"],"result":false}
|
||
{"args":["\n \n \n \n "],"result":false}
|
||
{"args":["user@host:~$ run demo "],"result":false}
|
||
{"args":["user@host:~$ tail -f logs "],"result":false}
|
||
{"args":["▌"],"result":false}
|
||
{"args":["user@host:~$ next up "],"result":false}
|
||
{"args":["Status \n loading \n and a badge word\n plus a big panel "],"result":false}
|
||
{"args":["LIVE"],"result":false}
|
||
{"args":["\n \n "],"result":false}
|
||
{"args":["editable prompt "],"result":false}
|
||
{"args":["textbox prompt "],"result":false}
|
||
{"args":["\n \n "],"result":false}
|
||
{"args":["deep footer terminal "],"result":false}
|
||
{"args":["\n \n \n \n \n"],"result":false}
|
||
{"args":["Body text viewport edge — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* Fixture for the body-text-viewport-edge rule. The rule fires\n when a <p> or <li> with substantial text content has rect.left\n within 16px of the viewport edge (or rect.right within 16px\n of viewport.width) AND the element has no own background-color\n AND is not inside <nav>/<header> AND is in normal flow. */\n body { font: 14px/1.5 system-ui, sans-serif; margin: 0; color: #111; background: #fff; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; padding: 24px; max-width: 1400px; margin: 0 auto; }\n .col h2 { font: 600 14px/1 system-ui; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .case-label { display: block; font-size: 12px; color: #64748b; margin: 16px 0 4px; font-style: italic; }\n .container-good { max-width: 720px; margin: 0 auto; padding: 16px 32px; }\n .container-tight { padding: 16px 32px; }\n .full-bleed-section { background: #fef3c7; padding: 24px 16px; }\n nav.site-nav { padding: 12px 24px; background: #f5f5f5; }\n header.banner { padding: 12px 24px; background: #e5e7eb; }\n\n /* Override the grid container's own padding so we can demonstrate\n a paragraph that genuinely bleeds to the viewport edge inside\n the flag column — the col itself shouldn't reset its padding. */\n .flag-fullwidth-p { /* no container; placed directly in body via .escape */ }\n .escape { position: relative; left: 50%; right: 50%; margin-left: -50vw; margin-right: -50vw; width: 100vw; padding: 0; }\n .edge-list { margin: 0; padding: 0; list-style-position: inside; }\n .edge-list li { margin: 0; }\n "],"result":false}
|
||
{"args":["\n\n \n \n \n \n\n \n \n\n \n"],"result":false}
|
||
{"args":["Lucia opened this place in 1978. She cooked every night for fifty years, making the same dishes: spaghetti carbonara, eggplant parmigiana, veal saltimbocca, fresh lasagna. She did not change a single recipe in all that time. Her grandson Anthony runs the kitchen now, using the same recipes with the same care."],"result":false}
|
||
{"args":["This second flush-to-edge paragraph confirms the rule fires on every body paragraph that touches the viewport, not just one. The rendered width should be the full viewport, with text starting essentially at x=0."],"result":false}
|
||
{"args":["This is a list item whose text content is substantially long enough to qualify as body content. It also runs flush against the viewport edge with no left padding."],"result":false}
|
||
{"args":["\n \n\n \n "],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["Should pass"],"result":false}
|
||
{"args":["paragraph in centered container with 32px padding"],"result":false}
|
||
{"args":["\n \n "],"result":false}
|
||
{"args":["This paragraph sits inside a max-width container that's centered and has 32px horizontal padding. It does not touch the viewport edges and should not trigger the rule."],"result":false}
|
||
{"args":["paragraph inside <nav> (excluded — nav can bleed)"],"result":false}
|
||
{"args":["This nav legitimately bleeds to the edges as a full-width header strip; the rule excludes elements inside <nav> or <header> so this does not flag."],"result":false}
|
||
{"args":["paragraph inside <header> (excluded — header can bleed)"],"result":false}
|
||
{"args":["Banner headers commonly bleed to viewport edges with their own internal layout. The rule excludes <header> descendants."],"result":false}
|
||
{"args":["paragraph inside full-bleed section with own background"],"result":false}
|
||
{"args":["This paragraph sits in a full-bleed section that has its own background-color set (intentional design move). The rule excludes paragraphs whose element has its own background-color."],"result":false}
|
||
{"args":["short label / button-ish text (excluded — < 40 chars)"],"result":false}
|
||
{"args":["Short label."],"result":false}
|
||
{"args":["\n \n \n "],"result":false}
|
||
{"args":["Notes"],"result":false}
|
||
{"args":["The flag cases above (rendered outside this grid) demonstrate the failure. The pass cases here show acceptable patterns. The rule gates aggressively to avoid false positives: it only flags <p> and <li> with >40 chars whose width spans more than 50% of the viewport AND whose rect.left or rect.right is within 16px of a viewport edge AND which are not inside nav/header AND which have no own background-color AND which are in normal flow (not position:fixed/absolute)."],"result":false}
|
||
{"args":["Border Anti-Patterns — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n body {\n margin: 0;\n padding: 24px;\n background: #f8fafc;\n color: #111827;\n font: 14px/1.5 system-ui, sans-serif;\n }\n\n h1 {\n max-width: 1180px;\n margin: 0 auto 8px;\n font-size: 34px;\n line-height: 1.05;\n font-weight: 720;\n }\n\n .intro {\n max-width: 1180px;\n margin: 0 auto 24px;\n color: #4b5563;\n }\n\n .grid {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n gap: 32px;\n max-width: 1180px;\n margin: 0 auto;\n }\n\n .col h2 {\n margin: 0 0 14px;\n color: #475569;\n font-size: 13px;\n font-weight: 760;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n\n .case {\n margin: 0 0 14px;\n padding: 16px;\n background: #fdfdfd;\n box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);\n }\n\n .case h3 {\n margin: 0 0 4px;\n font-size: 15px;\n line-height: 1.25;\n }\n\n .case p {\n margin: 0;\n color: #475569;\n }\n\n .flag-left {\n border-left: 4px solid #2563eb;\n border-radius: 10px;\n }\n\n .flag-right {\n border-right: 5px solid #dc2626;\n border-radius: 10px;\n }\n\n .flag-left-plain {\n border-left: 4px solid #0f766e;\n }\n\n .flag-top {\n border-top: 4px solid #7c3aed;\n border-radius: 10px;\n }\n\n .flag-bottom {\n border-bottom: 3px solid #ea580c;\n border-radius: 10px;\n }\n\n .flag-dark {\n background: #141820;\n color: #f4f7fb;\n border-left: 4px solid #38bdf8;\n border-radius: 10px;\n }\n\n .flag-dark p {\n color: #cbd5e1;\n }\n\n .pass-full-border {\n border: 1px solid #cbd5e1;\n border-radius: 10px;\n }\n\n .pass-neutral-side {\n border-left: 4px solid #d1d5db;\n border-radius: 10px;\n }\n\n .pass-thin-side {\n border-left: 1px solid #2563eb;\n border-radius: 10px;\n }\n\n .pass-status-alert {\n border-left: 4px solid #2563eb;\n }\n\n .flag-square-top {\n border-top: 4px solid #7c3aed;\n border-radius: 0;\n }\n\n .flag-square-bottom {\n border-bottom: 3px solid #ea580c;\n border-radius: 0;\n }\n\n .pass-tab-underline {\n border-bottom: 3px solid #2563eb;\n border-radius: 0;\n }\n\n .pass-thick-band {\n border-top: 16px solid #7c3aed;\n border-radius: 0;\n }\n\n .pass-dark-uniform {\n background: #161a22;\n color: #f4f7fb;\n border: 1px solid #334155;\n border-radius: 10px;\n }\n\n .pass-dark-uniform p {\n color: #cbd5e1;\n }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n\n \n"],"result":false}
|
||
{"args":["Border side-tab detector cases"],"result":false}
|
||
{"args":["A paired fixture for colored side borders and rounded accent borders. The left column should produce border findings; the right column should stay clean."],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Should flag"],"result":false}
|
||
{"args":["\n \n \n "],"result":false}
|
||
{"args":["Rounded card with colored left border"],"result":false}
|
||
{"args":["Classic side tab: border-left is thick, colored, and paired with rounded corners."],"result":false}
|
||
{"args":["Rounded card with colored right border"],"result":false}
|
||
{"args":["The right-side version is the same visual trope mirrored."],"result":false}
|
||
{"args":["Thick colored side border without radius"],"result":false}
|
||
{"args":["A thick chromatic side stripe still reads as a tab even without rounded corners."],"result":false}
|
||
{"args":["Rounded card with top accent border"],"result":false}
|
||
{"args":["A heavy top accent on a rounded card is the horizontal variant of the same pattern."],"result":false}
|
||
{"args":["Rounded card with bottom accent border"],"result":false}
|
||
{"args":["The bottom accent is also a decorative stripe on a card surface."],"result":false}
|
||
{"args":["Dark card with bright side stripe"],"result":false}
|
||
{"args":["Dark surfaces can hide the cliche, but the thick colored side stripe remains."],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Uniform full border"],"result":false}
|
||
{"args":["A quiet all-around border frames the card instead of creating a side tab."],"result":false}
|
||
{"args":["Neutral structural side rule"],"result":false}
|
||
{"args":["A gray structural rule can be a legitimate divider when it is not chromatic emphasis."],"result":false}
|
||
{"args":["Thin colored side rule"],"result":false}
|
||
{"args":["A 1px colored rule is below the decorative side-tab threshold."],"result":false}
|
||
{"args":["Status toast with severity accent"],"result":false}
|
||
{"args":["A live status region carries a colored side bar as a severity accent, not a decorative side tab."],"result":false}
|
||
{"args":["Square top border"],"result":false}
|
||
{"args":["A chromatic top stripe on a card is the horizontal variant of the side tab."],"result":false}
|
||
{"args":["Square bottom border"],"result":false}
|
||
{"args":["A chromatic bottom stripe on a badge or card is the same decorative tell."],"result":false}
|
||
{"args":["\n \n \n "],"result":false}
|
||
{"args":["Selected tab underline"],"result":false}
|
||
{"args":["An active-state underline inside a tablist is an affordance, not a stripe."],"result":false}
|
||
{"args":["Thick color band"],"result":false}
|
||
{"args":["A band well past stripe thickness is a compositional block, not an accent stripe."],"result":false}
|
||
{"args":["Dark card with uniform border"],"result":false}
|
||
{"args":["The dark surface has a full neutral outline, not a chromatic side stripe."],"result":false}
|
||
{"args":["\n \n \n \n"],"result":false}
|
||
{"args":["Clipped overflow container fixture"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; background: #fff; }\n .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 24px; }\n .col { padding: 16px; }\n .box { position: relative; width: 200px; height: 120px; margin: 0 0 24px; padding: 16px; border: 1px solid #ddd; }\n .pop { position: absolute; top: 100%; left: 0; width: 220px; background: #222; color: #fff; padding: 8px; }\n .tip-negative { position: absolute; top: -34px; left: 16px; width: 160px; background: #222; color: #fff; padding: 8px; }\n .flyout { position: absolute; top: 28px; left: 100%; width: 180px; background: #222; color: #fff; padding: 8px; }\n .contained-badge { position: absolute; top: 10px; right: 10px; width: 36px; height: 24px; background: #e5f0ff; }\n .button-shine { position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,.6), transparent); }\n .crop-photo { position: absolute; inset: -18px; background: linear-gradient(135deg, #b7d7ff, #486aa3); }\n .inside-overlay { position: absolute; left: 12px; right: 12px; bottom: 12px; min-height: 28px; background: rgba(255,255,255,.86); }\n .pass-hidden-button { position: relative; overflow: hidden; width: 160px; height: 52px; border: 1px solid #ddd; background: #111; color: #fff; }\n .pass-carousel-viewport,\n .pass-fisheye-list,\n .pass-split-container {\n position: relative;\n overflow: hidden;\n width: 200px;\n height: 120px;\n margin: 0 0 24px;\n border: 1px solid #ddd;\n }\n .carousel-slide-next { position: absolute; top: 100%; left: 0; width: 100%; height: 100%; padding: 16px; background: #f5f5f5; }\n .fisheye-next-item { position: absolute; top: 100%; left: 0; width: 100%; padding: 8px; background: none; border: 0; text-align: left; }\n .split-after-panel { position: absolute; top: 0; left: 50%; width: 100%; height: 100%; background: #f5f5f5; }\n "],"result":false}
|
||
{"args":["\n \n"],"result":false}
|
||
{"args":["\n \n \n\n \n \n "],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Menu"],"result":false}
|
||
{"args":["Dropdown that gets clipped by the hidden ancestor"],"result":false}
|
||
{"args":["Tooltip host"],"result":false}
|
||
{"args":["Tooltip clipped by overflow clip"],"result":false}
|
||
{"args":["Help"],"result":false}
|
||
{"args":["Tooltip clipped above the host"],"result":false}
|
||
{"args":["Actions"],"result":false}
|
||
{"args":["Flyout menu clipped to the right of the host"],"result":false}
|
||
{"args":["More"],"result":false}
|
||
{"args":["Dropdown with utility classes still gets clipped."],"result":false}
|
||
{"args":["Dialog"],"result":false}
|
||
{"args":["Overlay-style dialog content still needs to escape."],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Hidden container with only static content."],"result":false}
|
||
{"args":["Visible host"],"result":false}
|
||
{"args":["Positioned child can escape because overflow is visible."],"result":false}
|
||
{"args":["Scroll host"],"result":false}
|
||
{"args":["A genuine scroll region is allowed to contain positioned children."],"result":false}
|
||
{"args":["Positioned decoration remains inside the clipping box."],"result":false}
|
||
{"args":["\n Save\n \n "],"result":false}
|
||
{"args":["Mockup frame with a contained absolute overlay."],"result":false}
|
||
{"args":["A hidden next slide is clipped intentionally by the viewport."],"result":false}
|
||
{"args":["Next command"],"result":false}
|
||
{"args":["Before/after comparison panel clipped by the split frame."],"result":false}
|
||
{"args":["Color Anti-Patterns — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; background: #fafafa; padding: 24px; margin: 0; color: #1a1a1a; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1200px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .col h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 24px 0 8px; color: #64748b; }\n .card { padding: 14px 16px; border-radius: 10px; margin-bottom: 10px; }\n /* Styled button color cases (declared via stylesheet so jsdom resolves them\n reliably — inline color doesn't always win over user-agent rules in jsdom\n for <a> and <button>). */\n .styled-pill-low { background-color: rgb(31, 26, 21); color: rgb(91, 79, 68); display: inline-block; padding: 9px 18px; border-radius: 999px; font-weight: 500; font-size: 14px; text-decoration: none; }\n .low-contrast-button { background-color: rgb(55, 65, 81); color: rgb(108, 114, 128); display: inline-block; padding: 10px 20px; border-radius: 8px; border: 0; font-size: 14px; }\n .good-pill-high { background-color: rgb(20, 20, 25); color: rgb(245, 240, 232); display: inline-block; padding: 9px 18px; border-radius: 999px; font-weight: 500; font-size: 14px; text-decoration: none; }\n .inline-link-low { color: rgb(170, 170, 170); }\n /* Chip / badge cases: text-bearing spans painting their own background. */\n .chip-sev-low { display: inline-block; background-color: rgb(182, 50, 45); color: rgb(92, 84, 73); font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 2px; }\n .chip-ok-high { display: inline-block; background-color: rgb(20, 20, 25); color: rgb(245, 240, 232); font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 2px; }\n .chip-sub9-low { display: inline-block; background-color: rgb(182, 50, 45); color: rgb(150, 60, 55); font-size: 8px; padding: 1px 4px; }\n .panel-reset { background: rgb(28, 30, 38); color: rgb(230, 232, 237); padding: 12px; }\n .panel-reset code { background: rgb(246, 242, 244); border-radius: 3px; padding: 1px 4px; }\n .panel-reset pre code { background: none; }\n /* issue #409 Case A: gradient-clipped text. The gradient IS the glyph fill\n (text-fill-color: transparent), not a backdrop. The inherited `color`\n (#e8e6e3) is never painted, so measuring it against the element's own\n gradient stops (#6d8cff / #a78bfa) is a false positive. */\n .ox-grad-text { background: linear-gradient(135deg, #6d8cff 0%, #a78bfa 50%, #6d8cff 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; color: #e8e6e3; font-size: 40px; font-weight: 800; }\n /* issue #409 Case B: a 9%-alpha accent glow stop over a dark surface. The\n stop composites to ~#121f1f, not the full-opacity #34c0a8, so text stays\n high-contrast. The dark wrapper supplies the surface beneath the glow. */\n .ox-dark-wrap { background: #0f0f11; padding: 16px; }\n .ox-glow { background: linear-gradient(160deg, rgba(52,192,168,0.09) 0%, #141419 65%); padding: 20px; }\n .ox-glow p { color: #e8e6e3; font-size: 18px; }\n .ox-glow .muted { color: #8e8c89; font-size: 16px; }\n /* Gradient stop written as color-mix with a bright nested ingredient.\n The stop paints as a 16% wash of the light oklch color (composited\n over the dark wrap it is near-black); the nested oklch(90% ...) is an\n INGREDIENT, never painted. A parser that re-extracts nested tokens\n appends it as a phantom opaque light stop and the worst-case ratio\n then flags the light text at ~1:1 against a color nobody sees. */\n .mix-dark-wrap { background: #0f0f11; padding: 16px; }\n .mix-glow { background: linear-gradient(160deg, color-mix(in oklab, oklch(90% 0.02 95) 16%, transparent) 0%, #141419 65%); padding: 20px; }\n .mix-glow p { color: #ded9cf; font-size: 16px; }\n /* currentcolor surface: background-color paints with the element's own\n text color, which is itself a var() token here. jsdom hands both\n through verbatim, so the walk must resolve the token via the\n custom-prop map instead of abstaining on a knowable surface. */\n :root { --fixture-bone: #e8e2d6; }\n .currentcolor-surface { background-color: currentcolor; color: var(--fixture-bone); padding: 14px 16px; border-radius: 10px; margin-bottom: 10px; }\n .currentcolor-low-text { color: #cfc9bd; font-size: 14px; }\n .currentcolor-good-text { color: #3a352c; font-size: 14px; }\n /* Same shape but the token does not exist: the surface truly cannot be\n read, so the walk must abstain rather than guess. */\n .currentcolor-unknown { background-color: currentcolor; color: var(--fixture-undefined-token); padding: 14px 16px; border-radius: 10px; }\n .currentcolor-unknown p { color: #efe9dd; font-size: 14px; }\n "],"result":false}
|
||
{"args":["\n \n \n"],"result":false}
|
||
{"args":["\n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n \n\n \n \n \n \n \n\n \n \n\n \n \n\n \n \n \n \n \n\n \n \n \n\n \n \n \n \n \n\n \n \n \n "],"result":false}
|
||
{"args":["Pure black surfaces"],"result":false}
|
||
{"args":["Pure #000 background"],"result":false}
|
||
{"args":["Gray on color"],"result":false}
|
||
{"args":["Gray text on blue background"],"result":false}
|
||
{"args":["Gray text on green background"],"result":false}
|
||
{"args":["Low contrast"],"result":false}
|
||
{"args":["Light gray text on white — very low contrast"],"result":false}
|
||
{"args":["Dark gray text on near-black — low contrast"],"result":false}
|
||
{"args":["Welcome to Our Platform"],"result":false}
|
||
{"args":["The best solution for modern teams and businesses"],"result":false}
|
||
{"args":["Gradient text"],"result":false}
|
||
{"args":["\n Gradient Heading\n "],"result":false}
|
||
{"args":["AI color palette"],"result":false}
|
||
{"args":["Purple heading text"],"result":false}
|
||
{"args":["Styled <a> and <button> with low contrast"],"result":false}
|
||
{"args":["Get started"],"result":false}
|
||
{"args":["Submit"],"result":false}
|
||
{"args":["Text-bearing chip with low contrast"],"result":false}
|
||
{"args":["SEV-2"],"result":false}
|
||
{"args":["Tailwind color anti-patterns"],"result":false}
|
||
{"args":["bg-black — pure black bg"],"result":false}
|
||
{"args":["text-gray-400 on bg-blue-500 — gray on color"],"result":false}
|
||
{"args":["text-purple-500 heading"],"result":false}
|
||
{"args":["Purple-to-indigo gradient"],"result":false}
|
||
{"args":["currentcolor surface via var() token"],"result":false}
|
||
{"args":["Faint warm gray on a bone currentcolor surface"],"result":false}
|
||
{"args":["\n \n\n \n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n\n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n\n \n \n\n \n \n \n\n \n \n \n \n "],"result":false}
|
||
{"args":["Tinted neutrals"],"result":false}
|
||
{"args":["Near-black bg, near-white text — tinted, not pure"],"result":false}
|
||
{"args":["Near-white bg, near-black text — good contrast, tinted"],"result":false}
|
||
{"args":["Pure white surface is acceptable"],"result":false}
|
||
{"args":["Pure #fff background with dark text — acceptable contrast"],"result":false}
|
||
{"args":["Good contrast"],"result":false}
|
||
{"args":["Near-white text on dark blue — high contrast, not pure white"],"result":false}
|
||
{"args":["Dark green text on green bg — same hue family"],"result":false}
|
||
{"args":["Distinctive accents (not AI purple)"],"result":false}
|
||
{"args":["Red heading — not AI purple"],"result":false}
|
||
{"args":["Amber heading — distinctive"],"result":false}
|
||
{"args":["Background-image url() ancestor (not low contrast)"],"result":false}
|
||
{"args":["White text on image background — should not flag low-contrast"],"result":false}
|
||
{"args":["Tailwind opacity-modified bg-black (not pure black)"],"result":false}
|
||
{"args":["bg-black/3 — 3% opacity, not pure black"],"result":false}
|
||
{"args":["hover:bg-black/5 — hover state, not pure black"],"result":false}
|
||
{"args":["bg-black/50 — 50% opacity, not pure black"],"result":false}
|
||
{"args":["Inline <a> without own background (must remain skipped)"],"result":false}
|
||
{"args":["Read more about this if you want to learn more about the topic."],"result":false}
|
||
{"args":["here"],"result":false}
|
||
{"args":["Styled <a> with good contrast (must not flag)"],"result":false}
|
||
{"args":["High contrast pill"],"result":false}
|
||
{"args":["Chip with good contrast (must not flag)"],"result":false}
|
||
{"args":["HEALTHY"],"result":false}
|
||
{"args":["Sub-9px decorative chip (below the font floor, must not flag)"],"result":false}
|
||
{"args":["tick"],"result":false}
|
||
{"args":["background: none resets an earlier background-color"],"result":false}
|
||
{"args":["light text over the dark panel, not the light code surface"],"result":false}
|
||
{"args":["Gradient-clipped text (issue #409 Case A — must not flag contrast)"],"result":false}
|
||
{"args":["Gradient Clipped Heading Text"],"result":false}
|
||
{"args":["Faint accent-glow gradient (issue #409 Case B — must not flag contrast)"],"result":false}
|
||
{"args":["Light body copy on a faint accent glow that composites to near-black"],"result":false}
|
||
{"args":["Muted secondary line on the same faint glow area here"],"result":false}
|
||
{"args":["color-mix gradient stop: nested ingredient is not a surface"],"result":false}
|
||
{"args":["\n \n "],"result":false}
|
||
{"args":["Light copy on a faint mixed wash over a dark ground stays readable"],"result":false}
|
||
{"args":["currentcolor surface with good contrast"],"result":false}
|
||
{"args":["Dark ink text on a bone currentcolor surface"],"result":false}
|
||
{"args":["currentcolor surface with unresolvable token (must abstain)"],"result":false}
|
||
{"args":["Light text on an unknowable currentcolor surface"],"result":false}
|
||
{"args":["Emoji on light backgrounds"],"result":false}
|
||
{"args":["⚠️ 🚨 ✨ 🎨"],"result":true}
|
||
{"args":["Cramped Padding — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* Comprehensive fixture for the cramped-padding rule.\n Both columns include a wide variety of cases so we can verify the\n rule scales sensibly with font-size and treats the two axes\n independently (vertical can be tighter because line-height already\n provides built-in breathing room; horizontal has no equivalent). */\n body { font-family: system-ui, sans-serif; background: #fafafa; padding: 24px; margin: 0; color: #0f172a; line-height: 1.5; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1400px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .col h3 { font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; margin: 28px 0 10px; color: #64748b; }\n .case { margin-bottom: 14px; padding: 14px 16px; background: white; border: 1px solid #e2e8f0; border-radius: 10px; }\n .case-label { display: block; font-size: 12px; color: #64748b; margin-bottom: 8px; font-style: italic; }\n\n /* ── FLAG: cramped body in cards ── */\n .flag-card-4 { padding: 4px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; }\n .flag-card-2 { padding: 2px; background: #f1f5f9; border-radius: 6px; font-size: 14px; }\n .flag-card-bg4 { padding: 4px; background: #fef3c7; border-radius: 6px; font-size: 16px; }\n\n /* ── FLAG: asymmetric cramping ── */\n .flag-asym-v { padding: 1px 16px; background: #f1f5f9; border-radius: 6px; font-size: 14px; }\n .flag-asym-h { padding: 12px 4px; background: #f1f5f9; border-radius: 6px; font-size: 14px; }\n\n /* ── FLAG: cramped large text (currently MISSED by the 8px floor) ── */\n .flag-heading-tight { padding: 8px; background: #f1f5f9; border-radius: 6px; font-size: 24px; font-weight: 600; line-height: 1.2; }\n .flag-hero-tight { padding: 6px 16px; background: #f1f5f9; border-radius: 6px; font-size: 32px; font-weight: 700; line-height: 1.2; }\n\n /* ── FLAG: cramped multi-line code block ── */\n .flag-pre-tight { padding: 2px; background: #1e293b; color: #e2e8f0; border-radius: 6px; font-family: ui-monospace, monospace; font-size: 14px; line-height: 1.5; }\n\n /* ── PASS: small inline pills (the .detection-cmd category) ── */\n .pass-cmd {\n display: inline-block;\n font-family: ui-monospace, SFMono-Regular, monospace;\n font-size: 13px;\n line-height: 1.625;\n color: #0f172a;\n background: #f5f5f7;\n padding: 6px 14px;\n border-radius: 6px;\n white-space: nowrap;\n }\n .pass-cmd::before { content: '➜ '; color: #ec4899; }\n .pass-badge {\n display: inline-block;\n font-size: 11px;\n line-height: 1.6;\n padding: 4px 8px;\n background: #dbeafe;\n color: #1e40af;\n border-radius: 999px;\n font-weight: 600;\n }\n .pass-chip {\n display: inline-block;\n font-size: 12px;\n line-height: 1.6;\n padding: 4px 10px;\n background: #fef3c7;\n color: #92400e;\n border-radius: 4px;\n font-weight: 500;\n }\n .pass-inline-code-chip {\n display: inline-block;\n font-family: ui-monospace, SFMono-Regular, monospace;\n font-size: 12px;\n line-height: 2;\n padding: 0.3em 0.5em;\n background: #eef2f7;\n color: #0f172a;\n border-radius: 4px;\n }\n\n /* ── PASS: cards at current standards ── */\n .pass-card-min { padding: 8px; background: #f1f5f9; border-radius: 6px; font-size: 16px; line-height: 1.6; }\n .pass-card-good { padding: 16px; background: #f1f5f9; border-radius: 8px; font-size: 14px; line-height: 1.6; }\n .pass-card-large { padding: 12px; background: #f1f5f9; border-radius: 8px; font-size: 18px; line-height: 1.6; }\n\n /* ── PASS: comfortable code blocks ── */\n .pass-pre-min { padding: 8px; background: #1e293b; color: #e2e8f0; border-radius: 6px; font-family: ui-monospace, monospace; font-size: 14px; line-height: 1.5; }\n .pass-pre-good { padding: 12px; background: #1e293b; color: #e2e8f0; border-radius: 6px; font-family: ui-monospace, monospace; font-size: 14px; line-height: 1.5; }\n\n /* ── PASS: interactive elements ── */\n .pass-button-std { padding: 8px 16px; background: #3b82f6; color: white; border: 0; border-radius: 6px; font-size: 14px; cursor: pointer; }\n .pass-input-std { padding: 8px 12px; border: 1px solid #cbd5e1; border-radius: 4px; font-size: 16px; width: 100%; box-sizing: border-box; }\n\n /* ── PASS: big text with proportional padding ── */\n .pass-heading-good { padding: 16px; background: #f1f5f9; border-radius: 8px; font-size: 24px; font-weight: 600; line-height: 1.2; }\n .pass-hero-good { padding: 20px 32px; background: #f1f5f9; border-radius: 8px; font-size: 32px; font-weight: 700; line-height: 1.2; }\n "],"result":false}
|
||
{"args":["\n \n\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n "],"result":false}
|
||
{"args":["Cramped body in cards"],"result":false}
|
||
{"args":["14px body, 4px padding all sides"],"result":false}
|
||
{"args":["This text is crammed against the border with only 4px padding. It feels claustrophobic and hard to read because the eye has nowhere to rest."],"result":false}
|
||
{"args":["14px body, 2px padding all sides"],"result":false}
|
||
{"args":["Two pixels of padding is essentially nothing — body text needs room to breathe inside its container, especially across multiple lines."],"result":false}
|
||
{"args":["16px body, 4px padding, background only"],"result":false}
|
||
{"args":["A colored background containment with only 4px padding around 16px body text. The colored edge feels like it's pressing in."],"result":false}
|
||
{"args":["Asymmetric cramping"],"result":false}
|
||
{"args":["1px vertical / 16px horizontal, 14px body"],"result":false}
|
||
{"args":["Generous horizontal padding doesn't save you when the vertical padding is just one pixel. Lines stack on top of each other against the container edge."],"result":false}
|
||
{"args":["12px vertical / 4px horizontal, 14px body"],"result":false}
|
||
{"args":["Comfortable vertical padding can't fix horizontal text running right up against the side edges. Words feel like they're falling off the container."],"result":false}
|
||
{"args":["Currently missed: cramped large text"],"result":false}
|
||
{"args":["24px heading, 8px padding (passes the 8px floor but is too tight for the text size)"],"result":false}
|
||
{"args":["Tight Heading Container Label"],"result":false}
|
||
{"args":["32px hero, 6px vertical / 16px horizontal padding"],"result":false}
|
||
{"args":["Hero Section With Tight Vertical Padding"],"result":false}
|
||
{"args":["Cramped multi-line code block"],"result":false}
|
||
{"args":["<pre>, 2px padding, 14px monospace"],"result":false}
|
||
{"args":["function example() {\n const value = \"cramped\";\n return value;\n}"],"result":false}
|
||
{"args":["\n \n\n \n \n \n \n \n\n \n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n "],"result":false}
|
||
{"args":["Small inline pills"],"result":false}
|
||
{"args":[".detection-cmd replica: 13px font, 6px / 14px padding (the homepage case)"],"result":false}
|
||
{"args":["npx impeccable detect src/"],"result":false}
|
||
{"args":["small badge: 11px font, 4px / 8px padding"],"result":false}
|
||
{"args":["FEATURED ITEM BADGE"],"result":false}
|
||
{"args":["tag chip: 12px font, 4px / 10px padding"],"result":false}
|
||
{"args":["design system tag chip"],"result":false}
|
||
{"args":["long inline code chip: compact inline code should not be treated like a padded card"],"result":false}
|
||
{"args":["npx impeccable check"],"result":false}
|
||
{"args":["Cards at current standards"],"result":false}
|
||
{"args":["16px body, 8px padding (the original 8px floor)"],"result":false}
|
||
{"args":["Standard card with 8px padding around 16px body text. Comfortable and within current conventions."],"result":false}
|
||
{"args":["14px body, 16px padding (generous)"],"result":false}
|
||
{"args":["Generous 16px padding around 14px body text. The eye has plenty of room from the container edge."],"result":false}
|
||
{"args":["18px body, 12px padding (proportional)"],"result":false}
|
||
{"args":["Larger body text gets proportionally more padding. 12px around 18px text feels balanced."],"result":false}
|
||
{"args":["Comfortable code blocks"],"result":false}
|
||
{"args":["multi-line <pre>, 8px padding, 14px font"],"result":false}
|
||
{"args":["function example() {\n const value = \"ok\";\n return value;\n}"],"result":false}
|
||
{"args":["multi-line <pre>, 12px padding, 14px font"],"result":false}
|
||
{"args":["function example() {\n const value = \"comfortable\";\n return value;\n}"],"result":false}
|
||
{"args":["Interactive elements"],"result":false}
|
||
{"args":["button: 14px font, 8px / 16px padding"],"result":false}
|
||
{"args":["Standard sized button"],"result":false}
|
||
{"args":["form input: 16px font, 8px / 12px padding"],"result":false}
|
||
{"args":["Big text with proportional padding"],"result":false}
|
||
{"args":["24px heading, 16px padding"],"result":false}
|
||
{"args":["Heading With Room To Breathe"],"result":false}
|
||
{"args":["32px hero, 20px / 32px padding"],"result":false}
|
||
{"args":["Hero With Big Padding"],"result":false}
|
||
{"args":["Cream palette (Tailwind) fixture"],"result":false}
|
||
{"args":["A warm cream surface via Tailwind"],"result":false}
|
||
{"args":["The background comes from a utility class (bg-amber-50), not inline CSS, so the static engine never resolves it to a computed color. The class-list fallback should still catch it."],"result":false}
|
||
{"args":["Cream palette fixture"],"result":false}
|
||
{"args":["\n body { background: #f5efe2; color: #2a2622; font-family: Georgia, serif; margin: 0; padding: 40px; }\n h1 { font-size: 40px; margin: 0 0 16px; }\n p { max-width: 60ch; line-height: 1.6; }\n "],"result":false}
|
||
{"args":["A warm cream surface"],"result":false}
|
||
{"args":["The whole page sits on the default tasteful off-white that every generated landing page reaches for. The background is the tell, regardless of what sits on top of it."],"result":false}
|
||
{"args":["Real gradient-text styling"],"result":false}
|
||
{"args":["\n body { font-family: Georgia, serif; margin: 0; padding: 40px 24px; background: #fdfcf9; color: #1f2933; }\n /* SHOULD FLAG (gradient-text + ai-color-palette): a real style rule. */\n .gradient-heading {\n background: linear-gradient(135deg, #7c3aed, #a855f7);\n -webkit-background-clip: text;\n background-clip: text;\n -webkit-text-fill-color: transparent;\n font-size: 44px;\n }\n"],"result":false}
|
||
{"args":["\n\n\n\n\n\n\n\n\n\n"],"result":false}
|
||
{"args":["Ship Faster With Us"],"result":false}
|
||
{"args":["Inline gradient heading"],"result":false}
|
||
{"args":["Tailwind gradient heading"],"result":false}
|
||
{"args":["Body copy long enough to make this a real page for the scanners to read."],"result":false}
|
||
{"args":["Docs about CSS anti-patterns"],"result":false}
|
||
{"args":["\n body { font-family: Georgia, 'Iowan Old Style', serif; margin: 0; padding: 32px 24px 40px; color: #1f2933; background: #fdfcf9; }\n h1 { font-size: 34px; margin: 0 0 12px; }\n h2 { font-size: 20px; margin: 28px 0 8px; }\n p { font-size: 16px; max-width: 640px; }\n pre { background: #f1efe9; padding: 16px; overflow-x: auto; font-size: 13px; }\n code { font-family: 'SF Mono', ui-monospace, monospace; }\n"],"result":false}
|
||
{"args":["\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"],"result":false}
|
||
{"args":["Changelog"],"result":false}
|
||
{"args":["Detector"],"result":false}
|
||
{"args":[" gradients stop tripping the contrast rules when the glyph fill is transparent."],"result":false}
|
||
{"args":["background-clip: text"],"result":false}
|
||
{"args":["What the rule catches"],"result":false}
|
||
{"args":[".hero-title {\n background: linear-gradient(135deg, #667eea, #764ba2);\n -webkit-background-clip: text;\n background-clip: text;\n color: #7c3aed;\n}"],"result":false}
|
||
{"args":["The Tailwind form pairs bg-clip-text with bg-gradient-to-r on one element; the two utilities together are the tell."],"result":false}
|
||
{"args":["Pulsing dots"],"result":false}
|
||
{"args":[".status-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n animation: pulse 2s ease-in-out infinite;\n}"],"result":false}
|
||
{"args":["Side stripes"],"result":false}
|
||
{"args":[".card::before {\n content: \"\";\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 4px;\n background: #3b82f6;\n}"],"result":false}
|
||
{"args":["Fixture: gradient body ground, oklch stops (low-contrast)"],"result":false}
|
||
{"args":["\n /* The impeccable.style false-positive class: the page ground is a gradient\n set via `background:` shorthand, so the body's computed backgroundColor\n is transparent and the ground exists only as a backgroundImage whose\n stops serialize in oklch. The detector must measure text against those\n stops (composited under any translucent layers), never against an\n assumed white body. */\n body {\n margin: 0;\n padding: 48px;\n background: linear-gradient(180deg, oklch(7% 0.006 95), oklch(4% 0.004 95));\n font-family: Arial, sans-serif;\n }\n body > * { margin: 0 0 32px; }\n\n /* ── Should flag ─────────────────────────────────────────────────── */\n .flag-muted-direct {\n width: 560px;\n color: oklch(30% 0 0);\n font-size: 16px;\n line-height: 1.7;\n }\n .flag-muted-nested-wrapper { width: 560px; }\n .flag-muted-nested {\n color: oklch(32% 0 0);\n font-size: 16px;\n line-height: 1.7;\n margin: 0;\n }\n .frosted-light {\n width: 480px;\n padding: 24px;\n background: rgba(250, 249, 246, 0.88);\n border-radius: 4px;\n }\n .flag-light-on-frosted {\n color: oklch(88% 0 0);\n font-size: 15px;\n line-height: 1.6;\n margin: 0;\n }\n\n /* ── Should pass ─────────────────────────────────────────────────── */\n .pass-hero-title {\n width: 640px;\n color: oklch(91% 0 0);\n font-size: 48px;\n }\n .pass-hero-body {\n width: 560px;\n color: oklch(88% 0 0);\n font-size: 16px;\n line-height: 1.7;\n }\n .pass-on-raised {\n width: 420px;\n padding: 24px;\n background: oklch(11% 0.006 95);\n color: oklch(88% 0 0);\n font-size: 15px;\n }\n .pass-dark-on-frosted {\n color: oklch(15% 0.01 95);\n font-size: 15px;\n line-height: 1.6;\n margin: 0 0 12px;\n }\n .pass-hex-gradient {\n width: 520px;\n padding: 24px;\n background-image: linear-gradient(180deg, #14120f, #0a0908);\n color: oklch(90% 0 0);\n font-size: 15px;\n line-height: 1.6;\n }\n .pass-image-backed {\n width: 460px;\n padding: 18px 22px;\n background: linear-gradient(180deg, rgba(255, 255, 255, 0.3), rgba(255, 255, 255, 0.2)), url(\"/fixtures/antipatterns/no-such-texture.png\") center / cover;\n color: oklch(15% 0.01 95);\n font-size: 15px;\n line-height: 1.6;\n }\n .pass-photo-panel {\n width: 460px;\n padding: 18px 22px;\n background-image: url(\"/fixtures/antipatterns/no-such-photo.jpg\");\n color: oklch(20% 0 0);\n font-size: 15px;\n line-height: 1.6;\n }\n .pass-hidden-scene {\n opacity: 0;\n width: 560px;\n }\n .pass-hidden-scene p {\n color: oklch(30% 0 0);\n font-size: 16px;\n line-height: 1.7;\n margin: 0;\n }\n"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n"],"result":false}
|
||
{"args":["Light display text on the lacquer gradient"],"result":false}
|
||
{"args":["This paragraph sits directly on the body's dark gradient ground. Its near-white ink clears WCAG AA against both gradient stops by a wide margin, so a detector that measures the real ground must not flag it. A detector that assumes a white body flags it at about 1.3:1."],"result":false}
|
||
{"args":["Light text on a raised solid oklch surface between the text and the body gradient still resolves through the normal opaque-ancestor walk."],"result":false}
|
||
{"args":["\n \n \n "],"result":false}
|
||
{"args":["Dark ink on a translucent light wash over the dark gradient. The wash composites to a light surface, so this passes; measuring raw gradient stops would wrongly flag it."],"result":false}
|
||
{"args":["Light ink on the same translucent light wash genuinely fails once the wash is composited over the stops; raw stops would wrongly pass it."],"result":false}
|
||
{"args":["This muted dark-gray ink genuinely fails contrast against both stops of the dark gradient ground, which proves the gradient stops are being measured instead of the checks silently skipping."],"result":false}
|
||
{"args":["Muted ink reached through a transparent wrapper still resolves to the body gradient and still fails against its stops."],"result":false}
|
||
{"args":["A non-body gradient with legacy hex stops keeps working through the original rgb/hex parser, and light text on it passes."],"result":false}
|
||
{"args":["Dark ink on a translucent wash over a texture image. The image pixels are unknowable to the analytic walk, so contrast must skip rather than composite the wash over the wrong base."],"result":false}
|
||
{"args":["Dark ink on a photo-backed panel sitting over the dark body gradient. Walking past the photo to the gradient stops would wrongly flag this; the unmeasurable image layer must end the walk."],"result":false}
|
||
{"args":["Dark theme, modern color syntax"],"result":false}
|
||
{"args":["\n /* A lacquer-black ground painted as a gradient in oklch, the shape that\n floods dark themes with \"on #ffffff\" low-contrast false positives when\n the detector cannot read the ground and assumes white. */\n body {\n margin: 0;\n padding: 24px;\n background: linear-gradient(oklch(0.07 0.006 95), oklch(0.11 0.008 95));\n font-family: Georgia, serif;\n font-size: 15px;\n line-height: 1.6;\n }\n\n section {\n padding: 24px;\n margin-bottom: 24px;\n width: 640px;\n }\n\n p { margin: 0 0 12px; font-size: 15px; }\n\n /* Readable grounds, each written in a color syntax a real browser keeps\n verbatim in getComputedStyle output. */\n .panel-srgb { background-color: color(srgb 0.1 0.11 0.12); }\n .panel-mix { background-color: color-mix(in srgb, white 6%, #0b0b0d); }\n .panel-p3 { background-color: color(display-p3 0.07 0.07 0.08); }\n .panel-lch { background-color: lch(18 4 60); }\n .panel-light { background-color: color(srgb 0.98 0.97 0.95); }\n\n /* A color space the detector does not model. Nested inside the light\n panel so a detector that walks PAST the unreadable layer would measure\n light text against the light section and invent a finding. */\n .panel-unreadable { background-color: color(rec2020 0.05 0.05 0.05); padding: 20px; }\n\n /* An image layer stacked ABOVE a loud gradient. The image is the visible\n surface and its pixels are unreadable; a detector that falls back to\n gradient stops here measures a layer the visitor never sees. */\n .panel-image-over-gradient { background-image: url(\"opaque-panel.png\"), linear-gradient(#ff0000, #0000ff); }\n\n /* Keywords a real browser resolves before getComputedStyle output but a\n partial cascade hands through verbatim: inherit takes the parent's\n ground, currentcolor paints with the element's own text color. Both are\n knowable surfaces; abstaining on them would hide real findings. */\n .panel-inherit { background-color: inherit; padding: 20px; }\n .panel-current { background-color: currentcolor; color: color(srgb 0.94 0.93 0.91); }\n .text-pale-2 { color: color(srgb 0.78 0.77 0.75); }\n\n .text-light { color: oklch(0.92 0.01 90); }\n .text-dark { color: color(srgb 0.1 0.11 0.12); }\n\n .text-muted-ground { color: oklch(0.32 0.01 90); }\n .text-dim-srgb { color: color(srgb 0.28 0.28 0.3); }\n .text-dim-p3 { color: color(display-p3 0.35 0.35 0.36); }\n .text-dim-lch { color: lch(35 3 60); }\n .text-pale-light { color: color(srgb 0.75 0.74 0.72); }\n"],"result":false}
|
||
{"args":["\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"],"result":false}
|
||
{"args":["Flag Muted On Oklch Ground: charcoal body copy sitting straight on the lacquer gradient."],"result":false}
|
||
{"args":["Flag Dim On Srgb Panel: slate copy on a near-black srgb surface."],"result":false}
|
||
{"args":["Flag Dim On Display P3 Panel: slate copy on a near-black display-p3 surface."],"result":false}
|
||
{"args":["Flag Dim On Lch Panel: warm gray copy on a warm near-black lch surface."],"result":false}
|
||
{"args":["Flag Pale On Light Panel: bone copy on a bone-white srgb surface."],"result":false}
|
||
{"args":["Flag Pale On Inherited Light Panel: the ground is inherited from the light section, so the walk must resolve it rather than abstain."],"result":false}
|
||
{"args":["Flag Pale On Currentcolor Panel: the ground is the panel's own bone text color, so the keyword must resolve rather than abstain."],"result":false}
|
||
{"args":["Pass Light Text On Oklch Gradient Ground: warm ivory copy on the lacquer gradient, roughly 15:1."],"result":false}
|
||
{"args":["Pass Light Text On Srgb Panel: warm ivory copy on the near-black srgb surface."],"result":false}
|
||
{"args":["Pass Light Text On Color Mix Panel: warm ivory copy on a surface a browser reports as color(srgb ...)."],"result":false}
|
||
{"args":["Pass Light Text On Display P3 Panel: warm ivory copy on the near-black display-p3 surface."],"result":false}
|
||
{"args":["Pass Light Text On Lch Panel: warm ivory copy on the warm near-black lch surface."],"result":false}
|
||
{"args":["Pass Dark Text On Light Panel: ink copy on the bone-white srgb surface."],"result":false}
|
||
{"args":["Pass Unreadable Panel Inside Light Section: the ground is a color space the detector cannot read, so it must abstain instead of measuring against the light section or against white."],"result":false}
|
||
{"args":["Pass Image Over Gradient: the visible ground is an image whose pixels the engine cannot read, so it must abstain instead of measuring the loud gradient hidden beneath it."],"result":false}
|
||
{"args":["\n \n\n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n \n \n \n"],"result":false}
|
||
{"args":["\n .design-system-fixture {\n --brand-accent: #b8422e;\n background: #f7f4ee;\n color: #241f1a;\n font-family: \"IBM Plex Sans\", Arial, sans-serif;\n padding: 24px;\n }\n\n .case {\n margin: 0 0 12px;\n padding: 14px 16px;\n border-radius: 8px;\n border: 1px solid #d4c7b9;\n background: #ffffff;\n color: #241f1a;\n font-size: 16px;\n }\n\n .fixture-note {\n margin: 0 0 16px;\n padding: 12px 14px;\n border-radius: 8px;\n border: 1px solid #d4c7b9;\n background: #f7f4ee;\n color: #241f1a;\n font-size: 14px;\n line-height: 1.45;\n }\n\n .fixture-note strong {\n font-weight: 700;\n }\n\n .flag-font { font-family: \"Poppins\", sans-serif; }\n .flag-color { color: #ff00aa; }\n .flag-background { background-color: rgb(20, 180, 220); }\n .flag-border { border-color: #00a982; }\n .flag-radius { border-radius: 18px; }\n .flag-font-size { font-size: 12.5px; }\n\n .pass-display-font { font-family: \"Avenir Next\", Georgia, serif; }\n .pass-rem-font-size { font-size: 1rem; }\n .pass-relative-font-size { font-size: 1.2em; }\n .pass-generic-font { font-family: ui-sans-serif, system-ui, sans-serif; }\n .pass-token-color { color: var(--brand-accent); }\n .pass-alpha-color { color: rgba(184, 66, 46, 0.45); }\n .pass-close-color { color: #bb442f; }\n .pass-ramp-color { color: #d55a42; }\n .pass-zero-radius { border-radius: 0; }\n .pass-percent-radius { border-radius: 50%; }\n .pass-scale-radius { border-radius: 32px; }\n .pass-mid-pill-radius { border-radius: 100px; }\n .pass-pill-radius { border-radius: 999px; }\n "],"result":false}
|
||
{"args":["\n design-system detections need injected DESIGN.md context, so this page only shows global/browser findings. Use the CLI or hook tests to exercise the design-system rules.\n "],"result":false}
|
||
{"args":["Browser note:"],"result":false}
|
||
{"args":["Flag Font Unsupported"],"result":false}
|
||
{"args":["Flag Color Hot Pink"],"result":false}
|
||
{"args":["Flag Background Cyan"],"result":false}
|
||
{"args":["Flag Border Teal"],"result":false}
|
||
{"args":["Flag Radius Eighteen"],"result":false}
|
||
{"args":["Flag Font Size Twelve Point Five"],"result":false}
|
||
{"args":["Flag Google Font Source"],"result":false}
|
||
{"args":["Pass Display Font"],"result":false}
|
||
{"args":["Pass Rem Font Size"],"result":false}
|
||
{"args":["Pass Relative Font Size"],"result":false}
|
||
{"args":["Pass Generic Font"],"result":false}
|
||
{"args":["Pass Token Color"],"result":false}
|
||
{"args":["Pass Alpha Color"],"result":false}
|
||
{"args":["Pass Close Color"],"result":false}
|
||
{"args":["Pass Ramp Color"],"result":false}
|
||
{"args":["Pass Zero Radius"],"result":false}
|
||
{"args":["Pass Percent Radius"],"result":false}
|
||
{"args":["Pass Scale Radius"],"result":false}
|
||
{"args":["Pass Mid Pill Radius"],"result":false}
|
||
{"args":["Pass Pill Radius"],"result":false}
|
||
{"args":["Edge-flush cards fixture (browser-only)"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; background: #f2f2f4; }\n h2 { padding: 16px 24px 0; font-size: 16px; }\n .scroller { width: 600px; margin: 8px 24px 24px; overflow-x: auto; display: flex; scroll-snap-type: x mandatory; background: #e4e4e8; }\n .panel { flex: 0 0 auto; scroll-snap-align: start; }\n .card { background: #fff; border-radius: 10px; height: 90px; padding: 12px; box-sizing: border-box; margin-bottom: 12px; }\n .plain { height: 90px; padding: 12px; box-sizing: border-box; }\n\n /* FLAG: first panel is wider than the scroller's clip box, so its cards\n keep a 16px left gutter but end 4px past the right clip edge. */\n .flag-pager .panel-wide { width: 620px; }\n .flag-pager .panel-wide .card { margin-left: 16px; width: 588px; }\n .flag-pager .panel-ok { width: 600px; }\n .flag-pager .panel-ok .card { margin-left: 16px; width: 568px; }\n\n /* PASS: every panel keeps a symmetric inset. */\n .pass-even .panel { width: 600px; }\n .pass-even .card { margin-left: 16px; width: 568px; }\n\n /* PASS: deliberate peek carousel; the third card is cut mid-card, far\n past the clip edge, which reads as an affordance rather than a bug. */\n .pass-peek { gap: 12px; padding: 12px 0 12px 16px; }\n .pass-peek .card { flex: 0 0 auto; width: 260px; margin: 0; }\n\n /* PASS: flush children without card styling are plain content, not cards. */\n .pass-plain .panel { width: 620px; }\n .pass-plain .plain { margin-left: 16px; width: 588px; }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n\n \n \n\n \n \n"],"result":false}
|
||
{"args":["Flag: first panel wider than its clip box"],"result":false}
|
||
{"args":["Card cut off at the right clip edge while keeping its left gutter."],"result":false}
|
||
{"args":["Second card in the oversized panel, also flush against the edge."],"result":false}
|
||
{"args":["Sibling panel card with the intended inset on both sides."],"result":false}
|
||
{"args":["Pass: symmetric insets"],"result":false}
|
||
{"args":["Card with an even gutter on both sides of the clip box."],"result":false}
|
||
{"args":["Second panel card, also properly inset."],"result":false}
|
||
{"args":["Pass: deliberate peek"],"result":false}
|
||
{"args":["\n \n \n \n \n "],"result":false}
|
||
{"args":["First peek card fully visible."],"result":false}
|
||
{"args":["Second peek card fully visible."],"result":false}
|
||
{"args":["Third card cut mid-card as a scroll affordance."],"result":false}
|
||
{"args":["Fourth card entirely off-screen."],"result":false}
|
||
{"args":["Pass: plain content, not cards"],"result":false}
|
||
{"args":["Unstyled text block flush against the edge is not a card."],"result":false}
|
||
{"args":["Second plain block."],"result":false}
|
||
{"args":["Em-dash entity overuse"],"result":false}
|
||
{"args":["A page whose em-dashes hide inside HTML entities"],"result":false}
|
||
{"args":["\n The product is fast — it is also cheap — and it is honest\n — which matters — more than speed — or price\n — in the short term — and the long run — always.\n "],"result":false}
|
||
{"args":["Extreme negative tracking fixture"],"result":false}
|
||
{"args":["\n body { font-family: Georgia, serif; margin: 0; color: #1a1a1a; background: #fff; }\n .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 24px; }\n .col { padding: 16px; }\n p, h2 { margin: 0 0 20px; }\n "],"result":false}
|
||
{"args":["Tracking crushed em paragraph copy that should still read as a sentence."],"result":false}
|
||
{"args":["Tracking crushed pixels heading line"],"result":false}
|
||
{"args":["Tracking crushed heading variant text"],"result":false}
|
||
{"args":["Optical tighten display"],"result":false}
|
||
{"args":["Plain paragraph copy with default tracking that reads comfortably."],"result":false}
|
||
{"args":["first-viewport-column-overflow fixture"],"result":false}
|
||
{"args":["\n * { margin: 0; box-sizing: border-box; }\n body { font-family: system-ui, sans-serif; color: #111; background: #fff; }\n p { margin: 0 0 14px; line-height: 1.6; }\n\n /* ── FLAG: two-column opening section, right column runs far past the fold\n while the left column fits inside one viewport ── */\n .flag { display: grid; grid-template-columns: 1.4fr 1fr; gap: 40px; padding: 40px; }\n .flag .short { }\n .flag .short h1 { font-size: 40px; margin-bottom: 16px; }\n .flag .tall .filler { height: 1600px; background: #eee; border-radius: 8px; padding: 16px; }\n\n /* ── PASS: balanced two-column section, both fit ── */\n .pass-balanced { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; padding: 40px; }\n .pass-balanced .col { }\n\n /* ── PASS: single full-page hero (no sibling column) ── */\n .pass-hero { padding: 40px; }\n .pass-hero .art { height: 1400px; background: #f3f3f3; }\n"],"result":false}
|
||
{"args":["\n \n\n \n\n \n"],"result":false}
|
||
{"args":["Upload your film and add a narrator"],"result":false}
|
||
{"args":["Paste a link or drop a file. We transcribe it, translate, and one narrator reads over the top."],"result":false}
|
||
{"args":["The original stays audible underneath, just quieter."],"result":false}
|
||
{"args":["What is a lektor? A single calm voice reads every line of a foreign film."],"result":false}
|
||
{"args":["A tall editorial column with a long photo and a lot of copy."],"result":false}
|
||
{"args":["Left column with a normal amount of copy that fits comfortably within the opening viewport."],"result":false}
|
||
{"args":["Two short paragraphs, nothing that runs long."],"result":false}
|
||
{"args":["Right column, also short. Both sides sit inside the first screen."],"result":false}
|
||
{"args":["No column stretches the fold."],"result":false}
|
||
{"args":["Flush Against Border; Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* Fixture for the flush-against-border rule.\n\n The rule is more general than its name suggests: it fires when a\n container has any visible boundary (border, outline, OR a\n non-transparent background) AND zero inline padding on the\n bounded side, AND text-bearing children land flush against that\n boundary.\n\n Distinct from cramped-padding (which fires when an element has\n its OWN direct text and the padding/font-size ratio is wrong).\n Here the container has NO direct text; only children; so\n cramped-padding gives it a pass. */\n\n body { font-family: system-ui, sans-serif; background: #fafafa; padding: 24px; margin: 0; color: #0f172a; line-height: 1.5; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1400px; margin: 0 auto; }\n .col h2 { font-size: 22px; font-weight: 700; margin: 0 0 16px; color: #0f172a; line-height: 1.2; }\n .col h3 { font-size: 13px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.06em; margin: 28px 0 10px; color: #64748b; }\n .case { margin-bottom: 14px; }\n .case-label { display: block; font-size: 12px; color: #64748b; margin-bottom: 8px; font-style: italic; max-width: 60ch; }\n\n /* ────────────────────────────────────────────────────────────\n SHOULD FLAG\n ──────────────────────────────────────────────────────────── */\n\n /* ── FLAG: the /live-mode-frameworks bug; border + zero side-padding ── */\n .flag-frameworks {\n padding: 28px 0 0;\n background: #fffbe6;\n border: 1px solid #d4a017;\n border-radius: 8px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n }\n .flag-frameworks-label {\n font-family: ui-monospace, monospace;\n font-size: 11px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: #8b6914;\n }\n .flag-frameworks-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n list-style: none;\n padding: 0;\n margin: 0;\n }\n .flag-frameworks-list li {\n font-family: ui-monospace, monospace;\n font-size: 13px;\n padding: 6px 12px;\n background: #fef3c7;\n border: 1px solid #d4a017;\n border-radius: 4px;\n color: #78350f;\n }\n\n /* ── FLAG: child text flush against the four borders of a card ── */\n .flag-card-borders {\n padding: 0;\n background: white;\n border: 1px solid #cbd5e1;\n border-radius: 8px;\n }\n .flag-card-borders h4 { margin: 0; font-size: 14px; color: #0f172a; }\n .flag-card-borders p { margin: 4px 0 0; font-size: 13px; color: #475569; }\n\n /* ── FLAG: non-transparent background, zero padding ── */\n .flag-bg-only {\n padding: 0;\n background: #fde68a;\n border-radius: 6px;\n border: 0;\n }\n .flag-bg-only h4 { margin: 0; font-size: 14px; color: #78350f; }\n .flag-bg-only p { margin: 4px 0 0; font-size: 13px; color: #78350f; }\n\n /* ── FLAG: outline (no border), zero padding ── */\n .flag-outline-only {\n padding: 0;\n background: transparent;\n border: 0;\n outline: 2px solid #94a3b8;\n outline-offset: 0;\n }\n .flag-outline-only h4 { margin: 0; font-size: 14px; color: #0f172a; }\n .flag-outline-only p { margin: 4px 0 0; font-size: 13px; color: #475569; }\n\n /* ── FLAG: border-left + zero left-padding, other sides fine ── */\n .flag-asym-leftflush {\n padding: 12px 16px 12px 0;\n background: white;\n border: 1px solid #cbd5e1;\n border-radius: 4px;\n }\n .flag-asym-leftflush h4 { margin: 0; font-size: 14px; color: #0f172a; }\n .flag-asym-leftflush p { margin: 4px 0 0; font-size: 13px; color: #475569; }\n\n /* ────────────────────────────────────────────────────────────\n SHOULD PASS\n ──────────────────────────────────────────────────────────── */\n\n /* ── PASS: no boundary at all (no bg, no border, no outline) ── */\n .pass-no-boundary {\n padding: 0;\n background: transparent;\n border: 0;\n outline: 0;\n }\n .pass-no-boundary-label {\n font-family: ui-monospace, monospace;\n font-size: 11px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: #475569;\n margin-bottom: 10px;\n }\n .pass-no-boundary-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n list-style: none;\n padding: 0;\n margin: 0;\n }\n .pass-no-boundary-list li {\n font-size: 13px;\n padding: 6px 12px;\n background: #f5f5f7;\n border: 1px solid #e2e8f0;\n border-radius: 4px;\n color: #0f172a;\n }\n\n /* ── PASS: top-only border, no left/right border, padding-top fine ── */\n .pass-top-rule {\n padding: 24px 0 0;\n background: transparent;\n border: 0;\n border-top: 1px solid #cbd5e1;\n }\n .pass-top-rule-label {\n font-family: ui-monospace, monospace;\n font-size: 11px;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: #475569;\n margin-bottom: 10px;\n }\n .pass-top-rule-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n list-style: none;\n padding: 0;\n margin: 0;\n }\n .pass-top-rule-list li {\n font-size: 13px;\n padding: 6px 12px;\n background: #f5f5f7;\n border: 1px solid #e2e8f0;\n border-radius: 4px;\n color: #0f172a;\n }\n\n /* ── PASS: bordered card with comfortable inset on all sides ── */\n .pass-bordered-padded {\n padding: 18px 20px;\n background: white;\n border: 1px solid #cbd5e1;\n border-radius: 8px;\n }\n .pass-bordered-padded h4 { margin: 0; font-size: 14px; color: #0f172a; }\n .pass-bordered-padded p { margin: 4px 0 0; font-size: 13px; color: #475569; }\n\n /* ── PASS: background card with comfortable inset ── */\n .pass-bg-padded {\n padding: 14px 18px;\n background: #fde68a;\n border-radius: 6px;\n border: 0;\n }\n .pass-bg-padded h4 { margin: 0; font-size: 14px; color: #78350f; }\n .pass-bg-padded p { margin: 4px 0 0; font-size: 13px; color: #78350f; }\n\n /* ── PASS: outline with comfortable inset ── */\n .pass-outline-padded {\n padding: 14px 18px;\n outline: 2px solid #94a3b8;\n outline-offset: 0;\n border: 0;\n background: transparent;\n }\n .pass-outline-padded h4 { margin: 0; font-size: 14px; color: #0f172a; }\n .pass-outline-padded p { margin: 4px 0 0; font-size: 13px; color: #475569; }\n\n /* ── PASS: image-only container, no text means no flush issue ── */\n .pass-image-only {\n padding: 0;\n background: white;\n border: 1px solid #cbd5e1;\n border-radius: 8px;\n width: 200px;\n overflow: hidden;\n }\n .pass-image-only .placeholder-img {\n width: 100%;\n height: 80px;\n background: linear-gradient(135deg, #cbd5e1, #94a3b8);\n }\n\n /* ── PASS: parent has zero padding, but child margins create the visible inset ── */\n .pass-margin-inset {\n padding: 0;\n background: white;\n border: 1px solid #cbd5e1;\n border-radius: 8px;\n }\n .pass-margin-inset h4 { margin: 16px 18px 4px; font-size: 14px; color: #0f172a; }\n .pass-margin-inset p { margin: 0 18px 16px; font-size: 13px; color: #475569; }\n\n /* ── PASS: an inner shell, not the parent, owns the inset spacing ── */\n .pass-inner-shell {\n padding: 0;\n background: white;\n border: 1px solid #cbd5e1;\n border-radius: 8px;\n }\n .pass-inner-shell .inner-shell {\n margin: 18px;\n }\n .pass-inner-shell h4 { margin: 0 0 4px; font-size: 14px; color: #0f172a; }\n .pass-inner-shell p { margin: 0; font-size: 13px; color: #475569; }\n\n /* ── PASS: same background as parent, so the child draws no visible boundary ── */\n .pass-same-bg-parent {\n padding: 16px;\n background: #eef2f7;\n border-radius: 8px;\n }\n .pass-same-bg-child {\n padding: 0;\n background: #eef2f7;\n border: 0;\n border-radius: 8px;\n }\n .pass-same-bg-child h4 { margin: 0; font-size: 14px; color: #0f172a; }\n .pass-same-bg-child p { margin: 4px 0 0; font-size: 13px; color: #475569; }\n\n /* ── PASS: full-bleed marquee/surface, not a text card missing padding ── */\n .pass-marquee-shell {\n padding: 0;\n background: #172033;\n color: white;\n overflow: hidden;\n border: 0;\n }\n .pass-marquee-track {\n display: flex;\n gap: 12px;\n width: max-content;\n }\n .pass-marquee-card {\n width: 220px;\n padding: 14px;\n background: #23304a;\n border: 1px solid rgba(255, 255, 255, 0.16);\n }\n .pass-marquee-card p { margin: 0; font-size: 13px; }\n\n /* ── PASS: outer scene owns background; inner text surface owns padding ── */\n .pass-inner-text-surface {\n padding: 0;\n background: #0f172a;\n color: white;\n overflow: hidden;\n border: 0;\n }\n .pass-inner-text-surface-window {\n display: grid;\n gap: 6px;\n min-height: 92px;\n padding: 14px;\n background: #111827;\n border: 1px solid rgba(255, 255, 255, 0.16);\n }\n .pass-inner-text-surface-window p { margin: 0; font-size: 13px; }\n "],"result":false}
|
||
{"args":["Border + zero side-padding"],"result":false}
|
||
{"args":["section: full border, padding 28px 0 0; label and pills touch the left/right border lines."],"result":false}
|
||
{"args":["Supported dev servers"],"result":false}
|
||
{"args":["\n \n \n \n \n "],"result":false}
|
||
{"args":["Vite"],"result":false}
|
||
{"args":["Next.js"],"result":false}
|
||
{"args":["SvelteKit"],"result":false}
|
||
{"args":["Astro"],"result":false}
|
||
{"args":["All sides flush against a border"],"result":false}
|
||
{"args":["card: 1px border on all sides, padding 0; heading and body touch every side."],"result":false}
|
||
{"args":["Card flush against the borders"],"result":false}
|
||
{"args":["Body copy sits flush against every visible border line."],"result":false}
|
||
{"args":["Background color, zero padding"],"result":false}
|
||
{"args":["no border, but a non-transparent bg; text flush against the bg's edge counts too."],"result":false}
|
||
{"args":["Title flush against the bg edge"],"result":false}
|
||
{"args":["Body copy with zero padding around a colored fill."],"result":false}
|
||
{"args":["Outline (not border), zero padding"],"result":false}
|
||
{"args":["outline draws outside the box; content at the box edge sits next to the outline."],"result":false}
|
||
{"args":["Title flush against the outline"],"result":false}
|
||
{"args":["The outline counts as a visible boundary too."],"result":false}
|
||
{"args":["Left-side flush, other sides fine"],"result":false}
|
||
{"args":["card: padding 12px 16px 12px 0; text touches the left border, other sides are inset."],"result":false}
|
||
{"args":["Title flush only on the left"],"result":false}
|
||
{"args":["Padding-left zero with a full border."],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["No boundary at all"],"result":false}
|
||
{"args":["no bg, no border, no outline; zero padding is fine."],"result":false}
|
||
{"args":["\n \n \n \n "],"result":false}
|
||
{"args":["Top-only border, no side borders"],"result":false}
|
||
{"args":["border-top only, padding-top 24px; no left/right border to be flush against."],"result":false}
|
||
{"args":["Bordered card with proper inset"],"result":false}
|
||
{"args":["card: 1px border + padding 18px 20px; comfortable inset on every side."],"result":false}
|
||
{"args":["Card with proper inset"],"result":false}
|
||
{"args":["Body copy sits comfortably away from the borders."],"result":false}
|
||
{"args":["Background card with proper inset"],"result":false}
|
||
{"args":["non-transparent bg + padding 14px 18px; content has room to breathe."],"result":false}
|
||
{"args":["Title with inset on bg"],"result":false}
|
||
{"args":["Body copy is comfortably inset from the bg edge."],"result":false}
|
||
{"args":["Outline with proper inset"],"result":false}
|
||
{"args":["outline + padding 14px 18px; content is comfortably inside."],"result":false}
|
||
{"args":["Title with inset and outline"],"result":false}
|
||
{"args":["Outline boundary with proper inset works."],"result":false}
|
||
{"args":["Image-only container, no text"],"result":false}
|
||
{"args":["card: padding 0, border all sides; no text means nothing can be flush."],"result":false}
|
||
{"args":["Child margins provide the inset"],"result":false}
|
||
{"args":["card: parent padding 0, but text children have margins that create visible inset."],"result":false}
|
||
{"args":["Title inset by child margin"],"result":false}
|
||
{"args":["Body copy is not visually flush even though the parent has zero padding."],"result":false}
|
||
{"args":["Inner shell provides the inset"],"result":false}
|
||
{"args":["card: parent padding 0, but an inner shell is offset from every edge."],"result":false}
|
||
{"args":["\n \n \n "],"result":false}
|
||
{"args":["Title inset by inner shell"],"result":false}
|
||
{"args":["The direct child creates the breathing room."],"result":false}
|
||
{"args":["Same background as parent"],"result":false}
|
||
{"args":["child padding 0, but it has the same background as the parent, so no child boundary is visible."],"result":false}
|
||
{"args":["Same-surface visual shell"],"result":false}
|
||
{"args":["The text belongs to the parent surface, not to a distinct zero-padding card."],"result":false}
|
||
{"args":["Full-bleed marquee shell"],"result":false}
|
||
{"args":["marquee shell clips animated cards intentionally; the cards, not the shell, own text padding."],"result":false}
|
||
{"args":["Quote card with its own comfortable padding."],"result":false}
|
||
{"args":["Another moving card, also padded inside."],"result":false}
|
||
{"args":["Inner text surface"],"result":false}
|
||
{"args":["outer shell has a scene background; the inner window owns the actual text padding."],"result":false}
|
||
{"args":["Build step running"],"result":false}
|
||
{"args":["Detector output belongs to the mock window."],"result":false}
|
||
{"args":["Image hover-transform fixture"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; background: #fff; }\n .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 24px; }\n .col { padding: 16px; }\n img { display: block; width: 160px; height: 120px; margin: 0 0 24px; background: #ddd; }\n .flag-css-hover img { transition: transform 0.3s; }\n .flag-css-hover img:hover { transform: scale(1.05); }\n .card:hover { transform: translateY(-4px); }\n "],"result":false}
|
||
{"args":["A card that lifts on hover, which is fine because it is not an image."],"result":false}
|
||
{"args":["Dark Glow Anti-Patterns — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* Two-column fixture: left = should flag, right = should pass.\n The dark-glow rule fires when a colored box-shadow appears on a\n child whose ancestor has a dark background. Each column wraps\n its dark-context cases in its own .dark-context section so the\n parent-bg lookup behaves predictably regardless of layout. */\n body { font-family: system-ui, sans-serif; background: #f9fafb; padding: 24px; margin: 0; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1200px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .col h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 24px 0 8px; color: #64748b; }\n .dark-context { background: #111827; padding: 16px; border-radius: 12px; }\n .dark-context + .dark-context { margin-top: 12px; }\n /* Card variants: explicit dark vs light pairings, both WCAG AA compliant */\n .card { padding: 14px 16px; border-radius: 10px; margin-bottom: 10px; }\n .card h4 { font-weight: 600; font-size: 13px; margin: 0; }\n .card p { font-size: 12px; margin: 4px 0 0; }\n .card-dark { background: #1f2937; }\n .card-dark h4 { color: #f9fafb; }\n .card-dark p { color: #cbd5e1; }\n .card-light { background: white; }\n .card-light h4 { color: #0f172a; }\n .card-light p { color: #475569; }\n\n /* ── FLAG: dark background + colored glow ── */\n .glow-blue { box-shadow: 0 0 20px rgba(59, 130, 246, 0.4); }\n .glow-purple { box-shadow: 0 0 25px rgba(139, 92, 246, 0.35); }\n .glow-cyan { box-shadow: 0 0 15px rgba(6, 182, 212, 0.5); }\n .glow-multi { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 0 30px rgba(168, 85, 247, 0.3); }\n\n /* ── FLAG: modern color formats, var() refs, text-shadow ── */\n :root { --fixture-glow: oklch(0.78 0.14 155); }\n .glow-oklch { box-shadow: 0 0 18px oklch(0.72 0.15 195 / 0.5); }\n .glow-hex { box-shadow: 0 0 16px #3b82f6; }\n .glow-hsl { box-shadow: 0 0 22px hsl(280, 80%, 60%); }\n .glow-var { box-shadow: 0 0 10px var(--fixture-glow); }\n .glow-text h4 { text-shadow: 0 0 12px #22d3ee; }\n /* Zero-offset chromatic halo is slop on light backgrounds too */\n .glow-light-oklch { box-shadow: 0 0 20px oklch(0.65 0.2 300 / 0.45); }\n /* Unreadable surface: url() image ancestor. The zero-offset halo tell\n holds on ANY background, so an unknown surface must not suppress it.\n Both element loops must agree here (the browser adapter once returned\n [] for the whole element on an unresolved surface). */\n .photo-context { background-image: url('/fixtures/antipatterns/missing-photo.png'); padding: 16px; border-radius: 12px; }\n .glow-photo-halo { box-shadow: 0 0 26px rgba(217, 70, 239, 0.5); }\n /* Opaque gradient atop a url() layer: every stop is opaque, so the\n gradient provably covers the image — the surface IS the dark\n gradient, and the dark-background glow tell may score against it\n (browser hunt; the static loop has no gradient hunt). */\n .photo-opaque-grad { background: linear-gradient(#111827, #0b1220), url('/fixtures/antipatterns/missing-photo.png'); padding: 16px; border-radius: 12px; }\n .glow-photo-opaque { box-shadow: 0 6px 22px rgba(249, 115, 22, 0.5); }\n\n /* ── PASS: same dark/light backgrounds, but neutral or no glow ── */\n .light-colored-shadow { box-shadow: 0 2px 4px rgba(59, 130, 246, 0.15); }\n .dark-normal-shadow { box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); }\n .dark-focus-ring { box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); }\n .dark-subtle-shadow { box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2); }\n .dark-elevated-sm { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); }\n .dark-elevated-md { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.4); }\n .dark-elevated-lg { box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5); }\n .dark-elevated-inset { box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); }\n .dark-elevated-multi { box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.2); }\n .elevated-sm-light { box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); }\n .elevated-md-light { box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); }\n .elevated-warm-light { box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.15); }\n /* Medium-gray card: lighter than dark mode, used to demonstrate \"not dark enough\" for glow detection. White text for AA contrast. */\n .card-medium { background: #6b7280; }\n .card-medium h4 { color: #ffffff; }\n .card-medium p { color: #ffffff; }\n .medium-gray-shadow { box-shadow: 0 2px 4px rgba(59, 130, 246, 0.3); }\n /* Offset chromatic drop shadow on a light background: a real drop\n shadow, not a halo — must NOT flag under the zero-offset rule.\n (Element path; the page-level text scan caps at one finding.) */\n .light-offset-colored { box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25); }\n /* Achromatic zero-offset shadow: soft ambient elevation, stays legal */\n .light-soft-neutral { box-shadow: 0 0 24px rgba(0, 0, 0, 0.15); }\n /* Offset chromatic shadow under the same unreadable url() surface:\n the dark-background glow tell needs a surface we can read — abstain. */\n .photo-offset-colored { box-shadow: 0 6px 20px rgba(16, 185, 129, 0.35); }\n /* Translucent gradient atop a url() layer: the image shows through the\n 20% wash, so the real surface is a blend with unknowable pixels (a\n white photo composites to ~#cccccc, not black). Averaging the wash\n stops as if they were the surface scored this \"dark\" — abstain. */\n .photo-translucent-grad { background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)), url('/fixtures/antipatterns/missing-photo.png'); padding: 16px; border-radius: 12px; }\n .photo-translucent-offset { box-shadow: 0 6px 22px rgba(244, 63, 94, 0.45); }\n /* Offset neutral text-shadow on dark card: legibility aid, not a glow */\n .dark-text-offset h4 { text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }\n "],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["CSS colored glows on dark background"],"result":false}
|
||
{"args":["\n \n \n \n \n "],"result":false}
|
||
{"args":["Blue glow"],"result":false}
|
||
{"args":["box-shadow: 0 0 20px rgba(59, 130, 246, 0.4)"],"result":false}
|
||
{"args":["Purple glow"],"result":false}
|
||
{"args":["box-shadow: 0 0 25px rgba(139, 92, 246, 0.35)"],"result":false}
|
||
{"args":["Cyan glow"],"result":false}
|
||
{"args":["box-shadow: 0 0 15px rgba(6, 182, 212, 0.5)"],"result":false}
|
||
{"args":["Multi-shadow with colored glow"],"result":false}
|
||
{"args":["Normal shadow + purple glow combined."],"result":false}
|
||
{"args":["Inline-style glow (regex path)"],"result":false}
|
||
{"args":["Inline pink glow"],"result":false}
|
||
{"args":["Inline style for regex detection path."],"result":false}
|
||
{"args":["Modern color formats, var() refs, text-shadow"],"result":false}
|
||
{"args":["\n \n \n \n \n \n "],"result":false}
|
||
{"args":["OKLCH glow"],"result":false}
|
||
{"args":["box-shadow: 0 0 18px oklch(0.72 0.15 195 / 0.5)"],"result":false}
|
||
{"args":["Hex glow"],"result":false}
|
||
{"args":["box-shadow: 0 0 16px #3b82f6"],"result":false}
|
||
{"args":["HSL glow"],"result":false}
|
||
{"args":["box-shadow: 0 0 22px hsl(280, 80%, 60%)"],"result":false}
|
||
{"args":["Glow via var()"],"result":false}
|
||
{"args":["box-shadow: 0 0 10px var(--fixture-glow)"],"result":false}
|
||
{"args":["Glowing text"],"result":false}
|
||
{"args":["text-shadow: 0 0 12px #22d3ee on the heading."],"result":false}
|
||
{"args":["Zero-offset halo on LIGHT background"],"result":false}
|
||
{"args":["Chromatic halo on light page"],"result":false}
|
||
{"args":["box-shadow: 0 0 20px oklch(0.65 0.2 300 / 0.45)"],"result":false}
|
||
{"args":["Zero-offset halo on unreadable (image) surface"],"result":false}
|
||
{"args":["Halo over photo background"],"result":false}
|
||
{"args":["box-shadow: 0 0 26px rgba(217, 70, 239, 0.5)"],"result":false}
|
||
{"args":["Colored glow under an opaque gradient over an image (browser)"],"result":false}
|
||
{"args":["Opaque dark gradient covers the photo"],"result":false}
|
||
{"args":["box-shadow: 0 6px 22px rgba(249, 115, 22, 0.5)"],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n "],"result":false}
|
||
{"args":["Light page + colored shadow"],"result":false}
|
||
{"args":["Colored shadow on light background"],"result":false}
|
||
{"args":["Not dark mode, colored shadow is fine."],"result":false}
|
||
{"args":["Dark cards with neutral shadows"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Normal gray shadow on dark card"],"result":false}
|
||
{"args":["Gray/black shadow is not a glow."],"result":false}
|
||
{"args":["Focus ring (spread, no blur)"],"result":false}
|
||
{"args":["Functional ring, not decorative glow."],"result":false}
|
||
{"args":["Tiny blur (<5px)"],"result":false}
|
||
{"args":["Too subtle to be \"glowing\"."],"result":false}
|
||
{"args":["Small dark elevation"],"result":false}
|
||
{"args":["Subtle shadow on dark card."],"result":false}
|
||
{"args":["Medium dark elevation"],"result":false}
|
||
{"args":["Standard dark card shadow."],"result":false}
|
||
{"args":["Large dark elevation"],"result":false}
|
||
{"args":["Prominent shadow on dark card."],"result":false}
|
||
{"args":["Inset shadow"],"result":false}
|
||
{"args":["Inner shadow, not a glow."],"result":false}
|
||
{"args":["Multi-shadow dark card"],"result":false}
|
||
{"args":["Layered gray shadows, no color glow."],"result":false}
|
||
{"args":["Medium-gray background (not dark enough)"],"result":false}
|
||
{"args":["Not dark enough background"],"result":false}
|
||
{"args":["Medium gray doesn't qualify as \"dark mode\"."],"result":false}
|
||
{"args":["Colored drop shadow / neutral halo on light background"],"result":false}
|
||
{"args":["Offset colored drop shadow"],"result":false}
|
||
{"args":["Real drop shadow with a color tint — not a halo."],"result":false}
|
||
{"args":["Soft neutral halo"],"result":false}
|
||
{"args":["Achromatic zero-offset shadow is ambient elevation."],"result":false}
|
||
{"args":["Offset chromatic shadow on unreadable (image) surface"],"result":false}
|
||
{"args":["Offset colored shadow over photo background"],"result":false}
|
||
{"args":["Surface unknown, offset shadow — abstain, not flag."],"result":false}
|
||
{"args":["Offset chromatic shadow under a translucent gradient over an image"],"result":false}
|
||
{"args":["The wash blends with unknowable image pixels"],"result":false}
|
||
{"args":["Surface unknown — abstain, never score the wash stops as dark."],"result":false}
|
||
{"args":["Neutral text-shadow on dark card"],"result":false}
|
||
{"args":["Offset neutral text-shadow"],"result":false}
|
||
{"args":["Legibility aid, not a glow."],"result":false}
|
||
{"args":["Light elevated cards"],"result":false}
|
||
{"args":["Small elevation (Tailwind shadow-sm)"],"result":false}
|
||
{"args":["Standard subtle card shadow."],"result":false}
|
||
{"args":["Medium elevation (Tailwind shadow-md)"],"result":false}
|
||
{"args":["Standard card shadow."],"result":false}
|
||
{"args":["Warm deep shadow"],"result":false}
|
||
{"args":["Large spread neutral shadow."],"result":false}
|
||
{"args":["Recurring generated-UI tell fixture"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; background: #fff; }\n .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 24px; }\n .col { padding: 16px; }\n .card { padding: 24px; margin: 0 0 24px; border-radius: 12px; }\n .stripes { height: 80px; margin: 0 0 24px; border-radius: 12px; }\n .grid { height: 80px; margin: 0 0 24px; border-radius: 12px; }\n "],"result":false}
|
||
{"args":["\n \n \n \n \n "],"result":false}
|
||
{"args":["\n Thin hairline border paired with a wide diffuse shadow.\n "],"result":false}
|
||
{"args":["We retired the growth theater and shipped something that actually works."],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["\n Hairline border with a tight, purposeful shadow.\n "],"result":false}
|
||
{"args":["\n A quiet low-contrast hairline with a broad but restrained ambient shadow.\n "],"result":false}
|
||
{"args":["\n OKLCH shadow hue values should not be mistaken for shadow blur.\n "],"result":false}
|
||
{"args":["We measured real outcomes for the people who use this every day."],"result":false}
|
||
{"args":["Heading rhythm fixture"],"result":false}
|
||
{"args":["\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { font-family: Georgia, serif; font-size: 16px; line-height: 1.5; width: 720px; }\n h2 { font-size: 28px; line-height: 1.2; }\n h3 { font-size: 20px; line-height: 1.2; }\n p { max-width: 60ch; }\n\n /* FLAG: zero space above the heading, clear space below it */\n .flag-crowded { margin: 0 0 40px; }\n .flag-crowded + p { margin: 0 0 24px; }\n\n /* PASS: generous space above, tight below */\n .pass-generous { margin: 64px 0 16px; }\n\n /* PASS: near-equal spacing is ambiguous, not inverted */\n .pass-near-equal { margin: 20px 0 24px; }\n\n /* PASS: eyebrow label bound to the heading; real space sits above the cluster */\n .pass-eyebrow-label { font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; margin: 72px 0 8px; }\n .pass-eyebrow-heading { margin: 0 0 24px; }\n\n /* PASS: small card, its boundary provides the separation */\n .pass-card { box-shadow: 0 1px 4px rgba(0,0,0,0.2); background: #f4f4f4; padding: 16px; margin: 48px 0; width: 320px; }\n .pass-card h3 { margin: 0 0 20px; }\n\n /* PASS: heading first inside a banded section; the band edge separates */\n .pass-band { background: #ececec; padding: 8px 0 32px; margin-top: 8px; }\n .pass-band h2 { margin: 0 0 24px; }\n"],"result":false}
|
||
{"args":["\n\n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n\n \n \n\n \n \n \n\n \n\n \n\n"],"result":false}
|
||
{"args":["Pass First Heading"],"result":false}
|
||
{"args":["Opening paragraph after the first heading on the page. The first heading has\n nothing above it, so it is exempt from the rhythm comparison entirely."],"result":false}
|
||
{"args":["A closing paragraph of the previous block of content,\n sitting directly above the crowded heading with no gap at all between them."],"result":false}
|
||
{"args":["Flag Crowded One"],"result":false}
|
||
{"args":["The content this heading introduces sits forty pixels away while the previous\n block touches the heading, so the heading reads as a caption for the text above."],"result":false}
|
||
{"args":["Another closing paragraph of a previous content block, again flush against\n the heading that follows it in the document flow."],"result":false}
|
||
{"args":["Flag Crowded Two"],"result":false}
|
||
{"args":["Second crowded case. Two or more headings must violate the principle before\n the rule fires, because a single occurrence is treated as noise."],"result":false}
|
||
{"args":["A third closing paragraph that the next heading will visually attach to,\n which is exactly the inversion this rule measures."],"result":false}
|
||
{"args":["Flag Crowded Three"],"result":false}
|
||
{"args":["Third crowded case, this time an h3, to confirm the rule reads h2 through h4."],"result":false}
|
||
{"args":["Pass Generous Above"],"result":false}
|
||
{"args":["Sixty-four pixels above and sixteen below. The heading clearly binds to this\n paragraph, which is the healthy shape."],"result":false}
|
||
{"args":["Pass Near Equal"],"result":false}
|
||
{"args":["Twenty pixels above and twenty-four below. Ambiguous rather than inverted,\n so the rule leaves it alone."],"result":false}
|
||
{"args":["Chapter Four"],"result":false}
|
||
{"args":["Pass Eyebrow Cluster"],"result":false}
|
||
{"args":["The tiny label above belongs to the heading cluster. Space is measured from\n the top of the cluster, where seventy-two pixels separate it from this text."],"result":false}
|
||
{"args":["Pass Card Heading"],"result":false}
|
||
{"args":["A heading inside a small card. The card boundary provides the separation,\n so the rule exempts headings inside panels shorter than two hundred pixels."],"result":false}
|
||
{"args":["Pass Band Heading"],"result":false}
|
||
{"args":["This heading is the first content inside a section that draws its own\n background, so the band edge is the separator above it."],"result":false}
|
||
{"args":["Hero-Eyebrow-Chip — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* ─── Layout convention: two-column fixture ─────────────────────────\n Left column = patterns the rule SHOULD flag.\n Right column = legitimate patterns the rule should NOT flag.\n Each test case is annotated with a unique <h1> text so the test\n can match snippets back to expectations.\n ──────────────────────────────────────────────────────────────── */\n body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1200px; margin: 0 auto; }\n .col h2.col-label { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .case { margin-bottom: 32px; padding: 24px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }\n .case p { font-size: 14px; margin: 12px 0 0; color: #64748b; }\n .case h1 { margin: 0; font-family: 'Inter', sans-serif; line-height: 1.05; }\n\n /* Hero size used by most cases */\n .hero { font-size: 88px; font-weight: 600; }\n\n /* ── FLAG cases ── */\n\n .eyebrow-classic {\n text-transform: uppercase;\n letter-spacing: 0.15em;\n font-size: 12px;\n color: #64748b;\n font-weight: 600;\n margin: 0 0 16px;\n }\n\n .eyebrow-span {\n display: inline-block;\n text-transform: uppercase;\n letter-spacing: 0.16em;\n font-size: 11px;\n color: #475569;\n font-weight: 600;\n margin: 0 0 16px;\n }\n\n .eyebrow-pill {\n display: inline-block;\n text-transform: uppercase;\n letter-spacing: 0.14em;\n font-size: 12px;\n background: #f1f5f9;\n color: #334155;\n border-radius: 999px;\n padding: 4px 12px;\n font-weight: 600;\n margin: 0 0 16px;\n }\n\n .eyebrow-already-uppercase {\n letter-spacing: 0.18em;\n font-size: 11px;\n color: #b45309;\n font-weight: 700;\n margin: 0 0 16px;\n }\n\n /* ── PASS cases ── */\n\n .pass-eyebrow-no-tracking {\n text-transform: uppercase;\n letter-spacing: normal;\n font-size: 12px;\n color: #64748b;\n font-weight: 600;\n margin: 0 0 16px;\n }\n\n .pass-body-heading {\n font-size: 24px;\n font-weight: 600;\n font-family: 'Inter', sans-serif;\n margin: 0;\n }\n\n .pass-card-caption {\n text-transform: uppercase;\n letter-spacing: 0.18em;\n font-size: 11px;\n color: #94a3b8;\n font-weight: 600;\n margin: 0 0 8px;\n }\n\n .pass-bare-hero {\n font-size: 64px;\n }\n\n .pass-h2-above-h1 {\n font-size: 14px;\n text-transform: uppercase;\n letter-spacing: 0.18em;\n font-weight: 700;\n color: #475569;\n margin: 0 0 16px;\n font-family: 'Inter', sans-serif;\n }\n\n .pass-long-uppercase {\n text-transform: uppercase;\n letter-spacing: 0.14em;\n font-size: 12px;\n color: #475569;\n font-weight: 600;\n margin: 0 0 16px;\n }\n "],"result":false}
|
||
{"args":["\n\n \n \n\n \n \n\n "],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["\n \n \n \n "],"result":false}
|
||
{"args":["AI-NATIVE WORKFLOWS"],"result":false}
|
||
{"args":["Eyebrow Above Hero"],"result":false}
|
||
{"args":["Classic uppercase tracked div eyebrow above an 88px hero."],"result":false}
|
||
{"args":["NEW IN 2026"],"result":false}
|
||
{"args":["Span Eyebrow Above Hero"],"result":false}
|
||
{"args":["Span variant with the same uppercase + tracking + small-size shape."],"result":false}
|
||
{"args":["FEATURED"],"result":false}
|
||
{"args":["Pill Chip Above Hero"],"result":false}
|
||
{"args":["Pill-shaped chip with background, border-radius, padding."],"result":false}
|
||
{"args":["NEW"],"result":false}
|
||
{"args":["Already Uppercase Text"],"result":false}
|
||
{"args":["Text typed uppercase, no text-transform, but matching tracking and size."],"result":false}
|
||
{"args":["A VERY LONG UPPERCASE TABLE OF CONTENTS HEADER"],"result":false}
|
||
{"args":["Long Uppercase Sentence Above Hero"],"result":false}
|
||
{"args":["46-char uppercase tracked label — under the 60-char eyebrow ceiling, so it still reads as an oversized eyebrow."],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n "],"result":false}
|
||
{"args":["UPPERCASE LABEL"],"result":false}
|
||
{"args":["Eyebrow With Normal Tracking"],"result":false}
|
||
{"args":["Uppercase label above a hero but with letter-spacing: normal."],"result":false}
|
||
{"args":["CARD CAPTION"],"result":false}
|
||
{"args":["Plain body text below the caption."],"result":false}
|
||
{"args":["Uppercase Caption Far From Hero"],"result":false}
|
||
{"args":["Hero exists, but its preceding sibling is a paragraph, not the eyebrow."],"result":false}
|
||
{"args":["Hero With No Eyebrow"],"result":false}
|
||
{"args":["Bare h1 at 64px, nothing above it."],"result":false}
|
||
{"args":["SECTION HEADING"],"result":false}
|
||
{"args":["Heading Above Heading"],"result":false}
|
||
{"args":["An h2 styled like an eyebrow above an h1 — heading-tag exclusion must skip this."],"result":false}
|
||
{"args":["SECTION KICKER"],"result":false}
|
||
{"args":["Body-Sized Heading Below Eyebrow"],"result":false}
|
||
{"args":["A compact product/page heading is not an oversized marketing hero."],"result":false}
|
||
{"args":["NEXT FROM"],"result":false}
|
||
{"args":["Application Panel Heading"],"result":false}
|
||
{"args":["Context labels inside tab panels describe application state rather than decorating a marketing hero."],"result":false}
|
||
{"args":["\n \n \n \n "],"result":false}
|
||
{"args":["Hook inline ignore comments do not suppress detector findings"],"result":false}
|
||
{"args":["\n /* impeccable: ignore overused-font */\n body {\n font-family: \"Roboto\", ui-sans-serif, system-ui, sans-serif;\n margin: 0;\n padding: 48px;\n }\n\n main {\n max-width: 62ch;\n }\n "],"result":false}
|
||
{"args":["Roboto remains a detector finding"],"result":false}
|
||
{"args":["\n This fixture deliberately uses Roboto across enough visible text for\n the browser detector to report the overused-font page finding. Hook\n exceptions must live in .impeccable/hook.json instead of source comments.\n "],"result":false}
|
||
{"args":["\n The comments above look like old inline hook suppressions, but they are\n intentionally inert and should not hide the finding from headless scans.\n "],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Roboto usage sample one."],"result":false}
|
||
{"args":["Roboto usage sample two."],"result":false}
|
||
{"args":["Roboto usage sample three."],"result":false}
|
||
{"args":["Roboto usage sample four."],"result":false}
|
||
{"args":["Roboto usage sample five."],"result":false}
|
||
{"args":["Roboto usage sample six."],"result":false}
|
||
{"args":["Roboto usage sample seven."],"result":false}
|
||
{"args":["Roboto usage sample eight."],"result":false}
|
||
{"args":["Roboto usage sample nine."],"result":false}
|
||
{"args":["Roboto usage sample ten."],"result":false}
|
||
{"args":["Roboto usage sample eleven."],"result":false}
|
||
{"args":["Roboto usage sample twelve."],"result":false}
|
||
{"args":["Roboto usage sample thirteen."],"result":false}
|
||
{"args":["Roboto usage sample fourteen."],"result":false}
|
||
{"args":["Roboto usage sample fifteen."],"result":false}
|
||
{"args":["Roboto usage sample sixteen."],"result":false}
|
||
{"args":["Roboto usage sample seventeen."],"result":false}
|
||
{"args":["Roboto usage sample eighteen."],"result":false}
|
||
{"args":["Roboto usage sample nineteen."],"result":false}
|
||
{"args":["Roboto usage sample twenty."],"result":false}
|
||
{"args":["Icon-Tile-Stack — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* ─── Layout convention: two-column fixture ─────────────────────────\n Left column = patterns the rule SHOULD flag.\n Right column = legitimate patterns the rule should NOT flag.\n Each test case is annotated with a unique <h3> text so the test\n can match snippets back to expectations.\n ──────────────────────────────────────────────────────────────── */\n body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 960px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .case { margin-bottom: 24px; padding: 16px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }\n .case h3 { font-size: 18px; margin: 0 0 4px; color: #0f172a; }\n .case p { font-size: 14px; margin: 0; color: #64748b; }\n\n /* ── FLAG cases ── */\n\n /* 1: pastel-gradient icon-tile (the canonical massive-icons.html shape) */\n .icon-pastel {\n width: 80px; height: 80px;\n border-radius: 20px;\n background: linear-gradient(135deg, #eff6ff 0%, #dbeafe 100%);\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 16px;\n }\n\n /* 2: solid-color icon-tile, white svg inside (no gradient) */\n .icon-solid {\n width: 64px; height: 64px;\n border-radius: 16px;\n background: #3b82f6;\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 16px;\n }\n\n /* 3: white-bg outlined icon-tile (no chromatic background at all) */\n .icon-white {\n width: 72px; height: 72px;\n border-radius: 18px;\n background: #ffffff;\n border: 1px solid #e2e8f0;\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 16px;\n }\n\n /* 4: larger square (still inside the 32–128 range) */\n .icon-large {\n width: 96px; height: 96px;\n border-radius: 24px;\n background: #f1f5f9;\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 16px;\n }\n\n /* 5: inline emoji icon — tile contains an emoji/symbol character directly,\n not an <svg> or <i> child. This is the \"card-icon\" pattern from many\n generated demos. */\n .icon-emoji {\n width: 48px; height: 48px;\n border-radius: 12px;\n background: linear-gradient(135deg, #8b5cf6, #a855f7);\n display: flex; align-items: center; justify-content: center;\n font-size: 24px;\n margin-bottom: 16px;\n }\n\n /* ── PASS cases ── */\n\n /* a: round avatar above name (border-radius: 50% — circle, not rounded square) */\n .avatar-round {\n width: 64px; height: 64px;\n border-radius: 50%;\n background: #f59e0b;\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 16px;\n }\n\n /* b: wide thumbnail above article title (16:9, much wider than tall) */\n .thumb-wide {\n width: 200px; height: 112px;\n border-radius: 8px;\n background: #cbd5e1;\n margin-bottom: 16px;\n }\n\n /* c: inline icon next to heading (icon and h3 on the same row, NOT stacked) */\n .inline-row { display: flex; align-items: center; gap: 12px; }\n .inline-row .icon-inline {\n width: 80px; height: 80px;\n border-radius: 20px;\n background: #ddd6fe;\n display: flex; align-items: center; justify-content: center;\n flex-shrink: 0;\n }\n\n /* d: nothing above the heading at all */\n .no-icon h3 { margin-top: 0; }\n\n /* e: icon-tile that's too small (under 32px) — out of size range */\n .icon-tiny {\n width: 24px; height: 24px;\n border-radius: 6px;\n background: #93c5fd;\n display: flex; align-items: center; justify-content: center;\n margin-bottom: 16px;\n }\n\n /* f: oversized image card / hero image (over 128px) — out of size range */\n .img-huge {\n width: 200px; height: 200px;\n border-radius: 16px;\n background: #cbd5e1;\n margin-bottom: 16px;\n }\n "],"result":false}
|
||
{"args":["\n \n "],"result":false}
|
||
{"args":["Lightning Fast"],"result":false}
|
||
{"args":["Built for speed and performance from the ground up."],"result":false}
|
||
{"args":["Secure Storage"],"result":false}
|
||
{"args":["Enterprise-grade security for your peace of mind."],"result":false}
|
||
{"args":["Easy Setup"],"result":false}
|
||
{"args":["Get started in minutes with zero configuration."],"result":false}
|
||
{"args":["Powerful Analytics"],"result":false}
|
||
{"args":["Insights that drive smarter business decisions."],"result":false}
|
||
{"args":["⚡"],"result":true}
|
||
{"args":["Emoji Inline Icon"],"result":false}
|
||
{"args":["The tile contains an emoji directly, no SVG child."],"result":false}
|
||
{"args":["Sarah Chen"],"result":false}
|
||
{"args":["Product designer based in Berlin."],"result":false}
|
||
{"args":["Article Headline"],"result":false}
|
||
{"args":["A summary of an interesting article."],"result":false}
|
||
{"args":["\n \n "],"result":false}
|
||
{"args":["Inline Side By Side"],"result":false}
|
||
{"args":["Icon sits next to the heading, not above it."],"result":false}
|
||
{"args":["Plain Heading No Icon"],"result":false}
|
||
{"args":["Just a heading with body text, nothing above."],"result":false}
|
||
{"args":["Tiny Icon Above Me"],"result":false}
|
||
{"args":["The icon container is smaller than 32px."],"result":false}
|
||
{"args":["Huge Hero Image"],"result":false}
|
||
{"args":["The container is larger than the size cap."],"result":false}
|
||
{"args":["Fixture: image-backed text sampled at the pixel level by default"],"result":false}
|
||
{"args":["\n body { margin: 0; padding: 40px; background: #17150f; font-family: Arial, sans-serif; }\n /* A same-origin (data-URI) near-white image background. The analytic walk\n cannot know its pixels, so the default overlay pass must sample the image\n itself: white text on it fails, dark ink on it passes. */\n .photo-panel {\n width: 420px;\n padding: 24px;\n margin: 0 0 24px;\n background: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100'><rect width='100' height='100' fill='%23f5f2ea'/></svg>\") center / cover no-repeat;\n }\n .flag-white-on-light { color: #fdfdfd; font-size: 16px; line-height: 1.6; margin: 0; }\n .pass-ink-on-light { color: #262421; font-size: 16px; line-height: 1.6; margin: 0; }\n /* Fourteen gradient-backed decoys sit BEFORE the photo panels in DOM\n order. They are visual-contrast candidates (gradient background) but not\n image-backed; with the default 12-candidate cap they would starve the\n image-only pass to zero unless the mode filters inside the collector.\n Their ink is dark on a light gradient, so no analytic finding fires. */\n .decoy { display: inline-block; padding: 4px 8px; margin: 0 4px 6px 0; background: linear-gradient(180deg, #f2efe6, #e8e4d8); color: #24211c; font-size: 12px; }\n"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Decoy 1"],"result":false}
|
||
{"args":["Decoy 2"],"result":false}
|
||
{"args":["Decoy 3"],"result":false}
|
||
{"args":["Decoy 4"],"result":false}
|
||
{"args":["Decoy 5"],"result":false}
|
||
{"args":["Decoy 6"],"result":false}
|
||
{"args":["Decoy 7"],"result":false}
|
||
{"args":["Decoy 8"],"result":false}
|
||
{"args":["Decoy 9"],"result":false}
|
||
{"args":["Decoy 10"],"result":false}
|
||
{"args":["Decoy 11"],"result":false}
|
||
{"args":["Decoy 12"],"result":false}
|
||
{"args":["Decoy 13"],"result":false}
|
||
{"args":["Decoy 14"],"result":false}
|
||
{"args":["White text on a near-white image background is unreadable, and only pixel sampling can prove it."],"result":false}
|
||
{"args":["Dark ink on the same image background reads fine and must not flag."],"result":false}
|
||
{"args":["Italic-Serif-Display — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* ─── Layout convention: two-column fixture ─────────────────────────\n Left column = patterns the rule SHOULD flag.\n Right column = legitimate patterns the rule should NOT flag.\n Each test case is annotated with a unique <h1>/<h2>/<blockquote>\n text so the test can match snippets back to expectations.\n ──────────────────────────────────────────────────────────────── */\n body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1200px; margin: 0 auto; }\n .col h2.col-label { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .case { margin-bottom: 32px; padding: 24px; border: 1px solid #e2e8f0; border-radius: 12px; background: #fff; }\n .case p { font-size: 14px; margin: 12px 0 0; color: #64748b; }\n\n /* ── FLAG cases ── */\n\n /* 1: Fraunces 88px italic — the Lumina hero pattern */\n .hero-fraunces {\n font-family: 'Fraunces', Georgia, serif;\n font-style: italic;\n font-size: 88px;\n font-weight: 400;\n line-height: 1.05;\n margin: 0;\n }\n\n /* 2: Recoleta 64px italic */\n .hero-recoleta {\n font-family: 'Recoleta', serif;\n font-style: italic;\n font-size: 64px;\n font-weight: 400;\n margin: 0;\n }\n\n /* 3: Playfair Display 72px italic */\n .hero-playfair {\n font-family: 'Playfair Display', serif;\n font-style: italic;\n font-size: 72px;\n font-weight: 500;\n margin: 0;\n }\n\n /* 4: Unknown serif primary, but generic `serif` fallback in stack */\n .hero-unknown-serif {\n font-family: 'CustomSerifFace', serif;\n font-style: italic;\n font-size: 56px;\n font-weight: 400;\n margin: 0;\n }\n\n /* ── PASS cases ── */\n\n /* a: italic, but sans-serif primary (Karla) — italic display in a sans is rare and usually intentional */\n .pass-sans-italic {\n font-family: 'Karla', sans-serif;\n font-style: italic;\n font-size: 64px;\n font-weight: 600;\n margin: 0;\n }\n\n /* b: serif at 88px, but font-style: normal */\n .pass-roman-serif {\n font-family: 'Fraunces', Georgia, serif;\n font-style: normal;\n font-size: 88px;\n font-weight: 600;\n margin: 0;\n }\n\n /* c: italic serif blockquote at 24px (genuine pull quote, not a hero) */\n .pass-pull-quote {\n font-family: 'Fraunces', Georgia, serif;\n font-style: italic;\n font-size: 24px;\n font-weight: 400;\n margin: 0;\n color: #334155;\n }\n\n /* d: roman h1 with an italic <em> inside — v1 explicitly does NOT flag this */\n .pass-roman-h1-em-italic {\n font-family: 'Inter', sans-serif;\n font-style: normal;\n font-size: 72px;\n font-weight: 600;\n margin: 0;\n }\n .pass-roman-h1-em-italic em {\n font-family: 'Fraunces', Georgia, serif;\n font-style: italic;\n font-weight: 400;\n }\n\n /* e: italic serif h2 at 32px — under the 48px hero threshold */\n .pass-italic-serif-32 {\n font-family: 'Fraunces', Georgia, serif;\n font-style: italic;\n font-size: 32px;\n font-weight: 400;\n margin: 0;\n }\n\n /* f: baseline-clean h1 — sans-serif roman */\n .pass-h1-sans-roman {\n font-family: 'Inter', sans-serif;\n font-style: normal;\n font-size: 64px;\n font-weight: 700;\n margin: 0;\n }\n "],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Fraunces 88px italic"],"result":false}
|
||
{"args":["The Lumina hero pattern: oversized Fraunces italic display."],"result":false}
|
||
{"args":["Recoleta 64px italic"],"result":false}
|
||
{"args":["Recoleta italic at 64px, generic serif fallback."],"result":false}
|
||
{"args":["Playfair 72px italic"],"result":false}
|
||
{"args":["Playfair Display italic at hero scale."],"result":false}
|
||
{"args":["Unknown Serif Generic Fallback"],"result":false}
|
||
{"args":["Primary font unknown to the detector, but the stack ends in generic serif."],"result":false}
|
||
{"args":["Sans Italic Display"],"result":false}
|
||
{"args":["Italic at hero scale, but in a sans-serif face."],"result":false}
|
||
{"args":["Roman Serif Display"],"result":false}
|
||
{"args":["Fraunces at 88px, but font-style is normal (not italic)."],"result":false}
|
||
{"args":["Italic Serif Pull Quote"],"result":false}
|
||
{"args":["Italic serif at 24px is a legitimate pull-quote pattern, not a hero."],"result":false}
|
||
{"args":["Inline Em Inside "],"result":false}
|
||
{"args":["Roman"],"result":false}
|
||
{"args":["Roman h1 with an inline italic em — v1 deliberately does not flag this."],"result":false}
|
||
{"args":["Italic Serif at 32px"],"result":false}
|
||
{"args":["Italic serif h2 below the 48px hero threshold."],"result":false}
|
||
{"args":["h1 Sans-Serif Roman"],"result":false}
|
||
{"args":["Baseline-clean control: sans-serif, roman, hero scale."],"result":false}
|
||
{"args":["Kicker Above Heading Fixture"],"result":false}
|
||
{"args":["\n body {\n margin: 0;\n font-family: Arial, sans-serif;\n background: #f6f2ec;\n color: #171411;\n }\n main {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 32px;\n padding: 32px;\n }\n .col {\n min-width: 0;\n border: 1px solid #c9bfb2;\n padding: 24px;\n }\n .case {\n min-height: 120px;\n margin: 0 0 24px;\n padding: 16px;\n background: #fffaf3;\n }\n .kicker,\n .pass-kicker,\n .pass-kicker-caps {\n display: block;\n width: 240px;\n min-height: 16px;\n margin: 0 0 8px;\n font-size: 12px;\n line-height: 16px;\n letter-spacing: 0.11em;\n color: #5b5046;\n }\n .kicker,\n .pass-kicker-caps {\n text-transform: uppercase;\n }\n /* The wild's most common kicker recipe: 0.08em tracking at a sub-13px\n size computes to under 1px, which the old absolute floor excluded. */\n .tight-tracked-kicker {\n display: block;\n width: 240px;\n min-height: 16px;\n margin: 0 0 8px;\n font-size: 12px;\n line-height: 16px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #5b5046;\n }\n .smallcaps-kicker {\n display: block;\n width: 240px;\n min-height: 16px;\n margin: 0 0 8px;\n font-size: 12px;\n line-height: 16px;\n letter-spacing: 0.12em;\n font-variant: small-caps;\n color: #5b5046;\n }\n .untracked-caps {\n display: block;\n width: 240px;\n min-height: 16px;\n margin: 0 0 8px;\n font-size: 12px;\n line-height: 16px;\n letter-spacing: normal;\n text-transform: uppercase;\n color: #5b5046;\n }\n h1,\n h2 {\n margin: 0 0 12px;\n font-size: 32px;\n line-height: 38px;\n font-weight: 700;\n }\n h3 {\n margin: 0 0 12px;\n font-size: 24px;\n line-height: 30px;\n font-weight: 700;\n }\n h4 {\n margin: 0 0 12px;\n font-size: 20px;\n line-height: 26px;\n font-weight: 700;\n }\n .role-heading {\n margin: 0 0 12px;\n font-size: 28px;\n line-height: 34px;\n font-weight: 700;\n }\n .hero-h1 {\n margin: 0 0 12px;\n font-size: 64px;\n line-height: 70px;\n font-weight: 700;\n }\n .hero-eyebrow {\n display: block;\n width: 240px;\n min-height: 16px;\n margin: 0 0 8px;\n font-size: 12px;\n line-height: 16px;\n letter-spacing: 2px;\n text-transform: uppercase;\n color: #5b5046;\n }\n p,\n label,\n figcaption,\n a,\n li {\n font-size: 16px;\n line-height: 24px;\n }\n nav,\n form,\n figure,\n ol {\n margin: 0 0 24px;\n padding: 16px;\n border: 1px solid #d8cec0;\n min-height: 80px;\n }\n .card-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 12px;\n margin: 0 0 24px;\n }\n .suite-card {\n display: block;\n min-height: 120px;\n padding: 16px;\n border: 1px solid #d8cec0;\n background: #fff;\n }\n .taxonomy-label {\n display: block;\n margin: 0 0 8px;\n font-size: 12px;\n line-height: 16px;\n letter-spacing: 0.11em;\n text-transform: uppercase;\n color: #5b5046;\n }\n .suite-card h3 {\n font-size: 24px;\n line-height: 30px;\n }\n .stat {\n min-height: 100px;\n padding: 16px;\n background: #fffaf3;\n }\n .stat h3 {\n font-size: 40px;\n line-height: 46px;\n }\n .hidden-mockup {\n display: grid;\n gap: 12px;\n margin: 0 0 24px;\n }\n .mockup-hero {\n min-height: 120px;\n padding: 16px;\n border: 1px solid #d8cec0;\n background: #fff;\n }\n .mockup-hero h2 {\n font-size: 24px;\n line-height: 30px;\n }\n "],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Invitation protocol"],"result":false}
|
||
{"args":["\"A Single Kicker Still Flags\""],"result":false}
|
||
{"args":["One tracked uppercase label above one heading is enough. No repetition required."],"result":false}
|
||
{"args":["Output model"],"result":false}
|
||
{"args":["\"Standard Tracking Kicker\""],"result":false}
|
||
{"args":["Uppercase at 12px with 0.08em tracking is the most common authored kicker and must flag."],"result":false}
|
||
{"args":["Preview sequence"],"result":false}
|
||
{"args":["\"Kicker Above An H3\""],"result":false}
|
||
{"args":["Subsection headings get no eyebrow either."],"result":false}
|
||
{"args":["Access window"],"result":false}
|
||
{"args":["\"Kicker Above An H4\""],"result":false}
|
||
{"args":["The ban runs down to h4."],"result":false}
|
||
{"args":["Platform notes"],"result":false}
|
||
{"args":["\"Sub Hero Heading\""],"result":false}
|
||
{"args":["A 32px h1 is below hero scale, so this pair belongs to the general ban."],"result":false}
|
||
{"args":["materials briefing"],"result":false}
|
||
{"args":["\"Small Caps Kicker\""],"result":false}
|
||
{"args":["Small-caps styling is the same label in a different costume."],"result":false}
|
||
{"args":["Field studies"],"result":false}
|
||
{"args":["\"Heading Role Kicker\""],"result":false}
|
||
{"args":["Elements carrying a heading role are headings for this rule."],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Home / Journal"],"result":false}
|
||
{"args":["\"Breadcrumb Before Heading\""],"result":false}
|
||
{"args":["Home"],"result":false}
|
||
{"args":["Home / Journal / Archive"],"result":false}
|
||
{"args":["\"Breadcrumb Trail Outside Nav\""],"result":false}
|
||
{"args":["A path with separators is wayfinding, not a kicker, even without a nav wrapper."],"result":false}
|
||
{"args":["News · Jan 12, 2026"],"result":false}
|
||
{"args":["\"Dateline Above Headline\""],"result":false}
|
||
{"args":["Editorial category-and-date meta lines are content metadata."],"result":false}
|
||
{"args":["Field report"],"result":false}
|
||
{"args":["\"Editorial Card Meta\""],"result":false}
|
||
{"args":["Category labels inside an article card are structured metadata for scanning."],"result":false}
|
||
{"args":["Guest name"],"result":false}
|
||
{"args":["\"Form Heading Is Separate\""],"result":false}
|
||
{"args":["Plate study"],"result":false}
|
||
{"args":["\"Figure Caption Label\""],"result":false}
|
||
{"args":["Section 4.2"],"result":false}
|
||
{"args":["\"Limitation Of Liability\""],"result":false}
|
||
{"args":["Legal and document numbering identifies the clause; it is not decoration."],"result":false}
|
||
{"args":["§ 12.3"],"result":false}
|
||
{"args":["\"Indemnification Clause\""],"result":false}
|
||
{"args":["Statutory references keep their marker."],"result":false}
|
||
{"args":["\"Chapter Numbering Passes\""],"result":false}
|
||
{"args":["Spelled-out chapter numbering is document structure, like legal numbering."],"result":false}
|
||
{"args":["Next from"],"result":false}
|
||
{"args":["\"Application Panel Context\""],"result":false}
|
||
{"args":["Context labels inside application panels describe state, not decoration."],"result":false}
|
||
{"args":[" "],"result":false}
|
||
{"args":["Docs"],"result":false}
|
||
{"args":["\"Page Title After Nav\""],"result":false}
|
||
{"args":["A navigation landmark preceding the page title is chrome, not a kicker."],"result":false}
|
||
{"args":["\"48%\""],"result":false}
|
||
{"args":["Growth yoy"],"result":false}
|
||
{"args":["Stat callouts put the small label below the number. That direction is legal."],"result":false}
|
||
{"args":["A quiet lead-in sentence"],"result":false}
|
||
{"args":["\"Sentence Case Lead In\""],"result":false}
|
||
{"args":["Not uppercase, not small-caps: an ordinary standfirst."],"result":false}
|
||
{"args":["New arrivals"],"result":false}
|
||
{"args":["\"Untracked Caps Label\""],"result":false}
|
||
{"args":["Uppercase without tracking lacks the kicker styling signature."],"result":false}
|
||
{"args":["Archive code"],"result":false}
|
||
{"args":["\"Intentional Brand Label\""],"result":false}
|
||
{"args":["Deliberate brand systems can opt out with an explicit marker."],"result":false}
|
||
{"args":["Platform overview"],"result":false}
|
||
{"args":["\"Hero Owned By Hero Rule\""],"result":false}
|
||
{"args":["Hero-scale h1 eyebrows stay with hero-eyebrow-chip; this rule stands down."],"result":false}
|
||
{"args":["\n \n \n \n "],"result":false}
|
||
{"args":["Suite"],"result":false}
|
||
{"args":["\"Garden Suite\""],"result":false}
|
||
{"args":["Category labels inside sibling cards are metadata for comparison."],"result":false}
|
||
{"args":["\"Sea Loft\""],"result":false}
|
||
{"args":["The label is useful because the user is scanning sibling cards."],"result":false}
|
||
{"args":["Step 01"],"result":false}
|
||
{"args":["\"Step Indicator\""],"result":false}
|
||
{"args":["Step indicators in ordered flows are sequence markers."],"result":false}
|
||
{"args":["Amalfi Coast"],"result":false}
|
||
{"args":["\"Mockup Hero Variant One\""],"result":false}
|
||
{"args":["\n \n \n \n \n \n"],"result":false}
|
||
{"args":["Layout Anti-Patterns — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; background: #f9fafb; padding: 24px; margin: 0; color: #111827; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1280px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .col h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 24px 0 8px; color: #64748b; }\n "],"result":false}
|
||
{"args":["\n \n\n \n\n \n \n\n \n \n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["Nested cards (cardocalypse)"],"result":false}
|
||
{"args":["Outer Card"],"result":false}
|
||
{"args":["Inner Card"],"result":false}
|
||
{"args":["Card inside card."],"result":false}
|
||
{"args":["Level 1"],"result":false}
|
||
{"args":["Level 2"],"result":false}
|
||
{"args":["Level 3 — nesting inception."],"result":false}
|
||
{"args":["Outer (CSS)"],"result":false}
|
||
{"args":["Inner card via CSS."],"result":false}
|
||
{"args":["shadcn-style Outer Card"],"result":false}
|
||
{"args":["Another shadcn Card nested inside."],"result":false}
|
||
{"args":["shadcn Card sub-components (not nested cards)"],"result":false}
|
||
{"args":["Card Title"],"result":false}
|
||
{"args":["Card description goes here."],"result":false}
|
||
{"args":["CardContent — a sub-section, not a nested card. No shadow, no border-radius of its own."],"result":false}
|
||
{"args":["Action"],"result":false}
|
||
{"args":["Card with form inputs"],"result":false}
|
||
{"args":["Settings"],"result":false}
|
||
{"args":["\n \n \n \n \n "],"result":false}
|
||
{"args":["Select option"],"result":false}
|
||
{"args":["Card with code block"],"result":false}
|
||
{"args":["Installation"],"result":false}
|
||
{"args":["npm install @acme/sdk"],"result":false}
|
||
{"args":["Card with badges"],"result":false}
|
||
{"args":["Tags"],"result":false}
|
||
{"args":["React"],"result":false}
|
||
{"args":["TypeScript"],"result":false}
|
||
{"args":["Tailwind"],"result":false}
|
||
{"args":["Card with image"],"result":false}
|
||
{"args":["Project Preview"],"result":false}
|
||
{"args":["Image with rounded corners and shadow."],"result":false}
|
||
{"args":["Pricing cards (intentionally similar)"],"result":false}
|
||
{"args":["Free"],"result":false}
|
||
{"args":["$0"],"result":false}
|
||
{"args":["Pro"],"result":false}
|
||
{"args":["$29"],"result":false}
|
||
{"args":["Enterprise"],"result":false}
|
||
{"args":["Custom"],"result":false}
|
||
{"args":["Legitimate Border Patterns — Should NOT Flag"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; background: #f9fafb; padding: 2rem; color: #111827; }\n h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }\n h2 { font-size: 1.125rem; margin: 2rem 0 0.75rem; color: #6b7280; }\n .demo { max-width: 36rem; margin-bottom: 1.5rem; }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n"],"result":false}
|
||
{"args":["Legitimate Border Patterns"],"result":false}
|
||
{"args":["Every border here is a well-established web pattern. None should be flagged."],"result":false}
|
||
{"args":["Blockquotes"],"result":false}
|
||
{"args":["\n \"Design is not just what it looks like and feels like. Design is how it works.\"\n "],"result":false}
|
||
{"args":["Sidebar Navigation (Active State)"],"result":false}
|
||
{"args":["Dashboard"],"result":false}
|
||
{"args":["Projects"],"result":false}
|
||
{"args":["Form Validation"],"result":false}
|
||
{"args":["Email"],"result":false}
|
||
{"args":["Please enter a valid email address."],"result":false}
|
||
{"args":["Timeline"],"result":false}
|
||
{"args":["Order placed"],"result":false}
|
||
{"args":["March 15, 2026"],"result":false}
|
||
{"args":["Shipped"],"result":false}
|
||
{"args":["March 16, 2026"],"result":false}
|
||
{"args":["Delivered"],"result":false}
|
||
{"args":["Expected March 18"],"result":false}
|
||
{"args":["Code Diff Highlighting"],"result":false}
|
||
{"args":["\n"],"result":false}
|
||
{"args":["\n\n"],"result":false}
|
||
{"args":["- const old = getValue();"],"result":false}
|
||
{"args":["+ const value = getNewValue();"],"result":false}
|
||
{"args":[" return value;"],"result":false}
|
||
{"args":["Tab Navigation"],"result":false}
|
||
{"args":["Overview"],"result":false}
|
||
{"args":["Analytics"],"result":false}
|
||
{"args":["Data Table"],"result":false}
|
||
{"args":["Name"],"result":false}
|
||
{"args":["Revenue"],"result":false}
|
||
{"args":["Acme Corp"],"result":false}
|
||
{"args":["$1.2M"],"result":false}
|
||
{"args":["Globex"],"result":false}
|
||
{"args":["$850K"],"result":false}
|
||
{"args":["Alert Banner"],"result":false}
|
||
{"args":["\n Your trial expires in 3 days. \n "],"result":false}
|
||
{"args":["Warning:"],"result":false}
|
||
{"args":["Upgrade now"],"result":false}
|
||
{"args":["Anti-Patterns From Linked Stylesheet"],"result":false}
|
||
{"args":["\n body {\n margin: 0;\n padding: 24px;\n background: #f9fafb;\n color: #111827;\n font: 14px/1.5 system-ui, sans-serif;\n }\n\n h1 {\n max-width: 1080px;\n margin: 0 auto 8px;\n font-size: 34px;\n line-height: 1.05;\n }\n\n p.intro {\n max-width: 1080px;\n margin: 0 auto 24px;\n color: #4b5563;\n }\n\n .grid {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n gap: 32px;\n max-width: 1080px;\n margin: 0 auto;\n }\n\n .col h2 {\n margin: 0 0 14px;\n color: #475569;\n font-size: 13px;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n\n .cards {\n display: grid;\n gap: 14px;\n }\n\n h3 {\n margin: 0 0 4px;\n font-size: 15px;\n line-height: 1.25;\n }\n\n .cards p {\n margin: 0;\n color: #4b5563;\n }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n\n"],"result":false}
|
||
{"args":["Linked Stylesheet Anti-Patterns"],"result":false}
|
||
{"args":["\n These elements get their anti-pattern styles from an external CSS file.\n Regex-only scanning misses these — only computed style analysis catches them.\n "],"result":false}
|
||
{"args":["\n \n\n \n "],"result":false}
|
||
{"args":["External side-tab class"],"result":false}
|
||
{"args":["border-left plus border-radius from a linked stylesheet."],"result":false}
|
||
{"args":["External top accent class"],"result":false}
|
||
{"args":["border-top plus border-radius from a linked stylesheet."],"result":false}
|
||
{"args":["\n \n\n \n\n \n "],"result":false}
|
||
{"args":["External clean card"],"result":false}
|
||
{"args":["Uniform 1px border from the linked stylesheet."],"result":false}
|
||
{"args":["External neutral side rule"],"result":false}
|
||
{"args":["Thick side border, but the color is structural gray."],"result":false}
|
||
{"args":["External square top rule"],"result":false}
|
||
{"args":["Heavy top border with no rounded card corners."],"result":false}
|
||
{"args":["Side-Tab with Modern Color Formats (oklch/lab/lch/hsl/hwb)"],"result":false}
|
||
{"args":["\n /* Two-column fixture: left col = should-flag, right col = should-pass.\n The side-tab snippet doesn't embed identifying text, so the test\n asserts finding counts against the structural ids (#flag-N / #pass-N)\n by reading the element via data attributes in the check path. Since\n that isn't available to the Node fixture runner, we instead place a\n unique number of FLAG elements (each a distinct side-tab) and verify\n the side-tab count. */\n\n /* CSS custom properties — variables used by the var()-case fixtures.\n jsdom's CSSOM drops any border shorthand containing var(), which used\n to make every side-tab built with CSS variables invisible to the\n detector. These cases regression-test the fallback pass that parses\n border shorthands directly off the rule (not the computed style). */\n :root {\n --brand: #87a8ff;\n --warn: #8a6a2f;\n --terracotta: #aa674d;\n --line: #e5e7eb; /* neutral gray — must NOT trigger */\n --rule: #f3f4f6; /* near-white — must NOT trigger */\n }\n\n body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; background: #fafafa; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1120px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .case { padding: 12px 16px; margin-bottom: 16px; }\n .case h3 { font-size: 14px; margin: 0 0 4px; }\n .case p { font-size: 13px; margin: 0; color: #64748b; }\n\n /* ── FLAG cases: colored side-tab borders using modern color formats ── */\n\n /* 1: oklch border-left + border-radius (the canonical real-page bug) */\n #flag-oklch-1 {\n width: 400px;\n background: oklch(100% 0 0);\n border-radius: 4px;\n border-left: 3px solid oklch(65% 0.12 250);\n }\n\n /* 2: oklch mixed — neutral 1px border all around + colored 4px left +\n border-radius (mirrors .medication-card from the reproducer) */\n #flag-oklch-2 {\n width: 400px;\n background: oklch(100% 0 0);\n border: 1px solid oklch(80% 0.05 250);\n border-left: 4px solid oklch(75% 0.15 45);\n border-radius: 4px;\n }\n\n /* 3: oklch alert pattern — colored 4px border-left + border-radius */\n #flag-oklch-3 {\n width: 400px;\n background: oklch(98% 0.01 60);\n border-left: 4px solid oklch(75% 0.18 45);\n border-radius: 4px;\n }\n\n /* 4: oklab border-left + border-radius */\n #flag-oklab-1 {\n width: 400px;\n background: #ffffff;\n border-radius: 6px;\n border-left: 4px solid oklab(60% 0.1 0.15);\n }\n\n /* 5: lch border-left + border-radius */\n #flag-lch-1 {\n width: 400px;\n background: #ffffff;\n border-radius: 6px;\n border-left: 4px solid lch(55% 60 250);\n }\n\n /* 6: lab border-left + border-radius */\n #flag-lab-1 {\n width: 400px;\n background: #ffffff;\n border-radius: 6px;\n border-left: 4px solid lab(50% 40 -30);\n }\n\n /* 7: card-shaped <label> with colored oklch side border + radius\n (the checklist-item pattern from preop-portal reproducer) */\n label#flag-label-oklch {\n display: grid;\n grid-template-columns: auto 1fr;\n width: 400px;\n padding: 16px;\n background: oklch(100% 0 0);\n border-radius: 4px;\n border-left: 3px solid oklch(65% 0.12 250);\n cursor: pointer;\n }\n\n /* 8: card-shaped <label> with colored rgb side border + radius —\n proves the fix is not oklch-specific */\n label#flag-label-rgb {\n display: block;\n width: 400px;\n padding: 16px;\n background: #ffffff;\n border-radius: 6px;\n border-left: 4px solid #3b82f6;\n cursor: pointer;\n }\n\n /* 9: CSS variable in shorthand — the canonical real-page pattern from\n evals (e.g. 09-physics-study: border-left:8px solid var(--brand-2)).\n jsdom silently drops this entire declaration, so the computed-style\n path used to miss it. */\n #flag-var-shorthand {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 5px solid var(--brand);\n }\n\n /* 10: CSS variable + neutral 1px all-around border — mirrors the\n medication-card pattern with a colored accent on the left. */\n #flag-var-mixed {\n width: 400px;\n background: #ffffff;\n border: 1px solid var(--line);\n border-left: 4px solid var(--warn);\n border-radius: 4px;\n }\n\n /* 11: CSS variable shorthand, right side. Ensures the fallback works\n for both inline axes, not just border-left. */\n #flag-var-right {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-right: 4px solid var(--terracotta);\n }\n\n /* 12: card-shaped <label> with var() side border + radius —\n the checklist-row shape, built with tokens. */\n label#flag-label-var {\n display: block;\n width: 400px;\n padding: 16px;\n background: #ffffff;\n border-radius: 6px;\n border-left: 4px solid var(--brand);\n cursor: pointer;\n }\n\n /* ── PASS cases: legitimate or neutral borders, must NOT fire ── */\n\n /* 1: oklch chroma ~0 — true neutral gray border should not fire */\n #pass-oklch-neutral {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 3px solid oklch(80% 0 0);\n }\n\n /* 2: oklch low chroma (0.01) — still perceptually neutral */\n #pass-oklch-nearneutral {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 3px solid oklch(75% 0.01 250);\n }\n\n /* 3: oklch colored border all around (not one-sided) — no side-tab */\n #pass-oklch-allsides {\n width: 400px;\n background: #ffffff;\n border: 3px solid oklch(65% 0.12 250);\n border-radius: 4px;\n }\n\n /* 4: oklch colored 1px border-left (too thin to qualify) */\n #pass-oklch-thin {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 1px solid oklch(65% 0.12 250);\n }\n\n /* 5: lab neutral (chroma near zero) — must not fire */\n #pass-lab-neutral {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 3px solid lab(50% 0 0);\n }\n\n /* 6: lch neutral (chroma 0) */\n #pass-lch-neutral {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 3px solid lch(50% 0 0);\n }\n\n /* 7: plain inline form label — no border, no padding, no radius.\n Must NOT be flagged for anything. */\n label#pass-label-plain {\n font-size: 14px;\n font-weight: 500;\n color: #374151;\n }\n\n /* 8: form label with a small neutral gray left border — neutral color\n should not fire the colored side-tab rule. */\n label#pass-label-neutral-border {\n display: block;\n width: 400px;\n padding: 8px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 3px solid #e5e7eb;\n }\n\n /* 9: inline label inside a form row — default inline layout,\n no borders, must not fire. */\n label#pass-label-inline {\n font-size: 13px;\n color: #6b7280;\n margin-right: 8px;\n }\n\n /* 10: label with 1px colored left border — too thin to qualify\n (matches the existing 1px thin rule for divs). */\n label#pass-label-thin {\n display: block;\n width: 400px;\n padding: 8px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 1px solid #3b82f6;\n }\n\n /* 11: CSS variable resolving to a neutral gray — must NOT fire,\n even though the border shorthand contains var(). */\n #pass-var-neutral {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 3px solid var(--line);\n }\n\n /* 12: thin (1px) var-based left border — under the side-tab width\n threshold even when resolved. */\n #pass-var-thin {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 1px solid var(--brand);\n }\n\n /* 13: var-based uniform border on all four sides. Not a side-tab. */\n #pass-var-allsides {\n width: 400px;\n background: #ffffff;\n border: 3px solid var(--brand);\n border-radius: 4px;\n }\n\n /* 14: neutral 1px oklch right divider — context-bar hairline, not a side-tab. */\n #pass-oklch-neutral-right-divider {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-right: 1px solid oklch(92% 0 0 / 0.12);\n }\n "],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["oklch checklist"],"result":false}
|
||
{"args":["oklch border-left 3px + radius"],"result":false}
|
||
{"args":["oklch medication card"],"result":false}
|
||
{"args":["neutral all + colored left 4px"],"result":false}
|
||
{"args":["oklch alert"],"result":false}
|
||
{"args":["warm border-left 4px + radius"],"result":false}
|
||
{"args":["oklab card"],"result":false}
|
||
{"args":["oklab border-left 4px + radius"],"result":false}
|
||
{"args":["lch card"],"result":false}
|
||
{"args":["lch border-left 4px + radius"],"result":false}
|
||
{"args":["lab card"],"result":false}
|
||
{"args":["lab border-left 4px + radius"],"result":false}
|
||
{"args":["Card-shaped label with oklch side border"],"result":false}
|
||
{"args":["Card-shaped label with rgb side border"],"result":false}
|
||
{"args":["var() shorthand"],"result":false}
|
||
{"args":["border-left 5px + var(--brand)"],"result":false}
|
||
{"args":["var() mixed"],"result":false}
|
||
{"args":["neutral all + colored var left"],"result":false}
|
||
{"args":["var() right side"],"result":false}
|
||
{"args":["border-right 4px + var(--terracotta)"],"result":false}
|
||
{"args":["Card-shaped label with var() side border"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["oklch neutral"],"result":false}
|
||
{"args":["chroma 0 gray"],"result":false}
|
||
{"args":["oklch near-neutral"],"result":false}
|
||
{"args":["chroma 0.01"],"result":false}
|
||
{"args":["oklch all sides"],"result":false}
|
||
{"args":["uniform border"],"result":false}
|
||
{"args":["oklch thin"],"result":false}
|
||
{"args":["1px too thin"],"result":false}
|
||
{"args":["lab neutral"],"result":false}
|
||
{"args":["chroma 0"],"result":false}
|
||
{"args":["lch neutral"],"result":false}
|
||
{"args":["Email address"],"result":false}
|
||
{"args":["Label with neutral gray left border"],"result":false}
|
||
{"args":["Name:"],"result":false}
|
||
{"args":["Label with 1px colored left border (too thin)"],"result":false}
|
||
{"args":["var() neutral"],"result":false}
|
||
{"args":["--line resolves to gray"],"result":false}
|
||
{"args":["var() thin"],"result":false}
|
||
{"args":["1px too thin to qualify"],"result":false}
|
||
{"args":["var() all sides"],"result":false}
|
||
{"args":["uniform, not a side-tab"],"result":false}
|
||
{"args":["oklch neutral right divider"],"result":false}
|
||
{"args":["1px neutral context-bar hairline"],"result":false}
|
||
{"args":["Motion Anti-Patterns — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* ─── Two-column fixture convention ─────────────────────────────\n Left column = should flag. Right column = should pass.\n Tests assert (a) every flag-column rule fires and (b) the total\n finding count matches the number of flag cases (no false\n positives leaking from the pass column).\n ──────────────────────────────────────────────────────────── */\n body { font-family: system-ui, sans-serif; background: #f9fafb; padding: 24px; margin: 0; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1200px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .col h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 24px 0 8px; color: #64748b; }\n .demo { padding: 14px 16px; background: white; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); margin-bottom: 10px; }\n .demo h4 { font-weight: 600; font-size: 13px; margin: 0; }\n .demo p { font-size: 12px; color: #6b7280; margin: 4px 0 0; }\n\n /* ── FLAG: bounce / elastic easing ── */\n @keyframes bounce-keyframe {\n 0%, 100% { transform: translateY(0); }\n 50% { transform: translateY(-10px); }\n }\n .bounce-animation { animation: bounce-keyframe 1s infinite; }\n .elastic-transition { transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); }\n\n /* ── FLAG: layout property transitions ── */\n .width-transition { transition: width 0.3s ease; }\n .height-transition { transition: height 0.4s ease-out; }\n .padding-transition { transition: padding 0.2s linear; }\n .margin-transition { transition: margin 0.3s ease-in; }\n .max-height-transition { transition: max-height 0.5s ease; }\n .multi-layout-transition { transition: width 0.3s ease, height 0.3s ease; }\n .mixed-transition { transition: width 0.3s ease, opacity 0.3s ease; }\n .transition-property-width { transition-property: width; transition-duration: 0.3s; }\n\n /* ── PASS: good easing ── */\n @keyframes fade-in-keyframe {\n from { opacity: 0; transform: translateY(8px); }\n to { opacity: 1; transform: translateY(0); }\n }\n .fade-in-good { animation: fade-in-keyframe 0.4s cubic-bezier(0.16, 1, 0.3, 1); }\n .ease-out-quart { transition: transform 0.4s cubic-bezier(0.25, 1, 0.5, 1); }\n .ease-out-expo { transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); }\n\n /* ── PASS: safe transitions ── */\n .transform-transition { transition: transform 0.3s ease-out; }\n .opacity-transition { transition: opacity 0.2s ease; }\n .color-transition { transition: color 0.15s ease, background-color 0.15s ease; }\n .shadow-transition { transition: box-shadow 0.2s ease; }\n .all-transition { transition: all 0.3s ease; }\n .multi-safe-transition { transition: transform 0.3s ease, opacity 0.3s ease, box-shadow 0.2s ease; }\n "],"result":false}
|
||
{"args":["\n \n\n \n \n \n\n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Bounce / elastic easing"],"result":false}
|
||
{"args":["CSS bounce animation"],"result":false}
|
||
{"args":["animation: bounce 1s infinite"],"result":false}
|
||
{"args":["Elastic cubic-bezier"],"result":false}
|
||
{"args":["cubic-bezier(0.68, -0.55, 0.265, 1.55)"],"result":false}
|
||
{"args":["Layout property transitions"],"result":false}
|
||
{"args":["transition: width"],"result":false}
|
||
{"args":["Animating width causes layout thrash."],"result":false}
|
||
{"args":["transition: height"],"result":false}
|
||
{"args":["Animating height causes layout thrash."],"result":false}
|
||
{"args":["transition: padding"],"result":false}
|
||
{"args":["Animating padding causes layout thrash."],"result":false}
|
||
{"args":["transition: margin"],"result":false}
|
||
{"args":["Animating margin causes layout thrash."],"result":false}
|
||
{"args":["transition: max-height"],"result":false}
|
||
{"args":["Use grid-template-rows instead."],"result":false}
|
||
{"args":["transition: width, height"],"result":false}
|
||
{"args":["Multiple layout properties."],"result":false}
|
||
{"args":["transition: width, opacity"],"result":false}
|
||
{"args":["Layout property mixed with OK property."],"result":false}
|
||
{"args":["transition-property: width"],"result":false}
|
||
{"args":["Longhand form."],"result":false}
|
||
{"args":["\n \n\n \n \n \n \n\n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Good easing"],"result":false}
|
||
{"args":["Smooth fade in"],"result":false}
|
||
{"args":["animation: fadeIn with exponential ease-out"],"result":false}
|
||
{"args":["Ease-out quart"],"result":false}
|
||
{"args":["cubic-bezier(0.25, 1, 0.5, 1) — smooth deceleration"],"result":false}
|
||
{"args":["Ease-out expo"],"result":false}
|
||
{"args":["cubic-bezier(0.16, 1, 0.3, 1) — natural feel"],"result":false}
|
||
{"args":["Safe transitions (transform / opacity / color only)"],"result":false}
|
||
{"args":["transition: transform"],"result":false}
|
||
{"args":["Transform is GPU-accelerated and safe."],"result":false}
|
||
{"args":["transition: opacity"],"result":false}
|
||
{"args":["Opacity is GPU-accelerated and safe."],"result":false}
|
||
{"args":["transition: color, background-color"],"result":false}
|
||
{"args":["Color transitions are paint-only, no layout."],"result":false}
|
||
{"args":["transition: box-shadow"],"result":false}
|
||
{"args":["Shadow transitions are paint-only."],"result":false}
|
||
{"args":["transition: all"],"result":false}
|
||
{"args":["Too common to flag — usually paired with transform/opacity."],"result":false}
|
||
{"args":["transition: transform, opacity, box-shadow"],"result":false}
|
||
{"args":["Multiple safe properties combined."],"result":false}
|
||
{"args":["Side-Tab with CSS Named Colors (purple/rebeccapurple/crimson/teal)"],"result":false}
|
||
{"args":["\n /* Two-column fixture: left col = should-flag, right col = should-pass.\n Regression for issue #359: `border-left: 4px solid purple` in a .html\n file never fired side-tab because the static cascade's shorthand color\n extraction only recognized 9 named colors — every other spec name\n (purple, rebeccapurple, crimson, teal, ...) was dropped from the\n shorthand, leaving the side at the default black, which reads as\n neutral and silently skips the check. The same declaration in a .css\n file was flagged by the regex engine, so the two engines disagreed. */\n\n :root {\n --accent: orange; /* named color behind a var() — must resolve and flag */\n }\n\n body { font-family: system-ui, sans-serif; margin: 0; padding: 24px; background: #fafafa; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1120px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .case { padding: 12px 16px; margin-bottom: 16px; }\n .case h3 { font-size: 14px; margin: 0 0 4px; }\n .case p { font-size: 13px; margin: 0; color: #64748b; }\n\n /* ── FLAG cases: colored side-tab borders using CSS named colors ── */\n\n /* 1: the exact reproducer from issue #359 — named purple in a <style>\n rule with background + padding + radius */\n #flag-named-purple {\n width: 400px;\n background: #f6f6f6;\n padding: 16px;\n border-left: 4px solid purple;\n border-radius: 8px;\n }\n\n /* 2: rebeccapurple — a longer name that contains another color name\n (\"purple\") as a substring; must match whole-token. Width 5px so the\n finding snippet is unique to this case. */\n #flag-named-rebecca {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 5px solid rebeccapurple;\n }\n\n /* 3: horizontal stripe variant — named crimson riding the top edge */\n #flag-named-crimson-top {\n width: 400px;\n background: #ffffff;\n border-top: 4px solid crimson;\n }\n\n /* 4: named teal, 3px, no radius — the bare w >= 3 arm */\n #flag-named-teal {\n width: 400px;\n background: #ffffff;\n border-left: 3px solid teal;\n }\n\n /* 5: named color behind a var() in the shorthand. Width 6px so the\n finding snippet is unique to this case. */\n #flag-named-var {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 6px solid var(--accent);\n }\n\n /* ── PASS cases: neutral named colors and non-side-tab shapes ── */\n\n /* 1: dimgray — chromatic-looking name, neutral value; must NOT fire */\n #pass-named-dimgray {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 4px solid dimgray;\n }\n\n /* 2: gainsboro — light neutral named color */\n #pass-named-gainsboro {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 3px solid gainsboro;\n }\n\n /* 3: named black side border — neutral, not a colored stripe */\n #pass-named-black {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 4px solid black;\n }\n\n /* 4: 1px named purple — too thin to qualify */\n #pass-named-thin {\n width: 400px;\n background: #ffffff;\n border-radius: 4px;\n border-left: 1px solid purple;\n }\n\n /* 5: uniform named purple border on all four sides — not a side-tab */\n #pass-named-allsides {\n width: 400px;\n background: #ffffff;\n border: 3px solid purple;\n border-radius: 4px;\n }\n "],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["named purple"],"result":false}
|
||
{"args":["border-left 4px solid purple + radius"],"result":false}
|
||
{"args":["rebeccapurple"],"result":false}
|
||
{"args":["border-left 4px + radius"],"result":false}
|
||
{"args":["crimson top stripe"],"result":false}
|
||
{"args":["border-top 4px, horizontal variant"],"result":false}
|
||
{"args":["named teal"],"result":false}
|
||
{"args":["border-left 3px, no radius"],"result":false}
|
||
{"args":["var() to named"],"result":false}
|
||
{"args":["border-left 4px solid var(--accent)"],"result":false}
|
||
{"args":["inline named purple"],"result":false}
|
||
{"args":["style attribute, issue #359 case (a); width 7px keeps the snippet unique"],"result":false}
|
||
{"args":["dimgray"],"result":false}
|
||
{"args":["neutral named color"],"result":false}
|
||
{"args":["gainsboro"],"result":false}
|
||
{"args":["light neutral named color"],"result":false}
|
||
{"args":["named black"],"result":false}
|
||
{"args":["neutral side border"],"result":false}
|
||
{"args":["1px purple"],"result":false}
|
||
{"args":["too thin to qualify"],"result":false}
|
||
{"args":["uniform purple"],"result":false}
|
||
{"args":["all four sides, not a stripe"],"result":false}
|
||
{"args":["Nav CTA contrast construction sweep"],"result":false}
|
||
{"args":["\n :root{\n --v3bg: #f0b64e;\n --v3fg: #cfd9e9;\n }\n body{ margin:0; font-family: Georgia, serif; color:#1c2430; background:#ffffff; }\n header{ display:flex; align-items:center; gap:2rem; padding:1rem 2rem; background:#12213a; }\n .nav{ display:flex; gap:1.5rem; align-items:center; }\n .nav a{ font-size:0.9rem; text-decoration:none; }\n .cta{ display:inline-block; padding:0.6em 1.1em; border-radius:6px; font-weight:600; }\n\n /* v1: solid bg + own low-contrast color, both on the button class */\n a.cta.v1{ background:#e8b84b; color:#c9d6ea; }\n\n /* v2: specificity cascade — .nav a (0,1,1) beats .v2 (0,1,0) for color */\n .nav a{ color:#b7c6de; }\n .v2{ background:#e5a93f; color:#26313f; }\n\n /* v3: var() indirection for both colors */\n a.cta.v3{ background:var(--v3bg); color:var(--v3fg); }\n\n /* v4: gradient background, low-contrast text against every stop */\n a.cta.v4{ background:linear-gradient(90deg,#f2b854,#e8a83e); color:#d3dcec; }\n\n /* v5: pseudo-element paints the surface; button itself transparent */\n a.cta.v5{ position:relative; background:transparent; color:#d0daea; }\n .v5::before{ content:\"\"; position:absolute; inset:0; border-radius:6px; background:#efb352; z-index:-1; }\n\n /* v6: color inherited from header, button only sets bg */\n header{ color:#ccd8ea; }\n .v6{ background:#eab04a; }\n\n /* v7: alpha-composited own bg over the dark header */\n a.cta.v7{ background:rgb(240 178 74 / 0.85); color:#d2dcec; }\n\n /* v8: oklch serialization for both sides */\n a.cta.v8{ background:oklch(0.82 0.15 75); color:oklch(0.80 0.05 258); }\n\n /* v9: background-image url only (no computable color) + low contrast vs header */\n a.cta.v9{ background-image:url(\"data:image/gif;base64,R0lGODlhAQABAIAAAP7uSwAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==\"); color:#d5deed; }\n"],"result":false}
|
||
{"args":["Acme"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Product"],"result":false}
|
||
{"args":["Start now v1"],"result":false}
|
||
{"args":["Start now v2"],"result":false}
|
||
{"args":["Start now v3"],"result":false}
|
||
{"args":["Start now v4"],"result":false}
|
||
{"args":["Start now v5"],"result":false}
|
||
{"args":["Start now v6"],"result":false}
|
||
{"args":["Start now v7"],"result":false}
|
||
{"args":["Start now v8"],"result":false}
|
||
{"args":["Start now v9"],"result":false}
|
||
{"args":["A page that exists to host a header"],"result":false}
|
||
{"args":["The paragraphs below the header keep the document honest so the\n detector treats this as a full page rather than a fragment. Nothing here should trip any rule."],"result":false}
|
||
{"args":["Second paragraph of ordinary body copy with comfortable measure and\n line height, dark ink on paper."],"result":false}
|
||
{"args":["\n\n\n\n\n\n\n\n\n\n\n\n"],"result":false}
|
||
{"args":["This is a fairly long document title that easily exceeds twenty characters"],"result":false}
|
||
{"args":["\n /* This CSS comment is deliberately longer than twenty characters so the style\n element's text node clears the hasDirectText / textLen > 20 gate. */\n body { font-size: 10px; font-family: system-ui, sans-serif; }\n .rendered-body { font-size: 10px; }\n .rendered-nav-link { font-size: 9px; }\n .none-block { display: none; font-size: 10px; }\n .hidden-block { visibility: hidden; font-size: 10px; }\n"],"result":false}
|
||
{"args":["window.__ANALYTICS__ = { id: 1, ts: 0 }; console.log(\"an analytics payload string that is clearly longer than twenty characters\");"],"result":false}
|
||
{"args":["{\"@context\":\"https://schema.org\",\"@type\":\"Product\",\"name\":\"A product name long enough to exceed twenty characters\"}"],"result":false}
|
||
{"args":["Please enable JavaScript in your browser to view this page content correctly."],"result":false}
|
||
{"args":["\n\n\n\n\n\n\n\n\n\n\n\n\n\n"],"result":false}
|
||
{"args":["Hidden display-none block of body text long enough to exceed twenty characters."],"result":false}
|
||
{"args":["This is real rendered body copy at 10px that is definitely long enough to flag."],"result":false}
|
||
{"args":["Rendered Nav Link"],"result":false}
|
||
{"args":["Numbered Section Labels Fixture"],"result":false}
|
||
{"args":["\n body {\n font-family: Georgia, serif;\n font-size: 16px;\n margin: 0;\n padding: 40px;\n background: #ffffff;\n color: #222222;\n }\n .column { width: 520px; padding: 20px; }\n h2 { font-size: 28px; margin: 0 0 12px; }\n h3 { font-size: 22px; margin: 0 0 10px; }\n section { margin-bottom: 48px; }\n\n /* ── flag column styles ── */\n .sec-index {\n font-family: monospace;\n font-size: 11px;\n font-weight: 700;\n color: #b45309;\n display: block;\n margin-bottom: 8px;\n }\n .sec-index-micro {\n font-family: monospace;\n font-size: 12px;\n font-weight: 700;\n letter-spacing: 2px;\n color: #b45309;\n display: block;\n margin-bottom: 8px;\n }\n\n /* ── pass column styles ── */\n .big-index {\n font-size: 16px;\n font-weight: 700;\n font-family: monospace;\n display: block;\n }\n .plain-note {\n font-size: 12px;\n font-weight: 400;\n letter-spacing: 0;\n color: #666666;\n display: block;\n }\n .step-label {\n font-family: monospace;\n font-size: 11px;\n font-weight: 700;\n display: block;\n }\n .card {\n border: 1px solid #d4d4d4;\n border-radius: 8px;\n padding: 16px;\n }\n "],"result":false}
|
||
{"args":["\n\n \n \n\n \n \n\n"],"result":false}
|
||
{"args":["\n\n \n\n \n\n \n\n \n\n "],"result":false}
|
||
{"args":["01"],"result":false}
|
||
{"args":["Alpha ships first"],"result":false}
|
||
{"args":["Direct previous-sibling label: tiny, mono, bold, zero-padded index."],"result":false}
|
||
{"args":["02"],"result":false}
|
||
{"args":["Beta earns trust"],"result":false}
|
||
{"args":["Second section repeating the same index scaffold."],"result":false}
|
||
{"args":["03"],"result":false}
|
||
{"args":["Gamma holds the line"],"result":false}
|
||
{"args":["Label precedes the wrapper; the heading leads the wrapper."],"result":false}
|
||
{"args":["04 / ROLLOUT"],"result":false}
|
||
{"args":["Delta closes the loop"],"result":false}
|
||
{"args":["Index joined to a tracked micro-label by a separator glyph."],"result":false}
|
||
{"args":["\n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n "],"result":false}
|
||
{"args":["05"],"result":false}
|
||
{"args":["Epsilon reads large"],"result":false}
|
||
{"args":["16px index is a deliberate display device, not a micro label."],"result":false}
|
||
{"args":["12 · minute read"],"result":false}
|
||
{"args":["Zeta stays plain"],"result":false}
|
||
{"args":["Parses as an index but carries no micro-label styling: regular weight, neutral color, no tracking, serif body face."],"result":false}
|
||
{"args":["Step 6"],"result":false}
|
||
{"args":["Eta walks steps"],"result":false}
|
||
{"args":["Word-first label is not a numeric index."],"result":false}
|
||
{"args":["7"],"result":false}
|
||
{"args":["Theta counts casually"],"result":false}
|
||
{"args":["A bare unpadded single digit is ordinary list numbering."],"result":false}
|
||
{"args":["08"],"result":false}
|
||
{"args":["Iota lives in a list"],"result":false}
|
||
{"args":["09"],"result":false}
|
||
{"args":["Kappa lives in a list too"],"result":false}
|
||
{"args":["10"],"result":false}
|
||
{"args":["Lambda sits in a card"],"result":false}
|
||
{"args":["Per-card indices are item numbering, not section scaffolding."],"result":false}
|
||
{"args":["11"],"result":false}
|
||
{"args":["Mu tracks progress"],"result":false}
|
||
{"args":["Numbered section marker fixture"],"result":false}
|
||
{"args":["\n .fixture-grid {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 32px;\n }\n .case {\n margin: 0 0 18px;\n padding: 16px;\n border: 1px solid #d7d0c2;\n border-radius: 8px;\n }\n .marker {\n display: block;\n font: 700 12px/1.1 ui-monospace, SFMono-Regular, Menlo, monospace;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: #7c5c00;\n }\n .code-like::before {\n content: \"01 02 03\";\n }\n "],"result":false}
|
||
{"args":["\n \n \n \n \n \n "],"result":false}
|
||
{"args":["Strategy"],"result":false}
|
||
{"args":["Prototype"],"result":false}
|
||
{"args":["Launch"],"result":false}
|
||
{"args":["04"],"result":false}
|
||
{"args":["Measure"],"result":false}
|
||
{"args":["Readable date"],"result":false}
|
||
{"args":["Updated June 14, 2026."],"result":false}
|
||
{"args":["Statistic cards"],"result":false}
|
||
{"args":["Revenue grew 18%, margin reached 24%, and churn fell to 3%."],"result":false}
|
||
{"args":["CSS generated content"],"result":false}
|
||
{"args":["Generated numbers in CSS should not count as body copy."],"result":false}
|
||
{"args":["Inline SVG coordinates"],"result":false}
|
||
{"args":["Script constants"],"result":false}
|
||
{"args":["\n const colorStops = ['01', '02', '03', '04'];\n const luminance = 0.2126 + 0.7152 + 0.0722;\n const size = '11.5px';\n "],"result":false}
|
||
{"args":["Implementation constants in script blocks are invisible."],"result":false}
|
||
{"args":["Overlay Positioning Edge Cases"],"result":false}
|
||
{"args":["\n body {\n font-family: system-ui, sans-serif;\n background: #f9fafb;\n padding: 2rem;\n max-width: 900px;\n margin: 0 auto;\n }\n h1 { font-size: 2rem; margin-bottom: 0.5rem; }\n h2 { font-size: 1.125rem; margin: 2.5rem 0 0.75rem; color: #6b7280; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; }\n .scenario { margin-bottom: 2rem; padding: 1rem; border: 1px dashed #d1d5db; border-radius: 8px; }\n .scenario-label { font-size: 0.75rem; color: #6b7280; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; }\n\n /* ============================================\n 1. TRANSFORM ANCESTOR\n position:absolute overlays on body use document coords,\n but getBoundingClientRect is viewport-relative.\n Transforms on ancestors create new stacking contexts.\n ============================================ */\n .transform-container {\n transform: translateY(0);\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .transform-rotate {\n transform: rotate(0deg);\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .transform-scale {\n transform: scale(1);\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .transform-nested-outer {\n transform: translateX(0);\n padding: 1rem;\n background: #e5e7eb;\n border-radius: 8px;\n }\n .transform-nested-inner {\n transform: translateY(0);\n padding: 1rem;\n background: #d1d5db;\n border-radius: 8px;\n }\n\n /* ============================================\n 2. CLOSED <details>\n Elements inside collapsed details may report\n a bounding rect but aren't visually present.\n ============================================ */\n details { margin-bottom: 1rem; }\n summary { cursor: pointer; font-weight: 600; }\n\n /* ============================================\n 3. STICKY / FIXED POSITIONING\n Sticky elements change their containing block\n behavior during scroll.\n ============================================ */\n .sticky-container {\n height: 200px;\n overflow-y: auto;\n border: 1px solid #d1d5db;\n border-radius: 8px;\n }\n .sticky-header {\n position: sticky;\n top: 0;\n background: #1f2937;\n color: white;\n padding: 0.5rem 1rem;\n z-index: 10;\n }\n .sticky-content {\n padding: 1rem;\n }\n\n /* ============================================\n 4. OVERFLOW HIDDEN\n Elements that extend beyond an overflow:hidden\n parent are visually clipped but still in the DOM.\n ============================================ */\n .overflow-hidden-container {\n overflow: hidden;\n height: 60px;\n border: 1px solid #d1d5db;\n border-radius: 8px;\n padding: 0.5rem 1rem;\n }\n\n /* ============================================\n 5. ABSOLUTE / RELATIVE POSITIONING\n Elements that are offset from their normal flow\n position via top/left.\n ============================================ */\n .relative-offset {\n position: relative;\n top: 20px;\n left: 40px;\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .absolute-container {\n position: relative;\n height: 120px;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .absolute-child {\n position: absolute;\n bottom: 10px;\n right: 10px;\n }\n\n /* ============================================\n 6. FLEXBOX / GRID CENTERING\n Elements centered via flex/grid may have\n unexpected positions vs. document flow.\n ============================================ */\n .flex-center {\n display: flex;\n justify-content: center;\n align-items: center;\n height: 120px;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .grid-center {\n display: grid;\n place-items: center;\n height: 120px;\n background: #f3f4f6;\n border-radius: 8px;\n }\n\n /* ============================================\n 7. WILL-CHANGE / CONTAIN / FILTER\n These CSS properties create new containing\n blocks, similar to transforms.\n ============================================ */\n .will-change-container {\n will-change: transform;\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .contain-container {\n contain: layout;\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .filter-container {\n filter: brightness(1);\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .backdrop-filter-container {\n backdrop-filter: blur(0px);\n padding: 1rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n\n /* ============================================\n 8. MARGIN COLLAPSE / NEGATIVE MARGINS\n Elements with collapsed or negative margins\n can shift from expected position.\n ============================================ */\n .negative-margin-container {\n padding: 2rem;\n background: #f3f4f6;\n border-radius: 8px;\n }\n .negative-margin-child {\n margin-top: -1rem;\n margin-left: -1rem;\n }\n\n /* ============================================\n Anti-pattern triggers (tiny-text, cramped, ai-color)\n used across scenarios.\n ============================================ */\n .tiny { font-size: 10px; color: #374151; line-height: 1.4; }\n .cramped-box { border: 1px solid #d1d5db; padding: 3px; font-size: 14px; border-radius: 4px; }\n .ai-btn { background: linear-gradient(135deg, #8b5cf6, #6366f1); color: white; padding: 8px 16px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; }\n .side-tab-card { border-left: 4px solid #8b5cf6; padding: 1rem; background: white; border-radius: 0 8px 8px 0; }\n "],"result":false}
|
||
{"args":["\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"],"result":false}
|
||
{"args":["Each scenario places a detectable anti-pattern inside a layout context that can cause overlay misalignment."],"result":false}
|
||
{"args":["1. Transform Ancestors"],"result":false}
|
||
{"args":["1a. translateY(0) container"],"result":false}
|
||
{"args":["This tiny text is inside a transform: translateY(0) container. The overlay should frame this text precisely."],"result":false}
|
||
{"args":["1b. rotate(0deg) container"],"result":false}
|
||
{"args":["Tiny text inside a transform: rotate(0deg) container."],"result":false}
|
||
{"args":["1c. scale(1) container"],"result":false}
|
||
{"args":["Cramped text inside a transform: scale(1) container."],"result":false}
|
||
{"args":["1d. Nested transforms"],"result":false}
|
||
{"args":["Tiny text nested inside two levels of transformed parents."],"result":false}
|
||
{"args":["1e. Transform + absolute child"],"result":false}
|
||
{"args":["Absolutely positioned tiny text inside transformed parent."],"result":false}
|
||
{"args":["2. Closed Details"],"result":false}
|
||
{"args":["2a. Closed details with anti-pattern inside"],"result":false}
|
||
{"args":["Click to expand (closed by default)"],"result":false}
|
||
{"args":["This tiny text is hidden inside a closed details element. No overlay should appear for it."],"result":false}
|
||
{"args":["Cramped text also hidden inside closed details."],"result":false}
|
||
{"args":["2b. Open details with anti-pattern inside"],"result":false}
|
||
{"args":["This details is open"],"result":false}
|
||
{"args":["This tiny text IS visible because details is open. Overlay should frame it correctly."],"result":false}
|
||
{"args":["2c. Nested details (outer open, inner closed)"],"result":false}
|
||
{"args":["Outer details (open)"],"result":false}
|
||
{"args":["Some visible content."],"result":false}
|
||
{"args":["Inner details (closed)"],"result":false}
|
||
{"args":["Hidden tiny text inside nested closed details."],"result":false}
|
||
{"args":["2d. Transform inside closed details"],"result":false}
|
||
{"args":["Closed with transform inside"],"result":false}
|
||
{"args":["Tiny text inside transform inside closed details. Double trouble."],"result":false}
|
||
{"args":["3. Sticky and Fixed Positioning"],"result":false}
|
||
{"args":["3a. Sticky header inside scrollable container"],"result":false}
|
||
{"args":["Tiny text in a sticky header."],"result":false}
|
||
{"args":["Scroll this container to test sticky behavior."],"result":false}
|
||
{"args":["More content to enable scrolling."],"result":false}
|
||
{"args":["Even more content here."],"result":false}
|
||
{"args":["Cramped text below the sticky header."],"result":false}
|
||
{"args":["Additional content."],"result":false}
|
||
{"args":["More filler text."],"result":false}
|
||
{"args":["3b. Fixed footer (stays at bottom of viewport)"],"result":false}
|
||
{"args":["The fixed footer at the bottom of the page has tiny text. Its overlay should stay fixed too."],"result":false}
|
||
{"args":["Tiny text in a fixed footer -- overlay should track this on scroll."],"result":false}
|
||
{"args":["4. Overflow Hidden"],"result":false}
|
||
{"args":["4a. Content clipped by overflow:hidden"],"result":false}
|
||
{"args":["This paragraph is visible."],"result":false}
|
||
{"args":["This tiny text is below the overflow cutoff, clipped but still in DOM."],"result":false}
|
||
{"args":["This content is also clipped away."],"result":false}
|
||
{"args":["4b. overflow:hidden + transform ancestor"],"result":false}
|
||
{"args":["Visible inside transform + overflow."],"result":false}
|
||
{"args":["Clipped tiny text inside a transformed overflow:hidden container."],"result":false}
|
||
{"args":["5. Position Offsets"],"result":false}
|
||
{"args":["5a. Relative position with top/left offset"],"result":false}
|
||
{"args":["This tiny text is shifted via position:relative + top/left offset."],"result":false}
|
||
{"args":["5b. Absolute child in relative parent"],"result":false}
|
||
{"args":["Normal flow content at top."],"result":false}
|
||
{"args":["Absolutely positioned tiny text at bottom-right."],"result":false}
|
||
{"args":["5c. Negative top offset"],"result":false}
|
||
{"args":["Tiny text pulled upward via negative top offset."],"result":false}
|
||
{"args":["6. Flex and Grid Centering"],"result":false}
|
||
{"args":["6a. Flex-centered anti-pattern"],"result":false}
|
||
{"args":["Centered tiny text inside a flex container."],"result":false}
|
||
{"args":["6b. Grid-centered anti-pattern"],"result":false}
|
||
{"args":["Cramped text inside a grid-centered container."],"result":false}
|
||
{"args":["6c. Flex with transform"],"result":false}
|
||
{"args":["AI Gradient Button in Flex + Transform"],"result":false}
|
||
{"args":["7. Containing Block Creators"],"result":false}
|
||
{"args":["7a. will-change: transform"],"result":false}
|
||
{"args":["Tiny text inside a will-change: transform container."],"result":false}
|
||
{"args":["7b. contain: layout"],"result":false}
|
||
{"args":["Tiny text inside a contain: layout container."],"result":false}
|
||
{"args":["7c. filter: brightness(1)"],"result":false}
|
||
{"args":["Tiny text inside a filter container (creates containing block)."],"result":false}
|
||
{"args":["7d. backdrop-filter"],"result":false}
|
||
{"args":["Tiny text inside a backdrop-filter container."],"result":false}
|
||
{"args":["8. Margin Collapse and Negative Margins"],"result":false}
|
||
{"args":["8a. Negative margin shifting element position"],"result":false}
|
||
{"args":["Tiny text pulled out of its parent via negative margins."],"result":false}
|
||
{"args":["9. Combined Edge Cases"],"result":false}
|
||
{"args":["9a. Transform + sticky + overflow"],"result":false}
|
||
{"args":["Tiny text in sticky header inside transform inside scrollable overflow."],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Scroll content line 1."],"result":false}
|
||
{"args":["Scroll content line 2."],"result":false}
|
||
{"args":["Scroll content line 3."],"result":false}
|
||
{"args":["Cramped box scrolled below sticky."],"result":false}
|
||
{"args":["Scroll content line 4."],"result":false}
|
||
{"args":["Scroll content line 5."],"result":false}
|
||
{"args":["9b. Closed details + transform + flex"],"result":false}
|
||
{"args":["Closed combo scenario"],"result":false}
|
||
{"args":["Triple-nested: closed details > flex > transform > tiny text."],"result":false}
|
||
{"args":["9c. Grid + absolute + transform"],"result":false}
|
||
{"args":["Absolute inside grid inside transform: overlays must track all three contexts."],"result":false}
|
||
{"args":["Absolute cramped box at bottom-right of grid."],"result":false}
|
||
{"args":["9d. Overflow hidden + absolute breakout"],"result":false}
|
||
{"args":["Visible content."],"result":false}
|
||
{"args":["Absolute-positioned tiny text that breaks out below overflow:hidden."],"result":false}
|
||
{"args":["9e. Side-tab card inside transform + relative offset"],"result":false}
|
||
{"args":["\n This card has a side-tab border accent and is offset inside a transform container.\n "],"result":false}
|
||
{"args":["Oversized H1 browser fixture"],"result":false}
|
||
{"args":["\n body { font-family: Georgia, serif; margin: 0; color: #1a1a1a; background: #fff; }\n main { display: grid; gap: 48px; padding: 32px; }\n h1 { margin: 0; line-height: 1.04; }\n .flag-dominating-h1 {\n max-width: 1040px;\n font-size: 104px;\n }\n .pass-contained-large-h1 {\n max-width: 1120px;\n font-size: 83px;\n }\n "],"result":false}
|
||
{"args":["A sweeping product headline that sprawls across the whole viewport and keeps going"],"result":false}
|
||
{"args":["The missing design vocabulary for agents."],"result":false}
|
||
{"args":["Oversized H1 fixture"],"result":false}
|
||
{"args":["\n body { font-family: Georgia, serif; margin: 0; color: #1a1a1a; background: #fff; }\n .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 24px; }\n .col { padding: 16px; }\n h1 { margin: 0 0 24px; line-height: 1.05; }\n "],"result":false}
|
||
{"args":["A sweeping product headline that runs on far too long to sit at this size"],"result":false}
|
||
{"args":["Everything your whole team needs to launch quickly and finally rest at night"],"result":false}
|
||
{"args":["Bold."],"result":false}
|
||
{"args":["Ship faster"],"result":false}
|
||
{"args":["A perfectly ordinary headline that is reasonably long but set at a sane, readable size"],"result":false}
|
||
{"args":["Card description with close font sizes."],"result":false}
|
||
{"args":["Pseudo-element side stripe fixture"],"result":false}
|
||
{"args":["\n :root {\n --ok: oklch(0.78 0.145 155);\n --line: rgba(0, 0, 0, 0.08);\n }\n\n /* FLAG: classic ::before accent stripe — absolute, full height, left edge,\n chromatic background resolved through a custom property. */\n .card-stripe { position: relative; border-radius: 12px; }\n .card-stripe::before {\n content: \"\";\n position: absolute;\n left: 0; top: 0; bottom: 0;\n width: 5px;\n background: var(--ok);\n }\n\n /* FLAG: right-edge variant using inset shorthand + literal color. */\n .row-stripe { position: relative; }\n .row-stripe::after {\n content: \"\";\n position: absolute;\n inset: 0 0 0 auto;\n width: 4px;\n background-color: #3b82f6;\n }\n\n /* PASS: 1px neutral hairline divider (below width threshold + neutral). */\n .col + .col::before {\n content: \"\";\n position: absolute;\n left: 0; top: 0; bottom: 0;\n width: 1px;\n background: var(--line);\n }\n\n /* PASS: neutral 4px rail (timeline spine) — chromatic gate rejects it. */\n .timeline::before {\n content: \"\";\n position: absolute;\n left: 0; top: 0; bottom: 0;\n width: 4px;\n background: rgb(210, 210, 214);\n }\n\n /* PASS: blockquote decoration is exempt in prose contexts. */\n blockquote::before {\n content: \"\";\n position: absolute;\n left: 0; top: 0; bottom: 0;\n width: 4px;\n background: #d97706;\n }\n\n /* PASS: toggle knob — offset from the edge, not a stripe. */\n .switch::before {\n content: \"\";\n position: absolute;\n left: 2px; top: 2px;\n width: 10px; height: 10px;\n border-radius: 50%;\n background: #3b82f6;\n }\n\n /* PASS: wide decorative panel, not a stripe. */\n .panel::before {\n content: \"\";\n position: absolute;\n left: 0; top: 0; bottom: 0;\n width: 40px;\n background: #3b82f6;\n }\n\n /* PASS: horizontal underline accent (not full height, not edge-anchored\n vertically). */\n .heading::after {\n content: \"\";\n position: absolute;\n left: 0; bottom: 0;\n width: 6px; height: 3px;\n background: #3b82f6;\n }\n"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n"],"result":false}
|
||
{"args":["Card with pseudo stripe"],"result":false}
|
||
{"args":["Row with right stripe"],"result":false}
|
||
{"args":["One"],"result":false}
|
||
{"args":["Two"],"result":false}
|
||
{"args":["Quoted text"],"result":false}
|
||
{"args":["Panel"],"result":false}
|
||
{"args":["Heading"],"result":false}
|
||
{"args":["Pulsing status dot fixture"],"result":false}
|
||
{"args":["\n :root { --dot: 8px; }\n\n /* FLAG: tiny circular live dot on an infinite opacity pulse. */\n .live-dot {\n width: 7px; height: 7px;\n border-radius: 50%;\n background: #22c55e;\n animation: pulse 2.4s ease-out infinite;\n }\n @keyframes pulse {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0.3; }\n }\n\n /* FLAG: box-shadow ripple variant, var-sized, pill radius. */\n .status .dot {\n width: var(--dot); height: var(--dot);\n border-radius: 999px;\n background: #ef4444;\n animation: ripple 1.8s linear infinite;\n }\n @keyframes ripple {\n 0% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); }\n 100% { box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); }\n }\n\n /* FLAG: longhand animation-name + iteration-count. */\n .beacon {\n width: 10px; height: 10px;\n border-radius: 50%;\n background: #f59e0b;\n animation-name: pulse;\n animation-duration: 2s;\n animation-iteration-count: infinite;\n }\n\n /* PASS: rotation-only spinner — same size class, must never flag. */\n .spinner {\n width: 14px; height: 14px;\n border-radius: 50%;\n border: 2px solid #e5e7eb;\n border-top-color: #3b82f6;\n animation: spin 0.8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n\n /* PASS: named pulse but the local keyframes only rotate. */\n .fake-pulse {\n width: 8px; height: 8px;\n border-radius: 50%;\n animation: pulse-ring 1s linear infinite;\n }\n @keyframes pulse-ring { to { transform: rotate(180deg); } }\n\n /* PASS: large breathing card — not a dot. */\n .breathing-card {\n width: 240px; height: 120px;\n border-radius: 16px;\n animation: pulse 3s ease-in-out infinite;\n }\n\n /* PASS: finite attention pulse (no infinite). */\n .nudge {\n width: 8px; height: 8px;\n border-radius: 50%;\n animation: pulse 0.6s ease-out 3;\n }\n\n /* PASS: square badge, not circular. */\n .square-badge {\n width: 12px; height: 12px;\n border-radius: 2px;\n animation: pulse 2s infinite;\n }\n\n /* FLAG (error): header logo dot whose size and animation live in\n separate rule blocks, animation added inside a matching media query,\n with a reduced-motion reset that must not mask the default. */\n .rec-mark {\n width: 10px; height: 10px;\n border-radius: 50%;\n background: #dc2626;\n }\n @media (prefers-reduced-motion: no-preference) {\n .rec-mark { animation: pulse 2.4s ease-in-out infinite; }\n }\n @media (prefers-reduced-motion: reduce) {\n .rec-mark { animation: none; }\n }\n"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n \n \n"],"result":false}
|
||
{"args":[" Brand"],"result":false}
|
||
{"args":[" Online"],"result":false}
|
||
{"args":["Card"],"result":false}
|
||
{"args":["Quality (Typography & Readability) — Should Flag vs Should Pass"],"result":false}
|
||
{"args":["\n /* Two-column fixture: left = should flag, right = should pass.\n Covers the typography-quality rules that need real browser layout\n (line-length, tight-leading, tiny-text, justified-text, all-caps-body,\n wide-tracking, skipped-heading). All are browser-only — see\n tests/detect-antipatterns-browser.test.mjs for assertions. */\n body { font-family: system-ui, sans-serif; background: #fafafa; padding: 24px; margin: 0; color: #0f172a; line-height: 1.6; }\n .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 1280px; margin: 0 auto; }\n .col h2 { font-size: 14px; text-transform: uppercase; letter-spacing: 0.05em; margin: 0 0 16px; color: #475569; }\n .col h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; margin: 24px 0 8px; color: #64748b; }\n .case { margin-bottom: 16px; padding: 16px; background: white; border: 1px solid #e2e8f0; border-radius: 8px; }\n .case-label { display: block; font-size: 12px; color: #64748b; margin-bottom: 6px; font-style: italic; }\n\n /* ── FLAG: typography quality issues ── */\n\n /* Line length: 12px body text fits >85 chars/line in a ~528px column.\n 12px is not below the tiny-text 12px floor (rule fires at <12px). */\n .long-lines {\n font-size: 12px;\n line-height: 1.6;\n }\n\n /* Tight leading */\n .tight-leading {\n font-size: 16px;\n line-height: 1.0;\n max-width: 40em;\n }\n\n /* Tiny body text */\n .tiny-text {\n font-size: 10px;\n line-height: 1.6;\n max-width: 40em;\n }\n .tiny-text-11 {\n font-size: 11px;\n line-height: 1.6;\n max-width: 40em;\n }\n\n /* Justified text without hyphens */\n .justified-no-hyphens {\n text-align: justify;\n max-width: 30em;\n font-size: 16px;\n line-height: 1.6;\n }\n\n /* All-caps long body */\n .all-caps-body {\n text-transform: uppercase;\n font-size: 16px;\n max-width: 40em;\n line-height: 1.6;\n }\n\n /* Wide letter spacing on body */\n .wide-tracking-body {\n letter-spacing: 0.15em;\n font-size: 16px;\n max-width: 40em;\n line-height: 1.6;\n }\n\n /* ── PASS: comfortable typography ── */\n\n .good-measure {\n max-width: 65ch;\n font-size: 16px;\n line-height: 1.6;\n }\n\n .good-leading {\n font-size: 16px;\n line-height: 1.6;\n max-width: 40em;\n }\n\n .good-text-size {\n font-size: 16px;\n line-height: 1.6;\n max-width: 40em;\n }\n\n /* Justified text WITH hyphens — fine */\n .justified-with-hyphens {\n text-align: justify;\n hyphens: auto;\n -webkit-hyphens: auto;\n max-width: 30em;\n font-size: 16px;\n line-height: 1.6;\n }\n\n /* Short label in all-caps (passes — under 30 chars) */\n .label-caps {\n text-transform: uppercase;\n font-size: 12px;\n font-weight: 600;\n letter-spacing: 0.1em;\n color: #475569;\n }\n\n /* Wide tracking on a short uppercase label (passes — both conditions excluded) */\n .label-tracking {\n letter-spacing: 0.15em;\n text-transform: uppercase;\n font-size: 12px;\n font-weight: 600;\n color: #475569;\n }\n\n .mock-terminal-title,\n .mock-terminal-meta,\n .mock-diff-line {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n color: #475569;\n line-height: 1.5;\n }\n .mock-terminal-title { font-size: 11px; }\n .mock-terminal-meta { font-size: 10.8px; }\n .mock-diff-line { font-size: 11.84px; }\n "],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["Line length too long"],"result":false}
|
||
{"args":["no max-width on a paragraph"],"result":false}
|
||
{"args":["This paragraph has no max-width constraint at all, which means on a wide monitor or ultrawide display, each line of text can stretch to 150 or even 200 characters wide. Research consistently shows that line lengths beyond 75 characters significantly reduce reading speed and comprehension. The eye has to travel too far to find the beginning of the next line, causing readers to lose their place. A simple max-width of 65ch to 75ch on the paragraph or its container would fix this entirely."],"result":false}
|
||
{"args":["Tight line height"],"result":false}
|
||
{"args":["line-height: 1.0"],"result":false}
|
||
{"args":["This paragraph has a line-height of 1.0, which means the lines are touching. Multi-line body text needs breathing room between lines for readability. A line-height of 1.5 to 1.7 is generally recommended for body text."],"result":false}
|
||
{"args":["Tiny body text"],"result":false}
|
||
{"args":["10px body text"],"result":false}
|
||
{"args":["This body text is only 10px. While this might be fine for a disclaimer or legal footnote, it's too small for primary content that users need to actually read. Aim for at least 14px for body content, 16px is ideal."],"result":false}
|
||
{"args":["Justified text without hyphens"],"result":false}
|
||
{"args":["text-align: justify, no hyphens: auto"],"result":false}
|
||
{"args":["This paragraph uses text-align: justify, which forces each line to stretch to fill the full width. Without hyphenation support, this creates uneven gaps between words known as \"rivers of white space\" that flow vertically through the text. Left-aligned text is almost always more readable on the web."],"result":false}
|
||
{"args":["All-caps body text"],"result":false}
|
||
{"args":["text-transform: uppercase on a long passage"],"result":false}
|
||
{"args":["This entire paragraph is in uppercase via text-transform. While all-caps works for short labels, headings, or navigation items, longer body text in uppercase is significantly harder to read because we lose the word shape cues that come from ascenders and descenders in mixed-case text."],"result":false}
|
||
{"args":["Wide letter spacing on body text"],"result":false}
|
||
{"args":["letter-spacing: 0.15em on body text"],"result":false}
|
||
{"args":["This body text has letter-spacing: 0.15em applied to it. While subtle tracking adjustments can improve readability for headings or all-caps text, adding significant letter spacing to body text actually makes it harder to read by disrupting natural character groupings."],"result":false}
|
||
{"args":["Skipped heading levels"],"result":false}
|
||
{"args":["h1 → h3 (missing h2)"],"result":false}
|
||
{"args":["Top heading"],"result":false}
|
||
{"args":["Skips straight to h3"],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["Comfortable line length"],"result":false}
|
||
{"args":["max-width: 65ch on the paragraph"],"result":false}
|
||
{"args":["This paragraph has a max-width of 65ch, keeping the line length comfortable for reading. The eye can easily track from the end of one line to the beginning of the next without losing its place."],"result":false}
|
||
{"args":["Comfortable line height"],"result":false}
|
||
{"args":["line-height: 1.6"],"result":false}
|
||
{"args":["This paragraph has a line-height of 1.6, which gives multi-line text plenty of room to breathe and improves the rhythm of the page."],"result":false}
|
||
{"args":["Comfortable body text size"],"result":false}
|
||
{"args":["16px body text"],"result":false}
|
||
{"args":["This is 16px body text — the recommended baseline for comfortable reading on modern displays."],"result":false}
|
||
{"args":["Justified text with hyphens"],"result":false}
|
||
{"args":["text-align: justify + hyphens: auto"],"result":false}
|
||
{"args":["When justified text is paired with hyphens: auto, the browser can break long words across lines, eliminating the rivers of white space and making the justification look intentional rather than awkward."],"result":false}
|
||
{"args":["Short label in all-caps"],"result":false}
|
||
{"args":["short label, uppercase"],"result":false}
|
||
{"args":["Featured"],"result":false}
|
||
{"args":["Wide tracking on a short uppercase label"],"result":false}
|
||
{"args":["letter-spacing on a short uppercase label"],"result":false}
|
||
{"args":["Beta"],"result":false}
|
||
{"args":["Proper heading hierarchy"],"result":false}
|
||
{"args":["h1 → h2 → h3"],"result":false}
|
||
{"args":["Second level"],"result":false}
|
||
{"args":["Third level"],"result":false}
|
||
{"args":["Small mockup metadata"],"result":false}
|
||
{"args":["terminal/diff metadata at tiny sizes"],"result":false}
|
||
{"args":["/impeccable polish : scanning codebase"],"result":false}
|
||
{"args":["12 files inspected · 4 visual issues queued"],"result":false}
|
||
{"args":["+ Replace generic button label with action-specific copy"],"result":false}
|
||
{"args":["impeccable"],"result":false}
|
||
{"args":["radial-spotlight-glow fixture"],"result":false}
|
||
{"args":["\n /* jsdom / static-html engine does no layout: every case declares explicit\n pixel width + height so the size gate can read them. */\n body { margin: 0; background: #0b0d13; }\n .grid { display: flex; flex-wrap: wrap; gap: 24px; padding: 24px; }\n .case { color: #e6e6e6; }\n\n /* ───────── SHOULD FLAG: decorative low-opacity chromatic spotlights ───────── */\n\n /* The exact declaration that evaded the detector: soft blue spotlight fading\n to transparent, painted on a large mobile hero. */\n .flag-hero-blue {\n width: 360px; height: 560px;\n background: radial-gradient(circle at 52% 38%, rgba(80,111,255,0.26), transparent 44%);\n }\n /* Violet accent wash on a full section. */\n .flag-section-violet {\n width: 900px; height: 600px;\n background-image: radial-gradient(circle, rgba(139,92,246,0.30), transparent 60%);\n }\n /* Cyan overlay-style glow (the pseudo-element overlay div reflex). */\n .flag-overlay-cyan {\n width: 760px; height: 480px;\n background: radial-gradient(ellipse at top, rgba(34,211,238,0.22), transparent 70%);\n }\n /* Single glow drawn with two low-alpha stops of one hue fading out. */\n .flag-two-stop-rose {\n width: 640px; height: 400px;\n background: radial-gradient(circle, rgba(255,90,120,0.28) 0%, rgba(255,90,120,0.12) 45%, transparent 75%);\n }\n /* Same tell written as an 8-digit hex (#506fff at alpha 0x3d ≈ 0.24). */\n .flag-hex-alpha {\n width: 700px; height: 460px;\n background: radial-gradient(circle, #506fff3d, transparent 50%);\n }\n\n /* ───────── SHOULD PASS: real backgrounds, small elements, neutrals, rich art ───────── */\n\n /* Opaque radial background — a real surface, not a glow (no transparent end). */\n .pass-opaque-bg {\n width: 800px; height: 520px;\n background: radial-gradient(circle, #1e3a8a, #0f172a);\n }\n /* Small badge: chromatic low-alpha radial glow, but tiny. */\n .pass-badge {\n width: 40px; height: 40px;\n background: radial-gradient(circle, rgba(80,111,255,0.30), transparent);\n }\n /* Avatar-scale light: the subject IS a small light, exempt by size. */\n .pass-avatar-light {\n width: 72px; height: 72px;\n background: radial-gradient(circle, rgba(255,196,64,0.35), transparent 70%);\n }\n /* Neutral near-black vignette — grayscale, not the accent-glow tell. */\n .pass-neutral-vignette {\n width: 800px; height: 520px;\n background: radial-gradient(circle, rgba(0,0,0,0.40), transparent 80%);\n }\n /* Neutral near-white vignette — grayscale, exempt. */\n .pass-white-vignette {\n width: 820px; height: 540px;\n background: radial-gradient(ellipse, rgba(255,255,255,0.35), transparent 70%);\n }\n /* Rich three-color radial composition — real color structure, opaque, no fade. */\n .pass-rich-composition {\n width: 800px; height: 520px;\n background: radial-gradient(circle, #ff0080 0%, #7928ca 45%, #0070f3 100%);\n }\n /* Rich composition that does fade out, but its color stops are opaque. */\n .pass-rich-transparent {\n width: 780px; height: 500px;\n background: radial-gradient(circle, rgba(255,0,128,0.90) 0%, rgba(121,40,202,0.85) 50%, transparent);\n }\n /* Opaque center glow (the radial-halo case, alpha ≥ 0.45) — not this rule. */\n .pass-opaque-center {\n width: 800px; height: 520px;\n background: radial-gradient(circle, rgba(80,111,255,0.85), transparent);\n }\n /* Low-alpha chromatic wash, but LINEAR, not radial. */\n .pass-linear-wash {\n width: 800px; height: 520px;\n background: linear-gradient(120deg, rgba(80,111,255,0.28), transparent 60%);\n }\n"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n\n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Repeated Container Text Fixture"],"result":false}
|
||
{"args":["\n body {\n font-family: Georgia, serif;\n font-size: 16px;\n margin: 0;\n padding: 40px;\n background: #f5f5f4;\n color: #222222;\n }\n .card {\n background: #ffffff;\n border: 1px solid #d6d3d1;\n border-radius: 10px;\n padding: 20px;\n width: 360px;\n margin-bottom: 32px;\n }\n .shadow-card {\n background: #ffffff;\n border-radius: 10px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);\n padding: 20px;\n width: 360px;\n margin-bottom: 32px;\n }\n h2 { font-size: 22px; margin: 0 0 10px; }\n .big-word { font-size: 30px; font-weight: 700; color: #b91c1c; }\n .tag { font-size: 12px; text-transform: uppercase; color: #b91c1c; }\n .row { padding: 6px 0; border-bottom: 1px solid #eeeeee; }\n .status { font-size: 13px; color: #57534e; }\n td, th { padding: 6px 10px; font-size: 14px; }\n "],"result":false}
|
||
{"args":["\n\n \n\n \n \n\n \n \n\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n \n \n\n \n \n\n \n \n\n"],"result":false}
|
||
{"args":["Museumplein departure"],"result":false}
|
||
{"args":["Suspended"],"result":false}
|
||
{"args":["This service is until further notice."],"result":false}
|
||
{"args":["\n \n \n \n \n \n "],"result":false}
|
||
{"args":["Evening show"],"result":false}
|
||
{"args":["Unavailable"],"result":false}
|
||
{"args":["Currently at this venue."],"result":false}
|
||
{"args":["Deploy history"],"result":false}
|
||
{"args":["build 12"],"result":false}
|
||
{"args":["Rolled back"],"result":false}
|
||
{"args":["build 13"],"result":false}
|
||
{"args":["build 14"],"result":false}
|
||
{"args":["Departures"],"result":false}
|
||
{"args":["On schedule"],"result":false}
|
||
{"args":["Sections"],"result":false}
|
||
{"args":["Overview page"],"result":false}
|
||
{"args":["Toggles"],"result":false}
|
||
{"args":["Off"],"result":false}
|
||
{"args":["Scores"],"result":false}
|
||
{"args":["2026"],"result":false}
|
||
{"args":["Standby mode"],"result":false}
|
||
{"args":["Availability"],"result":false}
|
||
{"args":["Open slot"],"result":false}
|
||
{"args":["Corner case"],"result":false}
|
||
{"args":["Rescheduled"],"result":false}
|
||
{"args":["Working reveal-on-scroll fixture (browser-only pass case)"],"result":false}
|
||
{"args":["\n html { scroll-behavior: smooth; }\n body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; background: #fff; }\n main { max-width: 640px; margin: 0 auto; padding: 32px 24px; }\n section { margin: 0 0 480px; }\n .reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.4s ease-out, transform 0.4s ease-out; }\n .reveal.in-view { opacity: 1; transform: none; }\n .menu { display: none; }\n "],"result":false}
|
||
{"args":["\n \n\n \n"],"result":false}
|
||
{"args":["\n \n \n\n \n \n \n \n \n\n \n \n \n "],"result":false}
|
||
{"args":["Visible hero headline stays readable"],"result":false}
|
||
{"args":["Identical markup to the broken fixture, but the script parses and runs, so every\n reveal section becomes visible once the detector's reveal sweep scrolls it into view.\n Nothing on this page may produce a script-error or content-hidden finding."],"result":false}
|
||
{"args":["A menu that only opens on demand"],"result":false}
|
||
{"args":["Another menu entry with text"],"result":false}
|
||
{"args":["Hidden attribute content that never counts against the page."],"result":false}
|
||
{"args":["Decorative aria-hidden copy that also stays out of the denominator."],"result":false}
|
||
{"args":["\n const sections = document.querySelectorAll('.reveal');\n const io = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) { entry.target.classList.add('in-view'); io.unobserve(entry.target); }\n }\n }, { threshold: 0.1 });\n sections.forEach((el) => io.observe(el));\n "],"result":false}
|
||
{"args":["Fixture: data-impeccable-ignore scoped waivers"],"result":false}
|
||
{"args":["\n body { margin: 0; padding: 40px; background: #faf8f4; font-family: Arial, sans-serif; color: #262421; }\n /* Every case carries a UNIQUE border-left width so each finding's snippet\n (`border-left: Npx`) attributes to exactly one case. Flag widths: 5, 6,\n 8, 12. Pass widths: 4, 7, 9, 10, 11. */\n .tabbed {\n width: 420px;\n padding: 16px 20px;\n margin: 0 0 24px;\n background: #ffffff;\n border: 1px solid #ddd8d0;\n border-left: 6px solid #b8422e;\n }\n .w4 { border-left-width: 4px; }\n .w5 { border-left-width: 5px; }\n .w7 { border-left-width: 7px; }\n .w8 { border-left-width: 8px; }\n .w9 { border-left-width: 9px; }\n .w10 { border-left-width: 10px; }\n .w11 { border-left-width: 11px; }\n .w12 { border-left-width: 12px; }\n /* Marquee specimen: keyframes + an infinite loop track. The section that\n hosts it carries a marquee waiver; the CSS-scan finding must resolve the\n selector to the live element and honor the ancestor attribute. */\n @keyframes ticker-loop { from { transform: translateX(0); } to { transform: translateX(-50%); } }\n .quote-track { display: flex; width: max-content; animation: ticker-loop 80s linear infinite; }\n .quote-track span { padding: 8px 16px; }\n /* Dead grid CSS: this two-axis hairline grid selector matches nothing on\n this page, so the browser pass must drop the codex-grid finding — the\n pattern never renders here. */\n .never-rendered-grid {\n background:\n linear-gradient(90deg, rgba(200,180,120,0.08) 1px, transparent 1px),\n linear-gradient(180deg, rgba(200,180,120,0.08) 1px, transparent 1px);\n background-size: 24px 24px;\n }\n /* Overshoot bezier inside a keyframe STEP: enclosingCssSelector must refuse\n `to` as a selector (it would match nothing and wrongly drop the finding),\n so this bounce-easing finding stays page-level and is retained. */\n @keyframes springy {\n from { transform: none; }\n to { transform: translateY(-4px); animation-timing-function: cubic-bezier(0.3, 1.8, 0.6, 1); }\n }\n"],"result":false}
|
||
{"args":["\n \n \n\n \n \n\n \n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n"],"result":false}
|
||
{"args":["The control case has a 6px side-tab accent border and no waiver, so the side-tab rule must flag it."],"result":false}
|
||
{"args":["The waiver names pulsing-dot, not side-tab, so this 8px case must still flag."],"result":false}
|
||
{"args":["A waiver on a preceding sibling is not an ancestor; this 12px case must still flag."],"result":false}
|
||
{"args":["\"side-tabs\" is not a rule id; the 5px case must still flag."],"result":false}
|
||
{"args":["Inside a container waived for exactly side-tab; must not flag."],"result":false}
|
||
{"args":["Two levels below the waiving container; the ancestor walk must still find it."],"result":false}
|
||
{"args":["Inside a star-waived container; must not flag."],"result":false}
|
||
{"args":["side-tab appears in the comma list; must not flag."],"result":false}
|
||
{"args":["The waiver sits on the element itself; must not flag."],"result":false}
|
||
{"args":["One voice."],"result":false}
|
||
{"args":["Another voice."],"result":false}
|
||
{"args":["Script error + content hidden fixture (browser-only)"],"result":false}
|
||
{"args":["\n html { scroll-behavior: smooth; }\n body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; background: #fff; }\n main { max-width: 640px; margin: 0 auto; padding: 32px 24px; }\n section { margin: 0 0 480px; }\n .reveal { opacity: 0; transform: translateY(14px); transition: opacity 0.4s ease-out, transform 0.4s ease-out; }\n .reveal.in-view { opacity: 1; transform: none; }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n \n "],"result":false}
|
||
{"args":["The opening paragraph renders normally, exactly like the real broken sample: the\n first viewport looks fine while everything below it never appears."],"result":false}
|
||
{"args":["\n const sections = document.querySelectorAll('.reveal');\n const io = new IntersectionObserver((entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) { entry.target.classList.add('in-view'); io.unobserve(entry.target); }\n }\n }, { threshold: 0.1 });\n sections.forEach((el) => io.observe(el));\n // Deliberate syntax error: an unterminated string kills the whole block,\n // including the observer wiring above.\n const label = 'unterminated\n "],"result":false}
|
||
{"args":["Shadowed form.id regression fixture"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; background: #ffffff; color: #1a1a1a; margin: 0; padding: 24px; }\n .product { max-width: 640px; margin: 0 auto; }\n h1 { font-size: 28px; margin: 0 0 12px; }\n .price { font-size: 20px; font-weight: 600; }\n form { margin-top: 16px; }\n .add-to-cart { background: #1a1a1a; color: #ffffff; border: 0; padding: 12px 24px; border-radius: 6px; font-size: 16px; cursor: pointer; }\n label { display: block; margin: 8px 0 4px; font-size: 14px; }\n select, input[type=\"number\"] { padding: 8px; font-size: 14px; }\n"],"result":false}
|
||
{"args":["\n \n \n\n \n \n"],"result":false}
|
||
{"args":["Impeccable Test Product"],"result":false}
|
||
{"args":["$49.00"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Quantity"],"result":false}
|
||
{"args":["Variant"],"result":false}
|
||
{"args":["Small"],"result":false}
|
||
{"args":["Medium"],"result":false}
|
||
{"args":["Large"],"result":false}
|
||
{"args":["Add to cart"],"result":false}
|
||
{"args":["Shape-assembled illustration fixture"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Jan"],"result":false}
|
||
{"args":["Mar"],"result":false}
|
||
{"args":["May"],"result":false}
|
||
{"args":["Jul"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["Anti-Patterns That Should Be Flagged"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; background: #f9fafb; padding: 2rem; }\n h1 { font-size: 2rem; margin-bottom: 0.5rem; }\n h2 { font-size: 0.875rem; margin: 2.5rem 0 0.75rem; color: #4b5563; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.08em; }\n p.intro { color: #6b7280; margin-bottom: 2rem; max-width: 36rem; }\n .cards { display: grid; gap: 1rem; max-width: 28rem; }\n .card { background: white; padding: 1rem; border-radius: 0.375rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }\n .card h3 { font-weight: 600; color: #111827; font-size: 0.875rem; }\n .card p { font-size: 0.8125rem; color: #6b7280; margin-top: 0.25rem; }\n\n /* CSS side-tab variants */\n .card-shorthand-left { border-left: 4px solid #3b82f6; }\n .card-shorthand-right { border-right: 5px solid #8b5cf6; }\n .card-longhand-left { border-left-width: 3px; border-left-style: solid; border-left-color: #10b981; }\n .card-longhand-right { border-right-width: 6px; border-right-style: solid; border-right-color: #ef4444; }\n\n /* CSS top/bottom + border-radius */\n .card-css-top { border-radius: 12px; border-top: 4px solid #3b82f6; }\n .card-css-bottom { border-radius: 12px; border-bottom: 3px solid #8b5cf6; }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n \n\n \n \n \n\n \n \n \n \n \n \n\n"],"result":false}
|
||
{"args":["Anti-Patterns: Should Flag"],"result":false}
|
||
{"args":["Every example on this page should be detected by the anti-pattern scanner."],"result":false}
|
||
{"args":["Tailwind Side-Tab"],"result":false}
|
||
{"args":["border-l-4 + rounded-r"],"result":false}
|
||
{"args":["The classic AI tell."],"result":false}
|
||
{"args":["border-r-4 + rounded-l"],"result":false}
|
||
{"args":["Right side variant."],"result":false}
|
||
{"args":["border-e-8 + rounded-l"],"result":false}
|
||
{"args":["Logical inline-end, extra thick."],"result":false}
|
||
{"args":["CSS Side-Tab"],"result":false}
|
||
{"args":["border-left: 4px solid"],"result":false}
|
||
{"args":["CSS shorthand."],"result":false}
|
||
{"args":["border-right: 5px solid"],"result":false}
|
||
{"args":["CSS shorthand right."],"result":false}
|
||
{"args":["border-left-width: 3px"],"result":false}
|
||
{"args":["CSS longhand."],"result":false}
|
||
{"args":["border-right-width: 6px"],"result":false}
|
||
{"args":["CSS longhand right."],"result":false}
|
||
{"args":["Top/Bottom + Rounded"],"result":false}
|
||
{"args":["border-t-4 + rounded-lg"],"result":false}
|
||
{"args":["Top accent on rounded card."],"result":false}
|
||
{"args":["border-b-4 + rounded-xl"],"result":false}
|
||
{"args":["Bottom accent on rounded card."],"result":false}
|
||
{"args":["border-t-2 + rounded-md"],"result":false}
|
||
{"args":["Even thin top border on rounded."],"result":false}
|
||
{"args":["CSS border-top + border-radius"],"result":false}
|
||
{"args":["Top border from style block."],"result":false}
|
||
{"args":["CSS border-bottom + border-radius"],"result":false}
|
||
{"args":["Bottom border from style block."],"result":false}
|
||
{"args":["Dark Mode"],"result":false}
|
||
{"args":["Dark card + border-l-4 + rounded"],"result":false}
|
||
{"args":["Dark background doesn't make the side-tab OK."],"result":false}
|
||
{"args":["Dark card + border-t-2 + rounded"],"result":false}
|
||
{"args":["Top accent on dark rounded card."],"result":false}
|
||
{"args":["Dark CSS card + side border + radius"],"result":false}
|
||
{"args":["Inline dark card with side-tab."],"result":false}
|
||
{"args":["Clean Patterns — Should NOT Flag"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; background: #f9fafb; padding: 2rem; }\n h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }\n h2 { font-size: 1.125rem; margin: 2.5rem 0 0.75rem; color: #6b7280; border-bottom: 1px solid #e5e7eb; padding-bottom: 0.5rem; }\n p.intro { color: #6b7280; margin-bottom: 2rem; max-width: 36rem; }\n .section { max-width: 28rem; }\n .card-rounded { background: white; padding: 1.5rem; border-radius: 0.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); max-width: 28rem; margin-bottom: 1rem; }\n .card-rounded h3 { font-weight: 600; color: #111827; }\n .card-rounded p { font-size: 0.875rem; color: #6b7280; margin-top: 0.25rem; }\n .card-flat { background: white; padding: 1rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); max-width: 28rem; margin-bottom: 1rem; }\n .badge { display: inline-block; font-size: 0.75rem; padding: 0.125rem 0.5rem; border-radius: 9999px; background: #dbeafe; color: #1d4ed8; }\n\n /* Below-threshold side borders without radius */\n .thin-left { border-left: 1px solid #3b82f6; }\n .thin-right { border-right: 2px solid #8b5cf6; }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n \n\n \n \n \n \n \n \n\n"],"result":false}
|
||
{"args":["Clean Patterns: Should Pass"],"result":false}
|
||
{"args":["None of these should be flagged. They're all intentional, well-established web patterns."],"result":false}
|
||
{"args":["Clean Cards"],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Full border card"],"result":false}
|
||
{"args":["Subtle 1px border all around. Clean and intentional."],"result":false}
|
||
{"args":["Top border, no radius"],"result":false}
|
||
{"args":["A hairline top rule without rounded corners is a clean section divider."],"result":false}
|
||
{"args":["Bottom border, no radius"],"result":false}
|
||
{"args":["Bottom accent without rounded corners is also clean."],"result":false}
|
||
{"args":["New"],"result":false}
|
||
{"args":["No border at all"],"result":false}
|
||
{"args":["Just a shadow. Simple and effective."],"result":false}
|
||
{"args":["Below Threshold (Thin, No Radius)"],"result":false}
|
||
{"args":["border-left: 1px solid, no radius — not flagged"],"result":false}
|
||
{"args":["border-right: 2px solid, no radius — not flagged"],"result":false}
|
||
{"args":["1px inline border-left, no radius — not flagged"],"result":false}
|
||
{"args":["Dark Mode (Clean)"],"result":false}
|
||
{"args":["Dark card, full border"],"result":false}
|
||
{"args":["Uniform 1px border all around. Clean."],"result":false}
|
||
{"args":["Dark section, bottom border, no radius"],"result":false}
|
||
{"args":["Bottom accent without radius is fine."],"result":false}
|
||
{"args":["Dark card, no border"],"result":false}
|
||
{"args":["Shadow only. Clean."],"result":false}
|
||
{"args":["text-occlusion fixture"],"result":false}
|
||
{"args":["\n * { margin: 0; box-sizing: border-box; }\n body { font-family: system-ui, sans-serif; color: #111; background: #fff; }\n section { padding: 40px; position: relative; }\n\n /* ── FLAG 1: opaque box painted over text (>30%) ── */\n .flag-box-wrap { position: relative; height: 120px; }\n .flag-box-text { position: absolute; top: 40px; left: 40px; font-size: 20px; width: 300px; }\n .flag-box-cover { position: absolute; top: 30px; left: 30px; width: 240px; height: 60px;\n background: #1f7a3d; border-radius: 8px; z-index: 5; }\n\n /* ── FLAG 2: inline element with opaque fill + leaked vertical padding ── */\n .flag-leak-host { position: relative; margin-top: 30px; }\n .flag-leak { display: inline; background: #2f8543; padding: 36px; border-radius: 6px; }\n .flag-leak-card { background: #14202b; color: #fff; padding: 24px; border-radius: 6px; }\n\n /* ── FLAG 3: big headline overhanging an opaque card ── */\n .flag-hero { position: relative; height: 220px; }\n .flag-headline { position: absolute; top: 40px; left: 0; font-size: 64px; font-weight: 800;\n width: 760px; color: #b22; white-space: nowrap; }\n .flag-card { position: absolute; top: 20px; left: 700px; width: 360px; height: 180px;\n background: #f4f5f7; border: 1px solid #ddd; box-shadow: 0 2px 8px rgba(0,0,0,.08); border-radius: 8px; }\n\n /* ── PASS: stacked headline with tight leading (line-box bleed, no real overlap) ── */\n .pass-stack { margin-top: 40px; }\n .pass-eyebrow { font-size: 14px; letter-spacing: .1em; text-transform: uppercase; color: #a33; }\n .pass-title { font-size: 96px; line-height: .9; font-weight: 800; }\n\n /* ── PASS: text over image with a scrim, text on top (readable) ── */\n .pass-hero { position: relative; height: 200px; overflow: hidden; border-radius: 8px; }\n .pass-hero img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }\n .pass-hero .scrim { position: absolute; inset: 0; background: linear-gradient(transparent, rgba(0,0,0,.6)); }\n .pass-hero .cap { position: absolute; bottom: 16px; left: 16px; color: #fff; font-size: 24px; z-index: 2; }\n\n /* ── PASS: fixed status bar over content at the fold ── */\n .pass-under { margin-top: 20px; font-size: 16px; }\n /* Invisible interaction layer: an opacity-0 scrubber input stretched over\n text, like a before/after comparison handle. It paints nothing, so it must\n not read as an occluder despite its UA background-color. */\n .pass-scrubber-wrap { position: relative; margin-top: 30px; height: 80px; }\n .pass-scrubber-text { position: absolute; top: 20px; left: 10px; font-size: 20px; width: 320px; }\n .pass-scrubber-input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; z-index: 5; margin: 0; }\n .pass-fixedbar { position: fixed; left: 0; right: 0; bottom: 0; height: 40px; background: #b22;\n color: #fff; display: flex; align-items: center; padding: 0 16px; }\n"],"result":false}
|
||
{"args":["\n \n \n \n"],"result":false}
|
||
{"args":["\n \n\n \n\n \n\n \n\n \n\n \n "],"result":false}
|
||
{"args":["Root cause identified in four minutes"],"result":false}
|
||
{"args":["Incident closed. Missing index on the replica, found from one alert."],"result":false}
|
||
{"args":["The trace you need is the one they sampled away."],"result":false}
|
||
{"args":["INCIDENT 4417"],"result":false}
|
||
{"args":["service checkout-api"],"result":false}
|
||
{"args":["Family Italian on the waterfront"],"result":false}
|
||
{"args":["Trattoria da Nonna Lucia"],"result":false}
|
||
{"args":["Villa on the cliff"],"result":false}
|
||
{"args":["Every span of every trace, kept on disk, no sampling ever applied here."],"result":false}
|
||
{"args":["8/8 services up · p99 1.20s · retention infinite"],"result":false}
|
||
{"args":["Drag anywhere to compare versions"],"result":false}
|
||
{"args":["Text overflow fixture (browser-only)"],"result":false}
|
||
{"args":["\n body { font-family: system-ui, sans-serif; margin: 0; color: #1a1a1a; background: #fff; }\n .cols { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; padding: 24px; align-items: start; }\n .col { padding: 16px; }\n .box { width: 160px; border: 1px solid #ddd; margin: 0 0 24px; padding: 8px; }\n .sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n white-space: nowrap;\n border-width: 0;\n }\n .sr-only-clip-path { clip-path: inset(50%); }\n .sr-only-legacy-clip { clip: rect(0, 0, 0, 0); }\n .sr-only-tiny-hidden { clip: auto; }\n .sr-only-clipped-wide {\n position: absolute;\n width: 160px;\n height: 20px;\n overflow: visible;\n white-space: nowrap;\n clip-path: inset(50%);\n }\n .hidden-slide {\n opacity: 0;\n pointer-events: none;\n }\n .hidden-slide .pass-hidden-slide-overflow {\n display: block;\n width: 120px;\n white-space: nowrap;\n }\n "],"result":false}
|
||
{"args":["A single long line of running text that refuses to wrap and clearly spills past its fixed-width box."],"result":false}
|
||
{"args":["Supercalifragilisticexpialidocioussupercalifragilisticexpialidocious"],"result":false}
|
||
{"args":["IOC-QARTOD-ARGO-PROFILES-SPILLING-WELL-PAST-THE-CELL"],"result":false}
|
||
{"args":["\n \n \n \n \n \n \n \n \n \n \n \n "],"result":false}
|
||
{"args":["The very same unwrapping line, but inside a genuine horizontal scroll region meant to scroll."],"result":false}
|
||
{"args":["a-very-long-preformatted-code-line-that-intentionally-extends-beyond-the-box-width"],"result":false}
|
||
{"args":["Ordinary wrapping paragraph text that stays comfortably within its container."],"result":false}
|
||
{"args":["A long line living inside a scroll ancestor, which must not be flagged."],"result":false}
|
||
{"args":["A screen-reader-only label with long text that should never draw a visible overflow finding."],"result":false}
|
||
{"args":["A legacy clipped screen-reader-only label with long text that should not be flagged."],"result":false}
|
||
{"args":["A tiny overflow-hidden screen-reader-only label without a clip declaration should not be flagged."],"result":false}
|
||
{"args":["A fully clipped label with a normal-sized box and overflowing text should not be flagged."],"result":false}
|
||
{"args":["Fits fine"],"result":false}
|
||
{"args":["An ordinary inline span with wrapping text that stays inside its container across several lines."],"result":false}
|
||
{"args":["Typography Anti-Patterns — Should Flag"],"result":false}
|
||
{"args":["\n body {\n font-family: 'Inter', sans-serif;\n background: #f9fafb;\n padding: 2rem;\n }\n h1 { font-size: 18px; font-weight: 700; margin-bottom: 0.5rem; }\n h2 { font-size: 16px; font-weight: 600; margin: 2rem 0 0.75rem; color: #6b7280; }\n h3 { font-size: 15px; font-weight: 600; }\n p { font-size: 14px; color: #6b7280; margin-top: 0.25rem; }\n .caption { font-size: 13px; color: #6b7280; }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n\n \n \n\n \n \n \n\n \n \n\n"],"result":false}
|
||
{"args":["Typography Anti-Patterns"],"result":false}
|
||
{"args":["This page triggers three typography detections:"],"result":false}
|
||
{"args":["1. Overused Font"],"result":false}
|
||
{"args":["Inter is loaded from Google Fonts and set as the only font-family. It's the most common AI default."],"result":false}
|
||
{"args":["2. Single Font"],"result":false}
|
||
{"args":["There's no second font for headings or display text. Everything uses Inter — no typographic variety."],"result":false}
|
||
{"args":["3. Flat Type Hierarchy"],"result":false}
|
||
{"args":["The font sizes are 13px, 14px, 15px, 16px, 18px — all crammed into a 5px range. No visual contrast between heading and body."],"result":false}
|
||
{"args":["This caption is barely distinguishable from body text."],"result":false}
|
||
{"args":["A Subheading"],"result":false}
|
||
{"args":["Can you tell this is a subheading? Exactly."],"result":false}
|
||
{"args":["Typography — Clean Patterns"],"result":false}
|
||
{"args":["\n body {\n font-family: 'Karla', system-ui, sans-serif;\n background: #f9fafb;\n padding: 2rem;\n font-size: 16px;\n color: #111827;\n }\n h1 {\n font-family: 'Newsreader', Georgia, serif;\n font-size: 48px;\n font-weight: 700;\n margin-bottom: 0.5rem;\n }\n h2 {\n font-family: 'Newsreader', Georgia, serif;\n font-size: 32px;\n font-weight: 700;\n margin: 2rem 0 0.75rem;\n }\n h3 { font-size: 24px; font-weight: 600; }\n p { font-size: 16px; color: #6b7280; margin-top: 0.25rem; }\n .caption { font-size: 12px; color: #6b7280; }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n\n \n \n \n\n"],"result":false}
|
||
{"args":["Good Typography"],"result":false}
|
||
{"args":["This page uses distinctive fonts, proper pairing, and strong hierarchy."],"result":false}
|
||
{"args":["Two Font Families"],"result":false}
|
||
{"args":["Newsreader (serif) for headings, Karla for body. Clear contrast in both structure and personality, and neither face has been adopted into the current AI-default monoculture."],"result":false}
|
||
{"args":["Strong Size Hierarchy"],"result":false}
|
||
{"args":["Sizes range from 12px to 48px — a 4:1 ratio with clear visual steps."],"result":false}
|
||
{"args":["Caption text is clearly distinct from body."],"result":false}
|
||
{"args":["Typography Anti-Patterns — Side by Side"],"result":false}
|
||
{"args":["\n body {\n margin: 0;\n padding: 24px;\n background: #f9fafb;\n color: #111827;\n font: 16px/1.5 system-ui, sans-serif;\n }\n\n .grid {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n gap: 32px;\n max-width: 1180px;\n margin: 0 auto;\n }\n\n .col {\n min-width: 0;\n }\n\n .col-label {\n margin: 0 0 14px;\n color: #475569;\n font-size: 13px;\n font-weight: 760;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n }\n\n .flag-type {\n font-family: 'Inter', sans-serif;\n background: #fdfdfd;\n padding: 22px;\n border: 1px solid #e5e7eb;\n }\n\n .flag-type h1 {\n margin: 0 0 8px;\n font-size: 18px;\n line-height: 1.2;\n }\n\n .flag-type h2 {\n margin: 24px 0 8px;\n color: #4b5563;\n font-size: 16px;\n line-height: 1.25;\n }\n\n .flag-type h3 {\n margin: 18px 0 4px;\n font-size: 15px;\n line-height: 1.25;\n }\n\n .flag-type p {\n margin: 0 0 10px;\n color: #4b5563;\n font-size: 14px;\n }\n\n .flag-type .caption {\n color: #6b7280;\n font-size: 13px;\n }\n\n .flag-type .tight-leading-demo {\n line-height: 1.05;\n }\n\n .flag-type .tiny-body-demo {\n color: #374151;\n font-size: 10px;\n line-height: 1.6;\n }\n\n .flag-type .caps-body-demo {\n text-transform: uppercase;\n }\n\n .flag-type .tracked-body-demo {\n letter-spacing: 0.08em;\n }\n\n .flag-type .justified-body-demo {\n text-align: justify;\n }\n\n .pass-type {\n background: #fdfdfd;\n padding: 22px;\n border: 1px solid #e5e7eb;\n }\n\n .pass-type h1,\n .pass-type h2 {\n font-family: 'Newsreader', Georgia, serif;\n }\n\n .pass-type h1 {\n margin: 0 0 10px;\n font-size: 48px;\n line-height: 0.98;\n font-weight: 700;\n }\n\n .pass-type h2 {\n margin: 32px 0 10px;\n font-size: 30px;\n line-height: 1.08;\n font-weight: 700;\n }\n\n .pass-type h3,\n .pass-type p,\n .pass-type .caption {\n font-family: 'Karla', system-ui, sans-serif;\n }\n\n .pass-type h3 {\n margin: 22px 0 6px;\n font-size: 22px;\n line-height: 1.15;\n }\n\n .pass-type p {\n margin: 0 0 12px;\n color: #374151;\n font-size: 16px;\n }\n\n .pass-type .caption {\n color: #4b5563;\n font-size: 12px;\n }\n "],"result":false}
|
||
{"args":["\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n "],"result":false}
|
||
{"args":["This side uses element-level typography failures that can be highlighted directly in the detector lab."],"result":false}
|
||
{"args":["Tight Leading"],"result":false}
|
||
{"args":["This paragraph has a line-height that is too tight for body copy. The lines press into each other and make scanning harder, especially once the text wraps across multiple lines in a production layout."],"result":false}
|
||
{"args":["Tiny Body Text"],"result":false}
|
||
{"args":["This body text is set at 10px, which is too small for real reading content and should be reserved for neither primary copy nor explanatory product text."],"result":false}
|
||
{"args":["All-Caps Body"],"result":false}
|
||
{"args":["This entire paragraph is transformed to uppercase, which removes useful word shapes and makes longer body copy feel like a shout instead of a readable passage."],"result":false}
|
||
{"args":["Wide Tracking"],"result":false}
|
||
{"args":["This paragraph spreads every letter apart, a treatment that belongs on short labels at most, not on body copy that people need to read comfortably."],"result":false}
|
||
{"args":["Justified Text"],"result":false}
|
||
{"args":["This paragraph uses justified alignment without automatic hyphenation. In narrow columns it creates uneven word spacing and distracting rivers through the text block."],"result":false}
|
||
{"args":["Overused Font"],"result":false}
|
||
{"args":["Inter is also the dominant face on this side of the page, so the browser page-level detector has enough real text elements to show the global overused-font banner."],"result":false}
|
||
{"args":["\n \n \n\n \n \n\n \n \n \n "],"result":false}
|
||
{"args":["This side keeps readable body defaults, a real font pairing, clear scale, and visible hierarchy."],"result":false}
|
||
{"args":["Newsreader carries display text, while Karla handles reading surfaces and supporting labels."],"result":false}
|
||
{"args":["Sizes range from 12px to 48px with distinct roles for heading, subheading, body, and caption."],"result":false}
|
||
{"args":["Caption text is intentionally smaller but still readable."],"result":false}
|
||
{"args":["undersized-ui-text fixture"],"result":false}
|
||
{"args":["\n /* Explicit pixel dimensions because jsdom does no layout. */\n body { font-family: system-ui, sans-serif; font-size: 16px; }\n .col { display: inline-block; width: 480px; vertical-align: top; }\n\n /* --- should-flag styles --- */\n .nav-link { font-size: 8px; }\n .category { font-size: 8px; }\n .meta { font-size: 9px; }\n .btn-small { font-size: 10px; width: 120px; height: 24px; }\n .cell-small { font-size: 9px; }\n .caps-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.12em; }\n .footer-link { font-size: 8px; }\n\n /* --- should-pass styles --- */\n .legal { font-size: 10px; line-height: 1.5; } /* non-interactive footer smallprint at 10px */\n .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); font-size: 4px; }\n .sup-marker { font-size: 8px; }\n .sub-marker { font-size: 8px; }\n .em-parent { font-size: 20px; }\n .em-child { font-size: 0.6em; } /* resolves to 12px, above the floor */\n .mock-terminal { width: 400px; }\n .term-line { font-size: 9px; } /* legitimately small code/terminal text */\n .normal-link { font-size: 12px; } /* at the floor, allowed */\n"],"result":false}
|
||
{"args":["\n\n\n\n\n\n\n\n"],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n"],"result":false}
|
||
{"args":["Flag Nav Link"],"result":false}
|
||
{"args":["Flag Category"],"result":false}
|
||
{"args":["Flag Meta Row"],"result":false}
|
||
{"args":["Flag Button"],"result":false}
|
||
{"args":["Flag Table Cell"],"result":false}
|
||
{"args":["Flag Caps Label"],"result":false}
|
||
{"args":["Flag Footer Link"],"result":false}
|
||
{"args":["\n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n\n \n \n"],"result":false}
|
||
{"args":["Pass Legal Fine Print copyright 2026 all rights reserved across this jurisdiction"],"result":false}
|
||
{"args":["Pass Sr Only"],"result":false}
|
||
{"args":["Reference and water"],"result":false}
|
||
{"args":["Pass Sup Marker"],"result":false}
|
||
{"args":["Pass Sub Marker"],"result":false}
|
||
{"args":["Pass Em Sized"],"result":false}
|
||
{"args":["Pass Terminal Line"],"result":false}
|
||
{"args":["Pass Normal Link"],"result":false}
|
||
{"args":["Visual Contrast Fixture"],"result":false}
|
||
{"args":["\n :root {\n --paper: #f7f3ee;\n --ink: #171717;\n --muted: #566174;\n }\n\n body {\n margin: 0;\n padding: 32px;\n background: var(--paper);\n color: var(--ink);\n font-family: system-ui, sans-serif;\n }\n\n .grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 24px;\n max-width: 980px;\n margin: 0 auto;\n }\n\n .column {\n display: grid;\n gap: 14px;\n }\n\n .column > h2 {\n margin: 0 0 2px;\n color: var(--muted);\n font-size: 13px;\n font-weight: 700;\n letter-spacing: 0.08em;\n line-height: 1.4;\n text-transform: uppercase;\n }\n\n .image-card {\n position: relative;\n display: grid;\n align-content: end;\n min-height: 180px;\n padding: 28px;\n border-radius: 8px;\n overflow: hidden;\n background-size: cover;\n background-position: center;\n }\n\n .image-card p {\n position: relative;\n z-index: 1;\n max-width: 28ch;\n margin: 0;\n font-size: 18px;\n font-weight: 700;\n line-height: 1.45;\n }\n\n .light-image {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 420 240'%3E%3Crect width='420' height='240' fill='%23ece8df'/%3E%3Ccircle cx='308' cy='58' r='132' fill='%23faf7f0'/%3E%3Cpath d='M0 186c52-22 92-26 142-8s98 18 148-8 92-22 130 0v70H0z' fill='%23f5efe4'/%3E%3C/svg%3E\");\n }\n\n .pale-pattern {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 420 240'%3E%3Crect width='420' height='240' fill='%23edf2f7'/%3E%3Cg fill='%23f8fafc'%3E%3Ccircle cx='60' cy='64' r='42'/%3E%3Ccircle cx='182' cy='142' r='58'/%3E%3Ccircle cx='336' cy='84' r='74'/%3E%3C/g%3E%3Cg stroke='%23dbe4ef' stroke-width='14' opacity='.55'%3E%3Cpath d='M-20 220C74 112 150 94 232 164s138 46 214-62' fill='none'/%3E%3C/g%3E%3C/svg%3E\");\n }\n\n .mist-image {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 420 240'%3E%3Crect width='420' height='240' fill='%23b9c4cf'/%3E%3Cpath d='M0 52c74-28 138-22 194 18s124 38 226-28v198H0z' fill='%23cbd3dc'/%3E%3Ccircle cx='82' cy='176' r='68' fill='%23aeb8c4'/%3E%3C/svg%3E\");\n }\n\n .dark-image {\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 420 240'%3E%3Crect width='420' height='240' fill='%23191b20'/%3E%3Ccircle cx='316' cy='50' r='132' fill='%232d3440'/%3E%3Cpath d='M0 182c70-48 138-42 204-6s128 24 216-58v122H0z' fill='%23101318'/%3E%3C/svg%3E\");\n }\n\n .underlay {\n isolation: isolate;\n background: #1a1d24;\n }\n\n .underlay.light-shell {\n background: #f5f2ea;\n }\n\n .underlay img,\n .underlay svg {\n position: absolute;\n inset: 0;\n z-index: 0;\n width: 100%;\n height: 100%;\n object-fit: cover;\n }\n\n .underlay svg {\n display: block;\n }\n "],"result":false}
|
||
{"args":["Should flag after pixel sampling"],"result":false}
|
||
{"args":["White text on light image should be sampled by pixel contrast."],"result":false}
|
||
{"args":["Dark text on dark image should be sampled by pixel contrast."],"result":false}
|
||
{"args":["Translucent white text on a pale pattern should use rendered pixels."],"result":false}
|
||
{"args":["Muted gray text on a misty image should be sampled by pixel contrast."],"result":false}
|
||
{"args":["Should pass after pixel sampling"],"result":false}
|
||
{"args":["White text on dark image should pass the pixel contrast fallback."],"result":false}
|
||
{"args":["Dark text on light image should pass the pixel contrast fallback."],"result":false}
|
||
{"args":["White text over a dark image underlay should pass."],"result":false}
|
||
{"args":["Dark text over a light SVG underlay should pass."],"result":false}
|
||
{"args":["Hidden mockup text on a light image should not be sampled."],"result":false}
|
||
{"args":["\n .faint { color: #ff00aa; }\n .brand { font-family: Verdana } /* impeccable-disable-line design-system-font */\n .also { font-family: Verdana }\n"],"result":false}
|
||
{"args":["faint text"],"result":false}
|
||
{"args":["x"],"result":false}
|
||
{"args":["y"],"result":false}
|
||
{"args":["\n .card { font-family: Verdana, sans-serif; }\n .brand { font-family: Inter, sans-serif; }\n .dot { width: 8px; height: 8px; border-radius: 50%; animation: blink 1s infinite; }\n .off { color: #ff00aa; }\n"],"result":false}
|
||
{"args":["Hi"],"result":false}
|
||
{"args":["Brand"],"result":false}
|
||
{"args":["Off palette"],"result":false}
|