Blob: DESIGN.md
name: dab description: The private wing of your data library — a sparse control plane for a personal DAV host. colors: canvas: "#221f21" 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" accent-50: "#fdf2f8" accent-100: "#fce7f3" accent-200: "#fbcfe8" accent-300: "#f9a8d4" accent-400: "#f06da6" accent-500: "#db3a8b" accent-600: "#b8226e" accent-700: "#941b58" accent-800: "#76174a" accent-900: "#5b143b" semantic-emerald-400: "#34d399" semantic-red-400: "#f87171" semantic-amber-300: "#fcd34d" typography: display: fontFamily: "Fraunces, ui-serif, Georgia, serif" fontSize: "clamp(1.875rem, 2vw + 1rem, 2.25rem)" fontWeight: 800 lineHeight: "1.05" letterSpacing: "-0.025em" h1: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "1.5rem" fontWeight: 700 lineHeight: "1.2" letterSpacing: "-0.02em" h2: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "1.25rem" fontWeight: 600 lineHeight: "1.3" h3: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "1rem" fontWeight: 600 lineHeight: "1.4" body: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "0.875rem" fontWeight: 450 lineHeight: "1.55" caption: fontFamily: "Hanken Grotesk, ui-sans-serif, system-ui, sans-serif" fontSize: "0.75rem" fontWeight: 500 lineHeight: "1.4" letterSpacing: "0.1em" mono: fontFamily: "JetBrains Mono, SF Mono, Fira Code, monospace" fontSize: "0.75rem" fontWeight: 400 lineHeight: "1.5" rounded: sm: "0.5rem" md: "0.75rem" lg: "1rem" xl: "1rem" "2xl": "1rem" full: "9999px" spacing: "2": "0.5rem" "3": "0.75rem" "4": "1rem" "5": "1.25rem" "6": "1.5rem" "8": "2rem" components: button-primary: backgroundColor: "{colors.accent-600}" textColor: "#ffffff" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" typography: "{typography.body}" button-primary-hover: backgroundColor: "{colors.accent-500}" textColor: "#ffffff" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" button-secondary: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-200}" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" typography: "{typography.body}" button-danger: backgroundColor: "{colors.zinc-900}" textColor: "{colors.semantic-red-400}" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" button-ghost: backgroundColor: "transparent" textColor: "{colors.zinc-400}" rounded: "{rounded.md}" padding: "0 1rem" height: "2.5rem" 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.md}" padding: "0.625rem 1rem" typography: "{typography.body}" input-focus: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-100}" rounded: "{rounded.md}" padding: "0.625rem 1rem" badge-neutral: backgroundColor: "{colors.zinc-800}" textColor: "{colors.zinc-300}" rounded: "{rounded.full}" padding: "0.125rem 0.5rem" typography: "{typography.caption}" badge-accent: backgroundColor: "{colors.accent-900}" textColor: "{colors.accent-300}" rounded: "{rounded.full}" padding: "0.125rem 0.5rem" typography: "{typography.caption}"
Design System: dab
1. Overview
Creative North Star: "The private wing of the library."
dab is the small reading room behind tessera's front desk. Each subject's opaque five-word host is the placard at the entrance to a wing of stacks; inside, three labeled shelves hold calendars, contacts, and files. The control-plane UI is a place to organize what is on those shelves, hand visitors (DAV clients) a shelfmark, and leave. Nothing on this surface wants your attention longer than necessary.
The aesthetic is institutional-print restraint with a single warm bookmark
slip. Fraunces (warm variable serif) does the ceremonial work on page H1s and
the brand mark, the way a typeset placard sits above a row of stacks. Hanken
Grotesk does the steady labor everywhere else. Coral-magenta (#db3a8b)
appears rarely, never as decoration — the primary CTA, the active nav
state, the small heart in the footer, the bookmark slip on a hover row. Its
power is rarity.
The whole system explicitly rejects the share-link product (Dropbox / iCloud / Drive), the enterprise NAS appliance (Nextcloud / ownCloud / Synology), the AI-template dark-mode default (neon-on-near-black, gradient meshes, glassmorphism), and the cheerful-Apple pastel calendar aesthetic. dab is not a daily driver; the design is sized for the four-times-a-year visit and the sixty-second task, and every surface should read like a labeled shelf rather than a marketing tile.
Key Characteristics:
- Lifted-canvas dark only (
#221f21); no light-mode toggle. - One coral-magenta accent, one warm serif, one warm sans, one mono. Nothing more.
- Catalog-card density: each row carries display name, internal name (mono), one timestamp, the URL in a collapsed disclosure.
- Personality budget spent on the Layers glyph
-rotate-6on hover, the serif H1, the gold-magenta CTA, and the footer heart. Nowhere else. - 75ms default transition, scoped to specific properties. Never
transition-all.
2. Colors: The Library Palette
Warm-shifted zinc neutrals on a lifted canvas, with a single coral-magenta accent borrowed from a bookmark slip pressed into a closed book.
Primary
- Bookmark Coral (
#db3a8b,oklch(60.3% 0.219 357)): The primary accent. Appears on the primary CTA fill ataccent-600(#b8226e) with white text, hovering toaccent-500. Active nav-state background ataccent-500/10with text ataccent-300. The footer heart fills ataccent-500. Active row indicator on hover-state borders. A "you saw me twice on this page" frequency; if it appears more often, something is being decorated that should not be.
Neutral: Warm Zinc
Stock zinc shifted +2–3 R, –1–4 B per stop, so the rest cool cast lifts and the palette feels like cream-aged paper under low light. Darker stops get the larger shift; lighter stops barely move so text contrast is unaffected.
- Canvas (
#221f21, ~zinc-850): Body / main content. The brightest reading surface; the place the eye rests. - Chrome (
#1b181a,zinc-900): Header, recessed wrappers (code blocks, table headers, disclosure containers). Darker than canvas to frame and recede. - Elevated (
#2a2729,zinc-800): Menus, dialogs, popovers, dropdowns, card fill. Lighter than canvas to float over it. - Hover-inside-elevated (
#423f42,zinc-700): Hover state for rows inside an elevated surface; also the resting border on inputs and secondary buttons. - Body text (
zinc-100,#f5f4f4): Primary content. Contrast on canvas 14.3:1. - Secondary text (
zinc-300,#d6d4d7): Labels, soft headings. - Muted text (
zinc-400,#a3a1a8): Caption, descriptions, table cells that aren't the primary value. Contrast 5.9:1 on canvas. - Placeholder / disabled (
zinc-500,#747178): Input placeholders, inert glyphs.
Semantic
| Role | Background | Text | Border |
|---|---|---|---|
| Success | bg-emerald-500/10 |
text-emerald-400 |
border-emerald-500/20 |
| Error | bg-red-500/10 |
text-red-400 |
border-red-500/20 |
| Warning | bg-amber-500/10 |
text-amber-300 |
border-amber-500/20 |
| Info | bg-accent-500/10 |
text-accent-300 |
border-accent-500/20 |
Named Rules
The One Bookmark Rule. Coral-magenta is the primary CTA, the active nav state, the footer heart, and nothing else. Never a background fill. Never a gradient. Never a decorative shadow. Never as a row stripe. If a third coral-magenta element appears on a page, one of the three is wrong.
The Lifted Canvas Rule. Body background is #221f21, not zinc-950,
not #000. Lifting prevents halation for astigmatic readers and gives the
surface hierarchy room to express depth without shadows. Don't push canvas
darker for "drama"; it makes the wing feel like a basement.
The Warm-Shifted Zinc Rule. Override stock zinc in @theme with the
warm-shifted values. Stock zinc leans cool/clinical; the warm shift reads as
"neutral" rather than "cool gray" and rhymes with cream paper. The warmth
should be felt, not seen.
3. Typography
Display Font: Fraunces (variable serif, opsz 9–144, wght 500–800, SOFT 30–100) with Georgia / serif fallback. Loaded via Google Fonts; CSP allows fonts.googleapis.com because dab is not a credential surface (tessera is).
Body Font: Hanken Grotesk (variable, wght 400–700) with system-ui
fallback. The default body { font-weight: 450 } is heavier than 400 to
counteract halation on dark surfaces.
Mono Font: JetBrains Mono (variable, wght 400, 500) for IDs, mono input values, DAV URLs, host labels.
Character: Fraunces is warm, slightly opinionated, with the SOFT axis turned up so the strokes feel hand-cut rather than mechanical. Paired with Hanken's quiet workhorse sans, the effect is "typeset placard above a row of stacks" — the H1 carries weight in posture, the body is the steady labor, the mono is the URL on the catalog card.
Hierarchy
- Display (Fraunces 800,
clamp(1.875rem, 2vw + 1rem, 2.25rem), line-height 1.05, tracking-0.025em): Page H1 insidePageHeader, rendered throughfont-display. One per page. Holds the metaphor. - H1 (Hanken 700, 1.5rem, tracking
-0.02em): Section titles below a page H1. Rare; most surfaces don't need a second tier. - H2 (Hanken 600, 1.25rem): Card title, dialog title, sub-section.
- H3 (Hanken 600, 1rem): List labels, group headers.
- Body (Hanken 450, 0.875rem, line-height 1.55): Default body and control text. Max line length 65–75ch for paragraphs.
- Caption (Hanken 500, 0.75rem, tracking
0.1em, uppercase): Overlines, meta labels (<dt>content in a definition list), table column headers. This is the "catalog card label" tier. - Mono (JetBrains Mono 400, 0.75rem): IDs, URLs, host labels, copy-paste-shaped values. Always slightly smaller than body.
Named Rules
The Serif Once Rule. Fraunces appears on the page H1 inside PageHeader
and on the brand mark in the header — nowhere else. Section titles, card
titles, dialog titles all use Hanken. Spreading the serif across cards
turns the wing into a museum gift shop. One serif moment per page.
The 450 Floor Rule. Body text never drops below font-weight: 450. No
font-light, no font-thin. Light strokes halate on dark surfaces for
~33% of users. The 450 floor is set on <body> and cascades; only override
upward.
The Mono For Mono-Shaped Values Rule. JetBrains Mono is reserved for values that are themselves machine-shaped: UUIDs, DAV URLs, host labels, ETags, sync tokens. Never use mono for "tech feel" — that's the AI-template reflex. A user-facing display name is not mono.
4. Elevation
dab uses tonal layering, not shadows, to express depth. The four-tier
warm-zinc hierarchy is the elevation system; box-shadow is reserved for
the single context where tonal layering can't carry the load (dialogs).
Surfaces from depth to height:
- Chrome (
zinc-900/#1b181a): Header, sidebar (none in dab), table header rows, code blocks, disclosure containers. Darker than canvas to frame the content and recede visually. - Canvas (
#221f21): Body / main content. The brightest reading surface — the eye rests here. - Elevated (
zinc-800/#2a2729): Cards, menus, popovers, dropdown panels. Lighter than canvas to float. - Hover (
zinc-700/#423f42): Hover state on rows inside Elevated, resting borders, secondary button rest.
Shadow vocabulary:
- Dialog scrim (
backdrop:bg-zinc-950/60 backdrop:backdrop-blur-sm): Native<dialog>element backdrop. The 4px blur is cosmetic, not structural; never larger. - Dialog lift (
shadow-2xl): Native<dialog>only. The single context where a heavy shadow earns its place — the dialog is genuinely floating over an obscured backdrop.
Named Rules
The Tonal-First Rule. Depth comes from tone, not shadow. If you need
something to feel raised, give it a lighter zinc fill — don't reach for
box-shadow. Shadows on cards or buttons read as decoration; they earn
their place only on <dialog>.
The Sticky Header Blur Rule. Header is bg-zinc-900/95 backdrop-blur-sm (4px). Never backdrop-blur-xl (24px) — the larger
radius is ~6× more expensive per frame and barely distinguishable at high
background opacity. The 5% transparency lets a hint of canvas scroll
through.
5. Components
For each component, the character line states the role before the values.
Header
The chrome at the top of every authed page. Sticky, framed in zinc-900/95
with a 4px backdrop blur, separated from the canvas by a 1px
zinc-800/60 border-bottom. The Layers brand glyph (#db3a8b stroked,
24×24, strokeWidth={2}) sits left, with dab and your DAV-native data home stacked beside it. On hover the glyph rotates -rotate-6 — the
single warmth moment in the chrome. Nav links are inline pills, accent
background at 10% with accent-300 text when active, otherwise muted with
hover:bg-zinc-800/70.
- Background:
bg-zinc-900/95 backdrop-blur-sm - Border:
border-b border-zinc-800/60 - Position:
sticky top-0 z-50 - Container:
mx-auto max-w-7xl px-4 py-3 sm:px-6 - Brand mark: Layers icon,
text-accent-400,strokeWidth={2}, hover-rotate-6 transition-transform duration-200
Footer
Two links on one row, tight padding. Matches tessera's spacing exactly
(py-3 mobile, py-2 sm+) so the suite reads consistent.
- "Made with [heart] on Cloudflare" linking to
https://limic.dev; the heart is filled coral-magenta ataccent-500. - "Source code" linking to
https://git.limic.dev/rachel/dab. - Text:
text-xs text-zinc-500withunderline decoration-zinc-700 underline-offset-2andhover:text-accent-400.
Buttons
- Shape: Soft 12px corners (
rounded-xl). 10px height (h-10) at md; 8px (h-8) at sm. Internal paddingpx-4/px-3. - Primary:
bg-accent-600 text-white→hover:bg-accent-500. Solid fill, no gradient, no accent shadow.active:scale-[0.98]on press; scopedtransition-[background-color,transform,border-color]. - Secondary:
border border-zinc-700/60 bg-zinc-800/60 text-zinc-200→hover:bg-zinc-700/60. Resting on Chrome or Canvas; rests darker inside an Elevated container. - Danger:
border border-red-500/20 bg-red-500/10 text-red-300→hover:bg-red-500/20. Confirm-shaped actions only (Revoke, Delete); the row's "delete" affordance is a ghost button with a red icon, not a danger fill. - Ghost:
text-zinc-400 hover:bg-zinc-800/70 hover:text-zinc-100. The default for icon-only buttons and inline row actions. - Loading:
Loader2from lucide spinning on the left; disable interactions and dim opacity. Optimistic where the server is fast (PAT list invalidation); spinner where it isn't (mutation in flight).
Cards
The labeled shelf. One per record (calendar, address book, PAT, file row).
- Corner Style:
rounded-2xl(16px). Same radius as dialogs; consistency across surfaces. - Default:
border border-zinc-800/60 bg-zinc-900/50 p-5 sm:p-6. The Elevated tone over canvas without a shadow. - Accent (rare):
border border-accent-500/20 bg-gradient-to-br from-zinc-900/80 to-zinc-900/40. Reserved for the one card per page that's the "this is the new thing you should look at" affordance (currently used by the home page tile that says "create your first calendar"). Do not multiply. - Interactive: Add
hover:-translate-y-0.5 transition-transform cursor-pointer. Never a border-color hover transition; lighten within the same hue family if at all.
Inputs
- Style:
w-full rounded-xl border border-zinc-700/60 bg-zinc-800/80 px-4 py-2.5 text-zinc-100 placeholder:text-zinc-500. - Focus:
focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/30 focus:outline-none. The global*:focus-visiblering (ring-2 ring-accent-500/50 ring-offset-canvas) layers over for keyboard reach. - Error / disabled: No special fill; inherit form-level error banner
- native disabled opacity. The input is not the error site; the inline
<ErrorBanner>is.
- native disabled opacity. The input is not the error site; the inline
- Textarea: Same as input plus
resize-y rows={3}.
Badge
Pill-shaped chip, mono uppercase tracking, semantic by tone.
- Style:
inline-flex rounded-full border px-2 py-0.5 text-xs font-medium. - Tones:
neutral(zinc),accent(coral-magenta 500/10 fill + 300 text),success(emerald),warning(amber),danger(red). - Use: PAT status (Active / Revoked / Expired), scope chips on PATs, any short categorical tag. Never use a badge as decoration.
Dialog
Native <dialog> element; the browser handles focus trap and Escape.
- Container:
rounded-2xl border border-zinc-700/60 bg-zinc-800/95 text-zinc-100 shadow-2xl animate-scale-fade. - Backdrop:
backdrop:bg-zinc-950/60 backdrop:backdrop-blur-sm. - Layout: Header (title + close X), body, optional footer with right-
aligned actions. Header / footer separated from body by
border-y border-zinc-700/60. - Size:
max-w-sm/max-w-md(default) /max-w-2xl.
Copyable (signature)
A row with a mono input on the left and a "Copy" button on the right, border around the lot. The receipt for any operation that produces a URL, token, or shelfmark.
- Container:
rounded-xl border border-zinc-700/60 bg-zinc-900/60 p-2. - Value: Mono,
text-sm text-zinc-200,whitespace-nowrap overflow-x-auto. - Button: Secondary-shaped, transitions to a
Checkicon + "Copied" for 1.4s after success, then back. No celebratory animation; the icon swap is the entire feedback. - Toast:
toast.success("Copied ${label}.")on success;toast.error("Could not copy ${label}.")on clipboard rejection.
Layout containers
- Page max-width:
max-w-7xl(1280px) centered,px-4 sm:px-6. - Page-vertical padding:
py-8inside the main wrapper. - PageHeader:
border-b border-zinc-800/60 pb-6 mb-6. Icon (left,text-accent-400) + display H1 (Fraunces) + body description (Hanken zinc-400). Optional action row right-aligned. Stacks below sm. - Card grids:
grid gap-3 sm:grid-cols-2for two-column collections (calendars, addressbooks). Single column for everything else.
Named Rules
The One-H1-Per-Page Rule. Every authed page has exactly one display H1
inside PageHeader. The dashboard's tile titles are H2 / CardTitle, not
H1. The serif appears once.
The Borderless Hover Rule. Hover transitions stay within the same hue
family. Borders never cross-fade between zinc and accent; doing so produces
a muddy intermediate even at 75ms. Use background tint shifts (hover:bg- zinc-700/60) for hover state, not border-color transitions.
The 75ms Default Rule. --default-transition-duration: 75ms overrides
Tailwind's 150ms default. Anything that feels sluggish at 75ms is either
animating a layout property (don't) or has too many properties in scope
(also don't). Scope transitions explicitly: transition-colors,
transition-transform, transition-[background-color,transform,border-color].
Never transition-all.
6. Do's and Don'ts
Concrete, forceful guardrails. Quote PRODUCT.md anti-references verbatim where they apply.
Do
- Do use
accent-600for the primary CTA fill, hovering toaccent-500. Solid, never a gradient.bg-accent-600 text-white hover:bg-accent-500. - Do keep the page H1 in Fraunces via
font-display. One serif H1 per page. Section titles and card titles are Hanken. - Do treat each list row as a labeled shelf: display name, internal
name (mono), one timestamp, the URL in a collapsed
<details>. No charts, no sparklines, no KPI tiles. - Do copy tessera's footer spacing (
py-3mobile,py-2sm+). The suite reads consistent because the footer is. - Do make the brand glyph rotate
-rotate-6on hover. That single micro-interaction carries the "a person made this" message; everything else stays still. - Do render calendar color as a
h-3 w-3 rounded-fulldot beside the display name. Solid fill, 1px border inzinc-700/60. No tinted background, no soft-pastel pill. - Do return the user to the URL as the receipt. After PAT or
collection creation, the page shows the URL inside
Copyable. No "Successfully created!" banner. - Do respect
prefers-reduced-motionvia the global rule. No per-element guards. - Do use
tonallayering for depth (Chrome / Canvas / Elevated / Hover). Reach forshadow-2xlonly on<dialog>.
Don't
- Don't make dab feel like Dropbox, iCloud, or Google Drive. No tile-grid file browser. No "Upgrade to Pro" banner. No share-link modal with social buttons. No "Recents" carousel. No "+ New" floating action button.
- Don't make dab feel like Nextcloud, ownCloud, or Synology DSM. No Bootstrap-styled "Files / Contacts / Calendar" tab bar. No left-rail accordion. No cyan-on-gray palette. No "App Center" landing page. No settings buried five clicks deep.
- Don't ship any AI-template dark-mode signal: no neon-on-near-black,
no gradient meshes, no glassmorphism (
backdrop-blur-xlis forbidden), no gradient text (background-clip: text+ gradient is banned), no shield-plus-checkmark hero icon, no "Your data, secure" hero copy, no "Get started in 60 seconds" CTA. - Don't drift into the iCloud-style "soft pastel" calendar aesthetic. Calendar color is one 12px dot, not a tinted pill. No pastel event-pill mocks anywhere in the UI. dab does not render a calendar grid; don't let the aesthetic leak in via empty states.
- Don't use
transition-all. Always scope to the properties that change.transition-colors,transition-transform,transition-[border-color,box-shadow]. - Don't animate border hue shifts (zinc → accent on hover). The intermediate hues read as muddy even at 75ms. Use background tint shifts instead.
- Don't use
shadow-lgorshadow-xlon interactive elements. Reserveshadow-2xlfor<dialog>. No colored accent shadows (shadow-accent-500/10) anywhere. - Don't use
font-lightorfont-thin. Body weight floor is 450. - Don't use side-stripe borders (left/right
border-l>1px as a colored accent on cards or rows). Universal ban in the impeccable vocabulary; rewrite with leading icons or full borders instead. - Don't add a "Welcome back, {name}!" or "Successfully created!" banner. The user knows their name. The row appearing is the success.
- Don't put Fraunces on more than one element per page. Cards, dialogs, badges, and buttons all use Hanken. The serif is the signature, not decoration.
- Don't use mono for "tech feel." JetBrains Mono is for mono-shaped values (URLs, IDs, ETags, host labels) only.