Blog SVGs: raise six muted-grey text classes to the WCAG AA floor for their rendered size - #188
Conversation
Nineteen text nodes across five blog SVGs rendered below WCAG 3:1 on light grounds. Each is a class that declares its own muted grey and gets it, so nothing in the cascade was eating anything -- but `.none` at 1.70:1 reads as a rendering fault rather than as intentional de-emphasis. Keeps the hue, changes only the lightness, by the smallest 8-bit step that clears the AA floor for the node's rendered size (4.5:1 under 18px, or under 14px bold; 3:1 at or above). Six class fills, five files, six lines. No hue, font size, geometry, ground colour or wording changed anywhere. Ratios re-measured in Chromium at native viewBox size, ground sampled from a text-hidden raster of the same asset. Colour, ground, position and font drift across the other 1,925 measured nodes in the corpus: 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
|
PATCH ROUND — one value. Everything else in this PR is accepted; do not touch it. The gates are green (union I at The defect is in one word of the method: The ground under a node is taken as the median of nine samples across its glyph box. For a node on a flat ground that is the right statistic. For a node on a gradient, the median is the middle of the range the text crosses — and WCAG is not satisfied by the middle, it is satisfied by the worst point.
Seat measurement, same engine, same
At the string's leftmost glyphs it is 4.48:1, and against the gradient's actual The other 21 measurements are unaffected, because every other touched node sits on a flat ground; re-scored worst-case they come back 4.50–4.55 exactly as you have them, and the dark-card What to do:
Update the PR body's table and the "none by more than 0.06" line to match what you actually measure. Note for the report: this is worth a sentence in its own right, because it generalises. Every ground-sampling check this repo grows from here — #181's accent gold, and #183 if it is ever taken up — will meet gradient grounds, and "median ground" will keep passing things by a hair. Nineteen nodes was small enough that the difference showed up as 0.04; over 217 nodes it would not have been noticed at all. Serial constraints unchanged: #188 still owns these five SVGs alone. #182, #186 and #187 have landed on Generated by Claude Code |
… ground The ground under a text node was taken as the median of nine samples across its glyph box. That is the right statistic for a flat ground and the wrong one for a gradient: WCAG is satisfied at the worst point a string crosses, not at the middle of the range. .footdim is the one node in this set on a gradient — a single 16px line in a 1200-wide composition whose background ramps #f3f6fb to #eef2f8. Median ground #f0f4f9 scored 4.518; the worst ground under its own glyph ink is #eff3f8 (4.478) at native size and #eef2f8 (4.441) at 0.667x and 2x, so the line was under the 4.5 floor at every size the site actually renders it. Re-running the smallest-step search from the authored #8494b0 — hue and saturation held exactly, each candidate quantised to 8-bit sRGB before scoring, against #eef2f8 — gives #5d6f90: 4.513 on #eef2f8, 4.550 on #eff3f8, 4.590 on the old median ground. Hue drifts 0.64 degrees, saturation 0.2178 to 0.2152, both 8-bit rounding. The other 21 touched occurrences sit on uniform grounds and are unmoved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
|
ACCEPT — the patch round did what it was sent back for, re-measured and looked at.
All 22 occurrences now clear AA at their worst point. The other 21 are unmoved at 4.50–4.55 and the inline- The thing that could have gone wrong didn't. The comparison cover carries a deliberate two-tier footer, and darkening the lower tier risked flattening it. Measured:
The hierarchy survives, and it survives visually too. All five assets rendered at native size and looked at: the sourcing footnote still reads as secondary to the line above it; the four Union with current A note on reviewing this branch, for the record. A two-dot On the generalisation you were asked to draw. Median-ground was the right statistic for a flat ground and it passed 18 of 19 nodes; the one it missed, it missed by 0.04. Over the 217 nodes #183 would touch, that error would not have been visible at all. The corrected harness is what makes #181 and #183 safe to measure later, and that is worth more than the one hex value this round changed. Landing on Generated by Claude Code |
Fixes #166
Nineteen text nodes across five blog SVGs rendered below WCAG 3:1 on light grounds. Six class fills, five files, six lines changed. No hue, font size, geometry, ground colour or wording changed anywhere.
These are not the #140/#167 family. Every one is a class that declares its own muted grey and gets it — nothing in the cascade is eating anything. They were recorded rather than swept because re-picking them is a palette decision. The card was taken up on the seat's ruling that
.noneat 1.70:1 does not read as intentional de-emphasis, it reads as a rendering fault.The rule applied
Keep the hue, change the lightness, by the smallest step that clears the AA floor for the rendered size.
getComputedStyle().fontSizeat nativeviewBoxscale, not inferred from the class name. It mattered:.axisis 15px at weight 640 and.footdimis 16px, so both take the 4.5 floor rather than the 3.0 one a "large label" reading would have given them..capbinds on#fffefa, not on the#ffffffsix of its seven nodes sit on..footdimbinds on the dark end of the gradient it crosses, not on the middle of it.Before / after — every node changed
Ratios are against the worst ground pixel under each node's own glyph ink (see How it was measured).
enterprise-ontology-platform-comparison/cover-en.svg.footdim#8494b0#5d6f90#eff3f8enterprise-ontology-platform-comparison/read-write-line-en.svg.axis#8494b0#5e7293#f3f6fbforward-deployed-engineer-vs-solutions-architect/cover-en.svg.eyebrow#8a9a93#6a7a73#ffffffontology-vs-semantic-layer-vs-knowledge-graph/cover-en.svg.cap#8aa096#647b71#ffffffontology-vs-semantic-layer-vs-knowledge-graph/cover-en.svg.cap#8aa096#647b71#fffefaontology-vs-semantic-layer-vs-knowledge-graph/cover-en.svg.muted#a2b3ab#657b71#ffffffontology-vs-semantic-layer-vs-knowledge-graph/decision-table-en.svg.none#b3c0b9#60746a#fdf1e8Nineteen nodes, every one clearing its floor on the worst point of its own ground, none by more than 0.06 — the margins run from 0.002 (
.none) to 0.052 (.cap)..footdimis scored twice, and the darker reading is the one it was solved against. Its ground is the only non-uniform one in the set: a 1200-wide background ramping#f3f6fbto#eef2f8, under a card shadow that has almost faded by the footer. Under the node's own ink the worst pixel is#eff3f8at native size and#eef2f8— the gradient's own dark stop — at 0.667x and 2x, both of which the site renders. The smallest hue-and-saturation-preserving 8-bit step from the authored#8494b0that clears 4.5:1 against#eef2f8is#5d6f90, and that is the value here: 4.513 on#eef2f8, 4.550 on#eff3f8, 4.590 on the old median ground. Hue drifts 0.64 degrees, saturation 0.2178 to 0.2152.Three notes on what the diff deliberately does not touch:
#8494b0survives twice in the same two files. Incover-en.svgit is also.no(17px bold,read-only), which sits on the dark#1c3258row at 4.157:1 against a 3.0 floor and passes; inread-write-line-en.svgit is also the arrowheadpathin thearrowmarker, which is not text. Only the two failing class declarations moved, so.footdimis now slightly darker than.noand the axis labels slightly darker than the arrow they sit on..capand.mutedland one 8-bit step apart (#647b71/#657b71), which is not a typo..mutedstarted lighter than.capbut sits only on#ffffff, while.capmust also clear the darker#fffefa; the floor is the same, so the two converge. Their roles stay distinguished by size, weight, case and letter-spacing..capnodes on the dark ontology card are untouched. They carry an inlinestyle="fill:#9dc4b6"that already wins the cascade and reads at 5.748:1. The measurement confirms it: they appear in neither the recolour list nor the drift list.No collateral change — measured, not asserted
The full sweep was re-run at
b2f772fand compared node-by-node againstorigin/mainat435ff61, keyed on file plus DOM position plus text content:The comparison checks computed fill, sampled ground, contrast ratio, glyph box and font size/weight on every node. Drift count 0 on all of them; the 1,984 nodes this PR does not recolour come back identical. The sampled grounds are unchanged too, which is the expected result of a text-fill-only diff and worth having as evidence rather than as an assumption.
The corpus is 2,003 nodes rather than the 1,944 of the previous round because
mainmoved under the branch: #182, #186 and #187 landed, and one of them brought a new post with two SVGs. None of the three touches these five files, and the merge was clean.Corpus-wide, on the same statistic used throughout this PR, nodes below 3:1 go from 28 to 9. Of the nine remaining, two are the accent-gold pair filed as #181 and seven are the text-over-graphic collisions filed as #193 — a class no fill value fixes, and which is invisible to the median statistic this PR replaces. On the old median statistic, for comparison with the card, the count goes from 21 to 2, which is exactly the population #166 recorded.
How it was measured
text{…}cascades cannot be read off the source, so measurement runs in the shipping renderer, the method #167 established. Chromium loads each asset at its nativeviewBoxsize; for everytext/tspanleaf it readsgetComputedStyle().fill, the computed font size and weight, and the glyph box. The ground is sampled from a pixel rather than walked out of the DOM: each asset is rasterised a second time withtext{visibility:hidden}injected as an SVGstyleelement on the root. (addStyleTagcannot be used — an SVG document has noheadelement.) The hide is verified per file: the raster is rejected if any text node still computes to visible.The ground statistic is the worst point, not the middle one. The previous round took the median by luminance of nine samples across the glyph box. That is the right statistic for a flat ground and the wrong one for a gradient: WCAG is satisfied at the worst point a string crosses, not at the midpoint of the range it crosses. The ground is now the sample with the lowest contrast against that node's own fill — the darkest pixel on a light ground, the lightest on a dark one — taken:
.footdimit is#eff3f8at native size and#eef2f8at the other two — a 0.037 spread in the resulting ratio, straddling the floor.The old median is still recorded on every node, so the two statistics can be compared directly rather than argued about; every number in this PR that is labelled "median" comes from that column.
Renders — looked at
All five touched assets were re-rendered at native size in the same engine and looked at, before and after, one by one:
decision-table-en.svg— the four— none —cells were washing out into the#fdf1e8verb-gap band; they now read, and are still the faintest thing in the table (4.50 against.ideaat 7.09 and the mono cells at 11.8), so the "deliberately absent" reading survives.ontology-…/cover-en.svg—UNIT/ANSWERS/PRIMITIVESon the two light cards now match the weight of their counterparts on the dark card, which is how the design reads them.no actions · no permissionsis legible and still visibly secondary to themetric · dimension · joinline above it.forward-deployed-engineer-…/cover-en.svg— the three eyebrows now read likeUNTIL IT RUNSon the dark card, and still sit below the role names in prominence.enterprise-ontology-platform-comparison/cover-en.svg— the two-tier footer survives the extra step. The sourcing footnote at#5d6f90reads 4.550:1 on its worst ground; the.footline directly above it, untouched at#566480, reads 4.936:1 on its own worst ground (#e7eaf0— darker than the footnote's, because the card's drop shadow has not finished fading at that line). The footnote is still the lighter of the two by relative luminance, 0.1571 against 0.1265, and the two crops at 2x are visually indistinguishable from the previous round's. Nothing about the tier reading changed; one 8-bit step is not a perceptible move.read-write-line-en.svg—DESCRIBEandACTread as axis labels, still lighter than the heading.Nothing became body text; every darkened label still reads as de-emphasised.
Gates
Run through the shared verification lock at head
b2f772f(git rev-parse --short HEAD), each exit code captured by redirecting to a file before any pipe, withgit status --porcelainempty afterwards.b2f772fpnpm content:lint✓ content lint passed (335 files, 44 glossary terms checked)pnpm content:lint --published✓ content lint passed (335 files, 44 glossary terms checked)pnpm checkResult (135 files):·- 0 errors·- 0 warnings·- 0 hintspnpm build[build] 867 page(s) built in 36.98s·[build] Complete!pnpm seo:smokeSEO smoke test passed (866 HTML pages checked)Lock verdict:
VERDICT command-exit 0 · held the lock 50s · waited 0s.pnpm buildrunsgen:zh-hantfirst —✓ zh-Hant: generated 46, kept 0 hand-maintained, 5 with body links pointed at /zh-Hant/— and the tree was clean afterwards, so noindex.zh-Hant.mdxdrift rides along. No changeset — this repo has none.Notes on the card's premises
.footdim1,.axis2,.eyebrow3,.cap7,.muted2,.none4. Blog SVGs: give dark-card text an explicit fill so it stops rendering dark-on-dark #167 landing in between moved none of them, which is the expected result given it only touched dark-ground nodes.enterprise-ontology-platform-comparison,forward-deployed-engineer-vs-solutions-architectandontology-vs-semantic-layer-vs-knowledge-graphcontain no non--enSVG at all. The classes involved do appear in other posts (.capin eight other files,.accentin two), and every one of those instances was measured and passes.#8aa096→ "roughly#6b8177" for 4.5:1 on white; the measured smallest step is#647b71. The guess lands at 4.19:1 — under the floor, which is why the ruling's instruction to re-measure rather than trust it was the right call.?onenterprise-ontology-race-open-vs-closed/cover{,-en}.svg, at 1.79:1. It is not fixed here and it is not a muted grey — it is the composition's emphasis mark, and.accentalso fills five padlockrectshapes, so recolouring the class would change non-text geometry while an inline override would split the accent into two golds 28px apart inside one composition. Both routes are palette decisions of exactly the kind the ruling fenced off, so it is filed as The accent-gold?onenterprise-ontology-race-open-vs-closed/cover{,-en}.svgreads at 1.79:1 — but the mechanical darkening rule cannot be applied to it #181 with the measurement and the three real options..monoclass on a group element never reaches its text — code blocks render in the sans-serif house face #165 owns were flagged by neither pass.ai-ontology-open-protocol/one-definition-four-surfaces{,-en}.svgandobjectos-automation-engine/run-record-en.svghave no node below 3:1 and none below its AA floor, so there was nothing to report or defer on them.Out of scope, filed
?onenterprise-ontology-race-open-vs-closed/cover{,-en}.svgreads at 1.79:1 — but the mechanical darkening rule cannot be applied to it #181 — the accent-gold?pair at 1.79:1, above.#647872(102 nodes) and#5d736b. That is roughly half the blog corpus and amounts to re-picking the site's secondary text palette, so it is recorded for a decision rather than swept under this card. Two figures for whoever picks it up:mainhas grown since it was filed, so the same median-ground count at this head is 228 nodes in 46 files, and the corrected worst-ground statistic makes it 242 in 54 files. The population is the same house greys either way; the correction widens it by about 6%, not by an order of magnitude.All filed unassigned. Triage labels are the PM's to place.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Generated by Claude Code