A collaborative coding-agent orchestrator for atproto radl.app
radial DESIGN.md
40 kB


name: Radial description: Coordination UI for humans commissioning and reviewing coding-agent artifacts on atproto. colors: desk: "oklch(0.921 0.008 258)" desk-edge: "oklch(0.878 0.010 258)" rail: "oklch(0.963 0.005 258)" paper: "oklch(0.996 0.001 258)" raised: "oklch(1 0 0)" sunk: "oklch(0.972 0.004 258)" hover: "oklch(0.972 0.004 258)" sel: "oklch(0.918 0.008 258)" line: "oklch(0.902 0.006 258)" line-soft: "oklch(0.938 0.005 258)" ink: "oklch(0.255 0.016 262)" ink-2: "oklch(0.475 0.014 262)" stroke: "oklch(0.640 0.012 262)" accent: "oklch(0.475 0.150 264)" accent-deep: "oklch(0.410 0.150 264)" accent-mark: "oklch(0.545 0.165 264)" accent-wash: "oklch(0.955 0.028 264)" on-accent: "oklch(1 0 0)" ok: "oklch(0.470 0.115 152)" ok-mark: "oklch(0.560 0.135 152)" ok-wash: "oklch(0.958 0.032 152)" warn: "oklch(0.475 0.108 68)" warn-mark: "oklch(0.660 0.145 72)" warn-wash: "oklch(0.958 0.044 78)" bad: "oklch(0.475 0.165 27)" bad-mark: "oklch(0.575 0.190 27)" bad-wash: "oklch(0.958 0.036 27)" teal: "oklch(0.560 0.100 200)" plum: "oklch(0.520 0.130 330)" typography: display: fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Helvetica Neue, sans-serif" fontSize: "26px" fontWeight: 600 lineHeight: 1.18 letterSpacing: "-0.019em" headline: fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Helvetica Neue, sans-serif" fontSize: "24px" fontWeight: 600 lineHeight: 1.2 letterSpacing: "-0.018em" title: fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Helvetica Neue, sans-serif" fontSize: "14px" fontWeight: 600 lineHeight: 1.3 letterSpacing: "normal" body: fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Helvetica Neue, sans-serif" fontSize: "13.5px" fontWeight: 400 lineHeight: 1.62 letterSpacing: "normal" label: fontFamily: "-apple-system, BlinkMacSystemFont, Segoe UI, system-ui, Helvetica Neue, sans-serif" fontSize: "11.5px" fontWeight: 600 lineHeight: 1.3 letterSpacing: "0.004em" code: fontFamily: "ui-monospace, SFMono-Regular, SF Mono, Menlo, Cascadia Mono, monospace" fontSize: "11.5px" fontWeight: 400 lineHeight: 1.4 letterSpacing: "normal" rounded: xs: "4px" sm: "5px" md: "7px" lg: "8px" xl: "10px" window: "13px" pill: "999px" spacing: xs: "4px" sm: "7px" md: "10px" lg: "14px" xl: "22px" xxl: "30px" components: button: backgroundColor: "{colors.paper}" textColor: "{colors.ink}" rounded: "{rounded.md}" padding: "6px 12px" button-hover: backgroundColor: "{colors.hover}" textColor: "{colors.ink}" rounded: "{rounded.md}" padding: "6px 12px" button-primary: backgroundColor: "{colors.accent}" textColor: "{colors.on-accent}" rounded: "{rounded.md}" padding: "6px 12px" button-primary-hover: backgroundColor: "{colors.accent-deep}" textColor: "{colors.on-accent}" rounded: "{rounded.md}" padding: "6px 12px" button-disabled: backgroundColor: "{colors.sunk}" textColor: "{colors.ink-2}" rounded: "{rounded.md}" padding: "6px 12px" input: backgroundColor: "{colors.paper}" textColor: "{colors.ink}" rounded: "{rounded.lg}" padding: "8px 10px" badge-approve: backgroundColor: "{colors.ok-wash}" textColor: "{colors.ok}" rounded: "{rounded.pill}" padding: "3.5px 8px 3.5px 6px" badge-changes: backgroundColor: "{colors.bad-wash}" textColor: "{colors.bad}" rounded: "{rounded.pill}" padding: "3.5px 8px 3.5px 6px" badge-awaiting: backgroundColor: "{colors.warn-wash}" textColor: "{colors.warn}" rounded: "{rounded.pill}" padding: "3.5px 8px 3.5px 6px" badge-flat: backgroundColor: "{colors.sunk}" textColor: "{colors.ink-2}" rounded: "{rounded.pill}" padding: "3.5px 8px 3.5px 6px" badge-accent: backgroundColor: "{colors.accent-wash}" textColor: "{colors.accent}" rounded: "{rounded.pill}" padding: "3.5px 8px 3.5px 6px" uri-chip: backgroundColor: "{colors.sunk}" textColor: "{colors.ink-2}" typography: "{typography.code}" rounded: "{rounded.xs}" padding: "3px 6px" nav-item: backgroundColor: "{colors.rail}" textColor: "{colors.ink}" rounded: "{rounded.sm}" padding: "5px 8px" nav-item-current: backgroundColor: "{colors.sel}" textColor: "{colors.ink}" rounded: "{rounded.sm}" padding: "5px 8px" fab: backgroundColor: "{colors.accent}" textColor: "{colors.on-accent}" rounded: "{rounded.pill}" size: "46px" keycap: backgroundColor: "{colors.sunk}" textColor: "{colors.ink}" rounded: "{rounded.sm}" padding: "4px 6px" #

Design System: Radial #

Overview #

Creative North Star: "The Quiet Desk"

Radial is a working surface, not a dashboard. A rail of lists sits beside a sheet of paper, and the paper holds one thing at a time: a goal and the units of work under it. The whole visual system exists to make a signed record legible — who produced what, against which exact version, and what is waiting on a person — so it behaves like well-kept stationery rather than like software with something to prove. Things 3's rail and its coloured smart-list glyphs are the closest reference for the furniture; the pane is a document view.

Density is welcome where it serves scanning and nowhere else. A unit row is one line — a state circle, a type name, a summary, badges, whose disc, when — and everything behind it lives in a drawer that opens in place. The app is flat at rest: surfaces are separated by a hairline and a tone step, and the only things that lift are the ones genuinely above the page (an open drawer, the ⊕ menu, a popover, a compose card). Both themes are first-class; dark is not an inversion but its own token set, where raised means lighter rather than shadowed.

One accent — an ink indigo — carries every action and every selection. Sage, red and amber are spent exclusively on verdicts and on "stopped on you", which is what keeps them meaningful when they appear. The system rejects the generic AI-SaaS look outright: no gradients on chrome, no glassy cards, no metric tiles, no eyebrow above every section, no accent used decoratively.

Key Characteristics:

  • Two-column app shell: a 246px rail beside a paper pane, full-bleed in the tab (the comp's desk-and-window framing is opt-in via ?frame). The rail's edge is a grip: 190–460px, remembered per browser profile, double-click back to 246.
  • One accent (ink indigo), on actions and selection only; verdict colours are rationed to verdicts.
  • System UI sans + system mono. Mono is semantic: it marks machine identifiers (at:// URIs, CIDs, paths, commit hashes) and never English words.
  • Flat at rest. Elevation is a response to state, and a hairline plus a tone step does most of the separating.
  • State is never colour alone: every verdict, status and check pairs colour with a glyph and a word.
  • Motion is short and functional (110–400 ms, one easing curve), and prefers-reduced-motion disables all of it in one rule.

Colors #

A near-neutral blue-grey stack — desk, rail, paper, sunk — carrying a single indigo voice, plus a three-colour semantic vocabulary that only verdicts and blocked states may spend.

Primary #

  • Ink Indigo (oklch(0.475 0.150 264)): the one brand voice. Primary buttons, the ⊕, the current version in a rail, links, the focus ring's base. It marks the thing that acts and the thing that is chosen — never a heading, never a border for decoration.
  • Ink Indigo Deep (oklch(0.410 0.150 264)): the hover and active state of anything filled with the accent.
  • Indigo Mark (oklch(0.545 0.165 264)): the accent as a stroke — the focus outline, the state circle of an assigned or claimed unit — lightened so a 1.6px line reads at the weight the fill does.
  • Indigo Wash (oklch(0.955 0.028 264)): the tint inside an assigned unit's circle and behind an accent badge. A background signal, never text.

Neutral #

  • Desk (oklch(0.921 0.008 258)) and Desk Edge (oklch(0.878 0.010 258)): the surface the app sits on — a radial gradient between them. Visible around the sign-in card, and behind the window when ?frame is on.
  • Rail (oklch(0.963 0.005 258)): the navigation column, a half-step below paper so the pane reads as the thing in front.
  • Paper (oklch(0.996 0.001 258)): the pane, and the resting fill of a button and an input.
  • Raised (oklch(1 0 0) light, oklch(0.278 0.012 262) dark): what sits above paper — an open unit, a compose card. In dark it is lighter than paper; raised means closer to the light, in both themes.
  • Sunk (oklch(0.972 0.004 258)) and Hover (the same value in light, distinct in dark): inline code, a URI chip, a disabled control, a hovered row.
  • Line (oklch(0.902 0.006 258)) and Line Soft (oklch(0.938 0.005 258)): control strokes and section rules; the soft one separates rows inside a list, where a full hairline would stripe the page.
  • Ink (oklch(0.255 0.016 262)): primary text.
  • Ink 2 (oklch(0.475 0.014 262)): secondary text — a row's summary, a timestamp, a hint — held at ≥4.5:1 on paper deliberately.
  • Stroke (oklch(0.640 0.012 262)): the outline of an open state circle, and a menu glyph: the weight of something that exists and is not yet anybody's.

Semantic (verdicts and blocked states) #

  • Sage — text oklch(0.470 0.115 152), mark oklch(0.560 0.135 152), wash oklch(0.958 0.032 152): an approving verdict, a passing check, a merged artifact, a closed goal.
  • Red — text oklch(0.475 0.165 27), mark oklch(0.575 0.190 27), wash oklch(0.958 0.036 27): a request-changes verdict, a failing check, a destructive confirmation, a write that failed.
  • Amber — text oklch(0.475 0.108 68), mark oklch(0.660 0.145 72), wash oklch(0.958 0.044 78): stopped on you — an unanswered question, a stale sync, the fixture chip. Never "warning" in the generic sense.

Accents of last resort #

  • Teal (oklch(0.560 0.100 200)) and Plum (oklch(0.520 0.130 330)): the System glyph, and actor discs. Disc colour is derived from a DID, never authored — and it is what shows through when an identity publishes no profile picture.

Named Rules #

The One Voice Rule. Ink indigo is the only brand hue, and it appears only on things that act or are selected. A screen with an indigo divider, an indigo heading or an indigo illustration has failed.

The Rationed Verdict Rule. Sage, red and amber belong to verdicts, checks and "stopped on you". If a colour would mean "this is a bit important", it has no colour. Drift — a living document whose implementations have merged past it — is information, not an alarm, so its badge is neutral.

The Verdict-Is-Never-Colour-Alone Rule. Every verdict, check and status pairs its colour with a glyph and a word (✓ approved, ✕ changes requested, ● awaiting you). Roughly 1 in 12 readers cannot separate the sage from the red.

The Honest Depth Rule. In dark mode a raised surface gets lighter (raised sits above paper) rather than acquiring a heavier shadow. Depth is tone first, shadow second.

Typography #

Display / Body Font: system UI sans (-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, "Helvetica Neue", sans-serif) Label/Mono Font: system mono (ui-monospace, SFMono-Regular, "SF Mono", Menlo, "Cascadia Mono", monospace)

Character: The platform's own voice, deliberately. This is a tool people keep open beside an editor and a terminal; a webfont would announce itself, cost a request, and make the app look less like part of the machine it is driving. The expressive work is done by size, weight and measure — and by the sans/mono distinction, which carries meaning rather than flavour.

Hierarchy #

  • Display (600, 26px/1.18, −0.019em): the page title — a goal, a project, a space. One per view.
  • Headline (600, 24px/1.2, −0.018em): the sign-in card's title, the only heading outside a space.
  • Title (600, 14px/1.3): a compose card's heading, an admin row's name, a message author.
  • Row (500, 14px): a unit row and a goal row — the app's most common line, a notch above body so a list reads before its prose does.
  • Body (400, 13.5px/1.62, max 66ch): artifact bodies and prose. A brief is the same size at 1.55 and 64ch, indented behind a hairline.
  • Secondary (400, 12.5–13px): hints, group notes, key/value metadata.
  • Label (600, 11.5px, +0.004em): badges, field labels, the rail's group headers; a timestamp is 11.5px and tabular.
  • Section head (700, 12px, with a hairline under it): "Goals", "System", "Members" — a rule with a word on it and a count beside it.
  • Code (mono, 11.5–12.5px): at:// URIs, CIDs, paths, line numbers, registry type names, inline code in prose.

Named Rules #

The Mono-Means-Machine-Identifier Rule. Monospace marks something a machine issued or parses: a URI, a CID, a commit, a path, a registry name. English words are never mono — a version chip that says v3 is mono, and one that says "supersedes v2" is not.

The No-Eyebrow Rule. No small uppercase label above a heading. A section is a hairline with a word on it; that is the whole device.

The Measure Rule. Prose is capped at 64–66ch, and rules inside a drawer track the same measure. A rule longer than the block under it is a ragged edge.

Layout #

A two-column shell: a 246px rail and a pane that takes the rest (grid-template-columns: var(--rail-w) minmax(0, 1fr), --rail-w defaulting to 246px). The rail is resizable between 190 and 460px by the grip on its own edge, never past the point where the pane is left under 560px; the width is a property of the browser profile rather than of the space, so it is remembered in localStorage and applied before first paint, next to the theme. The app fills the tab; the comp's desk-and-window framing (1220×860, centred, on a radial-gradient desk) is opt-in with ?frame and exists for screenshots. The sign-in picker is the exception — with no rail to sit beside, it stays a min(620px) card centred on the desk.

Inside the pane: a 50px bar (crumb, freshness, quick find, account, appearance), then one scrolling column with an 880px sheet hard against the rail's edge, 0 22px of gutter and 120px of bottom padding so the ⊕ never sits on the last row. The sheet does not centre in a wider pane: the pane bar's crumb cannot centre with it, and a page with two left edges reads as an accident rather than a choice.

Rhythm is a 4 / 7 / 10 / 14 / 22 / 30 step. Rows are 8–9px vertical with a 10–11px gap and negative side margins, so a hover fill bleeds past the text without moving it. Section heads take 30px above and 6px below.

Responsive. At 860px the rail becomes an overlay drawer over a full-bleed pane (a hamburger in the bar, an interactive dismiss scrim behind, a 300ms slide) at its own fixed width with no grip — a drawer over the pane is not a column beside it, so there is no edge between two things to drag — ?frame gives up, and gutters tighten to 16px. On touch-primary devices — keyed on the pointer rather than on a width, because a landscape iPad is wider than 860px and behaves the same — every typed control rises to a 16px floor, the mono identifier fields included, so focusing one does not trigger Safari's viewport zoom. The three field sizes are tokens (--field-size, --field-ident-size, --field-find-size) and the floor raises the tokens, not the rules that read them: a value cannot be out-ranked by a longer selector, and nothing that somebody types into states its size any other way. At 560px, a list row stops being a flex line and becomes a paragraph: its name wraps as text and its metadata tail floats into that text, so the tail takes the space the name left over on the line and only drops below when there is none. The state glyph leaves the flow into a reserved gutter so wrapped lines stay aligned under each other, and the tail is capped at 58% of the row — past that the leftover is too narrow for a word to fit and the name would be pushed under its own metadata. Nothing is shed for the width: a name that would have been ellipsised wraps instead, and actor discs and every badge stay. Admin-row descriptions may wrap instead of ellipsising. The compact badge form, breadcrumb tail, fixture chip, account name and keyboard-help button still shed at this width because each has a nearby visual or interaction equivalent.

The pane bar sheds the two things in it that can be said another way, and gives each a tap to get back. Freshness keeps its dot and loses its sentence: a phone cannot hover the title off it, so the dot is a second control that answers a tap with what that tooltip would have said and offers the sync the wide chip does on a click — two buttons, each doing one thing, rather than one that asks how wide the window is. Quick find becomes a search icon, and opening it gives it the bar: the crumb and the dot step aside for as long as something is typed, because a box being typed into is what the reader is doing. The box is never display: none while it is collapsed — / and the icon both open it by putting focus in it, and a field that is not rendered cannot take focus — so it is clipped, and its own onfocus is what opens it. One way in, and no second state to keep in step.

A float only shortens the line boxes that follow it, so each row's tail is written before the name it floats into and order puts it back on the end of the flex line at wider widths. This is the one place the DOM is not in reading order, and it is the layout's load-bearing detail rather than a tidiness question — test/row-stack.test.mjs holds both halves together.

Elevation & Depth #

Flat at rest. Separation is a hairline plus a tone step (rail against paper, sunk against paper), and shadow is reserved for things genuinely above the page. Nothing has a resting shadow except the window itself, and the window only exists under ?frame.

Shadow Vocabulary #

  • Drawer / card (0 0 0 1px var(--line), 0 10px 26px -12px oklch(0.25 0.03 262 / 0.30)): an open unit row and the request card — the two things that lift out of a list in place. The goal composer is not one of them: it belongs to no list, so it is a modal.
  • Popover / menu (0 2px 6px oklch(0.25 0.03 262 / 0.10), 0 18px 40px -10px oklch(0.25 0.03 262 / 0.34)): the ⊕ menu and the account popover.
  • Floating action (0 2px 4px oklch(0.25 0.03 262 / 0.20), 0 10px 22px -6px oklch(0.35 0.12 264 / 0.45)): the ⊕, whose shadow carries the accent hue because the button is accent-filled.
  • Modal (0 2px 6px …, 0 24px 60px -14px oklch(0.25 0.03 262 / 0.4)): the space picker and the goal composer, each over its scrim.
  • Window (0 1px 1px …, 0 18px 34px -14px …, 0 44px 90px -30px …): the framed app and the sign-in card, on the desk.

In dark mode every one of these gives way to a single deeper, softer shadow and the work is done by tone instead: raised is lighter than paper, and that is what says "above".

Named Rules #

The Flat-At-Rest Rule. A row, a badge, a chip and a button have no shadow. If something has one it is open, floating or modal — and it got there because the reader opened it.

Shapes #

One soft-rectangle language, sized to the object: 4–5px for an inline chip or a nav item, 7px for a button or an input, 8px for a row and a textarea, 10px for an open unit, 11px for a popover, 13px for the window and the picker card. Pills (999px) are reserved for badges, the freshness chip, the ⊕ and a label — a label chip and the dashed one that adds a label, which are the same object at two moments and would read as two different things if only one of them were a pill. The field that types a label is not among them: it stands in a panel of its own rather than in the row, so it is the ordinary 7px field every other panel holds.

Two shapes carry meaning and are never decorative. The state circle is an 18px SVG whose form is the state: a hollow ring (open), an accent ring with a wash centre (assigned), the same ring dashed and slowly rotating (a live claim), a thick amber ring with a filled centre (awaiting you), a ring with a tick (landed), a solid ink disc (judged), a solid sage disc (merged), a struck-through ring (retracted). The disc is an actor: a circle for a human, a 5px-radius square for an agent, initials on a colour derived from the DID. When that identity publishes a profile picture, the picture fills the disc — same box, same shape, cropped to cover — and the derived fill stays behind it as the fallback for a picture that is loading, transparent, or gone.

Borders are hairlines, and a control's border is what changes on hover — not its fill, except where the fill is the state (a pressed version button, an "on" auto-review switch).

Components #

Buttons #

  • Shape: softly rounded (7px), 6px/12px padding, 13px/500.
  • Default: paper fill, hairline border, ink text. Hover raises the border to stroke and fills with hover; active nudges 0.5px down.
  • Primary: accent fill, on-accent text, 600 — one per surface, on the thing the surface exists to do.
  • Verdict buttons: an outline button whose border is mixed 28–32% from the verdict colour, filling with its wash on hover. The one place a verdict colour is a control rather than a report, and the glyph still says which is which. They are always the action itself — the verdict card's two are what write the record, never a selector sitting above a third button that does — so there is no pressed state to hold.
  • Disabled: sunk fill, ink-2 text, no transform — used for a rule already broken (an empty brief, a duplicate registry name), never to hide a capability the reader has.

Badges #

  • Style: pill, 600 11.5px, a glyph then a word, on an asymmetric 6px/8px pad (the glyph side is tighter).
  • Variants: ok / bad / warn / accent washes for verdicts, checks and "for you"; flat (sunk) for everything factual — a count, "archived", "custom type", a drift count.
  • Narrow form: below 560px a badge swaps its wide span for a narrow one — the count without the sentence. The goal row's counts (with an agent, open, to review) carry one, because they are what decides how much of a narrow row is left for the goal's own name; awaiting you has no count to fall back on and is the one thing on the row a human is meant to act on, so it keeps its words. completed goes further and keeps its glyph alone, because a green tick means completion in every list this product draws; the other endings cannot, since parked, dropped and superseded share one glyph and one colour and the word is the only thing telling them apart. The sentence is hidden from the eye and not from the accessible name: it is clipped rather than display: none, since the narrow form beside it is aria-hidden and a badge that shed both would say nothing at all.

Rows #

  • Unit row: state circle, type name, an optional version chip, a one-line summary, then a tail of badges, the actor's disc and an age. Below 560px the summary wraps as text and the tail floats into it. Rows are separated by a soft hairline drawn by the next row, so the first has none and an open row's neighbours drop theirs.
  • That summary line, and when it is ink rather than ink 2: an artifact carries its own short title, and where the line IS that title it is the record's name and reads in ink. Everything else in that slot is metadata and stays ink 2: a word about what an agent is doing, and — for a record written before artifacts had titles — the sentence lifted out of its body. It ellipsises on wider viewports and wraps below 560px; a title is capped at 200 characters, which is the only thing bounding how tall a narrow row may get.
  • Open state: the row keeps its place and the drawer below it animates open (grid-template-rows: 0fr → 1fr, 400ms); the whole unit lifts onto raised with the drawer shadow. Nothing navigates.
  • Goal row: a progress or state glyph, the goal or target name, then badges, actor discs and age. Below 560px its name wraps as text and the complete metadata tail floats into it, so a goal with a short name and a date costs one line rather than two.
  • Admin row: the same object five times over — a name, what the space knows about it, and at most one thing you can do to it — used for members, registry entries, projects, checks and switches. Below 560px its description wraps and a tail that no longer fits drops below as a group.

Inputs #

  • Style: paper fill, hairline border, 8px radius, 13.5px/1.5, 8px/10px padding; a textarea starts at 60px and resizes vertically. The composer's box is the exception, and the reason is under The composer below.
  • A field holding a machine identifier — a git remote, a branch, a check command, a finding's path and line, a handle or a space URI — is mono at 12.5px, per mono means machine identifier. It is a class (.ident, or every field in a finding's locator row), never a size written on the element: see the touch floor under Responsive.
  • Focus: the border becomes the accent and a 3px accent-wash ring appears outside it — the same treatment on every field the app has.
  • Quick find is the exception: a sunk pill with a transparent border that turns accent-marked and paper-filled on focus, because it sits in the bar rather than on the sheet.

The markdown editor #

  • It is a field, not a code editor. Every box holding markdown — a goal's body, a message, a brief, a type's template, a finding — carries the same border, radius, type and focus ring as the inputs above. What syntax highlighting adds is confirmation that what you typed is a heading or a link; drawing it as an editor would make a goal's body look like a different kind of thing from its title, and the fields that are not markdown (titles, paths, line numbers, one-line descriptions) stay ordinary controls for the same reason.
  • The palette is the app's. Headings and emphasis take weight and ink rather than a colour of their own; links take the accent; code takes the mono face and ink-2; the punctuation that makes something a heading recedes to stroke but is never hidden — markdown is text, and an editor that hides its own syntax has started lying about what the document says.
  • Adding an image is a link, not a button: Add image sits under the box with the formats beside it, because it is one more thing you can do with what you are typing rather than an action on the record. The buttons that write the record are already below it, and one of them says Adding image… and refuses while a file is in flight. The review card is the one surface with several markdown boxes and one record, so a picture in any finding puts that copy on both verdict buttons, and the row it is landing in cannot be removed until it has.
  • Items: 13.5px, 5px/8px, 6px radius, a 17px coloured glyph, a label and a right-aligned count. Hover fills with hover; the current page fills with sel and goes 600.
  • Glyph colour is the list's meaning: accent for "For me" and goals, amber for "Awaiting input", sage for the Logbook, teal for System, ink-2 for the space's own page — which sits last, after a rule, because it is where the space's shape is authored rather than where its work is read.
  • Groups collapse by disclosure triangle; projects start open, and only what the reader collapsed is remembered.
  • The grip is the rail's own edge: a 9px target straddling the hairline, invisible until pointed at, when the hairline thickens into the accent. It is a focusable separator — arrows move it 8px (32 with Shift), Home and End take it to its limits, Enter and a double-click restore 246px — and the width is written back only when the gesture ends. A drag takes focus, so the arrows are live straight afterwards, but does not draw the focus ring: the ring is for a reader who arrived by keyboard and needs telling where they are, not for one who has the pointer on the edge already.

The ⊕ (signature component) #

A 46px accent disc pinned to the pane's bottom-right with an accent-tinted shadow; it lifts 1.5px on hover and rotates its cross 135° into an ✕ when its menu is open. It is present on every view of an open space, and its menu always opens with New goal — a goal is the one record a human writes from scratch, and one that could only be written from a project's own goal list was one seven of the ten views had no way to write at all. Below that, separated by a hairline, it still makes an item of the list you are in: on a goal, a request of one of the registry's goal-scoped types; on System, a project-scoped one. That section is generated from the space's own registry records, so what it offers is data rather than markup. ⌘N is the same button — and n is the goal item alone, from anywhere.

Both open the card on the project the view is of: a project's own goal list, its System page, its settings, or any goal hanging off it. The reader is standing in the answer, so the card says New goal in radial-ng and starts there rather than asking a question the route has already answered. Only a view that names no project — a smart list, the Logbook, /space — leaves it open, and there the card falls back to the last project this profile actually wrote into, then to the space's sole project, and asks only when none of those settles it.

The rule this replaces was "it makes an item of the list you are in, and on a smart list it is absent rather than inert". It was coherent and it cost the product its one authored record: on /me, on the Logbook, on /space, there was no ⊕ and so no way to write down the thing you had just thought of. The goal is now the constant and the list's own item is the variable.

The ask bar #

A label — Ask an agent for — and one default button per registry type of the page's own scope, each with the type's 15px glyph in stroke that goes accent on hover, sitting under the title where the compose card opens: goal-scoped types on a goal, project-scoped ones on System. It is the ⊕'s request section as a control in the page, generated from the same registry records, and it exists because those two surfaces were the only ways in: a floating disc in the corner, and a button in a drawer under an artifact that already landed — which is no help at all on the page that has none, the one place where asking is the whole of what there is to do. A human initiating is the first move of the product's loop, so it is a thing you can see rather than a menu you have to find.

Nothing in it is primary and nothing is ordered by anything but the registry's own alphabet: which type to ask for next is the human's judgement, and an accent-filled "Request plan" would be the UI making it for them. It stays put while its card is open — that is where Escape hands focus back, and it is how the reader changes their mind about which type they are asking for — which is also the difference between it and a ⊕ menu item, since a menu is a surface and this is a place.

The composer #

One card, 10px radius, holding the box you type in and — inside the same border, along its bottom edge — the row of things you can do with what you typed: @agent, then Post. The box itself carries no border, no fill and no focus ring of its own; the ring belongs to the card, so focus lights the whole object rather than drawing a second rectangle inside the first. It grows with its content and has no resize grip, which is the one place an input in this system does not behave like the others: a brief nobody rereads can be a fixed box the reader drags bigger, but dragging a handle to see the sentence you are in the middle of writing is not a feature, and that grip was the only diagonal scribble on a page made of hairlines. ⌘↵ posts.

The @agent button is the ask, as one control: it carries the agent's own disc and handle — @planner — and pressing it writes both records, the message and the typed request asking that agent to reply. Which agent is a setting on the button, reached by the chevron joined to its right edge, which opens a small menu of the agents that answer and is absent when the space has only one. What this replaces was a full-width picker on its own line above a button called Post & ask an agent: two controls that had to be read together, gave no sign that they were one, and left the only way to find out what the picker did as pressing the other thing. The accent-filled Post stays last, because posting is what the box is for and asking is the variant — and when the space has no agent that answers, Post is the whole bar. The @ is a label on a button that writes a request, never a string parsed out of the body: a mention addresses somebody and dispatches nothing (§10).

The keyboard map #

/ puts the caret in quick find, n writes a goal from anywhere — in the project the view is of, where it is of one — ⌘N is the ⊕ of the view being shown, Escape closes one layer per press — and ? draws the map itself, over whatever is already open. It is the picker's paper card on the same scrim: a group heading per surface, a sans keycap per chord (Esc is a label printed on a key, not a machine identifier, so keycaps are never mono), and one plain sentence beside each. The pane bar carries a keyboard button next to the appearance toggle, titled with the key it stands for — a shortcut nobody can find is a shortcut nobody has. While the map is up it holds the keyboard: nothing behind it answers a key, and Escape takes the map away before it takes anything else. The same rule makes n inert under the space picker, and inert while a card already open has something typed into it — a bare letter is reachable by accident, and opening a second card is how the first one is abandoned.

The foreign-thread window #

The one framed thing on a goal page, and the frame is the argument: everything else on the sheet is a record this space holds, and a Roomy room is a live conversation it does not. A hairline box at 10px with sunk chrome top and bottom, holding a log that scrolls inside itself — min(52vh, 440px), opened at the newest message and kept there through a refresh, unless the reader has scrolled back, in which case nothing moves under them. It renders the newest 40 and reveals another 40 per press of a centred link at the top of the scroller, so a room of two thousand messages costs a goal page 440px and 40 rows. A day is a rule with a word on it, the same device the units above it use; a second message from one person inside five minutes drops its header and continues the turn. The top edge fades into the panel's own ground while there is something above it — a scroll affordance, not an elevation. Nothing here is a Radial actor, so nothing here gets a disc.

Selecting is the mode. A 14px checkbox sits in an always-reserved 18px gutter at opacity: 0, arriving on hover, on focus and on any selection existing (a touch device has no hover, so it is simply there, with an invisible 40px reach around it). A plain press takes one message, a shift-press takes the run between it and the last — and the foot of the panel, which was carrying one quiet sentence about what selecting is for, becomes the bar that acts: the count, what an import publishes, Clear, and one accent-filled Import n into goal context that writes one signed snapshot per message in the order the room said them. A failure part-way says how far it got and leaves the rest selected. This is the shape to reach for whenever the same action is worth doing to several rows at once: one control per row is a row nobody reads, and a per-row confirmation is a sentence written as many times as there are rows.

The URI chip #

Mono, 11.5px, sunk, 4px radius, cursor: copy, flashing sage on copy. It is how a record's identity stays available without a record ever being the headline.

Diagnostics disclosure #

A closed <details> at the foot of the pane — 11.5px ink-2, a rotating › marker, no other chrome — holding what the fold ignored and what it declined to edit. Absent when there is nothing to report, and never coloured: an ignored record is a fact about the space, not an alarm.

Build stamp #

The same disclosure, directly under the diagnostics one and sharing its rule and its gap — the foot of the pane is one group, so a second disclosure continues the first rather than starting another footer. It holds the commit, the moment, the origin the bundle was built for and whether the browser transport binding is in it. Unlike the diagnostics above it, it is never absent: a stamp that hid itself when the app looked fine would be missing at exactly the moment somebody is asked which build they have. It is drawn under the space picker as well, where there is no space to report on. Uncoloured like everything else here, and the mono/sans rule is honoured line by line — a commit and an origin are identifiers, the sentences beside them are not.

Skeleton #

While a space is opening, the shell is drawn in sunk bars on the real furniture — rail, pane bar, six rows — pulsing over 1.7s with a per-row stagger, with one plain sentence saying what is being read. Never a spinner, never a shimmer in the accent.

Do's and Don'ts #

Do: #

  • Do spend the accent only on things that act or are selected, and keep it under ~10% of any screen.
  • Do pair every verdict, check and status colour with a glyph and a word.
  • Do use mono for machine identifiers and only for machine identifiers.
  • Do keep surfaces flat at rest; add a shadow only when something is open, floating or modal.
  • Do give dark mode its own tokens — a raised surface gets lighter, not shadowier.
  • Do cap prose at 64–66ch and let rules inside a drawer track that measure.
  • Do show a control exactly where the rule that governs it says the record would count, and name the rule when a surface is withheld.
  • Do honour prefers-reduced-motion: one rule disables every animation and transition, and the claim circle's rotation explicitly.
  • Do keep the sheet hard against the rail at every width.

Don't: #

  • Don't put an eyebrow above a heading, or a card grid of icon + heading + text anywhere.
  • Don't colour a badge that is merely informative — drift, counts and states of record are flat.
  • Don't use amber for "warning" in general; here it means the work has stopped on a person.
  • Don't set English words in mono, or an identifier in the sans.
  • Don't give a row, chip or button a resting shadow, and don't put a gradient on chrome.
  • Don't frame the app in a window by default — ?frame exists for screenshots.
  • Don't introduce a second accent hue; teal and plum are the System glyph and DID-derived discs, and nothing else.
  • Don't disable a control to imply a pipeline the protocol does not enforce — warn beside it instead.