Repository navigation
Contrast: read halos, near-opaque glass and settled states - #1044
Conversation
The hide style cleared every text-shadow with the glyphs, so a soft dark halo left the second shot and most of the clip repainted (rhem.ai, klimaschutz-niedersachsen.de): the pass refused the box. The halo now stays in both shots; outlined text, whose zero-blur offset shadows draw its outline, still hides them with the glyphs. A backdrop-filter under a fill from 0.8 alpha up (smartbi.ca's header at 0.94, imageto3d.studio's tabs at 0.82) gives a new reason the pixel pass reads and the sampled pass refuses. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
- A box a timed one-shot entrance holds at opacity 0 counts at the entrance's end frame (rezyco.com's call to action, 2.45s delay). - A box a paused loop holds part way through a fade is not at rest (iubcr.vercel.app's notification rows). - The hit-test stacks take a fill at least 0.95 opaque as the surface the text sits on (juskoe.vercel.app's nav pill). - The link and span path keeps a 1.0:1 the stacks confirm on a box of the page's own (v0-memorialqrwebsitecode-90's white Start Chat on its white card); the page ground still drops it, as the static engine does. - The site's name set huge and faint is a decorative wordmark under r3-04's advisory (screen.movie, incredible.one). Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
A styled control (an own fill over 0.5 alpha) takes the full pass, which keeps its own reading of the page ground; the drop was leaking into it through the shared guard. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
- The pixel pass hides text shadows in its reading again, and keeps a halo for a second reading only when the first is refused as repainted and every shadow layer stands apart from the ink. A glow in the ink's own colour and a drop shadow under grey copy failed as counted warnings when kept (3.3:1 on neon pink copy that reads about 7:1). - A same-colour pair on the link and span path reports only for the whole visible label of a link or button: a hidden label beside a menu button's bars, or a span in no control, is hidden on purpose. - The wordmark's site names take only the title's first and last parts, never split on a hyphen, and the 3x-h1 path needs 80px. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
[Medium impact] The PR appears safe to merge; the numbered previous finding is fixed, and no new actionable issue was established. SummaryImproves browser contrast checks for halos, near-opaque glass, hidden labels, and settled animation states.
|
There was a problem hiding this comment.
🟡 Changes recommended
Incorrect contrast edge cases, repeated document traversal, and stale aggregate oracle expectations remain unresolved.
6 open findings
Add new fixtures to aggregate oracle goldens · New Use settled opacity for decorative text detection · New Avoid repeated full-document heading queries · New Exclude hidden or zero-area media from visible paint checks · New Inspect descendant decorations when the control is the label · New Apply entrance end opacity during opacity folding · New
What changed in this PR
Improves browser contrast detection for shadowed text, translucent surfaces, animation states, and decorative wordmarks.
Changes:
- Adds halo-preserving pixel retries and near-opaque glass support.
- Refines surface, control-label, animation, and wordmark classification.
- Adds regression fixtures, browser tests, oracle expectations, and contract documentation.
| File | Description |
|---|---|
| tests/oracle/golden/detect-fixture-text-low-contrast-settled-states-html.json | Records settled-state text output. |
| tests/oracle/golden/detect-fixture-text-low-contrast-pixel-reads-html.json | Records pixel-fixture text output. |
| tests/oracle/golden/detect-fixture-json-low-contrast-settled-states-html.json | Records settled-state JSON output. |
| tests/oracle/golden/detect-fixture-json-low-contrast-pixel-reads-html.json | Records pixel-fixture JSON output. |
| tests/fixtures/antipatterns/low-contrast-settled-states.html | Adds animation, label, and wordmark cases. |
| tests/fixtures/antipatterns/low-contrast-pixel-reads.html | Adds halo, glow, and glass cases. |
| docs/CLI-CONTRACT.md | Documents revised contrast behavior. |
| crates/html/src/decorative_text.rs | Keeps layout-dependent wordmarks browser-only. |
| crates/core/src/checks/rules.rs | Refines same-colour safe-tag scoring. |
| crates/core/src/checks/decorative_text.rs | Adds decorative wordmark classification. |
| crates/core/src/browser/visual.rs | Adds halo eligibility and glass routing. |
| crates/core/src/browser/painted.rs | Recognizes entrance endpoints and paused loops. |
| crates/core/src/browser/element_checks.rs | Updates surfaces, labels, and opacity handling. |
| crates/core/src/browser/decorative_text.rs | Collects browser wordmark evidence. |
| crates/browser/tests/low_contrast_settled_states.rs | Tests settled-state contrast behavior. |
| crates/browser/tests/low_contrast_pixel_reads.rs | Tests halo and glass pixel readings. |
| crates/browser/src/screenshot_contrast.rs | Retries eligible captures with halos preserved. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
- The white-on-white control-only rule now counts a picture or SVG inside a link that owns its text (one with a <title> too), and any other box inside it that paints and holds no text, so a hidden label beside its own icon takes no 1.0:1 verdict. A box of the label's own words still counts as label. - The contrast fold reads a timed entrance caught at 0 at its end frame, the same value the visibility gates count, so copy that settles faint is scored faded rather than at full strength. - The wordmark facts read the page's h1 sizes only for type between 80px and 120px, the band where they decide, instead of walking the page for every text the checks ask about. Run 1004 ratchet: identical to the previous head. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
A picture tag inside a link was counted as paint before its size was read, so an icon left in the markup at display: none dropped the white-on-white verdict on a real failure. Every box now needs a rendered area and visible style before it counts, pictures included. Run 1004 ratchet: identical to the previous head. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
|
About this review
Reviewed 1 file on 19 Oct 2026, 00:46 UTC · Run page Reviews (7) · Last reviewed commit: Judge a ghost wordmark at the opacity it settles at |
The directory and no-advisory sweeps now carry the five findings of the two new low-contrast fixtures (950 to 955); nothing else moved. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
The ghost-wordmark test read the captured opacity, so a site-name heading a timed entrance holds at 0 counted as faint and its contrast finding moved to advisory. It now reads the settled opacity the paint gates count (scroll- and entrance-held boxes at their end frame). Run 1004 ratchet: identical to the previous head. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Resolve the tests/oracle/DELTAS.md append conflict by keeping main's new entries and this branch's entry after them. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Review on #1044: - The same-colour guard's whole-label test compares the text a visitor sees: the control's text without its descendants that render none (display: none, visibility: hidden, an SVG title, script, style, template). A white label beside a hidden helper span keeps its 1.0:1. - paused_mid_loop requires a replacing animation composition (or none recorded): under add or accumulate a frame at 0 does not take the box out of sight. - The ghost-wordmark shape skips any text inside a control, read from a new in_control fact, not only text that is the control's whole label. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8b7434e. Configure here.
Review on #1044: visible_text_is subtracted the whole subtree of a visibility: hidden node, but a descendant can set visibility: visible and paint. A hidden box now gives up only its own text nodes, and each descendant answers for itself; display: none and the never-rendered tags still remove their whole subtree. Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Keep both CLI-CONTRACT and DELTAS sections, regenerate the browser bundle, re-record the five directory and no-advisory sweeps from the merged tree (977 -> 982 failures, the five round 11 contrast fixture findings). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1044: a site-name heading caught at 0.2 part way through a fade-in to 1 read as a faint wordmark. settled_opacity reads a timed entrance's end frame at any captured value, and an opacity animation or transition whose end cannot be read, or a paused loop, leaves the heading not faint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's contrast round (#1044: halos, near-opaque glass and settled states) comes in. One source conflict, in `crates/core/src/checks/ rules.rs`: main narrowed `scores_safe_tag_text` to `pub(crate)` and this branch had made it `pub` for the sampled-contrast path, which lives in the same crate, so main's visibility stands with this branch's doc sentence. The other conflicts were generated artifacts: the in-page bundle, regenerated from the merged source with `cargo xtask bundle`, and the sweep goldens, re-recorded from the merged binary (`detect-dir-json-all-fixtures`, `detect-dir-text-all-fixtures`, `detect-dir-quiet-all-fixtures`, `detect-no-advisory-json`, `detect-no-advisory-text`, `detect-scope-layout-text`). Prepared with AI assistance (Claude Code). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>



Fixes the contrast residue from corpus observations-1004 branch 3 and the contrast misses in walkthroughs-1004.
URL engine:
text-shadowkept. It does this only when every shadow layer stands apart from the ink (2:1 or more over the ink colour) and the text is not outlined. Its first reading still hides the shadow, so a glow in the ink's own colour or a drop shadow under grey copy reads as before.backdrop-filterunder a fill at least 0.8 opaque no longer blocks the pixel pass (new reasonbackdrop filter under a near-opaque fill). The sampled pass still refuses it.og:site_name,application-name, or the title's first or last part.Corpus:
Tests: unit tests per part, two browser fixtures with should-flag and should-pass twins (including a neon glow, a grey drop shadow, a hidden menu label and an "opens in a new tab" span), new oracle goldens,
cargo test --workspace,bun run test,bun run build. The in-page bundle is regenerated.AI assistance: prepared by Claude Code, under instructions from @pbakaus.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FTbrWaYuBu9QB8gE99M2xi
Note
Medium Risk
Touches core contrast routing, pixel screenshot logic, and opacity/animation heuristics; corpus behavior shifts (e.g. paused-loop findings dropped, new pixel verdicts) despite broad test coverage.
Overview
Improves low-contrast detection for halos, frosted glass, animation timing, same-color controls, and faint footer wordmarks.
The pixel contrast pass can retry after a “box repainted” refusal: it hides glyphs but keeps
text-shadowwhen the core marks a separating halo (haloSeparates), not outlined text.backdrop-filterunder fills ≥0.8 opaque gets a dedicated reason so pixels can measure it while the sampled pass still refuses. Candidates gainhaloSeparatesvia newhalo_separates_ink(each shadow layer ≥2:1 from ink).Element-pass contrast uses settled opacity for timed entrances caught at 0 (
entrance_end_opacityin folds), treats paused infinite loops that fade to nothing as mid-motion (paused_mid_loop), and picks ≥0.95 opaque overlays as the hit-test surface host for links on glass pills. On link/span safe-tag paths, ink-matching-background 1.0:1 only applies when the text is the sole visible label of a control on the page’s own surface; hidden menu text and decorative spans stay suppressed. The browser safe-tag guard now respectssame_color_surface_is_unread.Decorative text adds ghost wordmarks: huge faint site-name type (size/opacity thresholds, host/title/meta matching) → advisory, with browser-side
settled_opacityso mid-entrance captures are not misclassified.New browser fixture tests cover halo/glass pixel reads and settled-state contrast; extensive unit tests accompany each behavior.
Reviewed by Cursor Bugbot for commit 8b76f98. Bugbot is set up for automated code reviews on this repo. Configure here.