Kin / How Kin works
The Graphite+ identity
The design language behind the kin interface: a terminal-owned ground with cool Graphite+ structure and colour held in reserve for signal. This page explains the two-channel colour split and the palette behind it.
Read as MarkdownThe design language behind the kin interface: a terminal-owned ground with
cool Graphite+ structure and colour held in reserve for signal. This page
explains the two-channel colour split and the palette behind it.
Cyan voice, periwinkle thinking
One idea organizes the whole interface: kin’s live voice is cyan, its thinking is periwinkle.
Streamed output and the assistant speak in a clean cyan — the live signal. The reasoning trace is a distinct, recessive periwinkle — the private thinking channel. They are two cool hues, deliberately close in value but clearly apart from each other, so you can tell at a glance whether you are reading kin’s answer or watching it reason. The split is the point: a single colour for “kin is talking to you” and a quieter colour for “kin is working it out.”
Those two roles carry through kin’s own chrome — but they stop at the code band. Code in the transcript is content kin is showing you, not kin speaking, so fenced blocks are real syntax highlighting (a Pygments lexer per language) with the conventional editor cues readers already parse: muted-green strings, bone functions, periwinkle types. The signal hues are held back — cyan appears only on keywords, amber only on numbers — so code reads like an editor, not like the brand.
The markdown body
Everything kin says renders through one markdown identity (the KinMarkdown
widget), pinned to the role palette identically on both themes rather than
inherited from the toolkit’s defaults. Headings descend a quiet monotone ramp
— #/## in the cyan voice (h1 additionally underlined, left-aligned like
everything else in the transcript), ### in bone, deeper levels in receding
greys — all bold, so hierarchy reads by colour temperature instead of style
zigzags. Blockquotes sit flat on the ground behind a dim steel rail (the same
rail idiom tools and reasoning use — no lifted band). Tables rule themselves
with the deep-graphite RULE line and keep their headers off the chroma.
List bullets cycle a small quiet set (• ◦ ▪) from the glyph vocabulary
and stay grey — structure, not signal. Inline code and fenced code share the
same slightly-lifted code band; inline code stays in body bone, because the
band alone says “code” — a backticked git push never borrows the amber that
elsewhere means attention.
The palette
Colours are named by role, not by hue, so a re-tint is a one-place edit. The role names below are the constants defined in theme.py.
| Role | Hue | Hex | Used for |
|---|---|---|---|
GROUND |
graphite | #101114 |
Opaque fallback, code bands, and lifted structure |
FG |
bone | #E3E3E6 |
Body text |
PRIMARY |
cyan | #5FBFD6 |
The live signal: kin speaking, the spinner, accents, the wordmark |
REASON |
periwinkle | #9E97D2 |
The private reasoning / thinking channel |
SUCCESS |
green | #5FC198 |
A tool succeeded |
WARNING |
amber | #D6A855 |
Warnings — distinct enough from cyan that the two never read alike |
ERROR |
coral | #E5786E |
Errors and failures |
CODE_STRING |
muted green | #83B692 |
String literals inside the code band — the one content-only hue, never chrome |
RULE |
deep graphite | #2A2E36 |
Rules and the low-contrast scrollbar thumb |
These constants are the single place the palette is named. The Textual theme wires them into the UI’s CSS tokens, and the transcript widgets import the same constants directly for Rich text styles (Rich cannot read a CSS variable) — so a styled CSS rule and a hand-built renderable always land on the same colour.
Terminal ground
The default kin theme does not paint the main canvas. It inherits your
terminal’s own background, including translucency, while keeping Graphite+
truecolour accents and intentional opaque surfaces such as code bands, diff
washes, menus, and modal cards. This is a hybrid theme, not a retreat to the
terminal’s ANSI palette: cyan still means live voice, periwinkle still means
thinking, status colours keep their roles, and the composer cursor remains an
opaque cyan block so the insertion point stays clear on the inherited ground.
The current contract is dark terminals. If your terminal palette is light, or
you need a deterministic opaque canvas, run /theme kin-graphite. /theme
cycles between the transparent kin theme and that opaque fallback; the choice
is saved for future sessions. Any registered Textual theme can still be chosen
by name as an explicit escape hatch. Cursor and search-result tints follow that
selected theme’s primary colour; only transparent kin uses fixed Graphite+
preblends, because Textual cannot alpha-blend those cues against a
terminal-owned background.
Measured palette contrast
The recessive greys and the shimmer base are tuned for WCAG against the opaque
GROUND reference (#101114), not chosen by eye — the ratios are measured
with the standard relative-luminance formula and live in theme.py next to
each constant. Body-text grey (GREY_LO, reasoning tails / diff metadata /
pygments comments) clears AA body at 4.65:1; the shimmer base
(PRIMARY_DIM, the wordmark hairline + cosine-sweep rest colour) clears AA
Large at 3.21:1 while keeping a ~5.7-point delta below PRIMARY so the
sweep stays visible. A terminal-owned background can change those measured
ratios; use the kin-graphite fallback when guaranteed palette contrast
matters.
Glyph vocabulary
Alongside the palette, theme.py owns the functional glyph vocabulary — the full set of disclosure, connector, and chrome glyphs (G_RUNNING, G_CARET_*, G_ELBOW, G_BULLET, G_OK, G_ERR, G_AGENT, G_RETRY, G_PAUSE, G_TODO_*, METER_*, SPINNER_STYLES, …). These are functional — a status, a connector, a spinner frame — not skin, so they’re shared semantics with kin’s Go TUI and they live ONLY in theme.py. A literal glyph anywhere else in kin.tui is an audit-fail: a future re-tint of G_BULLET would otherwise leave stranded literals off-identity. Widgets and renderers import these constants; the same invariant covers palette colours and glyph vocabulary.
Spinner vibes
The busy spinner is choosable by vibe — a small curated set of named styles in theme.py’s SPINNER_STYLES, each a SpinnerStyle (constant cell-width frames + cadence + a one-word descriptor; widths may differ between styles — rain is two cells). Pick one interactively with /spinner or set the spinner key / KIN_SPINNER env directly; resolve_spinner falls back to the default on unknown names and forces the ASCII-safe line style under a truthy KIN_ASCII.
| name | vibe | frames | cadence |
|---|---|---|---|
rain (default) |
matrix | ⠁⡄ ⠋⡄ ⠏⡀ ⠟⡈ … — drips falling down two braille cells, each drop randomly skipping beats and varying its tail (120 deterministic pre-baked frames, seamless ~9.6s loop) |
0.08s |
braille |
classic | ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ |
0.10s |
arc |
orbital | ◜ ◠ ◝ ◞ ◡ ◟ |
0.12s |
toggle |
metronome | ⊶ ⊷ |
0.35s |
line |
plain | `- \ | /` |
Exactly one frame-stepped spinner position exists in the chrome — the status bar’s busy glyph — plus its one transcript mirror, a running workflow card; every other live surface expresses liveness by colour only (the shimmer and pulse). That restraint is the “one heartbeat, colour elsewhere” invariant (POLISH r11) in REFERENCE.md.
Why calm
The restraint is deliberate. kin reads as a precision tool, not a dashboard:
a quiet terminal field where colour means something every time it appears. The
screen, transcript, and collapsibles share one inherited ground, while opaque
Graphite+ surfaces are reserved for content that benefits from separation.
Nothing competes for attention until it carries signal. That is why the live
voice and the thinking channel get the only two saturated cool hues, and why
status colours (green, amber, coral) stay reserved for the moments they
actually report. Colour is information here, so it is spent sparingly.
Scrollbars follow the same restraint: they use a one-cell transparent trough and a translucent deep-graphite thumb, gaining a little presence on hover and rising to dim cyan only while actively dragged. The resting thumb stays close to the ground, leaving scrollback locatable without adding a bright rail to the edge of the conversation. The transcript keeps its left reading inset while the scrollbar occupies the far-right cell.