--- name: bland description: A quiet dark notes editor for developers colors: accent-50: "#faf7ff" accent-100: "#f1ecfe" accent-200: "#e2d7fe" accent-300: "#ccb4fb" accent-400: "#b48ef5" accent-500: "#9d6ee8" accent-600: "#8854d4" accent-700: "#7241b8" accent-800: "#5e3497" accent-900: "#4d2a7c" zinc-50: "#fafaf9" zinc-100: "#f5f4f4" zinc-200: "#e5e4e5" zinc-300: "#d6d4d7" zinc-400: "#a3a1a8" zinc-500: "#747178" zinc-600: "#555259" zinc-700: "#423f42" zinc-800: "#2a2729" zinc-900: "#1b181a" zinc-950: "#0c090b" canvas: "#221f21" overlay-surface: "#423f42" callout-info: "#b6c8e6" callout-tip: "#d8c8eb" callout-warning: "#e2c598" typography: display: fontFamily: "Bricolage Grotesque Variable, Outfit Variable, ui-sans-serif, system-ui, sans-serif" fontSize: "1.875rem" fontWeight: 700 lineHeight: 1.15 letterSpacing: "-0.025em" headline: fontFamily: "Outfit Variable, ui-sans-serif, system-ui, sans-serif" fontSize: "1.5rem" fontWeight: 600 lineHeight: 1.2 letterSpacing: "-0.015em" title: fontFamily: "Outfit Variable, ui-sans-serif, system-ui, sans-serif" fontSize: "1.25rem" fontWeight: 600 lineHeight: 1.3 body: fontFamily: "Outfit Variable, ui-sans-serif, system-ui, sans-serif" fontSize: "1rem" fontWeight: 450 lineHeight: 1.7 fontFeature: '"ss01" on, "cv01" on' label: fontFamily: "Outfit Variable, ui-sans-serif, system-ui, sans-serif" fontSize: "0.75rem" fontWeight: 500 letterSpacing: "0.1em" mono: fontFamily: "JetBrains Mono, SF Mono, Fira Code, monospace" fontSize: "0.875rem" fontWeight: 400 rounded: xs: "3px" sm: "0.25rem" md: "0.5rem" lg: "0.75rem" xl: "0.875rem" "2xl": "1rem" components: button-primary: backgroundColor: "{colors.accent-600}" textColor: "#ffffff" rounded: "{rounded.md}" padding: "0.5rem 1rem" button-primary-hover: backgroundColor: "{colors.accent-500}" textColor: "#ffffff" button-secondary: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-300}" rounded: "{rounded.md}" padding: "0.5rem 1rem" button-secondary-hover: backgroundColor: "{colors.zinc-700}" textColor: "{colors.zinc-100}" button-ghost: backgroundColor: "transparent" textColor: "{colors.zinc-400}" rounded: "{rounded.md}" padding: "0.375rem 0.75rem" button-ghost-hover: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-100}" card-default: backgroundColor: "{colors.zinc-900}" textColor: "{colors.zinc-100}" rounded: "{rounded.2xl}" padding: "1.25rem" card-accent: backgroundColor: "{colors.zinc-900}" textColor: "{colors.zinc-100}" rounded: "{rounded.2xl}" padding: "1.25rem" input: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-100}" rounded: "{rounded.lg}" padding: "0.625rem 1rem" sidebar: backgroundColor: "{colors.zinc-900}" textColor: "{colors.zinc-300}" overlay: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-100}" rounded: "{rounded.lg}" padding: "0.5rem" --- # Design System: bland ## 1. Overview **Creative North Star: "The Quiet Studio"** 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 (Intercom-style dashboards, cookie-cutter cards), the lazy "dev tool = monospace + neon + near-black" shorthand, and Notion's exact pixel grammar. It rejects pure dark backgrounds (`#09090b`-class) because they cause halation for astigmatic readers — the lifted canvas is an accessibility requirement, not an aesthetic preference. It rejects gradient buttons, accent-colored shadows, violet-500 stock purple, animated border-hue transitions, and `transition-all`. The voice is **deadpan, precise, warm**. Visual character lives in micro-copy, type pairing, the 75ms snap, and the deliberate inversion of chrome vs. canvas brightness — never in decoration. **Key Characteristics:** - **Lifted warm dark.** Canvas at `#221f21`, never near-black. Zinc palette is warm-shifted (+R, -B) at every stop. - **Inverted surface hierarchy.** Sidebar (`zinc-900`) is darker than the canvas. Chrome frames; canvas reads. - **Body weight 450, not 400.** Thicker strokes resist halation. - **Body line-height 1.7.** Letters get room to breathe. - **Default transition 75ms.** Not 150ms. Not 300ms. - **One accent, warm-shifted amethyst (`#9d6ee8`).** Used sparingly — CTAs, links, focus rings, selection. Solid, never gradient. - **Bricolage Grotesque + Outfit + JetBrains Mono.** No Hanken Grotesk (the limic-shared baseline). bland's display face is the per-project distinctive choice. ## 2. Colors: The Warm Amethyst Palette A warm-shifted dark palette: zinc neutrals nudged off the cool-blue cast they ship with, paired with a single low-electric amethyst accent. The palette is built around comfort for long reading sessions, not maximum contrast. ### Primary - **Warm Amethyst** (`#9d6ee8`, `accent-500`): The single accent. Solid hue at ~263°, ~73% saturation, ~67% lightness (warmer than Tailwind violet-500's 271°, lower-sat than its 90%). Carries CTAs (`accent-600` rest, `accent-500` hover), active nav items (`accent-500/10` bg, `accent-400` text), link color in editor body (`accent-400`), focus rings (`accent-500/50`), text selection (`accent-500/28%`), and the brand glyph in the header. ### Neutral: Warm Zinc (10 stops) The Tailwind `zinc` scale, warm-shifted at every stop (+2–3 R, -1–4 B). The shift is felt, not seen. - **Canvas** (`#221f21`): Body / main content / editor surface. The brightest baseline in the system — the place the eye should rest. Custom value, sits between stock `zinc-900` and `zinc-800`. - **zinc-900** (`#1b181a`): Chrome (sidebar, header) and recessed containers (code blocks, table wrappers). Same hex, two roles separated by borders. - **zinc-800** (`#2a2729`): Elevated surfaces — menus, dialogs, dropdowns, popovers, inputs. Lighter than canvas, gives a floating feel. - **zinc-700** (`#423f42`): Overlay hover states, borders, scrollbar thumb, blockquote rules. Also `overlay-surface` for floating toolbars and the editor's drag-handle. - **zinc-600** (`#555259`): Muted icons, disabled states, scrollbar hover. - **zinc-500** (`#747178`): Muted/placeholder text. Passes WCAG AA against canvas (~5.5:1) — would fail on `zinc-950`. - **zinc-400** (`#a3a1a8`): Secondary text, blockquote body, footer text. - **zinc-300** (`#d6d4d7`): Editor body text, near-white secondary headings. - **zinc-200** (`#e5e4e5`): Inline code text, prominent text. - **zinc-100** (`#f5f4f4`): Primary body text, headings. - **zinc-50** / **zinc-950**: Reserved for selection text (`accent-50`) and edge cases. ### Tertiary: Callout Accents Low-chroma OKLCH hues orbiting the amethyst hue (265°–295°), tuned for comfort over pop. Each conveys callout kind without competing with the accent. - **Info** (`oklch(0.82 0.06 265)`, approx `#b6c8e6`): Cool slate-blue for `:::info` callouts. - **Tip** (`oklch(0.89 0.05 295)`, approx `#d8c8eb`): Pale amethyst-adjacent for `:::tip` callouts. - **Warning** (`oklch(0.82 0.07 62)`, approx `#e2c598`): Muted warm gold for `:::warning` callouts. Low saturation deliberately — not a fire alarm. ### Semantic Used sparingly, only for state. Pair with icon or label — never color alone. - **Success** (`emerald-500/10` bg, `emerald-400` text, `emerald-500/20` border). - **Error** (`red-500/10` bg, `red-400` text, `red-500/20` border). - **Warning state** (`amber-500/10` bg, `amber-300` text, `amber-500/20` border). - **Info state** uses `accent-500/10` bg, `accent-400` text — folds into the brand. ### Named Rules **The One Voice Rule.** 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. **The Lifted Canvas Rule.** 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. **The Inverted Chrome Rule.** 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. **The Warm Shift Rule.** 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. **The No Gradient Accent Rule.** The accent is a solid color, always. `bg-accent-600 hover:bg-accent-500`. No `from-accent-500 to-accent-600`, no accent-colored shadows (`shadow-accent-500/*`). Gradients on the accent read as the AI-generated default. ## 3. Typography **Display Font:** Bricolage Grotesque Variable (fallback: Outfit Variable, then system sans). A characterful contemporary grotesque — slightly idiosyncratic letterforms that carry the "wit in the details" without shouting. Used only for the largest editor headings. **Body Font:** Outfit Variable (fallback: ui-sans-serif, system-ui, system stack). A modern variable sans tuned for screen reading at small sizes. Loaded as a variable font specifically so `font-weight: 450` is available — that 50-unit-heavier-than-regular body weight is core to the halation defense. **Mono Font:** JetBrains Mono (fallback: SF Mono, Fira Code). For code blocks, inline code, and the rare label that needs the developer-native register. **Character:** Outfit carries the calm-focus voice without being generic. Bricolage adds a small dose of personality at hero sizes. JetBrains Mono is the developer-tool register kept where it belongs — code only, not chrome. No Hanken Grotesk; bland deliberately picks its own display face rather than the shared limic baseline. ### Hierarchy - **Display** (Bricolage, 700, `1.875rem`/30px, line-height 1.15, tracking -0.025em): Editor H1 / page title only. - **Headline** (Outfit, 600, `1.5rem`/24px, line-height 1.2, tracking -0.015em): Editor H2. - **Title** (Outfit, 600, `1.25rem`/20px, line-height 1.3): Editor H3, card titles. - **Body** (Outfit, **450**, `1rem`/16px, line-height **1.7**, `ss01` + `cv01` enabled): Editor prose. Color `zinc-300`. Heading colors `zinc-100`. - **Label** (Outfit, 500, `0.75rem`/12px, tracking 0.1em, often uppercase): Overlines, meta labels, caption text. Color `zinc-500`. - **Mono** (JetBrains Mono, 400, `0.875rem`/14px): Inline code (`zinc-200` text on `zinc-800` bg, `0.25rem` radius, `0.15em 0.35em` padding). Block code in `zinc-900` with `zinc-800` 1px border, `0.5rem` radius. ### Named Rules **The Body Weight Rule.** 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. **The 1.7 Line-Height Rule.** Body text in content surfaces (editor, long-form reading) uses line-height 1.7. Tighter ratios save vertical space at the cost of readability for astigmatic users. Tightening is allowed for chrome/UI text where space is the constraint, but the editor surface is sacred. **The Display-for-Heroes Rule.** 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. **The Mono-Is-Code Rule.** JetBrains Mono appears only on code (inline or block) and the rare technical label. Using it anywhere else (entire nav, headings, body copy) is the lazy dev-tool shorthand that PRODUCT.md explicitly rejects. ## 4. Elevation bland is a **tonal-layered** system, not a shadow-driven one. The four-tier surface hierarchy (chrome / canvas / recessed / elevated) creates depth through value, with borders providing separation where the value differential is too small to read on its own. Shadows are reserved for the floating layer — popovers, dialogs, dropdowns, the editor's overlay toolbars and drag handle — and even there they stay subtle. Never use `shadow-lg` or `shadow-xl` on interactive surfaces; reserve `shadow-2xl` for modals/dialogs only. Colored accent shadows are forbidden — they read as AI-generated noise. ### Shadow Vocabulary - **Overlay shadow** (Tailwind `shadow-sm`-class, e.g. `0 1px 2px 0 rgb(0 0 0 / 0.20)`): Drop shadow under floating toolbars, dropdowns, drag handles. Subtle. - **Dialog shadow** (Tailwind `shadow-2xl`-class): Reserved for modals and full-overlay dialogs only. - **No card hover shadow by default.** Cards lift via `hover:-translate-y-0.5` and border tone, not shadow growth. ### Named Rules **The Tonal-First Rule.** 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. **The Subtle Shadow Rule.** Any shadow used is at `shadow-sm` weight unless the element is a modal. Colored shadows (`shadow-accent-500/*`, `shadow-purple-500/*`) are prohibited — they identify the design as AI-generated and add noise without affordance. ## 5. Components ### Buttons - **Shape:** Rounded corners — `rounded-md` (`0.5rem`) for most variants, `rounded-lg` (`0.75rem`) on larger CTAs. Avoid pill (`rounded-full`) outside of icon-only utility buttons. - **Primary** (`bg-accent-600 text-white hover:bg-accent-500 active:scale-[0.98]`): Solid accent, no gradient. White text. Padding `0.5rem 1rem` (md) / `0.375rem 0.75rem` (sm). - **Secondary** (`border border-zinc-700/60 bg-zinc-800/60 text-zinc-300 hover:bg-zinc-700/60`): Subtle border, elevated tone, never accent. - **Danger** (`border border-red-500/20 bg-red-500/10 text-red-400 hover:bg-red-500/20`): Tinted, not solid. Carries the destructive read without competing visually with the primary CTA. - **Ghost** (`text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-100`): No border, no background at rest. Used for utility actions in dense UI (editor toolbars, row actions). - **Press feedback:** `active:scale-[0.98]` on primary/secondary/danger, `active:scale-[0.97]` on ghost. Transition: `transition-transform` (not `transition-all`). - **Focus:** Inherits the global `*:focus-visible` rule (`ring-2 ring-accent-500/50 ring-offset-2 ring-offset-canvas`). - **Source of truth.** The class strings live in `src/client/components/ui/button-classes.ts`. Both `