{ "schemaVersion": 2, "generatedAt": "2026-05-17T00:00:00Z", "title": "Design System: bland", "extensions": { "colorMeta": { "accent-500": { "role": "primary", "displayName": "Warm Amethyst", "canonical": "#9d6ee8", "hue": "263°", "saturation": "~73%", "lightness": "~67%", "tonalRamp": [ "#4d2a7c", "#5e3497", "#7241b8", "#8854d4", "#9d6ee8", "#b48ef5", "#ccb4fb", "#e2d7fe", "#f1ecfe", "#faf7ff" ] }, "canvas": { "role": "neutral", "displayName": "Lifted Warm Canvas", "canonical": "#221f21", "tonalRamp": ["#0c090b", "#1b181a", "#221f21", "#2a2729", "#423f42", "#555259", "#747178", "#a3a1a8"] }, "zinc-900": { "role": "neutral", "displayName": "Warm Chrome", "canonical": "#1b181a", "note": "Sidebar, header, recessed containers. Darker than canvas — chrome frames the layout." }, "zinc-800": { "role": "neutral", "displayName": "Warm Elevated", "canonical": "#2a2729", "note": "Menus, dialogs, dropdowns, popovers, inputs. Lighter than canvas — floating feel." }, "zinc-700": { "role": "neutral", "displayName": "Warm Overlay Hover", "canonical": "#423f42", "note": "Hover states inside overlays, borders, scrollbar thumb, overlay-surface for floating toolbars." }, "zinc-500": { "role": "neutral", "displayName": "Warm Muted", "canonical": "#747178", "note": "Placeholder text, blockquote refs. Passes WCAG AA on canvas (~5.5:1)." }, "zinc-300": { "role": "neutral", "displayName": "Editor Body", "canonical": "#d6d4d7", "note": "Default editor body text color." }, "zinc-100": { "role": "neutral", "displayName": "Primary Body", "canonical": "#f5f4f4", "note": "Primary body text, heading color in editor." }, "callout-info": { "role": "tertiary", "displayName": "Slate Info", "canonical": "oklch(0.82 0.06 265)", "hex": "#b6c8e6", "note": "Low-chroma cool slate for :::info callouts. Tuned for astigmatism comfort." }, "callout-tip": { "role": "tertiary", "displayName": "Pale Amethyst Tip", "canonical": "oklch(0.89 0.05 295)", "hex": "#d8c8eb", "note": "Low-chroma amethyst-adjacent for :::tip callouts." }, "callout-warning": { "role": "tertiary", "displayName": "Muted Warm Gold", "canonical": "oklch(0.82 0.07 62)", "hex": "#e2c598", "note": "Muted gold for :::warning callouts. Deliberately not a fire alarm." } }, "typographyMeta": { "display": { "displayName": "Display (Bricolage)", "purpose": "Editor H1 / page title only. Reserved for hero sizes — overuse dilutes its character." }, "headline": { "displayName": "Headline (Outfit 600)", "purpose": "Editor H2. Tighter tracking for visual weight at 1.5rem." }, "title": { "displayName": "Title (Outfit 600)", "purpose": "Editor H3, card titles." }, "body": { "displayName": "Body (Outfit 450)", "purpose": "Editor prose. The 450 weight and 1.7 line-height are non-negotiable — primary halation defense for astigmatic readers." }, "label": { "displayName": "Label (Outfit 500, wide tracking)", "purpose": "Overlines, meta labels, caption text. Often uppercase." }, "mono": { "displayName": "Mono (JetBrains Mono)", "purpose": "Code only — inline or block. Never extend to nav, headings, or body." } }, "shadows": [ { "name": "overlay-sm", "value": "0 1px 2px 0 rgb(0 0 0 / 0.20)", "purpose": "Subtle drop under floating toolbars, dropdowns, drag handles. Tailwind shadow-sm class." }, { "name": "dialog-2xl", "value": "0 25px 50px -12px rgb(0 0 0 / 0.50)", "purpose": "Reserved for modals/dialogs only. Tailwind shadow-2xl class." } ], "motion": [ { "name": "default-transition", "value": "75ms", "purpose": "Default transition-duration override. Scope to specific properties — never transition-all." }, { "name": "ease-snappy", "value": "cubic-bezier(0.16, 1, 0.3, 1)", "purpose": "Out-quart curve for scale-fade entrances. Strong initial motion, soft landing." }, { "name": "fade-in", "value": "fade-in 0.4s ease-out both", "purpose": "Main content areas on page load. Opacity 0 → 1." }, { "name": "slide-up", "value": "slide-up 0.35s ease-out both", "purpose": "Page wrappers, modal/dialog entrances, toast entrance. Opacity 0 + translateY(12px) → visible." }, { "name": "scale-fade", "value": "scale-fade 0.3s cubic-bezier(0.16, 1, 0.3, 1) both", "purpose": "Cards, popovers, dropdown menus. Opacity 0 + scale(0.96) → visible — scale implies origin." }, { "name": "shimmer", "value": "shimmer 1.5s ease-in-out infinite", "purpose": "Loading skeleton placeholder." }, { "name": "caret-blink", "value": "caret-blink 1.1s steps(2, start) infinite", "purpose": "Editor caret. Slower than OS default for calmer rhythm." }, { "name": "stagger-step", "value": "60ms", "purpose": "Staggered reveal delay per sibling. Cap at 8 items (480ms total)." } ], "breakpoints": [ { "name": "sm", "value": "640px" }, { "name": "md", "value": "768px" }, { "name": "lg", "value": "1024px" }, { "name": "xl", "value": "1280px" } ] }, "components": [ { "name": "Primary Button", "kind": "button", "refersTo": "button-primary", "description": "Solid accent CTA. Never gradient.", "html": "", "css": ".ds-btn-primary { background: #8854d4; color: #fff; font-family: 'Outfit Variable', ui-sans-serif, system-ui, sans-serif; font-weight: 500; font-size: 0.875rem; padding: 0.5rem 1rem; border: none; border-radius: 0.5rem; cursor: pointer; transition: background-color 75ms, transform 75ms; } .ds-btn-primary:hover { background: #9d6ee8; } .ds-btn-primary:active { transform: scale(0.98); } .ds-btn-primary:focus-visible { outline: 2px solid rgba(157, 110, 232, 0.5); outline-offset: 2px; }" }, { "name": "Secondary Button", "kind": "button", "refersTo": "button-secondary", "description": "Subtle elevated surface, neutral border. Never accent.", "html": "", "css": ".ds-btn-secondary { background: rgba(42, 39, 41, 0.6); color: #d6d4d7; font-family: 'Outfit Variable', ui-sans-serif, system-ui, sans-serif; font-weight: 500; font-size: 0.875rem; padding: 0.5rem 1rem; border: 1px solid rgba(66, 63, 66, 0.6); border-radius: 0.5rem; cursor: pointer; transition: background-color 75ms, transform 75ms; } .ds-btn-secondary:hover { background: rgba(66, 63, 66, 0.6); color: #f5f4f4; } .ds-btn-secondary:active { transform: scale(0.98); }" }, { "name": "Ghost Button", "kind": "button", "refersTo": "button-ghost", "description": "No border, no rest background. Used for editor toolbar utility actions.", "html": "", "css": ".ds-btn-ghost { background: transparent; color: #a3a1a8; padding: 0.375rem 0.75rem; border: none; border-radius: 0.5rem; cursor: pointer; transition: background-color 75ms, color 75ms, transform 75ms; display: inline-flex; align-items: center; justify-content: center; } .ds-btn-ghost:hover { background: rgba(42, 39, 41, 0.7); color: #f5f4f4; } .ds-btn-ghost:active { transform: scale(0.97); }" }, { "name": "Card (default)", "kind": "card", "refersTo": "card-default", "description": "Surface for actual content (pages, attachments). Not for marketing copy.", "html": "

Q4 planning notes

Edited 12 minutes ago by Rachel.

", "css": ".ds-card { background: rgba(27, 24, 26, 0.5); border: 1px solid rgba(42, 39, 41, 0.6); border-radius: 1rem; padding: 1.25rem; color: #d6d4d7; font-family: 'Outfit Variable', ui-sans-serif, system-ui, sans-serif; font-weight: 450; line-height: 1.6; transition: border-color 75ms, transform 75ms; } .ds-card:hover { border-color: rgba(66, 63, 66, 0.6); transform: translateY(-2px); } .ds-card-title { margin: 0 0 0.25rem; font-size: 1rem; font-weight: 600; color: #f5f4f4; } .ds-card-body { margin: 0; font-size: 0.875rem; color: #a3a1a8; }" }, { "name": "Text Input", "kind": "input", "refersTo": "input", "description": "Elevated tier background, soft border, pill-ish radius.", "html": "", "css": ".ds-input { width: 100%; background: rgba(42, 39, 41, 0.8); color: #f5f4f4; font-family: 'Outfit Variable', ui-sans-serif, system-ui, sans-serif; font-weight: 450; font-size: 0.875rem; padding: 0.625rem 1rem; border: 1px solid rgba(66, 63, 66, 0.6); border-radius: 0.875rem; outline: none; transition: border-color 75ms, box-shadow 75ms; } .ds-input::placeholder { color: #747178; } .ds-input:focus { border-color: rgba(157, 110, 232, 0.5); box-shadow: 0 0 0 1px rgba(157, 110, 232, 0.3); } .ds-input:focus-visible { box-shadow: 0 0 0 2px rgba(157, 110, 232, 0.5); }" }, { "name": "Sidebar Nav Item", "kind": "nav", "refersTo": "sidebar", "description": "Chrome-tier navigation item. Active uses accent tint.", "html": "", "css": ".ds-sidebar { background: #1b181a; padding: 0.5rem; border-right: 1px solid rgba(42, 39, 41, 0.6); display: flex; flex-direction: column; gap: 0.125rem; font-family: 'Outfit Variable', ui-sans-serif, system-ui, sans-serif; min-width: 220px; } .ds-sidebar-link { display: block; padding: 0.375rem 0.625rem; color: #d6d4d7; font-size: 0.875rem; font-weight: 450; line-height: 1.4; text-decoration: none; border-radius: 0.375rem; transition: background-color 75ms, color 75ms; } .ds-sidebar-link:hover { background: rgba(42, 39, 41, 0.6); color: #f5f4f4; } .ds-sidebar-link.is-active { background: rgba(157, 110, 232, 0.1); color: #b48ef5; }" }, { "name": "Editor Surface", "kind": "custom", "refersTo": "canvas", "description": "The signature surface. Canvas background, 450 weight body at 1.7 line-height, accent links, JetBrains Mono inline code.", "html": "

The Quiet Studio

bland is a dark notes editor for developers who live in dark IDEs all day. The canvas is #221f21 — lifted, never near-black. Halation is the reason.

Comfort over contrast. The interface should be comfortable for hours, not minutes.
", "css": ".ds-editor { background: #221f21; padding: 2rem; color: #d6d4d7; font-family: 'Outfit Variable', ui-sans-serif, system-ui, sans-serif; font-weight: 450; font-size: 1rem; line-height: 1.7; font-feature-settings: 'ss01' on, 'cv01' on; border-radius: 0.5rem; } .ds-editor > * + * { margin-top: 0.75em; } .ds-editor h1 { font-family: 'Bricolage Grotesque Variable', 'Outfit Variable', ui-sans-serif, sans-serif; font-size: 1.875rem; font-weight: 700; line-height: 1.15; letter-spacing: -0.025em; color: #f5f4f4; margin: 0 0 0.5rem; } .ds-editor p { margin: 0; } .ds-editor a { color: #b48ef5; text-decoration: underline; text-underline-offset: 2px; transition: color 75ms; } .ds-editor a:hover { color: #ccb4fb; } .ds-editor code { font-family: 'JetBrains Mono', 'SF Mono', monospace; font-size: 0.875em; background: #2a2729; color: #e5e4e5; padding: 0.15em 0.35em; border-radius: 0.25rem; } .ds-editor blockquote { border-left: 3px solid #423f42; padding-left: 1rem; color: #a3a1a8; margin: 0; }" }, { "name": "Callout Block", "kind": "custom", "refersTo": "callout-info", "description": "Editor callout — info/tip/warning. Low-chroma OKLCH hues tuned for astigmatism comfort.", "html": "", "css": ".ds-callout { display: flex; gap: 0.875rem; background: rgba(182, 200, 230, 0.08); border-radius: 0.5rem; padding: 0.75rem 1rem; font-family: 'Outfit Variable', ui-sans-serif, sans-serif; font-weight: 450; font-size: 0.9375rem; line-height: 1.6; color: #d6d4d7; } .ds-callout-rule { flex: 0 0 3px; border-radius: 1.5px; background: oklch(0.82 0.06 265); } .ds-callout-body { flex: 1; } .ds-callout-body strong { color: #f5f4f4; font-weight: 600; }" }, { "name": "Focus Ring", "kind": "custom", "refersTo": "focus-ring", "description": "Global *:focus-visible ring. Canvas offset creates a gap between element and ring.", "html": "", "css": ".ds-focus-demo { background: #2a2729; color: #f5f4f4; font-family: 'Outfit Variable', sans-serif; font-weight: 500; padding: 0.5rem 1rem; border: 1px solid rgba(66, 63, 66, 0.6); border-radius: 0.5rem; cursor: pointer; outline: none; } .ds-focus-demo:focus-visible { box-shadow: 0 0 0 2px #221f21, 0 0 0 4px rgba(157, 110, 232, 0.5); }" } ], "narrative": { "northStar": "The Quiet Studio", "overview": "bland is a dark notes editor for developers who live in dark IDEs and want a writing surface that disappears. The aesthetic is sophisticated restraint: a warm, lifted dark palette; a clean studio desk with good lighting; calm focus over visual noise. Typography does the heavy lifting. Color is rationed. Motion is fast — 75ms — and unobtrusive. The product is named bland on purpose; the design is anything but. This system explicitly rejects generic SaaS surfaces, the lazy 'dev tool = monospace + neon + near-black' shorthand, and Notion's exact pixel grammar. It rejects pure dark backgrounds because they cause halation for astigmatic readers — the lifted canvas is an accessibility requirement, not an aesthetic preference. The voice is deadpan, precise, warm.", "keyCharacteristics": [ "Lifted warm dark — canvas at #221f21, never near-black", "Inverted surface hierarchy — sidebar (zinc-900) is darker than the canvas", "Body weight 450, not 400 — thicker strokes resist halation", "Body line-height 1.7 — letters need room to breathe", "Default transition 75ms, not 150ms or 300ms", "One accent — warm-shifted amethyst #9d6ee8, used on ≤10% of any screen", "Bricolage Grotesque + Outfit + JetBrains Mono (no Hanken Grotesk — bland picks its own display face)" ], "rules": [ { "name": "The One Voice Rule", "body": "The accent is used sparingly — primary CTA, active nav, focus rings, selection, links inside body content. If accent is on more than ~10% of any given screen, something is wrong.", "section": "colors" }, { "name": "The Lifted Canvas Rule", "body": "Canvas is #221f21, period. Never use stock zinc-950 (#09090b) or any pure-black-class background for content surfaces. The halation justification (astigmatism comfort) ranks above raw contrast scores.", "section": "colors" }, { "name": "The Inverted Chrome Rule", "body": "The sidebar is darker than the canvas. Chrome (zinc-900) frames from the edges; canvas (#221f21) is where the eye rests. Do not 'fix' this by lifting the sidebar — the inversion is the design.", "section": "colors" }, { "name": "The Warm Shift Rule", "body": "Every neutral stop is warm-shifted off stock Tailwind zinc (+R, -B). When adding new neutrals or hardcoded values in plain CSS, use the warm values — #1b181a not #18181b, #2a2729 not #27272a. Cool casts on dark surfaces register as clinical even when the user can't name them.", "section": "colors" }, { "name": "The No Gradient Accent Rule", "body": "The accent is a solid color, always. bg-accent-600 hover:bg-accent-500. No gradients, no accent-colored shadows. Gradients on the accent read as the AI-generated default.", "section": "colors" }, { "name": "The Body Weight Rule", "body": "Body text is 450, not 400. The 50-unit lift is non-negotiable — it is the primary halation defense. Never set body or paragraph weight below 450. Never use font-light or font-thin anywhere.", "section": "typography" }, { "name": "The 1.7 Line-Height Rule", "body": "Body text in content surfaces (editor, long-form reading) uses line-height 1.7. Tightening is allowed for chrome/UI text where space is the constraint, but the editor surface is sacred.", "section": "typography" }, { "name": "The Display-for-Heroes Rule", "body": "Bricolage is reserved for editor H1. Do not extend it to dialog titles, sidebar labels, or marketing copy — its character only carries weight at hero size, and overuse dilutes it.", "section": "typography" }, { "name": "The Mono-Is-Code Rule", "body": "JetBrains Mono appears only on code (inline or block) and the rare technical label. Using it anywhere else is the lazy dev-tool shorthand that PRODUCT.md explicitly rejects.", "section": "typography" }, { "name": "The Tonal-First Rule", "body": "Depth comes from value first (chrome darker than canvas, elevated lighter than canvas), borders second, shadows third. If you reach for a shadow before exhausting the surface stack, you're solving the wrong problem.", "section": "elevation" }, { "name": "The Subtle Shadow Rule", "body": "Any shadow used is at shadow-sm weight unless the element is a modal. Colored shadows are prohibited — they identify the design as AI-generated and add noise without affordance.", "section": "elevation" } ], "dos": [ "Do use the lifted canvas (#221f21) as the body background. Body text weight 450, line-height 1.7 on content surfaces.", "Do keep the sidebar darker than the canvas (zinc-900). The inverted chrome is the design.", "Do use the warm-shifted zinc values (#1b181a, #2a2729, #423f42, …) — never stock zinc — when hardcoding hex in plain CSS.", "Do use the solid accent for CTAs (bg-accent-600 hover:bg-accent-500) and ration it — ~10% of any screen, max.", "Do override transition duration to 75ms via --default-transition-duration. Scope transitions to the exact properties that change (transition-colors, transition-transform, transition-[border-color,box-shadow]).", "Do prefer tonal layering (surface stack) over shadows for depth. Reserve shadow-sm for floating UI, shadow-2xl for modals.", "Do stagger entrance animations at 60ms increments, capped at 8 items.", "Do keep wit in micro-copy — empty states, loading lines, error messages — never in decoration.", "Do respect prefers-reduced-motion via the global rule in app.css." ], "donts": [ "Don't use pure dark backgrounds (#09090b / stock zinc-950) for content surfaces. Causes halation. PRODUCT.md anti-reference.", "Don't ship 'generic SaaS' surfaces — Intercom-style dashboards, cookie-cutter cards, stock illustrations. PRODUCT.md anti-reference.", "Don't lean on 'monospace-everything-dark-mode-neon' as a shorthand for 'dev tool.' PRODUCT.md anti-reference. Mono is for code only.", "Don't try to be a pixel-perfect Notion clone. bland is self-aware about the resemblance and owns its own grammar. PRODUCT.md anti-reference.", "Don't use violet-500 (#8b5cf6) or any stock Tailwind purple as the accent — it is the most-recognizable AI-generated color choice. The amethyst accent is hue-shifted warmer and de-saturated for a reason.", "Don't use gradient buttons (from-accent-500 to-accent-600) or accent-colored shadows (shadow-accent-500/10). Solid color, subtle shadow only.", "Don't animate border hue shifts (zinc → accent via transition-colors). The intermediate tones look unnatural even at 75ms. Lighten within the same hue family instead.", "Don't use transition-all ever. Scope to actual properties.", "Don't use font-light or font-thin anywhere. Insufficient contrast on dark surfaces; halation makes thin strokes unreadable for astigmatic users.", "Don't use hover:brightness-* on gradient buttons. Forces per-frame GPU gradient recompute.", "Don't use backdrop-blur-xl (24px). backdrop-blur-sm (4px) is plenty and ~6x cheaper per frame.", "Don't add position: fixed pseudo-element background layers (body::before glow). Apply ambient gradients directly to body's background-image.", "Don't wrap marketing copy in primitives. Cards are for real content (pages, attachments), not decoration.", "Don't use zinc-600 for semantically meaningful text — insufficient contrast against any dark background.", "Don't rely on color alone for state — pair with icon or label." ] } }