Brickwork Theme
Status: normative consumer product contract (ADR-110).
Audience: consumers authoring a brand; package maintainers; agents.
Companions: brand-pack.md (directory unit), BRANDING.md (override mechanism), DESIGN.md (token vocabulary), INTEGRATION.md (Tailwind projection path).
What this is
Brickwork Theme is how a consumer builds a visual profile on the substrate:
values on published --bw-* tokens (and an optional density default), loaded
after package CSS, so the owned kit (.bw-*) and any consumer Tailwind
utilities that import tailwind-theme.css follow the brand.
It is the same kind of job as authoring a Tailwind @theme: constrained
dials, not rewriting components. It is not shipping a general Tailwind
utility layer inside brickwork.css, and not licence to restyle .bw-*
in brand CSS.
Three owners
| Owner | Decides | Does not decide |
|---|---|---|
| Substrate | Token names, derivation, a11y floors, axes | Your look |
| Theme profile (this doc) | Values on overridable axes | Component markup, ARIA, HTMX |
| Page | Composition, content, Tailwind layout | Chrome recipes, token schema |
Depth ladder
| Level | Name | What you author | Claim you may make |
|---|---|---|---|
| L0 | Base | Nothing | Package defaults |
| L1 | Recolour | Load-bearing colours + fg-on-accent (light and dark) |
Recolour in ~16 lines |
| L2 | Voice | L1 + --bw-font-family-sans / -display / -mono (optional type roles) |
Recognisable type voice |
| L3 | Material | L2 + radius and/or elevation and/or surface ladder beyond L1 | Material language |
| L4 | Rhythm | L3 + --bw-space-1 and/or density default / density tokens |
Product pacing |
A pack must declare the highest level it claims in PROFILE.md (see
brand-pack.md). Claiming L3 while only shipping L1 colours is
a documentation defect.
L1 required tokens (per theme)
--bw-color-surface--bw-color-fg--bw-color-border--bw-color-accent--bw-color-danger--bw-color-success--bw-color-warning--bw-color-fg-on-accent(verify ≥ 4.5:1 on accent)
Optional at L1: --bw-color-info (or collapse to accent), --bw-color-surface-inverse, --bw-color-focus-ring.
L2 required tokens
All L1 required, plus:
--bw-font-family-sans--bw-font-family-display--bw-font-family-mono
Optional: --bw-text-<role>-* family/size/weight/line-height/tracking where the
brand changes defaults. Prefer roles over raw --bw-font-size-* ladders. The
label role carries --bw-text-label-family and --bw-text-label-tracking
(consumed by .bw-btn) so button type can be set once on the ladder.
L3 requirements
All L2 required, plus a coherent authored set in at least one of:
- Surfaces: e.g.
--bw-color-surface-raised(and optionally sunken / overlay / marketing-tint / border-strong) - Radius: at minimum
--bw-radius-sm,--bw-radius-md,--bw-radius-lg - Elevation: at minimum
--bw-elevation-1,--bw-elevation-2,--bw-elevation-3(light and dark)
Document which material axes are deferred.
Overlay chrome beyond the L3 minimum: modal, toast, and popover panels
read --bw-radius-xl; open modal / slide-over read --bw-elevation-4; toast
reads --bw-elevation-5 (see DESIGN.md component map). Author those steps when
you want overlay altitude and radius to follow the pack. Cards, buttons, and
floor modal elevation already move on the L3 minimum set. Soft ambient shadow
layers beside elevation tokens are kit craft (beat / beautiful-defaults), not
theme dials; they stack with elevation and do not replace it.
Button elevation recipes (optional L3): when a brand wants button shadows
or a hover lift distinct from cards and panels, author
--bw-component-button-elevation, --bw-component-button-elevation-hover,
and optionally --bw-component-button-hover-translate (defaults keep package
chrome byte-identical; see DESIGN.md and icvoss/django-brickwork#649). These
are not part of the L3 minimum checklist; the shared --bw-elevation-*
ramp still moves default button chrome.
L4 requirements
All L3 requirements for the claimed material axes, plus either:
- author
--bw-space-1(spacing base), and/or - set a brand default density (
data-density) and/or author--bw-density-*for the shipped mode(s)
Page margins and one-off layout gaps remain page-owned (consumer Tailwind). Theme owns system rhythm, not every page inset.
Kit-owned (not theme dials)
Do not present these as ordinary theme axes:
| Family | Examples | Why |
|---|---|---|
| Focus geometry | --bw-focus-ring-width, -offset, -style |
A11y floor (colour may be L1 optional) |
| Z-index | --bw-z-* |
Layer contract |
| Motion | --bw-duration-*, --bw-ease-*, --bw-transition-* |
Kit interaction map |
| Breakpoints | --bw-breakpoint-* |
Shared layout; consumer Tailwind build-time only if overridden |
| Touch floor | --bw-size-touch-target-min |
A11y |
Most --bw-component-* |
Icons, toast width, toggle geometry | Kit chrome |
| Opacity roles | --bw-opacity-muted, disabled / HTMX opacities |
Sparse semantics, not a scale |
Opacity when composing: disabled/in-flight via component APIs; rare global
muted via token; one-off quieter blocks via a consumer wrapper (e.g.
Tailwind opacity-*). There is no general opacity= on kit tags.
Defaults you already inherit
Base-theme ships full scales (radius, spacing, elevation, type roles, border widths, density modes). L0/L1 consumers get them automatically. Higher levels replace selected values; they do not invent parallel scales.
Tailwind relationship
| Layer | Role |
|---|---|
--bw-* |
Source of truth |
.bw-* kit |
Reads tokens; no Tailwind @apply in component CSS |
tailwind-theme.css |
Optional projection so consumer utilities inherit the theme |
| Consumer Tailwind build | Page layout utilities (grid, gap-4, …) |
First-class recipe: INTEGRATION.md §1.3
and the copy-paste tree examples/vite-tailwind/.
Vendor the projection with
manage.py sync_brickwork_projection frontend/src/brickwork-theme.css
(or brickwork.services.css_delivery.sync_tailwind_theme); do not hand-copy
from site-packages.
Import order for a themed Tailwind consumer (shell already links
brickwork.css):
@import "tailwindcss";
@import "../src/brickwork-theme.css"; /* synced projection */
/* then your brand tokens.css */
brickwork.css alone does not emit a general utility layer.
Preview surfaces (L3 / L4)
An L3 or L4 pack must prove overrides on kit chrome, not only swatches:
- Application shell (topbar / sidebar chrome)
- A card
- A form (at least one text field + primary button)
- One marketing band (hero or CTA section)
Compose those from shipped brickwork tags, load the pack tokens.css after
brickwork.css, set data-bw-brand, and render:
{% load brickwork_theming %}
{% bw_token_specimen level="L3" %}
level="L3" / L4 uses recommended_tokens_for_level so radius,
elevation, type (and L4 space) samples are visible beside colours. Default
{% bw_token_specimen %} remains the load-bearing colour set.
Worked composition: examples/theming/theme-preview.html.
Honesty rules
- Never claim "rebrand the whole system in 16 lines" as a full brand.
- L1 = recolour. L3/L4 = theme depth, only when axes are authored.
- Never invent token names outside
token-manifest.json/ DESIGN.md. - Never restyle
.bw-*in a theme pack. - Kit craft (beauty of defaults) is a separate bar (VISUAL-BAR.md); a theme cannot rescue flat kit chrome.
- After Phase E (#600), public L3/L4 claims may state that authored material and rhythm axes move kit chrome (card / button / modal gated). They must not equate theme depth with meet-or-beat beauty (beat F.2 remains separate).
Worked artefacts
| Artefact | Role |
|---|---|
| examples/vite-tailwind/ | Vite + Tailwind 4 recipe + projection sync (#601) |
| examples/brand-pack/northline/ | Fictional L2 skeleton (PROFILE.md) |
| examples/brand-pack/northline-material/ | Fictional L3 torture (radius + elevation + raised surface) |
| examples/brand-pack/northline-dense/ | Fictional L4 torture (material + space-1 + compact density) |
harbour/, folio/ |
Additional fictional L2 voices |
render_brand_css() |
Emitter for validated overrides (colours + L2 to L4 overridable values) |
infer_theme_level / check_theme_level |
Pure level inference and claimed-vs-evidenced check (brickwork.services.theme_profile, re-exported from brickwork.services.tokens) |
recommended_tokens_for_level |
Checklist-driven recommended token names per level |
{% bw_token_specimen %} |
Live preview; level="L3"/L4 shows material axes (#268, #599) |
| Phase E audit | audits/2026-09-15-theme-phase-e-token-compliance.md (#600) |
Level check (Phase C)
from brickwork.services.tokens import check_theme_level, render_brand_css
report = check_theme_level("L3", light.keys())
assert report.ok, report.message # colours-only claiming L3 fails
css = render_brand_css(light, dark) # unknown names still raise BrandValidationError
Kit-owned tokens (focus geometry, z-index, motion, breakpoints, touch floor,
most --bw-component-*, opacity roles) may still emit via render_brand_css
when overridable, but never evidence a theme level (kit_owned_present /
ThemeLevelReport.kit_owned).
Related programmes
Theme profiles dress the kit. Raising zero-kwargs kit craft remains beat / beautiful-defaults work. Do not substitute one for the other.