# 09 · Visual Design Language > **Status:** foundational design pass. This is the _visual_ contract — the > counterpart to `docs/spec/` (behavior) and the rest of `docs/architecture/` > (structure). `styles/tokens.css`, `styles/base.css`, component CSS Modules, and > `src/core/vega-themes.ts` implement _to this doc_. > > **Companion:** [`visual-specimen.html`](./visual-specimen.html) — a standalone, > openable "kitchen sink" that renders every token and element with a live > theme/accent switcher. Edit tokens there first, eyeball them, then port the > settled values into `styles/tokens.css`. Astrolabe's look is **inspired by the IBM Design Language / Carbon**, but Carbon is **not a dependency** — we transcribe the values we want and reinterpret the principles in our own words. We borrow IBM's _engineered structure_; we keep _color and theming free_. --- ## 1. Principles IBM's four design principles map almost exactly onto Astrolabe's SOUL ("the spec is the star; the UI is a thin, considered shell"). Restated for us: 1. **Considered** — _remove everything gratuitous._ No decoration that isn't carrying meaning. Whitespace is a feature. 2. **Unified** — a _small fixed kit_ (one type family, a neutral ramp, one accent, a handful of components) reused systematically. Identity comes from consistency, not novelty per screen. 3. **Executed** — _everything communicates, including what we leave out._ Alignment, rhythm, and empty space are decisions, not leftovers. 4. **Progressive** — _every element reduces friction._ If it doesn't help the user read, edit, or find a snippet faster, it doesn't earn its place. …plus our own, where we part ways with IBM: 5. **Structure is rigorous; color is free.** The grid, type scale, spacing, and square geometry are systematic and fixed. Color, accent, and theming are the _expressive_ layer — open, swappable, and meant to be played with. --- ## 2. Deliberate divergences from Carbon What we **borrow** vs. where we **diverge** — recorded so future readers know these were choices, not drift: | Topic | IBM/Carbon | Astrolabe | | ------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------- | | Adoption | A framework + component lib | **Inspiration only.** Transcribed tokens, our own components | | Structure (grid, type, spacing) | 8px mini unit, modular type scale | **Borrowed wholesale** — it's the rigorous part worth having | | UI chrome corners | ~0–2px (near-square) | **Fully square, `radius: 0`** — one notch more austere/engineered | | Icons | Rounded exteriors, 2px soft corners + 90° interiors | **Kept rounded** (use Carbon's icon set) — the one warm, human touch | | Color | "Blue at the core"; other hues only for purpose | **Dropped.** Color/theming is free and expressive; accent is a token, many themes welcome | | Neutrals | Carbon gray ramp | **Borrowed** — accessible, well-tuned, a good legible base | | Motion | Productive vs. expressive | **Productive only** — subtle, purposeful, reduced-motion-aware | --- ## 3. Tokens All tokens are CSS custom properties on `:root`, themed by overriding them on `[data-theme]` (and, for accent, `[data-accent]`). As of M1.5 the settled values live in `styles/tokens.css`; the specimen remains the sandbox for trying new tokens/themes before porting them across. ### 3.1 Typography — IBM Plex - **Families:** `IBM Plex Sans` for UI, `IBM Plex Mono` for the editor, code, numeric/tabular data, and inline spec fragments. Self-hosted in production via `@fontsource/ibm-plex-sans` + `@fontsource/ibm-plex-mono` (offline/PWA — never a CDN). The specimen uses a CDN purely for preview convenience. - **Scale (px), from Carbon's modular scale:** `12 · 14 · 16 · 18 · 20 · 24 · 28 · 32 · 42`. Body is **14/20** (already our `--font-size-base`). Captions/labels 12. - **Weights:** 400 regular, 600 semibold for emphasis/headings; 300 light reserved for large display only. - **Breathing room:** Plex _"requires space to breathe."_ Don't over-tighten — body line-height ≥ 1.4, default tracking (no negative letter-spacing on text). Flush-left, clear hierarchy. ### 3.2 Spacing — the 8px base unit IBM's product/web rule: _"the 8px mini unit guides everything."_ Every gap, pad, and size is a relationship of 8 (with 2/4 as fine sub-steps): `--space-1: 2px · --space-2: 4px · --space-3: 8px · --space-4: 12px · --space-5: 16px · --space-6: 24px · --space-7: 32px · --space-8: 48px · --space-9: 64px`. > Note: this renumbers our current M0 scale to anchor on 8. The migration is > mechanical (search/replace `--space-*` usages) and lands with the design pass. ### 3.3 Color — role-based, theme-free Color is expressed as **roles**, never raw hexes, so themes can repaint the whole UI by swapping one set of values. Borrowed from Carbon's layering model: | Role token | Meaning | | ---------------------------------------------------- | -------------------------------------------------------------------------------------- | | `--bg` | App canvas (lowest layer) | | `--layer-01` / `--layer-02` | Raised surfaces (panels, cards, popovers) — elevation by lightness step, not shadow | | `--border` / `--border-strong` | Subtle and prominent separators | | `--text` / `--text-secondary` / `--text-placeholder` | Text hierarchy | | `--accent` / `--accent-hover` / `--accent-contrast` | The expressive accent — **swappable**; UI must never hardcode a hue | | `--accent-soft` / `--accent-soft-hover` | Low-emphasis accent **wash** (accent mixed into `--bg`) for a tinted-but-quiet surface | | `--focus` | Focus-ring color (defaults to `--accent`) | | `--support-error / -success / -warning / -info` | Status only — color = meaning | - **Neutrals** use the Carbon gray ramp (`#f4f4f4 … #161616`) — accessible and legible. **Accent and theming are open**: the default accent is **deep teal** (`#0e7490` / dark `#2dd4bf`), with opt-in alternates (blue, indigo, amber, rose) and light/dark themes to prove the system is free, not blue-bound. Pick, add, or invent themes freely. - **Status palette** (borrowed, stable): error `#da1e28`, success `#198038`, warning `#f1c21b`, info `#0043ce` — tuned per theme for contrast. - **Contrast:** target WCAG AA (4.5:1 text, 3:1 large/UI). Accent-on-`--bg` and text-on-`--accent` must both pass for any shipped theme. - **Soft accent is derived, not hardcoded.** `--accent-soft` / `--accent-soft-hover` are `color-mix(in srgb, var(--accent) 12–20%, var(--bg))`, so the wash follows whatever accent + theme is active rather than carrying a per-accent value. Use it for a surface that should be _noticed_ without competing with a primary action (the header's **Donate** button). - **Field-on-layer (Carbon layering).** A field is a layer _on top of_ the surface it sits on — so it steps **toward `--bg`**, never to a darker gray. On the white canvas and on a `--layer-01` panel alike, an input reads as **`--bg` fill + `--border-strong` outline**, not a `--layer-02` fill. Stacking a darker-gray field on a gray panel (canvas → `--layer-01` panel → `--layer-02` field) muddies into three indistinct grays — the bug this rule prevents (the snippet metadata panel). ### 3.4 Shape & elevation - `--radius: 0` for all chrome (buttons, fields, cards, panels). Square is the look. - **Icons are exempt** — they keep their rounded geometry (Carbon icon set, 2px corners). Icons are SVG, not chrome, so `--radius` doesn't touch them. - **Elevation is lightness, not shadow.** Stack `--bg → --layer-01 → --layer-02`. Shadows, if ever used, are minimal and reserved for true overlays (modals, popovers). - **Borders are 1px**, `--border` subtle by default. ### 3.5 Motion - **Durations (productive):** `--dur-fast: 70ms`, `--dur-fast-2: 110ms`, `--dur-moderate: 150ms`. Nothing slower in the core UI. - **Easing:** standard productive `cubic-bezier(0.2, 0, 0.38, 0.9)`. - **Restraint:** animate only what's vital (state changes, entrances of meaningful elements). No gratuitous motion. All transitions are already neutralized under `@media (prefers-reduced-motion: reduce)` in `base.css`. --- ## 4. Component conventions - **The control scale (two heights, enforced by tokens + primitives).** Every interactive control is `--control-height` (32px) or `--control-height-lg` (40px) — tokens.css. 32px is THE control height: buttons, fields, selects, segmented controls, icon buttons, anything in a toolbar, form row, or dialog action row. 40px is reserved for standalone primary CTAs (the library's Build Chart, a modal list-pane's New X) and modal footers. A third height is drift — the pre-token codebase accumulated 26/28/30/36/42px variants one component at a time, which read as "shaky" the moment controls shared a row. The rule is enforced mechanically: action buttons are the **`Button`** primitive, icon-only buttons are **`IconButton`** (24px `sm` exists solely for controls nested _inside_ a 32px control — a search field's clear, a toast's dismiss); writing `height:` on a new ad-hoc button is the code smell. - **Buttons:** square, via the `Button` primitive. Variants: **primary** (filled `--accent`), **secondary** (1px `--border-strong`, `--bg` fill — the same field-on-layer treatment as inputs, so a bordered control on a gray panel goes white, never a darker gray), **ghost** (text-only, borderless; a transparent border holds the box size), **soft-accent** (ghost on an `--accent-soft` wash — a low-emphasis solicitation, e.g. Donate), **danger** (filled `--support-error` — the confirm step), **danger-outline** (secondary geometry, red label, filling solid red on hover/focus — a destructive action sitting among peers, e.g. a detail view's Delete). 13px 600-weight label. Clear hover/active and a visible focus ring. **Inline link-style actions are a separate kind, not a Button variant**: small accent-text actions embedded in content ("+ Add filter", "Swap X/Y", "Use a constant", a popover's Reset) deliberately sit below the control scale — content-sized, 11–12px, no box — so they read as part of the prose/panel they act on, not as toolbar controls. Don't "promote" them to Buttons; their smallness is the emphasis level. - **Borders mark function, not decoration.** A 1px `--border-strong` box belongs to things that _hold or receive a value_: fields, selects/disclosure triggers, segmented controls, secondary buttons, drop targets (dashed). Plain actions are ghost or filled — never outlined boxes. List rows are flat (hairline dividers + hover fill), not stacked boxes. With square chrome, every box makes alignment errors visible, so each border must earn its place; when a region looks "busy", remove boxes before shrinking anything. (Carbon goes further — fields are bottom-border-only and its secondary button is a gray fill; our fully-boxed fields are a recorded divergence, §2, which is exactly why box discipline matters more here, not less.) - **Emphasis hierarchy (Carbon button/usage).** A region carries **one** high-emphasis (primary) button at most; everything else is lower emphasis. In toolbars/headers full of utilities, the utilities go **ghost** so they recede behind the work area and read as a row of equals — only the genuine call to action is filled. Worked examples: the **editor toolbar** (Publish is the lone primary; Extract/Revert are secondary, and collapse to icons when narrow — see [arch 10 §8](10-interaction-and-feedback.md)), and the **header** (Datasets / Import / Export / About are ghost; a divider then sets off the soft-accent Donate and the ghost theme toggle). - **Hover is variant-specific:** filled buttons (primary/danger) **darken** (`--accent-hover` / a slight brightness drop); outlined/ghost buttons **gain a fill one elevation step above their surface** — on `--bg` → `--layer-01`, on a `--layer-01` surface (dialogs, panels) → `--layer-02`. Filling to the _same_ layer as the surface reads as no hover at all (the collision that left the confirm dialog's Cancel looking dead). The step is mechanical: Button/IconButton hover with `var(--control-hover-fill, var(--layer-01))`, and an elevated surface sets `--control-hover-fill: var(--layer-02)` once on its container (the header, the modal chrome, the confirm card, the library's metadata panel) — controls inherit the right step instead of each re-encoding it. - **Focus ring:** a 2px `--focus` outline (offset 1–2px). Always visible on keyboard focus — accessibility is non-negotiable (principle 4). - **Fields** (text, textarea, select, search): square, 1px `--border-strong`, **`--bg` fill** (field-on-layer, §3.3 — white on the canvas _and_ on a `--layer-01` panel; never a darker-gray fill), accent border + focus ring on focus. Mono font for spec/JSON inputs. - **List rows** (snippet library): compact, full-row hover (`--layer-01`), active row marked by an accent left-border + `--layer-01` fill, secondary metadata in `--text-secondary`. Row-level actions reveal on hover. - **Status indicators:** a small dot/tag for draft vs. published; a dataset glyph when references exist. Status colors only. - **Toasts:** `--layer-02`, 1px border in the support color, square, brief. - **Dialogs (confirm / alert):** centered card on a dimmed backdrop (`rgb(0 0 0 / 0.5)`), `--layer-01` fill, 1px `--border`, minimal overlay shadow, square. A title, a `--text-secondary` message, and a right-aligned action row: **Cancel** (secondary) + the primary, which is a **danger** button (filled `--support-error`, `--on-status` label) for destructive intent. The in-app replacement for `window.confirm`; see [arch 03 → Confirmation & alert dialogs](03-modal-system.md#confirmation--alert-dialogs) for behavior (Carbon transactional rule: backdrop does **not** dismiss; Cancel takes focus for danger). Use a dialog only when a decision is required — non-blocking outcomes are **toasts**. - **Code / editor surfaces:** `--font-mono`, `--layer-01`, generous line-height. --- ## 5. Iconography > Geometry and the accessibility floor are set elsewhere: icons keep Carbon's > rounded 2px geometry and are exempt from `--radius` (§3.4), and every icon-only > control carries an accessible name with meaning never resting on colour alone > ([arch 10 §5–6](10-interaction-and-feedback.md)). This section is the **usage** > contract — _when_ a thing earns an icon, and how the set stays coherent. **Stance: balanced, label-first.** Astrolabe is a tool its user returns to often — the one context where GOV.UK concedes icons earn their place: _"Icons can be more useful in case working systems, where users are familiar with the interface and return to it frequently … In most cases it's still helpful to include a visible text label alongside any icons"_ (GOV.UK, `styles/images`). So we are neither icon-rich (Carbon's default density) nor icon-austere (GOV.UK's public-service default): the default is **text**; an icon is added only when it does real work, and usually _alongside_ the text, not instead of it. ### 5.1 The icon-vs-text decision Apply in order: 1. **Default to text.** If a label alone is clear, ship the label. An icon that only decorates fails principle 1 (Considered) and invites the ambiguity GOV.UK warns of — _"people can understand a single icon to mean different things."_ 2. **Add an icon when it does a job** — one of: speeds scanning of a list/row read repeatedly (dataset marker), signals status/type at a glance (draft dot), or affords a high-frequency action (delete). Carbon's rule holds: _"employ icons sparingly and strategically … to reduce cognitive load."_ 3. **Pair icon + text by default.** In any labelled control, menu item, or row, the icon rides _alongside_ its text — recognition support (NN/g #6), not a replacement for the word. 4. **Icon-only is the exception, and a closed set.** Permitted only for the **universal set** (§5.2) — glyphs whose meaning is unambiguous and which recur everywhere. A new icon-only control is never created ad-hoc; admitting one to the set is a contract change, not a per-component decision. | Form | When | Accessible name | | ----------- | -------------------------------------------------------- | ------------------------------------------------- | | Text only | The default. Label is clear on its own. | The visible text | | Icon + text | Icon aids scanning/status; text stays the primary label. | The visible text; icon `aria-hidden` (decorative) | | Icon only | Universal-set glyph in a space-constrained control. | `aria-label` on the control (APG button pattern) | > Accessible-name mechanics (APG button pattern): a control's name comes from its > text content, or from `aria-label`/`aria-labelledby` when there is none. So an > icon **beside visible text** is `aria-hidden` (the text names it — avoiding the > duplicate screen-reader readout GOV.UK flags); an icon **alone** needs an > `aria-label`. ### 5.2 The icon vocabulary (controlled set) One icon = one meaning, **app-wide** — GOV.UK: _"Do not use a single icon to represent more than one thing."_ The vocabulary is a **uniqueness ledger**, not a hall of fame: every glyph is registered here so the same meaning always reuses its glyph and no glyph is ever repurposed — even a one-off gets a row, so it can't be reused for something else later. The registry lives in code at [`src/app/components/Icon.tsx`](../../src/app/components/Icon.tsx) (the `IconName` union + `GLYPHS` map); this table is its prose mirror. Glyphs are traced from Carbon and drawn `fill: currentColor`. **Core set** — recurring, cross-surface: | Meaning | Carbon glyph | Form | Surfaces | | -------------------- | --------------- | ------------------------ | -------------------------------------------------------------------------------------------- | | Close / dismiss | `Close` (✕) | icon-only ⭐ | `ModalShell`, `Toaster` | | Theme → dark | `Asleep` (moon) | icon-only ⭐ | `ThemeToggle` (shown when light) | | Theme → light | `Light` (sun) | icon-only ⭐ | `ThemeToggle` (shown when dark) | | References a dataset | `DataTable` | icon + text | Library row marker, Linked-datasets list, header **Datasets**, editor **Extract**⁴ | | Add / create-new | `Add` | icon + text → icon-only⁴ | Library "Create New Snippet" (collapses to "+" when the pane is narrow), Datasets "New …" | | Delete | `TrashCan` | icon-only ⭐ (danger) | Library row delete¹ — text "Delete" in the panel² | | Import workspace | `Upload` | icon + text | Header **Import** (a file is brought into the app) | | Export workspace | `Download` | icon + text | Header **Export** (the workspace is written out) | | About / information | `Information` | icon + text | Header **About** | | Revert draft | `Reset` | icon + text → icon-only⁴ | Editor toolbar **Revert** (restore last published) | | Live search | `Search` | icon-in-field⁵ | Library search box (leading magnifier; the input's `aria-label`/placeholder names the field) | | Settings (gear) | `Settings` | icon-only ⭐ | Per-pane settings disclosures (Editor, Preview, Library dates) | | Unpublished draft | (CSS dot) | status-glyph | Library row (paired with a hidden label) | **Pane-toggle set** — the one **custom** sub-family (not single Carbon glyphs): a panel frame with one of three regions filled, where the filled bar's _position_ encodes which pane it toggles. Icon-only by design — position is the meaning — each carrying an `aria-label`: | Meaning | Glyph | Form | Surface | | ------------------- | ------------------------------ | ------------ | ----------------- | | Toggle library pane | panel frame, **left** filled | icon-only ⭐ | `PaneToggleStrip` | | Toggle editor pane | panel frame, **centre** filled | icon-only ⭐ | `PaneToggleStrip` | | Toggle preview pane | panel frame, **right** filled | icon-only ⭐ | `PaneToggleStrip` | **Status set** — Carbon's **filled** notification glyphs, one per severity. Unlike the outline UI set, these are coloured **by status** (not by surrounding text) and are a deliberate _filled_ sub-family. They add a **redundant, non-colour severity channel** (WCAG 1.4.1): meaning never rests on the bar colour alone, and the triangle shape-codes warning apart from the round error/success/info — so severity survives colour-blindness. Used wherever a status is signalled (toasts today; inline notifications/validation as they arrive): | Meaning | Carbon glyph | Colour | Surfaces | | ------- | -------------------- | ----------------------- | ------------------------------------------- | | Error | `ErrorFilled` | `--support-error` | `Toaster` (error) | | Warning | `WarningAltFilled` ▲ | `--support-warning-fg`³ | `Toaster` (warning), Chart Builder warnings | | Success | `CheckmarkFilled` | `--support-success` | `Toaster` (success) | | Info | `InformationFilled` | `--support-info` | `Toaster` (info) | **Scoped set** — single-surface, glyph **reserved** in the ledger but **not yet in the `Icon` registry**: | Meaning | Carbon glyph | Form | Surface / note | | ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Swap / transpose | `ArrowsHorizontal` | icon + text | Chart Builder "Swap X/Y" — the button **ships** today with an interim Unicode `⇄`, not a registry `Icon`. `ArrowsHorizontal` stays reserved here so the meaning is claimed; promote the button to it (add the `IconName` + glyph) when polishing the swap. | ⭐ = **icon-only set** (the glyph alone names the control, via `aria-label`): the **universal** glyphs `close` + `theme`; the conventional disclosure/affordance glyphs `settings` (gear) and the **pane-toggle** trio (position is the meaning); and **delete** as a deliberate destructive-row exception — a dense, repeated list action where a label would cost more than it gives. `search` is _not_ ⭐: its magnifier is a decorative lead-in to a labelled input (footnote ⁵), not a control named by the glyph. ✕ means **close only**; delete is `TrashCan`, never ✕ (that collision is exactly what one-glyph-one-meaning forbids). Admitting a glyph to ⭐ is a contract change (§5.1 rule 4), not a per-component call. ¹ Row delete is hover/focus-revealed and reddens on hover/focus (arch 10 — reveal & destructive-intent rules). ² "Duplicate" and "Delete" in the detail panel stay **text** (label-first; lower frequency, not a dense row). ³ The raw warning yellow fails contrast on light surfaces, so the warning glyph uses `--support-warning-fg` (darkened amber; the yellow `--toast-accent` stays on the decorative border). ⁴ **Responsive collapse**, not membership in the icon-only set: these are icon+text that _shed the label_ under width pressure (the editor toolbar's secondary actions, and the library's standalone Create CTA, when the pane is narrow — [arch 10 §8](10-interaction-and-feedback.md)), keeping the accessible name in `aria-label`/`title`. A degradation that preserves the name is distinct from a permanent icon-only control (§5.1 rule 4), so it isn't a closed-set change. ⁵ **Icon-in-field**: a decorative leading glyph _inside_ a labelled control — the search input's magnifier is `aria-hidden`, and the input itself carries the accessible name. Not icon-only (the control is named by its label, not the glyph). ### 5.3 Size Carbon's icon scale, paired to our type. The tokens are live in `styles/tokens.css`; the `Icon` component's `size` prop selects one: | Token | Size | Pairs with | Use | | ----------- | ---- | ------------------------------ | ------------------------------------------- | | `--icon-sm` | 16px | 14px body (`--font-size-base`) | Default — inline with text, row markers | | `--icon-md` | 20px | 16px text | Slightly emphasised controls (theme toggle) | | `--icon-lg` | 24px | — | When a larger icon is genuinely needed | | `--icon-xl` | 32px | — | Rare; large display only | - _"16px and 20px icons are optimized to feel balanced when paired with 14pt and 16pt IBM Plex"_ (Carbon) → **16px (`sm`) is our default**, since body is 14px. - Use an icon **at its scale** — don't rescale a 16px glyph to 11px or 13px (the old 11px dataset glyph and 18px toggle were the drift this fixed). - Carbon's glyphs are drawn on a 32-unit grid with a built-in stroke weight per size; because we render them **filled** (see §5.4) there is no stroke token to set — sizing the SVG is all that's needed. ### 5.4 Style, colour & alignment - **Geometry & fill:** Carbon's rounded 2px corners, per §3.4. Carbon's UI icons are **filled** shapes (`fill: currentColor`) that read as outlines — _not_ `stroke`-drawn. Our `Icon` primitive draws fill; the size classes set width and height only. (The two original hand-rolls used `stroke`; tracing the real Carbon glyphs moved us to fill.) - **Colour:** monochrome, one colour, **inherits `currentColor`** so it matches its text — Carbon: _"match your icon colour with your text colour … don't use different colours for text and icons"_; must pass contrast. Two sanctioned recolours: **destructive intent** (delete reddens to `--support-error` on hover/focus — arch 10) and the **status sub-family** (§5.2), coloured by severity rather than by text — those are graphical status objects (WCAG 3:1), and warning uses the darkened `--support-warning-fg` so it clears contrast on light surfaces. - **Alignment:** centre-align with adjacent text — never baseline-align (Carbon). - **Sourcing:** Carbon is **not a dependency**; we transcribe the glyph's SVG geometry into `Icon.tsx`'s `GLYPHS` map (Carbon's third-party rule, inverted — a new glyph must be _"visually balanced"_ with the set). Match an existing icon's 32-grid when adding one. - **Hit area:** the interactive target (the button), not the glyph, owns the click size — our 32/40px buttons already clear comfortable targets; never shrink the target down to the icon. ### 5.5 Current state The contract is implemented across the M1–M4 surfaces: - **Infrastructure:** `--icon-*` tokens in `styles/tokens.css`; a shared [`Icon`](../../src/app/components/Icon.tsx) primitive + the `GLYPHS` registry as the single source of truth. Components import `Icon`, never inline an SVG. - **Core set live:** `Close` (ModalShell, Toaster — replacing the bare ✕/×), `Asleep`/`Light` (ThemeToggle, now on-scale), `DataTable` (library row + linked list, replacing the 11px cylinder), `TrashCan` (row delete), `Add` (both "create-new" buttons). The draft dot is unchanged. - **Status set live:** the four filled glyphs (`ErrorFilled` / `WarningAltFilled` / `CheckmarkFilled` / `InformationFilled`) in `Toaster`, coloured by kind; the Chart-Builder warnings reuse `WarningAltFilled` (replacing the old ⚠ character). - **Deferred (scoped set):** only the Chart-Builder `ArrowsHorizontal` (swap-axes) — registered, not built; a one-button polish revisited with the next Chart-Builder pass. No open status thread remains — the status-glyph question is settled here. --- ## 6. Charts (`src/core/vega-themes.ts`) The chart `Config` is themed to match the app, per theme: - `background: transparent` (inherits the surface), Plex font for titles/labels, axis/grid colors derived from the neutral ramp + `--text-secondary`. - **Categorical palette** for `range.category` is part of the _free color_ layer — a distinct, colorblind-sequenced set (Carbon's data-viz palette is a good starting point, but not mandatory). Light and dark variants. This is where expressive color earns its keep. - Config is applied **at embed time**, never baked into the user's stored spec. --- ## 7. Implementation map | Artifact | Role | | -------------------------------------------------- | -------------------------------------------------------------------- | | [`visual-specimen.html`](./visual-specimen.html) | Living preview + token sandbox. Iterate here first | | `styles/tokens.css` | The settled tokens — ported from the specimen in M1.5 | | `styles/base.css` | Font wiring (`@fontsource`), reset, reduced-motion | | `src/app/components/Button.tsx` / `IconButton.tsx` | The shared control primitives (§4) — every action / icon-only button | | component `*.module.css` | Consume tokens only; no raw hexes, no hardcoded hue | | `src/core/vega-themes.ts` | Chart `Config` per theme; categorical palettes | **Order of work:** settle the specimen → port tokens to `tokens.css` → self-host Plex in `base.css` → restyle existing M1 components against the tokens → align `vega-themes.ts`. Verify by rendering the real app, not just the specimen. **What the specimen is (and isn't).** It is the **token sandbox** (try accents, ramps, themes before touching `tokens.css`) and a **catalog of reusable primitives** in both themes — buttons, fields, tabs/status/tags, the library-row pattern, toasts, the overlay dialog, the code surface. It is **kept in sync** with those primitives: when a primitive's canonical look changes or a new one lands (e.g. the confirm dialog), add/update its specimen entry. It does **not** mirror **feature surfaces** — the Datasets / Settings / Chart Builder modals, the editor, the full shell — those are app screens, verified in the running app (headless Chrome, both themes), not catalogued here. That primitive-vs-feature line is what keeps the specimen finite, honest, and worth trusting. --- ## 8. Inspiration sources — where to look for more We treat IBM/Carbon as inspiration, so we mine its **source repos**, not the live doc sites. The sites (`carbondesignsystem.com`, `ibm.com/design/language`) are JS-rendered and don't fetch cleanly — **clone the repo and read it locally instead.** Convention: clone under `/Users/oleh/code/reference/` with `git clone --depth 1 https://github.com/carbon-design-system/.git`. | Need | Repo | Where it lives | | -------------------------------------------------------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Principles / the "why"** (philosophy, 2x grid, color rationale, type, motion, icon geometry) | `design-language-website` | `src/pages/`: `philosophy/principles.mdx`, `2x-grid.mdx`, `color.mdx`, `typography/*.mdx`, `animation/overview.mdx`, `iconography/ui-icons/{design,usage}.mdx` (~1.4 GB clone — image-heavy; the MDX is what we want). Icon **usage** rules (§5) also draw on `carbon-website/src/pages/elements/icons/usage.mdx` + GOV.UK `styles/images/index.md` | | **Token values** (gray/blue ramps, type scale, font families, motion durations/easings, theme role→value maps) | `carbon` | `packages/colors/src/colors.ts`, `packages/type/src/{scale,fontFamily,fontWeight}.ts`, `packages/motion/src/index.ts`, `packages/themes/src/{white,g100}.ts` | | **Component-level usage guidance** | `carbon-website` | `src/pages/**/*.mdx` | | **Data-viz categorical chart palette** (for `vega-themes.ts` `range.category`) | `carbon-charts` | cloned in M1.5 → `packages/core/scss/_color-palette.scss` (the `'14'` pairing, white + g100); token→hex resolved against `carbon` `packages/colors/src/colors.ts` | > The decisions we made _from_ these sources are captured above (§1–7) and in the > specimen, so we don't need to re-derive them — only return to the repos to extend > the research (e.g. the chart palette, or a component pattern we haven't tackled).