Principles

Principle — CSS authoring IS canon-tier work

**CSS authoring IS canon-tier work. Consumers do not author CSS — consumers extend canon.**

Principle — CSS authoring IS canon-tier work

Status: ratified — warp#3672 Filed under: ADR-113 (principles tier above initiative) Cited by: canon-extension-discipline, single-prop-render, filter-vs-metric, calm-first-cta Enforced at PR review by: principle-aware reviewers per ADR-115; upcoming structural canon-extension gate (warp#3036 follow-on cluster).

Statement

CSS authoring IS canon-tier work. Consumers do not author CSS — consumers extend canon.

A consumer is any file under src/lib/components/canon/ (in ai-frontend or peer apps) that imports a canon primitive from @gyrum-labs/svelte/*. A <style> block in such a file declaring layout, sizing, positioning, or new design-tokens is not "just a wrapper class" — it is a parallel CSS system growing inside a canon shell. The shell hides it from the existing canon-adoption gate (every class still resolves through var(--*) tokens), but the cumulative effect is a domain-specific layout grammar that no other consumer can adopt, and that drifts independently of the canon primitive it pretends to extend.

Why this rule

Today's empirical evidence: ai-frontend/src/lib/components/canon/ProjectCard.svelte lines 187-349 — 162 lines of bespoke CSS declaring 16 class roots (.row-name, .row-tagline, .row-stack, .row-flag, .row-desc, .row-meta-bottom, .row-meta-item, .row-icon, .row-row, .row-class-pill, .row-class-strap, .row-actions, .row-cta, .row-cta-icon, etc.). Every declaration uses var(--*) token assignments (so the existing token-only check passes), but the cumulative shape is a parallel layout system: display: flex, gap, min-width: 0, flex: 1 1 auto chains, breakpoint-conditional flex-wrap, :has() selectors — the file is reinventing a GyListRow-internal layout grammar with no canon contract anyone else can read.

Two failure modes follow:

  1. Duplicate-text at production breakpoints — the bespoke layout shipped {project.tagline} rendered twice (once inline at line 124 inside row-name, once block at line 138 inside row-desc) because the consumer's responsive logic was supposed to collapse one branch via flex-wrap but at operator's actual viewport width both branches resolved. The canon primitive (GyListRow) would never have shipped this — it has one slot per content region. This is the single-prop-render guideline's load-bearing example.

  2. Drift from the canon primitive — the canon GyListRow has its own internal grid + spacing tokens (--row-gutter, --row-padding-y). The consumer's .row-row redefines them at a different scale. The visual contract on canon.gyrum.ai's /components/list-row no longer matches what consumers ship. The next consumer reads the canon docs, copies the consumer instead of the canon, and the parallel system propagates.

The local-author instinct ("I'll just add a wrapper class — the canon primitive doesn't quite fit") is the failure mode. The canon primitive not fitting is a canon-tier signal, not a consumer-tier signal. The fix is in canon, not in the consumer's <style> block.

Recursive application

This principle is a recursive sister of the generic-primitives principle (warp#724) and the block-composition principle (warp#698): "domain shape lives at the right layer, not in the substrate that everyone shares."

  • Library packages (generic-primitives) — package symbols are generic verbs/nouns; domain shape lives in product code.
  • Pipeline blocks (block-composition) — block names are abstract verbs; pipeline names are domain.
  • Canon-tier CSS (this principle) — primitive CSS lives in canon; consumer-tier files spawn ZERO new layout grammar. New layout grammar IS the signal that a new canon primitive is needed.

How to apply

  1. At write time — when a <style> block in a consumer file grows beyond ~3 token-only properties, stop. Ask: "is the consumer doing layout that the canon primitive doesn't support?" If yes, that is canon-tier work — go to canon and either extend the existing primitive (Path A) or file a sister ticket for a new primitive (Path B). See canon-extension-discipline.
  2. At PR review time — principle-aware reviewers (ADR-115) cite this principle when blocking PRs that grow a parallel CSS system inside a canon-shelled consumer. The upcoming structural canon-extension gate (warp#3036 follow-on cluster) refuses non-token-only declarations in consumer files unless tagged with a /* canon-extension: warp#NNN */ marker referencing an open sister ticket.
  3. At lift time — when a consumer's bespoke CSS gets lifted into canon, the lift is the canon-tier review. The bespoke CSS does not survive un-reviewed — its rules are either lifted into the canon primitive or dropped.
  4. At graduation time — existing consumers carrying bespoke CSS are tracked as canon-extension backlog. Each file lists in the consuming repo's canon-manifest.yaml under bespoke_css_debt: with the sister-ticket reference; the closed-loop sweeper auto-files retirement tickets when the sister ships.

Applies to

  • Any .svelte file under src/lib/components/canon/ (in any consumer repo) importing a canon primitive from @gyrum-labs/svelte/*.
  • Any .svelte file under src/routes/ that contains a <style> block declaring more than 3 token-only properties.
  • gyrum-ui/packages/svelte/src/Gy*.svelte is EXEMPT — that IS the canon-tier work. Authoring CSS there is the right path; this principle is the rule that funnels work to the right layer.

Cross-link

  • warp#3672 — this principle's live ticket
  • canon-extension-discipline — the operational guideline this principle drives
  • single-prop-render — sister guideline catching the duplicate-text failure mode
  • generic-primitives (warp#724) — sister principle, same shape one layer up
  • block-composition (warp#698) — sister principle, same shape one layer over
  • warp#3100 — canon-extension discipline (Path-A vs Path-B)
  • warp#2278 — canon-tier substrate EPIC
  • warp#2284 — Svelte canon primitives EPIC (ADR-182)
  • ADR-113 — principles tier (the schema this is filed under)
  • ADR-115 — principle-aware reviewers (how this principle reaches PR review)
  • ADR-182 — Svelte-native canon-tier (the substrate this principle protects)