Design system
for agents
Canonical URLs, migration state, hard rules, and executable code snippets for Codex, Claude, Cursor, ChatGPT, and any other coding agent building Outlex UI.
Canonical links
These are the only valid URL forms for this design system. All other paths (repo paths, redirected aliases) are NOT canonical.
Not canonical:
/tokens/tokens-v2.css ·
/brand-v2.md ·
/tokens/GRAMMAR.md ·
/tokens/design-tokens-v2.dtcg.json ·
brand/brand.md (repo path — not a URL)
These may redirect but are not the canonical form. Always use the live URL forms listed above.
Migration state
The v2 system is active. The product app still implements v1/V7 on unmigrated surfaces. Read this before touching any product file.
v2 is the canonical standard for all NEW surfaces — active since 2026-06-11. It is the brand book, all mockups, and any net-new component or page.
The product (app.outlex.ai) still implements v1/V7 on un-migrated surfaces: cream-editorial --v7-* tokens, Fraunces headings, the "warm editorial" aesthetic. Those surfaces are archived at /v1/.
Rule for agents: editing an EXISTING product surface → match the code that's there (V7 legacy, do NOT mix --ox-* tokens in). Building a NEW surface, mockup, or anything in the migration plan → use v2 (this document).
DESIGN_DIRECTION.mdCurrent precedent
The rule is: reuse before inventing. Before building any v2 component, check what's already been migrated and replicate it exactly.
Status as of 2026-06-11: NO production surface has been migrated to v2 yet. The first migration is the Lawyer Portal (PRO-3318).
The approved reference blueprints are the final mockups in mvp2 docs/research/2026-06-10-design-elevation/:
final-website-mockup.html— website / marketing surfacefinal-product-home-chats.html— product home + chatfinal-product-hub-doc.html— legal hub / document surface
When the first migration (Lawyer Portal) ships, update this section with: the merged PR number, a link to a live URL or screenshot, and the specific patterns used.
Hard rules
These are hard stops. Violating any of them produces a design-system failure that will be caught in review.
width="0" height="0" style="position:absolute". NEVER display:none — it breaks gradient rendering in Safari and Firefox. Gradients on #outlex-mark will not paint.#ic-lexi for monochrome, #outlex-mark gradient for avatars). Sparkle icons (#ic-sparkles) are banned on ALL AI affordances. May remain in symbol set for non-AI decoration only.#54D6A4 (teal dot via --ox-ai-fill). Amber is NEVER used as AI background, AI label colour, or AI identity. Amber is: warning dots, SLA deadline meta text, pending chips only.rgba(255,255,255,0.06) base). icon-box-amber is removed.--ox-fg-secondary. Colour lives ONLY in the 4–5px leading dot. Dot palette: terracotta = high/overdue · amber = pending · sage = ready/done · teal = AI/citation. Never colour the whole chip background.font-weight: 600. Newsreader (display): max weight font-weight: 400 (--ox-weight-display). V7/legacy Fraunces: max font-weight: 500. NEVER font-weight: 700 or font-bold with display/heading fonts.gap-1 (4px) or smaller spacing in layout. Minimum is gap-2 (8px) = --ox-space-2. No -5 utilities (20px is not on the grid) — use -6 (24px).text-black, bg-black, border-black, or literal #000000 / #FFFFFF text. Always semantic tokens: --ox-fg-primary, --ox-surface-0. Shadows must be warm-tinted OKLCH, not rgba(0,0,0,N).linear-gradient(180deg, #7BA081, #5F8266)).--ox-* tokens. Do NOT derive values from taste or hardcode hex. Copy exact values from GRAMMAR.md or tokens-v2.css. "Looks about right" is not acceptable — wrong colours are caught by the token linter.220ms var(--ox-ease). Exit: 130ms var(--ox-ease). Press: 80ms ease. Zero animation list: data tables, accordions, error states, reading contexts. Always respect prefers-reduced-motion with 0.01ms override.Copy-paste snippets
Executable blocks. All values are copied verbatim from GRAMMAR.md — do not derive or modify.
/* CSS @import — add before your own styles */ @import url('https://design.outlex.ai/tokens-v2.css'); /* OR as a <link> tag in <head> */ <link rel="stylesheet" href="https://design.outlex.ai/tokens-v2.css">
<!-- Default: data-theme="dark" on <html> --> <html lang="en" data-theme="dark"> <!-- Theme toggle script (run before first paint if possible) --> <script> (function() { var saved = localStorage.getItem('ox-theme'); var preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', saved || preferred); })(); </script> /* Toggle button handler */ function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('ox-theme', theme); }
.btn-primary {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--ox-space-2); /* 8px */
font-family: var(--ox-font-sans);
font-size: 14px;
font-weight: var(--ox-weight-medium); /* 500 */
letter-spacing: var(--ox-tracking-body);
padding: 10px 20px;
border: none;
border-radius: var(--ox-radius-md); /* 8px */
cursor: pointer;
text-decoration: none;
/* Sage gradient — canonical CTA surface */
background: linear-gradient(180deg, #7BA081, #5F8266);
color: #F0EDE8; /* warm white on sage */
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.25),
0 1px 2px rgba(0,0,0,0.3),
0 4px 16px oklch(0.60 0.10 150deg / 0.35);
transition:
transform var(--ox-press) ease,
box-shadow var(--ox-enter) var(--ox-ease);
}
.btn-primary:hover {
background: linear-gradient(180deg, #8BAD91, #6F9276);
color: #F5F2ED;
box-shadow:
inset 0 1px 0 rgba(255,255,255,0.30),
0 2px 4px rgba(0,0,0,0.35),
0 6px 20px oklch(0.60 0.10 150deg / 0.45);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(1px) scale(0.99);
}
/* ---- DARK ---- */ .card { background: linear-gradient(180deg, oklch(0.21 0.012 270deg), oklch(0.18 0.012 270deg) ); border: 1px solid rgba(255,255,255,0.07); border-radius: var(--ox-radius-lg); /* 12px */ box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), /* inset-highlight signature */ 0 1px 2px rgba(0,0,0,0.40), 0 4px 16px rgba(0,0,0,0.35), 0 12px 40px rgba(0,0,0,0.25); transition: transform var(--ox-enter) var(--ox-ease), box-shadow var(--ox-enter) var(--ox-ease), border-color var(--ox-enter) var(--ox-ease); } .card:hover { transform: translateY(-2px); border-color: rgba(255,255,255,0.12); box-shadow: inset 0 1px 0 rgba(255,255,255,0.12), 0 2px 4px rgba(0,0,0,0.50), 0 8px 24px rgba(0,0,0,0.45), 0 20px 60px rgba(0,0,0,0.35); } /* ---- LIGHT ---- */ [data-theme="light"] .card { background: linear-gradient(180deg, #FFFFFF, #FDFBF8); border-color: var(--ox-border); /* oklch(0.86 0.015 80deg) */ box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 1px 2px rgba(45,38,28,0.07), 0 4px 10px rgba(45,38,28,0.08), 0 12px 32px rgba(45,38,28,0.09); } [data-theme="light"] .card:hover { border-color: oklch(0.82 0.016 80deg); box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 2px 4px rgba(45,38,28,0.08), 0 8px 24px rgba(45,38,28,0.11), 0 20px 60px rgba(45,38,28,0.10); }
/* CSS */ [data-reveal] { opacity: 0; transform: translateY(16px); transition: opacity var(--ox-enter) var(--ox-ease), transform var(--ox-enter) var(--ox-ease); } [data-reveal].revealed { opacity: 1; transform: translateY(0); } @media (prefers-reduced-motion: reduce) { [data-reveal] { opacity: 1; transform: none; transition: none; } } /* JS */ document.querySelectorAll('[data-reveal]').forEach(function(el, i) { el.style.transitionDelay = (i * 40) + 'ms'; }); new IntersectionObserver(function(entries) { entries.forEach(function(e) { if (e.isIntersecting) e.target.classList.add('revealed'); }); }, { threshold: 0.1 }).observe(document.querySelector('[data-reveal]')); /* Note: call .observe() on each element in real usage */
<!-- HTML --> <div class="lexi-bubble"> <div class="lexi-header"> <div class="lexi-avatar"> <!-- gradient mark avatar (NOT monochrome for avatars) --> <svg width="16" height="16"><use href="#outlex-mark"/></svg> </div> <span class="lexi-label">LEXI</span> <span class="lexi-timestamp">Just now</span> </div> <p>Your response body goes here.</p> </div> /* CSS — neutral surface, NOT amber-tinted */ .lexi-bubble { background: linear-gradient(180deg, oklch(0.21 0.012 270deg), oklch(0.18 0.012 270deg) ); border: 1px solid rgba(255,255,255,0.07); border-radius: var(--ox-radius-lg); /* 12px — NOT asymmetric */ padding: 18px 20px; font-size: 14px; line-height: var(--ox-leading-loose); /* 1.75 — AI prose breathes */ color: var(--ox-fg-primary); box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), 0 1px 3px rgba(0,0,0,0.30), 0 4px 16px rgba(0,0,0,0.20); } [data-theme="light"] .lexi-bubble { background: linear-gradient(180deg, #FFFFFF, #FDFBF8); border-color: var(--ox-border); box-shadow: inset 0 1px 0 rgba(255,255,255,0.95), 0 1px 2px rgba(45,38,28,0.07), 0 4px 10px rgba(45,38,28,0.08), 0 12px 32px rgba(45,38,28,0.09); } .lexi-header { display: flex; align-items: center; gap: 10px; margin-bottom: var(--ox-space-3); } .lexi-avatar { width: 28px; height: 28px; border-radius: var(--ox-radius-md); /* 8px */ background: var(--ox-surface-2); border: 1px solid rgba(255,255,255,0.07); display: flex; align-items: center; justify-content: center; } .lexi-label { font-family: var(--ox-font-mono); font-size: 11px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ox-fg-muted); /* neutral muted — NOT amber */ } .lexi-timestamp { font-family: var(--ox-font-mono); font-size: 10px; color: var(--ox-fg-muted); margin-left: auto; }
/* Outer container — staged positioning context */ .product-stage { position: relative; padding: 48px 40px 32px; border-radius: 20px; background: linear-gradient(160deg, oklch(0.22 0.012 270deg), oklch(0.16 0.010 270deg) ); box-shadow: 0 0 0 1px oklch(0.36 0.008 270deg), 0 24px 80px oklch(0.05 0.02 270deg / 0.60), 0 8px 24px oklch(0.05 0.02 270deg / 0.40); } /* Window chrome — 3 traffic-light dots */ .product-stage::before { content: ''; position: absolute; top: 18px; left: 20px; width: 52px; height: 12px; border-radius: 6px; background: radial-gradient(circle at 8px 6px, #FF5F57 0%, #FF5F57 5px, transparent 5px), radial-gradient(circle at 26px 6px, #FEBC2E 0%, #FEBC2E 5px, transparent 5px), radial-gradient(circle at 44px 6px, #28C840 0%, #28C840 5px, transparent 5px); } /* Screenshot / content container */ .product-content { border-radius: 12px; overflow: hidden; box-shadow: 0 0 0 1px rgba(255,255,255,0.08), inset 0 1px 0 rgba(255,255,255,0.10); } /* Ambient glow below the stage */ .product-stage::after { content: ''; position: absolute; bottom: -40px; left: 10%; right: 10%; height: 60px; border-radius: 50%; background: oklch(0.57 0.15 270deg / 0.20); filter: blur(30px); pointer-events: none; }
Issue template
Copy this block into Linear issues that touch UI. It ensures agents know what to read first and what the routing rules are.
## Design system context **Read first (in order):** 1. `DESIGN_DIRECTION.md` in mvp2 root — migration state + which surfaces are V7 vs v2 2. https://design.outlex.ai/brand.md — canonical single-file brand reference 3. https://design.outlex.ai/GRAMMAR.md — exact CSS component recipes 4. Relevant mockup in `docs/research/2026-06-10-design-elevation/` (see DESIGN_DIRECTION.md) **Routing:** - Editing an EXISTING app surface → match V7 code already there (`--v7-*` tokens) - Building a NEW surface or mock → use v2 (`--ox-*` tokens, GRAMMAR.md recipes) - When in doubt: ask before mixing token systems **Hard-rule reminders:** - SVG symbol container: `width="0" height="0" style="position:absolute"` — NEVER `display:none` - Lexi = the Outlex mark (`#ic-lexi`). No sparkles on AI affordances - No amber fills on AI cards — teal dot (`#54D6A4`) is the AI accent - One accent per element — coloured icon + coloured label = violation - Dot chips only: colour in the dot, label always `--ox-fg-secondary` (neutral) - Display font (Newsreader): max weight 400. Inter: max weight 600. No `font-weight: 700` - 8px grid minimum — never `gap-1` (4px) - No pure black — use semantic `--ox-*` tokens **Acceptance criteria (add to this section):** - [ ] Screenshots vs blueprint mockup attached to PR - [ ] Token linter passes (0 hardcoded hex values in changed files) - [ ] Reduced-motion tested
Common mistakes
Real mistakes caught in this project's code reviews and sessions.
display:none on the SVG symbol container<use href="#outlex-mark"/> will render as a blank box in Safari and Firefox if the container has display:none. The gradient definitions inside cannot be resolved. Use width="0" height="0" style="position:absolute" exclusively.tokens-v2.css, GRAMMAR.md, brand.md exist only at the root. Using href="tokens-v2.css" from /components resolves to /components/tokens-v2.css → 404. Always use root-absolute paths: href="/tokens-v2.css" or the full https://design.outlex.ai/tokens-v2.css.tokens/tokens-v2.css, brand/brand-v2.md, tokens/GRAMMAR.md are file system paths inside the source repo. They are NOT valid URLs. The live URLs are https://design.outlex.ai/tokens-v2.css, /brand.md, /GRAMMAR.md. This causes agents to 404 when fetching docs.--ox-amber-*) to style AI cards, Lexi panels, or AI labels. Amber is exclusively: SLA deadline meta text, pending/review dot chips, focus rings. The AI accent is teal #54D6A4 via --ox-ai-fill. An amber-tinted Lexi bubble is the most common AI grammar violation.ic-sparkles is in the symbol set for legacy / non-AI decoration only. Placing it on "Ask Lexi", "Generate", or any AI call-to-action is a banned pattern. Use ic-lexi (the Outlex mark in monochrome) for all AI buttons and inline AI labels.rgba(255,255,255,0.06) background, icon inherits --ox-fg-secondary. The only colour is the leading 5px dot in the chip. Example of violation: sage icon container + sage chip label + sage dot all on the same row item.rgba(0,0,0,0.4) because it "looks like a dark shadow" — but the canonical recipe is oklch(0.05 0.02 270deg / 0.50) (warm-tinted). Or writing #7B8C81 because it "looks sage" instead of copying the exact gradient from GRAMMAR.md. Always fetch and copy; never derive from appearance.--ox-* tokens into a component that still uses --v7-* tokens creates visual inconsistency (different surface colours, shadow warmth, font weights). Check DESIGN_DIRECTION.md for the current migration state of the surface. If it's V7 → stay V7 until it's promoted.font-weight: 700 with display or heading fonts--ox-weight-display = 400. Fraunces (V7 heading) max is font-medium = 500. Semibold or bold with either creates visual shouting that breaks editorial restraint. Inter body max is 600 (--ox-weight-semibold). NEVER 700 or font-bold on headings..github/workflows/claude-code-review.yml in mvp2 feature PRsclaude-review run to 401 Unauthorized for that PR. Workflow changes must ship in standalone PRs to main only. This is a mvp2-specific CI constraint, not a design system rule — but it catches agents in PRs that happen to touch this file.