Design Language
Approved brand, tokens, components, and rules (dogfood copy)
Approved 2026-07-03 from the design exploration round (6 variants; "Protocol Rebuild" won). Amended 2026-07-23 — second exploration round (3 variants) locked "Open Harbor": light-first warm register + Space Grotesk display headings. Canonical mockups are now the
docs/designs/app-*.htmlset (app surfaces) plusvariant-3-open-harbor.html(marketing landing);review-page.htmlremains the anatomy reference for the review surface. Amended 2026-08-19 — status hues may now carry an agent action register on AI CTAs in both themes (see Tokens → Color). One deliberate widening of "chips, rings, and marks only", forced by light mode putting AI buttons in the same zinc as human ones. Provenance: clean-room rebuild of the Tailwind Plus Protocol template aesthetic — studied from the live preview, every line of markup written from scratch. No Tailwind Plus code may ever enter this repo (AGPL).
Brand
- Name: Kedge /kɛdʒ/ — the anchor a crew carries ahead and re-sets to pull the ship forward; kedging = progress by re-anchoring, which is the product's moat (comments that keep their place across versions).
- Tagline: "Comments that keep their place." (alt: "Reviews that hold fast.")
- Wordmark: lowercase
kedge, Space Grotesk (display face), semibold, tight tracking. - Mark: minimal line-drawn kedge anchor tilted ~15° forward (motion, not mooring); stroke weight matching panel hairlines; emerald-400 on dark, emerald-600 on light. Anchor-glyph fallback at favicon sizes. (Proper logo pass is a TODO — this is the working direction.)
- Voice: editorial-nautical restraint — "anchored / relocated / orphaned", "hold fast", "re-anchor". Never pirate-themed.
Principles
- Light-first, both themes always (flipped from dark-first 2026-07-23 with Open Harbor). Default to the warm light register — page on
stone-50, white cards — for the zero-signup "paste a URL" audience; every component still ships light + dark from day one via thedark:variant. Dark mode maps back to the original zinc register. Theme toggle in the header. - Panels are the unit of UI. Anything that isn't prose lives in a
rounded-2xlpanel with a hairline ring — threads, code, diffs, callouts, collapsed rows. One anatomy everywhere: header bar / body / action footer. - Code-things stay dark in light mode. Code panels and suggestion diffs keep the dark
zinc-900treatment in both themes (the signature move). Comment prose follows the theme. - Mono chips carry metadata. Statuses, versions, roles, and section refs render as tiny uppercase mono chips — never colored prose.
- System fonts for prose/UI; Space Grotesk for display (amended 2026-07-23). Headings, wordmark, and page/panel titles use Space Grotesk (OFL — self-host the woff2 in
web/, never a runtime Google Fonts fetch; mockups may use the CDN). Body, UI chrome, and prose stay onui-sans-serif/system-ui;ui-monospacefor code/chips. - Calm surface, quiet color. Zinc carries the page; emerald is the only brand accent. Status hues (amber/violet/rose) appear only inside chips, rings, and marks — plus the agent action register (Tokens → Color), the single deliberate extension of that rule.
Tokens
Color
| Role | Light | Dark |
|---|---|---|
| Page background | stone-50 | zinc-900 |
| Panel background | white | white/[.03] |
| Border/ring (hairline) | zinc-900/10 | white/10 |
| Divider (soft) | zinc-900/5 | white/5 |
| Heading text | zinc-900 | white |
| Body text | zinc-600 | zinc-400 |
| Muted/meta text | zinc-400 | zinc-500 |
| Brand accent | emerald-500/600 | emerald-400 |
| Code panel (both themes) | zinc-900 + ring-white/10 | same (dark:bg-white/[.03]) |
Status hues (chips, rings, marks only): open/success emerald · suggestion/pending amber · agent violet · orphan/danger rose · neutral/resolved zinc.
Agent action register (amended 2026-08-19, #143) — the one place a status hue is allowed onto a control instead of a mark. A CTA whose click leads to a model run wears violet in both themes.
Filled — the default, for any pill-shaped AI CTA:
| Light | Dark | |
|---|---|---|
| Fill | bg-violet-50 | bg-violet-400/10 |
| Text | text-violet-700 | text-violet-300 |
| Ring | ring-1 ring-inset ring-violet-600/20 | ring-violet-400/20 |
| Hover | hover:bg-violet-100 hover:text-violet-800 | hover:bg-violet-400/15 hover:text-violet-200 |
Two variants keep the text and the ring but change the resting fill, each for a stated reason:
- Quiet (
bg-white/dark:bg-white/5, hoverbg-violet-50/dark:bg-white/10) — for an AI control inside a pick-one group, where the fill already means "selected" and so cannot also mean "agent". Used by the reply-draft stance pills; the selected pill takesbg-violet-500/10with a stronger ring. - Icon square (no resting fill;
text-violet-600/dark:text-violet-300, hoverbg-violet-50/dark:bg-violet-400/10) — for an AI affordance sitting in a row of neutral icon buttons. Those siblings have no fill either, so a filled square there would read as "selected" rather than "agent". Used by the comment-split trigger. - Surface (added 2026-08-25, #151;
bg-violet-50 ring-1 ring-inset ring-violet-600/20/dark:bg-violet-400/10 dark:ring-violet-400/20, no hover) — for a block of model-written TEXT sitting among human text: the ask chat's assistant turns, against the reader's own zinc ones. Not interactive, so it defines no hover — a tint that moved under the pointer would promise an affordance that isn't there. The tint and the ring carry the register and the speaker label takes the hue (text-violet-700/dark:text-violet-300); the prose inside stays zinc, because an answer is paragraphs of reading and violet body text at that length is a headache rather than a signal.
The register is colour only: every control keeps its own geometry. A header pill stays rounded-full, the selection-popover Ask stays the rounded-lg shape of the Comment pill beside it, and the split trigger stays an h-7 w-7 square. Hue is the signal; shape belongs to the surface.
Why the rule widened. Light mode had every AI button fall back to the human primary (bg-zinc-900, white text), so a Sparkles icon was the only thing separating "this invokes a model" from "this posts as you" — a distinction the product's whole AI posture (hard rule 5: AI output is always a human-confirmed draft) depends on a reader making before they click. Dark mode had carried the violet since M4. The treatment stays inside the register's spirit — a tint and a hairline ring, never a solid violet fill — so violet still never competes with emerald as an accent.
Where the line falls. Violet means a model runs: Generate / Retry, Ask, Summarize, a reply-draft stance pill, the button that drops a draft into your composer, and any trigger that opens a panel whose only button starts a run (the header digest / improve-prompt / ask, the comment-split square). It does not mean "AI was involved": a write the person makes under their own name — post a comment, reply, approve a split proposal, approve the document — keeps the human register (bg-zinc-900 light / emerald dark), and a neutral utility that touches no model (Copy) stays zinc in both themes. Focus rings stay emerald everywhere per Interaction rules; the register is about the resting colour.
Implementation source of truth: web/components/app/ai-tone.ts. Contrast checked: violet-700 on violet-50 is 6.5:1, violet-700 on white 7.2:1, violet-300 on zinc-900 9.6:1 — all clear of WCAG AA for the type sizes in use.
Status chip recipe
font-mono text-[9-10px] font-semibold uppercase rounded-lg px-1.5 py-0.5 ring-1 ring-inset ring-{hue}-500/30 bg-{hue}-500/10 text-{hue}-700 dark:ring-{hue}-400/30 dark:bg-{hue}-400/10 dark:text-{hue}-400
Typography
- Display (headings): Space Grotesk (400–700) → system-sans fallback — H1
text-3xl font-bold tracking-tight, H2text-xl font-semibold, page/panel titles. - Sans (body/UI):
ui-sans-serif, system-ui— bodytext-sm/base leading-7, panel titlestext-xs font-semibold. - Mono:
ui-monospace— chips, inline code (ring-1 ring-zinc-300 dark:ring-zinc-700 rounded px-1), code blockstext-xs leading-relaxed.
Shape & elevation
- Panels/cards:
rounded-2xl(16px). Inner sub-panels (diffs):rounded-xl. Chips:rounded-lg. Buttons:rounded-full. - Elevation via rings, not shadows:
ring-1 ring-inset. Shadows only on the code panel (shadow-md) and floating mobile pill (shadow-xl).
Buttons
- Primary: light
bg-zinc-900 text-white hover:bg-zinc-700; darkbg-emerald-400/10 text-emerald-400 ring-1 ring-inset ring-emerald-400/20 hover:bg-emerald-400/15. Alwaysrounded-full text-sm font-medium px-3.5 py-1.5. Hero and approve CTAs may use the solid emerald variant in light:bg-emerald-600 text-white hover:bg-emerald-500. - Secondary:
bg-zinc-100 dark:bg-white/5 ring-1 ring-inset ring-zinc-900/10 dark:ring-white/10. - Text actions (panel footers):
text-xs text-zinc-400/500, hover → hue of the panel's status.
Page layout
Use the full viewport width — the docs grid must not reserve space for chrome that isn't there (--fd-layout-width: 100%, no phantom TOC column). The review pair (prose + rail) centers in the remaining width: prose column capped at 52rem (readability measure — full width goes to the pair and its breathing room, never to line length), comment rail 320px (360px at 2xl), gap 40–56px. Below xl the rail hides behind the mobile pill and prose keeps its measure.
Both side columns are collapsible (added 2026-07-21): the sidebar collapses from a button in its chip row and the rail from a slim w-12 gutter pinned to the right viewport edge; a collapsed column leaves a slim strip with a PanelLeft/RightOpen ghost icon (the rail strip also shows the open-thread count chip and a rose dot when orphans exist). Preferences persist per device (localStorage); both columns default to expanded, and activating a thread (highlight click, sidebar nav) re-opens a collapsed rail. Collapsing never widens the prose past 52rem — it buys measure on small screens and breathing room on large ones.
Component inventory (anatomy per review-page.html; Open Harbor restylings across the app-* set)
- Header — fixed,
h-14,bg-white/85 dark:bg-zinc-900/85 backdrop-blur, hairline bottom border. Logo · ⌘K search pill · nav links · theme toggle · approval avatar stack (stale approvals atopacity-50with title tooltip) · primary AI-digest button. - Sidebar — fixed
w-72, two nav groups on aborder-lhairline: Document (headings; active =-ml-px border-l-2 border-emerald-500+ heading-color text) and Threads (each row: title + right-aligned status chip — the Protocol "method label" idiom). Doc meta chips (v4,IN REVIEW, source path) at top; Re-sync as secondary button at bottom. - Version callout —
rounded-2xl ring-emerald-500/20 bg-emerald-50/50 dark:bg-emerald-500/5, info icon, trailing "View diff →" link. Never auto-swap the page. - Inline highlights —
<mark>withbg-{hue}-400/15 dark:/10,border-b-2 border-{hue}-500/60,box-decoration-clone. Emerald = comment thread, amber = suggestion. - Property list — mono chip + faded type + description,
divide-ysoft dividers. Used for anchor fields; reuse for any schema-ish content. - Code panel — dark in both themes: title bar (name + language tabs, active tab
text-emerald-400 border-b border-emerald-400) / mono context sub-bar onbg-zinc-950/40with anobj-style chip /prebody. Syntax: keysemerald-400, stringssky-300, numbersamber-300, basezinc-300. - Thread panel — header (type +
§ sectionmono ref + status chip) / body (avatarh-5 w-5initials, name, relative time; replies indented withborder-l-2) / footer (Reply · Resolve · Fork + right-aligned AI "Draft reply →"). - Suggestion panel — same anatomy; body holds a dark
rounded-xldiff (-linerose-400/90,+lineemerald-400,select-nonemarkers); footer = primary Accept / secondary Decline. - Agent thread — same anatomy with violet ring (
ring-violet-500/20) +AGENT · MCPchip. Agents are visually peers, tinted—not boxed away. - AI control — any CTA that starts a model run, or opens the panel that does: the agent action register (Tokens → Color) laid over whatever shape the surface already uses — the
rounded-fullheader and panel pills, therounded-lgAsk in the selection popover, theh-7 w-7split square in a row of thread icon buttons. Sparkles/wand/question icon, never a solid fill, never emerald. A pick-one group (the stance pills) takes the quiet variant and lets the fill mean "selected". - Ask chat panel (added 2026-08-25, #151) — the rail's second possible occupant. Header (title · "New conversation" once there is one · close) / scrolling transcript / composer pinned at the bottom with a removable selected-passage chip above the textarea and the send CTA in the agent register. A turn is two stacked bubbles: the reader's question in zinc (
bg-zinc-100/dark:bg-white/5), the answer on the AI surface variant above, carrying its coverage sentence verbatim, a zinc Copy, and the model's name. Pending, taking-too-long, and failure states all render on their OWN turn — in a conversation, "which question failed?" is a real question — with retry offered only where a retry could work. - Rail occupancy (added 2026-08-25, #151) — the right column holds the thread rail or the ask chat, never both and never a third column: overlaying would bury the rail, and a third column would push the prose below its 52rem measure. The chat wins while open, including over a collapsed rail; closing hands the column back to whatever the collapse preference already said, so the slim gutter and its stored preference stay thread-rail concerns. Below
xlthe chat presents as a slide-over sheet, the thread sheet's pattern. - Collapsed rows — resolved threads and the orphan tray as full-width
rounded-2xlghost buttons; orphan row uses rose ring/tint + "Re-attach →". - Section-approve control — "Is this section ready to approve?" + segmented Yes/No pill (the Protocol feedback-widget idiom, repurposed).
- Mobile — sidebar and rail hidden below
lg/xl; floating bottom pill ("3 open threads") opens the thread sheet.
Interaction rules
- Hover: text-color shifts and
bg-*/5tints only — no scale/translate effects. Respectprefers-reduced-motionfor anything animated. Exception (2026-08-19): a control that already carries a resting tint deepens it instead of stacking another translucent layer on top — the agent action register'shover:bg-violet-100/dark:hover:bg-violet-400/15, and the emerald human primaries'dark:hover:bg-emerald-400/15. The rule is about hover staying a tint, not about the exact step. - Focus: visible
focus-visible:ring-2 ring-emerald-500on all interactive elements (mockup omits this; implementation must not). - Highlight contrast in dark mode must keep body text readable through the tint — validated per hue before adding new mark colors.
Canonical mockups (locked 2026-07-23)
All in docs/designs/, cross-linked as a click-through prototype; every page has the light/dark toggle.
variant-3-open-harbor.html— marketing landing: hero, capability tour, workspace console, roadmap cards, self-host CTAapp-dashboard.html— workspace home: import box on top (M3.5), projects rail + Unfiled, filterable docs table with live import states, activity feed, M5 queue ghostapp-project.html— project page (M3.6): tracked sources with scan report + flagged deletions (never auto-delete), add-source → preview → bulk import, project doc listapp-document.html— the review surface in the Open Harbor register (layout/anatomy unchanged fromreview-page.html)app-versions.html— version timeline (incl. an ADR-0001 PR-candidate entry), diff with comment overlay, version-pinned approvals with stalenessapp-workspace.html— workspace settings: PAT connector (shipped), MCP agent tokens (M4 ghost), M6 connector + members ghostsapp-teams.html— post-v1 design preview of account → workspace → teams/members (hierarchy note: SPEC §10.1); not v1 scope
Implementation notes
- Target: Next.js + Tailwind v4 + Fumadocs shell restyled to these tokens (see SPEC §4.1; spike pending). Express tokens as Tailwind theme variables so both editions/themes stay in sync.