Lippes Mathias intake app

The design system of the Lippes Mathias ProLaw intake app (Next.js, shadcn/ui on Base UI, Tailwind v4). Every color, radius and type size an agent may use is here; the CSS custom properties in src/app/globals.css carry the same names and values. Color and spacing keys are the CSS variable names exactly (--color-text here is --color-text in globals.css). Values are hex.

From client-lippes-design/DESIGN.md @ 36f7360 2026-10-22Built 2026-10-22 by build.pyEvery ratio computed: WCAG 2.x and APCA Lc (as apca-w3 0.1.9)

Colors

Three scales on one OKLCH lightness grid (blue and slate at hue 247, red at 25), then semantic tokens that point at them. Components use semantic tokens only. Click a primitive to copy its hex; the figure under each is its contrast on white.

Primitives

Step
Blue
Slate
Red

Semantic tokens

DESIGN.md text: Colors

Current: every token below is defined in globals.css.

Contrast is WCAG ratio · APCA Lc, text or mark first, computed with apca-w3 0.1.9 (APCAcontrast(sRGBtoY())) and culori (wcagContrast, OKLCH) on 2026-10-22.

Floors: text 4.5:1, with body sentences at Lc 75, labels, chips and button text at Lc 60, headings at Lc 45, and disabled text at Lc 30 (WCAG-exempt). Non-text controls and meaningful marks need 3:1; a fine line 1–5px across needs Lc 45, a solid mark at least 5.5px across Lc 30, and a card edge Lc 15.

Primitives

Two scales on one OKLCH lightness grid at hue 247° (D77, D100): step k (50 → 0, 100 → 1, … 900 → 9, half steps at k + 0.5) has L = 0.969 − 0.0775·k; blue chroma = min(0.112, 0.89 × the largest in-gamut sRGB chroma at that L and hue); slate chroma = blue chroma ÷ 2.8. The formula reproduces every step within one hex unit, except blue-700 and slate-700, which are anchored to the brand blue #005288 and its slate at the same lightness and hue, and step 50 of both scales, which sits at L 0.981 instead of the grid's 0.969 so secondary text (slate-600) clears the body floor on the ground and the tints (Lc 75.5 on slate-50, 75.3 on blue-50), because one more half step on the scale cost more than a ground a sixth of a step lighter (D100). Slate-950 is the only half step: ink, because it is today's ink exactly. No scale has a 650 step (D100).

Step Blue Blue L / C / H Slate Slate L / C / H Slate on white (WCAG · Lc)
50 #f4f9fe 0.980 / 0.009 / 248 #f7f9fb 0.981 / 0.003 / 248 1.06 · 0
100 #c2dffc 0.892 / 0.051 / 248 #d2dde7 0.892 / 0.018 / 245 1.38 · 18.4
200 #93c8f9 0.814 / 0.089 / 247 #b3c5d6 0.815 / 0.031 / 247 1.77 · 32.5
300 #6db0ed 0.737 / 0.112 / 247 #96adc2 0.737 / 0.040 / 246 2.32 · 45.7
400 #5498d3 0.660 / 0.112 / 247 #7f95aa 0.660 / 0.040 / 247 3.10 · 57.9
500 #3c80b9 0.582 / 0.112 / 247 #687e92 0.583 / 0.040 / 246 4.21 · 69.0
600 #2369a0 0.505 / 0.111 / 246 #52677b 0.504 / 0.041 / 247 5.86 · 79.3
700 #005288 0.427 / 0.112 / 247 #3d5164 0.426 / 0.040 / 247 8.21 · 88.3
800 #0c3d62 0.349 / 0.082 / 247 #2e3c49 0.349 / 0.029 / 247 11.30 · 96.1
900 #062944 0.272 / 0.063 / 247 #1e2832 0.272 / 0.023 / 249 14.95 · 101.8
950 — — #161f27 0.234 / 0.020 / 246 16.67 · 103.6

A third scale, red, sits on the same grid at hue 25° with chroma capped at 0.15, for the danger buttons and error text (D25, D85). Its 50 step stays at L 0.969, because nothing sets text on it.

Step Red L / C / H On white (WCAG · Lc)
50 #fef2f0 0.970 / 0.013 / 29 1.10 · 0
100 #fbcfcb 0.891 / 0.050 / 24 1.41 · 19.8
200 #f9aba4 0.814 / 0.093 / 25 1.84 · 34.5
300 #f7837b 0.737 / 0.143 / 25 2.47 · 48.2
400 #df6862 0.660 / 0.150 / 25 3.34 · 60.2
500 #c4504b 0.582 / 0.150 / 25 4.57 · 71.0
600 #a93736 0.503 / 0.150 / 25 6.39 · 80.8
700 #8f1c20 0.426 / 0.150 / 25 8.92 · 89.0
800 #6d1115 0.349 / 0.125 / 25 12.06 · 96.2
900 #4c080b 0.271 / 0.098 / 25 15.59 · 101.6

Primitive names: --blue-50 … --blue-900, --slate-50 … --slate-950, --red-50 … --red-900, and --white #ffffff (1.000 / 0 / —). Semantic tokens point at primitives with var(); components use semantic tokens only.

Semantic tokens

One row per token. "Primitive" is the step the token points at. Roles that share a step are merged on purpose (D100): slate-50 is the page ground, item tint, every row and menu hover and the waiting fill; slate-100 is dividers, disabled border and fill, pressed tint, record-chip fill and rail hover; slate-500 is control borders, the rail track and the waiting outline; slate-700 is headings, nav and the cut; blue-800 is primary hover, pressed text and needs-you. They never sit on top of each other, or shape and text tell them apart.

Text

Token Primitive Use Key pairs (WCAG · Lc)
--color-text slate-950 Body text and input values (D77, D100) on white 16.67 · 103.6; on slate-50 15.80 · 99.8; on blue-50 15.74 · 99.6; on banner 15.85 · 100.1
--color-text-secondary slate-600 Meta, help text, queue cells, column heads (D100) on white 5.86 · 79.3; on slate-50 5.55 · 75.5; on blue-50 5.53 · 75.3; on banner 5.57 · 75.8
--color-text-heading slate-700 H1, H2, rail section and item labels (D73, D77) on white 8.21 · 88.3; on slate-50 7.77 · 84.6; on slate-100 5.95 · 67.5 (a hovered rail entry turns --color-text instead: 12.10 · 82.8)
--color-text-on-action white Text and icons on the primary and needs-you fills (D72, D78) on blue-700 8.20 · −92.4; on blue-800 11.29 · −99.5; on blue-900 14.91 · −104.4
--color-text-disabled slate-400 Disabled button text, all tiers (D25, D100) on white 3.10 · 57.9; on slate-100 2.25 · 37.1 (disabled, WCAG-exempt; above Lc 30)
--color-text-error red-700 Error sentences: field errors, invalid field labels, the destructive alert, blocked readiness and flag detail, the legacy .error and .error-summary (D85); the bright --color-state-blocked stays for icons, borders and the blocked chip on white 8.92 · 89.0; on slate-50 8.45 · 85.2; on blue-50 8.42 · 85.0

Surfaces

Two surfaces only, because finer near-whites blend on ordinary office monitors (D100). Tints and hovers sit only inside white cards; a tint placed directly on the page ground would vanish.

Token Primitive Use Key pairs
--color-surface white Cards, header, sticky bar, inputs, popovers, queue rows (D71, D100) slate-50 against it 1.06 · 0
--color-bg-page slate-50 Page ground (D100) ink 15.80 · 99.8; secondary 5.55 · 75.5; card edge 1.68 · 28.7
--color-surface-tint slate-50 Item tint: conflict and status boxes, inside white cards (D100) ink 15.80 · 99.8; secondary 5.55 · 75.5; control border 3.99 · 65.3; against white 1.06
--color-bg-row-hover slate-50 Queue row, workbench .rowlink and menu-item hover; one hover for all three (D100) ink 15.80 · 99.8; action 7.77 · 84.0; against white 1.06

Borders and dividers

Token Primitive Use Key pairs
--color-border-divider slate-100 Dividers inside white cards (D84, D100) on white 1.38 · 18.4
--color-border-card slate-200 Card edges on the ground, the header's bottom border, the sticky bar's top border, the select-menu popover edge (D84) on slate-50 1.68 · 28.7; on white 1.77 · 32.5 (edge floor Lc 15)
--color-border-disabled slate-100 Disabled secondary-button border, disabled control border (D100) on white 1.38 · 18.4 (disabled, exempt)

Controls

Inputs, textareas, selects, checkboxes and radios (D81 as amended by D100, D86). Menu-item highlight is --color-bg-row-hover (slate-50), set on the menu-item components as an override of shadcn's shared --accent, which stays blue-50 for ghost-button hover (D100).

State Token Primitive Key pairs
Rest border --color-border-control slate-500 on white 4.21 · 69.0; on slate-50 3.99 · 65.3 (3:1 and fine-line Lc 45 met on both)
Rest fill --color-surface white —
Hover border --color-border-control-hover slate-600 on white 5.86 · 79.3; no fill change, because a fill change blends on ordinary monitors (D100)
Pressed none — Controls have no pressed state.
Disabled fill and border --color-action-disabled-bg, --color-border-disabled slate-100 D100's merged disabled role; text --color-text-disabled on it 2.25 · 37.1.
Focus ring --color-focus-ring blue-700 2px outline, 2px offset on every control, button and row link (D86); on white 8.20 · 87.7, on slate-50 7.77 · 84.0; against blue-700 1.00 and blue-800 1.38, so the offset gap carries it; queue row links use offset −2px (D86); white on the navy home hero (.hero sets --color-focus-ring to white, because blue-700 on navy is 1.95:1) and 2px inside a keyboard-focused menu or combobox item (D86)
Checked fill and border --color-action blue-700 white mark on it 8.20 · −92.4; checking is an action (D72, D100)

Buttons

Three tiers, each with every state (D25), with D85's darker pressed text, and a danger version of each tier on the red scale (see Danger buttons under Components).

Token Primitive Use Key pairs
--color-action blue-700 Primary fill; secondary border and text; ghost text; links; nav underline; avatar initials (D72) white on it 8.20 · −92.4; on white 8.20 · 87.7; on blue-50 7.74 · 83.7; on slate-50 7.77 · 84.0
--color-action-hover blue-800 Primary hover fill (D100) white on it 11.29 · −99.5; against blue-700 1.38
--color-action-active blue-900 Primary pressed fill (D100); blue-800 would make hover and pressed identical white on it 14.91 · −104.4; against blue-800 1.32
--color-action-tint-hover blue-50 Secondary and ghost hover fill, avatar fill, count tag (D100) action on it 7.74 · 83.7; ink 15.74 · 99.6; against white 1.06
--color-action-tint-pressed slate-100 Secondary and ghost pressed fill, aria-pressed toggles (D25, D100) pressed text 8.19 · 74.9; against blue-50 hover 1.30 · 14.5
--color-action-pressed-text blue-800 Secondary and ghost pressed text (D85) on slate-100 8.19 · 74.9 (label floor 60; 0.1 under the preferred 75)
--color-action-disabled-bg slate-100 Disabled primary fill; disabled danger primary and secondary fill (D25, D100) disabled text 2.25 · 37.1; against white 1.38 · 18.4 (exempt)
--color-danger red-600 Danger primary fill; danger secondary text and edge at rest (D25) white on it 6.39 · −85.9; on white 6.39 · 80.8
--color-danger-hover red-800 Danger primary hover fill; danger secondary hover text and edge (D25) white on it 12.06 · −99.9; on red-100 8.55 · 74.0
--color-danger-active red-900 Danger primary pressed fill; danger secondary and ghost pressed text and edge (D25) white on it 15.59 · −104.4; on red-200 8.46 · 64.9 (label floor 60; held only during the click)
--color-danger-ghost-text red-700 Danger ghost text at rest and hover (D25) on white 8.92 · 89.0; on red-100 6.32 · 66.9
--color-danger-tint-hover red-100 Danger secondary and ghost hover fill (D25) against white 1.41 · 19.8
--color-danger-tint-pressed red-200 Danger secondary and ghost pressed fill (D25) against white 1.84 · 34.5
--color-danger-border-disabled slate-200 Disabled danger secondary edge, so it shows on its slate-100 fill (D25) against the slate-100 fill 1.28 · 11.7; on white 1.77 · 32.5 (disabled, exempt)
Tier Rest Hover Pressed / selected Disabled Focus
Primary --color-action fill, --color-text-on-action --color-action-hover fill --color-action-active fill --color-action-disabled-bg fill, --color-text-disabled --color-focus-ring
Secondary --color-surface fill, --color-action border and text --color-action-tint-hover fill --color-action-tint-pressed fill, --color-action-pressed-text --color-surface fill, --color-border-disabled, --color-text-disabled --color-focus-ring
Ghost no fill, --color-action text --color-action-tint-hover fill --color-action-tint-pressed fill, --color-action-pressed-text --color-text-disabled --color-focus-ring

Chips

State chips: one state per item, identifiable by icon and label, never by color alone (D40, D41); chips are non-interactive apart from a hover tooltip (D41). Done and blocked chips use the state families below. Record-status chips are borderless slate pills (D80, D83).

Token Primitive Use Key pairs
--color-state-needs-you-bg blue-800 Needs-you chip fill and the rail's filled marker (D78) white on it 11.29 · −99.5; on white 11.29 · 95.7
--color-state-needs-you-text white Needs-you chip text and icon (D78) as above
--color-state-waiting-bg slate-50 Waiting chip fill (D40, D100) waiting text 10.71 · 92.3; vanishes on a hovered row (slate-50), where the outline carries it
--color-state-waiting-border slate-500 Waiting chip outline and rail marker outline (D41, D100) on slate-50 3.99 · 65.3; on white 4.21 · 69.0
--color-state-waiting-text slate-800 Waiting chip text and clock icon (D40) on slate-50 10.71 · 92.3
--color-record-bg-hover slate-300 Hover fill of a combobox chip's remove ✕, which sits on the slate-100 chip, so hover shows (D91) icon --color-text on it 7.19 · 55.8 (the icon is a non-text mark, floor 3:1); against the slate-100 chip fill 1.68 · 24.9, where slate-200 was Lc 11.7, under the visible-step floor Lc 15
--color-record-bg slate-100 Record-status chip fill, title row and queue status column (D80) record text 8.20 · 75.3; silhouette on a hovered row 1.31 · 14.7 (under the edge floor; the text carries it)
--color-record-text slate-800 Record-status chip text (D83) on slate-100 8.20 · 75.3

Rail

The rail's labels use --color-text-heading, turning --color-text on hover; its count pills use --color-action on --color-action-tint-hover; its state markers use the chip tokens (D41).

Token Primitive Use Key pairs
--color-rail-hover slate-100 Rail-link hover on the page ground (D100); blue-50 would be 1.00:1 on slate-50 against slate-50 1.31 · 14.7; heading on it 5.95 · 67.5, so a hovered label turns --color-text (12.10 · 82.8)
--color-rail-track slate-500 Idle item tick and track line, 2px (D100) on slate-50 3.99 · 65.3
--color-mark-cut slate-700 The 64° cut on the title tab and the rail's current item (D79) on slate-50 7.77 · 84.6

Header and nav

The header is --color-surface with a --color-border-card bottom border (D84); the avatar is --color-action-tint-hover with --color-action initials, and the count tag uses the same pair (7.74 · 83.7); the current nav item's underline is --color-action (D72).

Token Primitive Use Key pairs
--color-text-nav slate-700 Header nav links (D100) on white 8.21 · 88.3

State families

These keep their own hues, because a blue or slate step would erase the meaning they carry (D40, D41, D76, D85, D100). They are the only tokens off the blue, slate and red scales.

Token Value L / C / H Use Key pairs
--color-state-blocked #c10007 — Blocked chip fill, rail octagon, heading octagon; white text and icon on the chip (D40, D41) white on it 6.42 · −84.3; as icon on white 6.42 · 79.1, on slate-50 6.08 · 75.3
--color-state-blocked-border #9f0712 — Blocked chip border (D40) against the fill 1.30 · 0 (edge only)
--color-state-done-bg #d0fae5 — Done chip fill (D40) done text 13.36 · 93.1
--color-state-done-border #009966 — Done chip border and check icon (D40, D41) on done fill 3.22 · 54.8; on white 3.65 · 63.5
--color-state-done-text #002c22 — Done chip text (D40) on done fill 13.36 · 93.1
--color-state-done-strong #17663a — Heading check disc (D41) on white 7.00 · 83.9
--color-bg-banner #fff9e8 0.982 / 0.023 / 91 Demo / integration-status banner (D76) ink 15.85 · 100.1; secondary 5.57 · 75.8
--color-border-banner #e0c88a 0.839 / 0.084 / 89 Banner bottom border (D76) on banner 1.56 · 24.9; on white 1.64 · 28.4
--color-warning-bg #fef3c6 0.962 / 0.059 / 96 Warning chip fill: the unqualified "Ready for ProLaw" record status (D76) warning text 5.32
--color-warning-border var(--color-warning-text) — Warning chip border; takes the text color, because the amber #e17100 was 2.87:1 on the fill (D76) on the warning fill 5.32 · 71.9; on white 5.93 · 79.3
--color-warning-text #8a5a00 — Warning chip text; the same value as --warning (D76) on the warning fill 5.32

The blocked red is a state color, not a step of the red scale; danger buttons and error text use the scale (D25, D85).

Shadows are not color tokens; see Elevation & Depth.

Mapping to globals.css

The app has two token sets: the shadcn contract (--primary and friends, OKLCH) and the pre-shadcn --legacy-* palette for hand-rolled screens. Each existing name takes the semantic token's value; the first column's values are the old ones, as hex, so a stray one is easy to spot and replace.

globals.css name (old value) Semantic token New value
--foreground, --card-foreground, --popover-foreground, --ink (#1e1e1e) --color-text slate-950 #161f27
--background, --panel (#f7f7f7) --color-bg-page slate-50 #f7f9fb
--card, --popover (#ffffff) --color-surface white, unchanged
--primary, --ring, --accent-foreground, --legacy-accent (#29588c) --color-action, --color-focus-ring blue-700 #005288 (D72)
--primary-foreground (#ffffff) --color-text-on-action white, unchanged
--secondary, --accent, --legacy-tint (#e8edf3) --color-action-tint-hover blue-50 #f4f9fe; shadcn also uses --accent for menu-item highlight, which D100 puts on slate-50, so menu items need --color-bg-row-hover set on the component
--secondary-foreground (#3d5265) --color-text-heading slate-700 #3d5164
--muted (#eef1f5) --color-surface-tint slate-50 #f7f9fb
--muted-foreground, --legacy-muted (#555862) --color-text-secondary slate-600 #52677b
--border, --line (#d9dfe6) --color-border-divider slate-100 #d2dde7
--input (#d9dfe6, was an alias of --border), --control-line (#7e8c9c) --color-border-control slate-500 #687e92; --input no longer aliases --border, because dividers stay slate-100 while control borders go to slate-500 (D81, D100)
--destructive (#9f2d2d) --color-state-blocked #c10007; its users are blocked states, invalid-field borders and icons; error sentences use --color-text-error instead (D85)
--danger (#9f2d2d) --color-text-error red-700 #8f1c20 (D85), for the legacy .error, .error-summary and required-mark text
--success (#17663a) --color-state-done-strong unchanged; the done chip uses its own three tokens
--warning (#8a5a00) no semantic token keep for warnings (D76 keeps yellow for meaning)
--legacy-navy (#0a2240) no semantic token nearest is blue-900 #062944; decide when its screen is redesigned
--brand-highlight (yellow) none not an app accent (D76)
--sidebar-*, --chart-* no semantic token out of scope
no old name the primitives; --color-border-card, --color-border-disabled, --color-action-hover, --color-action-active, --color-action-tint-pressed, --color-action-pressed-text, --color-action-disabled-bg, --color-text-disabled, --color-text-error, the --color-danger-* button tokens, --color-text-nav, --color-bg-row-hover, --color-rail-hover, --color-rail-track, --color-mark-cut, the needs-you, waiting, record, done and blocked tokens, the banner pair new

--color-border-control-hover (slate-600) and --color-focus-ring (blue-700) are also new (D86, D100).

Typography

One family, Figtree, on a perfect-fourth scale. Samples are editable.

h128px / 36px · 600
New matter request
h222px / 28px · 600
Client and adverse parties
h316px / 24px · 600
Conflict search
body14px / 20px · 400
Responsible attorney is required before the request can go to ProLaw.
small12px / 16px · 400
Submitted 3 hours ago by M. Alvarez
chip12px / 16px · 600
Needs you
button14px / 20px · 500
Send to review
DESIGN.md text: Typography

Current. One family, Figtree, loaded with next/font/google in src/app/layout.tsx and bound to --font-sans. Figtree is a variable font, so one file serves every weight; don't pass a weight array, which would download static files instead. Use four weights only: 400 for body and small text, 500 for button labels, 600 for headings, chips and the current rail item, 700 sparingly for emphasis inside text. 500 and 600 must stay visibly distinct, because button labels and headings rely on the difference.

The scale is a perfect fourth (1.333): H3, H2 and H1 are each a fourth apart, and body and small sit half a step below H3 and body, so the app's existing 16 and 12 land on the scale. The heading sizes are registered in @theme inline, so the Tailwind class names carry them.

Role Size / line height Weight Tailwind Use
H1 28 / 36 600 text-2xl Page title, one per page, --color-text-heading
H2 22 / 28 600 text-xl Section heading, --color-text-heading; 32 above and 8 below so it belongs to its rows
H3 16 / 24 600 text-lg Sub-section and card heading
Body 14 / 20 400 text-sm Everything else: values, labels, table cells, rail items
Small 12 / 16 400 text-xs Meta, help text, tooltips
Chip 12 / 16 600 text-xs font-semibold State and record chips
Button 14 / 20 500 text-sm font-medium Button labels

Layout & shape

8pt spacing, square surfaces, 2px controls, pill chips, and a flat page where only floating layers cast a shadow.

Spacing

Radius & depth

DESIGN.md text: Layout

Current. Spacing sits on an 8pt scale: 4, 8, 16, 24, 32, 40, 48 (the spacing tokens; Tailwind 1, 2, 4, 6, 8, 10, 12). Page spacing, padding and row heights land on the scale; icons, table cells and similar small details may be bespoke.

  • The page ground is --color-bg-page; content sits in white cards on it, separated by the card edge rather than by shadows or gaps alone.
  • Buttons are 32px tall (h-8); no clickable target drops under 32px, including wrapped lookup buttons (current: min-h-8 on the professional-discovery buttons).
  • The workbench has an always-open section rail on the left, the form in the middle and a sticky action bar at the bottom of the viewport; the review queue is one table with whole-row links.
DESIGN.md text: Elevation & Depth

The app is flat. Hierarchy comes from the card edge, the two surfaces and type weight, never from shadows on cards.

  • Cards, the header, the sticky bar, status boxes and chips have no shadow (current).
  • Floating layers, which sit over content, take a --color-border-card edge plus --shadow-popover 0 4px 12px rgba(22,31,39,.12) (the alpha base is the ink): the select menu, popovers, dropdown menus and alert dialogs (current).
  • Tints and hovers sit only inside white cards. A tint placed directly on the page ground vanishes, because the ground is the same slate-50.
DESIGN.md text: Shapes
  • Surfaces are square (current): --radius: 0px, so every shadcn surface (card, popover, dialog, menu, tooltip) and every hand-rolled surface (status box, item tint, the rail, the sticky bar) has 0 radius. The brand is square-cornered, and the 10px shadcn radius is the most recognizable stock default.
  • Controls are 2px (current): --radius-control: 2px on buttons, inputs, textareas, selects, checkboxes and the avatar (rounded-(--radius-control)).
  • Chips are pills (current): rounded-full on every state and record chip and the rail count tag, the one exception to square corners, because a 2px chip reads as a mini button and chips must never compete with buttons. Never use rounded-4xl or another --radius multiple for a pill; with --radius at 0 it goes square.
  • Radios stay round, because a square radio reads as a checkbox.
  • The avatar is square at the 2px control radius (current), to match everything else in the header.
  • The cut (current): the brand's angle as a 64° slanted edge (90° − 26°, measured from the vertical), drawn in --color-mark-cut with clip-path. Two sizes share the angle: the 40×24 title tab before the page <h1> (polygon(0 0, 100% 0, calc(100% - 11.7px) 100%, 0 100%), since 24 ÷ tan 64° = 11.7) and the 12×8 marker on the rail's current item (calc(100% - 3.9px)). The title tab appears once per page, only on request headers and the two queue pages, and is aria-hidden. Both sizes are the .cut rule in globals.css, which derives the slant from the height (var(--cut-height) / tan(64deg)); the title tab is the TitleCut component.

Components

Drawn with the DESIGN.md tokens only. Buttons respond to hover, press and keyboard focus; the static states beside them show each one at rest.

Buttons — three tiers and their danger versions, every state

Header, nav, avatar and banner

State and record chips

Needs you Waiting Done Blocked Claimed by you Ready for ProLaw

One state per item, by icon and label, never color alone. Pills are the one exception to square corners.

Form controls

Rest — slate-500 border
Hover — slate-600 border, no fill change
Focus — 2px ring, 2px offset
Disabled
Conflict search done Rush Existing client New client

Card, divider, item tint, row hover

Adverse parties
Northgate PropertiesAdded 2 days ago
Harbor Logistics Inc.Row hover
Item tint: a conflict or status box, only inside a white card.

Section rail

Request
Client3
Matter
Parties
Billing
Documents2

Current item: the 12×8 cut. Hover: slate-100 with the label turning ink, because blue-50 is 1.00:1 on the slate-50 ground.

Title tab and headings

Review queue

64° cut (90° − 26°): a 24px tab runs 24 ÷ tan 64° = px. Once per page.

Floating layers

Litigation
Corporate
Real estate
Waiting on the paralegal

Only floating layers take --shadow-popover. A keyboard-highlighted item draws the ring 2px inside.

DESIGN.md text: Components

Every component below is current: each one reads the tokens, with no hex or Tailwind palette class left in src outside globals.css. The YAML components block gives the same values as references; borderColor there is the one property the format doesn't define.

Buttons

Three tiers, each with every state, plus a danger version of each (see Danger buttons). One primary per page. The tier names and legacyVariants in ui/button.tsx stay.

Tier Rest Hover Pressed / selected Disabled Focus
Primary --color-action fill, --color-text-on-action --color-action-hover fill --color-action-active fill --color-action-disabled-bg fill, --color-text-disabled --color-focus-ring
Secondary --color-surface fill, --color-action border and text --color-action-tint-hover fill --color-action-tint-pressed fill, --color-action-pressed-text --color-surface fill, --color-border-disabled, --color-text-disabled --color-focus-ring
Ghost no fill, --color-action text --color-action-tint-hover fill --color-action-tint-pressed fill, --color-action-pressed-text (weight 600 for aria-pressed); aria-expanded takes --color-action-tint-hover --color-text-disabled --color-focus-ring
Link ghost's text colors with an underline — — --color-text-disabled --color-focus-ring

Every size uses rounded-(--radius-control). No variant sets its own focus-visible:outline-* color; the base rule's ring shows on all of them.

Danger buttons

Each tier has a danger version on the red scale with its own states, so a danger button never borrows the blue tiers' pressed blue or turns slate only when disabled (D25). Primary and secondary step 600, 800, 900 from rest to pressed; disabled is neutral on every tier, because slate-100 is the blue primary's disabled fill and still shows a shape on a white card (1.38:1). Focus is D86's one blue ring on every tier, and the hero's white override still applies.

Tier (variant) Rest Hover Pressed Disabled Focus Used by
Danger primary (danger-primary) --color-danger (red-600) fill, --color-text-on-action --color-danger-hover (red-800) fill --color-danger-active (red-900) fill --color-action-disabled-bg (slate-100) fill, --color-text-disabled (slate-400) --color-focus-ring The confirm dialog's final "Return to Submitter"
Danger secondary (danger; destructive aliases it) --color-surface fill, --color-danger (red-600) text and edge --color-danger-tint-hover (red-100) fill, --color-danger-hover (red-800) text and edge --color-danger-tint-pressed (red-200) fill, --color-danger-active (red-900) text and edge --color-action-disabled-bg (slate-100) fill, --color-danger-border-disabled (slate-200) edge, --color-text-disabled --color-focus-ring The header "Return to Submitter…", the return form submits, Reject's submit, the admin "Clear stored secret"
Danger ghost (danger-ghost) no fill, --color-danger-ghost-text (red-700) --color-danger-tint-hover (red-100) fill; aria-expanded takes it too --color-danger-tint-pressed (red-200) fill, --color-danger-active (red-900) text --color-text-disabled, no fill --color-focus-ring The Reject toggle

Text pairs: white on red-600 6.39 · −85.9, on red-800 12.06 · −99.9, on red-900 15.59 · −104.4; red-600 on white 6.39 · 80.8; red-800 on red-100 8.55 · 74.0; red-900 on red-200 8.46 · 64.9; red-700 on white 8.92 · 89.0, on red-100 6.32 · 66.9; disabled slate-400 on slate-100 2.25 · 37.1 (exempt).

Focus ring

Current for every control, button and row link. One rule for every control, button and row link: outline: 2px solid var(--color-focus-ring); outline-offset: 2px, set in @layer base on button, a, input, textarea, [role="checkbox"], [role="radio"], [data-slot=select-trigger] and [data-slot=combobox-chip]. The resting outline-color of every element is --color-focus-ring too, so transition-colors has nothing to fade. Because that rule is in a layer, a utility outline-none or shadcn's focus-visible:ring-* beats it, so components must not set either. The offset is never 0, because against the primary and needs-you fills the ring itself is near 1:1 and the offset gap carries the contrast. The one exception is the queue row link, which draws its ring 2px inside the row (outline-offset: -2px), because the table clips an outside ring. Two more scoped changes (D86, 2026-10-22): .hero redefines --color-focus-ring as white and its sign-in button uses the secondary tier, because blue-700 is 1.95:1 on the navy; and a menu or combobox item that keyboard focus reaches keeps its slate-50 highlight and draws the same ring 2px inside (focus-visible:-outline-offset-2 on dropdown and select items; data-highlighted while the popup is in keyboard mode on combobox items), because the highlight alone is 1.06:1 on white.

Form controls

Inputs, textareas, selects, checkboxes and radios: border --color-border-control (through border-input), hover border --color-border-control-hover with no fill change, disabled fill and border --color-action-disabled-bg/--color-border-disabled with --color-text-disabled, radius --radius-control (radios round), checked checkbox and radio fill and border --color-action with a white mark. --input is the control border and --border is the divider; they are separate tokens and must stay separate. Menu items in select.tsx and dropdown-menu.tsx highlight with focus:bg-(--color-bg-row-hover) focus:text-(--color-text), not shadcn's focus:bg-accent, because --accent is blue-50 for ghost-style hovers and a blue menu highlight would read as an action.

Cards and surfaces

One card style everywhere: --color-surface, a 1px --color-border-card edge, 0 radius, no shadow, no ring (shadcn's ring-1 ring-foreground/10 goes). Dividers inside a card are --color-border-divider. Item blocks (status and conflict boxes) are --color-surface-tint with 0 radius, inside a white card. One tint marks where you act: --color-action-tint-hover (blue-50) for a review item's action header, and blue-50 with a 4px --color-action left edge for its return form and the evidence panel, while slate-50 stays the tint for items and rows (D100); every pairing on blue-50 passes (secondary text 5.53 · 75.3). Queue rows, workbench row links and menu items hover with --color-bg-row-hover. The tooltip is --color-text with white text, 0 radius, and its arrow uses the same token. Legacy surfaces (.form-card, .notice, .conditional-panel, .error-summary, .code-block, .view-switcher) take the same card rule.

Chips

A state chip names one state per item, by icon and label, never by color alone; chips are non-interactive apart from a hover tooltip naming the state. Color follows whose move it is.

Chip Fill Border Text and icon Use
Needs you --color-state-needs-you-bg same as fill --color-state-needs-you-text The viewer's move
Waiting --color-state-waiting-bg --color-state-waiting-border --color-state-waiting-text, clock icon Someone else's move
Done --color-state-done-bg --color-state-done-border --color-state-done-text, check icon in the border color Resolved
Blocked --color-state-blocked --color-state-blocked-border white Cannot proceed
Record status --color-record-bg none --color-record-text Facts about the record, in the request title row (including the claim chip) and the queue's status column only

Record-status chips have no border and no shadow, so they stay apart from the waiting chip's outline. The unqualified "Ready for ProLaw" record chip keeps an amber warning look, because amber stays the app's warning color; its warning fill, border and text tokens are --color-warning-bg, --color-warning-border and --color-warning-text (current). The Badge's general variants map onto the same tokens: secondary and outline are neutral pills (--color-record-bg fill, --color-border-control edge, --color-record-text), info is --color-action-tint-hover with a --color-action edge and text, and default is --primary. My Requests uses state chips from the secretary's side (Draft, Changes Requested and Rejected read needs you, Submitted and In Review read waiting, Ready for ProLaw and Ready for Conflict Search read done), not record chips.

Section rail

Every section and item label --color-text-heading (current item weight 600, idle 400); count pills --color-action on --color-action-tint-hover; idle item ticks and the track --color-rail-track, 2px; link hover --color-rail-hover (slate-100, because blue-50 is 1.00:1 on the slate-50 ground) with the label turning --color-text; the current item's marker is the 12×8 cut in --color-mark-cut (the compact rail too); the count tag is --color-action-tint-hover with --color-action text. One state marker per open item, each with its own shape: a filled --color-state-needs-you-bg circle with a target (needs you), a --color-state-waiting-bg circle with a --color-state-waiting-border outline and a --color-state-waiting-text clock (waiting), a --color-state-blocked octagon with "!" (blocked). Done items leave the rail.

Header and nav

The header is --color-surface with a --color-border-card bottom border. Nav links are --color-text-nav; the current item's underline is --color-action. The avatar is --color-action-tint-hover with --color-action initials at --radius-control; the count tag uses the same pair. The demo and integration-status banner is --color-bg-banner with a --color-border-banner bottom border.

Do's and Don'ts

DESIGN.md text: Overview

The intake app is where Lippes Mathias secretaries submit new-matter requests, paralegals review and correct them, and approvers sign off before ProLaw. It should read as the firm's own tool: quiet, square and dense, with the firm's blue #005288 as the only action color and a slate neutral made from the same hue, so every screen comes from one brand hue plus a few state colors that carry meaning. Users scan long forms and queues all day on ordinary office monitors, so the system favors clear edges, high contrast and few near-white surfaces over subtle tints.

Methodology: the design-craft base: one OKLCH lightness grid for both scales, tinted neutrals, and contrast computed rather than eyeballed (WCAG ratio and APCA Lc, both given for every key pair below).

How this file maps to code. Every token here is a CSS custom property in src/app/globals.css :root with the same name: the primitives (--blue-*, --slate-*, --white) hold the hexes, and the semantic tokens (--color-*) point at them with var(), except the state families, which hold their own values. Components use semantic tokens only, as arbitrary-value utilities: bg-(--color-surface-tint), text-(--color-text-secondary), border-(--color-border-card). The tokens are deliberately not registered in @theme, because Tailwind's built-in --color-slate-* and --color-blue-* are different palettes and the --color-* theme namespace would turn each token into a clashing utility name. shadcn's own names (--primary, --border, --input …) and the older hand-rolled names (--ink, --line …) point at the semantic tokens; see "Mapping to globals.css" under Colors.

Status markers. Current means it ships in globals.css and the components today; target means it is decided and specified here but a component still hard-codes an older value. Build new work to the target, and when you move a component onto it, flip the marker in this file in the same commit. Today everything in this file is current: the tokens, the type, the radius scale, the focus ring, and the button, chip, card, control, rail, cut and header rules in Components.

Dark mode. Only the home hero sets .dark (a navy section on --legacy-navy), so .dark and dark: utilities are live there and nowhere else. App-wide dark mode is out of scope: there is no theme toggle, and the .dark block in globals.css is left as it is rather than guessing a palette nobody designed.

Contrast checks

Every ink, mark and border token on every surface it can sit on, computed from the hexes in DESIGN.md. Each cell is rendered in its own pair, so the sample is the evidence.

≥ 4.5 text-safe3–4.5 marks and large only< 3 disabled or decorative

Component pairs from the YAML

Each component's textColor on its backgroundColor. A translucent fill would be composited on white first, the way the browser draws it.