@nadicodeai/design-system 10.0.2 → 12.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/DESIGN.md CHANGED
@@ -2,13 +2,11 @@
2
2
  version: alpha
3
3
  name: nadicode Design System
4
4
  description: >-
5
- Reusable design-system contract for nadicode: one foundation of colour,
6
- type, spacing, radius, material, elevation, and motion, applied by three
7
- registers, the website, the Portal, and the Nadia desktop app, plus the
8
- package token, CSS, and asset exports they consume.
5
+ nadicode's design system and its only design authority: the tokens and the
6
+ rules every component, website section, and product screen in the website,
7
+ the Portal, and the Nadia desktop app builds on.
9
8
 
10
9
  colors:
11
- on-primary: "#ffffff"
12
10
  action: "#111111"
13
11
  action-foreground: "#ffffff"
14
12
  action-hover: "#2e2e2e"
@@ -21,16 +19,17 @@ colors:
21
19
  canvas-soft-2: "#f5f5f5"
22
20
  material-workspace: "#f7f7f7"
23
21
  material-solid: "#ffffff"
22
+ material-resting: "#ffffff"
23
+ material-raised: "#ffffff"
24
+ material-floating: "#ffffff"
25
+ material-modal: "#ffffff"
24
26
  line: "#e5e5e5"
25
27
  input: "#757575"
26
- seam: "#d4d4d4"
27
- cross: "#999999"
28
28
  focus-ring: "#111111"
29
29
  scrim: "#111111"
30
30
  link: "#00713d"
31
31
  link-deep: "#00522c"
32
32
  link-bg-soft: "#c6efd9"
33
- accent: "#ff5a1f"
34
33
  info: "#1e3cff"
35
34
  info-soft: "#e8ecff"
36
35
  info-deep: "#0e1d8a"
@@ -43,8 +42,6 @@ colors:
43
42
  warning: "#ffc220"
44
43
  warning-soft: "#fff3d1"
45
44
  warning-deep: "#7a5800"
46
- selection-bg: "#111111"
47
- selection-fg: "#fafafa"
48
45
  navigation-selection-bg: "#ededed"
49
46
  navigation-selection-fg: "#111111"
50
47
  navigation-selection-hover: "#e5e5e5"
@@ -68,30 +65,25 @@ colors:
68
65
  chart-4: "#d3302f"
69
66
  chart-5: "#0096a7"
70
67
  chart-6: "#6f42c1"
71
- chart-seq-1: "#7d99f2"
72
- chart-seq-2: "#4f6ae6"
73
- chart-seq-3: "#2743d8"
74
- chart-seq-4: "#1d33ad"
75
- chart-seq-5: "#16277f"
76
- chart-div-warm: "#e0a600"
77
- chart-div-mid: "#6b6b6b"
78
- chart-div-cool: "#4d6bff"
79
68
  dark-canvas: "#000000"
80
69
  dark-canvas-soft: "#111111"
81
70
  dark-canvas-soft-2: "#1a1a1a"
82
71
  dark-material-workspace: "#000000"
83
72
  dark-material-solid: "#000000"
73
+ dark-material-resting: "#0c0c0c"
74
+ dark-material-raised: "#121212"
75
+ dark-material-floating: "#181818"
76
+ dark-material-modal: "#1c1c1c"
84
77
  dark-ink: "#ededed"
85
78
  dark-body: "#c7c7c7"
86
79
  dark-muted: "#8f8f8f"
87
80
  dark-line: "#262626"
88
81
  dark-input: "#696969"
89
- dark-seam: "#1a1a1a"
90
82
  dark-on-primary: "#0a0a0a"
91
- dark-action: "#ffffff"
83
+ dark-action: "#ededed"
92
84
  dark-action-foreground: "#0a0a0a"
93
- dark-action-hover: "#e4e4e4"
94
- dark-action-active: "#d6d6d6"
85
+ dark-action-hover: "#d6d6d6"
86
+ dark-action-active: "#c7c7c7"
95
87
  dark-link: "#4ecf8c"
96
88
  dark-link-deep: "#93e3b8"
97
89
  dark-link-bg-soft: "#00351c"
@@ -115,26 +107,15 @@ colors:
115
107
  dark-state-blocked: "#d3302f"
116
108
  dark-state-complete: "#00a85a"
117
109
  dark-logo-dot: "#4d6bff"
118
- dark-selection-bg: "#ededed"
119
- dark-selection-fg: "#0a0a0a"
120
- dark-navigation-selection-bg: "#1a1a1a"
110
+ dark-navigation-selection-bg: "#1f1f1f"
121
111
  dark-navigation-selection-fg: "#ededed"
122
112
  dark-navigation-selection-hover: "#262626"
123
- dark-cross: "#555555"
124
113
  dark-chart-1: "#4d6bff"
125
114
  dark-chart-2: "#00a85a"
126
115
  dark-chart-3: "#ffd866"
127
116
  dark-chart-4: "#e8615f"
128
117
  dark-chart-5: "#33d1cb"
129
118
  dark-chart-6: "#9575cd"
130
- dark-chart-seq-1: "#4f6ae6"
131
- dark-chart-seq-2: "#7d99f2"
132
- dark-chart-seq-3: "#a9befa"
133
- dark-chart-seq-4: "#cdd9fd"
134
- dark-chart-seq-5: "#eef2ff"
135
- dark-chart-div-warm: "#ffd866"
136
- dark-chart-div-mid: "#e5e5e5"
137
- dark-chart-div-cool: "#b3c1ff"
138
119
 
139
120
  typography:
140
121
  display-xl:
@@ -219,8 +200,6 @@ rounded:
219
200
  xl: 10px
220
201
  2xl: 12px
221
202
  3xl: 16px
222
- pill-sm: 64px
223
- pill: 100px
224
203
  full: 9999px
225
204
 
226
205
  spacing:
@@ -248,489 +227,343 @@ spacing:
248
227
 
249
228
  ## Overview
250
229
 
251
- nadicode is an AI agency for SMBs. The design system makes the website and the shipped product read as one family: a calm, high-contrast monochrome architecture in which the identity layer — the Agent orbs and Nadia's striped ribbon among them — is the expressive element. Structure, chrome, and controls are neutral; a desaturated screen loses nothing structural. The interface helps a pragmatic buyer understand the work: repeated workflows, agents working beside people, inspectable outputs, human approval.
230
+ This file is nadicode's design system and its only design authority. The tokens above are the values; the sections below are the rules for applying them. Every component in `@nadicodeai/ui`, every website section, and every screen in the Portal and in Nadia builds on it. Another document may say how one product applies a rule, and links here for the rule; it never restates one. A rule that is missing here is added here first.
252
231
 
253
- One foundation, three registers. The foundation is every `core` value in this contract: the colour roles and their dark pairs, the two type families and the type scale, the spacing ladder and the control geometry, the radius scale, the material values, the four-rung elevation ladder, the motion durations and easings, the icon render, and the brand marks. It is owned by no single product and available to every product; a value stays foundation whether one product renders it or three. A register is one product's way of applying it: its direction, its density, the rungs and steps it reaches for, its dark-mode stance, its structural grammar, its motion budget. The three registers are the website, the Portal, and the Nadia desktop app; `## Registers` names each one's rulebook, its own token families, and its dark-mode stance.
232
+ nadicode puts AI Agents to work inside small and mid-sized companies. Its sites and products read as a technology company with a real product: the interface is neutral and exact, the product is shown working, and colour and depth give the page its energy. Three ideas carry every screen:
254
233
 
255
- A register decides where a foundation value appears and adds rules of its own. It never redefines a foundation value and never mints one: a value its physics need and the foundation lacks is authored in this contract under `register.<name>`.
234
+ - **Neutral structure.** Chrome, controls and text sit on an achromatic spine, so a desaturated screen loses nothing structural. The strongest contrast on a screen belongs to the action.
235
+ - **Colour as stage.** The five identity hues are full-strength grounds for sections, stages behind product views and glass, and the colour of Agents. They carry the brand; they never decorate a control.
236
+ - **Real depth.** Surfaces sit at four heights. In light, shadow shows the height; in dark, tone shows it. Glass floats over colour and moving Agents.
256
237
 
257
- `DESIGN.md` defines the reusable contract and exported scalar tokens; the `@nadicodeai/design-system/css` bundle (`src/css/**`) realises that contract. Homepage copy, page-specific ordering, and temporary prototypes do not belong in this file.
238
+ The brand is still: only Agents move on their own ("Motion").
258
239
 
259
- The settled target state of this contract is [the nadicode design system ADR](../../docs/adr/2026-09-23-the-nadicode-design-system.md): the brand is still, only Agents move. A section that ADR changes opens with a **Target state** paragraph naming the decision and the issue that ships it; until that issue lands, the rest of the section describes what is built. The issue's change replaces the pointer with the rule as shipped.
240
+ One foundation serves three registers. The foundation is every value in this file. A register is one product's way of applying it: the website, the Portal and the Nadia desktop app ("Registers"). A register adds rules and chooses which values it uses; it never redefines a value or mints one. A value one register needs is authored here under `register.<name>`, and a value two registers share moves to `core`.
260
241
 
261
242
  ## Colors
262
243
 
263
- **Two greens, and only two.** `{colors.link}` #00713d, dark `{colors.dark-link}` #4ecf8c, is the green that carries text: links, navigational accents, approval states, and the deep and soft steps the `success` pair reuses. Identity verde `{colors.identity-verde}` #00a85a is the green that carries marks. Nothing else in this contract is green. A third green always turned out to be one of those two at a slightly different lightness, which is why `primary` #007a3c and `verde-vivo` #008c45, with their dark pairs `dark-primary` and `dark-verde-vivo`, no longer exist: the logo is ink with a cobalt dot and needs no brand verde, and the presence mark `verde-vivo` painted is retired with the portrait it sat on. The shadcn `primary` role is unaffected and still maps to the neutral action family.
244
+ ### Neutral structure
264
245
 
265
- Neutrals carry the interface: grounds, chrome, hierarchy, and every control come from the achromatic spine, and the strongest contrast on any surface lands on the primary action. Chromatic colour appears where it says something a reader already knows how to read: the semantic feedback hues, the two greens in their scoped duties, the chart palettes on data, and the Agent identity palette inside the Agent visuals. Chrome, sections, and controls stay unpainted.
246
+ The neutral spine carries grounds, chrome, text and every control: `{colors.ink}`, `{colors.body}`, `{colors.muted}`, `{colors.canvas}`, `{colors.canvas-soft}`, `{colors.canvas-soft-2}`, `{colors.line}`, the material tones, and their dark pairs. Every neutral is an equal-channel grey (OKLCH chroma 0) in both themes, so no tint paints a surface. Colours are authored here as sRGB hex and emitted as `oklch()`.
266
247
 
267
- Interaction states are material, not meaning: focus, hover, and selection read as contrast steps on the neutral spine. A hover ground is a neutral step; the only change a control hover carries is a step inside its own family, as `{colors.action-hover}` does on the action control. Cobalto stays on info and running.
248
+ - **Action** (`{colors.action}`, `{colors.action-foreground}`, `{colors.action-hover}`, `{colors.action-active}`): the primary control is ink with a white label in light, and dark ink `{colors.dark-action}` with a near-black `{colors.dark-action-foreground}` label in dark, never pure white. The shadcn `primary` role maps here.
249
+ - **Text**: `{colors.ink}` for headings and primary text, `{colors.body}` for supporting text, which clears AA on every canvas. `{colors.muted}` is for placeholders and disabled or inactive text only, never for descriptions, labels or help.
250
+ - **Lines**: `{colors.line}` is the one decorative 1 px divider and is deliberately quiet. `{colors.input}` is the boundary of a field or any control whose edge identifies it (an input, a select, a checkbox, a radio), and clears the 3:1 non-text floor. A button is identified by its label, so an outline or secondary button's edge is the `{colors.line}` hairline, never `{colors.input}`. Never darken a decorative line locally to make it act as a control edge.
251
+ - **Focus** (`{colors.focus-ring}`): a full-opacity outline of `{spacing.focus-outline-width}` at `{spacing.focus-outline-offset}`, the maximum-contrast neutral on each ground, never a box-shadow ring and never a hue.
252
+ - **Interaction states** are contrast steps on the spine. Hover takes the `accent` role (`{colors.canvas-soft-2}`) and a selected row the `muted` role (`{colors.canvas-soft}`); a control's own hover is a step inside its family, as `{colors.action-hover}` is. Persistent navigation marks the current destination with `{colors.navigation-selection-bg}` and `{colors.navigation-selection-fg}`, and local tabs mark the current view with the same pair through the `selection` role.
253
+ - **Scrim** (`{colors.scrim}`): the modal overlay at the `scrim-opacity` material value, never blurred.
268
254
 
269
- **Table and action affordances.** Across products, hovering a table body row
270
- reveals a visible, full-width neutral fill. Use the full soft-canvas step
271
- (`bg-accent` in React), without reducing its opacity. The same row treatment
272
- accompanies keyboard focus inside a row and an open row menu; keep the focused
273
- control's own outline. Header and footer rows remain stable. Preserve text,
274
- status, and selection meaning, with no layout shift.
255
+ ### Colour as stage
275
256
 
276
- Enabled buttons, including the three-dot action trigger, show a pointer cursor.
277
- Disabled controls do not advertise an available action. Row highlighting also
278
- helps scan read-only data: only give the whole row a pointer cursor when it has
279
- an actual row action. Touch access must not depend on hover. Shared React
280
- behavior belongs to `TableRow` and the UI stylesheet, consumed by all product
281
- adapters. Verify light and dark themes, keyboard focus, menu opening and closing,
282
- and compact widths in the rendered product.
257
+ The identity palette is the brand's colour: cobalto `{colors.identity-cobalto}`, verde `{colors.identity-verde}`, arancio `{colors.identity-arancio}`, giallo `{colors.identity-giallo}` and rosso `{colors.identity-rosso}`, with `{colors.identity-ink}` and `{colors.identity-white}` as the poles type sits on. Azzurro `{colors.identity-azzurro}` has one job: the first stripe of Nadia's ribbon.
283
258
 
284
- Every value a role needs is carried by the role token itself; there is no numeric scale family. The neutral spine — `{colors.ink}`, `{colors.body}`, `{colors.muted}`, `{colors.canvas}`, `{colors.canvas-soft}`, `{colors.canvas-soft-2}`, `{colors.line}`, `{colors.seam}` — is strictly achromatic: every neutral is an equal-channel gray (OKLCH chroma 0) in both themes. The five identity-palette hues are cross-spectrum equals, so any neutral bias would flatter one and dirty another; hue arrives full-strength through the chromatic roles or not at all, and no tint ever paints an app surface. Colours are authored as sRGB hex here and emitted as `oklch()` in the generated CSS.
259
+ The hues appear at full strength and flat, with no tints, steps, gradients or hover variants, in these places:
285
260
 
286
- ### Semantic roles (functional tier)
261
+ - **Section grounds on the website.** A section may take one identity hue as its whole ground. Colour sections alternate with white, soft and black ones, and two neighbouring sections never share a ground.
262
+ - **Stages.** The ground behind a floating product view, and the colour a glass surface shows through ("Elevation & Depth").
263
+ - **Accents on the website.** A 2 px rule that marks one item of a set, one hue per item.
264
+ - **Agents.** The orbs ("Agent orbs").
265
+ - **The logo.** The dot of the wordmark and the icon tile ("Brand media authority").
266
+ - **Imagery.** Art direction reads these tokens and keeps no private palette.
287
267
 
288
- - **Action** (`{colors.action}` = ink `#111111`, `{colors.action-foreground}` = white, `{colors.action-hover}`, `{colors.action-active}`): the primary-control family is neutral — a white label on the ink field in light shells, and in dark shells the `dark-action-*` roles, a white control carrying a near-black label. Maximum contrast belongs to the thing that acts. The shadcn `primary` roles map here, and `{colors.on-primary}` / `{colors.dark-on-primary}` are the foregrounds that role carries.
289
- - **Canvas / Line / Input / Cross / Seam** (`{colors.canvas}`, `{colors.line}`, `{colors.input}`, `{colors.cross}`, `{colors.seam}`): white content cells, the universal decorative 1 px divider, the resting-control boundary, the stronger mark/crosshair gray, and the dashed connector guide. `{colors.line}` owns ordinary seams and is deliberately quiet — a decorative hairline, not held to a non-text contrast floor. `{colors.input}` owns fields and other interactive boundaries and clears the 3:1 non-text floor against its canvas. A labeled filter or search control already identified by readable text and its icon may use the quiet line role for an optional perimeter, configured by its composition owner. Form fields whose boundary identifies the input keep the input role. Focus remains a separate full-contrast state. Never darken a decorative border locally to make it behave like a control.
290
- - **Body / Muted** (`{colors.body}`, `{colors.muted}`): `{colors.body}` is readable supporting copy and clears AA on canvas; `{colors.muted}` is restricted to disabled, inactive, and placeholder content. Do not use muted text for ordinary descriptions, labels, or help text.
291
- - **Focus ring** (`{colors.focus-ring}` / `{colors.dark-focus-ring}`): the ink keyboard-focus indicator, a full-opacity `outline` of `{spacing.focus-outline-width}` at `{spacing.focus-outline-offset}` offset, never a box-shadow. Focus is a state of material, so the indicator is the maximum-contrast neutral on each ground rather than a hue; the offset keeps it independent of the resting border, and it clears the 3:1 non-text floor against the adjacent canvas.
292
- - **Scrim** (`{colors.scrim}` / `{colors.dark-scrim}`): the semantic modal-overlay foreground. Its opacity is the generated material token declared in `Elevation & Depth`; it is never combined with backdrop blur.
293
- - **Link** (`{colors.link}`, `{colors.link-deep}`, `{colors.link-bg-soft}`): inline links, navigational accents, approved/example proof states, and the green entity-status tone. Green, keeping the Italian signal without turning the interface into flag decoration; `link` and `link-deep` clear AA as text on canvas, and `link-bg-soft` is the tinted green ground for link and tag chrome only: a message or conversation plane is a neutral surface, never a link-tinted one.
294
- - **Navigation selection** (`{colors.navigation-selection-bg}`, `{colors.navigation-selection-fg}`, `{colors.navigation-selection-hover}`): the selected destination in persistent product navigation, a neutral selection field. Selection is a place, not a meaning, so it holds by contrast on the neutral spine; cobalto belongs to the semantic info and running roles only. The dark counterparts are tuned independently for the black console. Resting and hover pairs clear AA for text, and the offset focus outline stays visible against both the selection fill and the adjacent canvas.
295
- - **Semantic feedback** (`{colors.success}` = verde with `{colors.success-soft}`/`{colors.success-deep}`; `{colors.error}` = rosso funzionale `#d3302f` with `{colors.error-soft}`/`{colors.error-deep}`; `{colors.warning}` = giallo `#ffc220` with `{colors.warning-soft}`/`{colors.warning-deep}`; `{colors.info}` = cobalto `#1e3cff` with `{colors.info-soft}`/`{colors.info-deep}`): validation, caution, approval, and operational feedback. In each pair the `-soft` value is the tinted tag/banner ground and the `-deep` value is the AA-clearing text on it. On a solid `error` fill (the shadcn `destructive` role) text is white; on a solid `warning` or `success` fill text is ink. The success pair intentionally reuses the link pair's AA-safe verde values; its feedback meaning remains distinct from inline navigation and entity-status semantics.
296
- - **Accent** (`{colors.accent}` = arancio `#ff5a1f`): a small emphasis mark for exception flags and highlights; it appears as a mark, carries no text, and paints no fill or background. The identically-named shadcn surface role is a different thing — a neutral (`{colors.canvas-soft-2}` light, `{colors.dark-canvas-soft-2}` dark) with no hue.
297
- - **Workflow States** (`{colors.state-ready}` neutral, `{colors.state-running}` cobalto, `{colors.state-review}` giallo, `{colors.state-blocked}` rosso, `{colors.state-complete}` verde): the left-border/state vocabulary for agentic work surfaces — cobalto means in motion, verde means done. The dark counterparts (`{colors.dark-state-ready}`, `{colors.dark-state-running}`, `{colors.dark-state-review}`, `{colors.dark-state-blocked}`, `{colors.dark-state-complete}`) keep the same functional identity. These colours are carried by non-text state accents only — meter fills, delta marks, and the brand book's own rails; label text stays on the neutral foreground roles for contrast in both themes. Status is not one of their jobs: every status the product renders is the Badge status vocabulary (see `## Status Vocabulary`), which reads from the semantic feedback and link pairs, and the two token sets never cross.
268
+ Text on a colour ground is the pole that clears AA on that hue: `{colors.identity-white}` on cobalto and rosso; `{colors.identity-ink}` on verde, arancio and giallo. A control on a colour ground uses the button's on-colour variants ("Components"). Product chrome in the Portal and Nadia stays neutral: the identity hues enter a product screen only as Agents, charts and the feedback roles below.
298
269
 
299
- ### Chart categoricals (functional tier)
270
+ ### Meaning
300
271
 
301
- - **Charts** (`{colors.chart-1}`…`{colors.chart-6}`): the categorical series palette — cobalto, verde, giallo, rosso, plus a functional-only teal and viola. Chart colours belong to data surfaces only; identity planes and generated imagery draw from the Agent identity palette. Two light slots are series-tuned so the set clears the categorical data bands on the white data surface: `chart-3` steps the giallo darker (the identity giallo `{colors.identity-giallo}` overshoots the categorical lightness band and reads too pale as a mark) and `chart-5` makes the teal more chromatic (the raw functional teal otherwise sits under the chroma floor and reads gray). The darker giallo still falls below the 3:1 mark-contrast line and is carried by the always-on legend and the paired table the charting method already mandates — never a per-surface patch. On the dark canvas all six series carry dark-tuned steps (`{colors.dark-chart-1}`…`{colors.dark-chart-6}`), remapped in the `.dark` scope so any consumer reading the semantic token gets the right hue in both modes. Clearing the 3:1 WCAG 1.4.11 data-mark floor is necessary but not sufficient on the black canvas: full-strength hues read as loud blocks there, so each dark step lightens or desaturates while keeping series identity — the dark giallo and teal deliberately stay bright to hold their per-series deuteranopia separation from rosso — and all six clear 3:1 against `{colors.dark-canvas}`.
302
- - **Categorical distinguishability**: the same-family neighbours are held apart — `chart-3`/`chart-4` (giallo/rosso) and `chart-2`/`chart-5` (verde/teal) each clear a CIEDE2000 ≥ 20 normal / ≥ 15 deuteranopia-simulated floor, in the light set and the dark-tuned set alike. Two proximities are brand-locked and exempt from the numeric floor: `chart-2`/`chart-4` (verde/rosso, the flag pair, which collapses under red-green deficiency by identity) and `chart-1`/`chart-6` (cobalto/viola, adjacent blue-violet). Charts using the full six carry redundant encoding (the legend, position, or the paired table), never hue alone, so these two pairs stay readable under colour-vision deficiency.
272
+ - **Feedback** pairs a hue with a soft ground and a deep text step: success (verde), warning (giallo `{colors.warning}`), error (rosso `{colors.error}`), info (cobalto `{colors.info}`). The `-soft` value is the tinted ground of a tag or banner and the `-deep` value is the AA text on it; a component reaches the pair through the `destructive-soft`, `warning-soft`, `success-soft` and `info-soft` roles and their foregrounds. On a solid fill, error takes white text and warning and success take ink in light; in dark all three take `{colors.dark-on-primary}`.
273
+ - **Link** (`{colors.link}`, `{colors.link-deep}`, `{colors.link-bg-soft}`): inline links and the green status tone. Text green is always the link green; verde as a mark is always identity verde.
274
+ - **Workflow states** (`{colors.state-ready}`, `{colors.state-running}`, `{colors.state-review}`, `{colors.state-blocked}`, `{colors.state-complete}`) colour non-text marks only: meter fills and trend icons. Cobalto means in motion and verde means done. A state's word stays on the neutral text roles.
275
+ - **Status** is always a Badge tone with a mark and a word ("Components").
303
276
 
304
- ### Agent identity palette
277
+ ### Dark mode
305
278
 
306
- Five full-strength hues are the system's expressive chromatic range: verde `{colors.identity-verde}`, rosso `{colors.identity-rosso}`, cobalto `{colors.identity-cobalto}`, arancio `{colors.identity-arancio}`, and giallo `{colors.identity-giallo}`, with `{colors.identity-white}` and `{colors.identity-ink}` as the white and ink poles that carry type beside them. `{colors.identity-azzurro}` is the sixth hue and has one job: the azzurro stripe of Nadia's ribbon. The family emits under the `identity-*` spelling, and that is the one name every consumer uses. They live in exactly three places: the Agent orbs ([`packages/ui/docs/agent-orb.md`](../ui/docs/agent-orb.md) says which hues an orb may wear), the brand book's identity planes, and generated editorial imagery (`skills/marketing/blog-imagegen/references/editorial-register.md` reads these tokens and keeps no private palette). They appear at full strength with no tints, steps, or hover variants; interface states come from the semantic roles.
279
+ Every token `X` with a `dark-X` pair takes the dark value under a `.dark` ancestor; a component uses the same role in both modes and never ships a `dark:` utility or a mode branch of its own. The dark page is true black, `{colors.dark-canvas}`. Elevated surfaces lift from it by tone ("Elevation & Depth"); the soft steps `{colors.dark-canvas-soft}` and `{colors.dark-canvas-soft-2}` are interaction fills and never become a page, section or panel ground. On a lifted surface the hover and muted fills step up from that surface's own tone. Dark feedback and chart values are brightened so they read on black; `{colors.dark-input}` stays brighter than `{colors.dark-line}` so a field's edge clears 3:1. A surface that is dark by nature, such as Nadia's entry screen, a terminal console, a black website section or a black brand plane inside a product (`BrandField`), stays dark in every theme as one fixed treatment: it scopes `.dark`, so every role and control inside it takes its dark value.
307
280
 
308
- ### Dark primitives
281
+ ### Roles
309
282
 
310
- **Dark Primitives** (`{colors.dark-canvas}`, `{colors.dark-ink}`, `{colors.dark-line}`, and siblings) are the source tokens for the generated shadcn `.dark` role map — mode-bearing primitives, not a separate palette; consuming apps opt in by applying a `.dark` ancestor through their runtime theme provider. `dark-canvas` is true black: the only base plane for dark product shells. The remaining dark neutrals (`dark-canvas-soft`, `dark-canvas-soft-2`, `dark-ink`, `dark-body`, `dark-muted`, `dark-line`, `dark-input`, `dark-seam`, `dark-on-primary`, `dark-scrim`, and the neutral `dark-selection`/`dark-cross` marks) are exact equal-channel grays (OKLCH chroma 0), so no hue paints a surface. The soft canvas steps belong only to bounded interaction states such as hover, selection, keycaps, and inline controls; they never replace true black as a page, section, card, popover, sidebar, or large-panel ground. `dark-input` is the resting-control boundary, held brighter than the decorative `dark-line` border so a field edge clears the non-text floor before `{colors.dark-focus-ring}` appears. The `dark-action` family is a white control stepping through light grays, ink-labelled — the primary control stays neutral on black. Chroma lives only in the intentional accents: `dark-link`/`dark-link-deep` a lighter verde for links, and `dark-error`/`dark-error-deep`, `dark-warning`/`dark-warning-deep`, `dark-info`/`dark-info-deep`, `dark-success`/`dark-success-deep` the brightened feedback hues, each `*-soft` a deep tinted ground. `dark-navigation-selection-*` and `dark-focus-ring` sit on the neutral spine: selection and focus are material states, not meanings. The `dark-state-*` workflow family is not a text role; it preserves the matching ready/running/review/blocked/complete rail colours for 3 px state accents while text stays on neutral foreground roles. `dark-selection-bg`/`dark-selection-fg` invert the light selection, `dark-scrim` supplies the modal foreground, and `dark-cross` is the visible mark gray, brighter than the decorative `dark-line`. Never reintroduce chroma into the neutral set.
311
-
312
- This achromatic-neutral discipline is not dark-specific: it governs the generated shadcn surface-role bridge in both the light `:root` and dark `.dark` maps. The neutral surface family — `background`, `card`, `popover`, `muted`, `secondary`, `sidebar`, `sidebar-accent`, their neutral foreground/border variants, plus `border` and `input` — resolves only to exact equal-channel grays (OKLCH chroma 0) in both themes. Hue in the role bridge is reserved for the explicit semantic roles `destructive`, `success`, and `warning`; `primary` resolves to the neutral action family. The focus rings (`ring`/`sidebar-ring`) and the `sidebar-primary*` navigation-selection family resolve to equal-channel grays like the surface family: focus and selection are material states, and no interaction state carries a hue. In dark product shells, `background`, `card`, `popover`, and `sidebar` share `{colors.dark-canvas}` so page chrome, cards, tables, overlays, and nav rails read as one console. `muted`, `secondary`, and `accent` retain the `dark-canvas-soft` steps for small interaction states such as hover, selected rows, keycaps, and inline controls. Large product surfaces never take a tinted or soft-panel fill by default.
283
+ The shared colour roles are authored once, in the token extension's `semantic` group: each role (`background`, `card`, `popover`, `primary`, `muted`, `accent`, `border`, `input`, `ring`, `sidebar`, the feedback roles `destructive`, `warning`, `success` and `info` with their `-soft` pairs, the chart roles, and the rest) names one core token per mode. The Portal, the website and `@nadicodeai/ui` paint with these roles; Nadia's theme translates Hermes's role names onto the same ones and adds the shared roles Hermes has no name for. A product needing a role that does not exist adds it here; it never picks a colour locally.
313
284
 
314
285
  ## Data Visualization
315
286
 
316
287
  ### Chart source
317
288
 
318
- Every chart and KPI card is a stock shadcn component. They are pulled from the registry at `https://ui.shadcn.com/r/styles/new-york-v4/<name>.json`, because the `base-nova` style this system uses publishes no chart gallery. The KPI card is the `dashboard-01` block's `section-cards`. `@nadicodeai/ui` owns the pulled files and the card frame each one renders; a page never assembles a chart card by hand and never reaches into a chart's internals from its call site.
319
-
320
- These are the deviations from what the registry ships. The list is complete: a change to a pulled chart that is not here is drift, and `packages/ui/tests/guards/chart-kit.test.ts` holds each one.
321
-
322
- 1. **Value formatting.** `valueFormatter` prints the exact figure in the tooltip, in the units the surface uses. `axisFormatter` prints the axis as a scale (`1.6B`, `800M`, `$7.5`), because an axis is read for magnitude and a full figure on every tick crowds it.
323
- 2. **Color by entity.** A series takes its color from the entity-color map below, never from its position in the series array. See "Entity-color assignment".
324
- 3. **Value axis.** Always drawn, with five ticks handed to the axis on a 1, 2, 2.5, 5, or 10 step. Left to itself Recharts chooses its own count, and a rounded label then sits on an unrounded position.
325
- 4. **Bar width.** Bars cap at 48px. Stock has no cap, and one category drew a bar the width of the card.
326
- 5. **Reading aids.** The grid is dashed in both directions. The tooltip uses the stock line indicator, with a gap between a row's name and its value so a long name cannot run into the figure. Every multi-series chart has a legend below the plot, left-aligned, a round dot and a name, in the order of the series it names; the stock area and grouped-bar blocks ship without one, and Recharts sorts legend items by key.
327
- 6. **Share of a total.** `chart-bar-stacked` has a `share` layout: one bar split to 100% (`stackOffset="expand"`), for a breakdown with no time axis.
328
- 7. **A chart is a named section.** The card title is a heading at the level the page gives it, and it names the plot through `aria-labelledby`. The plot shows a focus ring while it has keyboard focus: stock hides the outline of the svg that Recharts' `accessibilityLayer` makes focusable.
329
- 8. **Signed category comparisons.** `chart-bar-horizontal` starts from the shadcn block of that name. Its numeric axis retains negative values and a visible zero reference so Customer losses and gains share one baseline; an all-positive or all-negative reading uses only its occupied side. Native positive and negative series give the stock legend semantic gain/loss colors. The stock tooltip's formatter slot preserves the original decimal reading and full category label when the axis abbreviates it. The compact ranking owner remains available for ordinary entity comparisons.
330
-
331
- Two things beside the charts also differ from stock. `Progress` takes a `tone` (default, warning, critical) and paints over-cap as a full critical bar. The pulled charts share one card frame, `chart-card`, in place of each file repeating it.
332
-
333
- Everything else is the registry's: bar geometry and corner radius, tooltip and legend content, animation, and the recharts `accessibilityLayer` as shadcn ships it. This contract mandates no custom mark geometry, no segment gap, and no hidden table twin of a chart.
289
+ Every chart and KPI card is a stock shadcn registry component owned by `@nadicodeai/ui`; a page composes them and never assembles a chart card or reaches into a chart's internals. The package keeps a short, tested list of deliberate deviations from the registry. The visual ones are rules:
334
290
 
335
- ### Entity-color assignment
291
+ - The tooltip prints the exact figure in the surface's units; the axis prints a scale (`1.6B`, `800M`).
292
+ - The value axis is always drawn, with five ticks on a 1, 2, 2.5, 5 or 10 step.
293
+ - Bars are capped in width, the grid is dashed in both directions, and plot heights are fixed.
294
+ - A multi-series chart has a legend below the plot, left-aligned and wrapping: a round dot and the entity name, in series order.
295
+ - A signed comparison keeps a visible zero line so losses and gains share one baseline, and colours each bar by its consequence: `success` where the value is favourable, `destructive` where it is not.
296
+ - A chart is a named section: its card title is the heading that names the plot.
336
297
 
337
- - **Entity-color assignment**: categorical hues (`{colors.chart-1}`…`{colors.chart-6}`) assign in fixed order to entities per page context, and the assignment follows the entity identity, never rank, sort position, or array index. One assignment map exists per surface: the same entity keeps the same color in a chart and its paired table, whether the chart uses one categorical bar per entity or a time series per entity. A filter or re-sort never repaints the entities that remain on screen. Beyond six entities, fold the tail into a single neutral "other" slot (`{colors.muted}` register), never a seventh hue.
298
+ No pie, donut, radar, radial or dual-axis chart, and no mark or logo inside a legend.
338
299
 
339
- ### Sequential ramp and diverging pair
300
+ ### Palette and assignment
340
301
 
341
- - **Sequential ramp** (`{colors.chart-seq-1}`…`{colors.chart-seq-5}`, a single cobalto hue running light→dark): ordered magnitude on one hue, never nominal categories, never a rainbow. `{colors.dark-chart-seq-1}`…`{colors.dark-chart-seq-5}` remap in the `.dark` scope like the categorical set, anchor flipped so `dark-chart-seq-1` is the dimmest step and `dark-chart-seq-5` the brightest, each clearing the 3:1 WCAG 1.4.11 data-mark floor against `{colors.dark-canvas}`.
342
- - **Diverging pair** (`{colors.chart-div-warm}` giallo/amber, `{colors.chart-div-mid}` a neutral mid, `{colors.chart-div-cool}` cobalto; dark counterparts `{colors.dark-chart-div-warm}`, `{colors.dark-chart-div-mid}`, `{colors.dark-chart-div-cool}` remapped in the `.dark` scope the same way): the warm and cool poles carry the sign, the neutral midpoint never carries hue. Meter tracks use a lighter step of the same ramp as their fill.
302
+ Six categorical series colour data and nothing else: `{colors.chart-1}` cobalto through `{colors.chart-6}` viola. Same-family neighbours (chart-3 and chart-4, chart-2 and chart-5) are held apart in normal and deuteranopia-simulated vision by the build. The light chart-3 sits below 3:1 on white, so a chart relies on its always-on legend and never on hue alone. Dark steps (`{colors.dark-chart-1}` and the rest) clear 3:1 on black.
343
303
 
344
- ### Meter grammar
304
+ A hue follows its entity, never its rank or position: one assignment per surface, the same in a chart and its table, stable under filtering and sorting. Past six entities the tail folds into one neutral "other" in `{colors.muted}`.
345
305
 
346
- A meter describes magnitude. The carrier is the stock shadcn `Progress`, with
347
- one `tone` variant: default, warning, or critical. There is no second meter
348
- geometry, no cap tick, and no separate overflow segment. A value over its cap
349
- reads as a full bar in the critical tone beside the printed figure, which is
350
- what states the overage.
306
+ ### Meters, deltas and labels
351
307
 
352
- The product defines its tone's semantic scope: the condition of the measured
353
- allowance, for example, or an operational access state. Those meanings are not
354
- interchangeable. Keep the measure, tone, label, and explanation consistent with
355
- the chosen scope; a ratio reaching its end does not independently establish the
356
- state of another capability. The foundation does not choose a product's color
357
- entry screens or reinterpret a recorded balance as an access guarantee.
308
+ - **Meter**: the stock `Progress` in one tone, default, warning or critical, with an accessible name and the real value printed beside it. Over its cap it is a full bar in the critical tone; the printed figure states the overage.
309
+ - **Delta**: an outline Badge with a trend icon (`TrendingUp`, `TrendingDown`, `Minus`) and the signed value, followed by a muted "vs {period}". Direction crossed with whether up is good sets the tone; a neutral direction stays neutral. Never bare signed text, colour alone, or a ▲▼ glyph. A figure with no comparison shows no badge.
310
+ - **Labels**: legend, axis and label text are ink roles and never wear the series colour. A single series is titled, not legended. Tooltips enhance a chart and never hold the only copy of a value; where a page needs the numbers as text, it shows a real table built from the same data.
358
311
 
359
- A meter keeps an accessible name, the real textual value beside it, and `aria`
360
- values bounded to the meter maximum. Color reinforces its defined meaning; it
361
- is never the only explanation.
312
+ ## Typography
362
313
 
363
- ### Delta and trend encoding
314
+ Geist and Geist Mono are the two families. The twelve tokens are named by role, never by size; strong is a weight, so a 500 run of body copy is `body-md` at weight 500. `code` and `label-mono` are Geist Mono; every other token is Geist.
364
315
 
365
- - **Delta and trend encoding**: a change reads as a shadcn `Badge` in its `outline` variant carrying a lucide trend icon (`TrendingUp`, `TrendingDown`, or `Minus`) and the signed value, followed by the muted phrase "vs {period}" that names what the change is measured against. Never bare signed text, never color alone, and never a ▲ or ▼ glyph. The icon carries the direction; tone follows direction crossed with whether up is good in that context, and a delta whose direction means nothing good or bad stays neutral. Sparklines normalize against a meaningful baseline, never the visible min-max span alone. A ranked "what moved" list uses the same badge as the KPI card, and a figure with no comparison shows no badge rather than a zero change.
316
+ | Token | Use |
317
+ | --- | --- |
318
+ | `{typography.display-xl}` | The largest page title. |
319
+ | `{typography.display-lg}` | A hero headline and the one statement of a page. |
320
+ | `{typography.display-md}` | A section heading. |
321
+ | `{typography.heading-lg}` | A standard section title inside a page or panel. |
322
+ | `{typography.heading-md}` | A card, artifact or panel title. |
323
+ | `{typography.heading-sm}` | The smallest heading, above a group or a step. |
324
+ | `{typography.body-lg}` | A lead paragraph. |
325
+ | `{typography.body-md}` | Default body copy; large control labels at 500. |
326
+ | `{typography.body-sm}` | Secondary copy, navigation, compact cells; control labels at 500. |
327
+ | `{typography.caption}` | Badges, compact chrome labels, footer lines. |
328
+ | `{typography.code}` | Tool calls, snippets, traces. |
329
+ | `{typography.label-mono}` | Opaque identifiers, codes and mono eyebrows; never a person's, Agent's or computer's display name. |
330
+
331
+ A token has one size at every width; nothing is sized in `vw` or `vh`. Only the three display tokens carry negative tracking. Card, panel and section titles are sentence-case Geist. The website and the CSS components name the tokens through the `nc-type-*` classes; product UI in `@nadicodeai/ui`, the Portal and Nadia sizes text with Tailwind's `text-xs`, `text-sm` and `text-base`, which match `caption`, `body-sm` and `body-md` in size.
366
332
 
367
- ### Legends and labels
333
+ ## Layout
368
334
 
369
- - **Legends and labels**: at two or more series the legend is the registry's `ChartLegendContent`, placed below the chart, left-aligned, and wrapping to a second line. It carries a color dot and the entity name, and nothing else: no maker's mark, no logo tile, no value, no share. A model's mark appears beside its name in tables and lists, where the mark identifies the model (rule `model-shown-with-logo`), never inside a legend. Legend, axis, and label text carries ink tokens (`{colors.ink}`, `{colors.muted}`) and never wears the series color. A single series is titled, not legended. Tooltips enhance a chart and never gate a value.
370
- - **Accessible alternative**: the chart's accessibility comes from the stock `accessibilityLayer`. A chart ships no visually hidden table twin of itself. Where a page needs the numbers as text, it shows a real table or list that the reader can see, built from the same result the chart reads.
335
+ Every gap, pad and stack step a page, section or composition sets is a rung of the spacing ladder, `{spacing.xxs}` 4 px through `{spacing.6xl}` 128 px; a layout picks the rungs it needs and never interpolates between them. Inside a component, the stock shadcn registry's own spacing stands. Every visible line has one owner: a seam is `{spacing.guide}` in `{colors.line}`, a control edge is `{colors.input}`, and focus is the focus outline.
371
336
 
372
- ## Typography
337
+ Control geometry is the same in every product:
373
338
 
374
- **Twelve sizes, named by the role they play.** A token says what a string is — a display line, a heading, body copy, a caption, code, a mono label — never how big it is, so a page that changes its mind about size changes one token and not every call site. Strong is a weight, not a token: a 500 run of body copy is `body-md` with `font-weight: 500`, which is why `body-md-strong`, `body-sm-strong`, `caption-strong` and the two `button-*` tokens are gone.
339
+ | Token | Owns |
340
+ | --- | --- |
341
+ | `{spacing.control-height}` | The height of every control in product chrome |
342
+ | `{spacing.control-padding-inline}` | A control's inline padding |
343
+ | `{spacing.touch-target}` | The minimum target on coarse or non-hover input, whatever the visual size |
344
+ | `{spacing.icon-stroke}`, `{spacing.icon-stroke-sm}` | The icon line ("Iconography") |
345
+ | `{spacing.focus-outline-width}`, `{spacing.focus-outline-offset}` | The focus outline |
375
346
 
376
- Geist Sans and Geist Mono are the two families, and there is no third. Each token below carries its own — `code` and `label-mono` are Geist Mono, every other token is Geist Sans — and its Use column says what that token is for. The register law that decides which family a given string takes, and what a card, panel, or section title therefore takes, is stated once in `## Components` → **`label-mono`**.
347
+ Product chrome uses the 32 px control. A page header, a toolbar, a card's header and footer, a tab row, a filter row and a collection row's actions take `{spacing.control-height}` with a `{typography.body-sm}` label at 500; a button's `sm` and `xs` sizes are for a control nested inside dense content, such as a composer or an input group, and never for chrome.
377
348
 
378
- Do not use viewport-scaled type (no `vw`/`vh` font sizing): use fixed token sizes stepped by breakpoint-specific rules when a headline must grow. Tracking is a display-tier exception and nothing else: the three display sizes tighten from -0.035em at 72px to -0.025em at 40px so large type reads optically tight, and every heading, body, caption, code and label tier stays at `0px`. The contract authors the result in px, because DTCG dimensions are px or rem.
349
+ ### The website column
379
350
 
380
- | Token | Size | Weight | Line Height | Tracking | Use |
381
- | --------------------------- | ---- | ------ | ----------- | ---------- | ---------------------------------------------------------------------- |
382
- | `{typography.display-xl}` | 72px | 600 | 74px | `-2.52px` | Hero headline at the widest breakpoint. |
383
- | `{typography.display-lg}` | 52px | 600 | 54px | `-1.5px` | Compact hero and major landing headlines. |
384
- | `{typography.display-md}` | 40px | 600 | 42px | `-1px` | Major section headings and the smallest display line. |
385
- | `{typography.heading-lg}` | 30px | 600 | 32px | `0px` | Standard section title. |
386
- | `{typography.heading-md}` | 24px | 600 | 32px | `0px` | Cell, artifact and panel titles; action bands and plan names. |
387
- | `{typography.heading-sm}` | 20px | 600 | 28px | `0px` | The smallest heading, inline above a group. |
388
- | `{typography.body-lg}` | 18px | 400 | 27px | `0px` | Hero and lead paragraphs. |
389
- | `{typography.body-md}` | 16px | 400 | 25px | `0px` | Default body copy, and large control labels at weight 500. |
390
- | `{typography.body-sm}` | 14px | 400 | 20px | `0px` | Secondary copy, nav, compact cell copy, control labels at weight 500. |
391
- | `{typography.caption}` | 12px | 400 | 16px | `0px` | Footer secondary lines, badge labels, compact chrome labels. |
392
- | `{typography.code}` | 13px | 400 | 20px | `0px` | Tool calls, snippets, traces. Geist Mono. |
393
- | `{typography.label-mono}` | 12px | 500 | 16px | `0px` | Mono eyebrows, identifiers and low-priority metadata. Geist Mono. |
351
+ A website page is a sequence of full-bleed sections, each centring its content in one column, `.nc-page`: `content-max` 1200 px wide with a side gutter of 20 px below 640 px, 40 px from 640 px and 64 px from 1024 px. From 1024 px the column's two edges are drawn as 1 px rails in `line` through every section, and each section closes with a 1 px line across the full width, so the page reads on one visible grid. Sections sit one step apart, 64 px below 1024 px and 96 px from it (`.nc-section-step`); a full-bleed picture that closes a section takes the step on its leading edge only (`.nc-section-step-lead`). Inside the column a section composes a heading, a split, a bento, a row of cards, a step rail or a logo row, and a composition may change its column count at 768 px. Nothing else sets a page width, and the page never scrolls sideways: horizontal overflow is clipped at the root, never on an inner surface.
394
352
 
395
- ## Layout
353
+ These values are the `register.website.layout` tokens; the breakpoints live in `foundation.css`, which collapses them into `--nc-gutter` and `--nc-section-step`.
396
354
 
397
- Layout is mathematical and single-owner in every register: one owner per visible line, every gap from the tokens below, never eyeballed.
355
+ ### Product shells
398
356
 
399
- The website's structure is three kinds of full-bleed section and nothing else: white for reading, black for a scene, and at most one flat, full-strength colour section per page for its single statement. Every section runs edge to edge and centres its content in the one column, `.nc-page`: a single content max-width with side gutters that grow from phone to desktop. Inside a section the content is a single column or a picture-and-text split, and between two sections there is one spacing step. Nothing else on the website sets a page width, and a section adds no frame, no row grid and no gutter texture.
357
+ The Portal and Nadia are product shells: persistent navigation on the left, the selected destination on the right, on the workspace ground ("Elevation & Depth"). They use the spacing ladder and the control geometry, not the website's column.
400
358
 
401
- The Portal and the Nadia desktop app build their own structure by their rulebooks (`## Registers`) and reach for the spacing ladder and the control geometry, not for the website's column.
359
+ - **Navigation names stable destinations.** An object opened from search or a link still shows its parent in a breadcrumb; local tabs directly below the top bar ask different questions about the same object, and they are always the package's Tabs ("Controls"); a return link remembers a visit and never replaces the parent. A scope selector keeps the current tab when the next object has it, and otherwise opens its overview.
360
+ - **Compact screens keep the same relationships.** Tabs scroll horizontally, primary content stacks before supporting facts, and no second sidebar appears.
402
361
 
403
- ### Spacing
362
+ ### Page compositions
404
363
 
405
- One ladder carries every gap, pad, and stack step. A register picks the rungs it needs; it never interpolates between them.
364
+ A product page is one of five compositions, each answering one question first:
406
365
 
407
- | Token | Value |
408
- | --- | --- |
409
- | `{spacing.xxs}` | 4 px |
410
- | `{spacing.xs}` | 8 px |
411
- | `{spacing.sm}` | 12 px |
412
- | `{spacing.md}` | 16 px |
413
- | `{spacing.lg}` | 24 px |
414
- | `{spacing.xl}` | 32 px |
415
- | `{spacing.2xl}` | 40 px |
416
- | `{spacing.3xl}` | 48 px |
417
- | `{spacing.4xl}` | 64 px |
418
- | `{spacing.5xl}` | 96 px |
419
- | `{spacing.6xl}` | 128 px |
420
-
421
- Control geometry is foundation, so a control has the same physical size and the same focus treatment in every register.
422
-
423
- | Token | Value | Owns |
366
+ | Composition | First answer | Structure |
424
367
  | --- | --- | --- |
425
- | `{spacing.guide}` | 1 px | Every ordinary seam |
426
- | `{spacing.icon-stroke}` | 1.5 px | The house icon line, on screen, at 16, 20 and 24 px |
427
- | `{spacing.icon-stroke-sm}` | 1.25 px | The same line at 12 px |
428
- | `{spacing.focus-outline-width}` | 2 px | The one keyboard focus outline |
429
- | `{spacing.focus-outline-offset}` | 2 px | That outline's offset |
430
- | `{spacing.control-height}` | 32 px | Minimum height of every default control |
431
- | `{spacing.control-padding-inline}` | 10 px | Inline padding of a control |
432
- | `{spacing.touch-target}` | 44 px | Minimum target on coarse or non-hover input |
433
-
434
- The website's column and its section rhythm belong to the website register and are authored in `register.website.layout`; the filter-chip sizes belong to the Portal register and are authored in `register.portal.density`. Both emit today's `--nc-*` names, and the values live in the one `json design-tokens` fence ("CSS Architecture & Token Pipeline"). Six tokens are the whole of the website's geometry; a page that needs a seventh is asking for a section kind the register does not have.
435
-
436
- | Token | Owns | Register |
437
- | --- | --- | --- |
438
- | `content-max` | 1200 px: the widest the reading column ever grows | Website |
439
- | `gutter-mobile` | 20 px: the side gutter beside that column below 600 px | Website |
440
- | `gutter-tablet` | 40 px: the same gutter from 600 px | Website |
441
- | `gutter-desktop` | 64 px: the same gutter from 960 px | Website |
442
- | `section-step-mobile` | 64 px: the one gap between two sections below 960 px | Website |
443
- | `section-step-desktop` | 96 px: the same gap from 960 px | Website |
444
- | `filter-chip-gap`, `filter-chip-height`, `filter-chip-icon` | The Portal's filter chip | Portal |
445
-
446
- ### Responsive Rules
368
+ | Overview | What needs attention? | Scoped facts, the exceptions that matter, links to their owners. Never a stack of previews of every section. |
369
+ | Collection | Which object? | Search and filters, compact identity rows, comparable facts, trailing actions. One row is one hover and one open target. |
370
+ | Object | What is this, where does it belong, what can I do? | The parent in the breadcrumb, local tabs, identity and grouped facts, the primary action beside its section. |
371
+ | Configuration | What can I change? | A centred reading column of titled sections, help beside each field, a local save and recovery. |
372
+ | Library | What reusable thing can I use? | Kind, text identity, description, source and revision; detail shows content and requirements. |
447
373
 
448
- The website has two breakpoints, 600 px and 960 px. They live in the application CSS layer, never in the generated token layer: `foundation.css` collapses `gutter-*` into `--nc-gutter` and `section-step-*` into `--nc-section-step` at those widths, so a section reads one name and resolves the value for the viewport it is on. No register adds a third breakpoint to this column.
449
-
450
- Page shells must prevent horizontal scrollbars when a full-bleed section uses viewport width. Clip horizontal overflow at the shell or root level; do not hide overflow on an inner surface to mask bad math.
451
-
452
- Compositions arrange approved primitives for a content role. They do not redefine the content max-width, the gutters, or the step between sections. If a composition needs structural geometry this section does not name, promote that geometry here first.
374
+ A section earns a card when it owns a decision, not for every label and value. A row links to its object; overflow menus hold occasional commands, and a frequent next action stays visible. A summary on an overview opens the same owner as its tab. Supporting detail collapses only behind a summary that keeps a count, state or consequence.
453
375
 
454
376
  ## Elevation & Depth
455
377
 
456
- Product UI uses one shared material ladder. The workspace is one continuous ground (`{colors.material-workspace}` / `{colors.dark-material-workspace}`); every solid object uses the same fill (`{colors.material-solid}` / `{colors.dark-material-solid}`). In dark mode, both are true black. The workspace remains legible because it carries the existing `dotted-field` texture and solid objects occlude it. Elevation never means progressively greyer cards. Physical distance changes only the falloff of the rung, and the four rungs are the four owned utilities: `nc-elevation-resting` for resting content, then `nc-elevation-raised`, `nc-elevation-floating`, and `nc-elevation-modal` when an object is physically raised, floating, or modal. Each rung is authored per mode, light and dark, and carries its own ring as the recipe's final layer, so an elevated root never adds a border, a second ring, a local ring tint, or an opacity override. In light mode every rung's ring is black at 5%, because the shadow carries the height. On true black the shadows vanish, so the dark ring carries it instead and steps with the rung: resting 12%, raised 16%, floating 22%, modal 28% white. The falloff layers are identical in both modes; only the ring differs. Product code chooses only the semantic role.
457
-
458
- Glass is a material variant, not an elevation step. Floating chrome may be glass: menus, popovers, selects, comboboxes, the command palette, and bars, the surfaces that stand over the room and show it through themselves. Content surfaces stay solid: cards, KPIs, tables, inspectors, and dialog, sheet, and drawer bodies, because glass floats but never carries content. Inside the window, glass composites only over our own content; a product whose window the operating system composites states its window material in its register. Glass keeps exactly one separator, the integrated edge ring of its elevation falloff, never ring plus shadow plus border stacked. Reduced-transparency mode replaces glass with the same solid material at the same elevation.
378
+ Product UI sits on one continuous workspace ground, `{colors.material-workspace}` (true black in dark), and solid objects rise above it at four heights: `nc-elevation-resting` for resting content, then `nc-elevation-raised`, `nc-elevation-floating` and `nc-elevation-modal` when an object is physically raised, floating or modal. Physical role chooses the height, never the importance of the content. Each recipe carries its own edge, so an elevated surface never adds a border, a ring or a second shadow.
459
379
 
460
- The material and elevation values are authored in the one `json design-tokens` fence under "CSS Architecture & Token Pipeline", "Authored token extension". `core.material` holds the four values this section applies and emits them as `--nc-material-*`: `scrim-opacity` becomes the theme layer's `--opacity-scrim`, and the three glass values are consumed by `@nadicodeai/ui/globals.css`. `core.elevation` holds the four rungs and emits `--nc-elevation-resting`, `-raised`, `-floating`, and `-modal`.
380
+ **In light, shadow carries height.** Every surface is white. Each recipe is a 1 px black edge at 8%, a tight contact shadow where the object meets the surface below, and an ambient shadow pushed down with negative spread so it stays under the object. The ambient shadow grows with the height, from 8 px of blur at resting to 128 px at modal.
461
381
 
462
- | Material role | Construction | Use |
463
- | ------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
464
- | Workspace | `dotted-field` on `{colors.material-workspace}` / `{colors.dark-material-workspace}`; no edge or shadow | The continuous work area behind product objects |
465
- | Resting | Solid fill; `nc-elevation-resting` with its own ring; `{rounded.md}` (6 px) | KPI, card, chart card, table, setting, and persistent content surfaces |
466
- | Raised | Same solid fill; `nc-elevation-raised`; `{rounded.md}` (6 px) | Active work, a selected object, or an attached inspector above resting siblings |
467
- | Floating | Same solid fill; `nc-elevation-floating`; `{rounded.md}` (6 px) | Menus, popovers, dropdowns, selects, anchored surfaces, tooltips, and toasts |
468
- | Modal | Same solid fill; `nc-elevation-modal`; `{rounded.md}` (6 px) | Dialog, sheet, drawer, and takeover |
469
- | Glass control | Translucent solid fill at 76%, 28 px backdrop blur, and the rung for its physical level with that rung's ring | Floating chrome above our own content: menus, popovers, selects, comboboxes, the command palette, and bars; never a content container |
382
+ **In dark, tone carries height.** Shadows cannot show on true black, so each height's surface is one fixed step lighter than the page: resting `{colors.dark-material-resting}` #0c0c0c, raised `{colors.dark-material-raised}` #121212, floating `{colors.dark-material-floating}` #181818, modal `{colors.dark-material-modal}` #1c1c1c. The steps are equal-channel greys with nothing between or above them, so dark surfaces still read as black. `{colors.dark-ink}` keeps at least 14.5:1 and `{colors.dark-muted}` at least 5.2:1 on the lightest step. Each dark recipe adds an inset white hairline and a 1 px top highlight that strengthen with the height, and an outer black edge that separates a surface from a lighter one below it. A component names only its height and its role (`bg-card`, `bg-popover`); `@nadicodeai/ui/globals.css` sets those roles to the height's tone on the element.
470
383
 
471
- `Card` is the fundamental product container. Its content does not create a second material family: KPIs, Agent cards, settings, and ordinary grouped content use the resting role unless the object is physically raised. Interaction variants may change state or emphasis without changing the material role.
472
-
473
- Absence follows the container that owns it. A page-level or region-level empty state is an opaque solid region with a dashed boundary. An empty state inside a Card is transparent and unframed so it does not draw a second container inside the first.
384
+ | Surface | Fill | Height | Use |
385
+ | --- | --- | --- | --- |
386
+ | Workspace | `{colors.material-workspace}` | None | The work area behind product objects. |
387
+ | Resting | `{colors.material-resting}` | `nc-elevation-resting` | Cards, KPIs, chart cards, tables, settings. The default. |
388
+ | Raised | `{colors.material-raised}` | `nc-elevation-raised` | Active or selected work, an attached inspector. |
389
+ | Floating | `{colors.material-floating}` | `nc-elevation-floating` | Menus, popovers, selects, tooltips, toasts. |
390
+ | Modal | `{colors.material-modal}` | `nc-elevation-modal` | Dialogs, sheets, drawers. |
391
+ | Outlined | `{colors.material-solid}` | None; one `line` border | A flat panel: fact cells inside a record, the website's explanatory cards. |
392
+ | Plain | `{colors.material-solid}` | None | A structural card with no edge of its own. |
474
393
 
475
- Resting form controls are not elevated material planes. They keep the 1 px `{colors.input}` / `{colors.dark-input}` boundary, `{rounded.md}` (6 px), and no shadow. Their inline padding is `{spacing.control-padding-inline}`, textareas use `{spacing.xs}` block padding, and every default control is at least `{spacing.control-height}` high.
394
+ **Glass** is a material, not a height: the floating fill at 60% over a 20 px backdrop blur at 180% saturation, so colour behind it comes through saturated rather than grey. Its edge is the floating recipe, whose inset top highlight and inner hairline show once the fill turns translucent, and a 5% monochrome grain keeps the blurred colour from banding. Glass may go where something behind it moves or carries colour:
476
395
 
477
- Modal overlays use `{colors.scrim}` / `{colors.dark-scrim}` at the generated `scrim` opacity with no blur. Focus is not elevation: controls use a full-opacity `{spacing.focus-outline-width}` `{colors.focus-ring}` / `{colors.dark-focus-ring}` outline at `{spacing.focus-outline-offset}` offset, never a box-shadow ring. Avoid nested decorative borders: one container owns containment, while child groups use spacing, headings, or dividers only when those dividers clarify structure.
396
+ - floating chrome over our own content: popovers, selects, comboboxes, context menus, the menubar, navigation popups, the command palette and control bars;
397
+ - the website header as the page scrolls under it;
398
+ - a small floating card over a colour ground or moving Agents.
478
399
 
479
- On coarse or non-hover input capability, every interactive target is at least 44 × 44 px at every viewport width. A compact visual mark such as a checkbox or radio stays compact while its label or hit area supplies the target size. Fine, hover-capable pointers may use the compact control scale.
400
+ Content stays solid: cards, KPIs, tables, chat, forms, and dialog, sheet and drawer bodies. A dropdown menu stays solid because it opens over dense rows its labels would compete with. Glass over a plain white or black ground shows nothing, so it never goes there. Reduced transparency replaces glass with the same solid surface at the same height.
480
401
 
481
- ## Shapes
402
+ **Product views on the website** are the product itself, built from the real components with example data that is labelled as such; never a screenshot, a device mockup or an invented interface. A product window takes the raised height on its ground; a card that overlaps it floats; its chrome may be glass over a colour or black ground; in a black section it renders in dark mode.
482
403
 
483
- One radius rounds every surface. The reference is the search field: `{rounded.md}` (6 px). A surface never earns a larger corner by sitting higher on the elevation ladder, and no register may reintroduce a second surface radius.
404
+ Absence follows its container. A page or region with nothing in it is an opaque region with a dashed boundary; an empty state inside a card is unframed, because the card already owns the edge. A persistent notice sits in the section it concerns, action feedback is a toast, and field validation stays with its field.
484
405
 
485
- - Use `{rounded.md}` (6 px) for every surface and every control: resting, raised, outlined, floating, and modal surfaces; cards, KPI cards, chart cards, tables, menus, selects, popovers, dropdowns, dialogs, sheets, drawers, takeovers, tooltips, toasts, inspectors; and buttons, inputs, chips, status tags, and small artifacts.
486
- - Use `{rounded.none}` for full-bleed sections, structural cells, and structural modules.
487
- - Use `{rounded.full}` only for intrinsically circular identity/avatar chrome and compact control marks whose geometry is inherently round or pill-shaped. It never turns a content surface, button, badge, or navigation item into a pill by default.
406
+ Modal overlays use the scrim with no blur. Focus is not elevation: it is always the focus outline.
488
407
 
489
- The radius scale itself is unchanged: `{rounded.xs}` through `{rounded.3xl}` stay exported for inner marks and for geometry a component owns, such as a progress track or a bar end. Only the rung that surfaces take is fixed.
408
+ ## Shapes
490
409
 
491
- Structural cells on the website are not cards and remain square, because the section around them supplies the visual system. Product cards use the resting material role. A details composition may divide its header and bound individual facts with the outlined role; each edge has one owner and fact cells add no elevation. Grouping does not justify another ring around the same container or a stronger shadow.
410
+ Every surface and control takes `{rounded.md}` (6 px): cards, tables, menus, popovers, dialogs, sheets, tooltips, toasts, buttons, inputs, chips and badges. A surface never earns a larger corner by sitting higher. Full-bleed sections take `{rounded.none}`. `{rounded.full}` is for things that are circles by nature: avatars, switch and radio marks, slider thumbs, status dots. The other radii are for inner marks and geometry a component owns, such as a progress track.
492
411
 
493
412
  ## Motion
494
413
 
495
- **One motion law.** Only Agents move on their own. The interface answers a person's action, briefly, and stops: motion follows the action, stays under 300 ms, grows from the element that was pressed, and settles. While a person waits for something they asked for, one waiting line moves and nothing else.
496
-
497
- Only Agents move on their own. An idle orb holds still, and under reduced
498
- motion a thinking or working orb breathes in opacity, 1.6 s ease-in-out from
499
- full ink to 0.45, so busy still reads as busy while every other orb is still
500
- ([agent orbs](../ui/docs/agent-orb.md)). Opening an Agent in the Portal is the
501
- one view transition: the orb travels from the list row into the Agent's page
502
- on `transition` with `in-out-strong`, over a root crossfade on `close`, and
503
- reduced motion navigates instantly; the Portal's `portal.css` owns those rules
504
- because that one navigation is the only place the product moves a page.
505
-
506
- **One default transition in every product**: `confirm`, 180 ms, `out-strong`. It is set once, on `--default-transition-duration` and `--default-transition-timing-function` in `src/css/motion.css`, so a bounded Tailwind transition utility inherits the cadence and no product declares a default of its own.
507
-
508
- **One reduced-motion policy**, stated once in `motion.css` and repeated nowhere: under `prefers-reduced-motion: reduce`, movement stops and paint continues. Transforms and keyframed movement go to their end state; opacity and colour transitions are kept, because a fade or a colour change is the state change itself rather than decoration around it, and an Agent that is thinking or working still has to read as busy. A component never answers reduced motion by turning every transition off.
414
+ Only Agents move on their own. The interface answers a person's action, briefly, and stops: motion follows the action, grows from the element that was pressed, and settles, within 300 ms; sheets and drawers that follow the pointer take their own durations. Keyboard actions and anything used a hundred times a day are instant. While a person waits for something they asked for, one waiting line moves and nothing else ("System activity"). Nothing on the website moves but Agents and the product at work: no parallax, scroll reveal, marquee or shimmer.
509
415
 
510
- The durations and easings live in `core.motion` of the authored token extension ("CSS Architecture & Token Pipeline"). The build emits every duration as `--nc-duration-*` and every easing as `--nc-ease-*`. `src/css/motion.css` owns the keyframes, the `nc-anim-*` and `nc-transition-*` classes, and the reduced-motion law, and references those names; it declares no duration, easing, or alias of its own. A register decides where motion appears; it never picks a duration outside this list, and `tests/guards/motion-durations.test.ts` fails on a time literal anywhere in the motion layer.
416
+ The default transition in every product is `confirm`, 180 ms, `out-strong`. Under reduced motion, movement stops and paint continues: transforms and keyframed movement go to their end state, and opacity and colour transitions stay, because a fade or a colour change is the state change itself. A component never answers reduced motion by turning every transition off.
511
417
 
512
418
  | Token | Use |
513
419
  | --- | --- |
514
- | `stagger` 60 ms | The step between siblings in one arrival |
515
- | `control` 100 ms | Hover and press on a control in a dense register |
516
- | `close` 140 ms / `open` 220 ms | Floating chrome and overlays leaving and arriving |
517
- | `confirm` 180 ms | Confirmation feedback, instant state flips, and the default transition |
518
- | `msg-in` 240 ms | A transcript message arriving |
519
- | `transition` 240 ms | Property transitions on a working surface, and the Agent view transition |
520
- | `drawer-swipe` 400 ms / `drawer` 450 ms | Sheets and drawers, pointer-driven and programmatic |
521
- | `wait` 1.6 s | The one waiting line, and nothing else |
522
-
523
- The easings are `emphasized`, `out-strong`, `in-out-strong`, and `drawer`. Every remaining duration has a job these rules allow, which is why `spin`, `typing` and `shimmer` retired with the spinner, the typewriter line and the three shimmer systems: a busy spinner is not an Agent, and ambient decoration is not an answer to anything a person did.
524
-
525
- ## Registers
526
-
527
- One foundation, three products. The foundation is every `core` value in this contract: owned by no single product, available to every product, and foundation whether one product renders it or three. A register is one product's way of applying it: direction, density, the rungs of the ladder it uses and where, the tiers of the type scale it speaks, its dark-mode stance, its structural grammar, its motion budget.
528
-
529
- A register adds rules. It never redefines a foundation value. A value one register's physics need and the foundation lacks is authored here under `register.<name>`; a register never consumes another register's token; a value two registers share moves to `core`; no product mints a value of its own.
420
+ | `control` 100 ms | Hover and press on a control in a dense surface |
421
+ | `close` 140 ms, `open` 220 ms | Floating chrome and overlays leaving and arriving |
422
+ | `confirm` 180 ms | Confirmation, instant state flips, the default transition |
423
+ | `msg-in` 240 ms | A message arriving in a conversation |
424
+ | `transition` 240 ms | Property transitions on a working surface, and the Portal's one page transition, which carries an Agent's orb from a list row into its page |
425
+ | `drawer-swipe` 400 ms, `drawer` 450 ms | Sheets and drawers, pointer-driven and programmatic |
426
+ | `wait` 1.6 s | The waiting line, and nothing else |
530
427
 
531
- | Register | Rulebook | Its own token families | Dark mode |
532
- | --- | --- | --- | --- |
533
- | Website | `apps/website/docs/design-doctrine.md` over `## Layout` here | `register.website.layout`: `content-max`, `gutter-*`, `section-step-*`. It speaks `display-xl`, `display-lg`, and `title-lg` | Light only |
534
- | Portal | `apps/portal/docs/information-architecture.md` for direction; `apps/portal/docs/design-doctrine.md` for implementation | `register.portal.density`: `filter-chip-gap`, `filter-chip-height`, `filter-chip-icon` | System, light, dark, on the `.dark` class |
535
- | Nadia desktop app | `apps/nadia/docs/design-doctrine.md` | `register.nadia.*`: colour-role and radius aliases; six deprecated elevation recipes and the legacy overlay border remain as compatibility data. New surfaces consume the foundation's material roles | Follows the operating system, on the `.dark` class |
536
-
537
- The website is built from three kinds of full-bleed section and nothing else: white for reading, black for a scene with a marble figure and the live orb, and at most one flat, full-strength colour section per page for its single statement, in the hue of that page's picture. One content max-width with growing side gutters centres the reading content, a section holds a single column or a picture-and-text split, and one spacing step separates two sections (`## Layout`).
538
-
539
- The website's depth is flat and architectural: the three section grounds, contrast, large Geist headlines and the approved pictures make hierarchy. It has no page frame, no row grid, no diagonal fill and no dotted field; the dotted field is the Portal's work surface. It uses no rung of the material ladder and no floating card stack as page structure, and no negative letter spacing outside the two display tiers.
540
-
541
- The Portal and the desktop are product shells on the material ladder above, each by its rulebook. The desktop's window material is the operating system's where the OS composites the window; inside the window the foundation's glass law holds.
428
+ The easings are `emphasized`, `out-strong`, `in-out-strong` and `drawer`; the waiting line alone travels at constant speed (`linear`). A CSS transition or animation takes its duration and easing from these tokens; the design system's own stylesheets are held to it by a guard.
542
429
 
543
430
  ## Components
544
431
 
545
- This section is the semantic catalog for framework-agnostic components. Exact reusable values stay in the token maps above; CSS owns component composition. The section grammar and the content column live in `## Layout`, while React components and reusable sections live in `@nadicodeai/ui`.
432
+ Every product builds from `@nadicodeai/ui`: stock shadcn components on Base UI, the chart kit, the assistant-ui conversation interface, and the compositions below. A component styles itself only through the roles and tokens in this file; it carries no raw colour, no `dark:` utility and no theme of its own. A page arranges and sizes a component and never restyles it: a new appearance is a typed variant or slot added to the component, and a missing composition is added to the package once two products need it. Visible controls always come from the package.
546
433
 
547
- ### Structural Surfaces
434
+ ### Controls
548
435
 
549
- One surface carries structure rather than content. It is square (`{rounded.none}`), takes no rung of the elevation ladder, and draws no border of its own.
436
+ - **Buttons.** Every button is `{spacing.control-height}` high with a `{typography.body-sm}` label at 500 and `{rounded.md}` corners ("Layout" says where the smaller sizes may go). `default` is the action (ink, white label; `{colors.dark-action}` with a `{colors.dark-action-foreground}` label in dark); `outline` and `secondary` are the quieter steps, edged with the `{colors.line}` hairline; `ghost` has no edge; `destructive` is the error pair; `link` is inline. On a colour or black ground a control takes a variant whose label clears AA there: `brandSolid` (white, ink label) or `brandGhost` (white label, no fill) on cobalto, rosso and black, and the default ink button on verde, arancio and giallo.
437
+ - **Tabs** switch between one object's views. They are pills in one row directly under the top bar: `{spacing.control-height}` high, a `{typography.body-sm}` label at 500, `{spacing.sm}` inline padding and `{rounded.md}` corners. The current tab is filled with the `selection` role (`{colors.navigation-selection-bg}` with `{colors.navigation-selection-fg}`, `{colors.dark-navigation-selection-bg}` in dark); the others have no fill and `{colors.muted}` text, which turns ink on hover. A tab that opens a URL is a link in `TabsNav` with the same look; the `line` variant is for tabs inside a panel.
438
+ - **Fields** keep the `{colors.input}` edge, `{rounded.md}`, no shadow and at least `{spacing.control-height}`. Help sits beside its field, and validation stays with it.
439
+ - **Focus** is drawn once, by the base layer, on every `:focus-visible` element. A component never repaints it; it may move it to a proxy that stands for the focused element, as a field group does for its input.
440
+ - Enabled buttons show a pointer; a disabled control advertises nothing. On coarse or non-hover input every target is at least `{spacing.touch-target}`, while a checkbox or radio mark stays compact and its label supplies the target.
550
441
 
551
- **`dotted-field`** — the Portal's work surface: the dotted ground behind conversations, outputs, and diagrams, on the material ladder's workspace role ("Elevation & Depth"). It indicates a work surface, never decoration, and the website does not use it.
442
+ ### Cards and sections
552
443
 
553
- ### Interface Atoms
444
+ `Card` is the product's container, at the resting height by default ("Elevation & Depth"). A row of cards is a `CardRow`: siblings share one height, and the row collapses to one column below its breakpoint. A card or settings section is composed around a real task, collection or form; its header names the content and may explain scope or consequence, and it is never a card holding only an introduction. A settings section owns its heading, a one-line description of what the setting changes, its fields and its local save. Its footer is a strip one tone below the card body, the `recessed` role (`{colors.canvas-soft}`, `{colors.dark-canvas}` in dark), behind a `{colors.line}` seam: an optional hint on the left, such as the plan a feature needs or a link to learn more, and the section's action on the right; a facts section shows labelled facts in two equal columns, stacked on compact screens, with every label kept in place when a value is missing. A KPI card is one figure: label, value, the change as a badge in the top right, and a footer with the comparison.
554
445
 
555
- Interface CSS components provide small metadata, label, and link chrome. They inherit nadicode tokens and stay subordinate to the layout; they must not create independent layout systems or repair structural seams. The generic interactive controls (navigation, buttons, inputs, footer chrome, the marketing card/code/pricing/proof surfaces, the standard sections) ship as React shadcn components from `@nadicodeai/ui`; this contract keeps only the brand/display CSS components and primitives that belong in the framework-agnostic package.
446
+ ### Menus and commands
556
447
 
557
- Keyboard focus has one owner: the kit's foundation layer draws the full-opacity `{spacing.focus-outline-width}` `{colors.focus-ring}` / `{colors.dark-focus-ring}` outline at `{spacing.focus-outline-offset}` offset on every `:focus-visible` element. A component never repaints focus with its own ring, hue, or width, and never removes the outline without a contrast-safe equivalent.
448
+ A record's actions sit behind one three-dot trigger, a `{spacing.control-height}` ghost icon button that opens an end-aligned menu. In a collection every row carries that one trigger for its occasional commands, renaming and disconnecting included; a row shows a visible button only for its one frequent next action. Menu rows put the label first and a decorative icon at the trailing edge. A collection's filters are chips, an Add filter button and one outline chip per active filter with its own remove control, never a dropdown above the table. The scope a page works in, such as the company or the account it reports on, is a scope switcher: a ghost trigger reading the current name and ⇅ that opens a popover with a Find search, the choices with the current one checked, and a Create row at the bottom when the scope can be created there. The account menu names who is signed in, carries one gear to the account section, and holds the Theme and Language choices as compact inline rows. The command palette is glass and fills its dialog.
558
449
 
559
- **`label-mono`** — mono eyebrow text, identifiers and code, and low-priority metadata. Use it for opaque identifiers and codes, not computer or Agent display names. Card, panel, and section titles use sentence-case sans.
450
+ ### Tables and missing values
560
451
 
561
- **`badge-secondary`** — small rounded metadata badge for "New", "Beta", "Live", or similar labels.
452
+ A table is native table structure at one density: 36 px header rows and 40 px body rows. A body row takes the full `accent` fill on hover, on keyboard focus within it and while its menu is open; a selected row takes `muted`; headers and footers never highlight. A collection fits its container without sideways scrolling: identity and actions keep their columns, and supporting facts move into a detail row inside the same record before the table gets cramped.
562
453
 
563
- **`filter-chip`** — removable selected-filter label. Neutral chrome only: `{colors.line}` border, muted/background fill, foreground text, compact radius, and an inline remove icon with a visible `:focus-visible` ring. It names the active facet/value pair and clears that facet; it does not carry lifecycle, workflow, product-tier, or feature-state color.
454
+ An unknown or not-applicable value in a table, a comparison list or a set of facts is a muted `—` with screen-reader text, in the same slot as a present value; zero stays zero. An absent report is not `None`: `None` is only for a complete report that found nothing. A stale value keeps its last reading with a short `Stale` qualifier. Loading, failure, missing permission and actionable setup each keep their own visible state and never turn into a dash. Essential reasons and actions never live only in a hover tooltip.
564
455
 
565
- **`link-inline`** — inline body link. Uses `{colors.link}` and normal body rhythm.
456
+ ### Status
566
457
 
567
- ### Content Surfaces
458
+ A status is one Badge: a mark and a word in one of five tones, so it reads without colour.
568
459
 
569
- Content surfaces display specific content types inside a section. They are valid when the content exists; they do not replace the section, the content column, or the seam rules.
460
+ | Tone | Means | Pair |
461
+ | --- | --- | --- |
462
+ | Gray | Default, and every terminal state that needs no action | `{colors.canvas-soft-2}` / `{colors.body}` |
463
+ | Green | Healthy, complete | `{colors.link-bg-soft}` / `{colors.link-deep}` |
464
+ | Red | Unusable, failed | `{colors.error-soft}` / `{colors.error-deep}` |
465
+ | Amber | Waiting on a person's decision | `{colors.warning-soft}` / `{colors.warning-deep}` |
466
+ | Blue | A step in motion | `{colors.info-soft}` / `{colors.info-deep}` |
570
467
 
571
- **`image`** — editorial media mount, the single home for art-directed imagery inside a section. A bounded media fill (img, picture, video, or inline SVG) with an optional mono tag chip overlaid on the media and an optional body-tone caption bar below it, both typed by the generated caption utilities applied in markup. Square-cornered (`{rounded.none}`): a mounted print, not a floating card — the parent frame supplies the visual system. With `data-frame="true"` it draws exactly one hairline (its own `{colors.line}` border), so it sits inside a borderless cell or a visual slot without repairing or stacking onto any grid seam. The aspect ratio is the per-instance `--nc-image-ratio` knob (default 4 / 3) — set the prop on the element, never re-derive geometry inline. It has one form: there is no divider-strip variant. Use `image` for every illustrated image and every photographic surface. Conversation artifacts render through assistant-ui rather than a design-system CSS family.
468
+ Each app keeps its whole status-to-tone map in one module; nothing else chooses a status colour. A status never moves.
572
469
 
573
- ### Agentic Work Surfaces
470
+ ### System activity
574
471
 
575
- Agentic work surfaces are the nadicode-specific display layer outside the conversation renderer. They show concrete work output or workflow state such as memory, state, run progress, identity, and handoffs. Conversation messages, tools, artifacts, approvals, and pending indicators belong to assistant-ui.
472
+ Who does the work decides how it looks. An Agent's work is that Agent's orb in its state and a sentence saying what it does. The system's own work never takes an orb:
576
473
 
577
- Agent identity is decorative product identity, not a work artifact or a status signal: the Agent orbs, owned by [`packages/ui/docs/agent-orb.md`](../ui/docs/agent-orb.md).
474
+ | The wait | What shows |
475
+ | --- | --- |
476
+ | Under about a second | The control's own label changes to its pending word; its place, size and icon stay. |
477
+ | Past a second | One 2 px waiting line of ink beside the work, a 32% segment crossing it on `wait`, and nothing else moves. |
478
+ | Measurable | A progress line with a written count. Never an invented percentage. |
479
+ | A page load | Still skeleton placeholders in the shape of the content, never pulsing and never paired with a waiting line. |
578
480
 
579
- **`artifact-surface`** — inspectable output area outside a conversation. Use for proposal previews, reports, plans, spreadsheets, schedules, run logs, and workflow maps.
481
+ One wait gets one indicator. A request waiting for a person is not a wait: queued, paused, blocked, failed and done take the status or feedback treatment. A wait is ink; info blue never marks one. Under reduced motion the waiting line breathes in opacity instead of travelling.
580
482
 
581
- **`usage-meter`** — token, cost, latency, or run-count display.
483
+ ### Agent orbs
582
484
 
583
- **`run-timeline`** — vertical step thread for an agent run, handoff, or workflow execution.
485
+ An Agent is its orb, in every product; there is no other Agent picture. An orb is one of seven forms, `ribbon`, `lattice`, `orbit`, `globe`, `network`, `braid` and `morph`, in one colour a person chose: cobalto, verde or arancio, or none, which draws it in the text colour. Giallo is not offered because it fails 3:1 on white; rosso is not offered because it is the error red. Nadia alone is her striped ribbon, azzurro, verde, the text colour and rosso along its whole length, the same at every size and in every state.
584
486
 
585
- **`memory-item`** — remembered-fact row for company brain, context, or persistent workflow knowledge.
487
+ - **Stored look.** An Agent's form is stored as `orb:<form>` and its colour as the identity hex or nothing; a template declares its starting look the same way. The root Agent always draws as Nadia. An unknown form draws `orbit`, an unknown colour the text colour, and a stored rosso draws arancio. The look is a person's choice and is never derived from a name, id or role.
488
+ - **Sizes** are 20, 24, 32, 40, 64, 96 and 160 px. On a photograph an orb takes the stronger `over-picture` weight; everywhere else it is `plain`.
489
+ - **States.** Idle holds one still pose. Working and thinking move, waiting holds, blocked makes one interrupted effort and stops, and done celebrates once and rests. A surface that only knows a machine checked in draws idle. No state draws a circle around the orb, and a label names the state wherever the exact state matters.
490
+ - **Status and ambient.** An orb that stands for an Agent shows its real state. An orb in decorative artwork, such as a sign-in picture, takes ambient motion: its form moves at its own pace with no state pose or cue, and it claims no activity.
491
+ - **Reduced motion.** Movement stops; a thinking or working orb breathes between full ink and 0.45 opacity so busy still reads as busy, and an ambient orb holds still.
586
492
 
587
- **`handoff-banner`** — agent-to-agent or human-to-agent handoff surface. It explains the handoff reason and destination. It carries a 3 px colored `border-left` consuming the matching `{colors.link}` token and a leading direction marker tinted with the same token; the accent edge and arrow carry the handoff direction.
493
+ Text sits beside an orb, never on it.
588
494
 
589
- ## Status Vocabulary
495
+ ### Agent card
590
496
 
591
- Status is the one thing every surface in the product says: an agent is active, a
592
- key is revoked, an invitation is waiting, a run is blocked. It is said one way:
593
- one badge carrying an icon and a word, in one of five semantic tones that only
594
- reinforce both, so a status is legible without colour and no surface invents a
595
- second grammar.
497
+ An Agent is shown as one card shape wherever it appears as an object, in the Portal's Library and Nadia's gallery alike: the orb in a 96 px box on the left; the name, the job and a status row beside it; and a footer with facts and one action when either exists. On an Agent's own page the name is the page heading.
596
498
 
597
- Each tone pairs a soft ground with its deep label from an existing semantic
598
- pair: gray on the neutral spine for the default and every terminal, no-action
599
- state; green on the link pair for healthy and complete; red on the error pair
600
- for unusable and failed; amber on the warning pair for waiting on a decision;
601
- blue on the info pair for a step in motion. One mark per tone, so the shape
602
- repeats the tone rather than adding a second axis.
499
+ ### Identity marks
603
500
 
604
- The carrier is the React `Badge` in `@nadicodeai/ui`, contracted in
605
- [`packages/ui/docs/contract.md`](../ui/docs/contract.md). This package ships no
606
- status class; a static surface shows a status as ordinary text. Which statuses
607
- exist and which tone each takes belong to the consuming app, which keeps its
608
- whole map in one module.
501
+ A recognised app, platform or model maker is drawn with its genuine mark beside its readable name, in a tile that owns the box; an unknown one keeps its name and a text monogram, never a guessed logo. A mark identifies and never replaces the label, and its colour identifies the maker, never a connection's health. Marks are used nominatively, to name a platform, an integration or a provider, never next to wording that implies endorsement. A brand mark never stands in for a UI icon, and a UI icon never stands in for a brand. Skills are text, without icons. In a chart, a model is named by its label, and marks stay out of legends.
609
502
 
610
- ## System activity
503
+ ### Product views
611
504
 
612
- Who does the work decides how it looks. An Agent's work shows that Agent's orb
613
- in its state and a sentence saying what it is doing; Nadia connecting is
614
- Nadia's ribbon, working. Everything below is the system's own work, and an orb
615
- means an Agent, so no system wait takes one.
505
+ A product view on the website is a live, inert fragment of the product built from its real components, captioned, with example data marked as example ("Elevation & Depth"). It takes its product's register: the Nadia window shows her Agents on the left and the selected conversation on the right; the Portal shows its sidebar and the page. A view is cropped to what it shows, never scaled below legibility, and never framed in a device.
616
506
 
617
- System work under about a second is a still pending label on the control: its
618
- own word changes, it holds its place and its icon, and it carries `disabled`
619
- and `aria-busy`. Past a second, one 2 px waiting line of ink moves beside the
620
- work, a 32% segment crossing its track linearly on `wait`; nothing else on the
621
- screen moves while it does. Measurable work is a progress line with a written
622
- count. A page load shows still placeholders in the shape of the content.
623
- Reduced motion drops the travel and breathes the whole line in opacity.
507
+ ### CSS components
624
508
 
625
- The colour of a wait is ink: the cobalto info role belonged to the retired
626
- activity particles and marks no system wait now. Success, warning and error
627
- keep their meanings; waiting for a person or a failed operation uses the
628
- feedback treatment, not an activity one.
509
+ These four ship as framework-neutral CSS; `@nadicodeai/ui` wraps the last three for React.
629
510
 
630
- The renderers are `PendingLabel`, `WaitingLine`, `Progress` and `Skeleton` in
631
- `@nadicodeai/ui`. The one-second delay, the geometry, the ink and the motion
632
- are owned by [`packages/ui/docs/activity.md`](../ui/docs/activity.md); the
633
- motion vocabulary owns `.nc-anim-wait` and its reduced-motion law; a consuming
634
- app chooses the wait and supplies its readable words.
511
+ **`dotted-field`** — the ground of a canvas where a person or an Agent places and arranges objects: a diagram of an Agent's work, a board of its outputs. The dots say that things here can be moved, so the field never sits behind a dashboard, a list, settings or a website section, and it never stands in for elevation.
635
512
 
636
- ## Do's and Don'ts
513
+ **`usage-meter`** — consumption shown against its allowance: tokens, cost, latency or runs, with the figure printed.
637
514
 
638
- This section states the correct form; a complete positive definition is the whole rule.
515
+ **`run-timeline`** — the vertical thread of an Agent's run, a handoff or a workflow, one step per row.
639
516
 
640
- - A website page is a sequence of full-bleed sections in three kinds only: white for reading, black for a scene, and at most one flat colour section for one statement. Each one centres its content in the same column, capped at `content-max` with the gutter its viewport gives it, and the sections sit one spacing step apart.
641
- - Every visible line has one owner: ordinary seams are `{spacing.guide}` at `{colors.line}`, tuned only at the token contract; required control boundaries are `{colors.input}` / `{colors.dark-input}`; named collection controls may use the optional-perimeter rule above; keyboard focus is the dedicated focus-ring roles.
642
- - Page structure is architectural: hairlines, luminance, spacing, and the section grammar above. One material role per product container; structural cells carry no elevation; the four `nc-elevation-*` rungs are the complete shadow vocabulary; modal scrims stay unblurred.
643
- - Typography uses fixed token sizes stepped by breakpoint; a component's height comes from its content and the spacing ladder, never from a fixed height of its own.
644
- - Agent orbs identify an Agent; conversations, artifacts, review states, and work output belong to assistant-ui and the agentic work surfaces. Text sits on the canvas beside an identity mark, never on it.
645
- - Components are named as nadicode-owned primitives, and this file carries contract only: tokens, semantics, and application rules — component prop APIs, state matrices, screenshots, and page-specific material live with their owners.
646
- - Each register keeps its own structure and chrome: the three section kinds on the website, the material ladder in the Portal and the desktop, the desktop window following the operating system.
517
+ **`memory-item`** — one fact an Agent remembers about the company or its work.
647
518
 
648
519
  ## Iconography
649
520
 
650
- Which icon set a product draws from is a register decision; the render
651
- discipline below, the sizes, the stroke, and the decorative-versus-named rule,
652
- is foundation and holds in every register.
653
-
654
- The website, the Portal, and `@nadicodeai/ui` draw from Lucide (ISC). Lucide's
655
- shapes are never edited; the nadicode house render changes only how they are
656
- drawn: a line of `{spacing.icon-stroke}` on screen at every size, held there by
657
- `vector-effect: non-scaling-stroke` on each shape, round caps, round joins, no
658
- fill, and `currentColor` inheritance. A 12 px icon takes
659
- `{spacing.icon-stroke-sm}`. The width is measured on screen, not on Lucide's
660
- 24-unit grid: a grid-unit stroke thins as the icon shrinks, so a 16 px icon
661
- drew a lighter line than the 14 px text beside it. The package bakes the
662
- complete pinned `lucide-static` set through that house render; a surface draws
663
- a small, disciplined vocabulary from it rather than reaching across the whole
664
- set.
665
-
666
- Sizes are sm / md / nav / lg = 12 / 16 / 20 / 24 px, md the default. Persistent
667
- navigation, the Portal sidebar among it, uses nav so its entries read before
668
- the content beside them; a collapsed icon rail keeps md.
669
-
670
- Three icons are the house's own, drawn on Lucide's 24-unit grid and rendered
671
- the same way: `arch`, the company and its doors; `approvals`, the arch with a
672
- check, for work waiting on a person; and `agent`, the orb seen face-on with the
673
- Agent as the dot on its path, the dot of the wordmark. `@nadicodeai/ui`
674
- exports them from `components/house-icons` as Lucide components, and the
675
- static set carries them beside Lucide's. They name those three concepts
676
- wherever an icon does, and no Lucide glyph stands in for them.
677
-
678
- Static and React icon sources stay version-aligned so a glyph has one
679
- silhouette everywhere. Text-paired icons are decorative; icon-only controls
680
- carry an accessible name.
681
-
682
- ## Brand media authority
683
-
684
- The company is **nadicode**, lowercase ([ADR](../../docs/adr/2026-09-23-the-nadicode-design-system.md)). Its logo has three approved forms:
521
+ Every product draws Lucide's shapes, unedited, in the house render: a 1.5 px line (`{spacing.icon-stroke}`) measured on screen at every size and 1.25 px (`{spacing.icon-stroke-sm}`) at 12 px, round caps and joins, no fill, and the text colour. Sizes are 12, 16, 20 and 24 px, 16 by default, and inside a stock component the registry's own icon size stands; persistent navigation uses 20 so its entries read before the content beside them, and a collapsed rail keeps 16. A surface draws a small, disciplined set, never the whole library.
685
522
 
686
- 1. **Wordmark.** nadicode drawn in one geometric line, with one dot over the i. The dot is the only colour in the logo: where an Agent sits. Use it wherever the company must be named.
687
- 2. **Mark.** The plain n cut from the wordmark, never with a dot or any other shape added. It stands alone where the name does not fit: compact navigation, headers inside the product.
688
- 3. **Icon.** The white n on a `{colors.identity-cobalto}` tile with rounded corners: app icons, favicons and avatars. The tile is the one place the brand colour fills a field.
523
+ Three icons are the house's own, drawn on Lucide's grid in the same render: `arch` for the company and its doors, `approvals`, the arch with a check, for work waiting on a person, and `agent`, the orb seen face-on with the Agent as the dot on its path. They name those three things wherever an icon does, and no Lucide glyph stands in for them. An icon beside text is decorative; an icon-only control carries an accessible name.
689
524
 
690
- Other companies' marks are not nadicode company logos and are not governed by anything in this section. They are generated identity images owned by `@nadicodeai/ui/components/brand-icons`; [`packages/ui/docs/contract.md`](../ui/docs/contract.md) carries their rules. A third-party mark never enters a nadicode logo form, composition, or clear-space measurement.
691
-
692
- Agent orbs, Nadia's included, and generated imagery are not nadicode company logos. The orbs live in [`packages/ui/docs/agent-orb.md`](../ui/docs/agent-orb.md); editorial imagery routes through [`skills/marketing/blog-imagegen/references/editorial-register.md`](../../skills/marketing/blog-imagegen/references/editorial-register.md). This contract owns only the package delivery interface.
693
-
694
- All forms derive from one authored, framework-neutral geometry source exported as `@nadicodeai/design-system/assets/logo-geometry`: on a 100-unit x-height, an 18-unit stroke with flat ends, bowls and the n's arch of radius 41, and the dot of radius 14 centred 79 units above the baseline over the i's stem. No SVG, React component, favicon, document, or app re-authors, traces, typesets, or rearranges those shapes. The eight reference files beside the ADR are the target each generated output matches, byte for byte, and the package guard fails on any output that drifts from them.
695
-
696
- On `{colors.canvas}` and the soft neutral canvases, the letters are ink and the dot is `{colors.logo-dot}`. On `{colors.identity-ink}` and other dark grounds, the letters are `{colors.identity-white}` and the dot is `{colors.dark-logo-dot}`, a brighter cobalt, so it does not sink into black. `{colors.logo-dot}` is the logo's own colour role and the only token a consumer paints the dot with; the generated mode layer flips it under any dark ancestor, so React draws one form on both grounds. A one-ink form, letters and dot in one colour, exists only where a single ink prints: embossing, stamps, one-colour print. On photography, place the logo in a quiet area or over a scrim that keeps every part legible; when neither is available, mount it on an approved solid field. One surface uses one treatment.
697
-
698
- The clear-space unit is one stroke width of the rendered wordmark or mark. Keep at least one unit free on every side, with no text, rule, image, or container edge inside it. Minimum rendered sizes are 72 CSS pixels wide for the wordmark, 12 CSS pixels for the mark, and 16 CSS pixels for the favicon. Browser and operating-system icon slots use the supplied favicon or app-icon files at the platform's required size. Never compress, crop, or distort a logo to fit.
699
-
700
- For a meaningful standalone image, use the accessible name `nadicode`; the accessible name is the label that identifies an image, link, or control to assistive technology. When a containing link or control already has that accessible name, hide the logo image from assistive technology with empty alt text. Never repeat the same accessible name on both the container and the image.
701
-
702
- Do not redraw, retype, reorder, stretch, crop, rotate, recolor, outline, shadow, animate, or add effects or gradients to a logo form. Do not add a dot or any shape to the mark, move the dot off the i, or substitute Nadia imagery for the logo.
703
-
704
- The package build renders and publicly exports seven files as SVG and transparent PNG from the geometry and the current `colors.*` contract: `assets/logo` and `assets/logo-white`, `assets/logo-one-ink` and `assets/logo-one-ink-white`, `assets/mark` and `assets/mark-white`, and `assets/app-icon`. React consumers use `BrandMark` or `BrandWordmark` according to the roles above; there is no lockup adapter, because the wordmark is the logo and the mark is already inside it, so no surface places the two together. The operator-only favicon generator draws the same icon tile at the sizes a browser and a platform ask for; it does not substitute another mark. Private geometry templates do not ship. A contract or geometry change is completed by regeneration, never by repainting an output or updating a fallback by hand.
525
+ ## Brand media authority
705
526
 
706
- ## CSS Architecture & Token Pipeline
527
+ The company is **nadicode**, lowercase. Its logo has three forms:
707
528
 
708
- How this contract becomes shipped CSS. The private CSS authoring graph lives in `src/css/`; the build flattens that graph and inlines its component icon dependencies into the single public `dist/css/index.css` artifact exported as `@nadicodeai/design-system/css`. Generated files and partials never ship as package subpaths. The cascade is `@layer tokens, theme, reset, foundation, motion, primitives, components;` with no `layout` and no `sections` layer. The bundle ships only framework-agnostic brand and display CSS components; generic marketing and SaaS components ship as React components from `@nadicodeai/ui`.
529
+ 1. **Wordmark.** nadicode in one geometric line with one dot over the i. The dot is the only colour in the logo, where an Agent sits. Use it wherever the company is named.
530
+ 2. **Mark.** The plain n cut from the wordmark, with nothing added, where the name does not fit: compact navigation, headers inside the product.
531
+ 3. **Icon.** The white n on a `{colors.identity-cobalto}` tile with rounded corners, for app icons, favicons and avatars; the tile is the one place the brand colour fills a logo's field. Nadia's desktop icon is the one exception: the `{colors.ink}` n on a white tile with its construction guides.
709
532
 
710
- ### Public style interfaces
533
+ On `{colors.canvas}` and the soft canvases the letters are ink and the dot is `{colors.logo-dot}`; on black and other dark grounds the letters are `{colors.identity-white}` and the dot `{colors.dark-logo-dot}`, a brighter cobalt. `logo-dot` is the only colour a consumer paints the dot with. A one-ink form exists only where a single ink prints. On a photograph, the logo sits in a quiet area or over a scrim that keeps it legible, or on an approved solid field.
711
534
 
712
- There is one public stylesheet interface per runtime, selected by the caller's runtime rather than composed by the caller:
535
+ Every form is drawn from one geometry source, `@nadicodeai/design-system/assets/logo-geometry`: on a 100-unit x-height, an 18-unit stroke with flat ends, bowls and the n's arch of radius 41, and a dot of radius 14 centred 79 units above the baseline over the i's stem. Nothing re-authors, traces, typesets or rearranges it. React draws `BrandWordmark` or `BrandMark`; there is no lockup, because the mark is already inside the wordmark.
713
536
 
714
- - Static HTML and non-React consumers use `@nadicodeai/design-system/css`.
715
- - React consumers use `@nadicodeai/ui/globals.css`. That React adapter composes the complete design-system CSS interface with Tailwind v4 and shadcn. Narrow protocol adapters may translate a third-party class vocabulary, but they do not declare another token hierarchy.
537
+ The clear space is one stroke width of the rendered logo on every side. The minimum sizes are 72 px wide for the wordmark, 12 px for the mark and 16 px for the favicon on screen, and 25 mm and 5 mm in print. A meaningful standalone logo is named `nadicode`; inside a link or control that already has the name, the logo is hidden from assistive technology. Never redraw, retype, stretch, crop, rotate, recolour, outline, shadow, animate or add a gradient to a logo; never add a shape to the mark or move the dot. Other companies' marks, Agent orbs and generated imagery are not nadicode logos.
716
538
 
717
- Callers never import generated files, CSS partials, or framework adapters directly. React callers never add `@nadicodeai/design-system/css` beside the UI stylesheet. The package export map and interface guards enforce this boundary. React installation and source-scanning setup live in [`packages/ui/docs/consuming-cross-repo.md`](../ui/docs/consuming-cross-repo.md).
539
+ ## Registers
718
540
 
719
- ### Authoring homes
541
+ | Register | Its rulebook | Its own tokens | Dark mode |
542
+ | --- | --- | --- | --- |
543
+ | Website | `apps/website/docs/design-doctrine.md` | `register.website.layout` | A light page; a black section scopes `.dark` for the product views in it |
544
+ | Portal | `apps/portal/docs/information-architecture.md` and `apps/portal/docs/design-doctrine.md` | None | System, light or dark, on the `.dark` class |
545
+ | Nadia | `apps/nadia/docs/design-doctrine.md` | None | System by default, light or dark, on the `.dark` class |
720
546
 
721
- Each part of the shipped CSS has exactly one authoring file:
547
+ - **Website.** The page grammar is "The website column": full-bleed sections on white, soft, black and colour grounds, alternating, with product views floating on them. Its cards are outlined; its product views take their real heights.
548
+ - **Portal.** A product shell on the workspace ground, built from the page compositions and the package components, with `globals.css` giving every elevated surface its tone.
549
+ - **Nadia.** The desktop runs inside Hermes's shell, which owns Tailwind and its own surface layering. Nadia's theme paints Hermes's roles with the shared `semantic` roles and core radii, and imports `@nadicodeai/ui/components.css` with the design system's host stylesheet, so the package components render with the same recipes and glass. The window follows the operating system's material where the system composites it.
722
550
 
723
- - Tokens: `src/css/tokens.generated.css`, built from this contract and never hand-edited. Runtime composition belongs to the owning layer, such as the website's responsive switching in `foundation.css`; there is no hand-authored token partial.
724
- - Type utilities: the `.nc-type-*` classes live in `foundation.css`.
725
- - The website's content column, `.nc-page`, and the responsive `@media` switching behind it: `foundation.css`.
726
- - CSS primitives and components: `primitives.css` plus `components/*`. Every shipped component is named once in `## Components`; the two-way guard derives the CSS relationship from that catalog.
727
- - Specimens: `examples/`, which only assemble already-defined tiers.
551
+ ## Do's and Don'ts
728
552
 
729
- ### Authored token extension
553
+ - Do keep the brand still: only Agents, and the product at work, move. Don't animate sections, pictures or scrolling.
554
+ - Do use the identity hues at full strength as grounds, stages and Agents, with the text pole that clears AA. Don't tint them, grade them, or paint a control, chrome or a status with them.
555
+ - Do choose a surface's height by its physical role, and let shadow show it in light and tone in dark. Don't add a border, ring or second shadow to an elevated surface.
556
+ - Do put glass on floating chrome over colour or motion. Don't make content glass, or put glass over a plain ground.
557
+ - Do show the product with its real components. Don't use screenshots, device frames or invented interfaces.
558
+ - Do round every surface and control at `{rounded.md}`. Don't give a higher surface a bigger corner.
559
+ - Do give every line one owner, and draw focus only with the focus outline.
560
+ - Do say a status with a Badge tone, a mark and a word. Don't use colour alone or a bare dot.
561
+ - Do use the dotted field only on a canvas where things are arranged. Don't put it behind lists, dashboards, settings or website sections.
562
+ - Do add a rule here before a product needs it. Don't restate a rule in another document; link to it.
730
563
 
731
- The YAML front matter carries the four groups the Google design schema defines: colors, typography, rounded, spacing. Everything else this contract owns is authored in the one `json design-tokens` fence below, as DTCG 2025.10. It holds `core.material`, `core.elevation` (four rungs per mode, each recipe ending in its own ring), `core.motion.duration` and `core.motion.ease`, and the register groups: `register.website.layout` (the website's column and section rhythm), `register.portal.density` (the filter-chip sizes), and `register.nadia` (the desktop's colour and radius aliases onto `core`, plus deprecated elevation data retained for older consumers).
564
+ ## Token extension
732
565
 
733
- An alias is written in the short `{core.color.canvas}` form here; the build rewrites it to the public rooted form `{nadicode.core.color.canvas}` in `dist/tokens/nadicode.dtcg.json`. `$extensions."ai.nadicode.targets"` on a register group lists where its values go: `css` puts them in the generated stylesheets, `data` keeps them in the token files only. A register group without that key fails the build. `core` carries no key and always reaches both.
566
+ The YAML front matter holds the four groups the DESIGN.md format defines: colors, typography, rounded and spacing. Everything else is authored in the one `json design-tokens` fence below, in the W3C design-tokens format: `core.material` (the scrim and glass values), `core.elevation` (four recipes per mode), `core.motion` (durations and easings), `semantic` (the shared colour roles) and `register.website` (the website column). An alias is written `{core.color.canvas}`. In CSS a token is `--nc-<name>`, an elevation is reached only through `nc-elevation-<height>`, and Tailwind names the values through the `nc-` and `nadicode-` namespaces. How the build turns this file into CSS and token files is described in `packages/design-system/README.md`.
734
567
 
735
568
  ```json design-tokens
736
569
  {
@@ -738,93 +571,79 @@ An alias is written in the short `{core.color.canvas}` form here; the build rewr
738
571
  "material": {
739
572
  "$type": "number",
740
573
  "scrim-opacity": {"$value":0.1},
741
- "glass-opacity": {"$value":0.76},
742
- "glass-saturation": {"$value":1.16},
743
- "glass-blur": {"$type":"dimension","$value":{"value":28,"unit":"px"}}
574
+ "glass-opacity": {"$value":0.6},
575
+ "glass-saturation": {"$value":1.8},
576
+ "glass-blur": {"$type":"dimension","$value":{"value":20,"unit":"px"}}
744
577
  },
745
578
  "elevation": {
746
579
  "$type": "shadow",
747
580
  "light": {
748
581
  "resting": {
749
582
  "$value": [
750
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":17.54,"unit":"px"},"blur":{"value":23.39,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
751
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":9.4,"unit":"px"},"blur":{"value":12.5,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
752
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":5.25,"unit":"px"},"blur":{"value":7,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
753
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2.79,"unit":"px"},"blur":{"value":3.72,"unit":"px"},"spread":{"value":-2,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.01}},
754
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1.16,"unit":"px"},"blur":{"value":1.5,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.01}},
755
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}}
583
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":2,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
584
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2,"unit":"px"},"blur":{"value":8,"unit":"px"},"spread":{"value":-2,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
585
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.08}}
756
586
  ]
757
587
  },
758
588
  "raised": {
759
589
  "$value": [
760
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":25,"unit":"px"},"blur":{"value":50,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
761
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":12,"unit":"px"},"blur":{"value":24,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
762
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":6,"unit":"px"},"blur":{"value":12,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
763
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":3,"unit":"px"},"blur":{"value":6,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
764
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1.5,"unit":"px"},"blur":{"value":3,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
765
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}}
590
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":2,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
591
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":6,"unit":"px"},"blur":{"value":12,"unit":"px"},"spread":{"value":-4,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.08}},
592
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":16,"unit":"px"},"blur":{"value":32,"unit":"px"},"spread":{"value":-8,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.1}},
593
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.08}}
766
594
  ]
767
595
  },
768
596
  "floating": {
769
597
  "$value": [
770
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":40,"unit":"px"},"blur":{"value":80,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
771
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":20,"unit":"px"},"blur":{"value":40,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
772
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":10,"unit":"px"},"blur":{"value":20,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
773
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":5,"unit":"px"},"blur":{"value":10,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
774
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2,"unit":"px"},"blur":{"value":4,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
775
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}}
598
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.75},"inset":true},
599
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.35},"inset":true},
600
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2,"unit":"px"},"blur":{"value":4,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
601
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":12,"unit":"px"},"blur":{"value":24,"unit":"px"},"spread":{"value":-6,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.12}},
602
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":32,"unit":"px"},"blur":{"value":64,"unit":"px"},"spread":{"value":-16,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.16}},
603
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.08}}
776
604
  ]
777
605
  },
778
606
  "modal": {
779
607
  "$value": [
780
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":60,"unit":"px"},"blur":{"value":120,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.07}},
781
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":30,"unit":"px"},"blur":{"value":60,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
782
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":15,"unit":"px"},"blur":{"value":30,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
783
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":7.5,"unit":"px"},"blur":{"value":15,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
784
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":3,"unit":"px"},"blur":{"value":6,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
785
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}}
608
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2,"unit":"px"},"blur":{"value":4,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
609
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":24,"unit":"px"},"blur":{"value":48,"unit":"px"},"spread":{"value":-12,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.18}},
610
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":64,"unit":"px"},"blur":{"value":128,"unit":"px"},"spread":{"value":-32,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.26}},
611
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.08}}
786
612
  ]
787
613
  }
788
614
  },
789
615
  "dark": {
790
616
  "resting": {
791
617
  "$value": [
792
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":17.54,"unit":"px"},"blur":{"value":23.39,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
793
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":9.4,"unit":"px"},"blur":{"value":12.5,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
794
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":5.25,"unit":"px"},"blur":{"value":7,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
795
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2.79,"unit":"px"},"blur":{"value":3.72,"unit":"px"},"spread":{"value":-2,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.01}},
796
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1.16,"unit":"px"},"blur":{"value":1.5,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.01}},
797
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.12}}
618
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.05},"inset":true},
619
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.07},"inset":true},
620
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":2,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.6}},
621
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.8}}
798
622
  ]
799
623
  },
800
624
  "raised": {
801
625
  "$value": [
802
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":25,"unit":"px"},"blur":{"value":50,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
803
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":12,"unit":"px"},"blur":{"value":24,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
804
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":6,"unit":"px"},"blur":{"value":12,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
805
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":3,"unit":"px"},"blur":{"value":6,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
806
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1.5,"unit":"px"},"blur":{"value":3,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
807
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.16}}
626
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.07},"inset":true},
627
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.09},"inset":true},
628
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":16,"unit":"px"},"blur":{"value":32,"unit":"px"},"spread":{"value":-8,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.6}},
629
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.8}}
808
630
  ]
809
631
  },
810
632
  "floating": {
811
633
  "$value": [
812
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":40,"unit":"px"},"blur":{"value":80,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
813
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":20,"unit":"px"},"blur":{"value":40,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
814
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":10,"unit":"px"},"blur":{"value":20,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
815
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":5,"unit":"px"},"blur":{"value":10,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
816
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2,"unit":"px"},"blur":{"value":4,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.02}},
817
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.22}}
634
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.09},"inset":true},
635
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.11},"inset":true},
636
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":12,"unit":"px"},"blur":{"value":24,"unit":"px"},"spread":{"value":-6,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.6}},
637
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":32,"unit":"px"},"blur":{"value":64,"unit":"px"},"spread":{"value":-16,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.7}},
638
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.8}}
818
639
  ]
819
640
  },
820
641
  "modal": {
821
642
  "$value": [
822
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":60,"unit":"px"},"blur":{"value":120,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.07}},
823
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":30,"unit":"px"},"blur":{"value":60,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.06}},
824
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":15,"unit":"px"},"blur":{"value":30,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
825
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":7.5,"unit":"px"},"blur":{"value":15,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}},
826
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":3,"unit":"px"},"blur":{"value":6,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.03}},
827
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.28}}
643
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.11},"inset":true},
644
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.13},"inset":true},
645
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":64,"unit":"px"},"blur":{"value":128,"unit":"px"},"spread":{"value":-32,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.8}},
646
+ {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.8}}
828
647
  ]
829
648
  }
830
649
  }
@@ -832,7 +651,6 @@ An alias is written in the short `{core.color.canvas}` form here; the build rewr
832
651
  "motion": {
833
652
  "duration": {
834
653
  "$type": "duration",
835
- "stagger": {"$value":{"value":60,"unit":"ms"}},
836
654
  "control": {"$value":{"value":100,"unit":"ms"}},
837
655
  "close": {"$value":{"value":140,"unit":"ms"}},
838
656
  "confirm": {"$value":{"value":180,"unit":"ms"}},
@@ -852,6 +670,124 @@ An alias is written in the short `{core.color.canvas}` form here; the build rewr
852
670
  }
853
671
  }
854
672
  },
673
+ "semantic": {
674
+ "radius": {"$type":"dimension","$value":"{core.rounded.md}"},
675
+ "color": {
676
+ "$type": "color",
677
+ "light": {
678
+ "background": {"$value":"{core.color.canvas}"},
679
+ "foreground": {"$value":"{core.color.ink}"},
680
+ "card": {"$value":"{core.color.material-solid}"},
681
+ "card-foreground": {"$value":"{core.color.ink}"},
682
+ "popover": {"$value":"{core.color.material-solid}"},
683
+ "popover-foreground": {"$value":"{core.color.ink}"},
684
+ "primary": {"$value":"{core.color.action}"},
685
+ "primary-foreground": {"$value":"{core.color.action-foreground}"},
686
+ "primary-hover": {"$value":"{core.color.action-hover}"},
687
+ "primary-active": {"$value":"{core.color.action-active}"},
688
+ "secondary": {"$value":"{core.color.canvas-soft}"},
689
+ "secondary-foreground": {"$value":"{core.color.ink}"},
690
+ "muted": {"$value":"{core.color.canvas-soft}"},
691
+ "muted-foreground": {"$value":"{core.color.body}"},
692
+ "placeholder": {"$value":"{core.color.muted}"},
693
+ "accent": {"$value":"{core.color.canvas-soft-2}"},
694
+ "accent-foreground": {"$value":"{core.color.ink}"},
695
+ "destructive": {"$value":"{core.color.error}"},
696
+ "destructive-foreground": {"$value":"{core.color.identity-white}"},
697
+ "warning": {"$value":"{core.color.warning}"},
698
+ "warning-foreground": {"$value":"{core.color.ink}"},
699
+ "success": {"$value":"{core.color.success}"},
700
+ "success-foreground": {"$value":"{core.color.ink}"},
701
+ "info": {"$value":"{core.color.info}"},
702
+ "info-foreground": {"$value":"{core.color.identity-white}"},
703
+ "destructive-soft": {"$value":"{core.color.error-soft}"},
704
+ "destructive-soft-foreground": {"$value":"{core.color.error-deep}"},
705
+ "warning-soft": {"$value":"{core.color.warning-soft}"},
706
+ "warning-soft-foreground": {"$value":"{core.color.warning-deep}"},
707
+ "success-soft": {"$value":"{core.color.success-soft}"},
708
+ "success-soft-foreground": {"$value":"{core.color.success-deep}"},
709
+ "info-soft": {"$value":"{core.color.info-soft}"},
710
+ "info-soft-foreground": {"$value":"{core.color.info-deep}"},
711
+ "border": {"$value":"{core.color.line}"},
712
+ "input": {"$value":"{core.color.input}"},
713
+ "ring": {"$value":"{core.color.focus-ring}"},
714
+ "scrim": {"$value":"{core.color.scrim}"},
715
+ "chart-1": {"$value":"{core.color.chart-1}"},
716
+ "chart-2": {"$value":"{core.color.chart-2}"},
717
+ "chart-3": {"$value":"{core.color.chart-3}"},
718
+ "chart-4": {"$value":"{core.color.chart-4}"},
719
+ "chart-5": {"$value":"{core.color.chart-5}"},
720
+ "selection": {"$value":"{core.color.navigation-selection-bg}"},
721
+ "selection-foreground": {"$value":"{core.color.navigation-selection-fg}"},
722
+ "recessed": {"$value":"{core.color.canvas-soft}"},
723
+ "sidebar": {"$value":"{core.color.canvas-soft}"},
724
+ "sidebar-foreground": {"$value":"{core.color.ink}"},
725
+ "sidebar-primary": {"$value":"{core.color.navigation-selection-bg}"},
726
+ "sidebar-primary-foreground": {"$value":"{core.color.navigation-selection-fg}"},
727
+ "sidebar-primary-hover": {"$value":"{core.color.navigation-selection-hover}"},
728
+ "sidebar-accent": {"$value":"{core.color.canvas-soft-2}"},
729
+ "sidebar-accent-foreground": {"$value":"{core.color.ink}"},
730
+ "sidebar-border": {"$value":"{core.color.line}"},
731
+ "sidebar-ring": {"$value":"{core.color.focus-ring}"}
732
+ },
733
+ "dark": {
734
+ "background": {"$value":"{core.color.dark-canvas}"},
735
+ "foreground": {"$value":"{core.color.dark-ink}"},
736
+ "card": {"$value":"{core.color.dark-material-solid}"},
737
+ "card-foreground": {"$value":"{core.color.dark-ink}"},
738
+ "popover": {"$value":"{core.color.dark-material-solid}"},
739
+ "popover-foreground": {"$value":"{core.color.dark-ink}"},
740
+ "primary": {"$value":"{core.color.dark-action}"},
741
+ "primary-foreground": {"$value":"{core.color.dark-action-foreground}"},
742
+ "primary-hover": {"$value":"{core.color.dark-action-hover}"},
743
+ "primary-active": {"$value":"{core.color.dark-action-active}"},
744
+ "secondary": {"$value":"{core.color.dark-canvas-soft}"},
745
+ "secondary-foreground": {"$value":"{core.color.dark-ink}"},
746
+ "muted": {"$value":"{core.color.dark-canvas-soft}"},
747
+ "muted-foreground": {"$value":"{core.color.dark-body}"},
748
+ "placeholder": {"$value":"{core.color.dark-muted}"},
749
+ "accent": {"$value":"{core.color.dark-canvas-soft-2}"},
750
+ "accent-foreground": {"$value":"{core.color.dark-ink}"},
751
+ "destructive": {"$value":"{core.color.dark-error}"},
752
+ "destructive-foreground": {"$value":"{core.color.dark-on-primary}"},
753
+ "warning": {"$value":"{core.color.dark-warning}"},
754
+ "warning-foreground": {"$value":"{core.color.dark-on-primary}"},
755
+ "success": {"$value":"{core.color.dark-success}"},
756
+ "success-foreground": {"$value":"{core.color.dark-on-primary}"},
757
+ "info": {"$value":"{core.color.dark-info}"},
758
+ "info-foreground": {"$value":"{core.color.dark-on-primary}"},
759
+ "destructive-soft": {"$value":"{core.color.dark-error-soft}"},
760
+ "destructive-soft-foreground": {"$value":"{core.color.dark-error-deep}"},
761
+ "warning-soft": {"$value":"{core.color.dark-warning-soft}"},
762
+ "warning-soft-foreground": {"$value":"{core.color.dark-warning-deep}"},
763
+ "success-soft": {"$value":"{core.color.dark-success-soft}"},
764
+ "success-soft-foreground": {"$value":"{core.color.dark-success-deep}"},
765
+ "info-soft": {"$value":"{core.color.dark-info-soft}"},
766
+ "info-soft-foreground": {"$value":"{core.color.dark-info-deep}"},
767
+ "border": {"$value":"{core.color.dark-line}"},
768
+ "input": {"$value":"{core.color.dark-input}"},
769
+ "ring": {"$value":"{core.color.dark-focus-ring}"},
770
+ "scrim": {"$value":"{core.color.dark-scrim}"},
771
+ "chart-1": {"$value":"{core.color.dark-chart-1}"},
772
+ "chart-2": {"$value":"{core.color.dark-chart-2}"},
773
+ "chart-3": {"$value":"{core.color.dark-chart-3}"},
774
+ "chart-4": {"$value":"{core.color.dark-chart-4}"},
775
+ "chart-5": {"$value":"{core.color.dark-chart-5}"},
776
+ "selection": {"$value":"{core.color.dark-navigation-selection-bg}"},
777
+ "selection-foreground": {"$value":"{core.color.dark-navigation-selection-fg}"},
778
+ "recessed": {"$value":"{core.color.dark-canvas}"},
779
+ "sidebar": {"$value":"{core.color.dark-canvas}"},
780
+ "sidebar-foreground": {"$value":"{core.color.dark-ink}"},
781
+ "sidebar-primary": {"$value":"{core.color.dark-navigation-selection-bg}"},
782
+ "sidebar-primary-foreground": {"$value":"{core.color.dark-navigation-selection-fg}"},
783
+ "sidebar-primary-hover": {"$value":"{core.color.dark-navigation-selection-hover}"},
784
+ "sidebar-accent": {"$value":"{core.color.dark-canvas-soft-2}"},
785
+ "sidebar-accent-foreground": {"$value":"{core.color.dark-ink}"},
786
+ "sidebar-border": {"$value":"{core.color.dark-line}"},
787
+ "sidebar-ring": {"$value":"{core.color.dark-focus-ring}"}
788
+ }
789
+ }
790
+ },
855
791
  "register": {
856
792
  "website": {
857
793
  "$extensions": {"ai.nadicode.targets":["css","data"]},
@@ -864,197 +800,7 @@ An alias is written in the short `{core.color.canvas}` form here; the build rewr
864
800
  "section-step-mobile": {"$value":{"value":64,"unit":"px"}},
865
801
  "section-step-desktop": {"$value":{"value":96,"unit":"px"}}
866
802
  }
867
- },
868
- "portal": {
869
- "$extensions": {"ai.nadicode.targets":["css","data"]},
870
- "density": {
871
- "$type": "dimension",
872
- "filter-chip-gap": {"$value":{"value":6,"unit":"px"}},
873
- "filter-chip-height": {"$value":{"value":28,"unit":"px"}},
874
- "filter-chip-icon": {"$value":{"value":14,"unit":"px"}}
875
- }
876
- },
877
- "nadia": {
878
- "$extensions": {"ai.nadicode.targets":["data"]},
879
- "color": {
880
- "light": {
881
- "$type": "color",
882
- "background": {"$value":"{core.color.canvas}"},
883
- "foreground": {"$value":"{core.color.ink}"},
884
- "card": {"$value":"{core.color.canvas}"},
885
- "card-foreground": {"$value":"{core.color.ink}"},
886
- "muted": {"$value":"{core.color.canvas-soft-2}"},
887
- "muted-foreground": {"$value":"{core.color.body}"},
888
- "popover": {"$value":"{core.color.canvas}"},
889
- "popover-foreground": {"$value":"{core.color.ink}"},
890
- "primary": {"$value":"{core.color.action}"},
891
- "primary-foreground": {"$value":"{core.color.action-foreground}"},
892
- "secondary": {"$value":"{core.color.canvas-soft}"},
893
- "secondary-foreground": {"$value":"{core.color.ink}"},
894
- "accent": {"$value":"{core.color.canvas-soft-2}"},
895
- "accent-foreground": {"$value":"{core.color.ink}"},
896
- "border": {"$value":"{core.color.line}"},
897
- "input": {"$value":"{core.color.input}"},
898
- "ring": {"$value":"{core.color.focus-ring}"},
899
- "midground": {"$value":"{core.color.body}"},
900
- "composer-ring": {"$value":"{core.color.focus-ring}"},
901
- "destructive": {"$value":"{core.color.error}"},
902
- "destructive-foreground": {"$value":"{core.color.on-primary}"},
903
- "sidebar-background": {"$value":"{core.color.canvas-soft}"},
904
- "sidebar-border": {"$value":"{core.color.line}"},
905
- "user-bubble": {"$value":"{core.color.canvas-soft-2}"},
906
- "user-bubble-border": {"$value":"{core.color.line}"}
907
- },
908
- "dark": {
909
- "$type": "color",
910
- "background": {"$value":"{core.color.dark-canvas}"},
911
- "foreground": {"$value":"{core.color.dark-ink}"},
912
- "card": {"$value":"{core.color.dark-canvas}"},
913
- "card-foreground": {"$value":"{core.color.dark-ink}"},
914
- "muted": {"$value":"{core.color.dark-canvas-soft-2}"},
915
- "muted-foreground": {"$value":"{core.color.dark-muted}"},
916
- "popover": {"$value":"{core.color.dark-canvas}"},
917
- "popover-foreground": {"$value":"{core.color.dark-ink}"},
918
- "primary": {"$value":"{core.color.dark-action}"},
919
- "primary-foreground": {"$value":"{core.color.dark-action-foreground}"},
920
- "secondary": {"$value":"{core.color.dark-canvas-soft-2}"},
921
- "secondary-foreground": {"$value":"{core.color.dark-body}"},
922
- "accent": {"$value":"{core.color.dark-canvas-soft-2}"},
923
- "accent-foreground": {"$value":"{core.color.dark-ink}"},
924
- "border": {"$value":"{core.color.dark-line}"},
925
- "input": {"$value":"{core.color.dark-input}"},
926
- "ring": {"$value":"{core.color.dark-focus-ring}"},
927
- "midground": {"$value":"{core.color.dark-muted}"},
928
- "composer-ring": {"$value":"{core.color.dark-focus-ring}"},
929
- "destructive": {"$value":"{core.color.error}"},
930
- "destructive-foreground": {"$value":"{core.color.on-primary}"},
931
- "sidebar-background": {"$value":"{core.color.dark-canvas}"},
932
- "sidebar-border": {"$value":"{core.color.dark-seam}"},
933
- "user-bubble": {"$value":"{core.color.dark-canvas-soft-2}"},
934
- "user-bubble-border": {"$value":"{core.color.dark-line}"}
935
- }
936
- },
937
- "radius": {
938
- "$type": "dimension",
939
- "xs": {"$value":"{core.rounded.xs}"},
940
- "sm": {"$value":"{core.rounded.sm}"},
941
- "md": {"$value":"{core.rounded.md}"},
942
- "lg": {"$value":"{core.rounded.lg}"},
943
- "xl": {"$value":"{core.rounded.xl}"},
944
- "2xl": {"$value":"{core.rounded.2xl}"},
945
- "3xl": {"$value":"{core.rounded.3xl}"},
946
- "4xl": {"$value":"{core.rounded.3xl}"}
947
- },
948
- "elevation": {
949
- "$deprecated": true,
950
- "$description": "Compatibility data for older Nadia consumers. New surfaces use core.elevation's four material roles; resting controls have no shadow.",
951
- "light": {
952
- "$type": "shadow",
953
- "control": {
954
- "$value": [
955
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
956
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":2,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.04}}
957
- ]
958
- },
959
- "panel": {
960
- "$value": [
961
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
962
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":3,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}}
963
- ]
964
- },
965
- "raised": {
966
- "$value": [
967
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
968
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":4,"unit":"px"},"blur":{"value":10,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.07}},
969
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":10,"unit":"px"},"blur":{"value":22,"unit":"px"},"spread":{"value":-14,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.16}}
970
- ]
971
- },
972
- "overlay": {
973
- "$value": [
974
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":8,"unit":"px"},"blur":{"value":18,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.09}},
975
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":20,"unit":"px"},"blur":{"value":36,"unit":"px"},"spread":{"value":-20,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.22}}
976
- ]
977
- },
978
- "modal": {
979
- "$value": [
980
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}},
981
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":14,"unit":"px"},"blur":{"value":30,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.13}},
982
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":32,"unit":"px"},"blur":{"value":60,"unit":"px"},"spread":{"value":-24,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.3}}
983
- ]
984
- },
985
- "composer": {
986
- "$value": [
987
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":2,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}}
988
- ]
989
- },
990
- "overlay-border": {
991
- "$type": "border",
992
- "$value": {"width":{"value":1,"unit":"px"},"style":"solid","color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.05}}
993
- }
994
- },
995
- "dark": {
996
- "$type": "shadow",
997
- "control": {
998
- "$value": [
999
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.18}},
1000
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":2,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.5}}
1001
- ]
1002
- },
1003
- "panel": {
1004
- "$value": [
1005
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.18}},
1006
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":2,"unit":"px"},"blur":{"value":6,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.6}}
1007
- ]
1008
- },
1009
- "raised": {
1010
- "$value": [
1011
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.18}},
1012
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":6,"unit":"px"},"blur":{"value":16,"unit":"px"},"spread":{"value":-10,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.85}}
1013
- ]
1014
- },
1015
- "overlay": {
1016
- "$value": [
1017
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":12,"unit":"px"},"blur":{"value":26,"unit":"px"},"spread":{"value":-12,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.9}}
1018
- ]
1019
- },
1020
- "modal": {
1021
- "$value": [
1022
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":0,"unit":"px"},"blur":{"value":0,"unit":"px"},"spread":{"value":1,"unit":"px"},"color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.18}},
1023
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":16,"unit":"px"},"blur":{"value":32,"unit":"px"},"spread":{"value":-14,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.95}}
1024
- ]
1025
- },
1026
- "composer": {
1027
- "$value": [
1028
- {"offsetX":{"value":0,"unit":"px"},"offsetY":{"value":1,"unit":"px"},"blur":{"value":2,"unit":"px"},"spread":{"value":0,"unit":"px"},"color":{"colorSpace":"srgb","components":[0,0,0],"alpha":0.5}}
1029
- ]
1030
- },
1031
- "overlay-border": {
1032
- "$type": "border",
1033
- "$value": {"width":{"value":1,"unit":"px"},"style":"solid","color":{"colorSpace":"srgb","components":[1,1,1],"alpha":0.18}}
1034
- }
1035
- }
1036
- }
1037
803
  }
1038
804
  }
1039
805
  }
1040
806
  ```
1041
-
1042
- ### Generated outputs
1043
-
1044
- `scripts/build.ts` runs the pipeline once. The `@google/design.md` CLI exports the YAML front matter, `scripts/sd/design-tokens-fence.ts` reads the one `json design-tokens` fence, and `scripts/sd/compose-token-graph.ts` composes the two into the single `nadicode` graph. That composer is the only code that creates the root: it merges without collision, rewrites every short alias to its rooted public form, and checks ownership and reference direction (`core` reaches outside nothing, a register reaches `core` and itself, never another register).
1045
-
1046
- The composer returns three views of one validated object. The canonical graph keeps every alias and every `$extensions` entry and ships as `dist/tokens/nadicode.dtcg.json` (`@nadicodeai/design-system/tokens/dtcg`), the public contract a conformance tool or a theme generator reads. The resolved graph replaces each alias with its value and ships as `dist/tokens/nadicode.dtcg-resolved.json` (`./tokens/dtcg-resolved`), for data-only consumers that want literals. Both are validated against the vendored DTCG 2025.10 schema (`scripts/sd/schemas/`) before they are written. The third view is the CSS projection: the leaves of every group whose `ai.nadicode.targets` includes `css`, which is `core` plus `register.website` and `register.portal`; `register.nadia` is data only and reaches no stylesheet.
1047
-
1048
- Style Dictionary (`scripts/sd/`) is the exporter over that projection, not the validator. It emits three CSS files:
1049
-
1050
- 1. The token layer `tokens.generated.css`: a complete projection of every export token, with each emitted `--nc-*` name appearing once. Never an allowlist or subset, and never containing `@media` or `calc()`; responsive and composed runtime variables live only in their owning layer and reference emitted `--nc-*` tokens. Names map per group: `core.color` and `core.spacing` emit `--nc-<name>`, as do `register.website.layout` and `register.portal.density`, so a moved token keeps the name it had; `core.rounded` emits `--nc-rounded-<name>`; `core.material` emits `--nc-material-<name>`; `core.elevation.light.<role>` emits `--nc-elevation-<role>` in `:root` and `core.elevation.dark.<role>` the same name inside `.dark`; `core.motion.duration` emits `--nc-duration-<name>` and `core.motion.ease` emits `--nc-ease-<name>`; and each composite `typography` scale expands per property into `--nc-type-<scale>-font-family`, `-font-size`, `-font-weight`, `-line-height`, and `-letter-spacing`, never the CSS `font:` shorthand, which drops the line-height unit. Values serialize deterministically: authored hex colors emit as `oklch()`, dimensions keep their authored value and unit, typography line-height comes from this contract's own unit-preserving Tailwind export because the DTCG composite export carries it as a bare number, durations emit as authored, easings as `cubic-bezier()`, elevation recipes as the complete multi-layer `box-shadow` for their mode, and the material ratios as the percentages their callers read. Emitted-but-unreferenced contract tokens remain part of the package vocabulary; private compatibility variables with no caller do not.
1051
- 2. The mode layer `modes.generated.css`: derives every color pair named `X` and `dark-X` from the complete token graph, then remaps `--nc-X` inside `.dark`. There is no component list or token allowlist to maintain. A dark-only token without an `X` counterpart remains available by its explicit name and is not remapped.
1052
- 3. The theme layer `theme.generated.css`: emits the standard shadcn `:root` and `.dark` role maps, the Tailwind v4 `@theme inline` semantic bridge, and the raw contract projection. Standard `--color-*` utilities are reserved for semantic shadcn roles. Raw contract colors use the explicit `--color-nc-*` namespace; the remaining Tailwind token families keep their generated font, type, radius, and spacing namespaces. Every emitted value derives from this contract, and the formatter holds no raw `--nc-*` mode-remap table. The projection fills `--ease-nc-<name>` for every easing; it fills no `--shadow-*` entry, because the four elevation rungs reach React only through the owned `nc-elevation-*` utilities, one spelling per rung; Tailwind v4 owns no duration namespace, so a call site writes `duration-(--nc-duration-open)`. The raw `@theme` block also carries a small hand-maintained alias set — `--outline-width-focus`, `--outline-offset-focus`, `--opacity-scrim`, `--spacing-control-height`, `--spacing-control-padding-inline`, `--spacing-touch-target` — each a `var()` reference onto a generated token, and each must have a live caller or be removed.
1053
-
1054
- The private source entry imports all three generated files, and the package build compiles the complete graph into an import-free public artifact. Raw mode behavior and the Tailwind semantic bridge therefore belong to the framework-neutral design-system module, not to the React adapter or individual CSS components. `@nadicodeai/ui/globals.css` composes that complete module with Tailwind and owns only narrow technology integration, such as ANSI class translation; React components consume the generated shadcn roles or namespaced raw utilities directly.
1055
-
1056
- `npm run build` also writes the two DTCG artifacts above and `dist/tailwind/nadicode.tailwind.json` as generated, tracked data artifacts, and bakes `dist/icons/` from the pinned `lucide-static` dependency. The Tailwind export stays for what it uniquely supplies: the typography line-height with its declared unit, which the DTCG composite normaliser strips, and the logo paint lookup.
1057
-
1058
- ### Enforcement
1059
-
1060
- Correctness is structural, not a byte snapshot: `scripts/sd/design-tokens-fence.test.ts` verifies the fence reader, one fence, strict JSON, typed leaves, legal names; `scripts/sd/compose-token-graph.test.ts` verifies the composition, one construction of the root, alias rewriting, ownership and reference direction, extension preservation, and the CSS projection; `tests/guards/dtcg-shape.test.ts` verifies both published artifacts against the vendored DTCG schema and the Nadicode profile; `tests/guards/token-structural-parity.test.ts` verifies the complete `--nc-*` projection; `tests/guards/tailwind-v4-theme.test.ts` verifies mode-pair derivation, `@theme` values, and light/dark role integrity; `tests/guards/component-contract-matches-css.test.ts` verifies the retained CSS component surface against this contract; `tests/guards/css-bundle.test.ts` verifies the public artifact is flat, complete, and contains no shipped partials; and `tests/guards/consumer-css-discipline.test.ts` verifies reference integrity, the absence of raw color literals in the hand-authored kit CSS and of raw color and radius literals in `examples/`, second value sources, and DTCG drift. These guards read freshly built output (the test global-setup rebuilds before any of them run), so they prove the generator; a hand-edited or stale committed generated file is caught by the CI cleanliness step in the `design-system` job, which requires a fresh build to reproduce the committed bytes exactly, with `dist/favicon/**` outside it, regenerated only by `generate:favicons` and fenced by review rather than machinery. Cross-package guards verify that React callers use only the React stylesheet interface.