shadcn-svelte
shadcn-svelte is the default component set for all Kinda products. Cairn provides the CSS tokens, theming model, and usage guidelines — shadcn provides the component implementations.
app.css overrides shadcn's default palette by
redefining --background, --primary, --border, and related variables
with Cairn tokens. Every shadcn component picks up Cairn's stone neutrals, brand colour, and radius scale
automatically — no per-component changes needed for base styling.
src/app.css and replace it with app.css from this
repo. Add the fonts to src/app.html:+layout.svelte, set data-theme on the
html element. The theme name maps to a [data-theme] block in app.css which
defines the 7 brand primitive tokens.app.css overrides with
Cairn tokens. All shadcn components reference these variables — setting them correctly themes the entire
component set.| shadcn variable | Cairn token | Light | Purpose |
|---|---|---|---|
--background |
neutral | #F5F5F4 |
App canvas — stone warm white, never pure grey |
--foreground |
ink | #18181A |
Primary text |
--card |
neutral-raised | #FFFFFF |
Cards and panels — white on stone canvas creates surface hierarchy |
--primary |
primary | #5B6EE1 |
Brand colour — buttons, focus rings, active states. Dark: shifts to primary-light |
--muted |
neutral | #F5F5F4 |
Ghost elements, placeholder backgrounds, disabled areas |
--muted-foreground |
neutral-mid | #78716C |
Secondary and tertiary text |
--accent |
primary-muted | #E8EAFF |
Ghost hover, sidebar active bg, selected row backgrounds |
--accent-foreground |
primary-text | #3A4DC8 |
Text on accent backgrounds |
--destructive |
error | #E05B40 |
Ember — destructive actions only, never a red brand variant |
--border |
neutral-border | #E5E5E3 |
All structural borders — used at 0.5px throughout |
--ring |
primary | #5B6EE1 |
Focus ring colour — focus-visible rule applies 12% opacity |
--radius |
rounded.lg | 12px |
Base radius — shadcn derives sm (~4px), md (~8px), xl (~16px) from this |
--sidebar |
primary-dark-nav | #0F1330 |
Always dark — sidebar is product context, not content area |
| Component | Override | Why |
|---|---|---|
| Button | Replace cursor-pointer with cursor-default in buttonVariants base class
|
Desktop app convention — pointer is for links, not actions |
| Badge | Add success, warning, outline variants to
badgeVariants |
shadcn default maps to badge-neutral only; Cairn needs 5 semantic states |
| Table row | Add hover:bg-muted transition-colors |
No zebra stripes — hover highlight only per Cairn table spec |
| Table head | Add text-[11px] uppercase tracking-[0.06em] text-muted-foreground |
Receded header style — eye lands on data, not labels |
| Separator | Add opacity-50 |
All app borders are 0.5px (50% opacity on a 1px element) |
.surface-marketing. shadcn components
inside automatically inherit --mkt- token overrides for the dark canvas context. Use
class="eyebrow" for section labels (12px / 600 / +0.09em / uppercase / brand-400).Design.md
The Design.md file is the machine-readable source of truth for the Cairn design system. It is the first thing any coding agent or AI tool should read before generating UI for a Kinda product.
typography
rounded
spacing
components
Colors
Typography
Components
Do's and Don'ts
Token mapping
Component overrides
New theme steps
@google/design.md CLI can lint the
file for broken references and WCAG contrast issues, and export tokens to Tailwind v4 CSS or DTCG format:
Architecture
Cairn uses three layers of design tokens. Components only ever reference semantic tokens — they are unaware of which product is active or which mode is set.
color-interactive, not a hex value. Swap the hex behind that token and
every component updates automatically.
stone-200
sage-600
radius-lg
color-surface
color-text-primary
nav-background
badge-text
focus-ring
Two surfaces
The system supports two fundamentally different surface types. They share the same token foundation but follow different rules for how those tokens are deployed.
.surface-marketing wrapper
class and a --mkt- token namespace that never bleeds into app components.
Typography
Space Grotesk across all surfaces. Space Mono for code, keys, and financial tables. The pairing shares design DNA — intentional, not assembled.
Why 600 weight only at 22px+? Space Grotesk 500 is optically heavier than Inter 500. Using 600 at body or label sizes produces a heavy, overworked feeling. 600 is reserved for display-scale headings where the extra weight reads as editorial rather than aggressive.
your growth comes from
Colour
Indigo is the default brand colour (Orbit product). Each product defines its own brand ramp. Stone neutrals carry all app surface structure. Sage and Ember are status-only.
Why is brand colour restricted to interactive elements? Every use of brand colour in the app UI should answer "is this something I can interact with?" If the answer is no, the colour shouldn't be there. Decorative brand colour dilutes the interactive signal and makes the UI harder to scan.
Spacing
4px base unit. All spacing is a multiple of 4. Page gutters: 24px mobile / 40px desktop (app). Marketing sections: 80px vertical padding desktop / 48px mobile.
Border radius
Five stops. Each maps to a specific element type — don't use radius-xl on a button or radius-sm on a modal.
Elevation
Elevation is expressed through border weight and surface contrast — never box shadows in the app UI.
| Use | Style | Exception |
|---|---|---|
| Default border | 0.5px solid color-border |
— |
| Hover / emphasis | 0.5px solid color-border-secondary |
— |
| Featured item | 1px solid color-interactive |
Only exception to 0.5px rule |
| App UI shadows | Never | — |
| Marketing CTA glow | box-shadow: 0 0 24px rgba(91,110,225,0.25) |
CTA buttons on dark canvas only |
Motion
Motion rules differ significantly between surfaces. App motion is nearly invisible. Marketing motion is choreographed.
Buttons
Four variants. One primary per view. No gradients on any button variant.
| Variant | Height | Font | Use |
|---|---|---|---|
| Primary | 36px | 14px / 500 | One per view. The most important action. |
| Secondary | 36px | 14px / 500 | Alternative actions. Can appear alongside primary. |
| Ghost | 36px | 14px / 500 | Tertiary actions, toolbar actions, low-emphasis. |
| Destructive | 36px | 14px / 500 | Delete, remove. Ember colour. Never use primary red. |
Badges
Pill shape. 12px / 500. Used for status, plan tier, and filterable tags.
Form inputs
36px height. Focus ring uses brand colour at 12% opacity — visible without being distracting.
Data tables
No zebra stripes. Hover highlight only. Numeric columns right-aligned with tabular numerals. Density: comfortable (44px) default, compact (32px) available.
| Customer | Plan | MRR | Joined | Status |
|---|---|---|---|---|
|
AL Acme Labs
|
Enterprise | £4,200 | Jan 2023 | Active |
|
CC Cedar Co.
|
Starter | £149 | Jun 2023 | Churned |
|
DS Datastream
|
Enterprise | £6,500 | Nov 2022 | Active |
Marketing buttons
More presence than app buttons. The ghost uses a visible white border — the app ghost would disappear on the dark canvas.
Section patterns
Three reusable section layouts. One idea per section. Consistent vertical padding (80px desktop / 48px mobile) creates rhythm.
Pricing cards
Featured plan uses a 1px brand-colour border — the only 1px border in the system. The card background is never filled with brand colour.
The handoff moment
The transition from marketing to app is a designed moment, not just a route change. The visual jump is intentional — it signals the user has crossed a threshold.
Per-product theming
Each product defines 7 brand tokens under a data-theme
attribute. Everything else is shared.
--color-interactive directly, you lose the ability to control light/dark mode separately. The
brand primitive tokens (--brand-500, etc.) sit between primitives and semantic tokens —
semantic tokens reference brand tokens, brand tokens are set per product. This means a product theme file
only needs to define 7 values, and the entire semantic layer — including dark mode variants — updates
automatically.
Important caveat: not all brand colours work in all positions. A pale or low-saturation brand colour cannot be used as a nav background the same way deep indigo can. Each product's theme file must be authored with care —
--brand-dark-nav in particular may need to be an independently chosen
dark value rather than a mechanical darkening of the brand colour.
Dark mode
Semantic tokens absorb the mode switch. Components never know which mode is active.
Creating a new theme
Each product requires exactly 7 brand primitive tokens. Everything else — typography, spacing, neutral palette, radius, component behaviour — is shared and updates automatically when those 7 values change.
--brand-dark-nav. Not all brand
colours work in all positions. A pale or low-saturation brand colour cannot serve as a nav background the
same way deep indigo can. --brand-dark-nav should be independently chosen — a dark surface that
feels right as a sidebar, not a mathematical shade derived from the brand. Treat it as a dedicated
dark-surface colour that happens to share the brand's hue family.
| Token | Used in | Contrast requirement |
|---|---|---|
--brand-500 |
Primary buttons, focus rings, active nav items, links | ≥ 4.5:1 on white (#FFFFFF) — WCAG AA |
--brand-400 |
Dark mode buttons, marketing eyebrow text | ≥ 4.5:1 on #0D0D12 (marketing canvas) |
--brand-50 |
Badge backgrounds, focus fill, hover states, selected rows | Must support brand-700 text at ≥ 4.5:1 |
--brand-700 |
Text on brand-50 backgrounds, badge labels | ≥ 4.5:1 on brand-50 — WCAG AA |
--brand-900 |
Light mode nav background | White text at 45% opacity must be legible |
--brand-dark-muted |
Dark mode badge fill, muted interactive backgrounds | Must be dark — a tint (50-stop) would read as light bleed |
--brand-dark-nav |
Dark mode nav background | White text at 100% must pass AA ≥ 4.5:1 |
| Check | Pass condition |
|---|---|
| brand-500 on white | ≥ 4.5:1 contrast ratio (WCAG AA) |
| brand-400 on #0D0D12 | ≥ 4.5:1 contrast ratio (WCAG AA) |
| brand-700 on brand-50 | ≥ 4.5:1 contrast ratio (WCAG AA) |
| White (45% opacity) on brand-900 | Nav resting state legible |
| White (100%) on brand-dark-nav | ≥ 4.5:1 contrast ratio |
| brand-dark-nav independently authored | Not a mechanical darkening of brand-900 |
| data-theme set on <html> in root layout | All semantic tokens resolve through brand layer |
| Run Design.md linter | npx @google/design.md lint Design.md exits 0 |