Skip to content

feat(templates,css,ci): expose a real consumer design API, split Leather's skin out of core - #175

Merged
forkwright merged 1 commit into
mainfrom
fix/55-consumer-design-api
Aug 17, 2026
Merged

feat(templates,css,ci): expose a real consumer design API, split Leather's skin out of core#175
forkwright merged 1 commit into
mainfrom
fix/55-consumer-design-api

Conversation

@forkwright

Copy link
Copy Markdown
Owner

Closes #55

What this exposes

static/css/style.css documented a consumer_css stylesheet hook that templates/base.html never wired. Both live consumers (ardent-tools-site, ardent-site) discovered this the hard way — one shadows base.html and 14+ downstream templates specifically to work around it (its own templates/base.html comment says so).

The real hook, wired:

  • templates/base.html renders config.extra.consumer_css — a list of stylesheet paths — as <link> tags after core's own style.css, in declared order.
  • A {% block styles %} slot alongside it for template-level (not just config-level) stylesheet injection.
  • bin/typikon-check-assets validates every consumer_css entry resolves to a real built file — confirmed by direct test that Zola's get_url() does not catch a missing one on its own (it happily renders a <link> to a nonexistent file; zola build exits 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 new styles.

The skin split:

  • Every hard-coded dye color in core'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 tokens, --accent-1 through --accent-4 (default: --text-mid, 9.27:1 against --bg), instead of a brand hue directly.
  • static/css/skins/leather.css is the new first-party skin — the dye tokens, .dye-entry-*/.swatch-*/.dye-marks content classes (unused by any core template — pure content-authoring vocabulary), and the decorative hover-wash effects, moved verbatim. It maps --accent-1..4 to the dye palette, preserving the Keep interactive text contrast compliant in every state #64 WCAG fix exactly (--accent-3 stays --aporia-interactive, never raw --aporia) one hop deeper.
  • docs/AGENTIC.md and theme.toml no longer describe the fictional hook — they describe what's actually wired.

The two-brand demonstration (issue's acceptance clause)

examples/sample-shop opts into the shipped leather skin (consumer_css = ["css/skins/leather.css"]) plus a new content/dyes.md exercising .dye-entry-*/.swatch-*/.dye-marks. examples/sample-blog opts 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:

sample-blog:  <link rel="stylesheet" href=".../css/site.css">
sample-shop:  <link rel="stylesheet" href=".../css/skins/leather.css">

And the resolved accent tokens genuinely differ, traced through the real cascade:

accent-1: sample-blog=#14495E  sample-shop=#581523
accent-2: sample-blog=#4A3B7A  sample-shop=#2C1B3A
accent-3: sample-blog=#0F6B52  sample-shop=#4A7350
accent-4: sample-blog=#2E6B8A  sample-shop=#8B5A2B

Mutation evidence (hook is real, not decorative)

Removed sample-shop's consumer_css declaration, rebuilt: the <link rel="stylesheet" href=".../css/skins/leather.css"> tag disappeared from the built HTML head (the skin file itself stays copied under public/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 diff on 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); to var(--aporia); (the original #64 regression, now one layer of indirection down) fails ci/check-interactive-contrast.py closed 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.shREAL_EXIT=0 with pa11y-ci reachable 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-ci installed at ~/.npm-global/bin, not on PATH) — REAL_EXIT=1, failing closed on exactly pa11y with "pa11y-ci not on PATH". This is a known environment gap, not a regression from this change — every other stage, including playwright-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-blog and examples/sample-shop — both exit 0, every stage (gate-artifacts-ignored through playwright-coverage) passing, run individually and re-verified after the mutation test above.

ci/check-interactive-contrast-selftest.py — passes; its #64 mutation fixture now targets the skin file's --accent-3 mapping (where that regression actually lives post-split) and restore/re-pass is verified byte-identical for both style.css and the skin.

Left for follow-up

Both live consumers pin themes/typikon at 4f8c1531 (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. Bumping ardent-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.

…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
forkwright merged commit b61228c into main Aug 17, 2026
4 checks passed
@forkwright
forkwright deleted the fix/55-consumer-design-api branch August 17, 2026 18:25
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expose a real consumer design API and remove Leather's skin from core

1 participant