Skip to content

SageOx DESIGN.md

A calm, precise interface for humans and AI working together. Calm · Trustworthy · Technical · Human

This is the canonical, AI-readable design system for all SageOx surfaces. Drop this file (or a reference to it) into any AI coding tool — Claude Code, Cursor, Codex, Stitch, Aider — and the agent gets enough context to produce on-brand UI without further prompting.

Token sources of truth live in tokens/*.yaml; this document flattens them for LLM consumption. If a value here disagrees with tokens/, the YAML wins — reconcile it.

Taste is articulable. Every decision in this document has a reason. If a section is silent on something you care about, that's a gap — fill it in with the reason.


0. Operating philosophy — design for an AI-native company

Read this section first. Every other rule in this document is downstream of it.

SageOx is an AI-native company. The product surface in 2026–2027 grows faster than any designer or product owner can review. Agents will create and ship features that no human reviewer ever sees — not as a failure mode, as the operating assumption. Every choice in this design system is made with that assumption in mind.

The implications:

1. Guidance, not guardrails

A guardrail blocks an action. Guidance shapes the action that's already happening. We name principles, encode them in tokens and primitives, and trust agents to apply them. When agents drift, we tighten the system, not the leash. Design reviews as a blocking gate — once normal — assume a human throughput we no longer have.

2. Pixel-perfect is over

A 2024 designer marked a Figma with padding: 14px and expected engineering to match. By 2026, hundreds of surfaces ship per week and no human will pixel-inspect most of them. The system has to be designed so "agent-generated, within tokens, semantically-classed" produces acceptable visual output by construction. Where pixel exactness still matters — the wordmark, the primary CTA, the device chrome — encode it as a contract or a token, not a memorized convention.

3. Designers are design engineers

The job is no longer "design a screen and hand it off." The job is to build the harness — tokens, components, lint rules, contrast checks, motion budgets, the inline guidance you're reading right now — that lets agents produce quality work autonomously. Plus to ship real features end-to-end yourself. A designer at SageOx writes code that goes to production. There is no translator engineer. If you can't build the system that produces the design at scale, the design doesn't ship.

4. The harness is the product

Every hour spent perfecting one screen is an hour not spent improving the harness that produces ten thousand. The leverage ratio is overwhelming. Polish the system, not the artifact.

5. Exploration before codification

This repo holds the system. sageox-design.netlify.app holds the studio. Dated mockups stress-test ideas in the studio. When an idea proves out, it's codified into DESIGN.md, the tokens, the components — and only then do agents start producing real surfaces against it. The mockups stay as archaeology so future readers see where a rule came from.

What this means in practice

  • Token-conformant beats pixel-conformant. If an agent reaches for --sage-500 instead of #7a8f78, that's a win even if a designer-specced screen used a slightly different shade.
  • The system fails loudly. Contrast checks, token lint rules, motion budgets — those are CI checks, not docs that humans read and remember.
  • Design reviews are spot audits, not gates. Sample agent-produced surfaces at random, find what slipped, codify a new rule. Don't try to review everything before ship.
  • A "design fix" usually means tightening DESIGN.md, not adjusting one screen. If a surface drifted, the system permitted the drift. Fix the system.
  • Mockups are stress tests, not pixel specs. They exist to surface what the system can and can't yet produce — not to be the implementation reference.
  • Tooling is design work. Building a contrast checker, a token linter, a <Surface> primitive that makes the wrong thing hard to type — that is the highest-leverage design work in this company.

How to actually do this job

The day-to-day practice — what tools to use (Claude Design, Claude Code), how to run the two loops (tighten the harness, ship a feature), how to onboard a new design engineer in 30 days, what's settled and what isn't — lives in HARNESS.md. Read it after this section. §0 is what we believe; HARNESS.md is how we work.


1. Voice & Aesthetic

SageOx UI is Apple × Linear × Tufte:

  • Apple — calm, status-oriented. Status lives at the edge (menu bar, not modal). Fewer borders, subtle animation. One primary action per screen.
  • Linear — compact, non-modal, keyboard-first. Tight spacing, minimal visual noise. Metadata collapses by default. Command palette over settings pages.
  • Tufte — data-dense, high information-to-ink ratio. Sparklines and inline visualizations. Remove chartjunk. Small multiples over single complex charts.

Five questions to ask before shipping any screen: 1. Is the user's primary intent immediately actionable? 2. Does one element clearly dominate the visual hierarchy? 3. Is system state visible without interaction? 4. Does every pixel earn its place? 5. Is optional metadata collapsed by default?

Component principles

Five rules for any reusable component, borrowed (with credit) from Emil Kowalski's work on Sonner and Vaul:

  1. Developer experience first. Minimal setup friction. A consumer should be able to add the component in one import and one element.
  2. Good defaults beat options. A component with five props that look great out of the box beats one with fifty props that don't.
  3. Naming creates identity. Memorable > discoverable. Toaster beats NotificationProvider.
  4. Handle edge cases invisibly. Hover during animation, rapid open/close, interrupted gestures — the component absorbs the mess so the consumer doesn't.
  5. Transitions, not keyframes, for dynamic UI. Transitions are interruptible and retarget cleanly; keyframes restart from zero.

2. Color

One brand hue — Sage (primary + action) — on warm earth neutrals: Dust (light surfaces), Silt (text + borders), Crater (dark surfaces). Copper is retired; the functional Warning (earthed gold) ramp is its only accent successor. Use semantic role names below — never hardcode hex.

Brand palette

Family Key stop Role Feeling
Sage 500 #7fa87c Primary surfaces + action (400/500 anchors) Calm, brand
Dust 50–400 #f7f5f2→#e5e2df Light surfaces / bg / borders Warm cream paper
Silt 500 #6b6d60 Text + borders (both modes) Warm mid-grey
Crater 800 #111411 Dark surfaces / bg Green-black
Forest accent #466144 Links (light mode) Depth

Full scales are in tokens/colors.yaml. Notable stops:

  • Sage (50–900): 50 #f1f5ef, 400 #9dc498 (UI/action), 500 #7fa87c (brand-fill), 700 #577a55, 900 #374d36
  • Dust (50–400): 50 #f7f5f2, 100 #f4f2ef, 200 #f2f0ec, 300 #ebe8e5, 400 #e5e2df
  • Silt (50–900): 300 #b7b6a3, 400 #8c8e7e, 500 #6b6d60, 600 #4a4d42, 900 #161812
  • Crater (500–900): 500 #1d221d, 700 #171b17, 800 #111411, 900 #0b0d0b

Semantic roles (use these, not raw scales)

Role Light Dark Meaning
surface.quiet dust-100 crater-900 Resting background
surface.selected dust-200 crater-700 Active/toggled element
surface.elevated dust-50 crater-600 Hover, subtle lift
surface.card white #ffffff crater-700 Card fill — lifts above chrome (see note)
text.primary silt-900 dust-100 Headlines, body
text.muted silt-600 sage-400 Secondary content
text.quiet silt-500 silt-500 Hints, placeholders
accent.intent sage-500 sage-400 CTAs
border.subtle dust-300 crater-600 Dividers
border.default dust-400 crater-500 Inputs
border.focus sage-500 sage-400 Focus rings
signal.success success-500 #57a07a success-400 #6fbe94 Operation completed
signal.warning warning-500 #c6a23c warning-400 #d9b654 Attention recommended
signal.danger error-500 #bd5b49 error-400 #d77e6c Error / destructive
signal.info info-500 #6e8ba0 info-400 #7e98a8 Neutral information

Light-mode depth = stark white cards on warm cream. In light mode the page canvas and chrome (sidebars, top bar, popovers) are warm cream Dust (surface.1 = dust-50 #f7f5f2); cards lift above them on pure white (surface.card = --ox-white #ffffff). The cream↔white step is the primary elevation cue — cards read as crisp sheets on paper, not a near-invisible cream-on-cream layer. surface.card is the only role that resolves to --ox-white, and it is light-only — in dark mode it flips to crater-700 (#171b17), one step lighter than the crater-800 chrome, keeping the same lift without the halation of a bright dark-mode surface. Chrome stays on surface.1; reach for surface.card only on content cards.

Specialized semantic families

  • Progress (slate-violet #5c68a8) — long-running async work, agent identity.
  • Public (teal #3e8e84) — content that is publicly accessible.
  • Admin (plum #8b6b91) — corporate admin features only. Sparingly.

Color rules

  1. One Sage CTA per view. Sage is the action hue — multiplying the primary action dilutes it. Light mode is a white label on deep sage-700 (~4.6:1); white on lighter sage fails AA, so the light CTA goes deep. Dark mode flips to a dark label (crater-800) on light sage-400 (DDR-006).
  2. Dust for light surfaces; Crater for dark surfaces; Silt for text + borders. Never invert.
  3. Brand colors never carry error or warning meaning — at any weight. Use the semantic error / warning palette. On light, warning-500 gold is a fill only (use warning-700 for text/borders), and severity is never color-only — pair with an icon/label (DDR-006).
  4. Text contrast ≥ WCAG AA. text.muted (silt-600) is the floor for body copy on light surfaces.
  5. Dark mode is a designed theme, not an inversion — and a preference we serve, not a performance win we claim. Default reading-heavy surfaces (docs, transcripts, long-form) to light/respect-OS; default dense tool surfaces (app shell, cast control, code) to dark — the 2024–2026 research shows positive polarity reads faster (especially small text) while dark suits dense analysis (DDR-008). Respect prefers-color-scheme; never force a mode.

Dark-mode tone discipline

Near-black surfaces, never pure black (#000 can't show elevation, smears on OLED, maximizes halation). The current base is the green-black rebrand (--ox-surface-1 #111411 over ambient --bg-default #0b0d0b); elevation steps lighter (surface-1 → 2 → 3), not via shadow. Body text is off-white dust-100 #f4f2ef, never pure white (21:1 white-on-black is the astigmatic-halation trap). Desaturate accents/syntax and cap surface chroma (the rebrand fixes OKLCH hue 145 at chroma ≤ 0.015) — saturated hues vibrate on dark and miss AA. Hue is a free brand axis above that floor; luminance contrast is what the research pins. Each mode clears AA independently (DDR-008, DDR-006).


3. Typography

Fonts, weights, role names, and the typographic principles are shared across every surface. Type sizes are surface-scoped — see DDR-011 and the note under the scale below.

Fonts

  • Headings (display, H1, H2): Hedvig Letters Serif (weight 400 only — single-weight display face; never bold it — see the anti-pattern below).
  • Body & UI (incl. H3): Inter, weights 400 / 500 / 600 / 700.
  • Code & data: Spline Sans Mono → ui-monospace → SFMono-Regular → Menlo → monospace.

Full fallback stacks:

  • sans: var(--font-inter), "Inter", system-ui, -apple-system, sans-serif
  • heading/serif: var(--font-hedvig), "Hedvig Letters Serif", Georgia, serif
  • mono: var(--font-spline-mono), "Spline Sans Mono", ui-monospace, SFMono-Regular, Menlo, monospace

Type scale (use semantic names)

The role names below are canonical across surfaces. The px sizes are the web scale — see the surface-scoped note after the table.

Token Font Size (web) Line-height Weight Letter-spacing Use
display Hedvig 36px 1.1 400 -0.02em Hero headlines
h1 Hedvig 24px 1.25 400 -0.015em Page titles
h2 Hedvig 20px 1.25 400 normal Section headers
h3 Inter 16px 1.25 600 normal Card titles
body Inter 14px 1.5 400 normal Default body
small Inter 12px 1.5 400 normal Captions, metadata
tiny Inter 10px 1.5 500 0.05em Uppercase labels, badges

Type sizes are surface-scoped, not universal. The px column above is the web scale. A 10-foot cast TV and a 360px Dot device bind their own size values to these same role names — they do not inherit the web px values. Only the role names, fonts, weights, line-heights, and letter-spacing are shared. See DDR-011 and tokens/typography.yaml (roles = shared; web.sizes = the web binding).

Responsive bumps (≥640px / ≥1024px) apply to the web 4xl–6xl raw scales — see tokens/typography.yaml web.responsive.

Anti-pattern: never weight a single-weight display face

Hedvig Letters Serif ships ONE master — weight 400. There is no bold cut. Applying any font-weight above 400 — Tailwind font-bold (700), font-semibold (600), font-medium (500) — does not load a heavier face. The browser synthesizes faux bold by thickening the 400 outline, which renders smeared and wrong. (This is the "Hedvig renders semibold everywhere" bug, fixed across 58 sites in sageox-monorepo, June 2026.)

Wrong Right
class="font-heading text-2xl font-bold" class="font-heading text-2xl"
class="font-heading font-semibold" .heading-1 / .heading-2 / .heading-display preset (bakes in 400 + tracking)

The rule generalizes: a single-weight display face is never given a weight utility — this binds any future single-master display font, not only Hedvig. Hierarchy on a display face comes from size and tracking (the .heading-* presets), never weight. Need a heavier display look? That's a different face with a real bold cut, recorded as a token — not a weight utility on a single-master font.

Runtime backstop (defense-in-depth). Product code sets font-synthesis-weight: none on .font-heading and :where(h1, h2) (sageox-monorepo apps/web/src/index.css), so a stray font-bold renders honest 400 instead of faux bold. Any surface binding the heading face should do the same — but the backstop only neutralizes the symptom; this docs rule is the primary guard. (DDR-016)

Type rules

  • Reading vs scanning. Body 14px / line-height 1.5 for comfort. Tables use font-variant-numeric: tabular-nums for alignment.
  • Human vs system. Inter for prose; mono for code, IDs, timestamps, anything machine-generated.
  • Hierarchy through size + weight, not color. Don't use color to imply hierarchy — that's the semantic palette's job.
  • Line length capped at ~65 characters for body copy. Beyond that, the eye loses its place on the return sweep. Use max-width: 65ch on prose containers.
  • Use (U+2026) for truncation, not three periods. A single character; aligns and kerns correctly. text-overflow: ellipsis produces this automatically — use it.
  • Underlines are reserved for links. For emphasis in prose, use weight or color. Underlining non-link text trains users to expect a click that isn't there.
  • Tabular numerics for any aligned numeric column. font-variant-numeric: tabular-nums on prices, metrics, timestamps, counts — anything where columns of digits need to line up.
  • Loosen letter-spacing on uppercase. Default tracking is tuned for mixed case; uppercase looks cramped without it. The tiny token already includes 0.05em — match that on any other uppercase label.

4. Spacing, Radius & Elevation

Spacing scale (4px baseline grid)

Token Value Use
space-1 2px Micro
space-2 4px Tight
space-3 8px Related elements
space-4 12px Component padding (button, input)
space-5 16px Card padding, list items
space-6 24px Section gap
space-7 32px Large section gap
space-8 48px Page sections, hero
space-9 64px Hero
space-10 80px Major breaks

Radius

xs 2 · sm 4 · md 6 (default) · lg 8 (cards) · xl 12 · 2xl 16 (modals) · 3xl 24 (hero) · full 9999 (pills, avatars).

Elevation (shadows)

Tier Light Dark Use
subtle 0 1px 2px rgba(0,0,0,.04) 0 1px 2px rgba(0,0,0,.3) Hover, minimal depth
elevated 0 4px 8px rgba(0,0,0,.08) 0 4px 8px rgba(0,0,0,.5) Cards, dropdowns
dramatic 0 16px 32px rgba(0,0,0,.12) 0 16px 32px rgba(0,0,0,.7) Modals, popovers

Layout

  • Header: 64px. Sidebar: 240px expanded / 64px collapsed.
  • Container breakpoints: sm 640 · md 768 · lg 1024 · xl 1280 · 2xl 1536.
  • Z-index: dropdown 1000 · sticky 1100 · modal 1400 · popover 1500 · tooltip 1600 · toast 1700.

5. Motion

"Subtle fade + 2–4px Y-translate. No bouncy transitions."

Tier Duration Use
Instant 50ms State changes
Fast 150ms Hover, focus
Normal 250ms Panel reveals
Slow 350ms Modal animations
Slower 500ms Full-screen transitions (rare)

Default easing: cubic-bezier(0.4, 0, 0.2, 1) (ease-in-out). Use ease-out for exits. ease-spring (cubic-bezier(0.175, 0.885, 0.32, 1.275)) reserved for moments of delight — rare.

Motion rule: if a transition longer than 150ms doesn't communicate something specific (state change, spatial relationship, attention shift), shorten or remove it.

Motion craft

Detailed motion guidance — easing-curve decisions, duration tables per element type, spring vs CSS-transition trade-offs, clip-path patterns, gesture/drag rules, @starting-style, performance — is governed by emilkowalski/skill. Consuming repos install it at .claude/skills/emil-design-eng/ and layer SageOx-specifics in .claude/rules/motion-sageox.md.

Do not duplicate motion craft rules here. This section sets voice and the duration tiers; the skill handles the rest. If the skill and this file disagree on a value, the skill wins for craft; this file wins for voice ("no bouncy", "subtle fade + Y-translate").


6. Components (defaults)

Concrete defaults so an agent can build a screen without re-deriving rules.

Buttons

Contrast (DDR-006): the light-mode Sage CTA is white text on deep sage-700 (~4.6:1) — white on lighter sage (400/500) fails AA, so the CTA goes deep rather than mid. Dark mode inverts: a dark label (crater-800) on light sage-400. The earthed-gold accent stays a dark label on gold, never white. Focus ring is ≥ 3:1 against the button.

  • Primary (Sage): light bg sage-700, hover/active sage-800, text white (~4.6:1). Dark mode: bg sage-400, hover sage-300, text crater-800. Padding 12px 16px, radius md, weight 500.
  • Secondary (earthed gold), dark label: bg warning-500, hover warning-600, text crater-800. Dark: bg warning-400, hover warning-300, text crater-900. This is the premium-accent role — at most one per view.
  • Ghost: transparent bg, text text.primary, hover bg interaction.hover (rgba(0,0,0,.04) light / rgba(255,255,255,.04) dark).
  • Disabled: opacity 0.4, no hover.

Font weight per variant

Variant Weight
Primary, Secondary 500 (font-medium)
Ghost, Link 400

Do not bump primary CTAs to 600 — it reads shouty against our calm voice. Inter at 500 on a saturated Sage background already carries the weight needed.

Size hierarchy

Size Height Padding Use
sm 32px 8px 12px Toolbar, dense lists, inline actions
md (default) 40px 12px 16px App default
lg 48px 12px 24px Marketing CTAs, hero

Hierarchy on a single screen is communicated by variant + color, not by size inflation. Don't make the primary button taller to "make it more obvious" — that breaks the Linear-compact register. Reserve lg for marketing surfaces.

CTA copy

Button text should complete the sentence "I want to ___."

Avoid Use
Submit Create Account
Click Here Start Free Trial
Continue See Pricing
Learn More Send Me Weekly Tips
Get Started Download the Guide

Verb-led benefit copy. Title Case. No question marks, no exclamation points. The label is a micro-promise — the more concrete the promise, the lower the click cost.

CTA context (supporting micro-copy)

The button isn't an island. The copy around it shapes the cognitive cost of acting. Three patterns:

Pattern Example
Social proof "Join 12,000+ developers"
Risk removal "No credit card required · Cancel anytime"
Time commitment "Setup takes 2 minutes"
Surface Use micro-copy?
Marketing pages, signup, upgrade flows Required — at least one of the three patterns near the primary CTA
In-app conversion moments (paywall, invite team, connect integration) Encouraged
Mid-task buttons (form submits inside an existing flow) Avoid — becomes noise; the user already committed to the task

Marketing vs in-app surfaces

Surface Default size Padding Supporting micro-copy
Marketing (www, hero, pricing) lg 12px 24px Required
In-app (signup, settings, modals) md 12px 16px Context-dependent
Dense lists, toolbars sm 8px 12px None

Hit-area minimum (44×44)

Visual buttons smaller than 44×44 px (icon-only buttons, compact toolbar actions) must extend the tap target to 44×44 px via a pseudo-element. The visual stays compact; the target meets Apple HIG and WCAG 2.5.5.

button.icon-only {
  position: relative;
}
button.icon-only::before {
  content: '';
  position: absolute;
  inset: 50%;
  width: 44px;
  height: 44px;
  transform: translate(-50%, -50%);
}

Inputs

  • Background: surface.quiet. Border: border.default. Focus ring: 2px border.focus (sage) at 0 0 0 2px outside. Padding 12px. Radius md. Body type, text.primary.
  • Error state: border + focus ring use signal.danger. Error text in small size, signal.danger.

Cards

  • Background surface.selected (light: dust-200; dark: crater-700). Radius lg. Padding space-5 (16px). Shadow subtle at rest, elevated on hover (only if interactive).

Status & loading

  • Inline status preferred over modals. Use small text + a colored dot (8px circle in signal.*).
  • Skeletons mirror final layout — same dimensions, surface.elevated background, fade pulse at 1500ms linear infinite.
  • Spinners only for indeterminate < 1s waits. Longer waits → progress + label.
  • See loading-processing-and-status.md for the full state taxonomy.

Lists & tables

  • See lint/ux-lists-and-pagination.md — must pass before merge.
  • Default to load-more over pagination. Tabular nums in numeric columns. Row hover interaction.hover. Selected row surface.selected.

Diagrams

  • Node radius 8px, min-width 160px, padding 12px, gap 24px. Edge stroke 2px (3px thick). Glow blur 12px, opacity 0.4. Level gap 80px. See tokens/spacing.yaml diagram.

Data visualization (Tufte)

Realizes the Tufte pillar (§1): data-dense, inline, chartjunk-free. Primitives, smallest first:

  • Sparkline — word-sized SVG trendline, 1.5px stroke (accent.primary), optional last-point dot. Variants: sparkbar (word-sized bars) and range band (min/max area under the line at 0.1 opacity). Lives inline in text, table cells, and list rows.
  • Inline metric — value plus a signed trend arrow (↑/↓ in signal.success / signal.danger), tabular nums. Tufte progress — a 20×1 bar (accent.primary on surface-2) plus value/max, inline in a sentence.
  • Metric grid — small multiples: one card per metric (label · value · change · sparkline), space-4 gap. Prefer the same chart repeated across entities over one complex chart.
  • Data-dense table — sparkline column + tabular-nums value + signed change; builds on Lists & tables above.
  • Calendar heatmap — single-hue intensity ramp (success); dot plot and horizon chart for compact many-series comparison.

Do: strip chartjunk (no gridlines, borders, or legends); label series directly, not via a legend (Rule 17); always show context (prior value or baseline); keep graphics inline with text. Render SVG for sparklines and small charts, canvas past ~1000 points; memoize scales and lazy-load off-screen. Every viz ships a non-visual channel — see §11 Rule 18.

Glass & blur (use sparingly)

  • Glass surface: rgba(255,255,255,.8) light / rgba(23,25,29,.8) dark, backdrop-filter: blur(12px). Reserved for floating panels over rich content.

7. Per-platform notes

The token system is multi-target. Surface-specific guidance:

  • Web (platforms/web/) — CSS custom properties, :root for light, .dark / [data-theme="dark"] for dark. Tailwind config available. Consume via @import '@sageox/design/css/tokens.css'.
  • React Native (platforms/react-native/) — TS theme objects. Use StyleSheet.create(); no inline styles. SafeAreaView always.
  • CLI / TUI (platforms/cli/) — adaptive Lipgloss colors with light/dark variants and ANSI 256 fallbacks. See tokens/colors.yaml cli.adaptive and cli.ansi. Tokyo Night-inspired terminal palette.
  • GitLab (platforms/gitlab/) — markdown / wiki theming.

Cross-platform invariant: token names are stable; values may differ per surface (e.g., primary is sage #4f6a48 light / #7a8f78 dark in CLI to maintain contrast on terminal backgrounds).


8. Brand assets

  • Wordmark + Icon never appear together. Pick one per surface.
  • Mascot (friendly ox) is for community-facing contexts only — social avatars, bot identities, playful empty states. Not for product chrome.
  • All logos in brand/logos/ with light/dark variants at 128/256/512px PNG and SVG.
  • File conventions: kebab-case names; SVG for vector; PNG at 1x/2x/4x for raster.

8a. Icons

Icons live in icons/ (peer of brand/ — third-party brand marks and generic glyphs are NOT SageOx-brand artifacts and don't belong in brand/). See icons/README.md for the full posture; the table below is the at-a-glance license + sourcing summary.

Source License Used for Vendored as
Pictogrammers MDI Apache-2.0 Generic glyphs (chevrons, status, abstract shapes) icons/glyphs/<name>/icon.svg
Simple Icons CC0-1.0 (marks retain trademark) Vendor brand marks where available icons/brands/<slug>/icon.svg
Home Assistant brands CC-BY-4.0 (marks retain trademark) Brand marks Simple Icons can't carry (AWS, Linode, …) icons/brands/<slug>/icon.png
SageOx custom MIT Bespoke marks (bubbles, future) icons/custom/<name>/icon.svg

Vendor only what we use. Don't bulk-import upstream catalogs — every entry is recorded in icons/manifest.yaml with upstream_url, upstream_sha, and license. icons/scripts/add-brand.ts pins the upstream commit SHA so vendored marks are reproducible and auditable.

Sizing grid (non-negotiable): authored on 24×24 viewBox, 22×22 live area; rendered at 16/20/24/32 px; 44×44 hit targets come from container padding, never icon scaling. icons/scripts/lint-svg.ts rejects any SVG whose viewBox isn't 0 0 24 24 (custom + glyphs; brand marks are upstream-vendored and exempt from grid lint).

<BrandChip> rule. Multi-tone brand marks render inside a fixed-size rounded-square chip (20px icon in 28px chip on surface-2). The chip normalizes mismatched marks (varying stroke weight, perspective, color) into a coherent grid. Glyphs and SageOx customs render bare on currentColor. Brand marks in mono variant may render bare in dense surfaces (sidebar, command palette). BrandChip ships from packages/ui (web) and src/components/BrandChip.tsx (sageox-app).

Adding / pulling / publishing:

# Author a new SageOx custom (in sageox-design):
cp icons/TEMPLATE.svg icons/custom/<name>/icon.svg

# Vendor an upstream brand (auto-detects simpleicons → ha-brands):
pnpm exec sageox-icons add-brand cursor

# Push from sageox-design to every sibling consumer (`/design-sync` skill):
npm run icons:push

# Pull from consumer side (`/design-pull` skill in sageox-mono, sageox-app):
pnpm exec sageox-icons sync

Catalog page: icons/catalog/index.html (auto-regenerated by npm run icons:render-catalog; renders every vendored entry with a size matrix + light/dark toggle + copy-paste import path).


9. Don'ts

Avoid Instead
Multiple primary (Sage) CTAs in one view One Sage CTA, rest neutral / ghost
Sage text on Sage background text.primary (silt-900) for contrast
Crater as a light-mode surface Dust scale for light surfaces
Brand colors for errors or warnings (at any weight — fills, borders, stripes) signal.danger / signal.warning (DDR-006)
Amber warn-500 as warning text or a thin border on white (2.1:1 — fails AA) warn-700 for text/borders on white; reserve warn-500 for fills (dark text on amber = 8.6:1) (DDR-006)
Severity conveyed by color alone Pair the signal.* color with an icon or label — color + shape redundancy (§11 Rule 3)
Weight utility on a single-weight display face (font-heading + font-bold/font-semibold/font-medium) → faux bold font-heading at 400, or a .heading-* preset; font-synthesis-weight: none as backstop (DDR-016)
All text same weight + color Use the full type scale and text.* roles
Modals for status Inline status with a signal.* dot
Decorative gradients Solid color or one purposeful gradient (hero only)
Bouncy / overshooting transitions Subtle fade + 2–4px Y-translate, ≤150ms for hover
Settings buried behind gear icons for critical-path features Inline controls
Forms that feel like paperwork Task-completion surfaces, progressive disclosure
Decorative icons next to every label Icons earn their place by adding scannability
Reinventing patterns the ecosystem already solved Search first; see AGENTS.md "Search Before Building"
Generic CTA copy ("Submit", "Click Here", "Continue") Verb-led benefit copy ("Start Free Trial", "Create Account")
Underlining non-link text for emphasis Weight or color; underlines mean "this is a link"
Hard-coded ... as ellipsis (U+2026) — single character, kerns correctly
Sub-44px tap targets on touch surfaces Extend hit-area via pseudo-element; keep visual compact
Duplicating motion-craft rules across repos Defer to emilkowalski/skill + repo overlay

10. Where the truth lives

Thing Source
Color, typography, spacing, motion values tokens/*.yaml
Brand voice, hero examples README.md, brand/typography.md
Loading / async / status taxonomy loading-processing-and-status.md
List / pagination rules lint/ux-lists-and-pagination.md
Per-platform output platforms/{web,react-native,cli,gitlab}/
Light/dark theme rationale & tone discipline DDR-008, usage/light-dark-themes-and-developer-preference.md
Calm mode — density/attention preference, coverage taxonomy, must-never list DDR-009
Architecture, build, publishing DEVELOPMENT.md

When this DESIGN.md disagrees with tokens/*.yaml, the YAML wins. This file is a flattened view for AI consumption — the YAML is canonical.


11. Cross-cutting design rules

Thirteen system-level rules that apply across surfaces — not specific to any one component or family. Rules 1–8 were codified during the KB V2 iteration; rules 9–13 during the May-2026 Conductor-themes review (see archaeology in §13). They govern any UI in this system. When a component-level decision conflicts with one of these, the rule wins.

1. Plain-language hierarchy

Rationale: Technical labels compete with readable titles for visual attention and force users to parse two vocabularies at once.

Do: 20px title (h2-scale, demoted from h1), baseline-aligned avatar, type badge demoted to slug line at 10.5px (tiny token, muted foreground).

Anti-pattern: ALL-CAPS TYPE BADGES adjacent to page titles; type identifiers rendered at the same visual weight as the title.

2. Three-layer motion policy

Rationale: Users have different sensitivity to motion; a single toggle is not enough. Layering OS preference, app mode, and loop budget gives each user the level of motion they can tolerate.

Do: Layer 1 — OS prefers-reduced-motion zeroes all CSS transitions and @keyframes. Layer 2 — SageOx Calm mode reduces layout density, hides optional metadata, and kills ambient/decorative motion while preserving consent/critical-state motion (DDR-009). Layer 3 — any looping animation (spinner, pulse, shimmer) must pause or stop after 3 cycles.

Anti-pattern: transition: all; bouncy/overshooting springs (cubic-bezier with overshoot); animations that run indefinitely while the user waits for a slow operation.

3. Color + shape redundancy

Rationale: Color-only signals are inaccessible to color-blind users and invisible in high-contrast or monochrome environments.

Do: Pair every color signal with a secondary differentiator — shape for dots, text for badges, pattern or texture for charts. Example: murmur freshness uses three states (pulsing filled dot = live, solid dot = recent, hollow ring = stale) so color and shape both encode freshness.

Anti-pattern: Green = success / red = failure with no other distinguishing signal; signal dots that differ only by hue.

4. Power-on-demand

Rationale: Keyboard shortcuts accelerate expert users but must not hide functionality from users who have not yet discovered them.

Do: Every keyboard shortcut must have a visible button equivalent discoverable without documentation. Advanced or infrequently-used affordances appear on hover or interaction rather than at rest. Example: ⌘J opens chat, but an "Ask" pill button is always visible for users who have not learned the shortcut.

Anti-pattern: Keyboard-only features with no visible entry point; settings buried behind gear icons when the setting is on the critical path.

5. Contrast lock (4.5:1 enforced)

Rationale: Text contrast degrades silently when semantic tokens are applied across surfaces without checking the combination, especially in dark mode.

Do: Every text token must meet WCAG AA 4.5:1 against its declared host surface. Surface hierarchy is bg < surface-1 < surface-2 < surface-3; muted/subtle text tokens are calibrated for surface-1 only. When placing text on a non-default surface, verify the combination with the contrast-check.html utility before shipping.

Anti-pattern: text-subtle on surface-2 in dark mode without a contrast check; using raw palette stops instead of semantic tokens (raw stops do not track surface changes).

6. "Doesn't exist yet" pattern

Rationale: Blank space or hidden sections leave users wondering whether content is missing, loading, or intentionally absent.

Do: Dashed border (border-dashed, border-subtle) with a muted interior background. Sage-700 icon (not gold, not danger) paired with descriptive text explaining what will appear here. Optional CTA uses a quiet sage-tint variant — reserve the full sage CTA for the page-level primary action.

Anti-pattern: Blank white space with no indication; "null" or "undefined" visible to users; hiding the section entirely until content exists (removes the user's mental model of what the surface does).

7. Every surface ships a live, agent-iterable design catalog

Rationale: In an AI-native company the people most likely to be looking at any given component on any given day are coding agents, not humans. An agent improving a component, fixing visual drift, or learning the system can only do its job if the live component catalog is one route away. Hand-curated documentation goes stale; a route that renders the actual registry never does. This is also the only honest mechanism for "ralph loops" — an agent iterating on a component while continuously checking its real rendered output.

Freshness + send-scoped coverage (DDR-007): the snapshot must not silently lag the product. The source repo regenerates-and-PRs each snapshot on change, every snapshot carries source provenance (PROVENANCE.yaml), and coverage extends (Phase 2) to every email-sending service, not just apps/web.

Do: Every surface ships a /internal/design-catalog (web), <cli> dev catalog (terminal), or equivalent route that renders every reusable component from the live registry, in both modes (dark/light, calm/climax), with the source path inlined next to each component. The catalog must be reachable from inside the running product, in dev environments at minimum. Every surface's publish-design-catalog.sh exports this same catalog as a self-contained snapshot to catalog/<surface>/ in the sageox-design repo so the snapshot is browsable at sageox-design.netlify.app/catalog/<surface>/.

Standard route conventions:

Surface type Live route Snapshot
Web app (authenticated console) /internal/design-catalog catalog/console/index.html
Web marketing site (public) /internal/design-catalog (marketing host) catalog/marketing/index.html
CLI / TUI <cli> dev catalog (and freeze SVG export) catalog/<surface>/index.html
Embedded device LVGL headless render, PNGs per screen catalog/<surface>/index.html
Mobile internal://design-catalog deep link catalog/<surface>/index.html
Email <service>/internal/email-catalog route on the sending service catalog/email/index.html (template HTML + per-client screenshots)

The console and the marketing site are distinct surfaces. They share tokens, brand language, and the philosophy in §0, but they serve different audiences and use different pattern libraries. Each gets its own catalog so marketing-only patterns (hero, pricing tiers, social proof, footer CTAs) don't leak into product code, and product-only patterns (data tables, modals, command palette) don't leak into the marketing site.

Contract: the full publishing contract (registry, build, push, coverage lint, chrome, reading order, script header) lives in catalog/README.md. New surfaces must satisfy it before the surface is considered "in the system." Two contract elements worth surfacing here because agents miss them most: catalogs render Surfaces → Components → Tokens in that order (real user flows first; primitives second), and every publish script starts with a header comment pointing at catalog/README.md so future edits don't silently drift the contract. See HARNESS.md Part 2 · Harness layers where catalog is named as one of the load-bearing layers.

Anti-pattern: "Components are documented in Figma / Storybook / a wiki." Storybook is fine as the underlying renderer, but the canonical entry point must be a route on the running surface so agents can find it without out-of-band knowledge. Documentation that drifts from the live registry is worse than no documentation — it lies to both humans and agents about what exists.

8. Marking actions — Favorites and Pins

Rationale: Conflating per-user curation (Favorite) with team-scoped surfacing (Pin) creates new-user surprise and locks vocabulary in a way that's expensive to undo later. Brigham's taxonomy and Slack / Linear / Teams precedent both converge on the same split.

Do: ⭐ Star icon = Favorite (per-user, "this matters to me"). 📌 Thumbtack icon = Pin (team-scoped, "this matters to the team"). Never both icons on the same item. Render pattern is surface-specific and enumerated (not designer's-choice) — see DDR-001.

Anti-pattern: Calling per-user marking "Pin"; introducing a "Bookmark" verb meaning "save into a bubble" (Bookmark is the reserved icon for the future Save-for-Later primitive — bubble membership is a domain action, not an overlay).

Full spec: DDR-001 — vocabulary, scope matrix, render patterns, the "what / why / for whom" gate, Zeigarnik warning for future SfL primitive.

9. Status at the edge, not in modals

Rationale: Modal dialogs interrupt the user's task to report status that could be ambient. Apple's menu-bar pattern and Linear's tray pattern prove that a small, always-visible signal at the periphery outperforms a center-stage dialog that demands attention. Modals are for decisions, not for facts.

Do: Render system state in the sidebar, in a corner indicator, in a row's leading icon, on a title's font-weight, or as a small badge — places the eye already passes through. Reserve modals for irreversible or branching decisions the user must answer before the surface can proceed. State that resolves on its own (sync, processing, ready, failed-but-recoverable) belongs at the edge. Title bolding for "awaiting input" is the lightest viable edge signal — use it for thread/workspace lists where the user is the bottleneck.

Anti-pattern: A modal that says "Processing complete." A toast that demands acknowledgement. A "results" dialog the user must close before resuming work. A status field that requires opening a detail panel to see.

10. Hover-reveal for secondary metadata

Rationale: Information density and scannability are in tension. Hover-reveal collapses that tension: the primary view stays clean and uniform; depth is one cursor-over away. Distinct from Rule 4 (Power-on-demand) which governs features; this rule governs information — the secondary attributes of an item already visible.

Do: On any dense surface (list rows, card grids, sidebar entries), the primary cell shows identity and the single most important status. Secondary metadata — timestamps, author, sub-counts, file paths, last-action — appears on hover via a tooltip or inline popover. Standard timing: --hover-delay-in: 150ms, --hover-delay-out: 75ms. Touch surfaces use long-press or a chevron toggle. The reveal augments the primary content; it does not replace it.

Anti-pattern: Cramming six metadata fields into every list row "in case the user wants them." Hover-reveal that replaces the primary content rather than augmenting it (the user loses their place). A hover popover that requires a click to dismiss.

11. Self-healing UI: every failure offers the next move

Rationale: A failed operation that just shows an error message turns the user into a debugger. A failed operation that offers "Investigate," "Retry," or "Ignore and proceed" keeps the user in flow and turns the error into an affordance. SageOx is AI-native — most failures have an AI-fix path that costs pennies and saves minutes.

Do: Every error toast, failed pipeline indicator, and empty-because-broken state ships with a concrete next-step button. Preferred verbs: Investigate (drops the user into a coworker chat pre-loaded with the failing artifact + logs), Retry, Skip and continue, Delete anyway (for archive failures). Failures expose the failing input so the user can edit-and-retry without re-doing upstream work. Pairs with docs/usage/loading-processing-and-status.md — error and loading are the same UX family, not separate ones.

Anti-pattern: "An error occurred. Please try again later." A failed setup script that just turns red. An archive button that fails silently and leaves the workspace in a half-archived state. A pipeline stage that fails with no path back into a coworker chat about why.

12. Keyboard-driven sequential review

Rationale: Users reviewing a set of things — comments, findings, transcript chapters, KB threads, security results, PR-comment runs — should not have to mouse between items. A single keystroke meaning "next item I owe attention to" turns a 30-minute mouse-driven review into a 5-minute keyboard-driven sweep. Conductor's ⌘⌥J (next comment) and ⌃V (mark viewed and advance) are the proven references.

Do: Any surface that lists N items the user iterates through ships a "next-attention" keybinding. Defaults: J / K for next/prior when not ambiguous, ⌘⌥J / ⌘⌥K when a single key would collide. The keybinding skips items that don't need attention; ordering matches the visual order. The current item gets a focus ring; the surface auto-scrolls to keep it in view. Wrap-around is announced explicitly ("Back to the top" toast or focus ring flash) so the user knows they finished. Pair with Rule 4 — every keybinding has a visible "Next" button discoverable without docs.

Anti-pattern: Forcing the user to click each comment, expand it, dismiss it, scroll to the next. Sequential keybindings that silently wrap to the top (the user can't tell they finished). "Next" that includes items the user already resolved.

13. Periodic typography & density audits

Rationale: Typography and density drift silently. A new component lands at the wrong heading size; a feature ships at row-height +2px to make space for a badge; a docs page picks up the marketing site's looser line-height. Individually invisible, collectively the surface "feels heavier" six months later. Conductor explicitly revisited typography at 0.33 (heading size, table proportions, vertical spacing) and capped chat-overview bars at 50 turns at 0.36.1 — the lesson is that periodic pruning is part of the system, not a one-time setup.

Do: Every two minor releases of @sageox/design (≈ quarterly), run a typography + density audit against the live /internal/design-catalog for each surface. Audit checklist: heading scale still monotonic across pages; row heights match their content density (no padding inflation); table column widths in proportion; line-heights from the same family on the same surface; no surface accidentally inheriting prose line-height from documentation styles; no single-weight display face carries a weight utility — font-heading + any font-bold/font-semibold/font-medium is faux bold and a regression (DDR-016). File findings as a decisions/<NNN>-audit-<date>.md short DDR — even if the conclusion is "no change needed," the audit happened and the next reviewer has a baseline.

Anti-pattern: Treating typography as a one-shot setup. Letting heading sizes accumulate (h2 redefined three different ways across pages). Inflating row heights to accommodate "just one more" piece of metadata. Capping density caps (e.g., "show all N turns") because the cap "felt restrictive" without measuring what changed.

14. Honest promotion: promote on capability, never on a cohort guess

Rationale: "Recommended for you" is a lie when there's no honest signal behind it — a guess dressed as a recommendation, and users learn to distrust it. But when a choice differs by an objective, verifiable capability (does this host render interactive cards? does this plan support the feature?), promoting the better option is honest and genuinely helpful. The MCP connect hub refused a cohort-based "recommended" row precisely because it had no honest signal — then added a featured "Best experience" row once an objective signal existed (MCP App / ui:// card support is a yes/no fact about the host).

Do: Promote, feature, or reorder only when the ranking key is a fact the user could verify themselves (capability, plan tier, version support, connection state). Name the reason ("renders interactive cards", "OAuth 2.0 supported"). Derive the promoted set from that fact in code so it can't drift into editorializing. A featured section is fine; it sits above the neutral grouping, and the promoted items are filtered out of the lower groups so nothing appears twice.

Anti-pattern: "Recommended for you" / "Popular" / "Best for your team" with no signal but a hunch. Hard-coding a favored vendor to the top. Promotion that can't answer "promoted because what?" in one objective clause.

15. Show the capability, don't just describe it

Rationale: When a surface promotes a richer mode the user hasn't seen, prose ("renders interactive cards") asks them to imagine the payoff. A small, faithful, static preview of the real thing lets them see it in two seconds — more convincing than a paragraph, and honest because it depicts the actual output, not an idealized one.

Do: Pair a capability claim with an inline depiction built from the real component's tokens/structure (ported, not invented), clearly captioned as an example, using anonymized sample data. Keep it static — it's a depiction, not a live instance — and theme-aware so it reads true in both modes. When the real component evolves, the preview is cheap to refresh.

Anti-pattern: Describing a rich experience entirely in words. A "preview" that invents a look the real feature doesn't have (sets a false expectation). A screenshot that goes stale silently because nothing ties it to the source component.

16. Provisioning-model chip on host & integration pickers

Rationale: Some integrations a user installs themselves; others a workspace admin provisions once for everyone. A picker that treats them identically sends a non-admin down a path they can't finish without IT, then strands them. A quiet chip up front sets the right expectation before the click.

Do: On any picker of external hosts/integrations whose setup model varies, mark admin-provisioned entries with a quiet neutral Admin chip (not a brand/accent color — it's metadata, not a CTA). Self-serve is the unmarked default. Keep it to one word; the per-item detail page explains the full flow.

Anti-pattern: A flat list where an admin-only integration looks identical to a self-serve one. Using an alarming or accent color for the marker (it reads as a warning or a recommendation). Burying the admin requirement in step 3 of setup, after the user has already committed.

17. Reference legends are hover-on-demand, not standing furniture

Rationale: A key that explains an encoding — what a status-rail color means, what a column measures, what a glyph stands for — is reference material the expert never reads and the novice reads once. Pinning it to the page as a standing card spends permanent vertical space and visual weight on information needed rarely and briefly. The encoding itself should be legible enough to use without the key (Rule 3: color + shape redundancy); the key is the safety net, not the signage.

Do: Collapse a rail/badge/column/glyph legend behind a single quiet affordance (lucide Info, --ox-icon-quiet) placed next to the thing it explains — a roster header, a column-header row, a matrix corner. Hover and focus reveal the full key in a popover (built on the shared <LegendInfo> over the Radix tooltip — hover + focus + touch-accessible). The resting page shows one glyph, not a block. Per-item ⓘ tooltips on individual headers are fine and complementary.

Anti-pattern: A "What each column means" / "What the rails & badges mean" card standing permanently above or below the data it annotates, scrolled past every visit. Encoding so opaque the page is unusable without the standing key — fix the encoding (Rule 3), then make the key on-demand.

18. Data-viz carries a non-visual channel

Rationale: A sparkline, heatmap, or chart encodes meaning in pixels a screen reader can't read and a color-blind user may not parse. A color-only series fails the same way a color-only status dot does (Rule 3) — the moment hue is the sole carrier, the trend is invisible to anyone who can't separate the hues. This is Rule 3 applied to the §6 data-visualization family.

Do: Every visualization ships a parallel non-visual channel. Sparklines and inline charts carry an aria-label stating the trend ("Coverage 82→87 over 5 weeks"); complex charts offer a table fallback with the same numbers. Distinguish series by shape, pattern, or direct label, not hue alone (Rule 3). Interactive charts are keyboard-navigable, and meaningful graphics clear 3:1 contrast (Rule 5). (DDR-013)

Anti-pattern: A sparkline with no aria-label; a multi-series chart separated only by color; a canvas viz with no table or text equivalent; hover-only tooltips as the only path to the underlying numbers.


12. Mockup explorations

Dated, in-flight design explorations live in this repo under proposals/YYYY-MM-DD-<slug>/ and publish to sageox-design.netlify.app. Each exploration is a self-contained static gallery and ships at the matching URL prefix on the published site. The site's root index links every exploration plus a pointer back to this DESIGN.md.

To publish, run bash scripts/publish-mockups.sh from this repo (--draft for a preview URL), or use the /publish-design-mockups Claude skill. Secrets are SOPS-encrypted at secrets/netlify.sops.yaml.

The role of mockups vs the system: this DESIGN.md and the tokens are the canonical, codified system everyone consumes. Mockups under proposals/ propose or stress-test changes to that system before they're codified here. When a mockup's idea proves out, it's promoted into DESIGN.md (rules, values, components) — and then the mockup remains as archaeology so future readers see where the rule came from.


13. v2 design evolution — archaeology

Decisions made during the v2.0 → v2.4 iteration that codified Section 11. Captured here so reviewers understand why values are what they are — and don't undo a deliberate choice without knowing what it cost the first time.

Surface & contrast

  • 4-tier surface system. bg < surface-1 < surface-2 < surface-3 replaced a 3-tier system. Ambient bg (#0a0d0f dark) sits behind surface-1 and surface-3 to give nested cards and popovers a visible depth step.
  • --text-muted bumped #8f99a3#9aa3ab in dark mode. V2.1 contrast audit found the original borderline on surface-2; the new value clears 4.5:1. (See Rule 5.)
  • --text-subtle bumped #6f767c#7d858c — original failed AA below 4.5:1 on surface-2. Same audit.
  • --border-strong added (#3a4146 dark / #a8b3a4 light) for active separators and focused outlines. Passive dividers continue to use --border-subtle; the two values should not be swapped.

Component-level decisions

  • Title demoted 24px → 20px. 24px (h1) competed with page-level layout hierarchy. 20px (h2) with letter-spacing: -0.015em reads as confident without dominating the view.
  • Type badge moved to slug line at 10.5px. Inline with a 24px title it read as identity; at 10.5px (tiny token, muted foreground) it reads as metadata. (See Rule 1.)
  • Avatar baseline-aligned, not top-aligned. Top-alignment made the header visually taller than it is; baseline alignment collapses the perceived header height.
  • Sage border-left for personal items, not a full tinted bg. A full accent background on a persistent "yours alone" banner read as alarming. A 2px left border signals "personal" without triggering a danger read.
  • translateY hover dropped from card grids. Every card lifting simultaneously in a grid reads as jittery. Reserve translateY hover for isolated CTAs only. (See Rule 2 on motion frequency.)
  • Flat fill avatars, no radial gradients. Radial gradients on 36px avatars are visual noise. Flat fill + monogram is cleaner at small sizes.
  • Murmur hover changed from sage-tint to neutral surface-2. Sage-tint hover competed visually with the sage-500 freshness dots; neutral surface avoids the collision.

Calm mode (v2.1 → v2.3)

Canonical rationale, coverage taxonomy, and must-never list: DDR-009. Calm is a density/attention preference (the third theme axis: light / dark / calm), independent of OS prefers-reduced-motion.

  • Calm mode is quiet, not silent. It hides optional metadata (calm-hide), dims secondary badges/counters to 35% opacity (--calm-badge-opacity, hover/focus-to-reveal), kills ambient/decorative motion, and flattens decorative per-kind tint — see the five-class coverage taxonomy in DDR-009 §3.
  • Hover/focus transitions kept at 120–150ms in calm mode. The UI still responds; calm is not dead.
  • Purposeful motion is preserved; ambient motion is killed. Motion that encodes currently-true, safety- or consent-relevant state (the live-recording / capture pulse) stays in calm mode — "you are being recorded right now" is signal, not chrome. Motion that decorates, or reports ambient others'-activity (typing dots, activity-pulse rings, presence dots, skeleton shimmer), goes. (See Rule 2, Layer 2, and DDR-009 §4–5 for the must-never list.)

Chat (v2.3)

  • Model badge collapsed to a 22px gear icon. Full model label + BYOK + scope labels added ~80px of horizontal chrome to the chat head. That detail is noise for 95% of interactions; the gear expands on click.
  • Two-rail rendering (gold-right for user, sage-left for assistant) replaces message bubble chrome. The rails create a readable column without heavyweight containers.
  • Send button ghost → sage on arm. Ghost state when the composer is empty prevents mis-clicks; sage fill when text is present signals "ready to send."

Anti-patterns confirmed during iteration

  • transition: all causes hover flicker on unrelated properties — banned. (See Rule 2.)
  • Bouncy / overshooting cubic-bezier springs read as marketing theater in a productivity tool. (See Rule 2.)
  • Two primary-weight actions on one card paralyze users — one primary CTA per view, period.
  • Radial gradients on avatars smaller than 48px are visual noise; flat fill wins.

May-2026 Conductor-themes review (rules 9–13)

A review of Conductor's changelog from v0.28 → v0.56 (Dec 2025 → May 2026) surfaced five recurring product moves that were already philosophically aligned with Apple × Linear × Tufte but not yet codified as system rules. The review promoted them into §11.

  • Rule 9 (Status at the edge). CLAUDE.md already had "status lives at the edge" as one of three Apple principles, but no anti-patterns and no enforcement. Conductor's sidebar status icons (merged / failing CI / conflicts) and workspace-title bolding for awaiting-input were the concrete proof. Promoted to a §11 rule with anti-patterns so the principle can be cited on a PR review.
  • Rule 10 (Hover-reveal for secondary metadata). Hover-reveal was implicit in Rule 4 (Power-on-demand) but conflated features with information. Conductor's hover-tooltip pattern on workspace cards / file diffs / context dial clarified that secondary information needs its own rule. Standard timing (150ms in, 75ms out) baked in from Conductor's observable feel.
  • Rule 11 (Self-healing UI). Conductor's "Failed setup → AI fix" button and "Delete anyway" toast secondary action proved that error UX is a first-class design surface, not an afterthought. Aligned with SageOx's AI-native operating philosophy — failures with Investigate buttons cost pennies and save minutes. Pairs with loading-processing-and-status.md.
  • Rule 12 (Keyboard-driven sequential review). Conductor's ⌘⌥J (next comment), ⌃V (view-and-advance), and ⌘/ (cheatsheet) showed that keyboard nav over sets is a load-bearing affordance for power reviewers, not a nice-to-have. Default keybindings (J/K or ⌘⌥J/⌘⌥K) codified to prevent surface-by-surface drift.
  • Rule 13 (Periodic typography & density audits). Conductor's 0.33 typography revamp and 0.36.1 "chat overview bars capped at 50 turns for readability" were the lesson. Density drifts silently; a quarterly audit cadence is the cheapest defense.

Composite patterns introduced in the May-2026 review

  • Attention tray. One consolidated "what needs my attention" tray per entity (recording / KB / workspace / chat thread). Modeled on Conductor's Checks tab (consolidates git status, deployments, CI, comments, user todos in one location). Full spec in DDR-004.

Patterns NOT adopted from Conductor's changelog

Recorded so a future reader doesn't try to import them by reflex:

  • Daily release cadence. Conductor ships 1–3× / week. SageOx's calm voice rejects always-shipping-fluff; match value-per-entry, not entries-per-week.
  • Additive-only sidebar growth. Conductor's sidebar accumulated status icons + counts + branches + city names + repo + directory through 2026. We periodically remove signals, not just add — see Rule 13.
  • Model picker in four different places. Conductor surfaced model selection in Settings, composer, ⌘K palette, and number-key shortcuts. That's discoverability theater. Pick one canonical surface per affordance — our /feature-flag and /coworker patterns already do.

Companion files

  • AGENTS.md — how to build the project (audience: coding agents).
  • DESIGN.md — how the project should look and feel (audience: design agents, this file).
  • proposals/ — dated mockup explorations; published to sageox-design.netlify.app via scripts/publish-mockups.sh.

For visual / UI decisions in any SageOx repo, read this file first.