# Token Contract Audit

Use when adding a new component, modifying an existing component's CSS, or investigating why a component misrenders under a theme.

---

## The Component Token Contract (mandatory)

Spec: `.claude/docs/specs/component-token-contract.md` (live source of truth, if this doc contradicts that one, the spec wins; patch this skill).

Every component's `.css` file must follow the **two-block @scope** pattern:

```css
/* Block 1: token declarations on :where(:scope), zero specificity */
@scope (my-component-ui) {
  :where(:scope) {
    --my-bg: var(--a-bg);
    --my-text: var(--a-fg);
    --my-radius: var(--a-radius-md);
  }
}

/* Block 2: styles using those tokens on :scope */
@scope (my-component-ui) {
  :scope {
    background: var(--my-bg);
    color: var(--my-text);
    border-radius: var(--my-radius);
  }

  /* Variants override TOKENS, not base styles */
  :scope[variant="danger"] {
    --my-bg: var(--a-danger-bg);
    --my-text: var(--a-danger-fg);
  }
}
```

### `@scope` donor selectors must be tag-independent when a component can swap its own rendered tag (ADR-0102, 2026-09-01)

A `@scope (my-component-ui)` donor selector matches by **literal tag
name**. That's a live gap for any component whose own contract lets it
render as a different tag than its own custom-element name, `text-ui`
already did this coincidentally (`variant="body"` → `<p>`,
`variant="caption"` → `<small>`), relying on ambient/bare-tag styling
rather than the component's own `[variant]` rules. ADR-0102 (`text-ui`'s
`level` prop, promoting the element to a real `<h1>`-`<h6>`) made this a
first-class case, requiring the donor selector to stay tag-independent, e.g. `@scope (text-ui, [data-text])` with the renderer stamping a stable
marker attribute on every such element regardless of the resolved tag, so a promoted `<h2 level="2" variant="heading">` keeps its component
styling instead of silently falling back to bare-tag `:where(hN)`
defaults elsewhere in the cascade. Any component that can render under a
tag other than its own name needs the same tag-independent scope check.

## Hard rules

1. **Zero raw color values** anywhere in component CSS
   - No `#fff`, `rgb(...)`, `oklch(...)` literals
   - All colors must reference `--a-chrome-*`, `--a-data-0..9`, `--a-fg*`, `--a-bg-*`, or family tokens (`--a-brand-*`, `--a-primary-*`, etc.)
2. **Two-block pattern**, token declarations separate from style rules
3. **Variants override tokens**, never rewrite base styles inside `[variant]` selectors. Change token values; the base styles absorb the change.
4. **`:where(:scope)`** for token defaults, zero specificity lets consumers override tokens from outside

## Audit procedure

1. **Find raw colors**

   ```bash
   grep -nE "#[0-9a-fA-F]{3,8}\b|rgb\(|rgba\(|oklch\(" \
     packages/web-components/components/<name>/*.css
   ```

   Any hit is a violation unless explicitly exempted (see below).

2. **Check for single-block @scope**

   ```bash
   grep -c "@scope" packages/web-components/components/<name>/*.css
   ```

   Should be `2` (or `0` for layout-only components).

3. **Check for variant rewrites of base properties**

   ```bash
   grep -nE ":scope\[variant.*\] \{" packages/web-components/components/<name>/*.css
   ```

   Inspect each match. If it sets `background`, `color`, `border` directly instead of `--my-bg`, `--my-text`, etc., it violates the variant rule.

4. **Check token usage in parent stylesheet**

   ```bash
   grep -n -- "--a-chrome\|--a-data\|--a-fg\|--a-bg" \
     packages/web-components/components/<name>/*.css
   ```

   Should have references; absence often indicates raw-value regressions.

## Documented exemptions (do not "fix")

- `chart.css` data slots reference `--a-data-0..9` (tokens, not raw)
- `card.css` mask uses `--a-chrome-light` (mask is opacity-only; value is semantic)
- Drawer/modal scrims use `--a-scrim-dialog` (aliases.css, gh#373: routes to `--a-chrome-scrim-dialog`, 80% black, not `--a-chrome-backdrop`)

If you find a raw value elsewhere, either:

- Fix it by adding a proper token in the owning `styles/` dimension×layer file (see "Where foundation styles live" below, `tokens.css` is a compat barrel, not a home)
- Or document the exemption in `.claude/docs/specs/component-token-contract.md`

## Chrome token palette

Added in v0.5.0, use these for UI chrome:

- `--a-chrome-light`, light scrim / overlay
- `--a-chrome-dark`, dark scrim
- `--a-chrome-border`, subtle hairline borders
- `--a-chrome-ring-subtle`, focus rings, outlines
- `--a-chrome-shadow-soft`, elevation shadows
- `--a-chrome-backdrop` - generic 50% chrome overlay, reserved for non-dialog UI chrome; no current consumers. Modal/drawer backdrops use `--a-scrim-dialog` instead (80% black, gh#373, see `--a-chrome-scrim-dialog` in features.css' CHROME block)

## Data palette

For charts, stat colors, category markers, use `--a-data-0` through `--a-data-9`. Do NOT hardcode chart colors.

## Where foundation styles live, dimension × layer

Every file under `packages/web-components/styles/` answers two questions, which *dimension* (color · type · space · size · radius · motion · elevation) and which *layer* (primitive → semantic → role → element → attribute-API → context → reset), and declares its cell with a machine-readable header tag `adia:<layer>/<dimension>`. When adding or moving a declaration:

- Spatial / size / radius / motion / elevation primitives → `styles/foundation/` (one file per dimension)
- Type scale (L1/L2), roles (L3), native-element defaults → `styles/type/`
- Color layers → `styles/colors/` (parameters · primitives-* · surfaces · scrims · semantics/*)
- The global attribute API (`[size]` `[density]` `[gap]` `[padding]` `[margin]` …) → `styles/api/{sizing,text,layout}.css`, the ONLY home for global `[attr]` selectors (governed by the grammar spec below)
- `tokens.css`, `typography.css`, `colors/semantics.css`, etc. remain **compat barrels** at their public paths, they only `@import`; never add declarations to them

Enforced by `scripts/release/check-foundation-layer-placement.mjs` (wired into `npm run check`): header tags present, primitive files free of attribute selectors, the attribute API confined to `api/`, no orphan files. Import order inside `colors/semantics/` is load-bearing (`aliases.css` last) and asserted in file headers.

Source: ADR-0035.

## The global attribute grammar, read the spec before naming an attribute

The global attribute API is a *designed system* with its own spec:
`.claude/docs/specs/attribute-api-system.md` (companion audit:
`.claude/docs/specs/attribute-api-audit.md`). That spec, not this checklist, owns the grammar. Two rules an author must know exist (read the spec for the
mechanics; never work from this summary alone):

- **Explicit vs ambient** (spec §5.2): `[gap]`/`[padding]`/`[margin]` are
  *explicit* per-element overrides (non-inheriting), while `[size]`/`[density]`
  are *ambient* context-setters (inheriting by design). Which axis an attribute
  sits on decides its `@property` registration and how components read it.
- **No shadowing** (`docs/ops/adr/adr-0053-no-shadowing-global-attributes.md`): a
  component-local attribute may not share a name with any global attribute: the global name always means the global thing. Before minting any attribute
  in a component yaml, check it against the spec's attribute inventory; the
  only sanctioned collisions live in the spec's §11 Global-Attribute Exemption
  List (first entries: `swatch-ui[color]` / `noodles-ui[color]`, ADR-0054;
  granted since: `qr-code-ui[color]` / `icon-ui[weight]`, ADR-0070;
  `badge-ui[weight]` → `icon-ui[weight]`, ADR-0100).

Beyond the global grammar, ADR-0063
(`docs/ops/adr/adr-0063-attribute-grammar-addendum.md`) ratifies seven
CROSS-SIBLING conventions for component-local attribute naming, the axis
ADR-0053/0054 don't cover. Any new attribute follows these:

- **`no-*` is the canonical negation prefix**, `hide-*` retires
  (`chart[hideAverage|hideGrid|hideValues]` → `no*`, `stream[hide-cursor]`
  → `[no-cursor]`).
- **Interactivity is opt-in by a positively-named boolean, default `false`**
  (the `stepper-item[interactive]` / `swatch[selectable]` shape), never
  opt-out. `chart-legend[static]` retires to `[interactive]` (inverted
  default). Applies to single-purpose toggles only, `noodles`'s
  `editable`/`readonly` pair is two orthogonal booleans and stays.
- **One spelling survives a documented alias; the alias retires
  everywhere it appeared**, never a deprecation window. `alert[dismissible]`
  retired (`[closable]` survives, the implemented prop); `card`'s style
  enum's duplicate `outline` member retired (`outlined` survives, the
  ADR-0044 style-axis token).
- **Two sanctioned multi-value encodings, never a hand-rolled delimited
  string:** (a) `multiple` boolean + a structured option model (`select`'s
  `options`/`<option>` shape); (b) a native array-typed JS property,
  JSON-serialized for attribute/form participation (`tags-input`'s
  `.value`). One explicit, operator-ratified EXCEPTION for existing
  components (recorded on gh#1563, 2026-08-17): `segmented-ui[multiple]`'s
  comma-separated value IS accepted as the sanctioned multiple+options
  encoding, and toggle-group's comma-string resolves under that same ruling
  rather than being re-ticketed. This is a ruling on those two existing
  components, not a loophole, a NEW component's multi-value attribute still
  uses form (a) or (b) above, never a delimited string, unless a separate
  ruling says otherwise.
- **Cross-sibling naming convergence:** `pane[side]` → `pane[edge]`
  (drawer keeps physical `side`); `menu-item[subtitle]` →
  `menu-item[description]` (the ~20-component majority word); `placement`
  ratified as-is for anchored popovers. Documented exception:
  `input[maxlength]`, a primitive wrapping a genuine native `<input>`
  keeps the native attribute's exact casing (ADR-0025/0055 conformance);
  new non-native length constraints use the framework's
  camelCase-property/kebab-attribute pair.
- **`completed` is the stage-terminal word** for a `status` lifecycle enum
  (`agent-reasoning[status]`'s `done` converges).
- **`-picker` is reserved for the outer trigger+popover form-associated
  composite**, never the inline substrate it composes. `color-picker-ui`
  (the inline substrate) renames to `color-area-ui`.
- **[verified 2026-08-19] Preset-boolean-vs-alias-retirement boundary**
  (ADR-0076): a boolean attribute that is additive sugar over several
  existing granular `no-*` opt-outs, `table-toolbar-ui[chrome-only]`
  equivalent to setting all four of `no-filter`, `no-sort`, `no-columns`,
  `no-search`, is NOT an ADR-0063-style alias-retirement case (the
  granular attributes aren't duplicate spellings of one concept the way
  `alert[dismissible]`/`[closable]` were; a consumer may legitimately want
  a subset off, a combination the preset alone can't express), so the
  granular attributes stay shipped, independently-addressable API with no
  deprecation. The preset's precedence is **pure, absolute OR, never a
  tri-state**: while set, all covered controls are off, full stop, with no
  partial re-enable via clearing an individual `no-*` while the preset
  remains set (ADR-0076: "`no-*` attributes are presence-based booleans …
  meaning 'presence forces off' is expressible but 'absence means defer to
  [the preset]'s own OR' is not without a tri-state amendment"). A second
  auto-snap-enum precedent alongside ADR-0074's chart `ratio` lands here
  too: `table-toolbar-ui[stage]` (`full | search-tight | icon-only |
  overflow`), unset auto-snaps via `@container` queries against studied
  breakpoints, an explicit value pins and overrides the query, no
  interpolation between stages.

The renames above have LANDED: dual-read compat shims shipped via gh#1563,
and the breaking cut removed the old names in 0.8.43 (gh#1617). The new
spellings are the sole ones, a yaml or demo still showing an old name is
stale and should be fixed.

## Disabled-state tokens

[verified 2026-08-19] ADR-0073 ratifies the standing convention for any
control supporting the `disabled` boolean attribute/state:

- **Shared bg role, container-low tier.** `--a-ui-bg-disabled`
  (`styles/colors/semantics/features.css`) resolves to
  `var(--md-sys-color-neutral-container-low)`, the same 10%-tint role
  `--a-bg-hover` / `--a-bg-muted` ride for REST-state de-emphasis. A
  component's own `--<component>-bg-disabled` indirection aliases this
  shared role, never a raw color, never a per-family `*-container-low`
  variant (none exists in the disabled path).
- **`[state][disabled]` specificity override for checked/selected fills.**
  A `[checked]`/`[selected]` selector outranks a plain `[disabled]` rule on
  CSS specificity (2 attribute selectors beat 1), so a checked+disabled
  control silently keeps its active-state fill unless the component adds an
  explicit higher-specificity override, `[checked][disabled]` /
  `[selected][disabled]`, routed through its own
  `--<component>-...-checked-disabled` (or `-selected-disabled`) custom
  prop, itself aliasing `--a-ui-bg-disabled`.
- **Reduced-contrast disabled border, the `--input-border-disabled`
  pattern.** Any control that renders a border when disabled adds
  `--<component>-border-disabled: var(--a-ui-border-disabled)`, applied as
  `border-color` under `:scope[disabled]`.

Quoting ADR-0073's Decision 5: "a control supporting `disabled` uses the
shared, single neutral `--md-sys-color-neutral-container-low` role for
background … always through a `--<component>-bg-disabled` … indirection
aliasing `--a-ui-bg-disabled` … uses the reduced-contrast
`--input-border-disabled` pattern … for any border it renders when disabled
… overrides any checked/selected state fill at `[state][disabled]`
specificity rather than relying on `[disabled]` alone."

The re-runnable check is `scripts/audit/audit-disabled-fill-tokens.mjs`
(`check:disabled-fill-audit`, advisory), it fails any `[disabled]`-scoped
`background`/`background-color` that doesn't resolve through a
disabled-aware indirection.

Source: [ADR-0073](../../../../../../docs/ops/adr/adr-0073-disabled-state-container-low-token-convention.md).

## When to update this reference

If you add a new token category (like `--a-chrome-*` was added), update both this file and `.claude/docs/specs/component-token-contract.md`. The spec doc is the live source of truth; this file is the practitioner's checklist.

## Cross-references

- [authoring-cycle.md](authoring-cycle.md) Step 5, verification gates (`npm run verify:palette` is the mechanical check for raw colors)
- [css-patterns.md](css-patterns.md), the two-block `@scope` pattern in depth, with rationale
- [anti-patterns.md](anti-patterns.md), failure catalogue including variant-rewrites-base bugs
