Single source of truth for all visual design decisions.
Implementation lives in frontend/src/app/globals.css (@theme block) and frontend/src/components/ui/.
The app uses a dark-only theme. All colors are defined as semantic tokens in the @theme block of globals.css. Every component must use these tokens — never raw Tailwind palette classes.
| Token | Hex | Tailwind class | Usage |
|---|---|---|---|
--color-surface-0 |
#09090b |
bg-surface-0 |
Page background, root shell |
--color-surface-1 |
#18181b |
bg-surface-1 |
Cards, panels, sidebar, form inputs |
--color-surface-2 |
#27272a |
bg-surface-2 |
Hover states on surface-1 elements, nested panels |
--color-surface-3 |
#3f3f46 |
bg-surface-3 |
Tooltips, elevated chips, scrollbar thumbs |
| Token | Hex | Tailwind class | Usage |
|---|---|---|---|
--color-border-subtle |
#27272a |
border-border-subtle |
Default card/panel borders, dividers |
--color-border-default |
#3f3f46 |
border-border-default |
Stronger borders, modal panels, tooltips |
| Token | Hex | Tailwind class | Usage |
|---|---|---|---|
--color-text-primary |
#fafafa |
text-text-primary |
Headings, primary body text, active labels |
--color-text-secondary |
#a1a1aa |
text-text-secondary |
Secondary labels, descriptions |
--color-text-tertiary |
#84848e |
text-text-tertiary |
Tertiary captions, timestamps |
--color-text-muted |
#52525b |
text-text-muted |
Placeholders, disabled text, idle icons |
| Token | Hex | Tailwind class | Usage |
|---|---|---|---|
--color-accent |
#3b82f6 |
bg-accent / text-accent |
Primary CTA, links, active tab indicators, focus rings |
--color-accent-hover |
#60a5fa |
bg-accent-hover / text-accent-hover |
Hover state for accent elements |
--color-accent-muted |
#3b82f620 |
bg-accent-muted |
Faint accent tint for ghost-accent hover backgrounds |
| Token | Hex | Tailwind class | Usage |
|---|---|---|---|
--color-success |
#34d399 |
text-success / bg-success |
Connected status, success messages |
--color-success-muted |
#34d39920 |
bg-success-muted |
Success toast backgrounds |
--color-warning |
#fbbf24 |
text-warning / bg-warning |
Warnings, loading indicators |
--color-warning-muted |
#fbbf2420 |
bg-warning-muted |
Warning backgrounds |
--color-error |
#f87171 |
text-error / bg-error |
Error messages, destructive indicators |
--color-error-muted |
#f8717120 |
bg-error-muted |
Error backgrounds, destructive hover tint |
--color-info |
#60a5fa |
text-info / bg-info |
Info messages |
--color-info-muted |
#60a5fa20 |
bg-info-muted |
Info backgrounds |
| Context | Value |
|---|---|
PWA theme-color |
#3b82f6 (accent) |
PWA background_color |
#09090b (surface-0) |
| App icon gradient | bg-gradient-to-br from-accent to-blue-700 |
Two typefaces loaded via next/font/google with display: "swap":
| Family | CSS variable | Tailwind class | Role |
|---|---|---|---|
| DM Sans | --font-sans |
font-sans |
All UI text: headings, body, labels, buttons |
| JetBrains Mono | --font-mono |
font-mono |
Code blocks, SQL, data tables, monospaced values |
| Role | Classes | Example |
|---|---|---|
| Page title | text-2xl font-bold tracking-tight |
Auth screen heading |
| Section heading | text-lg font-semibold |
Card titles, modal titles |
| Subsection heading | text-base font-semibold |
Panel sub-headers |
| Body | text-sm |
Chat messages, form labels, descriptions |
| Caption | text-xs |
Error messages, timestamps, toast text, secondary actions |
| Micro | text-[11px] |
Legal links, footnotes |
| Micro smallest | text-[10px] |
Tooltip text, inline labels on icon buttons, validation hints |
- Default
leading-normalfor body text. leading-relaxedfor multi-line descriptions and detail paragraphs in modals.tracking-tighton page titles (text-2xl).tracking-wider+uppercasefor divider labels (e.g., "or" separator).
Consistent use of Tailwind's spacing scale. Preferred values by context:
| Context | Padding | Gap | Margin |
|---|---|---|---|
| Icon button (xs) | p-1 |
— | — |
| Icon button (sm) | p-1.5 |
gap-1 |
— |
| Icon button (md) | p-2 |
gap-1 |
— |
| Form input | px-3.5 py-2.5 |
— | — |
| Text button / CTA | px-4 py-1.5 (small) / py-2.5 (full-width) |
— | — |
| Card / panel interior | p-5 or p-6 |
space-y-4 |
— |
| Section margin | — | — | mb-4 / mb-8 |
| Stacked form fields | — | space-y-4 |
— |
| Sidebar items | px-2 py-1 to px-3 py-2 |
gap-2 |
— |
| Page-level padding | p-4 |
— | — |
| Class | Use case |
|---|---|
rounded |
Small inline elements, icon buttons (xs/sm), status dots |
rounded-md |
Icon buttons (md), code blocks, tags |
rounded-lg |
Inputs, text buttons, modals, toasts, skip-link |
rounded-xl |
Cards, panels, auth form containers |
rounded-2xl |
Brand logo container, large decorative elements |
rounded-full |
Spinners, status dots, avatar circles |
| Class | Use case |
|---|---|
shadow-lg |
Toasts, dropdowns |
shadow-xl |
Modals, popover panels |
No shadows on cards or sidebar — elevation is conveyed through surface color difference and borders.
Custom Icon component (frontend/src/components/ui/Icon.tsx) rendering inline SVGs with Lucide-style paths.
Properties:
name: typed union of available icon names (55+ icons).size: pixel value. Standard sizes:10,12,14,16,20,28.- SVG uses
stroke="currentColor",strokeWidth={2},fill="none"— color is controlled via parent text color classes.
Size usage by context:
| Size | Context |
|---|---|
| 10 | ActionButton xs |
| 12 | ActionButton sm, inline error/validation icons |
| 14 | ActionButton md, sidebar icons |
| 16 | Default, standalone icons |
| 20 | Feature icons, section headers |
| 28 | Brand logo icon (zap) |
Do not import external icon packages. Add new icons to the PATHS record in Icon.tsx using 24x24 viewBox Lucide-style paths.
w-full py-2.5 bg-accent text-white rounded-lg text-sm font-semibold
hover:bg-accent-hover disabled:opacity-50 transition-colors
Used for main form submissions (Sign In, Create Account, Save).
px-4 py-1.5 text-xs rounded border transition-colors
text-text-secondary border-border-default
hover:text-text-primary hover:border-border-default
Used for Cancel actions in modals and secondary choices.
px-4 py-1.5 text-xs text-white rounded transition-colors
bg-red-600 hover:bg-red-500
disabled:opacity-40 disabled:cursor-not-allowed
Used for destructive confirmations (delete, disconnect).
Three variants defined in ActionButton.tsx:
| Variant | Rest state | Hover state |
|---|---|---|
ghost |
text-text-muted |
text-text-secondary bg-surface-2 |
danger |
text-text-muted |
text-error bg-error-muted |
accent |
text-text-muted |
text-accent bg-accent-muted |
Three sizes:
| Size | Padding | Min dimensions | Icon size | Radius |
|---|---|---|---|---|
xs |
p-1 |
min-w-6 min-h-6 |
10px | rounded |
sm |
p-1.5 |
min-w-8 min-h-8 |
12px | rounded |
md |
p-2 |
min-w-9 min-h-9 |
14px | rounded-md |
All buttons have:
transition-all duration-150- Focus:
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface-0 - Disabled:
opacity-40 cursor-not-allowed - Every icon button must have
aria-labeland be wrapped in<Tooltip>.
text-accent hover:text-accent-hover transition-colors
Used for inline actions (Register / Sign In toggle, footer links).
Canonical class string for all text/email/password inputs:
w-full px-3.5 py-2.5 bg-surface-1 text-text-primary rounded-lg text-sm
border border-border-subtle
focus:border-accent focus:ring-1 focus:ring-accent focus:outline-none
transition-colors placeholder-text-muted
Validation states:
- Error:
border-red-500 focus:border-red-500 focus:ring-red-500/30 - Error hint:
text-[10px] text-red-400 mt-1 px-1 - Neutral hint:
text-[10px] text-text-muted mt-1 px-1
Required attributes:
aria-labelon every input.aria-required="true"on required fields.aria-invalidwhen validation fails.
bg-surface-1 rounded-xl border border-border-subtle p-5
or p-6 for larger panels. Internal content uses space-y-4 for vertical stacking.
Cards sit on bg-surface-0 backgrounds. No shadow on cards — elevation is conveyed by the surface color step.
Overlay:
fixed inset-0 z-50 flex items-center justify-center bg-black/60
Panel:
bg-surface-1 border border-border-default rounded-lg p-5
max-w-sm w-full mx-4 shadow-xl
Behavior:
role="dialog"+aria-modal="true"+aria-labelledby.- Focus trap: Tab cycles within focusable elements.
- Escape key closes the modal.
- Click outside (on overlay) closes the modal.
- Entry animation:
animate-in fade-in zoom-in-95 duration-150.
pointer-events-none whitespace-nowrap px-2 py-1 rounded-md
text-[10px] font-medium leading-none
bg-surface-3 text-text-primary border border-border-default
animate-tooltip-in
- Rendered via
createPortaltodocument.body. - Positioned with
position: fixed+z-index: 9999. - 200ms delay before showing.
- Placements:
top,bottom(default),right. - Auto-repositions to stay within viewport.
aria-describedbyon the trigger element.
Position: fixed bottom-4 right-4 z-[60], stacked with gap-2, max width max-w-sm.
px-4 py-2.5 rounded-lg border text-xs shadow-lg
animate-[slideIn_0.2s_ease-out] flex items-start gap-2
Semantic variants:
| Type | Classes |
|---|---|
success |
bg-emerald-900/90 border-emerald-700 text-emerald-200 |
error |
bg-red-900/90 border-red-700 text-red-200 |
info |
bg-zinc-800/90 border-zinc-600 text-zinc-200 |
Each toast has role="alert", aria-live="polite", and a dismiss button with aria-label="Dismiss notification".
Migration note: toast colors use raw palette classes for the semi-transparent backgrounds. This is acceptable because the status variants need distinct hues beyond what the semantic tokens provide. If new toast types are added, follow the same
{hue}-900/90+{hue}-700+{hue}-200pattern.
Defined in StatusDot.tsx. A colored circle indicating connection or process state.
Sizes:
| Size | Classes |
|---|---|
sm |
w-1.5 h-1.5 |
md |
w-2 h-2 |
Colors: Use semantic tokens directly — bg-success, bg-warning, bg-error, bg-info, bg-surface-3 (idle).
Common: shrink-0 rounded-full inline-block. Optional animate-pulse-dot for loading state.
Every status dot must have role="img" and aria-label.
w-4 h-4 border-2 border-surface-3 border-t-text-secondary
rounded-full animate-spin
Wrapper: flex justify-center py-3. Must have role="status", aria-live="polite", and an sr-only "Loading..." label.
Three scrollbar variants defined in globals.css:
| Class | Width | Thumb color | Use case |
|---|---|---|---|
.sidebar-scroll / .scrollbar-thin |
4px | #3f3f46 (surface-3) |
Sidebar, small panels |
.chat-scroll |
6px | #27272a (border-subtle) |
Main chat area |
.data-table-scroll |
4px | #3f3f46 (surface-3) |
Data tables (with edge fade gradients) |
Also: .chat-markdown pre and .chat-markdown table get 4px horizontal scrollbars.
All use scrollbar-width: thin for Firefox and ::-webkit-scrollbar for Chromium/Safari. Tracks are transparent.
Full-height flex shell (100vh)
├── Sidebar (collapsible, hidden on mobile)
│ ├── Project selector
│ ├── Navigation sections (SidebarSection)
│ └── User/settings footer
├── Main content area (flex-1)
│ ├── Header (desktop) / Mobile header with hamburger
│ ├── Chat messages (scrollable, .chat-scroll)
│ └── Chat input (fixed bottom)
└── Notes drawer (right side, toggleable)
- Desktop: sidebar always visible (unless manually collapsed), main content fills remaining width.
- Mobile (
max-width: 767px): sidebar hidden behind hamburger menu, full-width content, bottom-anchored input. - All scrollable containers use the appropriate custom scrollbar class.
id="main-content"on the main area for skip-link target.
All custom animations live in globals.css. Standard timing: ease-out.
| Animation | Duration | Keyframe | CSS class | Usage |
|---|---|---|---|---|
| Fade in | 0.15s | fade-in |
.animate-fade-in |
Generic element entrance |
| Slide up | 0.25s | slide-up |
.animate-slide-up |
Mobile drawers, bottom sheets |
| Slide in from left | 0.2s | slide-in-left |
.animate-slide-in-left |
Sidebar items appearing |
| Slide in from top | 0.2s | slideIn |
animate-[slideIn_0.2s_ease-out] |
Toasts |
| Slide down | — | slideDown |
(inline) | Dropdown menus |
| Tooltip entrance | 0.15s | tooltip-in |
.animate-tooltip-in |
Tooltips (fade + slight scale) |
| Pulse dot | 1.5s infinite | pulse-dot |
.animate-pulse-dot |
Loading/status indicators |
| Spin (slow) | — | spin-slow |
(inline) | Background decorative rotation |
| Task item in | — | taskItemIn |
(inline) | Staggered list items |
| Onboarding forward | 0.2s | onboarding-slide-right |
.animate-onboarding-forward |
Wizard step forward |
| Onboarding back | 0.2s | onboarding-slide-left |
.animate-onboarding-back |
Wizard step backward |
| Standard spin | built-in | animate-spin |
Tailwind built-in | Spinner |
Rules:
- UI transitions: 0.15s-0.25s
ease-out. - Looping indicators: 1.5s+
ease-in-out infinite. - Never add new
@keyframeswithout documenting them here. transition-colorson all interactive elements for hover/focus state changes. Usetransition-all duration-150for buttons with multiple changing properties.
Breakpoint: Single mobile breakpoint at max-width: 767px.
Touch targets: Enforced globally via @media (pointer: coarse):
- Default:
min-height: 44px; min-width: 44pxon all buttons, links, and submit inputs. - Compact:
min-height: 36px; min-width: 36pxinside.compact-touchcontainers (dense sidebar lists, toolbars).
Mobile chat: Code blocks in chat constrained to max-width: calc(100vw - 4rem).
Viewport:
width: device-width,initial-scale: 1.maximum-scale: 5,user-scalable: true— never disable pinch zoom.
- Global
focus-visibleoutline:2px solid var(--color-accent)with2pxoffset. - Form inputs suppress the global outline and use their own
focus:ring-1 focus:ring-accent+focus:border-accent. - ActionButton focus:
focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-1 focus-visible:ring-offset-surface-0. - Modals trap focus and return it on close.
In layout.tsx:
<a href="#main-content" className="sr-only focus:not-sr-only ...">
Skip to main content
</a>
Visible only on keyboard focus. Uses bg-blue-600 text-white rounded-lg text-sm.
Global rule in globals.css:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}All animations and transitions are neutralized. No additional opt-out logic needed in components.
| Component | Required attributes |
|---|---|
| Icon buttons | aria-label (descriptive action) |
| Inputs | aria-label, aria-required, aria-invalid |
| Status dots | role="img", aria-label |
| Spinner | role="status", aria-live="polite", .sr-only label |
| Modals | role="dialog", aria-modal="true", aria-labelledby |
| Toasts | role="alert", aria-live="polite" |
| Tooltips | role="tooltip", trigger has aria-describedby |
| Dismiss buttons | aria-label="Dismiss notification" or equivalent |
See Section 3.3. Minimum 44x44px on touch devices, 36x36px in compact zones. Enforced via CSS, not per-component.
Some components still use raw Tailwind palette classes. The table below maps them to the correct semantic tokens. New code must always use the semantic column.
| Raw class | Semantic equivalent | Notes |
|---|---|---|
bg-zinc-900 |
bg-surface-1 |
|
bg-zinc-800 |
bg-surface-2 |
|
border-zinc-700 |
border-border-default |
|
border-zinc-700/50 |
border-border-default/50 |
Opacity modifier preserved |
text-zinc-200 |
text-text-primary |
|
text-zinc-300 |
text-text-primary |
Close enough; use primary |
text-zinc-400 |
text-text-secondary |
|
text-zinc-500 |
text-text-muted |
|
text-zinc-600 |
text-text-muted |
|
border-zinc-600 |
border-border-default |
|
text-red-400 |
text-error |
|
text-yellow-400 |
text-warning |
|
bg-blue-600 |
bg-accent |
Skip-link only |
hover:bg-zinc-700 |
hover:bg-surface-2 |
|
ring-zinc-500/40 |
ring-accent/40 |
Use accent for focus rings |
These components still use raw Tailwind classes where semantic tokens should be used:
| Component | File | Issue |
|---|---|---|
| ConfirmModal | components/ui/ConfirmModal.tsx |
Uses bg-zinc-900, border-zinc-700, text-zinc-* classes |
| ToastContainer | components/ui/ToastContainer.tsx |
Uses raw palette for status variants (acceptable, see 2.6) |
| Spinner | components/ui/Spinner.tsx |
Uses border-zinc-600 border-t-zinc-300 |
| ChatInput | components/chat/ChatInput.tsx |
Uses border-zinc-*, ring-zinc-* |
When touching these files, migrate them to semantic tokens.
When introducing a new token, component, or pattern:
- Add the token to the
@themeblock infrontend/src/app/globals.css. - Document it in the appropriate section of this file.
- If it's a new animation, add the
@keyframestoglobals.cssand list it in Section 3.2. - If it's a new UI component, place it in
frontend/src/components/ui/and document its variants, sizes, and ARIA requirements in Section 2. - Never introduce a new raw Tailwind color class when a semantic token exists or can be created.