# RamosLabs Design System > A mono-indigo, taxonomy-first design system. Tokens are the single source of truth. This is the complete, self-contained reference for AI agents building UI on the RamosLabs Design System. It inlines every documentation page and the full token table. Free to use under the MIT License. Keep the copyright and license notice, and credit RamosLabs. © 2026 RAMOS SOLUTIONS S.A.S.. https://ramoslabs.com Install: `bun add @ramoslabs/tokens` (public npm), then `@import '@ramoslabs/tokens/css'`. ## Token reference (213 tokens) ### breakpoint (5) | Token | Value | | --- | --- | | `--breakpoint-sm` | `576px` | | `--breakpoint-md` | `769px` | | `--breakpoint-lg` | `992px` | | `--breakpoint-xl` | `1200px` | | `--breakpoint-2xl` | `1366px` | ### color (130) | Token | Value | | --- | --- | | `--color-white` | `#ffffff` | | `--color-black` | `#000000` | | `--color-indigo-50` | `#eef2ff` | | `--color-indigo-100` | `#e0e7ff` | | `--color-indigo-200` | `#c7d2fe` | | `--color-indigo-300` | `#a5b4fc` | | `--color-indigo-400` | `#818cf8` | | `--color-indigo-500` | `#6366f1` | | `--color-indigo-600` | `#4f46e5` | | `--color-indigo-700` | `#4338ca` | | `--color-indigo-800` | `#3730a3` | | `--color-indigo-900` | `#312e81` | | `--color-indigo-950` | `#1e1b4b` | | `--color-slate-50` | `#f8fafc` | | `--color-slate-100` | `#f1f5f9` | | `--color-slate-200` | `#e2e8f0` | | `--color-slate-300` | `#cbd5e1` | | `--color-slate-400` | `#94a3b8` | | `--color-slate-500` | `#64748b` | | `--color-slate-600` | `#475569` | | `--color-slate-700` | `#334155` | | `--color-slate-800` | `#1e293b` | | `--color-slate-900` | `#0f172a` | | `--color-slate-950` | `#020617` | | `--color-emerald-50` | `#ecfdf5` | | `--color-emerald-100` | `#d1fae5` | | `--color-emerald-200` | `#a7f3d0` | | `--color-emerald-300` | `#6ee7b7` | | `--color-emerald-400` | `#34d399` | | `--color-emerald-500` | `#10b981` | | `--color-emerald-600` | `#059669` | | `--color-emerald-700` | `#047857` | | `--color-emerald-800` | `#065f46` | | `--color-emerald-900` | `#064e3b` | | `--color-emerald-950` | `#022c22` | | `--color-green-50` | `#f0fdf4` | | `--color-green-100` | `#dcfce7` | | `--color-green-200` | `#bbf7d0` | | `--color-green-300` | `#86efac` | | `--color-green-400` | `#4ade80` | | `--color-green-500` | `#22c55e` | | `--color-green-600` | `#16a34a` | | `--color-green-700` | `#15803d` | | `--color-green-800` | `#166534` | | `--color-green-900` | `#14532d` | | `--color-green-950` | `#052e16` | | `--color-amber-50` | `#fffbeb` | | `--color-amber-100` | `#fef3c7` | | `--color-amber-200` | `#fde68a` | | `--color-amber-300` | `#fcd34d` | | `--color-amber-400` | `#fbbf24` | | `--color-amber-500` | `#f59e0b` | | `--color-amber-600` | `#d97706` | | `--color-amber-700` | `#b45309` | | `--color-amber-800` | `#92400e` | | `--color-amber-900` | `#78350f` | | `--color-amber-950` | `#451a03` | | `--color-red-50` | `#fef2f2` | | `--color-red-100` | `#fee2e2` | | `--color-red-200` | `#fecaca` | | `--color-red-300` | `#fca5a5` | | `--color-red-400` | `#f87171` | | `--color-red-500` | `#ef4444` | | `--color-red-600` | `#dc2626` | | `--color-red-700` | `#b91c1c` | | `--color-red-800` | `#991b1b` | | `--color-red-900` | `#7f1d1d` | | `--color-red-950` | `#450a0a` | | `--color-blue-50` | `#eff6ff` | | `--color-blue-100` | `#dbeafe` | | `--color-blue-200` | `#bfdbfe` | | `--color-blue-300` | `#93c5fd` | | `--color-blue-400` | `#60a5fa` | | `--color-blue-500` | `#3b82f6` | | `--color-blue-600` | `#2563eb` | | `--color-blue-700` | `#1d4ed8` | | `--color-blue-800` | `#1e40af` | | `--color-blue-900` | `#1e3a8a` | | `--color-blue-950` | `#172554` | | `--color-violet-50` | `#f5f3ff` | | `--color-violet-100` | `#ede9fe` | | `--color-violet-200` | `#ddd6fe` | | `--color-violet-300` | `#c4b5fd` | | `--color-violet-400` | `#a78bfa` | | `--color-violet-500` | `#8b5cf6` | | `--color-violet-600` | `#7c3aed` | | `--color-violet-700` | `#6d28d9` | | `--color-violet-800` | `#5b21b6` | | `--color-violet-900` | `#4c1d95` | | `--color-violet-950` | `#2e1065` | | `--color-primary` | `#4f46e5` | | `--color-primary-light` | `#6366f1` | | `--color-primary-dark` | `#4338ca` | | `--color-primary-surface` | `#eef2ff` | | `--color-secondary` | `#8b5cf6` | | `--color-secondary-light` | `#a78bfa` | | `--color-secondary-dark` | `#7c3aed` | | `--color-secondary-surface` | `#f5f3ff` | | `--color-success` | `#10b981` | | `--color-success-strong` | `#059669` | | `--color-success-surface` | `#f0fdf4` | | `--color-success-border` | `#bbf7d0` | | `--color-success-text` | `#166534` | | `--color-warning` | `#f59e0b` | | `--color-warning-surface` | `#fffbeb` | | `--color-warning-border` | `#fde68a` | | `--color-warning-text` | `#78350f` | | `--color-error` | `#ef4444` | | `--color-error-strong` | `#dc2626` | | `--color-error-surface` | `#fef2f2` | | `--color-error-border` | `#fecaca` | | `--color-error-text` | `#991b1b` | | `--color-info` | `#3b82f6` | | `--color-info-surface` | `#eff6ff` | | `--color-info-border` | `#bfdbfe` | | `--color-info-text` | `#1e40af` | | `--color-text-heading` | `#0f172a` | | `--color-text-strong` | `#1e293b` | | `--color-text-secondary` | `#475569` | | `--color-text-body` | `#64748b` | | `--color-text-muted` | `#64748b` | | `--color-text-disabled` | `#cbd5e1` | | `--color-surface` | `#ffffff` | | `--color-surface-secondary` | `#f8fafc` | | `--color-surface-hover` | `#f1f5f9` | | `--color-background` | `#f8fafc` | | `--color-border` | `#e6e8eb` | | `--color-border-light` | `#e2e8f0` | | `--color-border-subtle` | `#f1f5f9` | | `--color-focus` | `#4f46e5` | ### duration (3) | Token | Value | | --- | --- | | `--duration-fast` | `150ms` | | `--duration-normal` | `200ms` | | `--duration-slow` | `300ms` | ### easing (5) | Token | Value | | --- | --- | | `--easing-linear` | `cubic-bezier(0, 0, 1, 1)` | | `--easing-in` | `cubic-bezier(0.4, 0, 1, 1)` | | `--easing-out` | `cubic-bezier(0, 0, 0.2, 1)` | | `--easing-in-out` | `cubic-bezier(0.4, 0, 0.2, 1)` | | `--easing-spring` | `cubic-bezier(0.34, 1.56, 0.64, 1)` | ### radius (8) | Token | Value | | --- | --- | | `--radius-none` | `0` | | `--radius-xs` | `0.25rem` | | `--radius-sm` | `0.375rem` | | `--radius-md` | `0.5rem` | | `--radius-lg` | `0.75rem` | | `--radius-xl` | `1rem` | | `--radius-2xl` | `1.25rem` | | `--radius-pill` | `9999px` | ### shadow (4) | Token | Value | | --- | --- | | `--shadow-sm` | `0 1px 2px 0 rgba(79, 70, 229, 0.06), 0 1px 3px 0 rgba(15, 23, 42, 0.04)` | | `--shadow-md` | `0 2px 4px -1px rgba(79, 70, 229, 0.06), 0 8px 16px -4px rgba(79, 70, 229, 0.10)` | | `--shadow-lg` | `0 4px 8px -2px rgba(79, 70, 229, 0.06), 0 16px 32px -8px rgba(79, 70, 229, 0.12)` | | `--shadow-focus` | `0 0 0 2px #ffffff, 0 0 0 4px #4f46e5` | ### space (14) | Token | Value | | --- | --- | | `--space-0` | `0` | | `--space-1` | `0.25rem` | | `--space-2` | `0.5rem` | | `--space-3` | `0.75rem` | | `--space-4` | `1rem` | | `--space-5` | `1.25rem` | | `--space-6` | `1.5rem` | | `--space-8` | `2rem` | | `--space-10` | `2.5rem` | | `--space-12` | `3rem` | | `--space-16` | `4rem` | | `--space-20` | `5rem` | | `--space-24` | `6rem` | | `--space-32` | `8rem` | ### state (4) | Token | Value | | --- | --- | | `--state-hover` | `0.08` | | `--state-focus` | `0.1` | | `--state-pressed` | `0.1` | | `--state-dragged` | `0.16` | ### font (21) | Token | Value | | --- | --- | | `--font-family-sans` | `Rubik, system-ui, sans-serif` | | `--font-family-display` | `'Red Hat Display', system-ui, sans-serif` | | `--font-family-alt` | `Roboto, system-ui, sans-serif` | | `--font-weight-thin` | `100` | | `--font-weight-extralight` | `200` | | `--font-weight-light` | `300` | | `--font-weight-normal` | `400` | | `--font-weight-medium` | `500` | | `--font-weight-semibold` | `600` | | `--font-weight-bold` | `700` | | `--font-weight-extrabold` | `800` | | `--font-size-xs` | `0.75rem` | | `--font-size-sm` | `0.875rem` | | `--font-size-base` | `1rem` | | `--font-size-lg` | `1.125rem` | | `--font-size-xl` | `1.25rem` | | `--font-size-2xl` | `1.5rem` | | `--font-size-3xl` | `1.875rem` | | `--font-size-4xl` | `2.25rem` | | `--font-size-5xl` | `3rem` | | `--font-size-6xl` | `3.75rem` | ### leading (5) | Token | Value | | --- | --- | | `--leading-none` | `1` | | `--leading-tight` | `1.1` | | `--leading-snug` | `1.25` | | `--leading-normal` | `1.5` | | `--leading-relaxed` | `1.625` | ### tracking (5) | Token | Value | | --- | --- | | `--tracking-tighter` | `-0.05em` | | `--tracking-tight` | `-0.025em` | | `--tracking-normal` | `0em` | | `--tracking-wide` | `0.025em` | | `--tracking-wider` | `0.05em` | ### z (9) | Token | Value | | --- | --- | | `--z-dropdown` | `100` | | `--z-sticky` | `200` | | `--z-fixed` | `300` | | `--z-fab` | `400` | | `--z-modal-backdrop` | `500` | | `--z-modal` | `600` | | `--z-popover` | `700` | | `--z-tooltip` | `800` | | `--z-toast` | `900` | # Introduction Source: https://design.ramoslabs.com/ The RamosLabs Design System is a taxonomy book, not a component bundle. One indigo accent, one token contract, and the rules that turn them into a UI library you own. The whole system in four numbers: - 213 design tokens - 3 taxonomy tiers - AA contrast floor - 1 accent color ## Three tiers, one direction - Tier 1, Foundations: the token contract. Color, type, space, radius, shadow, motion. Start here. References primitives. - Tier 2, Patterns: recipes built from foundations. States, forms, accessibility, voice. References foundations. - Tier 3, Components: SJ and W UI, built only from patterns and foundations. References patterns and foundations. The one rule: a tier may reference the tier below, never above. That single constraint keeps the system coherent as it grows. ## The name tells you where it lives - `SJ` prefix: shared, domain-free UI. `SJButton`, `SJInput`. Built only from tokens and patterns. No business logic. - `W` prefix: app-specific widget. `WEventCard`, `WCheckoutSummary`. Composes SJ components and holds the domain logic. - `--token`: the value contract. `var(--color-primary)`, `var(--space-6)`. The only source of color, space, and type. Never a raw hex. ## Authored once, compiled everywhere The pipeline: `tokens.json` (DTCG) to Style Dictionary to CSS variables plus TypeScript. Change a token once and every surface that references it updates in step. The DTCG JSON is the contract; nothing downstream is edited by hand. ```css /* app entry, once */ import '@ramoslabs/tokens/css'; /* then build against the contract */ .sj-card { background: var(--color-surface); padding: var(--space-6); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); } ``` ## Sources Built on the W3C Design Tokens spec, Atomic Design, and WCAG 2.1. # Installation Source: https://design.ramoslabs.com/installation/ Install `@ramoslabs/tokens` from npm, import the CSS variables once at your app entry, and reference the tokens everywhere. The tokens are the single source of truth; nothing hardcodes a value. ## 1. Install the tokens The package is published on the public npm scope. Use any package manager: `bun add @ramoslabs/tokens`, `npm install @ramoslabs/tokens`, `pnpm add @ramoslabs/tokens`, or `yarn add @ramoslabs/tokens`. ## 2. Import the CSS once At your app entry, import the generated CSS variables a single time: `@import '@ramoslabs/tokens/css';`. Every `--color-*`, `--space-*`, `--radius-*`, `--shadow-*`, `--font-*`, and motion variable is then available globally. ## 3. Build against the contract Reference the tokens in your styles — never a raw value. For example a card reads `background: var(--color-surface); padding: var(--space-6); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm);`. A raw hex or px in your CSS is a defect. Typed values are available from the package root for TypeScript, and the flat name-to-value map is `@ramoslabs/tokens/json`. ## Without installing You do not have to install the package to consume the system. Read the served flat token map at `/tokens.json`, or `@import` the served stylesheet from the documentation site. This is useful for a quick prototype or a build that cannot add the dependency. ## Framework notes The tokens are framework-agnostic CSS variables and typed values — they work in any stack (plain CSS, Vue, React, Svelte, Astro, Tailwind via CSS variables). Import the CSS once at the root; in a component framework that means the app entry or root layout, not each component. ## For AI agents An agent should not fetch or scrape these values by hand. Connect the MCP server and use `get_token` and `check_contrast` instead — see the For Agents and MCP Server pages. Components (`SJ`/`W`) are not shipped yet; until they land, build from these tokens and the documented patterns. # For Agents Source: https://design.ramoslabs.com/agents/ Agent-native. MCP-first. AI-ready by design. This design system is built to be consumed by an AI agent, not just read by a human. Every rule, token, and pattern is available as machine-readable data and as typed, verifiable tools, so an agent builds against the contract instead of guessing from rendered HTML. ## The contract, in one place Six artifacts make the whole system legible to a machine. All are served from the site root and are free to fetch, read-only, and need no auth. - **MCP server** (`/mcp`) — the design system as seven typed tools: `search_tokens`, `get_token`, `check_contrast`, `list_docs`, `get_doc`, `lint_css`, `get_agents_guide`. Streamable HTTP, read-only, no auth. This is the highest-leverage way to consume the system: the tools return structured, verified data with no interpretation and no invention. See the MCP Server page. - **Agent skill** (`/skill.md`) — a Claude Code skill that makes an agent build and review UI strictly by this system, never inventing a token, value, or rule. Copy it to `.claude/skills/ramoslabs-ds/SKILL.md` in a project, or `~/.claude/skills/…` globally. - **llms.txt** (`/llms.txt`) — a concise index of every documentation page with its URL and summary. - **llms-full.txt** (`/llms-full.txt`) — the entire system inlined into one self-contained document, plus the full token table. Fetch this when you want everything in context at once. - **registry.json** (`/registry.json`) — structured tokens-by-category, the pattern list, and the components array, as JSON. - **AGENTS.md** (`/AGENTS.md`) — the full agent onboarding guide: the hard contract and how to apply it. - **tokens.json** (`/tokens.json`) — the flat DTCG token map (name to resolved value). Look up the exact token here before typing any value. ## Verify, do not scrape The difference that matters: these tools are for correctness, not just discovery. `check_contrast` returns the WCAG ratio and the AA/AAA verdicts. `lint_css` flags a raw hex, rgb, px, or rem that should be a token and suggests the token. `get_token` returns the exact value and the `var(--…)` to use, and errors if the token does not exist so an agent can never invent one. An agent connected to the server builds against a contract it cannot get wrong, instead of interpreting a screenshot or a page of HTML. ## The one hard rule still applies Everything an agent produces follows the same contract a human does: never hardcode a color, spacing, typography, radius, shadow, or motion value — read the token. Indigo 600 is the single action accent. WCAG 2.1 AA is the floor. The agent layer exists to make that contract easy to honor and impossible to fake. # MCP Server Source: https://design.ramoslabs.com/agents/mcp/ The design system ships a Model Context Protocol (MCP) server so any MCP-capable agent (Claude Code, Cursor, Windsurf, and others) gets the system as typed, verifiable tools instead of scraping HTML or fetching files. It is stateless, read-only, and needs no auth. Transport is Streamable HTTP; the endpoint is `POST /mcp`. ## The seven tools - `search_tokens(query, limit?)` — find tokens by name or value substring. - `get_token(name)` — the exact token value plus the `var(--…)` to use. Errors if it does not exist, so an agent never invents one. - `check_contrast(foreground, background)` — the WCAG ratio plus AA/AAA verdicts for normal text, large text, and UI. - `list_docs()` — every documentation page title (foundations and patterns). - `get_doc(title)` — the full distilled doctrine for one page. Read it before implementing a pattern. - `lint_css(code)` — flag a raw hex, rgb, px, or rem that should be a token, and suggest the matching token. - `get_agents_guide()` — the full AGENTS.md agent guide. ## Not install — verify Most component MCP servers exist to discover and install code. This one exists so an agent gets the system right: contrast checked, CSS linted against the token contract, and token values returned exact and never invented. The tools are structured and verified, so the agent builds against a contract instead of interpreting rendered output. ## Connect it Claude Code: `claude mcp add --transport http ramoslabs-ds https://design.ramoslabs.com/mcp`. Other MCP clients (Cursor, Windsurf, …): add a server entry with the URL `https://design.ramoslabs.com/mcp` and transport `http`. The agent then has `search_tokens`, `check_contrast`, `lint_css`, and the rest as first-class tools — no WebFetch, no HTML interpretation, no invented values. ## Quick check Call `tools/list` against the endpoint to confirm the seven tools, then call `check_contrast` with the primary accent on white to confirm it verifies (indigo 600 on white returns roughly 6.3:1, which passes AA). # Colors Source: https://design.ramoslabs.com/foundations/colors/ RamosLabs runs on a single action accent. Indigo carries every deliberate action and nothing else competes for it. One voice for action, a deep neutral ramp for everything else, reserved signals for state. The action accent is `#4f46e5`. Violet appears as a decorative accent only, never for action. ## The accent, up close Eleven tested steps from light to dark. The interface lives at 600. Everything above tints, everything below deepens for hover and pressed states. Indigo `--color-indigo-*`: 50 `#eef2ff`, 100 `#e0e7ff`, 200 `#c7d2fe`, 300 `#a5b4fc`, 400 `#818cf8`, 500 `#6366f1`, 600 `#4f46e5` (the accent), 700 `#4338ca` (hover), 800 `#3730a3`, 900 `#312e81`, 950 `#1e1b4b`. Slate `--color-slate-*` (text, surfaces, borders, the 90% of the interface): 50 `#f8fafc`, 100 `#f1f5f9`, 200 `#e2e8f0`, 300 `#cbd5e1`, 400 `#94a3b8`, 500 `#64748b`, 600 `#475569`, 700 `#334155`, 800 `#1e293b`, 900 `#0f172a`, 950 `#020617`. Reserved for state, never decoration: - Emerald `--color-emerald-*`: success solid. - Green `--color-green-*`: success surface and text. - Amber `--color-amber-*`: warning. - Red `--color-red-*`: error. - Blue `--color-blue-*`: info. - Violet `--color-secondary`: decorative support only. ## Always reach for a role, not a raw color The ramps are primitives: raw hues, named by number, never by meaning. Product code never touches them. It consumes a role, a token named for the job it does. Example: `--color-indigo-600` is a primitive (a number, no job); `--color-primary` is the role (what you use). Re-theme by re-pointing roles, not by hunting hex across a codebase. Roles are theme-agnostic: a future dark theme re-points targets, it never renames the role. ## Roles in context - Primary action: rest `--color-primary`, hover `--color-primary-dark`, focus ring at `--color-focus`. The one accent for submit, confirm, active nav. - Selected / accent zone: ✓ Recommended: `--color-primary-surface` tint with primary-colored text. ✕ Avoid: white text on the tint, it fails contrast on the pale surface. - Text scale, one neutral hue: `--text-heading` 17.85:1, `--text-strong` 14.63:1, `--text-secondary` 7.58:1, `--text-body` 4.76:1, `--text-disabled` state only. - Feedback, each a full triad (strong solid + surface + text): - Success: `--color-success-strong` + `-surface` + `-text`. Strong solid for the icon so the white check clears 3:1. - Warning: `--color-warning` + `-surface` + `-text`. Amber is bright, so it carries dark text, never white. - Error: `--color-error` + `-surface` + `-text`. For a red button with white text, use `--color-error-strong`. - Info: `--color-info` + `-surface` + `-text`. Kept distinct from indigo so context never reads as an action. ## Contrast you can see Contrast is a floor. WCAG 2.2 asks 4.5:1 for normal text, 3:1 for large text and UI edges (AA); 7:1 is AAA. Measured pairings: - Primary on white: 6.29, AA - White on primary-dark: 7.90, AAA - Heading on white: 17.85, AAA - Body / muted on white: 4.76, AA - White on error-strong: 4.83, AA - White on error (red 500): 3.76, large only - Dark text on warning: 8.31, AAA - White on warning: 2.15, fail - Disabled on white: 1.48, by design Rule: pair every status color with an icon and a label, give every invalid field real text, and make focus a visible ring. Why: color is never the only channel (WCAG 2.2 SC 1.4.1), so meaning survives for color-blind and low-vision users. Color reinforces, it never carries meaning by itself. ## Sources WCAG 2.2 contrast floors SC 1.4.3 / 1.4.6 / 1.4.11 and use of color SC 1.4.1. Role-token model after Material 3 color roles and Refactoring UI numbered scales. The 60-30-10 accent balance is a common color composition rule. # Typography Source: https://design.ramoslabs.com/foundations/typography/ RamosLabs types on three families and one ten-step scale. Every piece of text takes a role (Display, Headline, Title, Body, or Label), and the role hands it a family, size, weight, line-height, and tracking. Pick the role; the type takes care of itself. ## Three voices - `--font-family-display`, Red Hat Display: geometric, slightly condensed, built to carry weight above 36px. Reserved for Display and Headline. Below the Title role its personality costs legibility. - `--font-family-sans`, Rubik: humanist, large x-height, open apertures, distinct at small sizes. The default for the whole product. If unsure which family to use, it is this one. - `--font-family-alt`, Roboto: metric-compatible fallback for dense system UI, embedded documents, or while web fonts load. Use it for a whole block on its own, never mixed with Rubik. ## One scale, ten steps 12px metadata to 60px display, a modular scale. 16px is the floor for body reading. Nothing on a surface sits between these steps. - `--font-size-6xl`: 3.75rem 60px, Display - `--font-size-5xl`: 3rem 48px, Display - `--font-size-4xl`: 2.25rem 36px, Display - `--font-size-3xl`: 1.875rem 30px, Headline - `--font-size-2xl`: 1.5rem 24px, Headline - `--font-size-xl`: 1.25rem 20px, Title - `--font-size-lg`: 1.125rem 18px, Title - `--font-size-base`: 1rem 16px, Body, the reading floor - `--font-size-sm`: 0.875rem 14px, Label, helper and labels - `--font-size-xs`: 0.75rem 12px, Label, metadata only ## Every role, one recipe Design and build against the role, never against a raw pixel. Five groups, after the Material 3 model. | Role | Family | Weight | Leading | Tracking | | --- | --- | --- | --- | --- | | Display | Red Hat Display | bold / extrabold | none / tight | tighter / tight | | Headline | Red Hat Display | bold | tight / snug | tight | | Title | Display or Sans | semibold | snug | normal | | Body | Rubik | normal | normal / relaxed | normal | | Label | Rubik | medium / semibold | none / snug | wide / wider | ## Three axes carry the rest Weight, line-height, and tracking express hierarchy without inventing a new size. Weight: the scale runs 100 to 800. Working range of five: 400 body, 500 label, 600 title, 700 head, 800 display. Rule: keep the three light weights for display sizes, and set text at 16px and under in 400 or heavier. Why: below 16px, thin strokes lose effective contrast and fail low-vision readers. Line height, inverse to size: `--leading-tight` 1.1 for display, `--leading-normal` 1.5 for the body floor. Rule: set body at 1.5 line-height or more, and move long-form to `--leading-relaxed` 1.625. Why: 1.5 within paragraphs is the WCAG 1.4.8 Visual Presentation (AAA) target. WCAG 1.4.12 Text Spacing (AA) is a separate promise: text must stay readable when a reader forces 1.5x spacing, which a rem scale that reflows cleanly already meets. Tracking, tight for big, wide for caps: headline tighter -0.05em, body normal 0em, label wider 0.05em. Rule: reserve positive tracking for uppercase Labels, and leave lowercase body at 0. Why: extra space restores the legibility all-caps removes, while any tracking on lowercase body distorts word shape. ## The measure Line length decides whether a paragraph is calm or exhausting. Cap running text between 45 and 75 characters with `max-width` in `ch`. Around 66 is the sweet spot. - ✓ Recommended, 45 to 75ch: the eye finds the start of the next line without effort. Cap body columns in ch, never leave running text at full container width. - ✕ Avoid, over 75ch: past seventy-five characters the reader travels too far back, loses their place, and rereads. The single most common readability failure in dashboards and admin tools. Hierarchy must survive one color. Distinguish levels with size, weight, and space, and back every color step with a real size or weight difference. The scale is defined in `rem`, so text still scales to 200% (WCAG 1.4.4), and body holds at 1.5 line-height or more (WCAG 1.4.8). ## Sources Role groups after Material 3 type scale. The 45 to 75ch measure from Butterick, Practical Typography, and Baymard. Fluid type with clamp() from Utopia and web.dev. Accessibility floors from WCAG 2.1 SC 1.4.4 Resize Text, 1.4.12 Text Spacing, and 1.4.8 Visual Presentation. # Spacing Source: https://design.ramoslabs.com/foundations/spacing/ Space is picked once: a 4px base, an 8px rhythm, and 14 fixed steps. Every gap in the product is one of those steps. No 13px here, no 15px there. ## One base, an 8pt rhythm The scale rests on a 4px base with an 8px primary beat. Four gives fine control for tight gaps; eight is the workhorse, because it divides cleanly across screen densities and renders crisp from a 1x phone to a 3x display, never landing on a half pixel. Spacing, component sizes, and line boxes all snap to the same beat. ## The scale Fourteen steps, every value a multiple of 4px. Past `--space-6` the steps widen (skipping 7, 9, 11) because at larger sizes the eye needs a bigger jump. Tokens ship in `rem`, so spacing scales with the user's root font size. | Token | rem | px | Typical use | | --- | --- | --- | --- | | `--space-0` | 0 | 0 | Reset, collapse a gap | | `--space-1` | 0.25rem | 4px | Hairline gaps, icon-to-label | | `--space-2` | 0.5rem | 8px | Tight inline spacing, chip padding | | `--space-3` | 0.75rem | 12px | Compact input padding | | `--space-4` | 1rem | 16px | Default padding, card interior | | `--space-5` | 1.25rem | 20px | Roomy control padding | | `--space-6` | 1.5rem | 24px | Gap between related components | | `--space-8` | 2rem | 32px | Block separation, generous padding | | `--space-10` | 2.5rem | 40px | Group separation | | `--space-12` | 3rem | 48px | Section padding on mobile | | `--space-16` | 4rem | 64px | Between-section rhythm | | `--space-20` | 5rem | 80px | Large section breaks | | `--space-24` | 6rem | 96px | Desktop section padding | | `--space-32` | 8rem | 128px | Hero spacing | ## Touch targets are non-negotiable Spacing is an accessibility contract. Three floors anchor the system: - 24 by 24px: WCAG 2.2 floor (AA), the AA minimum. - 44 by 44pt: Apple HIG target. - 48 by 48dp: Material target. On phones, add safe-area insets to edge-anchored bars so a sticky CTA never sits under the home indicator: `padding-bottom: calc(var(--space-4) + env(safe-area-inset-bottom))`. - ✓ Recommended: size primary actions to the 44 to 48px range, and keep 24px as the hard floor for dense, secondary controls. - ✕ Avoid: letting a tap target shrink to its visual glyph. A 20px icon with no added padding misses the floor; expand the hit area with padding or a pseudo-element so it reaches 44 to 48px while the glyph stays small. ## Density lowers whitespace, never the floor Density is how tightly the same components pack. You step interior spacing down the scale, never invent off-scale values. | Mode | Interior step | When to apply | | --- | --- | --- | | Comfortable (default) | `--space-4` / `--space-6` | Touch-first screens, marketing, reading, onboarding | | Compact | `--space-2` / `--space-3` | Data tables, dashboards, admin tools, power-user views | Rule: keep the 24px hit-area floor in every density mode, compact included. Why: density trims padding, not accessibility. ## Sources Touch-target floors: WCAG 2.2 SC 2.5.8 Target Size Minimum, 24px AA; Apple HIG 44pt; Material 48dp. The 8pt grid and density model after Material layout and density, and Refactoring UI numbered scales. Safe-area insets via CSS `env()`. # Border Radius Source: https://design.ramoslabs.com/foundations/border-radius/ A corner sets personality. Sharp reads technical, fully round reads playful, the considered middle reads premium. This system commits to one 6px signature on every control, a short ladder up to hero surfaces, and keeps the fully round pill as an accent, never a default. ## The radius scale Eight tokens, each with a role. The four middle steps do almost all the work. - `--radius-none`: 0. Square. Tables, full-bleed media, dividers. - `--radius-xs`: 4px. Nested inner corners, tight inline elements. - `--radius-sm`: 6px. Signature. Buttons, inputs, selects. - `--radius-md`: 8px. Comfortable controls, small badges. - `--radius-lg`: 12px. Cards, menus, popovers, dropdowns. - `--radius-xl`: 16px. Modals, sheets, large surfaces. - `--radius-2xl`: 20px. Hero panels, feature surfaces. - `--radius-pill`: 9999px. Accent only. Chips, tags, avatars, toggles. ## The golden rule The pill is an accent, not a default. Buttons are not pills. Scale the radius to the size and role of the surface. Reserve `--radius-pill` for small, single-line elements where the round shape is the point: chips, tags, avatars, toggles, status badges. Every button, input, and select takes the 6px signature. Bigger surfaces earn bigger radii: a card is not shaped like a button, and a modal is not shaped like a card. - Do: wide button at `--radius-sm`, the 6px signature. Calm, precise, unmistakably a control. - Don't: the same button forced to a pill. The larger corners read as a toy, not a product. ## The law of concentric corners Nest one rounded element inside another and the corners must stay concentric: parallel curves sharing a center, like a screen inside its housing. ``` inner radius = outer radius - padding ``` Example: an outer `--radius-xl` (16px) with 8px of padding gives an inner surface of `--radius-md` (16 minus 8), so the corners run parallel. Leaving the inner at the full 16px makes the arcs collide. Apple ships this rule as a first-class API: `ConcentricRectangle` and `containerShape` in SwiftUI. ## Continuous corners, when the browser allows A plain CSS corner is a quarter circle: the curvature jumps from zero to maximum at a single point. A squircle eases the curvature in and out, reading as softer at the same nominal radius. The CSS `corner-shape` property brings it to the web, with `corner-shape: squircle` defined as `superellipse(2)`. Ship it as progressive enhancement inside an `@supports` query, so the base `border-radius` works everywhere and supporting browsers get the smoother curve for free. `corner-shape` is new, landing first in recent Chromium. That is why it is an enhancement, not a requirement. Never gate layout, hit targets, or legibility on it. ## The scale at real size A chip stays a pill because it is small and one line. A button takes the 6px signature. A card steps up to 12px, a modal to 16px. - Chip: `--radius-pill` - Button: `--radius-sm`, 6px - Card: `--radius-lg`, 12px - Modal: `--radius-xl`, 16px ## Token reference | Token | Value | Role | | --- | --- | --- | | `--radius-none` | 0 | Square. Tables, full-bleed media, section dividers | | `--radius-xs` | 4px | Nested inner corners, tight inline elements | | `--radius-sm` | 6px | Signature control radius. Buttons, inputs, selects | | `--radius-md` | 8px | Comfortable controls, small badges, compact containers | | `--radius-lg` | 12px | Cards, menus, popovers, dropdowns | | `--radius-xl` | 16px | Modals, sheets, large surfaces | | `--radius-2xl` | 20px | Hero panels, feature surfaces | | `--radius-pill` | 9999px | Accent only. Chips, tags, avatars, toggles, one-line status. Never wide buttons or cards | ## Sources Material 3, Shape scale: shape as a role-based scale rather than one global radius; step values tuned to this brand. Vercel Geist: a 6px control radius on buttons and inputs. Apple SwiftUI: `RoundedRectangle(style: .continuous)` draws the squircle, and `ConcentricRectangle` with `containerShape` formalizes concentric nesting. CSS corner-shape draft: `squircle` equals `superellipse(2)`, support landing first in recent Chromium. The token mapping, the pill-as-accent reading, and the 6px signature are house rules. # Shadows Source: https://design.ramoslabs.com/foundations/shadows/ Every floating surface picks one of three heights, each a soft two-layer shadow tinted with brand indigo instead of hard black. A shadow says one thing only: this surface sits above another. Add a focus ring at `--shadow-focus` and that is the entire system. ## The elevation ladder Flat on the page, raised, overlay, floating. The taller a surface reads, the higher it sits, and `lg` is the ceiling every surface stays within. - On the page: no shadow. - Raised: `--shadow-sm`. - Overlay: `--shadow-md`. - Floating, the ceiling: `--shadow-lg`. ## Three levels, plus a ring - `--shadow-sm`, Raised: buttons and resting cards. A whisper of lift that separates an element from the page. - `--shadow-md`, Overlay: dropdowns, popovers, menus. Content that opens over the current view and closes again. - `--shadow-lg`, Floating, the ceiling: modals, sheets, command palettes. The highest surface. If something must feel higher, add a scrim, not more blur. - `--shadow-focus`, Focus ring: a two-tone ring, a white gap inside an indigo edge, paired with `:focus-visible` to show keyboard position. Accessibility, never decoration. ## Two layers, tinted indigo Real shadows are two things at once: a crisp contact shadow close to the object, and a soft ambient shadow far from it. - Near layer: short offset, tight blur. The crisp contact shadow that grounds the object. - Far layer: long offset, wide blur, negative spread. The soft ambient shadow that gives height. - Both, tinted indigo: the two combined and tinted. This is `--shadow-md`. Tinting both with indigo lets the shadow belong to the surface instead of staining it. ## Shadow is height A shadow says one thing: this surface sits physically above another. Rule: pick one of the three elevation levels for a surface and keep it fixed for the surface's whole life. Why: a constant elevation reads as real, physical height, so hover, press, and selection stay legible in their own channel, color. - ✓ Recommended: fix each surface at one level, then carry hover, press, and selection in color and state layers. Color answers "what is happening to this control", shadow answers "how high does this surface sit". - ✕ Avoid: swelling a dramatic shadow on hover, usually paired with a `translateY` lift. It fakes physics the surface does not have and collides with the rule against transform-based hover. State feedback lives in color. Hover, press, and selection ride on color and state layers rather than elevation. See Patterns / Interactive for the state-layer model. ## In the dark, tint carries height As a surface rises it can take a stronger indigo tint. In dark themes a drop shadow nearly vanishes against a dark background, so a lighter, more tinted surface is the clearer signal. Prefer tint in the dark, shadow in the light, and the two can combine. Height reads from lightness, not from ink. ## Token reference Four tokens, the whole system. The scale stays closed: `lg` is the top, and hover reuses the resting shadow. | Token | Role | Value | | --- | --- | --- | | `--shadow-sm` | Raised, buttons and resting cards | 0 1px 2px rgba(79, 70, 229, .06), 0 1px 3px rgba(15, 23, 42, .04) | | `--shadow-md` | Overlay, dropdowns and popovers | 0 2px 4px -1px rgba(79, 70, 229, .06), 0 8px 16px -4px rgba(79, 70, 229, .10) | | `--shadow-lg` | Floating ceiling, modals and sheets | 0 4px 8px -2px rgba(79, 70, 229, .06), 0 16px 32px -8px rgba(79, 70, 229, .12) | | `--shadow-focus` | Focus ring, pairs with :focus-visible | 0 0 0 2px #ffffff, 0 0 0 4px #4f46e5 | ## Sources Material 3 elevation and tonal elevation: level count and the surface-tint approach. Tobias Ahlin, layered smooth shadows: stacking soft box-shadow layers for a natural falloff. Apple Human Interface Guidelines, Materials: soft, minimal shadows for depth. Exact rgba layer values are specific to this system's tokens, and reading `lg` as an absolute ceiling is a house rule. # Motion Source: https://design.ramoslabs.com/foundations/motion/ Every animation earns its place by answering one question: where did that come from, are these the same thing, did my tap register. No job, no motion. The system is deliberately small, three durations and five easings, so the whole product moves at one tempo: fast, and quiet. ## Purposeful, not decorative Motion earns its place by clarifying a change, never by dressing one up. - ✓ Recommended: - Orientation. A menu grows from the button that opened it. - Continuity. A row animates to its new spot instead of jumping. - Feedback. A control settles after a tap, confirming the press. - ✕ Avoid: - Entrances that animate with nothing to orient. - Looping, attention-grabbing movement with no status behind it. - Long, cinematic reveals that make the interface wait. ## The three durations Match the duration to the size of the change. Nothing runs past 300ms, where responsive starts to feel slow. - `--duration-fast`, 150ms: local state on a control the user is already watching. A hover tint, a focus ring, an icon toggling, a checkbox filling. - `--duration-normal`, 200ms: the default. Things entering, leaving, or moving a short way: a dropdown, a tooltip, a tab indicator, an accordion. - `--duration-slow`, 300ms: a large surface that takes over the view and needs a beat to read: a modal, a bottom sheet, a full-screen overlay. Larger surface or longer travel, longer duration. The ceiling is 300ms, and most motion lives at 200ms. ## Five easings, five jobs Easing is the shape of speed over time. Each curve has one job. - `--easing-linear`, `cubic-bezier(0, 0, 1, 1)`: constant speed. Only for continuous loops: spinners, indeterminate progress. On anything that begins and ends it feels robotic. - `--easing-out`, `cubic-bezier(0, 0, 0.2, 1)`: quick start, gentle landing. The default for anything entering or answering a tap. - `--easing-in`, `cubic-bezier(0.4, 0, 1, 1)`: slow start, accelerating exit. For elements leaving the screen. Never for entrances, a slow start there reads as lag. - `--easing-in-out`, `cubic-bezier(0.4, 0, 0.2, 1)`: eased on both ends. For an element moving between two on-screen positions, both visible the whole time: a reordering row, a toggle thumb, a tab indicator. - `--easing-spring`, `cubic-bezier(0.34, 1.56, 0.64, 1)`: overshoots, then settles. Reserved for a small, discrete success: a checkmark, a like, a badge popping in. Comfort not information, so gate it behind reduced motion and keep it off large surfaces. ## Anatomy of a transition Every transition names three things: the property, a duration token, an easing token. ```css transition: opacity var(--duration-normal) var(--easing-out); ``` - property: what changes. Prefer `transform` and `opacity`. Name it, never `all`. - duration: how long. 150ms local, 200ms to enter or leave, 300ms for a takeover. - easing: the feel. Out to enter, in to leave, in-out to move, linear to loop, spring to celebrate. Name the property, never `transition: all`, which animates things you never meant to touch. Reference the tokens instead of hardcoding milliseconds. ## Performance is part of the contract A frame at 60fps is about 16ms. The browser can animate `transform` and `opacity` on the compositor, off the main thread, with no layout and no repaint. Animating geometry recomputes layout every frame, and that layout thrash is the usual cause of jank. - ✓ Recommended: `transform`, `opacity`. Handled on the compositor, off the main thread. A slide is `translateY`, not a change to `top`. - ✕ Avoid: `width`, `height`, `top`, `left`, `margin`. Each frame recomputes layout, then repaints. On a long list or weak device this drops frames. ## Reduced motion is the floor The browser exposes the OS setting as `prefers-reduced-motion: reduce`, a firm request to hold still that every animation honors. It aligns with WCAG 2.1: 2.3.3 asks that interaction-triggered motion can be disabled, and 2.2.2 covers looping motion. Honoring it does not mean deleting meaning. Because information rides on color, opacity, and the final state, the interface stays legible without movement: collapse the animation, keep the destination. Keep a spinner turning slowly rather than freezing it, a stopped spinner reads as a hung app. ```css /* Default: motion is welcome. Enter fast, settle gently. */ .menu { transition: opacity var(--duration-normal) var(--easing-out), transform var(--duration-normal) var(--easing-out); } /* Firm request to hold still: collapse the movement, keep the meaning. */ @media (prefers-reduced-motion: reduce) { .menu { transition-duration: 1ms; /* appears, does not fly in */ transform: none; /* no slide */ } .spinner { animation-duration: 1.4s; /* slower, not gone */ } } ``` ## Rules of motion - Rule: give every animation a job: orientation, continuity, or feedback. Why: decorative motion clarifies nothing and costs the same frame time. - Rule: animate `transform` and `opacity`. Why: the compositor carries them off the main thread with no layout or paint. Animating `width`, `height`, `top`, `left`, or `margin` recomputes layout every frame. - Rule: name the property, a duration token, and an easing token explicitly. Why: `transition: all` animates properties you never meant to touch, and hardcoded milliseconds drift from the shared tempo. - Rule: match the duration to the change: `fast` for local state, `normal` to enter or leave, `slow` for a takeover. Why: anything past 300ms reads as sluggish. - Rule: pick easing by job: `out` to enter, `in` to leave, `in-out` to move, `linear` to loop. Why: `linear` on anything with a start and an end feels robotic. - Rule: reserve `spring` for a small, discrete success, and gate it behind reduced motion. Why: an overshooting modal looks broken, and spring is comfort, not information. - Rule: honor `prefers-reduced-motion`: collapse the movement, keep the meaning. Why: motion alone must never carry meaning; information rides on color, opacity, and the final state. ## Token reference | Token | Value | Role | | --- | --- | --- | | `--duration-fast` | 150ms | Local state on one control: hover, focus, toggle | | `--duration-normal` | 200ms | Default. Enter, leave, or move a short distance | | `--duration-slow` | 300ms | A large surface taking over the view. The ceiling | | `--easing-linear` | cubic-bezier(0, 0, 1, 1) | Continuous loops only: spinners, indeterminate progress | | `--easing-in` | cubic-bezier(0.4, 0, 1, 1) | Elements leaving the screen | | `--easing-out` | cubic-bezier(0, 0, 0.2, 1) | Elements entering, and taps. The default | | `--easing-in-out` | cubic-bezier(0.4, 0, 0.2, 1) | Moving between two visible positions | | `--easing-spring` | cubic-bezier(0.34, 1.56, 0.64, 1) | Small discrete successes. Overshoots, then settles | ## Sources Material 3 Motion: duration buckets and easing sets. Apple HIG Motion: purposeful, communicates status, respects Reduce Motion. web.dev and MDN: animate transform and opacity to stay on the compositor. MDN prefers-reduced-motion. W3C WCAG 2.1: 2.3.3 Animation from Interactions (AAA) and 2.2.2 Pause, Stop, Hide (A). The three durations, five easings, 300ms ceiling, and gating spring behind reduced motion are specific to this system's tokens. # Responsive Source: https://design.ramoslabs.com/foundations/responsive/ Design for the content, not the device. Start from the smallest reasonable screen, let the content decide where it strains, and reach for a fixed breakpoint last. A layout keyed to a few famous device widths breaks the moment someone opens a split view, rotates a foldable, or reads at 200% zoom. ## Mobile first is a discipline, not a phone rule The small screen first forces the hard question early: what is genuinely essential. What survives is the core; what a wider screen adds is progressive enhancement layered on a base that already works. 1. Base is the floor. Write the small-screen layout with no media query. That is the default every browser gets, and it must stand on its own. 2. Enhance upward. Each `min-width` query only adds: more columns, a sidebar that was stacked. Never subtract from wide to reach narrow. 3. Content sets the line. Add a breakpoint where the layout actually strains, when a line runs long or a card cramps, not where a popular phone happens to end. ## Five breakpoints, each a structural move The system ships exactly five named tokens, all `min-width` thresholds. Below the first is the base, the no-query mobile layout. | Token | Min width | Structural role | | --- | --- | --- | | `base` | 0 | The default. Single column, thumb-reachable, no media query. Everything enhances this | | `--breakpoint-sm` | 576px | Large phones. Room for a two-up grid and side-by-side fields | | `--breakpoint-md` | 769px | Tablets. The first real shift: a stacked shell becomes two panes, nav moves inline | | `--breakpoint-lg` | 992px | Laptops. Persistent sidebars, three columns, the full desktop chrome | | `--breakpoint-xl` | 1200px | Wide desktops. Cap line length and center the shell | | `--breakpoint-2xl` | 1366px | Very wide. Add breathing room and max-widths, not more columns | Rule: key page-shell breakpoints to the five named `min-width` tokens, and no others. Why: there is no `xs` because the base below `sm` needs no query to name it. A media condition cannot read a custom property, so the query carries the literal value, `@media (min-width: 769px)`, while the token stays the single source of that number. - ✓ Recommended: one component that strains between two tokens: solve it with fluid sizing or a container query, so the scale stays at five. - ✕ Avoid: inventing a sixth breakpoint for that one component. It fragments the scale and every reader now has an extra threshold to reason about. ## Media query or container query A media query asks how wide the viewport is; right for the page shell, wrong for a component. A container query lets an element respond to its own container. Baseline since 2023. - Media query, viewport level, `@media (min-width: 992px)`: asks about the whole viewport. Right for the page shell: global grid, top nav, where the sidebar lives. Home of capability and preference features: `prefers-color-scheme`, `prefers-reduced-motion`, `pointer`, `hover`. Keyed to the five breakpoint tokens. - Container query, component level, `@container (min-width: 24rem)`: asks how much space this element was actually given. Right for reusable parts: cards, tiles, widgets. The parent opts in with `container-type`; children size to `cqw` units. The component becomes portable. Rule: match the query to its scope: a media query against a token for the page shell, a container query for any part that must work in more than one slot. Why: a media query asks how wide the viewport is, which is what the shell responds to. A container query asks how much room an element was actually given. - ✓ Recommended: for a capability or preference, dark mode, reduced motion, a coarse pointer, reach for a media query. - ✕ Avoid: reaching for a container query there. A container knows only its own size, never color scheme, motion preference, or pointer type. ## The best breakpoint is the one you never wrote Between the structural thresholds, layout should flow, not jump. Intrinsic layouts lean on what CSS already does: a grid that fits as many columns as comfortably fit, type and spacing that scale as a range. The result adapts at every width, with far fewer media queries. - Intrinsic grid, one rule, no media query: `grid-template-columns: repeat(auto-fit, minmax(160px, 1fr))`. Columns reflow on their own. - Fluid type: `clamp(1.5rem, 3vw + 1rem, 2.75rem)` grows and shrinks smoothly, held between a floor and a ceiling. Use `clamp()`, `min()`, and `max()` for type, spacing, and measure. Rule: reach for fluid sizing and an intrinsic grid before you write any breakpoint. Why: an auto-fit grid and clamped type adapt at every width, not just at the five thresholds, and with far fewer queries to keep in sync. - ✓ Recommended: let the layout flow: `repeat(auto-fit, minmax(160px, 1fr))` for columns, `clamp()` for type and spacing. - ✕ Avoid: stacking breakpoints to imitate that flow. It steps between thresholds and leaves more queries to maintain. ## Safe areas and notches On modern phones the screen is not a clean rectangle. Notches, rounded corners, and the home indicator carve in, and a layout drawn to the physical edge can bury a primary action under a system gesture. Honor the insets for anything anchored to a screen edge, and add the inset to your spacing token rather than replacing it. Insets are unlocked by `viewport-fit=cover`; on a device with none, `env()` resolves to zero and spacing is unchanged. Rule: honor the safe-area insets for anything anchored to a screen edge. Why: notches, rounded corners, and the home indicator carve into the screen, so a control drawn to the physical edge can land under a system gesture. - ✓ Recommended: add the inset to your token: `calc(var(--space-4) + env(safe-area-inset-bottom))`. Where a device has no insets, `env()` resolves to zero and the spacing is unchanged. - ✕ Avoid: replacing the token with `env()` alone, or drawing to the physical edge. Where there are no insets the spacing collapses to zero and the control can fall under system chrome. Thumb-reach and target sizing for anchored controls live in Foundations / Spacing. ## Sources Mobile-first and progressive enhancement: Wroblewski, Mobile First, and Marcotte, Responsive Web Design. Let content set breakpoints: web.dev responsive basics. Container queries, Baseline since 2023: MDN and web.dev. Intrinsic layouts: Jen Simmons. Fluid sizing and reflow: MDN clamp() and grid auto-fit / minmax. Safe areas: MDN env() and Apple HIG Layout. The five values and reading them as coarse structural ceilings are specific to this system's tokens. # Helpers Source: https://design.ramoslabs.com/foundations/helpers/ Utilities are single-purpose classes that do exactly one thing, and every value traces back to a token. This is a curated set, not a dump. If a helper would type `16px` by hand instead of reading `--space-4`, it does not belong here. ## Token, utility, or component Three layers carry style. Reach for the layer that answers the question you actually have. - Layer 1, Token: the value, named once. `--space-4`, `--color-primary`, `--radius-lg`. What is the value? - Layer 2, Utility: one class, one declaration, sourced from a token. `.gap-4` sets `gap: var(--space-4)`. Apply one value, here, now. - Layer 3, Component: a named pattern with scoped styles, structure, and states. An event card, a button, a checkout row. A whole reusable thing. Rule: source every utility value from a token, never a hand-typed value. Why: that is what separates a design-system utility from a loose grab bag of CSS. - ✓ Recommended: `.gap-4 { gap: var(--space-4); }`. Reads the token, so one change to the scale updates every gap at once. - ✕ Avoid: `.gap-4 { gap: 16px; }`. A hand-typed pixel drifts from the token and quietly breaks the system. ## The catalog Every class is one declaration, and every sized value traces to a token. Display: `.d-none` display: none; `.d-block` display: block; `.d-inline` display: inline; `.d-inline-block` display: inline-block; `.d-flex` display: flex; `.d-inline-flex` display: inline-flex; `.d-grid` display: grid. Flexbox: `.flex-row` flex-direction: row; `.flex-col` flex-direction: column; `.flex-wrap` flex-wrap: wrap; `.flex-1` flex: 1 1 0%; `.flex-auto` flex: 1 1 auto; `.flex-none` flex: none. Alignment: `.items-start` align-items: flex-start; `.items-center` align-items: center; `.items-end` align-items: flex-end; `.justify-center` justify-content: center; `.justify-between` justify-content: space-between; `.justify-end` justify-content: flex-end. Layout and overflow: `.w-full` width: 100%; `.h-full` height: 100%; `.min-w-0` lets a flex child shrink so truncation works; `.overflow-auto` overflow: auto; `.truncate` one line, ellipsis (hidden overflow, nowrap, ellipsis together). Gap (every step is a token): `.gap-1` var(--space-1) 0.25rem; `.gap-2` var(--space-2) 0.5rem; `.gap-3` var(--space-3) 0.75rem; `.gap-4` var(--space-4) 1rem; `.gap-6` var(--space-6) 1.5rem; `.gap-8` var(--space-8) 2rem. Typography: `.text-center` text-align: center; `.text-sm` var(--font-size-sm); `.text-base` var(--font-size-base); `.text-lg` var(--font-size-lg); `.text-xl` var(--font-size-xl); `.font-medium` var(--font-weight-medium); `.font-semibold` var(--font-weight-semibold); `.font-bold` var(--font-weight-bold). Text color (semantic tokens only): `.text-primary` var(--color-text-heading); `.text-secondary` var(--color-text-secondary); `.text-muted` var(--color-text-muted); `.text-error` var(--color-error-text); `.text-success` var(--color-success-text). Border radius: `.rounded-sm` var(--radius-sm) 0.375rem; `.rounded-md` var(--radius-md) 0.5rem; `.rounded-lg` var(--radius-lg) 0.75rem; `.rounded-xl` var(--radius-xl) 1rem; `.rounded-pill` var(--radius-pill) 9999px. Rule: name each helper after the token it reads, so one vocabulary runs top to bottom. Why: a class named for a value the token layer never uses is exactly the drift that erodes trust in the system. - ✓ Recommended: `.rounded-pill` reads `var(--radius-pill)`. The class mirrors the token name, so the two stay in sync. - ✕ Avoid: `.rounded-full`. Describes a scale the tokens never define; the mismatch invites drift. ## Utilities at work A handful of structural helpers, read in one glance, is fair use. The moment the same cluster becomes a card you build twenty times, give it a name and let the helpers go. ```html
Ticket status Confirmed
Teatro Metropolitano Jose Gutierrez Gomez
``` ## The two helpers that matter most Two exist purely to serve assistive technology. Treat them as required infrastructure, not decoration. - `.sr-only`: visually hidden, still announced by screen readers. For icon-button labels, form hints, and status that is obvious by sight but silent to the tree. - `.focus-ring`: applies the system focus treatment on `:focus-visible`, pairing with `var(--color-focus)` so keyboard users always see where they are. Visually hidden is a solved problem with a canonical recipe. The common shortcut `display: none` is wrong for it: that also drops the element from the accessibility tree, the opposite of the goal. Inside `.sr-only`, declaration by declaration: - `position: absolute`: pulls the element out of flow so its collapsed box disturbs nothing. - `width: 1px; height: 1px`: collapses the box to one pixel. Not zero, since a zero-sized box can be dropped from the tree. - `overflow: hidden`: clips the real content down to that one-pixel box so none of it paints. - `clip + clip-path: inset(50%)`: clips the visible region to nothing. The clip-path rule does the work in modern engines; the deprecated clip stays as a fallback. - `white-space: nowrap`: stops the collapsed text from wrapping, which could reintroduce a sliver of layout. - `margin: -1px; border: 0`: zeroes the box model and nudges it off-screen so nothing leaks. ```css /* The canonical visually-hidden utility */ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); clip-path: inset(50%); white-space: nowrap; border: 0; } ``` Rule: use `.sr-only` for meaning that is obvious by sight but silent to the tree, never to stuff invisible keywords. Why: the word behind an icon-only button, the meaning of a required asterisk. A skip link that must reveal itself on keyboard focus uses the focusable variant. See Patterns / Accessibility. - ✓ Recommended: `Close`. Keeps the pixels hidden while the label still reaches assistive tech. - ✕ Avoid: `display: none`. Drops the element from the accessibility tree, so a screen reader never hears it. ## Which layer, in five questions Walk down until one fits. 1. Just need the value, inside a component's own scoped CSS? Reference the token, for example `var(--space-4)`. No utility needed. 2. One structural adjustment in markup, a gap, an alignment, a width? Use the matching utility. 3. The same three or four utilities always travel together on the same kind of element? That pattern has a name. Extract a component. 4. The element needs structure, states, or behavior, a card, a modal, a checkout row? Build it as a named component with its own scoped styles from the start. 5. The need is purely for assistive tech, hiding a label or showing focus? Use `.sr-only` or `.focus-ring`. Infrastructure, not styling. ## Sources Utility-first model and the extract-to-component threshold: Tailwind CSS and Adam Wathan, CSS Utility Classes and Separation of Concerns. The composition-plus-blocks balance follows Andy Bell, CUBE CSS. The visually-hidden recipe: W3C WAI WCAG technique C7 and Scott O'Hara, Inclusively Hidden. Visible focus is WCAG 2.1 SC 2.4.7. The exact class names, token bindings, and the reading of `.sr-only` and `.focus-ring` as required infrastructure are specific to this system. # Token Reference Source: https://design.ramoslabs.com/foundations/token-reference/ A token is the smallest unit of the language: one color, one step of space, one shadow, authored as data and published to CSS and TypeScript from a single source. There are 213 tokens across 2 tiers, 8 radius steps, and 3 elevation shadows. The exhaustive table of every token name and value is auto-generated from `tokens.json` into `llms-full.txt`. This page distills the naming, categories, and consumption rules that govern that table. ## Tokens as a contract Every token is authored as data, not CSS, in the W3C Design Tokens Community Group (DTCG) format: a JSON object with a reserved `$value` and a `$type` like `color` or `dimension`. One authored decision publishes to CSS, TypeScript, and native from the same source. The JSON is the contract. It runs in two tiers. - Tier 1, Primitive: the raw palette. Eight color ramps of eleven shades each, plus pure white and black. These carry a value and nothing else, no role, no meaning. Product code rarely touches this tier directly. Naming is descriptive: `--color-indigo-600`. - Tier 2, Semantic: the layer you build with. Each token names a role, not a hue, and aliases a primitive: `--color-primary`, `--color-text-body`, `--color-border`. This indirection makes the system themeable. Change one alias, and every consumer moves with it. Naming is intentional: role first, variant second. Pipeline: hand-authored DTCG JSON under `packages/tokens/src/tokens/` (one file per category) to Style Dictionary (resolves aliases, applies transforms, formats each target) to generated `dist/tokens.css` (`:root` custom properties) and `dist/tokens.ts` (typed constants). Both carry a do-not-edit banner: they are build output, not source. ## Categories - Primitive palette: eight ramps, `--color-indigo-*`, `--color-slate-*`, `--color-emerald-*`, `--color-green-*`, `--color-amber-*`, `--color-red-*`, `--color-blue-*`, `--color-violet-*`, each 50 through 950, plus `--color-white` and `--color-black`. These ramps are Tailwind CSS's open-source (MIT) default palette, adopted hex-for-hex as the primitive layer; what this system owns sits on top, the mono-indigo constraint and the semantic mapping. Indigo is the brand ramp and single action accent; slate carries text, surfaces, and borders; the four status ramps feed the feedback tokens; violet backs the decorative secondary role, never for action. - Semantic color: brand (`--color-primary*`, `--color-secondary*`), text (`--color-text-heading`, `-strong`, `-secondary`, `-body`, `-muted`, `-disabled`), surface and border (`--color-surface*`, `--color-background`, `--color-border*`, `--color-focus`), and feedback (`--color-success*`, `--color-warning*`, `--color-error*`, `--color-info*`, each with the strong/surface/border/text variants). - Spacing: `--space-*`, one scale for margin, padding, and gap. - Border radius: `--radius-*`, eight steps. `sm` at 6px is the signature control radius; `pill` is the fully rounded accent. - Shadow: three elevation levels plus `--shadow-focus`. Each level is two soft, indigo-tinted layers. Shadow means height, never state. - State layers: `--state-*`, interaction feedback as opacity, not elevation. - Typography: `--font-family-*` (three families), `--font-weight-*` (eight weights), `--font-size-*` (ten steps), `--leading-*` (line height), `--tracking-*` (letter spacing). - Motion: `--duration-*` (three) and `--easing-*` (five curves). - Z-index: `--z-*`, a stepped stacking order. - Breakpoints: `--breakpoint-*`, five minimum widths. Borders are dividers, not control outlines. Rule: use `--color-border` and `--color-border-light` only to separate regions (dividers, table rows, card edges). Why: both sit near 1.2:1 against the surface, below the 3:1 floor WCAG 1.4.11 requires for the boundary of an interactive control. For the visible edge of an input, select, or checkbox, reach for a token that clears 3:1, for example `--color-text-muted` at `#64748b`, which is 4.55:1 on white. Never let a subtle border be the only thing marking a control's bounds. State layers are opacity: each `--state-*` token is the opacity of an indigo overlay painted on a control. Numbers only, so the same layer works on any surface color. `--state-hover` 0.08 (gate with `@media (hover: hover)`), `--state-focus` 0.1 (focus-visible), `--state-pressed` 0.1 (active press, primary feedback on touch), `--state-dragged` 0.16. ## How to consume a token Reference the variable, never the literal. Reach for the semantic tier first, because it carries the meaning and stays stable across themes. Drop to a primitive only when no semantic token fits. If you find yourself wanting a raw hex, that is the signal a semantic token is missing, not that you should hardcode. ```css /* Yes: reference the semantic token */ .card { background: var(--color-surface); color: var(--color-text-body); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); } /* No: a hardcoded value drifts from the system on the next theme change */ .card { background: #ffffff; border-radius: 12px; } ``` ## How to name a new token 1. Pick the tier. A raw value with no role belongs in primitive, named by description. A value that carries meaning belongs in semantic, named by role. 2. Name a semantic token role first, variant second: category, role, variant. So `color`, `text`, `secondary` becomes `--color-text-secondary`. Never encode the hue in the name. 3. Alias, do not copy. A semantic token points at a primitive with `{color.slate.600}`, so the value has one home. If two tokens share a value, both alias the same primitive. 4. Add a `$description` when the role is not obvious from the name. It becomes the comment in the generated CSS and the note in this reference. ## How a change propagates A change starts in the JSON and ends on the screen without a single manual edit downstream. Edit the `$value` in `packages/tokens/src/tokens/`, run the Style Dictionary build, and the generated `tokens.css` and `tokens.ts` update together. Because every consumer references the variable, the new value reaches every product surface at once. Never edit the generated files: they carry a do-not-edit banner and the next build overwrites them. - ✓ Recommended: consume `var(--token)` references and prefer the semantic tier; author new tokens in the JSON source, then rebuild; name semantic tokens by role, category first, hue never; alias a primitive instead of repeating a hex; add a `$description` for any non-obvious role. - ✕ Avoid: hardcoding a hex, px, or raw integer that a token already covers; editing `dist/tokens.css` or `dist/tokens.ts` by hand; reaching for a primitive when a semantic token fits; encoding a color name into a semantic token; duplicating a value across tokens instead of aliasing. ## Sources Design Tokens Community Group format specification: the open W3C standard defining a token as a JSON object with `$value` and `$type` and the alias syntax the semantic tier relies on. Style Dictionary: the build tool that reads the token source, resolves aliases, and formats the CSS and TypeScript outputs. Salesforce primitive and semantic token tiers: the two-tier model. Every token name, value, and description is quoted from this system's source JSON and generated CSS. The elevation ceiling and the pill-as-accent-only rule are house rules. # Interactive Source: https://design.ramoslabs.com/patterns/interactive/ Touch is the majority of every session, and a finger has nothing to hover with. So the press comes first, the keyboard gets a ring, and hover is a pointer-only garnish laid on top. One tonal indigo layer expresses all three, stepped through the `--state-*` scale. ## 1. Three inputs, one state each Each input arrives its own way and reads its own signal. Build for all three, or you build for a mouse alone. - Pointer: rests over a control without committing. That idle moment is hover, and it belongs to the pointer alone. - Touch: no idle state. A finger commits on contact, so its signal is the press, fired the instant it lands. - Keyboard: moves by Tab, commits with Enter or Space. Its signal is focus-visible, and without it the interface is unusable. **Mobile first.** The press is the one state every device can make. Design `:active` first, add `:focus-visible` for keyboards, layer `:hover` for pointers. Never the reverse. ## 2. The state overlay Every state is a color, never a jump in position. Filled deepens to indigo 700 on hover, 800 on press. The tonal button and card gain an indigo state layer instead. A single translucent indigo overlay, the mechanism Material 3 standardized, builds hover, press, and drag alike. One overlay, four opacities: - `--state-hover`: 0.08, pointer only - `--state-focus`: 0.10, keyboard - `--state-pressed`: 0.10, all inputs - `--state-dragged`: 0.16, while dragging ```css /* One indigo overlay, opacity driven by the --state-* scale. Works on any surface: button, card, list row. */ .control { position: relative; overflow: hidden; border-radius: var(--radius-sm); /* 6px */ } .control::after { content: ''; position: absolute; inset: 0; background: var(--color-primary); opacity: 0; transition: opacity var(--duration-fast) var(--easing-out); pointer-events: none; } .control:active::after { opacity: var(--state-pressed); } /* every input */ ``` **iOS caveat for custom controls.** On iOS Safari `:active` does not fire on a non-native control (a `div` or `span` with `role="button"`) unless that element carries `cursor: pointer` or an empty `ontouchstart` handler to arm it. Real ``. Role, accessible name, focus stop, and keyboard activation come for free, and it behaves the same in every browser and assistive tech. - ✕ Avoid: `
Buy tickets
`. No role, no keyboard, no focus stop. You would have to rebuild `tabindex`, key handling, and the focus ring by hand. 1. Prefer the native element whenever it carries the semantics and behavior you need. A real `
...
``` ## Sources WCAG 2.2 success criteria (w3.org/TR/WCAG22): contrast 1.4.3 / 1.4.11, use of color 1.4.1, keyboard 2.1.1 / 2.1.2, focus 2.4.7 / 2.4.11 / 2.4.13, target size 2.5.8 / 2.5.5, motion 2.3.3 / 2.2.2, name-role-value 4.1.2, status messages 4.1.3. POUR and the five rules of ARIA from W3C WAI and Using ARIA; widget patterns from the ARIA Authoring Practices Guide. The one-in-six disability figure is WHO; the 44px touch target follows the Apple Human Interface Guidelines. Ratios use the WCAG relative-luminance formula. # Form Elements Source: https://design.ramoslabs.com/patterns/form-elements/ The spine is one rule: reach for the native control first, tune it with the right attributes, and build custom only where the platform genuinely falls short. # Part 1: Doctrine, the rules under every field ## 1. Native-first, and doubly so on mobile The default is always the plain HTML control with the right `type`, `inputmode`, and `autocomplete`. On a phone this is leverage: a semantic `` raises the correct on-screen keyboard, opens the platform picker, and arrives with keyboard support, focus handling, and screen-reader semantics already built in. MDN: prefer the semantic input type over hacks. Go custom only when one of two things is true: the pattern does not exist in HTML (a real multi-select, a filtering combobox, tags, rating), or the brand must style something the platform will not let you style (the popup of a `` arrow and popup, and the date and time picker widgets. That single limit, not taste, is what pushes a control from native to custom. The clearest case is a date. GOV.UK: for a date the user already knows or can look up without a calendar, such as a birthday or an issue date, do not open a calendar. Use three plain text inputs, grouped in a `
` with a ``, each with `inputmode="numeric"`. Reserve the native calendar picker for dates the user picks by looking, like choosing a future session. - ✓ Recommended: a memorable date, as three fields (day / month / year, each `inputmode="numeric"`). - ✕ Avoid: a calendar for a birthday. A picker that opens on the current month forces the user to page back through decades to reach a year they could have typed. ## 2. Theming the accent without giving up native You can brand native controls without rebuilding them. Three CSS properties carry it. Set them once at the root and every native checkbox, radio, range, and text caret in the product turns indigo, and every control follows the OS light or dark theme, with zero per-control work. ```css :root { color-scheme: light dark; /* native controls follow the OS theme */ accent-color: var(--color-primary); /* tints checkbox, radio, range, progress */ caret-color: var(--color-primary); /* the text insertion cursor */ } ``` Read the coverage exactly. `accent-color` tints four controls and nothing else. It does not touch ``. Attrs: `autocomplete`, `enterkeyhint`, `maxlength`. Standard keyboard; keep font-size at 16px. Visible `