markKeptSurfaces() marks any element whose declared background is below LIGHT_SURFACE_LUMINANCE, with no area cap, so a 600px layout card is marked
exactly like a button. The CSS then exempted the marked element and its whole
subtree:
*:not([data-ihm-keep]):not([data-ihm-keep]*)
A light table nested inside a dark card was therefore never touched. In the
reported specimen 14 of 21 light panels survived and rendered as beige sheets
in an otherwise themed message.
The change
Being inside a painted surface is no longer inherited past a sheet.
The walk now tracks that state and emits a second mark, data-ihm-in-keep, on
elements that sit on paint with no background of their own. The CSS exempts
those explicitly rather than exempting every descendant. A nested light sheet
ends the protection, and paint resumes below it — so a button inside such a
sheet is still kept whole.
The walk is iterative rather than recursive; this runs over untrusted mail and
deeply nested tables are what old newsletter HTML is made of.
Alternatives not taken
Both were raised in the report, and it ranked them correctly:
Dropping the descendant half of the selector puts back exactly what #294
fixed. <td bgcolor="#1155CC" style="color:#fff"> with a nested <a> is
common, and the label goes unreadable.
An area threshold is a magic number that misfires on both a legitimate
full-width hero banner and a small dark panel with a light chip in it.
Tests
Four, all in web/src/lib/__tests__/html.test.ts, and worth a note on how they
assert. They pull the neutraliser selector out of EMAIL_BASE_CSS and ask el.matches() — so they test what the reader sees, and cannot drift from the
rule they check.
That matters here. The first draft asserted on which attributes got set, and passed against the unfixed code — the bug was in the rule that reads the
marks, not in the marking, so attribute assertions proved nothing. All four now
fail without this change:
× keeps a coloured button and drops the white sheet around it
× neutralises a light panel nested inside a dark painted card
× still keeps a button that sits inside a nested light panel
× leaves no light panel exempt across the whole reported specimen
The last reconstructs the reported shape: 21 light panels, 14 nested in dark
cards, and asserts none is left exempt while all 7 cards stay painted.
Full suite: 1153 passing across 113 files. Typecheck clean.
Still wanted
@taisau offered to run a patch against the real specimen, with the
white-wrapper template as the regression. That is the check this cannot do from
a reconstruction, so I would like that before merging.
Closes #310. Reported by @taisau in
https://github.com/Coffey-Labs/ihasmail/issues/290#issuecomment-5593496568
## The bug
`markKeptSurfaces()` marks any element whose declared background is below
`LIGHT_SURFACE_LUMINANCE`, with no area cap, so a 600px layout card is marked
exactly like a button. The CSS then exempted the marked element **and its whole
subtree**:
```css
*:not([data-ihm-keep]):not([data-ihm-keep] *)
```
A light table nested inside a dark card was therefore never touched. In the
reported specimen 14 of 21 light panels survived and rendered as beige sheets
in an otherwise themed message.
## The change
Being inside a painted surface is no longer inherited past a sheet.
The walk now tracks that state and emits a second mark, `data-ihm-in-keep`, on
elements that sit on paint with no background of their own. The CSS exempts
those explicitly rather than exempting every descendant. A nested light sheet
ends the protection, and paint resumes below it — so a button inside such a
sheet is still kept whole.
The walk is iterative rather than recursive; this runs over untrusted mail and
deeply nested tables are what old newsletter HTML is made of.
## Alternatives not taken
Both were raised in the report, and it ranked them correctly:
- **Dropping the descendant half of the selector** puts back exactly what #294
fixed. `<td bgcolor="#1155CC" style="color:#fff">` with a nested `<a>` is
common, and the label goes unreadable.
- **An area threshold** is a magic number that misfires on both a legitimate
full-width hero banner and a small dark panel with a light chip in it.
## Tests
Four, all in `web/src/lib/__tests__/html.test.ts`, and worth a note on how they
assert. They pull the neutraliser selector **out of `EMAIL_BASE_CSS`** and ask
`el.matches()` — so they test what the reader sees, and cannot drift from the
rule they check.
That matters here. The first draft asserted on which attributes got set, and
**passed against the unfixed code** — the bug was in the rule that reads the
marks, not in the marking, so attribute assertions proved nothing. All four now
fail without this change:
```
× keeps a coloured button and drops the white sheet around it
× neutralises a light panel nested inside a dark painted card
× still keeps a button that sits inside a nested light panel
× leaves no light panel exempt across the whole reported specimen
```
The last reconstructs the reported shape: 21 light panels, 14 nested in dark
cards, and asserts none is left exempt while all 7 cards stay painted.
Full suite: 1153 passing across 113 files. Typecheck clean.
## Still wanted
@taisau offered to run a patch against the real specimen, with the
white-wrapper template as the regression. That is the check this cannot do from
a reconstruction, so I would like that before merging.
**Merged** 2026-09-08 as coffey-labs/ihasmail@38fb78a095fb
<sub>Rebuilt from: git history, notification email, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #310. Reported by @taisau in
https://github.com/Coffey-Labs/ihasmail/issues/290#issuecomment-5593496568
The bug
markKeptSurfaces()marks any element whose declared background is belowLIGHT_SURFACE_LUMINANCE, with no area cap, so a 600px layout card is markedexactly like a button. The CSS then exempted the marked element and its whole
subtree:
A light table nested inside a dark card was therefore never touched. In the
reported specimen 14 of 21 light panels survived and rendered as beige sheets
in an otherwise themed message.
The change
Being inside a painted surface is no longer inherited past a sheet.
The walk now tracks that state and emits a second mark,
data-ihm-in-keep, onelements that sit on paint with no background of their own. The CSS exempts
those explicitly rather than exempting every descendant. A nested light sheet
ends the protection, and paint resumes below it — so a button inside such a
sheet is still kept whole.
The walk is iterative rather than recursive; this runs over untrusted mail and
deeply nested tables are what old newsletter HTML is made of.
Alternatives not taken
Both were raised in the report, and it ranked them correctly:
fixed.
<td bgcolor="#1155CC" style="color:#fff">with a nested<a>iscommon, and the label goes unreadable.
full-width hero banner and a small dark panel with a light chip in it.
Tests
Four, all in
web/src/lib/__tests__/html.test.ts, and worth a note on how theyassert. They pull the neutraliser selector out of
EMAIL_BASE_CSSand askel.matches()— so they test what the reader sees, and cannot drift from therule they check.
That matters here. The first draft asserted on which attributes got set, and
passed against the unfixed code — the bug was in the rule that reads the
marks, not in the marking, so attribute assertions proved nothing. All four now
fail without this change:
The last reconstructs the reported shape: 21 light panels, 14 nested in dark
cards, and asserts none is left exempt while all 7 cards stay painted.
Full suite: 1153 passing across 113 files. Typecheck clean.
Still wanted
@taisau offered to run a patch against the real specimen, with the
white-wrapper template as the regression. That is the check this cannot do from
a reconstruction, so I would like that before merging.
Merged 2026-09-08 as coffey-labs/ihasmail@38fb78a095
Rebuilt from: git history, notification email, session transcript.