* The rendered rules measure what rendered: the line instead of the box that holds it, the space around the text instead of the declared padding, gray by chroma at its own lightness, the AI palette by two tell hues rather than one accent, and the eyebrow named as the element its own design document declares
Five browser rules were reporting something other than what the reader sees,
and a review that charges those numbers is charging noise.
- `line-length` measured `rect.width / (fontSize * 0.5)`, the box's capacity.
A paragraph in a 1022px column whose text stops at 571px was charged 142
characters a line it never rendered. The probe now hands back the client
rects of the direct text one per line box (`direct_text_line_rects`), the
characters divide between the lines in proportion to the ink each carries,
and the charge needs more than one long line: the harm named is the eye
tracking back to the start of the next line, which takes a column to do.
- `cramped-padding` read the declared padding. A 44px control with
`padding: 0 16px` and a flex-centred label has 12px of air above the label
and was charged "0px vertical padding"; the measurement is now the inset
between the rendered text and the inside of the border box. Its wrapper half
read a text-bearing child's border box the same way, so a `<td>` that fills
its table and insets its own text counted as flush; it reads the text now.
- `gray-on-color` called anything under 0.85 relative luminance gray, which
takes in every off-white: `#e8edf2` measures 0.84 there and 0.93 as
lightness. Gray is now low chroma at the lightness the ink actually sits at
(saturation, which is chroma normalized for lightness) and neither of the
two neutral inks a coloured surface carries. The contrast check beside it is
untouched, and the recorded vectors still pass.
- `ai-color-palette` charged every hue between 160° and 200° on a dark ground
as neon, which lit one ordinary teal accent 18 places on a page with nothing
wrong with it. A gradient in a tell hue is still the pattern on its own;
flat neon ink on near-black waits for a second tell hue to turn up somewhere
on the page, because one saturated accent on a dark system is an accent.
- `kicker-above-heading` reported against `body`, so a charged row had nothing
to point at, and it fired on eyebrows a design document documents. It names
the eyebrow element now, and stands down where the repository's DESIGN.md
declares the class by name — the prose's backticked class selectors travel
on the design-system config the colour and radius rules already read.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
* A rendered line is the whole line: the fragments merge, the snapshot carries them or the rule stands down, an ignored subtree gets no vote in the palette, and a class a design document forbids declares nothing
The review threads on #840 found four ways the new rendered rules were
still measuring something other than what the reader sees.
- `line-length` read one `getClientRects()` box per direct text node and
called each of them a rendered line. An inline `<strong>` in the middle
of a sentence is its own text node, so one visual line arrived as two
fragments and the paragraph's characters were divided between them —
and the `<strong>`'s own text was never measured at all while its
characters still counted toward `text_len`. Both halves of the measure
are the same text now: the probe collects the rects of every text node
under the element, and `Dom::text_line_rects` merges the ones that share
a row back into the line they rendered as. A column of long lines split
by inline markup used to charge nothing; it charges now, and a wrapped
sentence in two fragments is one line, not two.
- The same function divided every rect by the line box to recover a line
count. On live per-line rects that was double-counting: a leading
tighter than the glyph box makes `round(height / line_box)` 2, and one
long line pushed twice satisfied "at least two long lines". Nothing is
divided any more, because nothing that reaches the rule is a union.
- The union was what a snapshot-backed scan had — the extension's
offscreen document and any strict-CSP page — and a union of a long
first line and a short tail is the same union as two even lines, so
every line inferred from it was invented. The capture records the rects
now (`dl`, with `textLines` on the snapshot saying it did), and a
capture that did not answers `None`: the rule stands down rather than
guessing. That is also what any other DOM that cannot split a wrapped
run answers.
- `ai-color-palette` accumulated its tell hues before the scoped ignores
ran. A cyan tell inside a `data-impeccable-ignore="ai-color-palette"`
subtree opened the page-wide two-hue gate and charged neon ink on an
element nobody had waived. Ignored content now gets no vote.
- `declared_component_selectors` took every backticked class in DESIGN.md
as a declaration, including the ones the document writes down in order
to forbid them. "Do not write a new `.hero-cta-primary`" exempted
`.hero-cta-primary` from `kicker-above-heading` — the parser silencing
exactly the misuse the document was written to catch. Each occurrence
is read in the document's own structure now: the heading chain above it
(a "Don't" section, and its subsections with it) and the clause it sits
in, where a clause is cut on punctuation and on the phrases that turn a
sentence around. "No ALL CAPS outside the `.eyebrow` class" still
declares `.eyebrow`, because what "no" governs ends at "outside"; a
class the document calls deprecated anywhere is declared nowhere.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
* A snapshot records each line once, a line stops at the gutter, and a design document's directive governs what follows it
The re-review of the first round found four places where the new
measurements reached further than they should.
- The snapshot recorded the rects of every text node *under* each element,
so a line rendered deep in a page was written down again for every
ancestor above it. On a deep, text-heavy page that multiplies the
capture by its depth and can carry it past the byte cap, and a capture
that fails is a scan that returns nothing at all. Each element records
only its own text now — one line, one entry — and `text_line_rects`
assembles an element's lines by walking the tree the capture already
serialized.
- Merging rects by vertical band alone made two columns that happen to
sit on the same rows into one page-wide line. A fragment joins a row now
only when it runs on from it: a horizontal gap no wider than the row's
own line box. The fragments of a wrapped line are contiguous; a gutter
is not. An inline image wider than the leading splits its line by the
same test, which understates a line rather than overstating it, and that
is the direction this rule should err in.
- In DESIGN.md, where the negative word sits decides what it governs. A
state ("`.card-old` is deprecated") describes whatever its clause is
about; a directive ("never use `.x`") condemns what follows it and
nothing before it. Reading the whole clause for either lost the
sanctioned half of "Use `.kicker` and never `.tagline`". The headings a
document uses to retire a set — "Retired components", "Unsupported
patterns" — are read as negative now, and a heading that names both
sides ("Dos and Don'ts") heads a section of both, so its subsections are
what say which is which.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
* A heading names both sides only when something joins them
`DESIGN_BOTH_SIDES_HEADING` asked for a `do` within twelve characters of a
`don't`, which "Don't do this" and "What we don't do" both satisfy while
meaning only the one thing. A section headed either of those stopped
condemning the classes it names, so its counter-examples became declared
components and the rules stood down on exactly the misuse the heading
introduces.
The two sides have to be joined by something that pairs them now — and,
or, a slash, a comma — which is what "Dos and Don'ts" and "Do / Do not"
have and a sentence that happens to contain both words does not.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
* A compound separator pairs the two sides too
"Do's, and Don'ts" and "Do and/or Don't" put a comma and a conjunction, or
a conjunction and a slash, between the two halves, and a joiner that
accepted exactly one token read those headings as negative and dropped the
classes their "Do" sections sanction. One joiner or several, up to four.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
* The probe's two text-rect helpers say which text they read
`__textLineRects` collected only the direct text once the line probe grew
its own deep walk, so its name promised lines it no longer produced: it is
`__directTextRects`, the list the union rect is built from. The snapshot's
union helper takes the rects rather than the node now, and its comment says
so. No behavior.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LQBUunp8QttxZqihybNmtL
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>