Files
astrolabe/docs/architecture/09-visual-design.md
T

38 KiB
Raw Blame History

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 — 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. Consideredremove 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. Executedeverything communicates, including what we leave out. Alignment, rhythm, and empty space are decisions, not leftovers.
  4. Progressiveevery 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:

  1. 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 ~02px (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
Secondary button A dark gray fill ($button-secondary) Outlined (Carbon's tertiary shape) — one filled button per region stays the rule

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 separators / component boundaries (3:1 non-text contrast)
--field-01 / --field-02 (+ --field-hover-*) Field fills: one step off the canvas / off a --layer-01 surface
--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. --border-strong is a component boundary, not decoration — it must hold 3:1 against the surface it bounds (WCAG 1.4.11): gray-50 #8d8d8d light (3.32:1 on --bg, 3.02:1 on --layer-01), gray-60 #6f6f6f dark (3.60:1 on --bg). Carbon's gray-30/gray-70 "strong" values fail this; don't drift back to them.
  • Soft accent is derived, not hardcoded. --accent-soft / --accent-soft-hover are color-mix(in srgb, var(--accent) 1220%, 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's fill is one step off the surface it sits on, alternating like Carbon's field set: --field-01 on the canvas (gray on white / near-black on black), --field-02 on a --layer-01 surface (white on gray / a step lighter on dark). The alternation is what keeps a field visible without a box and prevents three indistinct grays from stacking (canvas → panel → field — the snippet-metadata-panel bug). Mechanism: components consume the contextual --field / --field-hover tokens only; an elevated surface sets --field: var(--field-02) (and the hover variant) once on its container — the same pattern as --control-hover-fill. Setters today: the modal chrome and body (ModalShell), the library metadata panel, the settings popover.

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

  • How a shared look travels. Exactly four mechanisms, in escalating order: design tokens (styles/tokens.css) for values; contextual custom properties set once by a surface and consumed by everything on it (--control-hover-fill, --field); base.css element baselines at zero specificity for looks every instance of an element shares (the focus ring, the field recipe); React primitives (Button, IconButton) when shared behavior or enforced variants justify a component. Nothing else — no CSS-module composes, no utility classes, no mixin layer. A fifth mechanism is drift, even when it's locally cleaner. 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 — 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, 1112px, 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. Fields and select-like triggers are not boxes — they're the quiet-field recipe below (fill + bottom border). A 1px --border-strong box is reserved for the few value-holding controls that need full enclosure: segmented controls, secondary buttons, drop targets (dashed), the color-swatch input. Plain actions are ghost or filled — never outlined boxes; passive chrome (tags, badges, type glyphs) takes --border, never --border-strong. 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.
  • 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), 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 12px). Always visible on keyboard focus — accessibility is non-negotiable (principle 4).
  • Fields (text, textarea, select-trigger, search): the quiet field — square, --field fill (one step off the surface, §3.3), bottom border only in --border-strong, no box; 2px --focus ring hugging the box (offset 2px) on focus. Mono font for spec/JSON inputs. The recipe is declared once in styles/base.css as a zero-specificity element baseline (text-like input types + textarea, excluding Monaco's internal widgets); component modules add only idiosyncrasies — width, padding, font size — never a competing border or fill. Select-like triggers are <button>s the baseline can't reach, so SelectControl/SortControl restate it (they are fields, not buttons: a trigger holds a value); their hover/open fill is --field-hover. Writing border: on an input is the code smell — the field look has exactly one home. The quiet treatment is Carbon's; the boxed alternative (GOV.UK's canon — 2px solid enclosure) is equally legitimate a11y-wise but spends a box on every field, and in a square-chrome editor UI boxes are reserved for the few controls that need full enclosure. A field's boundary is carried by its fill step plus the 3:1 underline.
  • 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 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 §56). 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 (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), 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 M1M4 surfaces:

  • Infrastructure: --icon-* tokens in styles/tokens.css; a shared Icon 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 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/<repo>.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 (§17) 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).