Skip to content

Contrast: read halos, near-opaque glass and settled states - #1044

Merged
pbakaus merged 23 commits into
mainfrom
corpus/round11-contrast
Oct 19, 2026
Merged

pbakaus merged 23 commits into
mainfrom
corpus/round11-contrast

Conversation

@pbakaus

@pbakaus pbakaus commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Fixes the contrast residue from corpus observations-1004 branch 3 and the contrast misses in walkthroughs-1004.

URL engine:

  • When the pixel pass refuses copy because hiding it repainted most of the box (a wide dark halo under light copy over a photo), it takes a second reading with the text-shadow kept. 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.
  • A backdrop-filter under a fill at least 0.8 opaque no longer blocks the pixel pass (new reason backdrop filter under a near-opaque fill). The sampled pass still refuses it.
  • The hit-test stacks treat a fill at least 0.95 opaque as the surface the text sits on, so nav links on a 0.95 white pill are scored.
  • On the link and span path, a same-colour pair reports 1.0:1 only for the whole visible label of a link or button on a box of the page's own (a white "Start Chat" on its white card). A hidden label beside an icon, a span in no control, and the page ground are still dropped. Styled controls and the static engine are unchanged.
  • A box a timed one-shot entrance holds at opacity 0 at capture counts at the entrance's end frame.
  • A box a paused loop holds part way through is not at rest when the loop fades to 0.1 or below. A paused breathing loop that never vanishes is read at its frame. That split is a judgment.
  • The site's own name set huge and faint is a decorative wordmark and reports as advisory (taste call r3-04). It applies at 120px, or at 3x the h1 and at least 80px, with 0.25 or less of its ink showing, and its words must match the host, og:site_name, application-name, or the title's first or last part.

Corpus:

  • No new ratchet violations on runs 1004, 47, 42 and 35. The ratchet replays the element pass only, so it does not cover the halo and backdrop changes.
  • On run 1004 the element pass adds 16 findings and moves 9 ghost wordmarks to advisory; run 35 moves 6 more. The 16 are missed nav links, two white-on-white buttons, and an entrance-held CTA reported under three rules.
  • A side-by-side live recapture covered the 7 evidence sites plus 10 sites with text-shadow glows and halos. The pixel side gains counted verdicts where the pass used to refuse or block: smartbi.ca 11, imageto3d.studio 2, a grey count badge on fastsocial.co, klimaschutz-niedersachsen.de 3.4:1, rhem.ai 3.0:1. It loses iubcr.vercel.app's four paused-loop readings.
  • Nothing new fails on the glow and halo sites. Every other difference reproduces with both engines on the same capture.
  • Known leftover: an incoming crossfade slide caught at opacity 0 is now counted while the outgoing one still paints.

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-shadow when the core marks a separating halo (haloSeparates), not outlined text. backdrop-filter under fills ≥0.8 opaque gets a dedicated reason so pixels can measure it while the sampled pass still refuses. Candidates gain haloSeparates via new halo_separates_ink (each shadow layer ≥2:1 from ink).

Element-pass contrast uses settled opacity for timed entrances caught at 0 (entrance_end_opacity in 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 respects same_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_opacity so 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.

pbakaus and others added 8 commits October 15, 2026 09:38
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
Copilot AI balanced review requested due to automatic review settings October 15, 2026 17:37
@greptile-apps

greptile-apps Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; the numbered previous finding is fixed, and no new actionable issue was established.

Summary

Improves browser contrast checks for halos, near-opaque glass, hidden labels, and settled animation states.

  • The screenshot pass can score text over separating halos and near-opaque glass.
  • Browser checks score same-color text when it labels a control on its own surface.
  • Browser contrast checks use the opacity a timed entrance settles at.
  • Faint oversized versions of a site’s name count as decorative wordmarks.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Browser text] --> B[Read surface and settled opacity]
  B --> C[Element contrast check]
  B --> D[Uncertain background]
  D --> E[Compare screenshots with text hidden]
  E --> F{Separating halo and box repainted?}
  F -->|Yes| G[Retry with halo kept]
  F -->|No| H[Pixel result]
  G --> H
  C --> I[Warning or decorative advisory]
  H --> I
Loading

Reviews (9) · Last reviewed commit: "Judge a ghost wordmark at the opacity it..." · Reviewed by Greptile

Comment thread crates/core/src/browser/element_checks.rs Outdated
Comment thread crates/core/src/browser/painted.rs
Comment thread crates/core/src/browser/decorative_text.rs Outdated
Comment thread tests/fixtures/antipatterns/low-contrast-pixel-reads.html

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Incorrect contrast edge cases, repeated document traversal, and stale aggregate oracle expectations remain unresolved.

6 open findings
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.

Comment thread crates/core/src/browser/decorative_text.rs Outdated
Comment thread crates/core/src/browser/decorative_text.rs Outdated
Comment thread crates/core/src/browser/element_checks.rs
Comment thread crates/core/src/browser/element_checks.rs Outdated
Comment thread crates/core/src/browser/painted.rs

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/core/src/browser/visual.rs
Comment thread crates/core/src/browser/decorative_text.rs
pbakaus and others added 3 commits October 15, 2026 11:09
- 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
Comment thread crates/core/src/browser/element_checks.rs
pbakaus and others added 3 commits October 15, 2026 11:26
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
@impeccable-reviews

impeccable-reviews Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Retrigger
Nothing to fix.

5 of 5 5/5 · Ready: no major issues found
This push leaves nothing to fix.

About this review
  • crates/live/assets/detect-antipatterns-browser.js is too large to read.
  • Read from the code alone; no page was opened.

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

pbakaus and others added 4 commits October 15, 2026 11:29
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
@github-actions github-actions Bot added blocked: merge conflicts PR cannot merge until conflicts are resolved waiting on contributor Waiting for the PR author to respond or make changes labels Oct 16, 2026
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
Comment thread crates/core/src/browser/painted.rs
Comment thread crates/core/src/browser/element_checks.rs
Comment thread crates/core/src/checks/decorative_text.rs Outdated

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread crates/core/src/browser/element_checks.rs
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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread crates/core/src/browser/element_checks.rs Outdated
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
@github-actions github-actions Bot added needs maintainer review Ready for a maintainer to review or decide and removed blocked: merge conflicts PR cannot merge until conflicts are resolved waiting on contributor Waiting for the PR author to respond or make changes labels Oct 18, 2026
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>
Comment thread crates/core/src/browser/decorative_text.rs Outdated
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>
@pbakaus
pbakaus merged commit 0665bd8 into main Oct 19, 2026
20 checks passed
@linear-code

linear-code Bot commented Oct 19, 2026

Copy link
Copy Markdown

REN-905

abdulwahabone added a commit that referenced this pull request Oct 19, 2026
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>
@pbakaus pbakaus mentioned this pull request Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs maintainer review Ready for a maintainer to review or decide

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants