Kontour Design System#
Status: canonical design direction for Kontour product interfaces and brand usage. Audience: designers, engineers, product teams, and coding/design agents. Core promise: Trust the work AI does.
This file is the design constitution for Kontour AI. It follows the
DESIGN.md format: machine-readable tokens in
the front matter, roles and rules in the body. It was restructured from the owner-supplied
design draft (2026-09-27) and reconciled against the shipped --k-* token contract.
This file states roles and rules. Values live in tokens/. The front matter is generated
from tokens/tokens.css, tokens/themes.css, and react/styles.css by
scripts/generate-design-md.mjs; npm run check:design fails when it drifts. Change a value in
tokens/, then run node scripts/generate-design-md.mjs --write. Never hand-edit the front
matter. Prose restates a shipped value only as a backticked token name, an equals sign, and a
backticked value (optionally with a scope such as (light) or (flow) after the name). The
same check verifies every such quote against tokens/, wherever it appears (table header rows
and "Draft" columns included). It also fails when any other value-looking text (a px/em/rem
size, a hex or rgb()-style color, or a bare number) appears anywhere on a prose line, or
anywhere in a table row (header rows included), that names a token. For that second rule only,
table columns whose header starts with "Draft" are exempt, since they hold the draft's
direction rather than shipped values. It ignores link targets (link text is still scanned),
#n issue references except 3-, 4-, 6- or 8-digit ones (those read as hex colors, so such an
issue beside a token must be written as a link), acronym versions such as WCAG 2.2, and
OPEN-n ids. Not covered: a value placed on a different line from the token name.
Reading the packaged copy under node_modules/@kontourai/ui/: the generator script is not in
the package. The tokens/*.css files next to this file are the source of every value; the
front matter is a projection of them made before publishing.
Authority#
Precedence, highest first: approved canonical SVGs and tokens → this file → approved product
docs → existing, unsuperseded production patterns → reference artwork → generated concepts.
Token values are canonical as shipped (owner decision, 2026-10-01): tokens/ holds the palette
and the spacing, radius, and type scales, and this file restates them. Where the earlier draft
named different values, the draft was not adopted; its values stay in this file only as a
record of what was considered. Changing a token value is a design-system change and needs its
own decision. Generated images are references, not production assets. A generated concept sheet
is held privately by the owner; it is reference only, never a production source.
Anything marked OPEN is unresolved. Do not silently resolve it in either direction: an OPEN item records what ships today and where the design is headed, and both stay true until someone decides. An item marked Decided keeps its id and carries the date and who decided. The full register is in Open Decisions.
Reading the token block#
The DESIGN.md format has no concept of modes or themes, so the generated keys encode them:
| Key shape | Meaning | Example |
|---|---|---|
name |
The CSS custom property minus --k-, dark default (:root) |
space-5 is --k-space-5 |
name-light |
The value under [data-theme="light"] |
bg-light |
name-<theme> |
The value under .theme-<theme> (dark) |
brand-flow |
name-<theme>-light |
The value under [data-theme="light"] .theme-<theme> |
brand-flow-light |
A reference such as focus-ring: "{colors.focus}" means "the focus value of the same mode and
theme". CSS custom properties that use var() resolve where declared and descendants inherit
the computed value. So every scope that sets the focus role redeclares the compatibility
--k-focus-ring alias, and every scope that sets a status tone redeclares its soft fill
(npm run check:tokens enforces both). Each theme's light block matches the theme class and
data-theme="light" on one element, the class below the attribute, and the attribute below
the class, so those three placements resolve to the theme's light values; which of them applies
when modes or themes nest is set by Theme and mode scoping. One
thing is not covered:
- A local inline override of an input (say, the focus role on one panel) does not update the
alias on that element's descendants. That is why consumer CSS should read
--k-focusrather than the alias.primaryis not a CSS token; it is derived from whatever.btn-primarypaints, so the format's requiredprimarycolor stays truthful.
Type levels (display, title, body-md, label, eyebrow, badge, button) are read from
the shipped selectors that use them, and text-* levels are the bare size steps. Components are
read from their classes in react/styles.css; properties that are shorthands or calc()
expressions, and rules inside @media, are left out rather than approximated. Tokens the format
cannot carry (font-family stacks, shadows, soft status fills, motion, stroke widths,
line-height and letter-spacing scales, stacking order, trust-state line styles) are listed with their values in a
comment at the end of the front matter, so the drift check covers every token.
The generator buckets each --k-* token by name pattern; an alias (var(--k-other)) takes its
target's bucket; any other token whose value is a literal hex or rgb()/rgba() color is
treated as a color. A token that fits none of these, or a token rule nested in an at-rule,
makes the generator fail until someone decides how this file represents it.
npx @google/design.md lint DESIGN.md reports every color that no generated component
references as an orphaned-tokens warning: default structure tokens such as bg, line,
line-strong, and text-faint, the status tones, the trust-state inks and fills, and every
light and theme variant. That is expected: only five components are generated, for the dark
default, and status tones and trust states are consumed by their classes rather than a component
entry.
Token architecture#
The draft's direction: primitive tokens → semantic tokens → component tokens → justified product overrides, and "components consume semantic tokens whenever practical." What ships maps onto it only partly:
- Primitive (not shipped): there is no separate palette layer;
tokens/tokens.cssassigns literal values straight to semantic names. The draft's named brand palette (Ink, Stone) was not adopted (#74, decided 2026-10-01); see Product themes. - Semantic (shipped, partly): the
--k-*structure, text, status, trust-state, action, and focus roles.--k-actionand--k-action-contrastform the primary action pair;--k-focussupplies keyboard focus. They are independent of the product brand slot. - Component (not shipped as tokens): primitives in
react/styles.cssread semantic tokens directly. The front matter'scomponentsentries are derived from those rules; they are not a separate token layer. - Justified product overrides (shipped as theme classes): product themes set the identity
slot (
--k-brand,--k-brand-contrast) and their interaction roles independently..theme-consolealso sets a fuller surface and status skin. A white-label deployment can change only the identity slot without repainting primary buttons or focus rings.
Components consume semantic tokens. Do not scatter raw hex values, font sizes, arbitrary radii,
or one-off spacing through repositories; npm run check:tokens already forbids literal colors
in react/styles.css.
Overview#
Kontour AI makes software that helps end users understand, inspect, validate, and trust work performed by AI. A contour makes hidden structure legible; Kontour applies that idea to AI work:
Turn uncertainty into clarity.
The product should not demand trust. It exposes enough evidence, provenance, state, and explanation for users to establish justified confidence.
The brand feels precise, calm, rigorous, clear, capable, contemporary, and human; technical without hype. Avoid cyberpunk, magical-AI imagery, surveillance aesthetics, fear, generic enterprise SaaS, and complexity for its own sake.
Interfaces feel calm, deliberate, inspectable, organized, and information-rich without noise.
The Kontour world#
Kontour owns the terrain. Products own what happens within it.
| Identity | Phenomenon | Meaning |
|---|---|---|
| Kontour | Contour field resolving into clarity | AI work becoming legible |
| Station | S-shaped river or path | Orient, inspect, establish state |
| Flow | Directional movement | Work progressing through a system |
| Surface | Defined, exposed region | A complete state made visible |
| Future products | Purpose-specific phenomenon | Must map to actual behavior |
Products share a grammar (contour-derived geometry, compatible line weights, controlled organic forms, environmental semantics, flat and monochrome viability, typography, and restrained material depth), not one recolored icon.
Brand hierarchy#
The company name is Kontour AI. Decided: the preferred lockup text endorses a product under the company name.
Station
by Kontour AI
Surface
by Kontour AI
OPEN-9 — Flow endorsement form and product tiering. Whether the endorsed product is presented as "Flow" or "Flow Agents", and how other products are tiered, is open.
Don't invent compound names that stack the company name, a product name, and "AI" or
"Platform" (e.g. "Kontour <Product> AI"); use the endorsement form above, or the approved one
once an open entry is decided.
Colors#
Color roles, not values. The values for each mode and theme are in the front matter.
| Token | Role |
|---|---|
--k-bg |
Page canvas. |
--k-panel |
Default container surface. |
--k-panel-raised |
Raised or inset surface inside a panel (controls, nested regions). |
--k-line, --k-line-strong |
Hairline separation; line-strong for control borders and emphasis. |
--k-text, --k-text-muted |
Primary and secondary text. |
--k-text-faint |
Tertiary text (hints, timestamps, counts). Meets WCAG AA for normal text on --k-bg, --k-panel, and --k-panel-raised in every theme and mode; npm run check:contrast rates all three. |
--k-brand, --k-brand-contrast |
The product identity slot, and its paired text color when a brand-filled surface is used. |
--k-action, --k-action-contrast |
Primary action fill and its text; an independently checked pair. |
--k-focus |
Keyboard focus color, checked against page and panel surfaces. |
--k-status-contrast |
Text on filled status tones, independent of the brand slot. |
--k-positive, --k-caution, --k-negative, --k-active, --k-neutral |
Shared status tones (see below). |
--k-*-soft |
Low-alpha tone fills behind status text; derived at runtime from the active tone. |
--k-focus-ring |
Compatibility alias of --k-focus; redeclared in scopes that change focus. New primitives read the focus role directly. |
--k-trust-<state> |
Trust-state ink: the label, glyph, and border of that state's chip (see Trust UX). One per state, never shared. |
--k-trust-<state>-fill |
The chip's own background, so its label pair does not depend on the surface it sits on. |
--k-trust-<state>-line |
The state's line style, a border-style keyword: the non-color cue for chip borders; charts translate it to a stroke (see Data Visualization). |
--k-basis-caveat-decoration |
The trust-basis caveat cue: a dashed underline, a text-decoration value that names no color, so it takes the muted text's color (see Trust basis). |
White-label overrides#
Override --k-brand to change identity accents such as eyebrows, the topbar edge, progress,
and the activity spinner. An override of that property alone does not change primary buttons,
checked controls, or focus because those read the interaction roles.
Where an override goes decides whether it applies:
- Set values per mode. One value cannot serve both modes, so an inline
styleon a single element is only suitable when the mode never changes. - Declare them with the same selectors the theme uses, in a stylesheet loaded after the
tokens: the
.theme-<theme>base block, and for light mode all three forms of its light block ([data-theme="light"].theme-<theme>,[data-theme="light"] .theme-<theme>:where(:not(...))and:where(.theme-<theme>) [data-theme="light"]:where(:not(...)), copying each:not(...)tail fromtokens/themes.css), and for a dark element below a light one the theme's dark-island block. A declaration on:rootloses to the theme's light block whenever a theme class is present, and an inline style on<html>does not reach a theme class placed on<body>. - With no theme class, override
:rootfor dark and[data-theme="light"]for light, and add the dark reset[data-theme="dark"]:where([data-theme="light"] *)beside:root: a dark element below a light one resets there, not to:root.
Migrating overrides. Dark islands now resolve dark (ui#80), so an override written with the earlier selectors can misplace values. Change each selector as follows:
Earlier selector Now .theme-<theme>(dark)keep it, and add the theme's dark-island block [data-theme="dark"]:where(.theme-<theme> *):where([data-theme="light"] *):where(:not(...)); without it a dark element below a light one shows the shipped product brand[data-theme="light"] .theme-<theme>[data-theme="light"] .theme-<theme>:where(:not(...)); without the tail a dark theme element on a light page takes the light override onto dark surfaces:where(.theme-<theme>) [data-theme="light"]:where(.theme-<theme>) [data-theme="light"]:where(:not(...)), so the override stops at a nested theme:root(no theme class)add [data-theme="dark"]:where([data-theme="light"] *)Each
:not(...)stands for a tail to copy fromtokens/themes.css;docs/consumer-guide.mdshows a full example.npm run check:designchecks every selector named in this section (except the earlier ones) against the shipped CSS, for every theme.
Rules for the values:
- Brand used as text (eyebrows, panel counts) must meet AA text contrast on the panel in each
mode. If a brand-filled surface carries text, set
--k-brand-contrastwith it and check that pair too. --k-actionand--k-action-contrastare a pair: override both, per mode, never the fill alone. The shipped action text is chosen for the shipped fill, and a new fill without its text can drop below AA.- Check
--k-focusagainst both the page and the panel (non-text contrast). - Status-filled surfaces use
--k-status-contrast; leave it unless the status tones change. - A runtime that applies a theme at run time (rather than a reviewed stylesheet) must reject a
pair that fails the thresholds
npm run check:contrastapplies to the roles: AA text contrast for text on a fill, and non-text contrast for the focus ring and checked controls. UsevalidateBrandOverridefrom@kontourai/ui/contrast, which applies those thresholds and the allowlist above; see "Validating a white-label theme" indocs/consumer-guide.md.
--k-focus-ring remains as an alias for existing consumer CSS; new CSS reads --k-focus.
Product color is identity, not status#
Product accents express which product you are in. They never encode state, success, risk, or confidence, and never form an implicit status scale in charts. Status uses the status tones.
The shared status scale maps product words to five tones: positive, caution, negative,
active, neutral (see docs/adr/0001-console-kit-token-contract.md). Never encode critical
state through color alone; status always carries text, and icon or color reinforces it.
OPEN-7 — status model. Decided (owner, 2026-10-01): five tones plus the nine trust states.
The status model is the five tones above and, separately, the trust states (Surface's claim
statuses, decided in OPEN-12) with their own tokens, TrustState, and k-trust-state (see
Trust UX). The draft's separate semantic tokens for success, warning, danger,
information, pending, and neutral are not adopted. A review of the token consumers (Station,
Surface, Survey, and Flow) found none that reads a status token outside the five tones
(#88). Product words map onto the five tones
through toneForValue or a product's own mapping: success is positive, warning and pending
are caution, error is negative.
A new tone needs a named consumer: a shipped product surface with a status that none of the five tones and none of the nine trust states can express, recorded in an issue before any token is added. Use a trust state, not a tone, for the status of a claim; never let two different trust states render identically.
Product themes#
One theme class on a stable root selects the product identity (docs/consumer-guide.md):
.theme-flow,.theme-surfaceset brand and interaction colors..theme-surveysets brand and interaction colors plus canvas and faint text, in each mode (its light canvas is the default light canvas)..theme-stationsets the draft accent as its base with mode-specific contrast adjustments..theme-consoleis a full skin: brand, interaction colors, surfaces, lines, text, status, radii, and its own font stacks.
OPEN-6 — brand palette and product accents. Decided (owner, 2026-10-01): the shipped values are the palette. No token changed with this decision. The draft's values are listed beside them as a record only:
| Role | Canonical (shipped) | Draft value, not adopted |
|---|---|---|
| Corporate ink | none; dark canvas --k-bg = #0a0e13 |
Ink #0B1220 |
| Corporate light ground | none; light canvas --k-bg (light) = #f5f4ef |
Stone #F2EFEA, White #FFFFFF |
| Station accent | .theme-station: --k-brand (station) = #966aff, --k-focus (station) = #9364ff, --k-action (station) = #7c3aed; --k-brand (station-light) = #7c3aed |
#7C3AED |
| Flow accent | .theme-flow: --k-brand (flow) = #3890ae, --k-action (flow) = #2f88a6; --k-brand (flow-light) = #1f6f88 |
#2563EB |
| Surface accent | --k-brand (surface) = #14a37a, --k-brand (surface-light) = #0f6b52 |
#14B8A6 |
| Default brand | --k-brand = #5ce0c6, --k-brand (light) = #0e7c64 |
not specified |
The draft's Ink, Stone, and Flow and Surface accent values are not adopted, so the palette
migration (#74) closes without a value change.
There is no separate palette layer: do not add --kontour-* custom properties. Dark-mode
colors are the shipped :root values. No color ramps ship; adding one would be a new decision.
Changing any value above is a design-system change: it needs its own decision and must keep
npm run check:contrast passing.
Station ships the owner's draft accent, adjusted only where contrast requires it. In dark mode,
the draft accent remains the action fill with white text; the focus ring uses a lighter
same-hue accent, and brand text a slightly lighter one again so it reads at 4.5:1 on the page,
panel, and raised panel. In light mode, the draft accent is brand, action, and focus.
npm run check:contrast rates every one of these pairs in both modes.
Flow dark, Console light, and Survey light follow the same rule. Brand used as text is rated at
4.5:1 on the page, panel, and raised panel in every theme and mode, by check:contrast and by
validateBrandOverride alike. Flow's dark brand is its accent lightened at the same OKLCH
chroma and within 0.3° of its hue until brand text reads at 4.5:1 on all three surfaces; the
action and focus keep the accent. Console's light brand is its lime darkened (hue within 0.1°)
for the same reason. It keeps white brand-contrast text, since dark text on the darker lime
would fall below 4.5:1, and the action and focus keep the brighter lime with dark text.
Survey's light brand is its accent darkened at the same hue and chroma just enough to read on
the light canvas; the action and focus keep the accent, and brand-contrast stays white.
OPEN-10 — Console and Survey themes. Decided with OPEN-6 (2026-10-01). The draft did not
cover theme-console or theme-survey. Their shipped values are canonical like every other
token value, so nothing about them waits on a palette migration.
OPEN-11 — marketing-site palette. Decided (owner, 2026-10-01): a documented exception, for
now. The marketing site (kontourai.io) keeps its own palette and is not required to use these
product tokens or themes. The exception covers that site only; product interfaces keep to the
--k-* tokens. Nothing here schedules a convergence: moving the site onto these tokens would be
a new decision.
Modes#
OPEN-5 — default mode. Decided (owner, 2026-10-01): dark is the default. :root carries the
dark contract (color-scheme: dark), and light is opt-in with [data-theme="light"]. The
draft's light Stone/Ink default is not adopted. Design every surface for both modes.
Choosing a mode:
Dark (the default). Set nothing: a page with no
data-themerenders dark.data-theme="dark"only has an effect below a light element, where it restores the dark contract.Light. Put
data-theme="light"on the root element (<html data-theme="light">), or on any element to make that subtree light.Follow the OS preference. The tokens ship no
prefers-color-schemerule, so a page that sets nothing stays dark whatever the OS prefers. To follow the OS, the page sets the attribute from the media query. Run this inline in<head>, before the first paint, so a light-preferring visitor does not see a dark flash:<script> (() => { const root = document.documentElement; const prefersLight = matchMedia("(prefers-color-scheme: light)"); const applyMode = () => { let stored = null; try { stored = localStorage.getItem("theme"); } catch {} const light = stored === "light" || (stored !== "dark" && prefersLight.matches); if (light) root.dataset.theme = "light"; else delete root.dataset.theme; }; applyMode(); prefersLight.addEventListener("change", applyMode); })(); </script>A stored choice wins over the OS preference: if the product has its own mode switch, it stores
"light"or"dark"(here under thethemekey; use your own) and the script applies that value, on load and when the OS preference changes. With nothing stored, the page follows the OS. A strict Content Security Policy blocks inline scripts, so give this one a nonce or load it as an external script placed first in<head>.
Dark mode is not inversion. Preserve hierarchy, restraint, product identity, contour
legibility, status semantics, and contrast. Light-mode status and brand hues are darkened so
text clears WCAG AA on light panels; keep that property when changing them
(npm run check:contrast). Fine contour artwork may need optical adjustment on dark surfaces.
The dark surface, text, and line values are the shipped :root values (decided with OPEN-6).
Theme and mode scoping#
Rule: the nearest data-theme picks the mode, and the nearest theme class picks the
product. Both are counted from the element itself outward, at any depth, and independently:
- An element with
data-themeresolves to its mode's contract plus its nearest theme's values for that mode. A theme further out contributes no mode-dependent tokens to it (its mode-independent ones, such as radii and fonts, still inherit). data-theme="dark"inside a light element restores the dark contract (surfaces, text, status, trust states, and roles) and its nearest theme's dark values. With no light element above it there is nothing to restore, and it inherits.- A theme class with no
data-themeof its own takes the nearest mode above it. Its values layer over what it inherits, in both modes:.theme-flowinside.theme-consolekeeps Console's surfaces and uses Flow's brand and roles. - Each theme's light block sets every mode-dependent token its base block sets, so light mode
never keeps a dark value, and sets nothing else: a token outside the mode reset (a radius, a
font) would carry its light value into a dark island below.
npm run check:contrastfails either way.
How it is built: plain selectors with :where(:not(...)) tails. The class-below-attribute
light form stops at a nearer data-theme="dark"; the attribute-below-class form stops at a
nearer (or its own) other theme class; dark islands have their own blocks that only match below
a light element. :where() keeps every selector's weight as it was, so a white-label override
written later with the same selectors still wins in every documented placement. Browser support
is that of :where() and complex selectors inside :not(): Chromium 88, Firefox 84, and Safari
14 or later.
@scope was considered and not used: a scoped rule outranks an unscoped rule of the same
weight, so shipped scoped rules would beat existing white-label overrides written with the
documented selectors, and every value would be declared twice (a scoped copy beside the
fallback for browsers without @scope).
Not covered: the theme class of an element below a mode that switches back (light, then dark,
then light again above it) matches as if the inner light were absent. Put data-theme on that
theme element. The same holds for a theme nested inside itself through another theme.
Typography#
Three family roles:
| Token | Role | Draft role name |
|---|---|---|
--k-font-display |
Display and titles: page headings, dialog titles. | font-brand |
--k-font-ui |
Interface text: controls, buttons, body. | font-ui |
--k-font-mono |
Identifiers, labels, badges, metadata, numerals in dense data. | font-mono |
Typography is precise, contemporary, calm, intelligent, slightly human, and highly legible. Avoid sci-fi, techno, novelty-map, excessively geometric, and generic rounded SaaS faces. Identifiers and technical values use the mono role and must wrap and copy gracefully.
OPEN-4 — typefaces. Decided (owner, 2026-10-01): keep the shipped families. Fraunces
(display), Hanken Grotesk (UI), and IBM Plex Mono (mono), self-hosted in tokens/fonts/;
.theme-console swaps display and UI for a condensed stack. The draft asked for a humanist or
restrained neo-grotesk with open counters, strong numerals, excellent UI rendering, useful
weights, and enough character to avoid sameness, and left the family undecided; this decision
closes that search. Generated concept typography is not a production font and not a wordmark;
the wordmark is a separate decision (see Wordmark).
OPEN-3 — type scale. Decided (owner, 2026-10-01): the shipped scale is canonical.
Shipped steps run from --k-text-xs = 11px to
--k-text-2xl = clamp(26px, 3.4vw, 38px); the top step is fluid, and the front matter records
its upper bound. The draft's named levels are not adopted and are kept as a record only (do not
create these tokens):
| Draft level (not adopted) | Size / line | Use |
|---|---|---|
| display-xl | 64 / 68 | major hero |
| display-lg | 48 / 52 | page hero |
| heading-xl | 36 / 42 | page heading |
| heading-lg | 28 / 34 | section heading |
| heading-md | 22 / 28 | panel heading |
| body-lg | 18 / 28 | emphasized prose |
| body-md | 16 / 24 | default body |
| body-sm | 14 / 20 | dense UI |
| label | 13 / 18 | controls, metadata |
| caption | 12 / 16 | secondary metadata |
Note that shipped control text is --k-text-md = 14px, the size of the draft's body-sm.
Shipped styling sets text-transform: uppercase on buttons, badges, status labels, field
labels, metric labels (.metric span), top-bar meta labels (.topbar-meta-item span),
eyebrows, panel headings, and titles (.topbar h1, .dialog__title). That is current
implementation, not a rule from the draft; keep it consistent until a typography decision
changes it.
Wordmark#
Primary naming: Kontour AI. Generated concept typography is not a production font or a custom wordmark.
Decided (owner, 2026-10-05) — wordmark. The wordmark spells "Kontour", not "Kontour AI".
That decides the word on the wordmark only: the company name and the endorsement text in
Brand hierarchy ("<Product> by Kontour AI") are unchanged.
Adopted with it, as proposed in #114. The cut
of icons/kontour-wordmark.svg, outlined from Fraunces at weight 600 and optical size 144 (see
Production assets), is in force. It was not separately put to the owner,
so changing it does not reverse an owner decision.
Layout#
Use a 4px base rhythm, and only the spacing tokens.
OPEN-1 — spacing scale. Decided (owner, 2026-10-01): the shipped six steps are canonical. The draft numbered its steps differently and is not adopted. Danger when reading older layouts: the same step numbers mean different sizes.
Canonical (shipped) Draft, not adopted --k-space-1=4pxspace-1: 4px--k-space-2=8pxspace-2: 8px--k-space-3=12pxspace-3: 12px--k-space-4=16pxspace-4: 16px--k-space-5=24pxspace-5: 20px--k-space-6=32pxspace-6: 24pxnot shipped space-8: 32pxnot shipped space-10: 40pxnot shipped space-12: 48pxnot shipped space-16: 64pxnot shipped space-20: 80pxnot shipped space-24: 96pxA layout written against the draft's
space-5orspace-6renders larger than intended on the shipped tokens. Always read the step's value from the front matter, never from its number. Do not add--space-*properties. Any renumbering is a breaking change for every consumer.
Layout rules:
- Prioritize outcome and status, reveal evidence progressively, keep provenance accessible.
- Responsive layouts preserve hierarchy rather than merely stacking desktop panels.
- Technical identifiers wrap and copy gracefully.
- Group with cards and panels only when the grouping means something; avoid card soup.
- Final breakpoints are OPEN.
Agents may extend spacing and components responsively when consistent with the tokens and existing patterns.
Elevation & Depth#
Depth comes from tonal layers first: --k-bg → --k-panel → --k-panel-raised, separated by
--k-line. Prefer restrained borders and surface shifts to heavy shadows.
--k-shadowis the soft panel shadow.--k-elevation-overlayis the single layered shadow for floating surfaces (dialogs, menus, palettes, toasts), so elevation tunes in one place.--k-z-*is the stacking scale; overlays read it instead of inventing local z-index values.
Shadow and stacking values are in tokens/tokens.css and listed in the front matter comment.
Shapes#
Do not round everything. Shipped radius roles:
| Token | Role |
|---|---|
--k-radius-control |
Tight radius for dense controls (inputs, selects). |
--k-radius-overlay |
Dialogs, popovers, and other floating surfaces. |
--k-radius-sm |
Buttons and badges. |
--k-radius-md |
Framed placeholder cards (.empty--prominent, .empty--compact). |
Shipped .panel sets no radius: panels are square-cornered, so do not assume containers are
rounded. .theme-console sets --k-radius-sm (console) = 0, --k-radius-md (console) = 0,
--k-radius-control (console) = 0, and --k-radius-overlay (console) = 0.
OPEN-2 — radius scale. Decided (owner, 2026-10-01): the shipped radii are canonical.
They are --k-radius-sm = 9px, --k-radius-md = 14px,
--k-radius-control = 4px, --k-radius-overlay = 10px.
The draft's 6 / 10 / 16 / 24px as radius-sm / md / lg / xl is not adopted.
The names overlap and the values do not; do not add --radius-* properties.
Components#
The shipped primitives (@kontourai/ui/react, @kontourai/ui/elements) implement these rules;
docs/explorer-manifest.json lists each one's states and token contract.
- Buttons. Clear primary, secondary, tertiary, and destructive hierarchy. Mapped to the
shipped variants: primary →
primary; secondary →ghost; tertiary → no shipped variant (OPEN); destructive → no defined pattern (OPEN;negativeis the nearest shipped variant, a status-toned button).positiveandcautionare status-toned buttons outside the draft's hierarchy. The product accent is not automatically the primary action color:.btn-primaryreads--k-actionand--k-action-contrast, while focus reads--k-focus. Existing themes keep their familiar fills except the Console light action text, which uses a dark contrast role to meet AA. Future role values can differ from the brand independently. - Inputs (
Field,Input,Select,Textarea,Checkbox,Toggle). Visible labels; placeholders are not labels. Validation explains recovery. - Cards and panels (
Panel). Only for meaningful grouping. - Tables. Optimize scanning, alignment, sorting and filtering, copyable values, and explicit statuses. (No shipped primitive yet.)
- Inspectors. Preserve context while exposing deeper evidence and provenance. (No shipped primitive yet.)
- Dialogs (
Dialog). Reserve interruption for actions that need focus or confirmation. - Status (
Badge,StatusBadge,StatusBar). Always include text; icon and color reinforce it. - Trust state (
TrustState,k-trust-state). A chip naming one of Surface's nine claim statuses, with an optional visible detail that says what was checked. See Trust UX. - Trust basis (
TrustBasis,k-trust-basis). The muted line after a trust-state chip that says how the status was established, rendered from Surface'sclaimBasisView. See Trust basis. - Loading (
Skeleton,Spinner,Progress). Communicate useful state without fake precision. Never imply completion while work is pending. - Empty states (
Empty). Explain purpose and the next action. - Errors. State the failure, its impact, recoverability, and the next action; reveal technical detail progressively.
- Iconography. UI icons use simple geometry, consistent optical weight, thin-to-medium strokes, and excellent small-size legibility. Do not turn utility icons into contour art. Avoid humanoid robots, glowing brains, floating code, and cyber grids.
Do's and Don'ts#
Token rules:
- Do style with
--k-*tokens; don't hard-code hex values, font sizes, radii, or spacing. - Don't introduce the draft's
--kontour-*,--space-*, or--radius-*names as CSS. - Don't assume a step number means the same size in the draft and the shipped scale (OPEN-1).
- Don't use product accents as status, and don't use status tones as identity.
- Don't hand-edit this file's front matter; regenerate it.
Agents MUST preserve:
- the contour and environment metaphor
- the corporate/product distinction
- Station's ownership of the S-river
- each product's unique phenomenon
- the semantic separation of product color and status color
- uncertainty → clarity
- progressive trust inspection
- flat and monochrome viability
- accessibility
- canonical asset authority
Agents MUST NOT:
- put Station's S-river in the company logo
- add a compass to every product
- make every product a recolored contour blob
- introduce generic AI motifs
- use gradients as a substitute for structure
- assume concept typography is final
- redraw canonical SVGs without an explicit identity task
- invent unresolved brand decisions, or resolve an OPEN item silently
- hide uncertainty for visual cleanliness
- reduce provenance to decorative metadata
Agents MAY extend spacing and components responsively when consistent with the tokens and existing patterns. Changes to master marks, the core palette, product semantics, typeface families, or trust-state semantics are design-system changes and need an explicit decision.
Marks & Identity#
Corporate mark#
The Kontour symbol is a contour field moving from uncertain → resolved: one area continuous and legible, another interrupted, sparse, dotted, or incomplete. Use roughly 4–6 nested paths from related geometry. Preserve deliberate spacing, small-size legibility, controlled organic variation, and a distinctive silhouette. Avoid perfect concentricity and decorative noise.
Never add Station's S-river, a compass rose, a literal mountain, a location pin, a checkmark, shield, lock, brain, neural nodes, circuitry, AI sparkles, a robot, a chat bubble, a generic hexagon, or an infinity loop to the corporate mark.
Decided (owner, 2026-10-05) — corporate symbol. The owner kept the proposed symbol,
icons/kontour-symbol.svg, as shipped: three nested circular contours whose centres step down
and to the left, the outer one continuous around the lower left and broken into two dashes and
a dot at the upper right, on the product marks' 24-unit frame and stroke weight.
It departs from the direction above in three ways. They were listed in the proposal (#114) and are accepted with the symbol rather than open:
- it has three nested contours, not 4–6; the dashes and dot are pieces of the outer contour, not further contours
- every contour is a perfect circular arc, so there is no organic variation; only the offset centres keep it from being concentric
- at 16px the contours close up and the broken section stops reading; it reads from 24px, which is why the usage rules prefer 24px or more for it
The direction above still guides other contour artwork; it does not reopen the symbol.
Station#
A river or path forming a subtle S through a contour environment; the S emerges from the landscape rather than appearing as a letter. Station-specific: never the corporate mark. Station communicates orientation, location, arrival, state, inspection, checkpoint, reference, and reliability. A compass or reference marker may stay Station-specific if simplified, subordinate, and semantically useful. Do not propagate it to every product. Release channels (stable, beta, dev, nightly) may use controlled accent variants that keep the geometry; the channel colors are OPEN.
Flow#
Progression, routing, transfer, orchestration, transformation: work moving through a system. Use contour-derived directional behavior. A subtle F may emerge naturally; never force a literal F. Flow must stay distinct from Station at app-icon size.
Surface#
A state, region, layer, or plane made visible: exposed plane, plateau, basin, bounded region, section, or revealed layer. See the state clearly. Surface is not another generic contour blob; the surface itself is the defining phenomenon.
Future products#
Start with: what phenomenon in the Kontour world represents what this product actually does? Territories such as ridge, field, basin, trace, range, layer, boundary, current, crossing, or vale are concepts, not reserved names. Before approving an identity, document its function, phenomenon, semantic mapping, differentiation, shared grammar, 24px behavior, and monochrome behavior.
Flat vs. dimensional#
Every core mark needs a flat version for navigation, favicons, docs, badges, CLI, and small UI. Dimensional or cartographic treatment (shallow elevation, subtle shadow, terrain or paper material, inset paths, restrained depth) is allowed for app icons, launch screens, marketing, product cards, and presentations. Avoid photorealism. If removing gradients, texture, shadow, embossing, and color destroys the identity, the mark is unfinished.
Contour artwork belongs in identity, hero moments, explanatory diagrams, meaningful empty states, and selective launch or loading moments, not behind every card.
Production assets#
The marks ship in icons/, each as an SVG file (@kontourai/ui/icons/<name>.svg) and as a
component. Every shipped mark is in force; what the owner decided and what was adopted with it
differ:
| Marks | Status |
|---|---|
| The 22 product icons | Decided (owner, 2026-10-01; #75): promoted to production marks as drawn, having passed the gates below. |
kontour-symbol.svg |
Decided (owner, 2026-10-05), as shipped. See Corporate mark. |
kontour-wordmark.svg |
The word "Kontour": decided (owner, 2026-10-05). The cut: adopted with it, as proposed. See Wordmark. |
kontour-lockup-horizontal.svg |
Adopted with the decided marks, as proposed: symbol, then wordmark, at the shipped gap. Not separately decided. Composed from the other two, never drawn separately. |
The product marks are flat UI icons. They do not implement the contour-derived directions above for Station, Flow, and Surface, and redrawing toward those directions stays open. The owner decided (2026-10-05) that no such redraw is published until the owner has seen it rendered at real sizes (the contact sheet below, at 16, 24, and 32px). A stacked lockup and per-product lockups do not exist.
Components: ProductIcon and <k-product-icon> for product marks; BrandMark
(KontourSymbol, KontourWordmark, KontourLockup) and <k-brand-mark> for the corporate
marks. The path modules under react/src/ are the source; node scripts/check-marks.mjs --write regenerates the files. The wordmark is outlines, so drawing it needs no font;
scripts/generate-wordmark.py --check re-derives it from the vendored Fraunces file. Raster
exports derive from the SVGs; never use generated concept images as production logo sources,
and never trace one.
Accessible name. The wordmark and the lockup are exposed to assistive tech as an image
named "Kontour" by default, so a header that shows only the lockup still announces the company.
Pass title to rename one, or decorative to hide it where visible text beside it already
says the name. The symbol and the product marks have no default name: they are hidden unless
given a title.
Gates#
Enforced for every mark by npm run check:marks and the browser suite. A mark that fails one is
fixed or not shipped.
- a valid, self-contained SVG with a
viewBox: only shape elements, and only plain numeric geometry inside the frame; no scripts, raster embeds, external references, styles, or live text currentColoras the only paint; no filters, gradients, masks, or opacity- at 16, 24, and 32px, rendered alone: enough ink that still holds 3:1 after antialiasing on the page and panel surfaces in light and dark; not more ink than a drawing has; ink and empty space both present, with ink in the middle of the frame
- no two marks render alike
- shape-count and file-size ceilings
- for the square marks: renders standalone, square, at 16 and 32px, as a favicon does
What the gates do not establish. They measure ink: how much, how dark against the surface, and how it is distributed. They do not measure whether anyone can tell what a mark is. Compared with the requirements this section carried before:
- "~24px recognition" is not automated. The ink measurements do not stand in for it; they
catch blank, hairline, washed-out, frame-only, and duplicated marks. Recognition stays a human review
requirement: before a mark is approved, a person looks at it at 16, 24, and 32px on the
contact sheet (
node scripts/render-marks-sheet.mjs <out.png>) and confirms it reads as what it is and is not mistaken for a neighbouring mark. - The app-icon test is not covered. No gate renders a mark as an app icon (masked, padded, on a filled tile); that needs its own review when an app icon is made.
- The favicon gate proves the file renders alone at favicon sizes. It does not prove it reads in a real tab strip.
Usage#
Adopted with the decided corporate marks, as proposed in #114. These rules are in force, but they were not separately put to the owner, so changing one does not reverse an owner decision.
- Color. One color, from the surrounding text color. Use a text or brand role that holds 3:1 on the surface behind it. Never color parts of a mark differently.
- Clear space. Keep a margin of one quarter of the mark's rendered height on every side, free of text, other marks, and container edges.
- Minimum size. 16px high for every mark. Prefer 24px or more for the symbol, the wordmark, and the lockup.
- Which mark. The lockup where the company is introduced; the symbol where space is tight or the name is already present; the wordmark where a symbol would repeat. Product marks identify products and never stand in for the corporate mark.
- Favicons. The square marks render as SVG favicons. A standalone SVG has no text color to
inherit, so
currentColordraws black: for a dark tab strip, ship a copy with the color set, generated from the canonical file.
Don't:
- stretch, rotate, skew, outline, or crop a mark
- add a gradient, shadow, glow, texture, or container shape to a flat mark
- retype the wordmark as live text, in Fraunces or any other face
- pair the symbol and wordmark in an arrangement other than the shipped lockup
- place a mark on a photo or a surface where it falls under 3:1
- redraw a mark outside an explicit identity task
Motion#
Shipped tokens: --k-ease and --k-dur (values in the front matter comment). Exact durations
and easing beyond these are OPEN.
Preferred metaphors:
- Resolve: broken contours become continuous.
- Trace: a path progressively reveals.
- Focus: secondary layers recede.
- Traverse: work moves through a defined path.
- Layer: deeper evidence becomes visible.
Avoid random particles, AI glows, constant ambient animation, gratuitous morphing, and motion
that implies completion while work is pending. Respect prefers-reduced-motion.
Interaction Model#
Complex underneath. Clear on the surface.
Progressive inspection: Outcome → Status → Explanation → Evidence → Provenance → Raw execution. Outcome answers what happened. Status states what Kontour can establish. Explanation says why. Evidence supports it. Provenance shows origins and transformations. Raw execution exposes model, tool, and event detail.
These are contours of detail. Users move between them without losing context.
Trust UX#
Prefer evidence over assertion. Do not merely say Verified when the product can explain what was checked, against what, by what mechanism, and with what limitations. Prefer "12 cited records matched the extracted values" over "Fully trustworthy."
Where relevant, expose source, actor, model, tool, timestamp, transformation, validation step, human approval, artifact or result, version, and lineage.
Distinguish the trust states: unknown, proposed, assumed, verified, stale, disputed, superseded, rejected, and revoked. Never collapse these into one generic confidence state.
A claim is shown on two axes, and neither stands in for the other:
- Status: what state the claim is in. The trust-state chip, one of Surface's claim statuses (see Trust states).
- Basis: how that status was established. The trust-basis line after the chip: the mechanism, how much evidence backs it, and its caveats (see Trust basis).
Only evidence linked to the claim bears on it: evidence that supports or cites the claim, or counts against it. Most tool calls in an agent session are exploration, retries, or corrected mistakes; the execution trail is not evidence. A tool call or its result that is not linked to the claim never changes the status or the basis line, whether it succeeded, failed, or was retried. It stays inspectable at the raw-execution layer. A declared check that is linked to the claim is evidence, so its failure does count.
Trust states#
OPEN-12 — trust-state vocabulary. Decided (owner, 2026-09-27): Surface's vocabulary. The
trust states are Surface's claim statuses (TRUST_STATUSES, the same set the open trust format
derives), in Surface's order, and their default labels are Surface's display names. Kontour UI
does not redefine them. An earlier draft list (known, inferred, estimated, uncertain,
conflicting, failed, unavailable, not checked) described how a value was produced; it is not a
set of component states. @kontourai/surface is an exact-pinned development dependency, used
only by npm run check:surface-parity, which fails when the states, their order, or their
default labels differ from Surface's; bumping it is how a Surface vocabulary change reaches this
package. Nothing shipped imports Surface (npm run check:pack enforces it).
Shipped: tokens in tokens/tokens.css, the TrustState React primitive, the
k-trust-state element, and, for renderers that build HTML strings,
renderTrustStateHtml from @kontourai/ui/trust-state. All three render from one
framework-free module, and the string renderer's output is tested against the element's
markup. A string consumer includes the tokens and @kontourai/ui/trust-state.css, the chip
rules alone, generated from react/styles.css. Each state has its own ink and fill in both modes, resolved the same in
every product theme, and never reads a product accent, the action role, or the focus role
(npm run check:tokens). npm run check:contrast rates every label on its fill and every
border against the panel, in every theme and mode, and fails when two states' inks are too
close to tell apart.
Meaning never rests on color. Every chip always shows a text label, an SVG glyph whose shape is unique to the state, and a line style that groups states:
| State | Default label | Glyph | Line | Surface meaning |
|---|---|---|---|---|
| unknown | No evidence | empty circle | dotted | No evidence has been recorded. |
| proposed | Pending review | clock | dashed | Asserted, not yet reviewed or verified. |
| assumed | Assumed | wave | dashed | Treated as true without evidence. |
| verified | Verified | check mark | solid | Current evidence supports the claim under its policy. |
| stale | Needs refresh | refresh arrow | dotted | Was verified; the verification aged out or the subject changed. |
| disputed | Disputed | not equal | double | Producers or reviewers disagree. |
| superseded | Superseded | arrow to a bar | dotted | A newer claim replaces this one. |
| rejected | Rejected | cross | solid | Checked and found not to hold. |
| revoked | Revoked | circle with slash | dotted | The producer withdrew the claim. |
Solid means a current, definite outcome; dashed means asserted but not established; dotted means no current support (nothing recorded, expired, replaced, or withdrawn); double means contested. Assumed is never styled like verified: it has no evidence behind it.
A product may pass its own label; an empty one falls back to the default, and an override keeps the default label as visually hidden text so assistive technology still hears the status. The detail slot is where evidence goes: prefer a chip with a detail such as "12 source records matched" to a bare chip. An input the component does not recognize renders as its own text with no state styling or glyph; it is never coerced into a state.
Trust basis#
Decided (owner, 2026-09-27; ui#87). The basis is a plain text line after the status chip, never a second chip:
[VERIFIED] 1 cited only · Extracted from a source · 2 entail the claim
[PENDING REVIEW] Model-derived · 1 not evaluated · Extracted from a source
[NO EVIDENCE] Basis not recorded
Surface owns the summary. claimBasisView(claim, evidence) from @kontourai/surface/display
chooses the facets, their order, their words, which ones are caveats, and the wording of every
missing state. TrustBasis and k-trust-basis render that view as given: they never add, drop,
reorder, relabel, or truncate a facet, and they do not import Surface. Kontour UI keeps a
structural copy of the TrustBasisView type; npm run check:surface-parity compares it with
Surface's in both directions and checks that the views the tests and gallery render are what
Surface produces for their inputs.
The rules, applied by Surface and rendered here:
- At most three facets on the line, caveats first, and a caveat is never dropped. The caveats are Model-derived, not evaluated, cited only, contradicts the claim, and failed checks. When there are more than three caveats, the line shows every caveat and nothing else. After the caveats come the method, then support counts, then review.
- Caveats get a dashed underline, never a color.
--k-basis-caveat-decorationis a line style in the text's own color. The whole line is muted text (--k-text-muted), so it never outranks the status. There is no tone prop, and basis rules read no brand, action, focus, status-tone, or trust-state token (npm run check:tokens). - Never blank. A claim with nothing to summarize shows a labelled missing state: Basis not recorded, Basis restricted (not visible to this viewer), Basis unavailable (the read failed), or Basis not available (the host cannot or should not say which). Input the component cannot use renders Basis not available and warns in the console. Validation is all or nothing: one malformed facet or detail row makes the whole view unusable, because rendering the rest could silently drop a caveat.
- Producer ratings stay in the inspector. A producer's own evidence-strength rating and a calibrated confidence appear only as inspector rows labelled as producer-supplied, never on the line.
- Voice. Write "Model-derived", never "AI thinks" or "AI inferred". "Verified" belongs to the status only; basis copy avoids "Confirmed", "Trusted", and "Certain".
Two densities. Inline is the line alone. Inspector shows the same line and adds a
definition list below it with one labelled row per detail Surface reports (How, Support,
Results, Derived, Review, Producer rating, Calibrated confidence, Sources). The inspector keeps
the line (decided, 2026-09-27) because the rows carry no caveat marking: without the line, the
caveats would lose their underline and their caveats-first position. Both densities render a
visually hidden "Basis:" prefix, and the visible · separators are hidden from assistive
technology, so a screen reader hears "Basis: 1 cited only, Extracted from a source, 2 entail the
claim". Each facet carries data-field, data-code, and data-caveat for hosts and tests.
Caveats are announced by their wording and their position, not by the underline. The underline is a visual cue only; the words themselves ("Model-derived", "1 not evaluated", "1 cited only", "1 contradicts the claim") already say that they limit the status, and caveats always come first. Kontour UI does not add its own spoken copy such as "caveat:", since that would be basis wording outside Surface. If an explicit announcement is wanted, Surface owns it, in the view's labels.
Human review is first-class provenance. Failures state what failed, the impact, recoverability, retained partial work, and the next action.
AI & Agent Activity#
Do not anthropomorphize routine system behavior. Prefer "Model returned…", "Tool call failed…", "Kontour verified…", and "3 sources support this value…" over "The AI thought…".
Distinguish planned, running, completed, failed, skipped, retried, human-approved, and superseded events. Raw traces are not the default explanation: summarize first, inspect downward.
Data Visualization#
Charts prioritize comparison and interpretation over decoration, and every chart answers a
user question. Never use product identity colors as an implicit status scale. Represent
uncertainty and completeness with multiple cues (labels, line style, shape, opacity, pattern,
and color). A series that carries a trust state reuses that state's ink (--k-trust-<state>)
and the line style its --k-trust-<state>-line token names. That token is a CSS border-style
keyword, not a stroke value, so a chart translates it:
| Line keyword | SVG stroke |
|---|---|
| solid | no dash array |
| dashed | a dash pattern, such as stroke-dasharray: 4 3 |
| dotted | round caps on zero-length dashes, relative to the stroke width: a dash of 0 and a gap of about twice the width (stroke-dasharray: 0 3 for a 1.5px stroke), with stroke-linecap: round, so each dot is as wide as the stroke |
| double | no stroke equivalent: draw two parallel strokes, or use the state's glyph as the marker |
Broken/continuous contour logic is allowed only when semantically appropriate. Provide textual equivalents where needed.
Voice & Writing#
Kontour sounds grounded, clear, precise, human, and optimistic. Avoid hype, fear, grand claims, excessive jargon, vague intelligence claims, and unsupported certainty. Specificity builds credibility.
- Prefer "See how this result was produced" over "Unlock unprecedented AI transparency."
- Prefer "Verified against 12 source records" over "AI you can finally trust."
Accessibility#
Accessibility is part of trust. Require keyboard access, visible focus (--k-focus-ring; buttons
draw it as an outline offset onto the surrounding surface, so it never depends on the button's fill),
semantic markup, accessible names, sufficient contrast (npm run check:contrast enforces WCAG
AA on the shipped pairs), reduced-motion support, no critical color-only encoding, usable zoom
and reflow, appropriate target sizes, screen-reader-readable status changes where needed,
meaningful table headers, and textual equivalents for charts.
Design QA Checklist#
Before calling a UI or brand task complete, verify:
- Does it make the user's primary outcome clear?
- Can a user tell what Kontour knows from what it infers?
- Is evidence accessible at the appropriate depth?
- Is provenance preserved?
- Are errors and uncertainty explicit?
- Does product identity stay distinct from status semantics?
- Does Station keep its S-river ownership?
- Are contours meaningful rather than decorative noise?
- Are canonical assets reused rather than approximated?
- Does it work in light and dark where required?
- Is important meaning available without color?
- Is keyboard and focus behavior correct?
- Is the hierarchy usable on narrow screens?
- Are technical values copyable where useful?
- Is the copy specific rather than hype-driven?
- Does it use
--k-*tokens only, and did it resolve no OPEN item by accident? - Did the implementation introduce a new design-system rule accidentally?
- Does the result feel calm, precise, and inspectable?
Canonical Direction#
Intentional, not up for casual change: the Kontour/contour naming relationship; environmental product architecture; the uncertainty → clarity metaphor; the corporate contour-field concept; Station's S-river; product-specific environmental phenomena; a restrained corporate palette; distinct product accents; "Trust the work AI does."; separation of corporate and product iconography; the progressive "contours of detail" interaction model; evidence and provenance as first-class trust UX.
Open Decisions#
Do not silently resolve these. When one is decided, update this file and the canonical assets and tokens together, in the same change. A decided row stays in the register as the record of the decision.
Draft vs shipped tokens#
| ID | Item | Status | Shipped | Draft direction | Tracking |
|---|---|---|---|---|---|
| OPEN-1 | Spacing scale | Decided (owner, 2026-10-01): shipped scale is canonical | --k-space-5 = 24px, --k-space-6 = 32px |
not adopted: 20px / 24px at steps 5 / 6; steps up to 96px | #74 |
| OPEN-2 | Radius scale | Decided (owner, 2026-10-01): shipped radii are canonical | --k-radius-sm = 9px, --k-radius-md = 14px, --k-radius-control = 4px, --k-radius-overlay = 10px |
not adopted: 6 / 10 / 16 / 24px | #74 |
| OPEN-3 | Type scale | Decided (owner, 2026-10-01): shipped scale is canonical | --k-text-xs = 11px to --k-text-2xl = clamp(26px, 3.4vw, 38px) |
not adopted: ten named levels, 12px to 64px | #74 |
| OPEN-4 | Typefaces | Decided (owner, 2026-10-01): keep the shipped families | Fraunces / Hanken Grotesk / IBM Plex Mono | humanist or restrained neo-grotesk, family undecided; closed by this decision | — |
| OPEN-5 | Default mode | Decided (owner, 2026-10-01): dark is the default | dark default, [data-theme="light"] opt-in; following the OS preference is the consumer's script (see Modes) |
not adopted: light Stone/Ink default | — |
| OPEN-6 | Palette and product accents | Decided (owner, 2026-10-01): shipped values are the palette | per-theme --k-brand; .theme-station ships with a contrast-adjusted dark brand |
not adopted: Ink, Stone, the draft's Flow and Surface accents | #74 |
| OPEN-7 | Status model | Decided (owner, 2026-10-01): five tones plus the nine trust states; a new tone needs a named consumer | five tones; the nine trust states (Surface's statuses) as separate tokens and a primitive | not adopted: semantic status tokens beyond the five tones | #88 |
| OPEN-8 | Primary action and focus | Resolved by #79 | --k-action and --k-focus roles, independent of the brand slot |
accent is not automatically the action color | #72 |
| OPEN-9 | Flow endorsement form and product tiering | OPEN | Station and Surface: "<Product> by Kontour AI" (decided) |
Flow: "Flow" or "Flow Agents"; tiering of other products | — |
| OPEN-10 | Console and Survey themes | Decided with OPEN-6 (2026-10-01): shipped values are canonical | shipped | not covered by the draft | — |
| OPEN-11 | Marketing-site palette relationship | Decided (owner, 2026-10-01): the marketing site keeps its own palette, as a documented exception for now | — | — | — |
| OPEN-12 | Trust-state vocabulary | Resolved (owner, 2026-09-27) | Surface's claim statuses and display names, checked against @kontourai/surface |
— | #73 |
Brand decisions still open#
Contour spacing and line weights; contour-derived Station, Flow, and Surface vectors (the shipped product marks are flat UI icons, #75; none is published before the owner sees it at real sizes); stacked and per-product lockups; app-icon treatments; complete typography tuning; environment and release-channel colors; exact motion durations and easing; final breakpoints; final component-library mappings.
Final Principle#
The visual identity and the product experience express the same idea:
Make invisible structure visible.
Kontour is not trustworthy because it looks secure. It earns trust by making AI work understandable, attributable, inspectable, and appropriately uncertain.
The contour is not decoration. It is the model for the product.