Colors

Warm Canvas
Background · marketing
#FDFAF5
Warm Cream
Muted background
#FEF3E2
Linen
Border
#E8E3DD
White
Background · product · card
#FFFFFF
Muted Cool
Muted background
#F5F5F4
Border Cool
Border
#E7E5E4
Mid-Stone
Divider · disabled text
#B8AFA8
Stone
Muted text
#756D68
Graphite
Secondary foreground
#433E3A
Charcoal
Foreground
#1C1917
Copper Soft
Selected background
#FDECD8
Copper Border
Selected ring
#F5C9A1
Copper Focus
Focus ring
#F59342
Copper
Brand accent · borders · links
#E2711D
Copper Dark
Accent hover
#CC5D0A
Copper Ink
Primary button fill (AA 5:1)
#B45309
Copper Ink Dark
Primary button hover
#92400E
Sky
Info subtle
#EFF6FF
Blue Border
Info border
#BFDBFE
Blue
Info emphasis
#3B82F6
Blue Dark
Links
#2563EB
Emerald
Success
#16A34A
Red
Error
#DC2626
Amber
Warning
#EAB308

Typography

DisplaySpace Grotesk 400 · 64/1.02 · -0.02em
Documents in, data out.
H1Space Grotesk 400 · 48/1.05 · -0.02em
Configurable extraction
H2Space Grotesk 400 · 32/1.15 · -0.01em
Define the template once
H3Space Grotesk 500 · 20/1.3
Feed many documents through it
H4Space Grotesk 500 · 16/1.4
Data lands where you need it
Body LIBM Plex Sans 300 · 18/1.55
Sem Digitar turns receipts, invoices, and contracts into structured rows in a spreadsheet. Define the template once, then feed hundreds of documents through it.
BodyIBM Plex Sans 300 · 15/1.55
Claude reads the document, applies your template, and writes the extracted fields to Google Sheets, an email, or a webhook. No manual typing.
Body emIBM Plex Sans 400 · 15/1.55
Each field carries a name, a type, and a rule — the same ones you'd teach a new intern.
SmallIBM Plex Sans 400 · 12/1.5
Sample · caption · helper text · meta annotation · footnote
NumericGeist Mono 600 · 32 · -0.01em
1,248 documents · 99.2%
MonoGeist Mono 500 · 14 · 0.02em
{invoice_number} · {total_amount} · {issued_at}

Spacing & Layout

Standards & overrides. This system adopts the mainstream layout rules (4pt grid, fixed type scale, three named max-widths, layout primitives over ad-hoc flex, Grid only for two-axis alignment). It overrides the reference standard's specific numbers in four places: spacing scale skips 20 / 40 / 80 / 128; body type is 15px (Linear/Stripe-style) rather than 16/17px; radii are subtle (6 / 8 / 10) rather than dramatic; container widths are device-derived (sm 640 / md 768 / lg 1024 / xl 1100) rather than role-derived. Treat these as authoritative — consuming projects follow this system, not the reference standard, when the two disagree.
sp-1--sp-1
4px
sp-2--sp-2
8px
sp-3--sp-3
12px
sp-4--sp-4
16px
sp-6--sp-6
24px
sp-8--sp-8
32px
sp-12--sp-12
48px
sp-16--sp-16
64px
sp-24--sp-24
96px
r-2--r-2 · Tight
6px
r-3--r-3 · Default
8px
r-4--r-4 · Card
10px
r-full--r-full · Pill
full
smReading width
640px
mdForm · dialog
768px
lgApp content
1024px
xlDesign doc · marketing
1100px
Three reusable insets. Every card, dialog body, and surface panel reaches for one of these — never an ad-hoc literal. Reach for tight in dense rows or compact lists, default for standard cards / dialogs, lg for hero frames.
tight--pad-card-tight · sp-4
16px
default--pad-card-default · sp-6
24px
lg--pad-card-lg · sp-8
32px
One token, every viewport. --pad-x: clamp(20px, 5vw, 32px). Use on <main>, marketing shells, and any page-level container — padding fluidly grows with the viewport from 20px on phones to 32px above 640px. Replaces ad-hoc padding: 0 32px + media-query overrides.
Three steps. --bp-sm: 640px · --bp-md: 768px · --bp-lg: 1024px. CSS custom props can't appear inside @media queries (spec limitation), so the same literals are repeated in media rules. The tokens exist so JS, container queries, and clamp() math share a single source of truth.
Pick the role, not the size. sm reading-width prose · md form / dialog · lg app content · xl design doc / marketing. Build pages from the .center-* primitives below — they apply both the right max-width and --pad-x.
Compose pages from these — don't re-derive flex per section.Each primitive owns its own gap and breakpoint logic, so spacing compounds consistently across section boundaries. Reach for raw display: flex/grid only when none of these fits.
Catalog.
  • .stack-{1,2,3,4,6,8} — vertical flow with token gap. The default for almost everything.
  • .cluster — horizontal wrap (tags, action rows, nav).
  • .center / .center-prose / .center-form / .center-wide — max-width + horizontal centering + --pad-x.
  • .sidebar — fixed-width rail + flexible main (app shells).
  • .switcher — flex-row above ~320px child width, flex-column below (card rows).
.stack-3 — gap: 12px
child 2
child 3
.switcher · row above 320px child, column below
child 2
child 3

Components & Icons

SuccessErrorWarningInfo
Badge vs Chip vs Button. Badge is a read-only status marker (Success / Error). Chip is a selectable taxonomy token — filter, tag, option — that toggles pressed state. Button triggers a one-shot action. If it's interactive and toggles, it's a chip; if it fires once, it's a button; if it only describes state, it's a badge.
Toggle chip vs Removable chip. Toggle chips use aria-pressed — click flips state, chip stays visible (filters, multi-select). Removable chips use .chip-removable with aria-label="Remove [item]" — the whole chip is a one-shot button; click anywhere removes it. Do NOT set aria-pressed on a removable chip: it is not a toggle, and screen readers should not announce a pressed state for an action that only disappears.
Invoice template
Extracts supplier, total, issue date, and line items from Brazilian NF-e invoices.
Receipt template
Reads retail receipts and pulls merchant, amount, category, and timestamp.
When to use. Mutually-exclusive short list (2–5 items), all options visible at once, user picks one value. Billing cycle (monthly / annual), density (comfortable / compact), role (owner / admin / viewer). If the list is longer than 5, use a <select>. If selecting a tab switches a content panel, use tabs instead — this pattern sets a value, not a view.
Semantics. Container is role="radiogroup" with aria-label; each button is role="radio" + aria-checked="true|false". Arrow keys (←/→, ↑/↓) move focus and selection; only the active button is tab-stoppable.
Dialog vs AlertDialog. role="dialog" is for anything modal: forms, info, onboarding flows. role="alertdialog" is reserved for destructive / interrupting confirms — screen readers announce it more forcefully. Rule of thumb: if ignoring the prompt has a cost, it's an AlertDialog. Otherwise it's a Dialog.
Build with native <dialog>. Use the HTML <dialog> element with .showModal(). You get focus-trap, escape-to-close, ::backdrop, and correct default ARIA for free.
Action-button layout.Desktop: right-aligned, [cancel · confirm]. Mobile (< 480px): reversed-stack, confirm on top for thumb-reach. Primary button verb matches the action — "Delete", "Send invites", "Cancel subscription". Never "OK" / "Yes".

Invite teammates

They'll get an email with a magic link to join your workspace.

Delete template "Invoices-BR"?

The template has extracted 8,412 documents. Deleting removes the template and its field mappings — extracted data stays. This cannot be undone.

Tier the treatment to the blast radius. Not every destructive action needs a modal. Match the friction to the cost of a mistake. This system does not ship a red "danger" button variant — copy, context, and type-to-confirm carry the signal.
Reversible
Archive, hide, soft-delete. No modal. Show an undo toast for 5–10s in an aria-live="polite" region. Gmail archive, Notion move-to-trash, Linear archive issue.
Recoverable
Move to trash with a retention window (e.g. 30 days). Use <dialog role="alertdialog">. Neutral primary button; destructive intent lives in the copy. "Move to trash? We'll keep it for 30 days."
Irreversible
Hard delete, revoke API key, cancel subscription, destroy workspace. AlertDialog + type-to-confirm. Confirm button stays disabled until the user types the exact target name.

Delete workspace "Acme Labs"?

All templates, extracted documents, webhooks, and team seats will be permanently deleted. Billing history is retained for legal reasons. This cannot be undone.

Two loaders, two contexts. Full-page loader when a whole surface or large region is waiting on one operation. Inline loader when the wait is local — inside a card, chat bubble, button action, or a step within a form. Never show both in the same viewport at once.
Loading…
Loading…
Preview the shape of what's coming. Skeletons are not loaders — they are placeholder blocks sized to match the content that will render, shown while that content is being fetched.
check
x
plus
arrow-right
arrow-up
search
info
alert
document
upload
external-link
settings