<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/scripts/theme-studio/test-contrast.mjs, branch main</title>
<subtitle>My Emacs configuration
</subtitle>
<id>https://git.cjennings.net/dotemacs/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/dotemacs/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/'/>
<updated>2026-06-09T23:53:05+00:00</updated>
<entry>
<title>feat(theme-studio): add the background-contrast safety core</title>
<updated>2026-06-09T23:53:05+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-09T23:53:05+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=e7021bfe47072d8d9cb0fa6ec8d240d877f13cf0'/>
<id>urn:sha1:e7021bfe47072d8d9cb0fa6ec8d240d877f13cf0</id>
<content type='text'>
A background overlay sits behind many foregrounds at once, so its real constraint is the worst-case contrast over the whole set, not the single fg/bg pair the contrast cell shows today. Phase 3 adds three pure functions in app-core.js for that.

fgSetFor(face, state) builds a covered face's foreground set: the distinct syntax-token colors plus the default foreground, each labeled by syntax role. It returns a structured reason ('out-of-scope' or 'empty') rather than a bogus set when the face isn't covered or has no syntax assignments. floor(bgHex, fgSet) returns the minimum WCAG contrast over that set with the limiting foreground's hex and label. lMax(hue, chroma, fgSet, target) finds the lightest background that still clears the target, scanning L up from black to bracket the dark-side crossing then binary-searching it, and reports status ok/none/all/clamp.

state is passed explicitly (covered set, syntax assignments, default fg) so the functions read no globals and the Node tests stay direct. The closed five-face covered set lives here as COVERED_FACES, shared with app.js. Tests include the sterling keyword-blue worst case as a fixture, plus lMax's none/all/clamp branches.
</content>
</entry>
</feed>
