This commit is contained in:
Rainer Leit
2026-09-25 17:02:24 +03:00
parent cc43ed8dc8
commit 9597629951
2149 changed files with 460234 additions and 1770 deletions
+486
View File
@@ -0,0 +1,486 @@
---
name: terrain studio
description: A dark instrument panel for painting a planet's geology and reading back the ground it makes.
colors:
hot: "#d6a03a"
hot-ink: "#17181a"
ok: "#6fbf73"
ok-ink: "#bfe6c1"
warn: "#b4553f"
warn-ink: "#f0b4a4"
void: "#0b0e11"
well: "#11151a"
bg: "#14181d"
panel: "#1b2027"
row-hover: "#222831"
row-live: "#252b34"
raised: "#262d36"
line: "#2b323b"
ink: "#d8dee6"
dim: "#8b95a1"
scrim: "#0d1014ee"
scrim-hud: "#0d1014dd"
chip-edge: "#0006"
typography:
body:
fontFamily: "ui-monospace, \"Cascadia Mono\", Menlo, Consolas, monospace"
fontSize: "13px"
fontWeight: 400
lineHeight: 1.45
letterSpacing: "normal"
data:
fontFamily: "ui-monospace, \"Cascadia Mono\", Menlo, Consolas, monospace"
fontSize: "11.5px"
fontWeight: 400
lineHeight: 1.45
letterSpacing: "normal"
note:
fontFamily: "ui-monospace, \"Cascadia Mono\", Menlo, Consolas, monospace"
fontSize: "11px"
fontWeight: 400
lineHeight: 1.45
letterSpacing: "normal"
label:
fontFamily: "ui-monospace, \"Cascadia Mono\", Menlo, Consolas, monospace"
fontSize: "11px"
fontWeight: 600
lineHeight: 1.45
letterSpacing: "0.09em"
rounded:
sm: "2px"
md: "3px"
lg: "4px"
spacing:
xs: "4px"
sm: "6px"
md: "8px"
lg: "10px"
xl: "16px"
components:
button:
backgroundColor: "{colors.raised}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: "6px 10px"
button-hover:
backgroundColor: "{colors.raised}"
textColor: "{colors.ink}"
button-disabled:
backgroundColor: "{colors.raised}"
textColor: "{colors.dim}"
button-primary:
backgroundColor: "{colors.hot}"
textColor: "{colors.hot-ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: "6px 10px"
button-warn:
backgroundColor: "{colors.raised}"
textColor: "{colors.warn-ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: "6px 10px"
button-toggle-on:
backgroundColor: "{colors.raised}"
textColor: "{colors.hot}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: "6px 10px"
swatch-row:
backgroundColor: "transparent"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: "4px 6px"
swatch-row-hover:
backgroundColor: "{colors.row-hover}"
textColor: "{colors.ink}"
swatch-row-selected:
backgroundColor: "{colors.row-live}"
textColor: "{colors.ink}"
chip:
rounded: "{rounded.md}"
size: "18px"
chip-key:
rounded: "{rounded.md}"
size: "12px"
input-number:
backgroundColor: "{colors.well}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.md}"
padding: "2px 5px"
width: "78px"
toast:
backgroundColor: "{colors.scrim}"
textColor: "{colors.ink}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: "6px 9px"
toast-error:
backgroundColor: "{colors.scrim}"
textColor: "{colors.warn-ink}"
toast-ok:
backgroundColor: "{colors.scrim}"
textColor: "{colors.ok-ink}"
hud:
backgroundColor: "{colors.scrim-hud}"
textColor: "{colors.dim}"
typography: "{typography.body}"
rounded: "{rounded.lg}"
padding: "6px 9px"
progress-track:
backgroundColor: "{colors.well}"
rounded: "{rounded.md}"
height: "6px"
progress-fill:
backgroundColor: "{colors.hot}"
height: "6px"
ramp:
rounded: "{rounded.sm}"
height: "12px"
---
# Design System: terrain studio
## Overview
**Creative North Star: "The Instrument Panel"**
terrain studio is a gauge with a world in it. The left rail is calibrated measurement — a rate typed in mm/yr
comes back as the hillslope angle it buys, a brush size comes back as metres on the ground, a bake comes back as
minutes — and the rest of the window is the specimen those numbers describe. Everything in the chrome is either a
reading, a control, or the hairline that separates the two. There is no third category, and nothing in the frame
exists to be looked at.
The room is dark because the work is coloured. Every hue that means anything in this product belongs to the
painting: a class colour is an uplift rate, a ramp is a field in mm/yr, a region colour is a landmass. So the
instrument around it is made of iron and ink, with exactly one warm colour — brass — reserved for the two things
the machine can tell you about itself: what you can act on, and what is currently live. A second hue in the
chrome would be a colour that looks like data and isn't.
It is patient, and it explains itself. Under a control that takes a number nobody has an intuition for sits an
11px note in graphite saying what that number does to the ground, and the notes say *why* rather than *what* —
"a mark you cannot see is one you will paint over". That voice is part of the system, not commentary bolted to
it. The tool assumes a reader who will read, and it is written for someone who opens it every few weeks rather
than every day.
**Key Characteristics:**
- One monospace family, three sizes (13px / 11.5px / 11px), no display type anywhere.
- A fixed 300px rail against a fluid canvas; the canvas absorbs every pixel of a resize.
- Flat by doctrine: not one `box-shadow` in the system. Depth is tonal value plus a 1px hairline.
- Brass (#d6a03a) is the only warm colour and never describes data.
- State changes are instant. The single animation in the product measures something that actually takes time.
- Monospace alignment is load-bearing — reports are `white-space: pre` and the columns are the layout.
## Colors
An iron room lit by one brass lamp, with verdigris and rust kept in reserve for the two things the machine needs
to say out loud.
### Primary
- **Brass** (#d6a03a): The only warm colour in the product, and the only colour in the chrome that isn't data. It
marks exactly two conditions. *Filled* (brass background, #17181a text) means "this is the action" — the Plan
button, the Bake button. *Outlined* (brass border, brass text, surface unchanged) means "this is the one that
is live" — the selected class swatch, the open tab, the map currently drawn over the painting, the border that
appears under the cursor. It is also the fill of the bake progress bar, which is the same statement in a third
grammar: this is the thing currently happening.
- **Brass Ink** (#17181a): Text on a filled brass surface. Near-black rather than the room's own background, so
a primary button reads as a lit plate and not as a hole.
### Secondary
- **Verdigris** (#6fbf73): Success, and only success — the border of a confirmation toast. Patina on brass: the
same metal, cooled. It appears for four seconds and then removes itself.
- **Verdigris Light** (#bfe6c1): Text inside a success toast, lifted so the message reads at 13px against a
near-black scrim.
### Tertiary
- **Rust** (#b4553f): Failure and danger — the border of an error toast, and the border of the Cancel button that
stops a running bake. Oxidised iron: the room's own material gone wrong.
- **Rust Light** (#f0b4a4): Text on a rust-bordered surface. Warm enough to be legible at 11.5px, desaturated
enough that it never competes with brass for "act on this".
### Neutral
The neutrals are one continuous ladder of the same cold iron, and their order *is* the depth system:
- **Void** (#0b0e11): The canvas clear colour, behind and around the world. The darkest value in the product,
because it is the only surface that is genuinely nothing.
- **Well** (#11151a): Recessed surfaces — number inputs, the progress track. Below the panel: things you put
values into.
- **Cold Iron** (#14181d): The page and the stage. The default ground.
- **Panel Iron** (#1b2027): The 300px rail. One step up from the stage, which is the whole reason the rail reads
as a separate instrument.
- **Iron Hover** (#222831): A selectable row under the cursor.
- **Iron Live** (#252b34): The selected row, paired with a brass border.
- **Raised Iron** (#262d36): Buttons. Above the panel: things you press.
- **Hairline** (#2b323b): Every border in the product, always exactly 1px.
- **Cold Ink** (#d8dee6): Primary text and every value the user typed or chose.
- **Graphite** (#8b95a1): Labels, notes, HUD readouts, report bodies, section headings. The voice of the
instrument, as distinct from the values it holds.
- **Scrim** (#0d1014ee) and **Scrim HUD** (#0d1014dd): Floating chrome over the canvas — the toast at 93% and the
cursor readout at 87%. Darker than any inline surface and deliberately translucent, so a floating thing is
legible without ever hiding the world underneath it.
- **Chip Edge** (#0006): The 40%-black hairline around a colour chip, so a swatch keeps its edge against both a
pale class colour and a dark one.
### Named Rules
**The One Warm Thing Rule.** Brass is the only warm colour on screen. It marks what you can act on (filled) or
what is currently live (outlined), and it never describes data. If a new element needs attention and is neither
of those two things, it does not get brass — it gets a note.
**The Data Owns Colour Rule.** Hue inside the canvas belongs to the painting, the legend and the map ramps. The
chrome is iron, ink, and one accent. Nothing in the frame may be a colour that could be mistaken for something
the solve will read.
**The Ladder Rule.** The neutrals are ordered by depth, not by taste. A new surface picks its value by answering
whether it sits below the panel (a well), on it (a row), or above it (a control). There is no free choice of grey.
## Typography
**Display Font:** none. This system has no display role and does not want one.
**Body Font:** ui-monospace, with "Cascadia Mono", Menlo and Consolas behind it
**Label/Mono Font:** the same family, at 11px and weight 600
**Character:** One monospace family does every job in the product, because half of what the interface shows is a
column-aligned report and the other half is numbers that must line up under each other while you edit them. The
result reads as instrument labelling rather than as prose: terse, evenly coloured, and impossible to mistake for
a document. There is no font loading at all — the stack is the platform's own, which is also what keeps the tool
a single offline binary.
### Hierarchy
- **Body** (400, 13px, 1.45): Everything the user typed or chose. Class names, button labels, input values, toast
messages. `font: inherit` on every control, so a number in a field is the same size as the same number in a
sentence.
- **Data** (400, 11.5px, 1.45, `white-space: pre`): Plan reports, bake status, map-key items. Set in graphite,
with `<b>` in cold ink for the one word that matters (`**baking**`, `**done**`).
- **Note** (400, 11px, 1.45): Annotations under a control, in graphite. This is where the system explains itself,
and it is a first-class role rather than a caption.
- **Label** (600, 11px, 0.09em, uppercase): Section headings in the rail — `brush`, `this class`, `planet`,
`bake` — and map-key titles. Uppercase and tracked at 11px so a heading reads as a divider without needing a
rule above it. 16px of space above, 6px below; 2px above when it is first in the rail.
### Named Rules
**The Alignment Is Layout Rule.** Reports are `white-space: pre`, and their columns come from `padEnd`/`padStart`
in the code that generates them. Never rebuild one as a table, a grid, or flex rows. The monospace font is
structural here, and switching families would silently break every report in the product.
**The Lowercase Rule.** Headings are lowercase in the source and uppercased by CSS; labels and buttons are
sentence case at most. Nothing in this interface is Title Case. The one exception is a proper noun the domain
already capitalises.
## Layout
A two-column grid at full viewport height: a **300px fixed rail** and a fluid stage (`grid-template-columns:
300px 1fr; height: 100%`). The rail scrolls on overflow and owns a 1px right hairline; the stage clips and holds
the canvas at 100% × 100% with a `crosshair` cursor. There are no breakpoints and no media queries — this is a
desktop instrument, and PRODUCT.md records that as deliberate rather than unfinished.
**Rail padding** is 10px on every side. Floating chrome is inset 10px from the stage edges: the message toast
top-left at `max-width: 70%`, the cursor readout bottom-left with `pointer-events: none` so it can never
intercept a brush stroke.
**Rhythm.** The spacing scale is 4 / 6 / 8 / 10 / 16px. 6px is the workhorse — the gap in a label/value row, the
gap in a button bar, the vertical padding of a button, the space under a section heading. 4px separates tabs and
map buttons; 8px separates a colour chip from its name; 10px is panel padding and overlay inset; 16px opens a new
section. Two values sit off the scale and are load-bearing anyway: rows carry `margin: 3px 0` so a stack of eight
number fields stays dense, and `<hr>` carries `margin: 14px 0 0` so a rule sits closer to what follows it than to
what precedes it.
**Row grammar.** Every parameter is one `.row`: a graphite label at `flex: 1` on the left, its control on the
right. Labels for nested parameters are indented with leading spaces in the label text itself (`" floor mm/yr"`,
`" fraction"`), which is a monospace trick and entirely in keeping — the indent is exactly two characters
because the font makes that a reliable measurement.
### Named Rules
**The Rail Is Fixed Rule.** 300px, never a percentage, and number inputs are 78px. A label and its value must not
reflow when the window changes size; the canvas absorbs the entire delta. An author resizing the window is
looking at the world, not at the panel.
**The Canvas Is Never Chrome Rule.** Nothing in the stage has a background, a border or a panel of its own except
the two floating readouts, and both are translucent. The world goes edge to edge.
## Elevation & Depth
**There is no `box-shadow` in this system, and there will not be one.** Depth is carried entirely by tonal value
and a single 1px hairline, and this is doctrine rather than an accident of the first implementation.
The ladder runs in both directions off the panel. Inputs and the progress track sink below it to **Well**
(#11151a). Rows lift to **Iron Hover** (#222831) and then **Iron Live** (#252b34). Buttons sit above it at
**Raised Iron** (#262d36). Every one of those surfaces carries the same 1px **Hairline** (#2b323b) border, so
edges are declared rather than implied. Things that genuinely float over the canvas — the toast, the cursor
readout — do not rise in value at all; they go *darker* than anything inline, to a translucent near-black scrim
with the same hairline. Floating is expressed as separation from the world, not as height above it.
This is the right answer for this product specifically. The canvas is a data image whose values the author is
trying to read honestly, and a drop shadow spilling onto it would imply a physical stack that does not exist. The
one surface allowed to sit over the painting does so by dimming it, which is what a real instrument overlay does.
### Named Rules
**The No-Shadow Rule.** No `box-shadow`, no `filter: drop-shadow`, no glow. If something needs to separate, it
changes value or gains a hairline.
**The Two-Direction Rule.** A surface declares its role by which way it moves off the panel: down means you type
into it, up means you press it, level means you select it. A new surface that is the same value as the panel is
undecided, and undecided is a bug.
**The Scrim Rule.** Anything floating over the canvas is darker than the panel and translucent (#0d1014dd–ee).
It never becomes opaque and it never gets lighter, because the point of a floating readout is that you can still
see what it is describing.
## Shapes
Three radii, each with a job. **2px** (`{rounded.sm}`) is for a gradient strip — the map-key ramp, which is
almost a rectangle because it represents a continuous scale and a rounded end would misdescribe it. **3px**
(`{rounded.md}`) is for small inlaid things: colour chips, number inputs, the progress track. **4px**
(`{rounded.lg}`) is for anything you press or that can become selected: buttons, swatch rows, the floating
readouts. Nothing in the product is more rounded than 4px and nothing is a pill.
Every edge is a 1px hairline. Colour chips carry a 40%-black edge (#0006) instead so the chip reads against both
a pale class colour and a dark one. Chips are 18px in the rail and 12px in a map key — the same square at two
scales, never a circle.
The one deliberately hard-edged form in the product is the brush disc, which is written straight into `ImageData`
with no antialiasing because a blended pixel would classify as a third class. It is worth stating here as form
language: this system does not soften an edge that carries meaning.
### Named Rules
**The Reserved Border Rule.** Anything that can become selected carries `border: 1px solid transparent` at rest,
so selection changes colour and never geometry. Nothing in this rail is allowed to shift, grow or reflow when you
click it.
**The Square Chip Rule.** A colour sample is a 3px-radius square with its own dark edge, at 18px in the rail and
12px in a key. Never a circle, never borderless, never a swatch of the class colour used as a background behind
text.
## Components
### Buttons
- **Shape:** Barely rounded plates (4px radius), 1px hairline, `padding: 6px 10px`, `font: inherit` so a button
label is the same 13px as everything else.
- **Default:** Raised Iron surface (#262d36), cold ink text, hairline border. Used for everything that saves,
toggles or opens — `Save legend`, `Save painting`, `Re-roll`.
- **Primary:** Filled brass (#d6a03a) with near-black text (#17181a) and `font-weight: 600`. There is at most one
of these visible per group: `Plan (4 s)` in the main bar, `Bake` in the bake block.
- **Hover:** The border alone turns brass. The surface does not change and nothing moves. Instant, no transition.
- **Disabled:** `opacity: .5` and `cursor: default`, applied to whatever variant it already was — a disabled
primary is a dimmed brass plate, not a grey one.
- **Toggle (on):** Brass border *and* brass text on the unchanged Raised Iron surface. This is how tabs and map
buttons show which one is showing. **Filled means act; outlined means live** — the distinction is the whole
button system.
- **Danger:** Rust border (#b4553f) and rust-light text (#f0b4a4) on Raised Iron. Exactly one exists — `Cancel`
on a running bake — and it appears only while there is something to cancel.
### Swatch rows
The signature control of the rail, and a gauge as much as a picker. One row is: an 18px colour chip, the class
name at `flex: 1`, and a right-aligned graphite reading that says what the class *does* — `3.4° rolling`,
`120 m deep`, `coast pinned`. Selecting a class and reading its consequence are the same glance.
- **Rest:** transparent background, `border: 1px solid transparent`, `padding: 4px 6px`, 4px radius, 8px gap.
- **Hover:** Iron Hover background (#222831). No border change.
- **Selected:** Iron Live background (#252b34) *and* a brass border.
- **Erase variant:** the chip becomes a 45° hatch (`repeating-linear-gradient(45deg, #3a424c 0 4px, #232931
4px 8px)`). Absence is drawn, never left blank — a blank chip would read as a colour.
### Inputs / Fields
- **Number:** Well background (#11151a), cold ink text, hairline border, 3px radius, `padding: 2px 5px`, fixed
78px width. Recessed against the panel, which is how you can tell at a glance which things in the rail accept
a value.
- **Range:** the native control at `width: 100%`, unstyled. Used once, for brush size, directly under its own
readout.
- **Checkbox:** native, right-aligned in its row like any other control.
- **Focus:** *not currently styled.* The browser default is doing this job, which is the one hole in the hairline
system — see Do's and Don'ts.
### Navigation
Two full-width tabs at the top of the rail (`flex: 1` each, 4px gap, `padding: 4px 6px`) switching the brush
between the class painting and the overlay. The active tab uses the toggle-on treatment — brass border, brass
text, `font-weight: 600`. A tab that cannot apply (a planet with no overlay legend) is `disabled` rather than
hidden, and carries a `title` explaining what would enable it. Duplicated on the `o` key.
### Message toast
Floating at the stage's top-left, 10px inset, Scrim background (#0d1014ee), hairline border, 4px radius,
`padding: 6px 9px`, `max-width: 70%`. Three states: neutral (hairline border, cold ink), **error** (rust border,
rust-light text) and **ok** (verdigris border, verdigris-light text). Success auto-dismisses after 4 seconds and
only if the message is still the one that was posted; errors stay until replaced. Preserves newlines, because
this is where multi-line guidance appears — including the `file://` rescue message that explains why every
request failed.
### Cursor readout
Bottom-left, 10px inset, Scrim HUD (#0d1014dd), graphite, `white-space: pre`, `pointer-events: none`. Three
facts, continuously: pixel position and the same position in kilometres east, then zoom percentage and the
current brush name. The one piece of chrome that never waits to be asked.
### Bake progress
A 6px Well track (#11151a, 3px radius, `overflow: hidden`) with a brass fill whose width carries
`transition: width .3s` — **the only animation in the product**. Below it, the status block in Data type with the
state word bolded to cold ink, then the last few landed regions and a dimmed tail of the last four log lines at
`opacity: .65`.
### Map key (signature)
A key is published with every map view and generated by the code that drew that map. Two shapes: a **ramp** (12px
gradient strip, 2px radius, chip-edge border, with its two end values in graphite beneath at `space-between`) for
continuous fields, and a **two-column grid** (`gap: 1px 8px`) of 12px chips with names for categorical ones.
Under either, a `note` paragraph in graphite explaining what the map means and what it does *not* mean.
### Monospace report (signature)
A `white-space: pre` block in Data type, graphite, column-aligned by `padEnd`/`padStart` at generation time. It
mixes a table, warnings and prose in one flow — a seam warning, a per-class share table, the fault and lithology
summary, an overlay tally, a region estimate. Its density is the point: a plan report is the four-second answer
the whole product is built around, and it is meant to be read whole rather than scanned.
## Do's and Don'ts
### Do:
- **Do** use filled brass for the one action in a group and outlined brass for the one item that is live. Never
both in the same role.
- **Do** reserve `border: 1px solid transparent` on anything selectable, so selection never changes geometry.
- **Do** put an 11px graphite `note` under any control whose raw value has no obvious consequence, and write it
to say *why*. The note is part of the control.
- **Do** report a number back in the unit the author thinks in — degrees, metres, kilometres, minutes — beside
the field that takes it.
- **Do** publish a key with every map, generated by the code that drew the map, so the two cannot drift apart.
- **Do** pick a new surface's value off the ladder by asking whether it sinks, sits or rises relative to the
panel (#11151a / #1b2027 / #262d36), and give it the 1px hairline.
- **Do** keep reports in `white-space: pre` with their columns padded in code.
- **Do** disable a control that cannot apply and give it a `title` saying what would enable it, rather than
hiding it.
### Don't:
- **Don't** add a `box-shadow`, a glow or a `drop-shadow` anywhere, at any elevation.
- **Don't** add a transition to a state change. Hover and selection are instant. The only animation in this
system is the bake progress width (`.3s`), and it earns that because it measures a two-hour process.
- **Don't** introduce a second font family or a fourth type size. 13px / 11.5px / 11px in one monospace stack is
the entire scale, and a family change would break every column-aligned report.
- **Don't** put a hue in the chrome. Colour in this product means an uplift rate, a depth, a rock type or a
region; a decorative hue would be indistinguishable from one of those.
- **Don't** use brass on anything that is neither actionable nor live — not on a heading, not on a value, not to
emphasise a word in a note.
- **Don't** let a control change size, padding or position between states.
- **Don't** round anything past 4px, and don't make a colour sample a circle.
- **Don't** leave a control with only the browser's default focus ring. Number inputs currently do, and it is the
one gap in the hairline system: a keyboard user gets a focus treatment this design never specified.
- **Don't** ship a second letter-spacing for the label role. The map-key title is at `0.08em` against the section
heading's `0.09em`; that is drift, and `0.09em` is the value.