ADR-096 ActifActive contract

Semantic layout container-width tokens

DateDate
2026-09-11
DécideursDecision makers
Guilherme Negreiros — Design System Lead

Context

This gap surfaced during a real AI-interoperability test: running GitHub Copilot against Agentica-Copilot, a clone of this repo, to build a standalone demo/ page. Copilot needed a page container max-width and asked — correctly, per AGENTS.md's escalation rule for any change touching a semantic token — whether to reuse semantic.size.control.max-width (480px). That token's contract (ADR-094) restricts it explicitly to agtc-input's field wrapper; reusing it for a page container would have been a coincidental-value reuse, not a shared intent, and was rejected for that reason.

Investigating to give Copilot a real answer found that no semantic.layout category exists at alltokens/semantic.json has color, shadow, space, size, radius, fontWeight, typography, icon, marketing, nothing for page/section container widths. What exists in site/build.js instead is an inconsistent mix:

the one part of this that was already done right, just never promoted past extension-token status.

audit-ignore exception per governance/rules/layout-pattern.md.

720px, 760px (×2), 780px, repeated across ~15 call sites in generated HTML.

Before drafting this ADR, that last cluster was audited page-by-page (not just counted) to separate real, repeated intent from copy-paste drift — see commit 3fc935c7 (chore(site): remove dead .rd-cta-inner CSS, align outlier max-widths to 700px):

the Redesign/ mockup (ADR-058). Deleted.

no evidence of a distinct intent) were both folded into 700px — the value already used identically at 7 other call sites for the same visual role (intro/hero copy block).

What's left after that cleanup is two clean, internally consistent, already-repeated patterns (600px ×6, 700px ×9) plus the two pre-existing values above (960px ×1, 1180px ×7) plus one more real, undeduplicated pattern (1280px ×2, wide marketing sections). Five real values, each with a distinct, evidenced role — not five arbitrary numbers.

Breakpoints (@media(max-width:768px) etc.) are a separate, larger, currently fully-ungoverned problem — out of scope here, see Consequences.

Decision

Add a new top-level semantic category, semantic.layout, with five tokens under semantic.layout.container:

TokenValueRoleCurrent call sites
layout.container.cta600pxClosing CTA block, centered (.shell.section-final)6
layout.container.intro700pxHero/intro copy block (.shell > .copy, .section-heading)9
layout.container.docs960pxDocumentation page content (.content)1
layout.container.default1180pxStandard marketing section (promotes --agtc-content-max)7
layout.container.wide1280pxWide/cinematic marketing sections (.rd-inner, .rd-cinematic-inner)2

Not aliased to primitive.space.*

Unlike ADR-094's control-width tokens (which alias primitive.space.120/.160), these five values are declared as literal semantic values, not backed by new primitives. Reaching these numbers via the primitive space scale would require five new one-off primitives (space.150, .175, .240, .295, .320 — pure value ÷ 4 labels with no relation to the scale's existing progression). primitive.space represents a spacing/gap scale consumed at component and layout-gap granularity; a page's container width is a different design concern that only coincidentally shares a pixel unit with spacing. This follows the same precedent already used elsewhere in tokens/semantic.json for values with no meaningful primitive step (e.g. the marketing card border at 6% white opacity, documented as "a literal value not aliased to a primitive — the white scale stops at white.12 (18%), no step exists at 6%").

--agtc-content-max migration

--agtc-content-max (the ADR-058 extension token) is kept as a thin alias — --agtc-content-max: var(--agtc-semantic-layout-container-default) — rather than rewritten at its 7 call sites. This avoids a wide, purely-cosmetic diff across site/build.js for no behavior change; new code should reference the semantic variable directly, and the alias can be removed once no call site still references the old name.

Rejected alternatives

AlternativeReason for rejection
Reuse semantic.size.control.max-width for the page containerThe actual incident that triggered this ADR — contract violation (ADR-094 scopes it to agtc-input's field wrapper only); two values that happen to be close in magnitude are not a shared intent.
No max-width constraint on page-level contentNot a governance-neutral choice — an unconstrained container is a real readability regression (unbounded line length), not a way to dodge the token question.
Alias the five values to new primitive.space.* entriesConflates the spacing scale with a page-layout-container scale; the derived primitive names (150/175/240/295/320) don't correspond to any meaningful step in the existing progression, unlike ADR-094's 120/160 which at least remained in a component-sizing context.
Wait for a full breakpoints/grid token system before tokenizing container widthsBreakpoints are a separate, larger, currently fully-ungoverned area (raw, inconsistent @media values throughout site/build.js) — real future work, but blocking a scoped, already-deduplicated fix on it serves no one.

Consequences

site/build.js call sites now reference var(--agtc-semantic-layout-container-*); --agtc-content-max is a thin alias (var(--agtc-semantic-layout-container-default, 1180px)) rather than a rewrite of its 7 sites. npm run tokens + npm run build (site) verified: 148 pages, 0 WCAG violation, 0 warning; node scripts/audit-tokens.js verified: no hardcoded values, no phantom tokens, structure consistent (the pre-existing button.ghost.border orphaned-token warning is unrelated to this change).

governance/rules/tokens-system.md. No new primitives are added, so the Principal Designer primitive-approval tier is not triggered.

future decision, not folded into this one.

page built against Agentica (by a human, or by any AI tool reading tokens/semantic.json) has a real, documented answer for "how wide should my content container be," instead of guessing or reusing an unrelated component token.

← ADR-095