The written contract for codeaf's visual surface. internal/tui3/styles.go is
the code that holds it and internal/tui3/designlanguage_test.go is the part a
build can fail on; where this doc and the code disagree, the code wins. Written
after an audit of basecamp/omarchy — the
credit is stated properly at the end, along with what we took, what we measured
ourselves, and the one thing we refused.
A surface with no stated language accumulates one. Somebody needs a row to stand out, so they bold it. Somebody else needs a different row to stand out, so they give it a border. A third person, finding both already spent, reaches for a new colour — and now the surface has three ways of saying one thing and a person has to learn all three. Nobody made that decision; it happened.
So the decisions are made here, once, in advance, and they are made SMALL. There are four ways this surface can raise something, and this document is all of them.
A row can wear a ground — a run of cells lifted off the terminal's own background. There are four steps and there will never be a fifth.
| step | what it means |
|---|---|
| rest | nothing at all. The row is the terminal's own background |
| cursor | the row the pointer is over, or the row the keyboard cursor is on |
| selected | the chosen thing: the current row, the current chip |
| mark | a marked span: copy mode's selection, the text a yank would take |
REST IS NOT A COLOUR. It has no value in the table because there is nothing to author: an unremarkable row is painted by not painting it. This is the emptiness law wearing its background clothes, and it is what makes the other three legible — a surface that tinted every row would be a surface where a tint said nothing.
Cursor and hover are ONE step, not two. Whether a person arrived at a row
with the mouse or with ↓, the row they are on is the row they are on, and it
does not change appearance depending on which hand they used.
The heading of the section the cursor is standing in wears a ground, and one heading per frame wears it.
The ladder's steps mark a ROW, and a screen made of several regions — home's three columns are the case that provoked this — needs the same fact one scale up. The cursor step is deliberately faint, so a frame where every region is at rest and one row somewhere in it is faintly lifted answers which row and never which region. Saying it twice, at two scales, in the channel the ladder already has, costs no new colour and no fifth step: the block and the row inside it.
The heading takes the cursor step, not a rung of its own. What a marked heading says is "the cursor is in here", which is the cursor step's own sentence read at the scale of a section. It is senior to the row it stands over by being NEW — a heading has never worn a ground at all — rather than by being louder. Where the selected step is unspent on a screen it is the better candidate, being the container of the chosen thing; on home it is not, because the conversation this terminal holds already wears it in the same column, and one step carrying two meanings is what the refusal of a fifth step exists to prevent.
One heading, or none. Two marked headings would be two answers to a question that has one. None at rest, and none while a list is being filtered — a heading over a search result groups rows rather than naming a place a person is standing. And it follows the KEYBOARD only: the pointer previews without selecting, so a hover moves the card and never the mark.
The heading's text does not move. Headings are furniture and stay muted or
dim — THE ACCENT BUDGET below forbids lighting them, and a lit heading is the
same defect arriving from the other side. The ground alone carries the fact.
A heading that is a door underlines under the pointer, and that is all the pointer does to a heading. Home's headings open the place they name, and a person holding a mouse over one is asking whether it will; the answer is the underline — no ink, no ground, the one attribute a terminal has always used to say "this opens somewhere" — on the word alone, and only on a heading that opens somewhere. It is an affordance and not a step of the ladder: it marks nothing about where the cursor is, so the marked heading above still follows the keyboard only (added 2026-09-17).
A row is emphasized by raising its ground and turning its leading text accent. Nothing else ever changes.
No new colour arrives for the emphasized state. No run of bolding spreads across the row. And above all no outline is added — emphasis is a step UP this ladder, never a ring drawn around a thing.
A QUESTION'S FRAME IS NOT EMPHASIS. The dim frame a
question hangs in (WHAT WE REFUSED → Borders, amended) says "this is one object,
and it is asking you" — a claim about what the thing IS, drawn once, the same
way whatever row inside it is focused. It never lights, never thickens and never
follows the cursor. Emphasis inside it is still exactly this law: the focused
answer's ground steps up to selected and its leading mark turns; nothing else
changes, and no ring is ever drawn around a row.
Two moves, both already in the palette. The ground says which band of pixels is being spoken about; the accent on the leading glyph or word says what it is. A lane that finds itself reaching for a third move has found a state the ladder does not have, rather than a colour the palette is missing.
The honest way to build this ladder is the compositor's way: take the foreground colour, composite it over the background at a stated alpha, and every step inherits the theme's own hue for free and self-inverts on a light terminal with no light-mode branch at all. In a constructor we cannot. There is no variable that states the terminal's background and no query a constructor may block on, so at the moment the palette is built there is nothing to composite over.
Both halves now exist, and which one you see depends on your terminal. The
first frame asks the terminal for its background with tea.RequestBackgroundColor
and nothing waits for the answer; a terminal that replies gets every value on this
ladder derived against its real ground, and a terminal that stays silent keeps the
authored values below forever. The derivation is internal/tui3/adaptive.go and
is aimed at exactly the ratios this section already states — it changes what the
steps are measured against, not what they are aimed at. Where an authored value
already lands in band on the measured ground it is not touched, so most terminals
see nothing change; a pure black screen and a tinted page, the two grounds nobody
could aim at, are what move.
The steps below are therefore authored per ladder, dark and light, and they
are what a silent terminal paints — which is not a degraded mode, it is four
waves of aim. What is
authored is aimed rather than guessed: the assumed dark ground is the range real
terminals sit in, #101014 through #1e1e2e, and the light ground is near
white.
| step | dark | vs #101014 |
vs #1a1b26 |
vs #1e1e2e |
xterm-256 |
|---|---|---|---|---|---|
| rest | none | — | — | — | — |
| cursor | #242932 |
1.30:1 | 1.17:1 | 1.09:1 | 235 |
| selected | #2E3440 |
1.52:1 | 1.37:1 | 1.31:1 | 237 |
| mark | #434C5E |
2.20:1 | 1.98:1 | 1.90:1 | 239 |
| step | light | vs #FFFFFF |
vs #ECEFF4 |
xterm-256 |
|---|---|---|---|---|
| rest | none | — | — | — |
| cursor | #E5E9F0 |
1.22:1 | 1.06:1 | 255 |
| selected | #D8DEE9 |
1.35:1 | 1.17:1 | 254 |
| mark | #B7C0D1 |
1.83:1 | 1.59:1 | 251 |
The aim was cursor at 1.1–1.2:1 and selected at 1.35–1.5:1 — far below the 3:1 any accessibility guideline demands of a UI boundary, and just above the ~1.05:1 where a large flat area stops being perceptible at all. That is deliberate. A selection tint is not a boundary; it is an anchor for something the row already says in text.
The cost of authoring rather than deriving is stated rather than hidden, and it
is exactly the cost a reply pays off: a fixed ground reads one notch louder on a
blacker terminal and one notch quieter on a lighter one, and on a tinted page like
#ECEFF4 the whole light ladder drops close to invisible. That is the price of
not knowing. It is still cheaper than a query that hangs — which is why the query
does not hang.
Two things survived the retune unchanged, both deliberately. #2E3440 is what
this surface has drawn under the pointer since the day it first drew a
background — it moves down one rung to become the selected step, so the row
a person has been looking at for four waves keeps its exact weight while the
pointer's own step gets quieter. And the light ladder's first two steps are
byte-identical to what they were, because they were already inside the band; a
value in band is not touched.
One more line holds the ladder honest below truecolor: all six steps resolve into the xterm-256 grey ramp (235–239, 251–255) rather than its colour cube. A ground that rounded into a hue would be a tint that looked like it meant something, and no step on this ladder means anything by itself.
The palette divides in two, and the division is the reason a surface can carry eight colours and still read calm.
The signal hues answer what kind of thing is this. None of them outranks the others, so none of them may be lighter than the others — the eye reads lightness as figure and ground and hue as identity, so a set of signals at one lightness reads as a single field at a glance and only resolves into colours when somebody looks.
THE SIGNAL HUES SIT INSIDE A FIFTEEN-POINT HSL LIGHTNESS BAND, on both ladders.
TestTheSignalHuesAreIsoluminantholds them to it.
The reading tiers — ink, muted, dim — answer how loudly is this being said. They are a ladder by construction, lightness is the whole of their meaning, and the band deliberately does not govern them. The test excludes them by name rather than by silence.
The body may not be the brightest thing on the screen. The reading tier's top rung sits between 8:1 and 11:1 against the middle of its assumed ground, on both ladders.
TestTheBodyInkDoesNotGlareholds it there.
The signal band governs hue; this governs the one colour a person looks at for minutes at a time. Above about 11:1 a white on a dark terminal stops being legible and starts being a lamp — strokes halate, counters fill in, and every quieter thing beside it reads as switched off.
The dark ink was #D8DEE9 for five waves and measured 12.65:1 against
#1a1b26, half again as bright as the person's own accent at 9.26:1. So the
loudest thing on a surface whose accent budget is one lit element per screen
was the paragraph, and the budget bought nothing because whatever it was spent
on was outshone by the text around it.
| ink | vs #101014 |
vs #1a1b26 |
vs #1e1e2e |
xterm-256 |
|---|---|---|---|---|
#D8DEE9 |
14.05:1 | 12.65:1 | 12.14:1 | 254 |
#C6CDDA |
11.88:1 | 10.70:1 | 10.27:1 | 252 |
The move is the smallest one that fixes it: hue held at 219°, lightness down
L 88.0 → 81.6, saturation eased 28% → 21% — a body white is the one colour here
with no identity to carry, and a tint nobody can name is a tint paid for in
contrast. The 256 neighbour was re-checked, as every change to this table owes:
#C6CDDA lands on 252, claimed by nothing on either ladder. The near miss
is worth naming — the old ink's own 254 is the light ladder's selected ground
and 255 is its cursor step, so an ink that drifted back up a rung would share an
index with furniture.
The ladder still reads as a ladder, which is the other half of the law: ink 10.70, muted 6.67, dim 3.54 against the middle of the range. Coming down far enough to be comfortable without landing on the second voice is the whole width of the move, and the test asserts that gap as well as the ceiling.
The light ladder's ink is untouched at #3B4252 — 10.06:1 against #FFFFFF
and 8.73:1 against nord's #ECEFF4. It was measured against the same band and
was already inside it, and a value in band is not touched. The test walks both
ladders regardless.
And the transcript reaches the ink through a seam. A model's markdown is
rendered by internal/tui2/prose, which resolves colour from
internal/tui2/tokens, whose body tier is brighter still — so the reply was
painted by a palette this surface does not own while every row around it wore
this one. Two whites, one screen, the louder on the thing people read most.
tokens.Styler.WithBodyInk is the fix: the colour authority a caller hands
prose may be asked to say the body tier in the caller's own voice. One Styler,
one answer to which white, and the heading ladder, the code ramp and the
raised plane under an inline span all stay prose's. The v2 surface keeps tokens'
own white — the override travels on the Styler and never touches the table.
| role | before | after | H | S | L | in band? |
|---|---|---|---|---|---|---|
| ink | #D8DEE9 |
#C6CDDA |
219° | 28→21% | 88.0 → 81.6 | reading tier — see THE GLARE LAW |
| accent | #9DC3E6 |
unchanged | 209° | 59% | 75.9 | signal — band top |
| muted | #7FA6C9 |
unchanged | 208° | 41% | 64.3 | reading tier |
| dim | #6B7280 |
unchanged | 220° | 9% | 46.1 | reading tier |
| add | #A3BE8C |
unchanged | 92° | 28% | 64.7 | signal |
| del | #BF616A |
#C67173 |
354→359° | 42→43% | 56.5 → 61.0 | signal — was the outlier |
| bad | #D08770 |
unchanged | 14° | 51% | 62.7 | signal |
| ask | #C08FE8 |
unchanged | 273° | 66% | 73.5 | signal |
| warn | #EBCB8B |
unchanged | 40° | 71% | 73.3 | signal |
| data | — | #91C5D4 (new) |
193° | 43% | 70.0 | signal — the payload rule's datum hue |
| violet | #8F6FA8 |
unchanged | 274° | 25% | 54.7 | shared by both ladders; was the shell operator's until the transcript restraint greyed shell grammar — held in the table, currently unspent |
Signal spread: 19.4 points before, 14.9 after (unchanged by data, which sits mid-band).
Byte-identical throughout — it was measured and it was already in band.
| role | value | H | S | L | in band? |
|---|---|---|---|---|---|
| ink | #3B4252 |
222° | 16% | 27.6 | reading tier |
| accent | #5E81AC |
213° | 32% | 52.2 | signal |
| muted | #8098B8 |
214° | 28% | 61.2 | reading tier |
| dim | #9AA3B2 |
218° | 14% | 65.1 | reading tier |
| add | #7BA23F |
84° | 44% | 44.1 | signal |
| del | #B55B64 |
354° | 38% | 53.3 | signal — band top |
| bad | #C57A3C |
27° | 54% | 50.4 | signal |
| ask | #6F3FA8 |
267° | 46% | 45.3 | signal |
| warn | #A6791F |
40° | 69% | 38.6 | signal — band floor |
| data | #2C8A9E (new) |
191° | 56% | 39.6 | signal — the datum hue, deepened for the page |
Signal spread: 14.7 points (unchanged by data).
#BF616A is nord's own red and it carried a diff's minus lines for four waves.
At L 56.5 it sat a clear five points under everything else in the signal set and
read as a dimmer class of fact than the plus lines beside it — which is not
what a diff means. Neither half of a diff outranks the other.
The move is as small as a move can be: hue and saturation held, lightness alone raised, from L 56.5 to L 61.0. It is recognizably the same red one shade up.
The 256-colour neighbour was re-checked, because that is where an unchecked
change silently becomes a different colour. #C67173 resolves to 167 — a
brick red, one clear step from bad's 173. The obvious alternative, L 62, lands
on 168, which is a pink; a diff whose minus lines went pink on every
256-colour terminal would have been the fallback nobody looked at, again.
Questions use the shared amber status hue.
#C08FE8 (light #6F3FA8) was spent on the moment the agent is waiting for a
person and on nothing else. What was true: it was also spent on every WORD of that
moment — the head, each answer, each key — while home, the places and the chip
said the same "waiting on you" in warn's amber, so the one meaning wore two
colours. What is true now (colour pick C, COLOUR IS STROKE, NEVER FILL): the role
is gone from both ladders, palette.ask paints with warn, and a question's
amber touches its three marks (?, ▸, ◆) and nothing else. The two rows above
keep the old values as history; neither is on any ladder any more.
A place paints from the table above and authors nothing of its own. There is no
second palette, no second ceiling, no second band, and no page ground on any surface —
placeSignalBand, placeGlareCeiling and the place ground ladder are gone from
internal/tui3/designlanguage_test.go, and REST IS NOT A COLOUR is one law again.
| The design's role | What a place asks the palette for | The hue it gets |
|---|---|---|
| tier 1, the subject | p.ink, bold where the design bolds |
hueInk #C6CDDA |
| tier 2, what is true of it | p.muted |
hueMuted #7FA6C9 |
| a note about the subject | p.narr |
hueNarr #848FA6 |
| tier 3, the margin | p.dim |
hueDim #6B7280 |
| amber, needs a human | p.warn / p.ask |
hueWarn #EBCB8B |
| cyan, alive | p.accent |
hueAccent #9DC3E6 |
| green, money | p.money |
hueMoney #90D0AA |
| the page | — | the terminal's own background |
| the band | p.selected |
hueSelected, THE GROUND LADDER's own step |
ONE ACCENT PER MEANING SURVIVES THE REVERSAL, BECAUSE IT IS ARITHMETIC ABOUT HOW MANY MEANINGS A SCREEN CARRIES rather than a claim about which hexes carry them. Amber for waiting on a person, the accent for in flight, and nothing else on a place is in colour. Green is a unit rather than a signal. Every hierarchy step past the greys is made with bold, case, indent or air.
So placeRampFrom retires three of the conversation's roles and re-points nothing
else: the question's violet onto the amber (home said one thing in two colours — and
since 2026-09-11 the conversation says it in amber too, so that re-point is gone), the
payload cyan onto the body ink (a datum lifts by being the subject), and the finished
tick's olive onto the second voice (the ✓ already says it landed, and the one green
on a place is money). It takes the ramp the palette is ALREADY HOLDING, so adaptive.go's
measured derivation reaches the places exactly as it reaches a transcript.
What the fidelity wave landed that was never a colour all stays: the ? and ◐ marks,
the design's feet word for word, the bold subject in the tab bar, p.money on every
figure in dollars, and the four laws about the plain floor, private-use glyphs, the
marks and italics.
One lit element per screen.
The accent is the loudest thing the palette can say, and its whole worth is that a person's eye goes to it without being asked. That is a budget, not a colour. Spend it twice and it buys nothing.
So the accent marks the one live or chosen thing and nothing else. Headings
are not that: a heading is furniture, it sits in the same place every time, and a
column of lit headings is a screen with no answer to "where am I". Headings, band
labels and wordmarks wear muted — the same hue one step back, which reads as
structure rather than as a summons. The person's own › glyph and the rail keep
the accent, because that is where the eye starts and where the work is.
No test can hold this. A screen is composed at fifty call sites and "how many lit
things does this frame have" is not a question a unit test can ask. It is held by
review, and by being written down in two places on purpose — here and at the head
of styles.go.
A line may be quiet; the fact it carries may not be.
The accent budget above says what a screen may LIGHT. This says what a quiet line owes the person reading it, and the two are the same argument from opposite ends.
This surface says a great deal on its own account — a note, a hint, a legend, an
announcement — and every word of it was written in the reading tiers, because
none of it is the conversation. That is right about the LINE and it was wrong
about what the line is for. checker ⌖ moonshotai/kimi-k3 · every task until you unpin it · you are still talking to deepseek-v4-flash was one flat dim run
from end to end: the words a person already knew, and the two model ids they
typed the command to learn, at exactly the same weight. The sentence was legible and the ANSWER
inside it was not.
So the prose of an informational line stays where it is, and each load-bearing datum inside it steps up one role.
| what | role |
|---|---|
| the prose | dim, or whatever quiet tier the surface already used |
| a datum | data (#91C5D4 dark / #2C8A9E light) — a model id, a figure, a count, a name, a key chord |
| something typeable | the chip a slash command already wears |
| — | never accent |
Why a hue and not a rung. The rule's first draft lifted a datum to ink,
and it failed in the field the day it shipped: ink is the body's colour, so a
"lifted" model id two rows under a paragraph of body text read as ordinary
prose — the exact defect the rule was written against, arriving one rung later.
The eye reads lightness as loudness and hue as identity, and a datum is a
different kind of thing, not a louder one. So data wear a hue of their own —
the syntax-highlighting contract every calm terminal theme keeps (greyscale for
prose, colour for identifiers) — sitting inside the fifteen-point signal band so
a line full of data still reads as one quiet field until somebody looks.
#91C5D4 resolves to xterm-256 116, one clear step from the accent's 146;
#2C8A9E to 31, colliding with nothing on the light ladder. The dark value
was nord's own #88C0D0 for four waves and it did collide: #88C0D0 rounds
to 110, which is muted's index and a steel blue rather than a cyan, so on
every 256-colour terminal the datum wore the second voice's own colour. Only the
lightness moved (L 67.5 → 70.0, hue and saturation held), and
TestNoTwoRolesShareA256Index now walks the whole table on both ladders so the
next one fails instead of shipping.
The key–value legends (a card's enter open · ctrl+t new chat here, the foot
hints, the task card's keys) are written in the hint grammar and painted by the
same mechanism, so every key/verb pair splits the same way everywhere.
The chip is the slash command's mark and is not lent out. A key chord is
typeable too, and the obvious move was to give the chords in the legend and the
key sheet the same lifted run of cells. They do not get it. A chip has meant
exactly one thing on this surface since it existed — this word is a command
this surface runs — and a second kind of thing wearing it is a mark that has to
be read twice to learn which one it is. A chord steps to data instead, which
is the same one-step move every other datum makes and costs the budget nothing.
Never the accent, and the budget is why. A note appears, is read, and scrolls away. The accent marks the one live or chosen thing on a screen; a passing line is not it, and a surface that spent the accent on every fact it mentioned would have spent the budget forty times a minute.
Strategy over decoration. ONE LINE CARRIES ONE OR TWO DATA. If everything in
a line is bright then nothing in it is, and a rule that lifted every noun would
have bought back the flat line it started from. So the hint slot lifts the key
and never the verb beside it; /status lifts the figure and never its label;
the /crew panel lifts the model ids and leaves the seat words, auto · now
and the words the person has just typed where they were — those are the
question, and the ids are the answer.
A datum is named, never guessed. internal/tui3/payload.go holds the whole
mechanism: a note's builder names the words that are the answer, in the order
they appear, and the painter finds them on word boundaries. Nothing recognizes a
shape, so a line whose builder says nothing is drawn exactly as it was before
the rule existed. The one exception is the legend's hint slot, which is written
in a grammar tight enough to read — chord verb · chord verb, spelled the same
way at twenty-five call sites — and is therefore read rather than made to carry
a list of its own keys at each of them.
Alternating row backgrounds are banned. A striped list is a list that has to be read to learn nothing: the stripes carry no information, they are simply noise arranged regularly enough that the eye stops noticing it, and they fight every ground the ladder above draws.
Where a long list needs help being scanned, the answer is a fade with depth — rows nearest the top at full ink, rows further down receding toward the background. That is the same mechanism the thinking window already uses for its three lines of live reasoning, generalised: a gradient tells you which end is which, where a stripe tells you only that rows exist.
Restated from the existing law, because it is the one most easily lost.
Saturated colour on this surface touches glyph strokes and single characters.
The ground is enormous and quiet; the colour is hairline. A task's identity hue
paints exactly one cell — the glyph at the head of its row — and the title beside
it keeps the ordinary ink. A needs-you row's amber is on the ? and not on the
sentence.
A QUESTION WEARS AMBER ON ITS MARKS AND NOWHERE ELSE. Previously the conversation painted a question's head, every
answer label, every key and every separator in a violet of its own (#C08FE8)
while home and the places said the same "waiting on you" in amber — two hues for
one meaning, and whole rows in a status colour, which is this law broken on the
one object a person must act on. Now the question's three marks — ? the
question, ▸ the pointer, ◆ the recommended answer — are amber, the home
page's question hue, in chat, in the question's page, on home, in the task room
and on the status line alike; every word is ordinary ink, every aside dim, the
frame's edge dim, and the focused row sits on the selected ground. The violet
is retired: palette.ask paints with the amber, and no row is ever painted in
it (a law test walks the question renderers' paints).
The glyph carries the hue; the text stays calm. A whole row in a status colour is a row that shouts, and a screen of shouting rows is a screen with no priority at all. The grounds in THE GROUND LADDER are the sole exception, and they are exactly as quiet as the table above says.
Adopted with #1071 (merged 2026-09-21): the current model is always bold in the data hue on
home and conversation seams. It must be easy to find without hovering or first
changing it. The provider beside it and the surrounding telemetry keep their
existing weight; the emphasis is confined to the model name.
The effort follows a colon without a space or badge: model (provider):effort. Its
styling stays separate from the model. The conversation seam carries no git branch. Home starts
with those controls and puts project: <path> at the far right. Project paths on
home preserve their absolute root (or ~/ under the home directory) and truncate
at the right end, never at the left.
One token wears one visible mark.
A mark must say one thing without borrowing emphasis from another channel. An inline code span normally wears the raised plane; when that span is a linkable path, it drops the plane and keeps the underline that says it is a location. The OSC 8 hyperlink stays because it occupies no cells and is not a visible mark. Non-path inline code keeps its plane and never gains an underline, while fenced code blocks are unchanged.
Stacking plane, underline and brighter ink made plumbing paths louder than the facts around them. The rule keeps both meanings legible while refusing to turn their overlap into extra emphasis.
Also restated, because it is the emptiness law seen from the design side.
A thing with nothing to say occupies zero pixels — not a greyed-out
placeholder, not 0 tok, not $0.00, not an empty heading. Unknown renders as
nothing. A status element appears because it has something to report, and its
appearing is the report.
And when it does appear, it appears as a shape before a number. A spinner
says a call is running; the elapsed seconds are for when you want them. The one
deliberate exception is the live status line, which keeps $0.00 so its segments
do not jump sideways as they update.
Where a label does hold ground over nothing, it teaches. Home's panels are
the one place on this surface that keep a heading with no rows under it — a map
that redraws itself is not a map — and each carries one dim line naming what
arrives there and the one thing that puts it there. That line says what the region
is FOR; it never says that the region is empty. no tasks yet and every
sentence like it are the emptiness law inverted into words and they stay
banned. The exception is the label, not the announcement.
The first movement key lands the cursor in the region the layout itself declares primary.
A screen with several regions has already said which one is the main thing, before a person touches a key: it said it with width, with density, and with where on the frame it put each region. Focus is the fourth signal, and its only job is to agree with the other three. A layout that says this is the main thing with three hundred cells and but start over here with the one cursor on it has spent its whole hierarchy arguing with itself, and a person cannot obey both.
The landing is fixed, and never a function of machine state. Whatever the
flanks happen to hold this morning, the first key goes to the same place — the
whole return on a landing is that it becomes a habit, and a landing that moves
with what is running is one nobody can learn. This is the rule that bites: home
used to wake in needs you when something was waiting and in the list when
nothing was, which is two screens wearing one set of keys.
The flanks are reached by pointing at them, or by a named key. ← and →
follow the geography, and a key with a semantic name — tab next zone — enters
the region it is named after. Neither is diminished by not being the default;
what a default buys is one less decision on the way in, and it can only be spent
once.
Reading order is not an argument. A line list built flank-first is an implementation detail of how the screen was assembled, and letting it choose where focus wakes is the assembly order leaking through the design.
Whitespace is this surface's border system. It is not leftover room and it is not a kindness each block adds for itself: it says which words belong together, where one voice ends, and where a section begins. The best existing screens already use a short ladder; this names it so a fifty-first call site cannot invent a fifth distance.
| step | value | what it means |
|---|---|---|
| clause | 0 blank rows | parts of one sentence stay on one line, divided by · |
| block | 1 blank row | related blocks, bands in one card, or machinery within one speaker |
| boundary | 1 blank row, or a hairline with 1 row of clearance | a person's next turn, a section change, or the seam above a foot |
| breath | 2 blank rows | only the tall-window breathing room above the composer; it steps down to one and then zero as height is taken away |
A GAP ASKED FOR TWICE IS STILL ONE GAP. The conversation compositor owns its block rows for exactly this reason, and every screen builder joining optional blocks owes the same idempotence. Blank rows do not open or close a block. Two consecutive blank rows are not emphasis; outside the named breathing rung they are drift. On phone pages, two empty rows may belong to a three-row tap target; those rows are control geometry rather than separation and must share the control's hit target.
The horizontal ladder is just as small. The conversation's machinery and every surface note have a 2-cell lead; the closed work rail also spends 2 cells, one of air and one on its handle. Home's panes and its three-column bridge use a 4-cell gutter, padded on every row so alignment, not a border, makes the edge. Sheet rows may use the established 4-cell hanging indent when a second line belongs under a labelled first line. The message box's 1-cell inset is input geometry, not a new content margin.
A NEW DISTANCE NEEDS A NEW RELATIONSHIP. Reuse the nearest named step when the relationship is the same. A new value may be added only here and as a named constant beside the shared geometry it governs, with the screen and width where the existing ladder fails. A lone call site does not get to mint a rung.
An audit is only worth as much as the parts of it you decline.
Borders. Omarchy's surfaces are separated by hairline borders and small equal gaps — a subdivided plane. codeaf is one continuous surface: zones are made of typography, alignment and at most a hairline rule. A border is a claim that two regions are different kinds of thing, and on a conversation surface they mostly are not. This is also why the emphasis law forbids adding a ring: rings are borders arriving one row at a time.
a question
hangs above the box in a frame. What was true: no border anywhere, with three
dim rounded boxes that shipped anyway as argued exceptions (the /folder
chooser, the ctrl+k switcher card, the onboarding panel), each with its own
copy of the corner pieces. What is true now: a question IS a different kind of
thing from the conversation around it — it is the one object on the surface
that is waiting for the person, it is answered and then gone, and borderless it
read as more transcript with the answers somewhere inside it. So a question
hangs below the latest submitted question as ONE framed object: a rounded, dim edge; its title in
the top edge and a right-aligned aside beside it; its keys in the bottom edge;
and on a terminal refused box drawing, two plain rules and no sides. There is
exactly one frame on this surface (internal/tui3/frame.go), its pieces are
vocabulary slots like every other mark, and the three older boxes are drawn by
it. Everything else is still one continuous surface: a frame is for an object
that asks, or a sheet raised over the page on purpose, and never for a zone.
Icon-only minimalism. Their status bar renders four glyphs and no digits, and the most consistent complaint about it — from people who otherwise admire the work — is that it reads as empty, and that you cannot tell an unfamiliar glyph from a decorative one. That is the best-substantiated criticism in the whole audit and we take it seriously. Every mark on this surface has a word near it: the mark says it fast, the word says it certainly.
Keyboard speed at discoverability's expense. This is the important one, and it is a straight refusal rather than a trade we tuned differently.
Omarchy accepts a steep learning curve as the price of keyboard speed — "It's OK that it'll take longer to learn. It's OK to have a manual." That works for a system whose users chose it precisely for that. codeaf is a new interaction paradigm, and nobody has intuitions about it yet. A person cannot be expected to learn what they cannot see.
So: the keyboard stays first-class, and every chord keeps a visible, clickable, self-teaching door beside it. If a key does something, a person who never pressed it can still find that something, click it, and learn the key from what they clicked. There is no gesture on this surface whose only documentation is documentation.
This language was written after a close audit of basecamp/omarchy — its configuration tree, its nineteen shipped themes, the explicit design-token file its 4.x rewrite added, and the public reception of both. Where a number here matches a number there, that is not coincidence and it is not hidden.
What we took is the arithmetic and the reasoning: that a selection tint at 1.15:1 is enough when the row already says what it is; that a wide spread of hues at one lightness reads calm while the same hues at scattered lightnesses flicker; that a state ladder should be four named steps rather than an ad-hoc set; that emphasis is done by raising the ground rather than by adding an outline; that consistency is best enforced by refusing to let anything else own a value.
What we did not take is any value transplanted by analogy. Their ladder is alpha over a background they know; ours is authored, because we do not know ours — and every number in the tables above was measured against codeaf's own assumed grounds, codeaf's own ink, and codeaf's own xterm-256 fallback, which is a rung their compositor does not have. Two of our steps ended up on values this file already held, one hue moved five points, and everything else stayed where it was. A design language you adopt wholesale is a costume; the parts worth keeping are the ones that survive being re-derived in your own material.
The running-logo studies occupy a reserved nine-column slot immediately below
the latest submitted question, before the answer and tool details. A two-column
gap precedes a dim two- or three-word developer caption that changes every ten seconds. Its 28-column slot
keeps any later content aligned. A slow decoding ripple changes one letter
at a time into symbols before restoring it, with readable pauses between passes. The row scrolls with the question and
disappears on completion; it adds nothing to the input chrome. Chevrons and a
gold dot use native glyphs. Accessible and small-window views retain text.
The shared geometry is implemented in internal/tui3.