v0.91.0: the design language, enforced — grammar tokens, cairn-audit, and the admin-screens skill
The design-infrastructure initiative's window: three passes held under one cut. The admin's design language becomes named structure (the grammar tokens and role utilities), mechanically checkable (the cairn-audit bin, nine static and eleven rendered rules, the norms manifest), and loadable by an agent (the packaged cairn-admin-screens skill, installed by cairn-doctor --fix). Two Consumers must: lines below; the type-grammar adoption recipe is in Upgrade cairn.
Added
-
The admin's structural type and spacing scale is now named: ten CSS custom properties
(--cairn-type-title/subtitle/body/meta/label/chip,--cairn-gap-label/control/group/section)
declared once incairn-admin.css, outside the light and dark theme blocks, so a heading's size
or a control-to-control gap holds across both themes. Ten role utilities
(type-title/type-subtitle/type-body/type-meta/type-label/type-chip,
gap-control/gap-label/gap-group/gap-section) are the supported way to reach a token from
markup; each sets one property and nothing else. All ten ship in the compiled admin stylesheet
whether or not cairn's own screens use them, so a role is available to a custom admin route on
the strength of the documentation alone. See Admin grammar
tokens. -
src/lib/admin-toolkitand the built-in engine admin screens migrate their on-scale type and
spacing literals to the new role utilities and tokens, pixel-identically: the committed
admin-visual snapshots do not move. The toolkit's scoped styles carry the measured literal as a
var()fallback, since those components can mount outside the admin theme root; engine screens
reference the token bare. -
The palette/grammar boundary is now a documented contract: a site re-tunes the palette tokens
(--color-*) to its own brand and never redeclares a grammar token, since grammar names
structure and palette is the brand layer. -
The type scale closes at seven roles, each carrying a ruled line-height, and the grammar
inventory grows from ten custom properties to eighteen. Every type role gains a paired
--cairn-type-<role>--leadingtoken, and a seventh role,--cairn-type-headingwith its
type-headingutility (18px, bold, the display face), unifies the two heading recipes the admin
had been running side by side. Eachtype-*utility now declares
line-height: var(--tw-leading, var(--cairn-type-<role>--leading)), so aleading-*utility
still composes over a role rather than being overridden by it. cairn's own admin screens migrate
onto the closed scale: 129 named type steps, 120 twelve-pixel sites resolved ontotype-metaor
type-labelby the relationship each site expresses, both heading families ontotype-heading,
and the stray 9.6px and 11.2px slips ontotype-chipandtype-label. Five sites keep a value
off the scale and each carries a counted suppression directive naming why: the three wordmark
sites (the K4 keming fix) and the editor canvas and its document title, which set the editor's
own type scale rather than the admin chrome's. The losing heading family and part of the
twelve-pixel set change size by ruling, so theadmin-visualbaselines were regenerated once on
CI and read by eye. See Admin grammar tokens. -
Two container role utilities,
card-shellandcard-shadow, are the supported replacement for
hand-copying the admin's card-shell class string
(rounded-box border border-[var(--cairn-card-border)] bg-base-100, optionally with
shadow-[var(--cairn-shadow)]).card-shellcarries the shell's radius, hairline border, and
fill;card-shadowcarries its elevation, kept separate because a surface already nested inside
a shadowed container takescard-shellalone. Both ship in the compiled admin stylesheet
regardless of cairn's own usage, the same safelist discipline as the eleven grammar role utilities.
cairn's own admin components migrate every verbatim shell site onto the new utilities,
pixel-identically. See Admin grammar tokens. -
The package ships a new bin,
cairn-audit, the design-language audit. An install puts it on the
project's path, andnpx cairn-auditruns the static rules over the admin surfaces. The
substrate issvelte/compilerfor markup and the builtcairn-admin.cssfor resolution, never a
regex over source text: a class token is matched exactly, sotext-base(the size utility) and
text-base-content(the daisyUI color utility) can never read as the same class, and a class
written as an array, an object, a template literal, or aclass:directive is seen the same as
one written in a plain attribute. Configuration is one optionalcairn-audit.config.jsonwhose
every key defaults, so a site that has written no config still gets a meaningful run; a config
that names a scan path the tree does not have fails the run instead of quietly auditing less.
See Thecairn-auditCLI. -
Nine static rules ship, all error tier.
no-uncompiled-class: every markup class token compiles
into the built sheet or is a name the component's own<style>block defines.type-scale:
every font size a text-sizing token resolves to comes from a--cairn-type-*role.
gap-scale: an arbitrary margin, padding, or gap literal resolves to a--cairn-gap-*role or
lands on Tailwind's own spacing grid.stock-default-hazards: four stock daisyUI patterns
cairn's own recipes replace (badge-ghost, the focus-driven bare.dropdown, nativedisabled
on a guarded button, a flatbase-300card border), each finding citing the refuted alternative
on record.token-colors: no raw hex,rgb(), named-color, or pure-achromatic literal outside a
declared palette site.grammar-boundary: consumer CSS never redeclares a grammar token.
focus-parity: every hand-authored:hoverselector has a matching:focus-visibleor
:focus-within.motion-band: every declared duration lands in the admin's 150 to 250ms band,
andtransition: allnever ships.reduced-motion: every motion-bearing selector is named again
inside aprefers-reduced-motion: reduceguard. -
Suppression is a co-located, reasoned, counted comment:
cairn-audit-disable-next-line <rule-id> -- <reason>, in HTML,//, or/* */form. The reason
is required, a directive that silences nothing is itself a finding, and neither of those errors
can be suppressed in turn, so a build that passes by suppression reads as one. "Next line"
resolves to the next AST node rather than the next physical line, which is what makes a directive
above a multi-line element attach to that element's whole source range. This replaces the
file-plus-token JSON allowlists the repo's own gates carried, whose entries orphaned silently on
a rename and exempted a whole file at a time. -
Two repo gates graduate onto the packaged engine and keep their names and their places in CI.
npm run check:invisible-craftnow runsmotion-band,gap-scale, andtoken-colors, and
npm run check:admin-css-classesrunsno-uncompiled-class; both are thin wrappers, and the
hand-rolled comment strippers, brace-matched attribute scanners, and duration and color regexes
they carried are gone. Their two JSON side files go with them: every entry in
admin-css-classes-allowlist.jsonproved already dead, and each entry in
invisible-craft-budget.jsonresolved to a regex false positive the new rules recognize as
compliant, a site outside the audited scope, a declared palette file, or a co-located suppression
directive at the site itself. -
The package now ships a norms manifest: the admin's measured design norms as data, derived by
rendering the admin screens in both themes and reading the computed styles of twelve semantic
roles.npx cairn-audit norms <selector-or-role>answers from it, so a developer or an agent
building a new admin surface reads a measured control height, padding ratio, border treatment,
radius, or type recipe instead of inferring one from a screenshot. Each entry states the band, how
many distinct element sites it rests on, whether a ratified decision settles it, and every caveat:
an entry an open design question governs is flagged and never reads as settled, a band resting on
one site says so, and a palette-dependent property is stored as a relationship
(var(--cairn-card-border), acolor-mixformula) rather than a resolved Warm Stone value, so a
re-tuned palette invalidates nothing. See Thecairn-auditCLI. -
cairn-audit's rendered mode has a working harness. It renders every configured admin page in
both themes, always, since a rendered rule can pass one theme and fail the other; it never starts
a server (BASE_URL must already answer, or the run fails naming the URL it tried), and it imports
Playwright dynamically from the consumer's own install, printing a one-line
npm i -D playwright && npx playwright install chromiuminstruction when it is absent. A rule
declares which interaction states it reads from (a rest render, an open menu, a keyboard
focus-visible pass), so a rule that never needs a menu-open pass never pays for one. Exemptions
live in a page+selector+reason JSON allowlist (rendered.allowlistin
cairn-audit.config.json), the same reason-required discipline the static suppression comments
carry; an allowlist entry whose selector matches nothing the run actually visited is reported as
a stale entry rather than silently doing nothing, the same fail-loud discipline every other part
of the engine holds. See Thecairn-auditCLI. -
npx cairn-audit --renderedruns, and the six error-tier rendered rules spec 6.3 defines are
registered:one-filled-action(at most one accent-filled control per surface),
focus-renders(every tab stop renders a focus indicator),interactive-contrast(interactive
text against its own composited background at a ratio of at least 1.5),touch-targets(a tap
target's activation region at a 390px viewport),viewport-overflow
(nothing wider than the viewport at 390 and at 320), andchip-ground-collision(a chip's fill
distinguishable from the background behind it). A rendered rule that compares two colors resolves
them by painting each on a canvas in the page and reading the sRGB bytes back rather than parsing
color syntax, so a themed admin in any color space (cairn's own palette isoklchend to end)
measures correctly. A rule that cannot make its measurement, a gradient leaves no single ground
to compare against, reports an advisory finding naming what it could not read rather than
skipping silently. See Thecairn-auditCLI. -
The five advisory rendered rules spec 6.3 defines are registered, completing the rendered rule
set at eleven:border-contrast(a border reads at 3:1 against at least one of the two surfaces
it separates),weight-budget(at most two distinct font-weights per content region),
norms-bands(a component's geometry against the bands the norms manifest observed),
screen-anatomy(one PageHeader h1, content in the card region, no accent-filled action buried
outside both), andrelational-spacing(the--cairn-gap-*scale matches the relationship the
markup renders). Advisory means advisory: none of the five can change the process exit code,
through its own findings, through a selector the browser cannot parse, through a stale
suppression, or through a throw inside a rule. -
Rendered rules share one set of in-page measurement helpers rather than each carrying a copy, so
"is this visible" and "what selector names this element" mean one thing across the rule set. An
element the visually-hidden recipe clips is no longer counted as rendered, an element under an
ancestoropacity: 0is no longer measured, and every reported selector is escaped, so a
Tailwind class such aslg:ml-56stays a valid CSS selector the allowlist can match on. -
A rendered allowlist entry may name the rule it exempts (
"rule": "border-contrast"). A stale
entry is then reported at that rule's own tier, so suppressing an advisory finding cannot gate
the build when the selector later churns. An entry whose selector the browser refuses to parse
reports separately and always advisory, since unreadable is a different claim from stale. -
A rendered rule that throws reports a finding at its own tier instead of aborting the run. A rule
that reads a file inside its check (norms-bandsreads the shipped manifest) could otherwise
take an entire audit to exit code 2 on a substrate condition in a consumer install. -
Four design rulings settle what three rendered rules enforce, each recorded in the rule's own
source and on the reference page so the reasoning does not have to be re-derived.
touch-targetsenforces a 24x24 floor, the number WCAG 2.2 level AA's success criterion 2.5.8
sets, rather than AAA's 44x44, since AA is the conformance level cairn can honestly claim. The
rule is a strict superset of that criterion rather than an implementation of it, so a finding is a
house-bar failure and not on its own an AA failure; four of the criterion's five exceptions are
not evaluated, and each finding says so. It measures the activation region rather than the painted
box: the control's own box, a qualifying::beforeinset expansion, and every label the platform
reports as activating the control. The--cairn-card-borderhairline is a ratified exception,
exempt at the rule rather than blanket disabled, soborder-contraststill reports every other
boundary.chip-ground-collision's floor of 1.5 is ratified, and it andinteractive-contrast
share both the number and the reason: each tests that a control or a chip is not accidentally
camouflaged against its own ground, which is a different claim from legibility. Legibility is WCAG
1.4.3 Contrast (Minimum), at 4.5:1, and no rule in this engine measures it; the reference page now
states the engine's whole WCAG coverage boundary rather than leaving a reader to infer one.
weight-budget's content region excludes chrome, and chrome is defined by HTML tag and the
equivalent ARIA role rather than by class, so a rewritten component stays covered. -
A rendered rule can declare its own exemption, for a ratified exception no page+selector entry
names and no source-positioned comment can reach: a design token every recipe shares, on every
page. An exemption suppresses without silencing. The rule still constructs the finding, the
finding still carries its own measurement, and it reaches the report's suppressed list with the
ruling printed beside it. Only an advisory rule can exempt itself; on an error-tier finding the
run refuses the reason, prints the refusal, and the exit code stands, because a gate any rule
could quiet in one line is worth no more than the runs it passes. The allowlist gains the
matching honesty in the other direction: an entry whose selector still matches an element while
suppressing nothing reports as a dead entry, at the tier of the rule it names. -
cairn-audit --renderedreads an optionalCAIRN_AUDIT_COOKIESenvironment variable, Cookie-
header syntax (name=valueentries separated by a semicolon), and adds every parsed cookie to
each browser context alongside the theme cookie. This is how a rendered run reaches a consumer's
authenticated admin, a session cookie against a localwrangler dev, since the run-specific
credential belongs in the environment rather than the config file, the same reasoningBASE_URL
follows. A malformed entry throws rather than the parser silently skipping it, and an entry named
cairn-admin-themethrows too, since the run owns that cookie itself. See Thecairn-audit
CLI. -
StatusChip(@glw907/cairn-cms/admin-toolkit) gains aregister: 'bounded' | 'quiet'prop
(design infrastructure Pass 3):'bounded', the default, is today's discrete-object reading, its
border demoted frombadge-outline's full-strengthcurrentColor(which read as a clickable
button) to a hairline that clears the audit's own 3:1border-contrastfloor in both themes;
'quiet'is a new borderless, token-tinted recipe for a settled state (a Published pill, say)
that should recede rather than compete. Both recipes are measured, not invented
(docs/internal/probes/2026-07-28-chip-registers/).badge-ghostretires from cairn's own tree:
EditPage's Published pill, CairnAdminShell's CMS pill, and every barebadge-outlinesite move
onto the two registers, andstock-default-hazards' guidance forbadge-ghostnow points at
them, naming'quiet'as the sanctioned put-away recipe. Consumers must: replace any own
badge-ghostusage (stock-default-hazardsnow errors on it) withStatusChip register="quiet"or the equivalent.cairn-chip-quietrecipe incairn-admin.css. -
The package now ships a Claude Code skill,
skills/cairn-admin-screens/(design infrastructure
Pass 3), carrying the admin design standard as loadable reference: an always-loaded core (screen
anatomy, the register rules, the done-gate), two annotated exemplar screens (a list and a
detail/slide-over), a form-anatomy contract, an extension grammar for deriving a component the
toolkit lacks, a calibrated grader prompt, and a craft chapter translating an invisible-polish
catalogue into rule-backed recipes.cairn-doctorgains askill.admin-screenscheck: missing or
stale (by a content hash of both trees) reports advisory and never fails the run, and
cairn-doctor --fixinstalls or refreshes the packaged skill into a consumer's own
.claude/skills/cairn-admin-screens/before the checks run. See thecairn-audit
CLI andcairn-doctor.
No consumer action is required for the entries above beyond the badge-ghost migration named
above. No exported type, prop, or route contract changed otherwise.
Fixed
- The Media Library no longer 500s when a stored asset's alt text is
nullor missing. A
committed or branchmedia.jsonrow is trusted wholesale on read, so a hand-edited or
older-schema manifest could cross the trust boundary with a non-stringalt;mediaLibraryEntry,
the one placeMediaLibraryEntryis constructed, now normalizes it to'', the library's
existing needs-alt signal. PageHeader(@glw907/cairn-cms/admin-toolkit) no longer leaks its default<h1>/<p>margins
past its owngap-0.5heading-stack intent.OfficeList, the componentPageHeader's own doc
calls itself "the shape, generalized", carried this UA-margin fix already;PageHeaderhad not
received it at graduation, so its title-to-meta gap rendered at roughly 58px instead of the
intended 4px. Themetaprop also now renders at the meta type role (13px, matching its own
prop name) rather than the body role (14px), so it reads at the same size as a screen's own
ListToolbarcount line when both appear together. Consumers must: expect the header stack
on any screen mountingPageHeaderto render visibly tighter (a shorter title-to-meta gap) and
itsmetaline one step smaller; no prop or type changed.cairn-audit --rendered'schip-ground-collisiondemotes from error to advisory (design
infrastructure Pass 3, corpus C, 2026-07-28): the formula has no chroma term and cannot see hue,
which produced 24 false errors of 40 on the first consumer admin it measured, so as coded it
could not serve as a consumer gate. The formula itself is unchanged; a chroma-aware repair is
filed in ROADMAP and re-promotes the rule on re-measured evidence. Consumers must: expect a
chip-ground-collisionfinding to no longer failcairn-audit --rendered's exit code; it still
reports.- Cairn's own admin's
cairn-audit --renderederror tier is clean (design infrastructure Pass 3):
ConceptList's Title/Date sort buttons gain an outward::beforehit-area expansion (a
touch-targetsfix) instead of a font-size bump, and its row-title link grows through real
padding on its own box, sincetruncate'soverflow: hiddenwould clip a::beforereaching
past it;ListToolbar's segmented filter group can now shrink below its own preferred width
(flex: 0 1 auto), so it wraps onto a second line at 320px instead of overflowing the viewport
(aviewport-overflowfix). Internal admin CSS and markup only; no consumer action.
Documentation
- A new explanation page, Why the design language is
enforced, states the sixth front-door principle: cairn's
admin design language is enforced, not merely documented, and the payoff is that a developer
spends less effort building an admin interface. The README's principle ledger grows from five
entries to six to match. Upgrade cairn gains the grammar-release
adoption recipe: how to read atype-scalefinding, match it to a named role in Admin grammar
tokens, and rename the class, plus the safelist
reachability note that makes the rename safe.