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
| Role | Token | Light | Peat | Use |
|---|---|---|---|---|
| Paper | mm-paper | #fdfbfa | #151210 | Page background. Never cream. |
| Surface | mm-surface | #ffffff | #1c1916 | Panels, tables and the editor |
| Raised | mm-raised | #ffffff | #26221f | Popovers, menus, dialogs |
| Sunken | mm-sunken | #f6f3ef | #181513 | Wells, table header bands, sidebars, the blade, inline code |
| Rule | mm-rule | #e1ddd8 | #36322f | Hairlines and panel borders, decorative only |
| Control border | mm-control-border | #948d86 | #76706a | Inputs, selects, checkboxes, secondary buttons |
| Ink | mm-text | #191511 | #eae7e3 | Primary text, inverse fills, focus ring |
| Ink-2 | mm-text-2 | #595450 | #a8a49f | Secondary text, labels, table headers |
| Ink-3 | mm-text-3 | #6f6a65 | #86817c | Tertiary text and captions |
| Burnt sienna | mm-primary | #b05019 | #eb9666 | Links, active tab, brand |
| Burnt sienna fill | mm-primary-fill | #b05019 | #b3531d | Primary button fill, with mm-on-primary |
| Hover | mm-primary-hover | #97400d | #9d4616 | Hover and pressed |
| Tint | mm-primary-tint | #fcede2 | #322219 | Agent tint, the current lineage node. Selection uses mm-selected (neutral) |
| Contour | mm-contour | #a17651 | #b08560 | The contour motif only, never UI state |
| Index contour | mm-contour-index | #915828 | #c49a6c | The heavier every-fifth contour |
| Moss | mm-success | #4d7455 | #8fb594 | Succeeded, healthy, allowed |
| Ochre | mm-warning | #a27825 | #d4a75a | Icons and fills: stale, truncated, expiring |
| Ochre text | mm-warning-text | #85611a | #d4a75a | Warning text |
| Slate | mm-info | #506775 | #93aab8 | Running, informational, people |
| Crimson | mm-danger | #a83634 | #ef8a84 | Failed, 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-toyversusperson). The tint is secondary:mm-primary-tintfor agents,mm-info-softfor 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-numswherever 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-1tospace-12). Page gutters arespace-8(32px) on desktop andspace-4(16px) on mobile. Within a panel,space-4tospace-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-fullfor 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-ruleborder and the paper-to-surface step are enough. Only floating layers get a shadow:shadow-floatfor popovers, menus and toasts,shadow-dialogfor 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) orrow-default(40px). Reduce row height rather than font size. Controls arecontrol-h(36px) orcontrol-h-sm(30px). The details blade isblade-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.cssexposes--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.