Documentation Index
Fetch the complete documentation index at: https://docs.yourhq.ai/llms.txt
Use this file to discover all available pages before exploring further.
Every value in the HQ interface — every color, font size, radius, shadow, transition — resolves to a token defined in apps/ui/src/app/globals.css. Tokens are CSS custom properties. Tailwind v4’s @theme inline block maps them to utility classes. There is no tailwind.config.js; the CSS file is the theme.
This page is the inventory. It is the source of truth that the principles, components, and patterns docs cite.
How tokens are layered
The token system has three layers, in order:
- shadcn neutral base.
--background, --foreground, --card, --popover, --primary, --secondary, --muted, --accent, --destructive, --border, --input, --ring, the chart palette, and the sidebar palette. Defined in both :root (light) and .dark.
- HQ semantic extensions. The state, status, priority, and text tokens we layer on top:
--surface-hover, --surface-selected, --border-strong, --status-*, --priority-*, --text-secondary, --text-tertiary. Defined in both :root and .dark so the same utilities resolve in either theme.
- Motion tokens.
--duration-fast/normal/slow, --ease-out, --ease-in-out. Defined once in :root only — motion is theme-invariant.
@theme inline mapping. Each token gets a --color-*, --radius-*, or font alias inside the top @theme block, which Tailwind v4 turns into utility classes (bg-surface-hover, text-status-success, rounded-md, font-sans, etc.).
The rule: never inline a hex. If an existing token covers the role, use it. If a new semantic role is needed (a new status state, a new surface depth), add a token to globals.css and wire it through @theme inline.
Color
All colors use the OKLch color space — perceptually uniform lightness, so a value at 0.72 in green and 0.71 in red read as the same brightness. We do not use hsl() or rgb().
Base palette
All neutral surfaces carry a subtle warm chroma (0.004–0.006) at hue 75, giving backgrounds a warm paper quality rather than clinical gray. The --primary token IS the brand color (Pine teal #1F7A6E by default), so all components using bg-primary automatically render in the brand color. Workspace admins can customize the brand via Settings → Appearance.
| Token | Light (:root) | Dark (.dark) | Used for |
|---|
--background | oklch(0.98 0.003 75) | oklch(0.14 0.005 75) | Page background — warm paper / warm charcoal |
--foreground | oklch(0.15 0.004 75) | oklch(0.93 0.004 75) | Primary text — warm near-black / warm off-white |
--card | oklch(1 0 0) | oklch(0.17 0.005 75) | Card surface |
--popover | oklch(1 0 0) | oklch(0.19 0.005 75) | Floating surfaces (menus, tooltips) |
--primary | oklch(0.50 0.09 175) | oklch(0.72 0.085 175) | Brand — Pine teal for buttons, links, active states |
--primary-foreground | oklch(1 0 0) | oklch(0.14 0.005 75) | Text on primary |
--secondary | oklch(0.955 0.004 75) | oklch(0.21 0.005 75) | Secondary action background |
--muted | oklch(0.96 0.004 75) | oklch(0.20 0.005 75) | Subdued surface |
--muted-foreground | oklch(0.46 0.008 75) | oklch(0.60 0.008 75) | Secondary text, labels |
--accent | oklch(0.95 0.005 75) | oklch(0.22 0.006 75) | Hover and selected fills on lists |
--destructive | oklch(0.55 0.20 25) | oklch(0.65 0.18 25) | Destructive actions |
--border | oklch(0.91 0.004 75) | oklch(1 0 0 / 10%) | Default 1px divider |
--input | oklch(0.91 0.004 75) | oklch(1 0 0 / 10%) | Input border |
--ring | oklch(0.50 0.09 175) | oklch(0.72 0.085 175) | Focus ring (matches brand) |
Surface ladder
The depth system. Built from neutral-on-surface transparency so the same component reads correctly across hover, focus, selection, and divider states without parallel color stacks. Light mirrors dark with black-on-light at slightly lower opacity (black on white reads stronger than white on near-black at the same alpha).
| Token | Light | Dark | Role |
|---|
--surface-hover | oklch(0 0 0 / 4%) | oklch(1 0 0 / 5%) | Hover lift on rows, cells, list items |
--surface-selected | oklch(0 0 0 / 6%) | oklch(1 0 0 / 8%) | Selected/active row fill |
--border | oklch(0.91 0.004 75) | oklch(1 0 0 / 10%) | Default divider, list separators |
--border-strong | oklch(0.84 0.005 75) | oklch(1 0 0 / 16%) | Section breaks, emphasized edges |
Applied via Tailwind utilities: bg-surface-hover, bg-surface-selected, border-border, border-strong.
Semantic text
| Token | Light | Dark | Role |
|---|
--foreground | oklch(0.15 0.004 75) | oklch(0.93 0.004 75) | Primary text |
--muted-foreground | oklch(0.46 0.008 75) | oklch(0.60 0.008 75) | Secondary text, labels |
--text-secondary | oklch(0.46 0.008 75) | oklch(0.60 0.008 75) | Same role as muted-foreground; available where you need a non-mapped utility |
--text-tertiary | oklch(0.60 0.006 75) | oklch(0.44 0.006 75) | Tertiary text — timestamps, helper hints, low-emphasis metadata |
Note the inversion: in light mode --text-tertiary is lighter than --text-secondary (closer to background = lower contrast); in dark mode it is darker (also closer to background). The visual hierarchy is the same — the OKLch numbers move opposite directions because the background does.
Available as .text-secondary and .text-tertiary utility classes.
Status
Reserved for state, never decoration. Use these tokens for any badge, dot, or icon that conveys a record’s state. Light values keep the same hue and chroma as dark but at lower lightness so they read on a white background.
| Token | Light | Dark | Use for |
|---|
--status-success | oklch(0.52 0.14 145) | oklch(0.68 0.13 145) | Completed, ready, healthy (hue 145, distinct from brand 175) |
--status-warning | oklch(0.60 0.15 70) | oklch(0.74 0.13 70) | Pending, blocked, attention needed |
--status-error | oklch(0.52 0.19 25) | oklch(0.66 0.17 25) | Failed, errored, missed |
--status-info | oklch(0.50 0.14 250) | oklch(0.65 0.13 250) | Queued, informational |
--status-progress | oklch(0.50 0.14 290) | oklch(0.65 0.13 290) | Actively executing, in flight |
--status-neutral | oklch(0.46 0.008 75) | oklch(0.58 0.008 75) | Inactive, archived, idle |
--status-info and --status-progress are intentionally distinct hues (blue and purple). The lifecycle of a daemon-driven record — agent commands, inbox items, source syncs — passes through queued (info) → claimed (warning) → executing (progress) → terminal (success or error). Collapsing queued and executing into one color erases meaningful information at a glance.
Tailwind utilities: text-status-success, bg-status-warning/20, border-status-error, etc.
Priority
Used by tasks and any other module that ranks records by urgency. Aligned with status tones where the meaning overlaps.
| Token | Light | Dark | Level |
|---|
--priority-urgent | oklch(0.52 0.19 25) | oklch(0.66 0.17 25) | Urgent |
--priority-high | oklch(0.58 0.15 55) | oklch(0.72 0.13 55) | High |
--priority-medium | oklch(0.62 0.14 90) | oklch(0.76 0.12 90) | Medium |
--priority-low | oklch(0.50 0.12 240) | oklch(0.65 0.11 240) | Low |
Tailwind utilities: text-priority-urgent, bg-priority-high/15, etc.
Chart palette
Five distinct hues, OKLch-balanced so adjacent series stay readable side by side. --chart-1 matches the brand color.
| Token | Light | Dark |
|---|
--chart-1 | oklch(0.50 0.09 175) | oklch(0.72 0.085 175) |
--chart-2 | oklch(0.55 0.12 145) | oklch(0.65 0.11 145) |
--chart-3 | oklch(0.50 0.14 250) | oklch(0.62 0.12 250) |
--chart-4 | oklch(0.60 0.14 70) | oklch(0.70 0.12 70) |
--chart-5 | oklch(0.50 0.14 290) | oklch(0.62 0.12 290) |
Recharts wrapper at components/ui/chart.tsx consumes these.
Accent palette
Twelve identity colors for category/tagging purposes — module badges, label dots, agent avatars, knowledge kinds. Never used for semantic state (use status tokens instead). Consumed via text-accent-blue, bg-accent-purple/20, etc.
| Token | Light | Dark |
|---|
--accent-blue | oklch(0.50 0.14 250) | oklch(0.67 0.12 250) |
--accent-purple | oklch(0.50 0.14 300) | oklch(0.68 0.12 300) |
--accent-emerald | oklch(0.50 0.12 145) | oklch(0.67 0.11 145) |
--accent-cyan | oklch(0.50 0.10 220) | oklch(0.68 0.09 220) |
--accent-violet | oklch(0.50 0.14 290) | oklch(0.67 0.12 290) |
--accent-amber | oklch(0.58 0.13 70) | oklch(0.74 0.12 70) |
--accent-teal | oklch(0.50 0.10 195) | oklch(0.67 0.09 195) |
--accent-orange | oklch(0.56 0.13 45) | oklch(0.70 0.12 45) |
--accent-pink | oklch(0.52 0.14 350) | oklch(0.68 0.12 350) |
--accent-slate | oklch(0.44 0.01 260) | oklch(0.58 0.01 260) |
--accent-indigo | oklch(0.48 0.14 275) | oklch(0.65 0.12 275) |
--accent-sky | oklch(0.50 0.12 235) | oklch(0.68 0.10 235) |
The shell sidebar has its own token group so it can sit slightly darker than the page background and still match the surface ladder. --sidebar-primary matches the brand color for active/highlighted items.
--sidebar, --sidebar-foreground, --sidebar-primary, --sidebar-primary-foreground, --sidebar-accent, --sidebar-accent-foreground, --sidebar-border, --sidebar-ring. Defined in both themes. Consumed via Tailwind utilities bg-sidebar, text-sidebar-foreground, etc.
Typography
Fonts
apps/ui/src/app/layout.tsx loads Geist via next/font/google:
- Geist Sans →
--font-geist-sans → font-sans utility. Body, headings, controls.
- Geist Mono →
--font-geist-mono → font-mono utility. Code blocks, IDs, timestamps when monospaced alignment matters.
OpenType features cv11, ss01, ss03 are enabled globally in body. They sharpen digits and disambiguate zeros — important for tables full of IDs and counts. Antialiasing and text-rendering: optimizeLegibility are also set globally. Tables and any element with class .tabular get font-variant-numeric: tabular-nums.
Type scale
Six steps, defined as utilities. The base is 13px — anything larger is intentional emphasis.
| Class | Size | Line height | Weight | Tracking | Transform | Color |
|---|
.text-display | 24px (1.5rem) | 1.2 | 600 | -0.02em | — | --foreground |
.text-title | 18px (1.125rem) | 1.3 | 600 | -0.01em | — | --foreground |
.text-heading | 15px (0.9375rem) | 1.4 | 600 | — | — | --foreground |
.text-body | 13px (0.8125rem) | 1.5 | 400 | — | — | --foreground |
.text-label | 11px (0.6875rem) | 1.3 | 500 | 0.04em | UPPERCASE | --muted-foreground |
.text-caption | 11px (0.6875rem) | 1.4 | 400 | — | — | --muted-foreground |
When to use which:
.text-display — page titles in the page header.
.text-title — section headings inside a detail view, modal titles.
.text-heading — card titles, list-group headers.
.text-body — default. The base reading size.
.text-label — form labels, sidebar group headers, table column headers, metadata captions over values.
.text-caption — timestamps, helper text, “showing 12 of 84” counts.
Editor typography
The Novel/TipTap editor in knowledge pages and skills has its own scale, defined in globals.css lines 265-450. H1 is 30px, H2 is 22px, H3 is 18px — larger than the app scale because long-form reading benefits from more vertical rhythm. Body lines render at line-height: 1.7 inside .ProseMirror.
Radius
A single base, derived scale.
--radius: 0.375rem; /* 6px — base */
--radius-sm: calc(var(--radius) - 4px); /* 2px */
--radius-md: calc(var(--radius) - 2px); /* 4px */
--radius-lg: var(--radius); /* 6px */
--radius-xl: calc(var(--radius) + 4px); /* 10px */
--radius-2xl: calc(var(--radius) + 8px); /* 14px */
--radius-3xl: calc(var(--radius) + 12px);/* 18px */
--radius-4xl: calc(var(--radius) + 16px);/* 22px */
Tailwind utilities: rounded-sm, rounded-md, rounded-lg, rounded-xl, rounded-2xl, etc.
When to use which:
rounded-md — buttons, inputs, badges, default fills.
rounded-lg — cards, popovers, dialogs, sheets.
rounded-xl and above — hero surfaces, full-bleed panels.
rounded-full — avatars, status dots, kbd chips, single-character tokens.
Spacing
Tailwind’s default spacing scale (0, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, …) is the contract. We do not override it.
Density conventions for layout:
- Form rows: 8px gap (
gap-2) between label and control, 12px gap (gap-3) between rows.
- Page sections: 20px padding (
px-5), 20px between section blocks.
- Card content: 16px padding (
p-4), 12px gap between elements.
- Sidebar groups: 8px padding, items 28px tall.
- Toolbars and headers: 12px gap (
gap-3) between controls.
If a layout needs an off-scale value, the right move is almost always to pick the next step up or down, not to inline a custom number.
Motion
Three durations, two easings. Animation is deliberate — used to clarify state changes (a sheet sliding in, a row reordering), not to entertain. Defined once in :root only; motion does not change between themes.
--duration-fast: 120ms;
--duration-normal: 180ms;
--duration-slow: 280ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
When to use which:
- 120ms — micro-interactions: hover, focus rings, dropdown opens, tooltip appearances.
- 180ms — default. Most transitions, including theme accents and small position changes.
- 280ms — full-surface motion: sheets sliding in, drawers opening, modal mounts.
--ease-out for entrances and most movement. --ease-in-out only for bidirectional transitions (something that animates in and back out the same way).
The animation library is tw-animate-css, giving us animate-in, animate-out, fade-in-0, slide-in-from-top-2, zoom-in-95, etc. We do not use Framer Motion. If a motion can be expressed as a CSS transition or a Tailwind animation utility, that is the answer.
Theming
Light/Dark switching
Theme switching is instant, class-driven, and does not animate. next-themes is wired up at the root layout with:
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
The mechanism: next-themes toggles a dark class on <html>. The :root declarations in globals.css define the light palette. The .dark block overrides every token. Components reference tokens by name only — they never branch on theme.
disableTransitionOnChange is on intentionally. We do not animate every element on the page when the user flips themes — it would be visually loud and slow. The flip is a single-frame swap.
The toggle component lives at components/theme-toggle.tsx — a dropdown with Light / Dark / System and a Sun ↔ Moon icon transition.
User-customizable theme (Settings → Appearance)
The theme system supports per-workspace customization at two tiers:
Simple tier — covers 90% of use cases:
- Brand color — a single OKLch color from which
--primary, --ring, --sidebar-primary, and --chart-1 are derived. Preset swatches + native color picker.
- Mode — Light / Dark / System.
- Surface warmth — controls the chroma on neutral surfaces (0 = cool gray, 0.008 = warm). Adjusts the warm tint at hue 75 across all surfaces.
Advanced tier — expandable section for OSS users:
- Override individual tokens: surfaces, semantic colors, the 12-accent palette, text colors, borders.
- Each override has a “Reset to derived” button so users can selectively pin values while the rest auto-derive from the brand color.
Architecture:
lib/theme/types.ts — ThemeConfig, OklchColor, ThemeTokens types.
lib/theme/derive.ts — deriveLightTokens(brand, warmth) and deriveDarkTokens(brand, warmth) generate all CSS variable values. Also provides oklchToHex() / hexToOklch() converters.
components/theme-applier.tsx — client component in the root layout, reads workspace.settings.theme from Supabase and injects a <style> tag overriding CSS variables.
app/dashboard/settings/appearance/page.tsx — the settings UI.
- Persistence: stored in the workspace
settings JSON column under the theme key.
Rules for contributors:
- Never branch on
isDark in component code — tokens handle it.
- Never hardcode hex/rgb/hsl. Use Tailwind utilities that resolve to CSS variables.
- If you add a new interactive element, use
bg-primary / text-primary for the active state — it will automatically pick up whatever brand color the user has configured.
- The derivation engine can be extended. To add a new derived token, update
deriveLightTokens() and deriveDarkTokens() in lib/theme/derive.ts.
Iconography
lucide-react is the only icon library. No custom icon set, no second pack.
- Default size — 16px. Set globally via
[&_svg:not([class*='size-'])]:size-4 on container components, so any icon dropped inside a button, badge, or list item without an explicit size renders at 16px.
- Stroke width — Lucide default (1.5px). Don’t override unless designing a one-off display element.
- Two icons should never mean the same thing. If a Lucide icon is needed in two roles, pick a second icon for the second role.
When choosing an icon, prefer the most literal name in Lucide. If nothing fits, the action probably needs a clearer label, not an icon at all.
Tailwind and shadcn configuration
- Tailwind v4, no
tailwind.config.js. Theme lives in the @theme inline block at the top of globals.css.
- shadcn/ui configured at
apps/ui/components.json: style new-york, base color neutral, CSS variables enabled, no class prefix, RSC-aware, Lucide icons.
- Aliases —
@/components, @/lib/utils, @/components/ui, @/lib, @/hooks. Always import via aliases; never relative-cross paths.
- Plugins —
tw-animate-css (animation utilities), @tailwindcss/typography (only for the editor’s .ProseMirror styles, not for body content).
Adding a new token
When a screen needs a value that no existing token covers:
- Confirm the role is semantic, not stylistic. “We need a slightly darker red” is not a new token. “We need a token for the ‘snoozed’ state on tasks” is.
- Add the variable in
globals.css. Both :root and .dark if the role is theme-dependent (color, surface, text). :root only if it is theme-invariant (motion, radius, spacing).
- Wire it into
@theme inline with a --color-*, --radius-*, or matching alias so Tailwind utilities pick it up.
- Use the Tailwind utility everywhere. Never reach back to
var(--…) in component code unless the value is being passed into a CSS-in-JS or inline style attribute that does not resolve utilities.
The goal is that any future contributor reading globals.css can see the entire visual language at a glance, in one file.