Split from #881. That issue's reported case (gradient stops fading to transparent) is fixed separately; this is the second defect found while fixing it, and it affects every translucent background in URL and in-page scans, not only gradients.
What happens
The visual contrast walk never composites a translucent background over what is beneath it. When a layer is translucent, both walk drivers (sample_background_impl in crates/browser/src/snapshot_engine.rs and sampleVisualBackgroundAtPoint in browser-bundle/35-visual.js) recurse with stack_nodes(parent(node), depth + 1). stack_nodes starts at the first stack node that is the parent or inside it, which is the text element again, so the walk re-samples the same translucent node until the depth cap and then composites the sample over itself. The result is the layer's own RGB treated as opaque, labelled +alpha.
Repro
<body style="margin:0;background:#fff">
<section style="padding:80px;background:linear-gradient(180deg, rgba(99,102,241,.08), transparent)">
<p style="color:#67696f;font:16px system-ui">Body copy on a faint tint over a white page.</p>
</section>
</body>
impeccable detect http://localhost:8000/
[low-contrast] browser contrast 1.2:1 median 1.2:1 (need 4.5:1) via analytic-gradient+alpha
True ratio is about 5:1. The 8% indigo tint is judged as solid indigo. The same thing makes white text in a glass card (rgba(255,255,255,.1)) over a dark gradient report 1.0:1 via solid-background+alpha.
What a fix has to settle
I tried the obvious fix (continue down the same elementsFromPoint stack instead of restarting from the parent) on the #881 branch and backed it out, because truthful compositing exposes several things the self-composite was hiding:
- An element's own lower layers (its
url() image, its background-color) sit beneath its gradient and have to be part of the stack, in declared layer order.
- A ground the engine cannot read (cross-origin or missing image, small tiled image, a still-blank canvas pixel) has to end the search as unresolved so the screenshot check decides. Skipping it composites the scrim over the page behind the photo.
- The "worst stop" rule is position blind. Today white text at the dark end of a scrim over a light photo passes by accident (the scrim reads as opaque black). With real compositing the faded end fails, though the pixels where the text sits are fine. Stops that straddle the threshold probably need to go to the pixel check.
- A
pointer-events: none ancestor that paints the ground is missing from the hit stack. The parent recursion reaches it today; a same-stack walk does not.
There are no URL-mode goldens, so this needs a real-browser test (crates/browser/tests/design_system_palette.rs is the precedent). I have a work-in-progress patch and about a hundred probe pages from the attempt.
Filed with AI assistance (Claude Code), at the maintainer's direction.
Split from #881. That issue's reported case (gradient stops fading to
transparent) is fixed separately; this is the second defect found while fixing it, and it affects every translucent background in URL and in-page scans, not only gradients.What happens
The visual contrast walk never composites a translucent background over what is beneath it. When a layer is translucent, both walk drivers (
sample_background_implincrates/browser/src/snapshot_engine.rsandsampleVisualBackgroundAtPointinbrowser-bundle/35-visual.js) recurse withstack_nodes(parent(node), depth + 1).stack_nodesstarts at the first stack node that is the parent or inside it, which is the text element again, so the walk re-samples the same translucent node until the depth cap and then composites the sample over itself. The result is the layer's own RGB treated as opaque, labelled+alpha.Repro
True ratio is about 5:1. The 8% indigo tint is judged as solid indigo. The same thing makes white text in a glass card (
rgba(255,255,255,.1)) over a dark gradient report1.0:1 via solid-background+alpha.What a fix has to settle
I tried the obvious fix (continue down the same
elementsFromPointstack instead of restarting from the parent) on the #881 branch and backed it out, because truthful compositing exposes several things the self-composite was hiding:url()image, itsbackground-color) sit beneath its gradient and have to be part of the stack, in declared layer order.pointer-events: noneancestor that paints the ground is missing from the hit stack. The parent recursion reaches it today; a same-stack walk does not.There are no URL-mode goldens, so this needs a real-browser test (
crates/browser/tests/design_system_palette.rsis the precedent). I have a work-in-progress patch and about a hundred probe pages from the attempt.Filed with AI assistance (Claude Code), at the maintainer's direction.