diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..400a051 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,327 @@ +# Linkder Design System + +**Version 1.0** · derived from the wix.com design language, adapted for Linkder. + +This document is the single source of truth for how Linkder looks. Every screen must be +buildable from the tokens and components below. If a screen needs something that is not in +here, add it here first, then build it. + +## Provenance and scope + +The visual language is adapted from **wix.com** (sampled 2026-08-21): the Madefor typeface +pair, the signature action blue, the navy-black ink, the pill geometry and the generous +whitespace rhythm. Values below were read off the live stylesheet, not eyeballed. + +Adopted: typefaces (Wix Madefor Display / Text, published under the SIL Open Font License via +Google Fonts), colour ramps, radius and spacing conventions, component geometry. + +**Not** adopted: Wix's logo, wordmark, product names, illustrations or photography. Linkder is +not affiliated with Wix and must never present itself as such. + +--- + +## 1. Principles + +1. **Blue means action.** `brand-500` is reserved for things the user can do. It is never + decoration, never a background wash for a whole section, never body text. +2. **Ink is near-black, not grey.** Headings sit at `ink-950` (`#000624`) — black with a blue + cast. Grey text is a deliberate demotion, not a default. +3. **Type carries the design.** Big, tight display type over ornament. Negative tracking on + anything above 24px; zero tracking on body. +4. **Pills for actions, soft rectangles for containers.** A pill is clickable. A rounded + rectangle holds content. Never the reverse. +5. **Whitespace over borders.** Separate with space first, a hairline second, a shadow last. + +--- + +## 2. Colour + +All values are sampled from wix.com. Hex is normative. + +### 2.1 Brand — action blue + +| Token | Hex | Use | +|---|---|---| +| `brand-50` | `#EDF3FF` | Hover fill on ghost / outline controls | +| `brand-100` | `#DCE8FF` | Selected-chip fill, info banner background | +| `brand-200` | `#95B9FF` | Focus ring halo, disabled primary | +| `brand-400` | `#5E97FF` | **Dark-mode** link and accent text | +| `brand-500` | `#116DFF` | **Primary action.** Buttons, links, active states | +| `brand-600` | `#094BCC` | Primary hover / pressed | +| `brand-700` | `#082F7B` | Primary active on dark surfaces | + +`brand-500` on white is **4.5:1** — it clears AA for normal text, but only just. Do not tint it +lighter for text. On dark surfaces use `brand-400` (7.0:1 on `ink-950`) instead. + +### 2.2 Ink — text and surfaces + +| Token | Hex | Use | +|---|---|---| +| `ink-0` | `#FFFFFF` | Card surface, page background (light) | +| `ink-50` | `#F7F8F8` | Alternating section background | +| `ink-100` | `#F0F0F0` | Inset fills, skeletons, track backgrounds | +| `ink-200` | `#E2E2E2` | **Hairline borders** | +| `ink-300` | `#C2C2C2` | Border on dark, disabled border | +| `ink-400` | `#B0B0B0` | **Dark-mode** secondary text | +| `ink-500` | `#8F8F8F` | Placeholder and disabled text **only** (3.2:1 — never body copy) | +| `ink-600` | `#6E6E6E` | **Secondary body text** (5.1:1 on white) | +| `ink-800` | `#212121` | Body text on tinted surfaces | +| `ink-950` | `#000624` | **Headings, primary text, dark surfaces** | + +### 2.3 Semantic + +| Token | Hex | Meaning | +|---|---|---| +| `go-50` | `#F3F8F0` | Success banner background | +| `go-100` | `#D2E4C7` | Success border | +| `go-400` | `#92B079` | Success on dark | +| `go-600` | `#618741` | **Success** — verified, accepted, right-swipe | +| `go-700` | `#39641D` | Success pressed | +| `sun-50` | `#FFE9DF` | Warning / pending banner background | +| `sun-100` | `#FFBFA1` | Warning border | +| `sun-400` | `#FA854F` | Warning on dark | +| `sun-500` | `#EA6020` | **Warning** — pending review, expiring soon | +| `sun-600` | `#C94001` | Warning pressed | +| `stop-50` | `#FFECEC` | Error banner background | +| `stop-100` | `#FFC9CB` | Error border | +| `stop-400` | `#FF6A70` | Error on dark | +| `stop-500` | `#ED1C24` | **Error / destructive** — rejected, left-swipe | +| `stop-600` | `#C4141B` | Error pressed | + +Never use green/red as the *only* signal. Pair with an icon and a word. + +### 2.4 Dark mode + +Dark mode inverts the ink ramp and lightens the accent. It is not a separate palette. + +| Role | Light | Dark | +|---|---|---| +| Page background | `ink-0` | `ink-950` | +| Raised surface (card) | `ink-0` | `#0C1230` | +| Sunken surface | `ink-50` | `#070C22` | +| Primary text | `ink-950` | `ink-0` | +| Secondary text | `ink-600` | `ink-400` | +| Hairline | `ink-200` | `rgb(255 255 255 / .14)` | +| Accent text / link | `brand-500` | `brand-400` | +| Filled primary button | `brand-500` bg, white text | unchanged | + +--- + +## 3. Typography + +### 3.1 Families + +| Role | Family | Weights | +|---|---|---| +| Display — h1–h3, big numbers, buttons | **Wix Madefor Display** | 400–800 variable | +| Text — body, labels, inputs, meta | **Wix Madefor Text** | 400–800 variable | + +Fallback stack: `'Segoe UI', system-ui, -apple-system, sans-serif`. + +Load via `next/font/google` with `display: 'swap'`, exposed as CSS variables. Never link a +stylesheet in `` — it costs a render-blocking round trip. + +### 3.2 Scale + +Display sizes use `clamp()` so one token works from 360px to desktop. + +| Token | Size | Line | Tracking | Weight | Family | +|---|---|---|---|---|---| +| `display-xl` | `clamp(2.75rem, 6vw, 4.5rem)` | 1.04 | `-0.03em` | 700 | Display | +| `display-lg` | `clamp(2.25rem, 4.5vw, 3.25rem)` | 1.08 | `-0.025em` | 700 | Display | +| `h1` | `clamp(1.875rem, 3.5vw, 2.5rem)` | 1.12 | `-0.02em` | 700 | Display | +| `h2` | `clamp(1.5rem, 2.5vw, 2rem)` | 1.18 | `-0.02em` | 700 | Display | +| `h3` | `1.375rem` | 1.25 | `-0.015em` | 600 | Display | +| `h4` | `1.125rem` | 1.35 | `-0.01em` | 600 | Display | +| `body-lg` | `1.125rem` | 1.55 | `0` | 400 | Text | +| `body` | `1rem` | 1.6 | `0` | 400 | Text | +| `body-sm` | `0.875rem` | 1.5 | `0` | 400 | Text | +| `meta` | `0.75rem` | 1.45 | `0` | 500 | Text | +| `overline` | `0.75rem` | 1.4 | `0.08em` | 700 | Text, uppercase | + +### 3.3 Rules + +- **One display element per view.** A page has one `h1`. A card has none. +- Tracking is negative above 24px and exactly zero below it. Never positive except `overline`. +- Body copy maxes at **68 characters** (`max-w-[68ch]`). +- Headlines use `text-balance`; paragraphs use `text-pretty`. +- Prices, counters and table figures use `tabular-nums`. + +--- + +## 4. Space and layout + +An **8px grid**. The only legal spacing values are `4 8 12 16 24 32 40 56 72 96 128`. Anything +else is a bug. + +| Container | Width | Used for | +|---|---|---| +| `prose` | 680px | Reading, forms, single-column flows | +| `app` | 1080px | Dashboards, lists | +| `wide` | 1280px | Marketing sections | + +- Page gutter: **24px** mobile, **32px** ≥768px. +- Vertical rhythm between page sections: **72px** mobile, **96px** desktop. +- Gap between sibling cards in a list: **12px**. +- Label → control: **8px**. Control → helper text: **8px**. Field → field: **24px**. + +--- + +## 5. Radius and elevation + +| Token | Value | Applies to | +|---|---|---| +| `radius-pill` | `999px` | **All buttons**, chips, avatars, badges | +| `radius-card` | `18px` | Cards, banners, modals | +| `radius-lg` | `12px` | Inputs, list rows, small containers | +| `radius-md` | `8px` | Tags, thumbnails, inline code | +| `radius-deck` | `28px` | The swipe card only | + +Elevation is restrained. Four steps, no more: + +| Token | Shadow | Use | +|---|---|---| +| `shadow-none` | — | Default. Most surfaces are flat with a hairline. | +| `shadow-sm` | `0 1px 2px rgb(0 6 36 / .06), 0 1px 3px rgb(0 6 36 / .04)` | Hover on a list row | +| `shadow-md` | `0 4px 12px rgb(0 6 36 / .08), 0 2px 4px rgb(0 6 36 / .04)` | Sticky header, popovers | +| `shadow-lg` | `0 12px 32px rgb(0 6 36 / .12), 0 4px 8px rgb(0 6 36 / .06)` | Deck card, modal | + +Shadows are tinted with `#000624`, never pure black. + +--- + +## 6. Components + +### 6.1 Button + +Always a pill. Always Display family, weight 600. Label is sentence case, never ALL CAPS. + +| Variant | Fill | Text | Border | Hover | +|---|---|---|---|---| +| `primary` | `brand-500` | white | none | `brand-600` | +| `dark` | `ink-950` | white | none | `#1A2145` | +| `outline` | transparent | `ink-950` | `1.5px ink-950` | `ink-50` fill | +| `ghost` | transparent | `brand-500` | none | `brand-50` fill | +| `danger` | `stop-500` | white | none | `stop-600` | + +| Size | Height | Padding-x | Font | +|---|---|---|---| +| `sm` | 36px | 16px | `body-sm` | +| `md` | 44px | 24px | `body` | +| `lg` | 56px | 32px | `body-lg` | + +Control heights are their own scale — **36 / 44 / 56** for buttons, **48** for inputs. They are +exempt from the 8px spacing grid in §4, which governs margins, padding and gaps. + +- Minimum touch target is **44px**. `sm` is desktop-only or must sit inside a 44px row. +- Disabled: `opacity: .45`, `cursor: not-allowed`. Never grey out by swapping colour. +- Busy: leading 16px spinner, label unchanged, control disabled. Never replace the label. +- Icon-only buttons are circles and require `aria-label`. + +### 6.2 Input, textarea, select + +- `radius-lg` (12px), `1.5px ink-200` border, `ink-0` fill, **16px** text (never smaller — iOS + zooms below 16px), padding `12px 16px`, height 48px. +- Focus: border `brand-500` + `0 0 0 3px brand-200`. Never remove the outline without + replacing it. +- Error: border `stop-500`, message below in `body-sm` `stop-500` with `role="alert"`. +- Placeholder is `ink-500` and never carries meaning the label doesn't. +- Every control has a visible `