feat(templates,css,ci): expose a real consumer design API, split Leather's skin out of core - #175
Merged
Merged
Conversation
…her's skin out of core
static/css/style.css documented a `consumer_css` stylesheet hook that
templates/base.html never wired — the extension path was fictional, and
both live consumers ended up shadowing base.html (and everything that
extends it) to get their own stylesheet in at all.
Wire the real thing instead:
- templates/base.html renders `config.extra.consumer_css` (a list) as
<link> tags after core's own style.css, plus a `{% block styles %}`
slot for template-level injection. base.html's header comment now
documents the full block surface (styles/head/nav/footer/ld_json/
scripts/content) that was already there but undocumented.
- bin/typikon-check-assets validates every consumer_css entry resolves
to a real built file, the same way it already does for favicon_path/
logo_path/og_image (confirmed Zola's get_url() does NOT catch a
missing one on its own).
Split Leather's skin out of core:
- Every hard-coded dye color in style.css's interactive-state CSS
(.nav-links hover, a:hover's underline, .buy-btn, .triad-1/2/3,
.products-list/.journal-list, .entry-nav, .faq-anchor) now resolves
through four neutral --accent-1..4 tokens (default: --text-mid,
9.27:1 on --bg) instead of a brand hue directly.
- static/css/skins/leather.css is the first-party skin: it carries the
moved dye tokens, the .dye-entry-*/.swatch-*/.dye-marks content
classes (unused by any core template), and the decorative hover-wash
effects — moved verbatim, nothing rewritten. It maps --accent-1..4
to the dye palette, preserving the #64 WCAG fix (--accent-3 stays
--aporia-interactive, never raw --aporia) one hop deeper.
- ci/contrast.py's parse_root_tokens now merges every :root{} block in
a concatenated source (not just the first) and resolves var()-alias
declarations, not just literal hex — needed for a skin's
`--accent-3: var(--aporia-interactive);` to resolve at all.
- ci/check-interactive-contrast.py now scans core plus this repo's own
first-party skins (FIRST_PARTY_SKINS), so the exact WCAG protection
#64 established doesn't go blind the moment its token moves out of
style.css. Does not extend to arbitrary consumer CSS — MATRIX stays
hand-curated over typikon's own selectors, same scope as before.
- ci/check-interactive-contrast-selftest.py's #64 mutation now targets
the skin's --accent-3 mapping (where the regression actually lives
post-split) instead of core, and restores/verifies both files.
examples/sample-shop opts into the shipped leather skin (and a new
content/dyes.md exercising its .dye-entry-*/.swatch-*/.dye-marks
classes); examples/sample-blog opts into its own consumer_css palette
(different accent hues, no skin). Both render through the unmodified
core templates — neither shadows base.html or any other template.
docs/AGENTIC.md and theme.toml no longer describe the fictional hook;
they describe what's actually wired.
Left for follow-up: the fictional hook's discovery traces to
ardent-tools-site's 14+-template shadow, which this PR does not
un-shadow (both live consumers pin an old commit and won't see this
until their pin bumps — protective, not permission to break them).
Bumping that pin and folding ardent-tools-site's shadowed templates
back onto the new block surface is separate, consumer-side work.
Closes #55
forkwright
pushed a commit
that referenced
this pull request
Aug 18, 2026
🤖 I have created a release *beep* *boop* --- ## [0.5.0](v0.4.2...v0.5.0) (2026-08-17) ### Features * **templates,css,ci:** expose a real consumer design API, split Leather's skin out of core ([#175](#175)) ([b61228c](b61228c)) * **templates,schemas:** render audience, derive the journal word count from Zola ([#178](#178)) ([af66fa0](af66fa0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: forkwright <cody@forkwright.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #55
What this exposes
static/css/style.cssdocumented aconsumer_cssstylesheet hook thattemplates/base.htmlnever wired. Both live consumers (ardent-tools-site,ardent-site) discovered this the hard way — one shadowsbase.htmland 14+ downstream templates specifically to work around it (its owntemplates/base.htmlcomment says so).The real hook, wired:
templates/base.htmlrendersconfig.extra.consumer_css— a list of stylesheet paths — as<link>tags after core's ownstyle.css, in declared order.{% block styles %}slot alongside it for template-level (not just config-level) stylesheet injection.bin/typikon-check-assetsvalidates everyconsumer_cssentry resolves to a real built file — confirmed by direct test that Zola'sget_url()does not catch a missing one on its own (it happily renders a<link>to a nonexistent file;zola buildexits 0).templates/base.html's header comment now documents the block surface that already existed but was undocumented:head_extra,nav,footer,ld_json,scripts,content, plus the newstyles.The skin split:
.nav-linkshover,a:hover's underline,.buy-btn,.triad-1/2/3,.products-list/.journal-list,.entry-nav,.faq-anchor) now resolves through four neutral tokens,--accent-1through--accent-4(default:--text-mid, 9.27:1 against--bg), instead of a brand hue directly.static/css/skins/leather.cssis the new first-party skin — the dye tokens,.dye-entry-*/.swatch-*/.dye-markscontent classes (unused by any core template — pure content-authoring vocabulary), and the decorative hover-wash effects, moved verbatim. It maps--accent-1..4to the dye palette, preserving the Keep interactive text contrast compliant in every state #64 WCAG fix exactly (--accent-3stays--aporia-interactive, never raw--aporia) one hop deeper.docs/AGENTIC.mdandtheme.tomlno longer describe the fictional hook — they describe what's actually wired.The two-brand demonstration (issue's acceptance clause)
examples/sample-shopopts into the shipped leather skin (consumer_css = ["css/skins/leather.css"]) plus a newcontent/dyes.mdexercising.dye-entry-*/.swatch-*/.dye-marks.examples/sample-blogopts into its own palette (consumer_css = ["css/site.css"], distinct marine-teal/violet/pine accents, no skin). Neither shadows any template — both build against unmodified core.The rendered
<link>tags differ exactly as declared:And the resolved accent tokens genuinely differ, traced through the real cascade:
Mutation evidence (hook is real, not decorative)
Removed
sample-shop'sconsumer_cssdeclaration, rebuilt: the<link rel="stylesheet" href=".../css/skins/leather.css">tag disappeared from the built HTML head (the skin file itself stays copied underpublic/css/skins/via Zola's unconditional static passthrough — it's the reference that vanished, which is what a browser actually needs to apply it). Restored the declaration, rebuilt: the link reappeared,git diffon the file returned to 0 lines against the intended state, and the full gate re-passed clean.Also mutation-tested the WCAG regression guard itself: reverting the skin's
--accent-3: var(--aporia-interactive);tovar(--aporia);(the original #64 regression, now one layer of indirection down) failsci/check-interactive-contrast.pyclosed with the expected "below the 4.5:1 WCAG 1.4.3 floor" message — proving the contrast gate still protects the moved color, not just the core file. Restored, byte-identical, gate re-passes.Verification
bash ci/run-fixtures.sh— REAL_EXIT=0 withpa11y-cireachable on PATH (every stage passes:typikon-check-assets,zola-check/build,csp-enforce,asset-provenance,lychee,pa11y— 5 and 11 routes respectively,playwright-smoke— 16 and 27 checks,local-base-gate-check,check-xml-output,check-faq-rendering).With this box's actual default PATH (
pa11y-ciinstalled at~/.npm-global/bin, not on PATH) — REAL_EXIT=1, failing closed on exactlypa11ywith"pa11y-ci not on PATH". This is a known environment gap, not a regression from this change — every other stage, includingplaywright-smoke's dynamic contrast assertions against both fixtures' real rendered colors, passes either way. Not "fixed" by weakening the stage.bin/typikon-check examples/sample-blogandexamples/sample-shop— both exit 0, every stage (gate-artifacts-ignoredthroughplaywright-coverage) passing, run individually and re-verified after the mutation test above.ci/check-interactive-contrast-selftest.py— passes; its#64mutation fixture now targets the skin file's--accent-3mapping (where that regression actually lives post-split) and restore/re-pass is verified byte-identical for bothstyle.cssand the skin.Left for follow-up
Both live consumers pin
themes/typikonat4f8c1531(30 commits behind) — they won't see any of this until their pin bumps, which is protective, not permission to have broken them. This PR does not touch either consumer repo. Bumpingardent-tools-site's pin and folding its 14+ shadowed templates back onto the new block surface (so it stops re-diffing every upstream change by hand) is separate, consumer-side work — tracked as a natural next issue once this lands.