Skip to main content

Colour, type and space

Every value is a CSS custom property named after its token (--mm-text, --mm-primary, --space-4, --radius-md). Components reference semantic tokens and never raw hex. There are two themes: Light and Peat (data-theme="dark"), a separately tuned warm graphite, not an inversion. Load components/bundle.css after the tokens for the mm-* component classes.

Colour​

RoleTokenLightPeatUse
Papermm-paper#fdfbfa#151210Page background. Never cream.
Surfacemm-surface#ffffff#1c1916Panels, tables and the editor
Raisedmm-raised#ffffff#26221fPopovers, menus, dialogs
Sunkenmm-sunken#f6f3ef#181513Wells, table header bands, sidebars, the blade, inline code
Rulemm-rule#e1ddd8#36322fHairlines and panel borders, decorative only
Control bordermm-control-border#948d86#76706aInputs, selects, checkboxes, secondary buttons
Inkmm-text#191511#eae7e3Primary text, inverse fills, focus ring
Ink-2mm-text-2#595450#a8a49fSecondary text, labels, table headers
Ink-3mm-text-3#6f6a65#86817cTertiary text and captions
Burnt siennamm-primary#b05019#eb9666Links, active tab, brand
Burnt sienna fillmm-primary-fill#b05019#b3531dPrimary button fill, with mm-on-primary
Hovermm-primary-hover#97400d#9d4616Hover and pressed
Tintmm-primary-tint#fcede2#322219Agent tint, the current lineage node. Selection uses mm-selected (neutral)
Contourmm-contour#a17651#b08560The contour motif only, never UI state
Index contourmm-contour-index#915828#c49a6cThe heavier every-fifth contour
Mossmm-success#4d7455#8fb594Succeeded, healthy, allowed
Ochremm-warning#a27825#d4a75aIcons and fills: stale, truncated, expiring
Ochre textmm-warning-text#85611a#d4a75aWarning text
Slatemm-info#506775#93aab8Running, informational, people
Crimsonmm-danger#a83634#ef8a84Failed, denied, destructive

Each status has a -soft fill (mm-success-soft, mm-warning-soft, mm-info-soft, mm-danger-soft): the same hue at about 95% lightness. Text on a soft fill uses mm-text or the status text colour, never a lighter step. The editor has its own fixed set: mm-code-bg #171513 with mm-code-text and the mm-syn-* syntax colours.

Lineage: these values refine Marmot's existing earthy palette. The old earthy-terracotta-700 maps to mm-primary and earthy-brown-50 maps to mm-paper. If an existing screen uses an earthy-* class, translate it to the nearest semantic token rather than keeping the old value.

In Peat, keep neutral chroma at or below about 0.008 so it reads as warm graphite, not coffee-shop brown. Never use pure #000 or #fff for working surfaces. Show elevation with lighter surfaces (mm-paper, then mm-surface, then mm-raised), not shadows. Accents are desaturated and lightened so nothing glows.

Colour rules​

  • The accent is burnt sienna: an earthy orange, deeper than the bright terracotta of many tools and well clear of crimson, so the primary button can't be mistaken for an error or a delete. Keep at least that distance from red.
  • Burnt sienna is for the primary button, links, the active tab underline and the logo only. It carries no status meaning and is not used as a large fill. Selected items use the neutral mm-selected. Per row or card, burnt sienna appears at most once.
  • Destructive actions use crimson plus an icon plus the word ("Delete asset"), so they can never be mistaken for the primary.
  • Agents and people are told apart by icon and label (Material Symbols smart-toy versus person). The tint is secondary: mm-primary-tint for agents, mm-info-soft for people.
  • Provider logos (PostgreSQL, Kafka, Snowflake and so on) are the only full-colour artwork in working views. They identify sources and are never recoloured.

Typography​

Two free font families under the SIL Open Font License: Geist for all text, titles included, and its companion Geist Mono for machine strings. They were drawn as a pair, so code and prose sit together without a change of voice. Both are plain and easy to read at small sizes, with clear, evenly fitted figures for a product full of counts and measures, and they sit quietly beside the wordmark. The interface uses no serif type; the one serif is the Marmot wordmark (QT Friz Quad, a free version of Friz Quadrata), which is fixed artwork. Character comes from the wordmark, burnt sienna and the contours, not from a stylised typeface. The fonts are self-hosted (Fontsource packages @fontsource-variable/geist and @fontsource-variable/geist-mono), so self-hosted installs make no calls to Google.

Display, --font-display (Geist). The same family as the text, set large in semibold.

Page titles (display-title, 34px), KPI values (display-kpi, 32px), section titles of 24px and above (display-section, 26px), empty-state headlines and marketing (display-hero, 56px). Weight 600, tracking −0.02em to −0.03em (tighter as the size grows), line height 1.0–1.15, at least weight 600 and 24px. Smaller headings use heading or heading-sm, and body text, buttons, labels, navigation and table cells use the text styles. Numbers in it use tabular figures. One display title per view region.

Text, --font-text (Geist). All interface text. body is 16px/1.55; dense contexts (lists, tables, sidebars, metadata, toolbars, forms) use dense at 15px/1.45; captions use caption at 14px.

Nothing is smaller than 13px. Every style moves together: change the scale as a whole rather than one context on its own. Weight 400 for text, 600 for emphasis, buttons and labels, 700 for headings under 24px. Sentence case everywhere. Field labels and column headers use label: 14px, 600, sentence case, mm-text-2.

Mono, --font-mono (Geist Mono). Machine strings only: MRNs and asset identifiers, SQL and the editor, column types, IDs and hashes, log timestamps, result-grid cells and keyboard keys.

Set it about 1px smaller than the surrounding text (mono, mono-dense, mono-editor). Its zero, O, one, l and I are distinct by design, so no slashed-zero feature is needed.

No uppercase. Nothing in the interface is set in capitals: no uppercase labels, eyebrows, metadata strips or buttons.

Metadata under a title is a sentence-case caption in the text face and mm-text-2 ("1,284 rows · Updated 5 hours ago"), with relative times and only facts shown nowhere else on the page. Keyboard keys keep their own keycap style.

  • Numbers: tabular-nums wherever numbers are compared. Right-align numbers and their column headers. Format compactly (1.28M, 412 ms) with a non-breaking space before units.
  • Measure: prose runs 60–75 characters. Tables and logs are exempt.

Space, shape and elevation​

  • 4px base grid (space-1 to space-12). Page gutters are space-8 (32px) on desktop and space-4 (16px) on mobile. Within a panel, space-4 to space-5. Between related controls, space-2.
  • Radius follows role: radius-sm (4px) for controls, inputs, tags and keycaps; radius-md (6px) for panels, cards, buttons and menus; radius-lg (10px) for dialogs; radius-full for avatars, status pills and filter pills only. A child's radius is never larger than its parent's.
  • Panels and tables have no shadow: a 1px mm-rule border and the paper-to-surface step are enough. Only floating layers get a shadow: shadow-float for popovers, menus and toasts, shadow-dialog for dialogs and the command palette. Both are two-layer and ink-tinted.
  • Do not nest panels. Inside a panel, separate sections with hairline rules or spacing.
  • Table rows are row-compact (36px) or row-default (40px). Reduce row height rather than font size. Controls are control-h (36px) or control-h-sm (30px). The details blade is blade-w (36rem).

Motion​

  • 120–160ms for presses and hovers, 150–200ms for menus and popovers, 200–250ms for dialogs and blades. Nothing exceeds 300ms. bundle.css exposes --mm-ease (cubic-bezier(0.23, 1, 0.32, 1)) and --mm-dur-press, --mm-dur-menu, --mm-dur-dialog.
  • Animate only transform and opacity (and colour for state). Never write transition: all.
  • The command palette, context menus and anything triggered from the keyboard appear instantly.
  • No scroll-triggered fade-ups. Cards do not lift on hover; their border darkens instead.
  • Delay spinners and skeletons about 200ms so fast loads never flash, then keep them visible for at least 400ms.
  • A pulse is allowed only on something genuinely live, such as a running pipeline (mm-status--live).
  • Under prefers-reduced-motion, movement becomes an opacity change and every loop stops.