← Design notes

Style Guide

Every part of the live Chat UI — its name and its current design, kept in sync with lib/room-ui.html on every ship. When we say “the egg”, “the join capsule”, “the workhorse row”, we mean the same thing; each card renders the real component. Use the theme toggle for both palettes (light · dark — the app has no e-ink). The folded changelog lives in The drawer section. Version history is not kept here — it lives in git (git log -- lib/room-ui.html) and on the design pages each card links.

House rule worth restating here: colour is the seat — a persona has no intrinsic colour; each voice is handed a per-room seat colour (rotated by room), so the same persona may wear different colours in different rooms while a cast always reads distinct. Grey is system (the egg, telemetry, the leave capsule, your own bubble): if a thing is grey, it isn't a character.
And the two rules under every page. Every block is standard unless it is named special — the vocabulary is the block catalog, one page carrying every object the app is built from with the name we call it, generated from the app's own stylesheet so a specimen cannot drift. And a right-pane page is 600 wide, centred — see The column.

Foundations the shared layer — tokens · scales · reusable modules

Palette, fonts, the scale tokens, and every once-defined component live in lib/theme.css, linked before each page's own styles with a ?v= buster (bumped alongside BUILD whenever the file changes). Anything marked ♺ theme.css is a reusable module — defined once, consumed everywhere; page CSS adds only page-specific tokens and z-ladders. This guide mirrors the values inline so it renders standalone.

Radius

--radius-s
8px · chips · inputs
--radius-m
10px · buttons · cards
--radius-l
12px · popovers
--radius-xl
16px · dialog card
--radius-pill
999px · pills

Space · avatar · shadow

--space-1…54·8·12·16·24px
--av-xs…xl24·30·40·64·84px
--shadow-1
--shadow-2
--shadow-3

Seat palette ♺ theme.css

Colour is a per-room seat, not a persona's identity — the room rotates this 8-slot palette (OKLCH-spaced) so a cast always reads distinct, and the same figure can wear a different colour in another room. Slot 0 is coral; each has a -soft tint for backgrounds. Grey (--egg) is system.

seat-0
seat-1
seat-2
seat-3
seat-4
seat-5
seat-6
seat-7

Reusable modules ♺ theme.css

The components below are defined once in the shared layer; a card elsewhere in this guide carries the badge when it renders one of them.

#moduleclassesused by
#22dialog close / back.dialog-x · .dialog-backevery dialog · sheet · drawer
#22bdismissal grammar v516–517.modal-head.hbar · .dialog-x.task · .dialog-back.corner · .dialog-commit · .hide-narrowphone-only, opt-in — see the four types
#23search chrome.search-box · .search-x · .ps-suggestall three persona search boxes
#25dialog scaffold.modal-scrim/.modal · .vp · .mep — the 580 card + the .40 scrimmain & sub dialogs · the Me pane · profile · survey
#26buttons.btn-ghost · .btn-primary · the coral CTA baseevery dialog action · Call pill · Post · Done
#28pill family.lib-mine (the spec other pills follow)library filters · toggle chips
#29popover engine.popmenu · .rowmenu · .cardmenu⋮ menus · model switcher · quick actions
#30toast.toast (+ the --toast-* tokens)every transient notice
palette · fonts · scales · seatsthe token blocks abovethe whole app + the embedded Studio iframe

Rows the seven names — the owner’s, fixed 2026-08-12 · canon: row unification

Every list in the app is built from seven rows, ordered by name — the numbers all come from the twelve --row-* tokens (inset 16 · gap 12 · title 15 · sub 11.5 · lead 40/26, chat avatar 46 · heights 60/72), and six smoketest lints keep a row from declaring its own pixels. Everything except the chat row (a component) and the text row (the field family) is ONE class wearing different leads and trails.
chat row「chat rows」
DDa Vinci-ishYesterdayLeonardo · with Bobby, mary
a conversation — title, time, unread, members. Always ordered as the COMPONENT (roomRowEl): search results, Shared chats and the forward picker look right for free. Avatar 46, semi-bold title (owner-locked).
contact row「contact rows」
DCharles DarwinNaturalist
a person or persona in the Friends list — face, name, role (local name beside it when it differs).
drawer row「drawer rows」
Notifications3
opens a page — the › is the promise. Coloured tile on the landings (Vibes · Studio · Notes · Settings…), bare icon inside Settings (Privacy & friends · Archived chats). Admin console wears ↗ instead: it leaves the app.
toggle row「toggle rows」
Let people find meOff by default.
a setting you flip in place — a switch (Privacy) or a segmented control (Appearance · Text size · Enter to send). One class everywhere since v785: the Privacy switches ride the workhorse too.
dropdown row「dropdown rows」
Default languageEnglish ▾
a setting you pick from a list — the dropdown sits in the trail.
text row「text rows」
New password••••••
a label over a box you type in (New password · Invite code) — the workhorse in its text dress since v785 (the box on its own line, v768). The old field family survives only in Edit profile. Grouping carries meaning: one question per card-gap.
entry row「entry rows」
Da Vinci-ishLeonardo · 1 Jul 2026RestorePurge
a stored thing with its own verbs — Archived chats · Deleted chats · Deleted Seen · Deleted persona. Not a door: the buttons act, the row does not.

The screen layout regions, wide view

chats pane persistent left column (wide)
full-screen root (narrow)
‹ back Title
the stream
topbar parts: back button · chat title + chevron · infinity button (becomes while the info screen is open)  ·  bottom = composer. The bar + composer are frosted glass — the stream blurs as it scrolls beneath them.

The column v743/745 · a DEFAULT since v818 — three lints hold it

The right pane is ~900 wide; anything you read or fill in is 600, centred. Chrome (a header, a boundary line) spans the pane — ⚠ and you cap the content, never the scroller, so the scrollbar stays at the pane's far right. It is the DEFAULT: a page opts out, never in. Special by name, and these are the only two: the chat (790, its contents capped inside it) and the Vibes feed.
⚠⚠ A cap can apply and still do nothing (v960). The column works by making a page's body a grid with one 600px track — so if anything later in the sheet leaves that body display:block, grid-template-columns computes correctly, reads correctly in devtools, and has no effect at all. That is exactly what happened to the three account pages (Edit profile · Change password · New invite code): measured at 1280, body 917 and rows 897 for however many versions. When a column「is not applied」, check the box is still the display type the cap needs before you touch the cap. The third lint holds that pairing — and it exists because the first two only ever walked class="drw" pages, while these are the shared modal scrim wearing .as-page, so nothing could see them drift.

Topbar & chrome

back button v894
aka: back · .drw-back
One ‹ in the app, never re-drawn locally: a 27px chevron in --ink, centred in a 46px round hit area, inset 2px from the pane edge — the same button in every sub-page head and the chat topbar. In the chat it slides the window back to the full-screen chats list. ⚠ 「Inset 2px」is the whole spec — a page breaks it by re-stating the head's padding beside the button, not by re-drawing it. Glyph centre 25px, on every sub-page.
‹ Back peels one layer
Back gesture
aka: hardware Back · iOS edge-swipe · Cancel
Hardware Back, the iOS edge-swipe and Esc peel the topmost open layer first — menu → sheet → drawer — then (narrow) step the open chat back to the list, then exit; a cold relaunch lands on the chats list. Android also gets a drawn left-edge swipe (24px edge, 70px travel). A retract fires only on our own history stamp, so Back can never navigate out of the app (v734, linted).
‹ Notes · Chats · Studio · Vibes ›
swipe between tabs
aka: tab swipe
Narrow only. A horizontal swipe on a root’s top level steps between the four roots — Ink · Chats · Friends · Me (v1039; Discover retired at v1038) — the neighbouring pane sliding in from the swipe direction, hard stops at the ends. Off inside any sub-page (no pager teleports you out of a page you navigated into); vertical scrolls and the edge Back-swipe are left alone.
Three flats — which appreciates?
chat title + chevron
aka: the title, the chevron
Centred in the topbar. Tapping it grows the title bar into a full-screen chat-info screen (Telegram-style) — the title flies down + grows into the hero, and this small title hides while open.
Three flats — which appreciates?
5 personas · 2 people · DeepSeek v4 Pro
CostPeak ×2$0.480
Current Cache 41.8k tok
Д
+
chat-info screen
aka: the info screen, the grown title bar
Grows out of the title bar into a full-screen sheet: a hero — the room’s 56px tile, the title with an inline-rename , the N personas · N people · model line — then Cost + the cache bar, the model switcher, vividness + floor rows, the members grid and people list. Scrolling collapses it; ‹ or tapping the title flies it back. Cost carries a rate badgePeak ×2 filled, Off-peak hairline — where the model prices by the clock.
infinity button
aka: the , the wildcard button  (was the “meatball” ⋯)
Top-right, borderless. Opens the wildcard drawer from the right, which collects artifacts the panel emits. A red dot appears when a new artifact lands while the drawer is closed. While the chat-info screen is open it's replaced in place by a (more — placeholder for now).
wildcard
DOCRevised proposalcopy
from Li

Three flats, one buyer — here's the revised split…

wildcard drawer
aka: the ∞ drawer, the artifact pane
Right-side drawer opened by the infinity button. Collects every artifact a host hands over — the seven kinds — as cards, plus this chat’s saved lines; an All · Artifacts · ✦ Saved filter is the only control (the global Notebook is Me → Notes). Mutually exclusive with the confer drawer.
chats
chat window
chats pane
aka: “your chats”, the conversations list
All your conversations — a persistent left column on wide, the full-screen root on narrow. It rides the base app surface and reads as home: acting on a row keeps you on the list. On wide the column resizes by its right edge (363–545px, double-click resets), remembered per user, restored before first paint.

Chats pane rows · the long-press menu · the stores

Three Shanghai flats — which appreciates?
14:32
Li · Doronin
with Dan, Mary
Where to retire on $1.2M?
Yesterday
Xiao Pang
chat row
aka: a chat item, a conversation row
A 46px avatar leads; the same room tile follows the room — 22px beside the chat title, 56px in the info hero. A group’s disc blends the room’s seat colours (humans contribute grey); a 1:1 keeps its counterpart’s face. Semi-bold 15/600 title, the panel named in seat colours, the unread dot on the avatar’s corner, WeChat relative time at the right. Opening is instant. Canon: chat-list avatars.
row menu (long press)
aka: the chat-row menu · v716 (the per-row ⋮ retired)
A 480ms hold (right-click on desktop) on a chat row, anchored to the press: Rename · Fork · Mute · Archive · Delete (red). Archive sets a chat aside; Delete sends it to Deleted chats — both reversible. It rides the row builder, so Shared chats and the search results inherit it unchanged.
Fork
aka: clone, branch a chat
A ⋮ menu item. Clones the chat to “<name> (fork)”, carrying its model, language, cast, and the whole conversation — then opens the copy. The original is untouched; the two diverge from here.
Rename
aka: retitle from the list
A ⋮ menu item — retitle any chat from the list (not just the open one). Prefills the current title; saving updates the chats list and, if it's the open chat, the topbar too. (The ✎ pencil on the chat-info hero does the same for the open chat.)
My contents (the stores)
aka: Archived chats · Deleted chats · Deleted Seen · Deleted persona
The things you MADE, put away — their own Settings group (v758), each row a door to its own as-page store: one bucket per door, named for what it holds. Store rows are entry rows — the buttons act, the row does not.
Old draft — physics tangent
Einstein · Mary · 1 turn · 25m ago
RestoreDelete
Archived chats
aka: the archive, set-aside chats
Chats you set aside. Each entry row: Restore (back to the list) or Delete (on to Deleted chats). Reached from Settings → My contents.
Scratch chat
Shane · 1 turn · 2d ago
RestoreDelete forever
Deleted chats
aka: the trash
Deleted chats wait here, still recoverable — Restore brings one back; Delete forever (red, confirmed) is the only permanent erase, and Empty trash clears the bucket.
Einstein vs Bohr — the dice
Einstein · Bohr
conferring dot
aka: the busy dot, the background-turn pulse
A soft grey pulse on a chat row whose panel is still conferring while you're looking at a different chat — it clears when that turn lands. System grey, like the egg: it belongs to no character.
live v1002 · Nerd becomes Geek; a chat is a chat, not a room
build badge
aka: the build tag, the version string
At the foot of Me, under the ✦ -ish brand (v754 — it left the chats-pane foot): the running live vNN string with a one-line note on the latest ship. Bumped on every UI deploy — the quickest check that a new build actually reached this browser.

The drawer v689 — the app's ONE sub-page shape; there are no dialogs

On wide, a sub-page is the root’s right pane: it appears rather than travels (no slide), the first page wears no header (a ‹ back to a place you never left would lie), a left-pane press replaces it, and entering a chat collapses everything (v730). Nothing animates across a root swap.

Stats
Notes
Notifications
Settings
the drawer · items · item groups
replaces: the profile dialog · the persona-browser modal · the pane-view sub-pages
Every sub-page is a drawer — full screen on narrow, sliding in from the right under the one shared (every dismissible layer registers for the Back gesture in one loop); the root’s right pane on wide. Headers sit on the page background (v691); Me has none. Item groups: an inset hairline inside a group, a 14px page break between groups (--row-group-gap), no kicker labels.

Root tab bar v679 → v1038 — four roots at the foot of the pane: Ink · Chats · Friends · Me (Discover retired: Studio → Friends, Seen → Me)

the root tab bar
replaces: the header tab strip (Notes · Chats · Studio · Vibes) · the ≡ hamburger · the “Call the panel” pill
Four roots — Ink (the ink-drop glyph, first) · Chats · Friends · Me — 58px, at the foot of the left pane — never the window. Active coral, resting muted; the glyph carries the badge, never the label: Chats unread · Friends waiting requests + the Studio's cooking/fresh dot (since v1038, the row having moved there) · Me unread notices · Ink none yet. A root registers no Back layer — Back at a root leaves the app, like every messenger.
Me — hero · Notes · Notifications · Stats · Settings
the Me root
aka: Me · replaces the ≡ Me sheet (v684)
A pane view: hero (name · -ish ID | role · one-liner), then Notes · Notifications · Stats · Settings · Admin console as drawer rows, the ✦ -ish brand + build tag at the foot. Settings holds Preferences · Privacy & friends · Account · My contents. No header — the hero names the page.
Chats — 23px/600, left-aligned
the root title · the two chrome numbers
aka: --header-h 58 · --footer-h 68
Each root names itself in the pane head — left-aligned, 23px/600, WhatsApp’s page-heading treatment. Two numbers run the chrome: --header-h 58 (every head bar and topbar) and --footer-h 68 (tab bar = the composer’s floor) — the equalities hold by construction, not by hand.
( search… )  ⊕
the head pair (magnifier · ⊕)
aka: v704 — WeChat’s header; the ✎ compose glyph retired
To the right of the title on all three list roots: a magnifier and a — one button each, only the ACTION following the root (⊕ = new chat · Add someone · Persona studio). Me carries neither. Same header at every width (v937) — wide used to trade the title for a standing field; it now carries both, like the phone.
‹ lincoln — 2 of 369
search
aka: omni search · the head field · canon
ONE search — same field, same answer, on all three roots, narrowing the pane in place (the search page is retired). Four groups: Friends · Personas · Group chats · Chat history, each result drawn by the row it stands for. A pick opens it — a chat at its line, a contact or a persona in the right pane. At every width the field grows out of the magnifier, its glyph becoming .
↑ A B C … Z #
the A–Z rail
aka: the Friends alphabet rail (v704, rebuilt; phone-only v738)
Friends’ full-height alphabet rail — phone only, a thumb instrument; wide scrolls. Always the whole alphabet ↑ A–Z #, positions never moving; empty letters dim yet still jump to the nearest peopled one; a drag scrubs with the letter shown large. Jumps set scrollTop outright — never scrollIntoView.

The stream messages & turns

Li Ka-shingmagnate

Keep some back, whatever you choose.

persona bubble
aka: a character / AI bubble
A character's message — left-aligned, tinted in that character's seat colour (avatar + name + a left colour-bar).
DDan

Does the newest one have a metro line coming?

Which of the three appreciates most?

Yyou
human message
aka: the human bubble · the user monogram
De-emphasized on purpose — quiet neutral grey; the eye stays on the panel. Yours right-aligned, monogram + “you” under the bubble; another human’s left-aligned, name above, like a persona. The monogram (greyscale Bauhaus wash + serif initial, id-seeded) is the one per-user mark; an uploaded photo layers over it, the monogram staying the forever fallback.
Euler is weighing in…
8.4s
the egg
aka: the conferring bubble, the thinking bubble
The placeholder shown while the panel confers — breathes + soft-pulses, grey (belongs to no character). The label is upright serif (not italic) and rotates through canned lines on a slow cross-fade (≈2.6s — fast alternation read as fake); a host's name wears its seat colour. The elapsed timer counts from the turn's real start and survives a room switch (no reset).
ringing the newsroom…
3.1s
search bubble
aka: the assistant fetching, the robot egg
Shown while the @Web Search fetches a web dispatch. Same breathing shell as the egg, but it belongs to a robot — so it reads in the mono face (JetBrains Mono, not the personas' serif) and rotates "newsroom" phrases ("ringing the newsroom…", "scanning the headlines…"). Its own elapsed timer, also switch-proof. Replaced by the dispatch card when the brief lands.
egg-hatch
aka: the hatch, the reveal
The resolution: the egg grows into a shell the size of the turn, the replies fill in one by one, the shell dissolves. It grows WITH the pieces — each holds the screen about as long as it would take to write (~170–780ms by length), under a whole-turn budget so a long ensemble never drops its tail in a heap.
setting the table…
4.6s
the staging bubble
aka: the shuffle
Shown while a host is setting up a tool — the deliberate turn’s act call, after the host has already spoken. The egg’s sibling: same shell, a rulebook glyph instead of a face (nobody is speaking), dots that bounce rather than breathe. It never simply disappears — it dissolves into the cards it was waiting for.
skeleton
aka: the loading shimmer, ghost bubbles
Shown while an uncached chat’s history crosses the wire. The chrome paints instantly (the chats pane already holds it); grey shimmer bubbles mirror the persona/you layout so nothing jumps on swap. A cached chat skips it; a fast fetch never flashes it (~160ms delay-guard).
confer capsule
aka: “view confer”, the deliberation chip
A small chip under a turn. Opens the confer drawer (from the left) showing the panel's internal discussion before they answered. Only on turns that conferred — a direct answer (solo chat, an @name reply, or the opening) has no backstage confer, so it shows no capsule.
the question(s)
“Okay, here's the full picture so you can weigh in properly. I have three flats shortlisted — one in Xuhui near the river, one in Pudong that's brand new, one in Jing'an that's small but central. My budget is tight after the down payment…”
Show more
confer drawer
aka: the deliberation panel, the confer slide-out
Slides in from the left when you tap a confer capsule. Header is the question(s) — the prompt that triggered the turn — over their internal discussion. A long prompt folds to 4 lines with a Show more / Show less toggle so it never fills the header.
⚙ cache write · 8.7k tok · $0.040 · 8.8s
cost boundary
aka: the telemetry pill, the cost line
System telemetry under each turn — token count, cost, and the generation time (· 8.8s under 10s, whole seconds above). The prefix flags a cache write (this turn wrote more prefix than it reused — a cold or TTL-expired prefix, a cast change, or a model switch). The time is the panel's wall-clock for that turn — a quick way to read the fast vs (reasoning) DeepSeek gap.
resumed — the panel remembers
note line
aka: a system note
A centred grey timeline marker for system events (e.g. resuming a saved chat). Lowercase, flanked by rules. An err variant turns coral, flanked by !!! — it carries failures the room must never swallow, e.g. "the panel didn't respond — try sending again".
✦ Web Search

Shanghai eases home-purchase rules

On June 3, Shanghai lowered the down-payment floor for second homes to 30%…

Sources: Caixin · June 3 · Reuters · June 4

dispatch card
aka: the web fact-brief, the @Web Search card
The result of @Web Search …: a breathing search bubble, then the dated, sourced fact-brief lands in-stream for everyone, with a search-cost pill breaking out the SERP fee (counted in every total; shown separately only here). The brief also files into the ∞ pane as a WEB artifact; the hosts react after, having seen it in context. Engines: Serper/Brave, Chinese Bocha first.
Dan is typing…
typing line
aka: the presence shimmer, "X is typing…"
A live presence line while another human in the room types — their name with a soft left→right shimmer. Ephemeral: RAM-only, never persisted, driven by a debounced heartbeat; it fades when they send or fall idle, and coalesces several typers ("Dan, Mary are typing…"). Never the panel.

@Mary ignore the hosts a sec — did you call the bank?

You → Mary
aside
aka: an @human side-note, the panel-skip line
A line addressed only to another human (an @person mention, no host): it posts in the stream for everyone, but the panel does not answerno turn, no egg. A side conversation the hosts simply overhear (they catch up on their next real turn). @Web Search is never an aside.
Mary

I'd hold the Pudong one — newer build, better resale.

other-human bubble
aka: a co-human's bubble
Another person's message in a multi-human room — left-aligned with a neutral theme-grey accent bar + their name (colour stays reserved for AI). Distinct from your bubble (right, grey, no name) and a persona bubble (left, seat colour).
Yesterday 21:40

Sending the floor plan tomorrow.

10:05
小胖agent

North-facing kitchen — the good way round.

time stamp
aka: the clock line, WeChat's stamp
WeChat's rule (v980): a centred line above a bubble — the chat row's time face and size (Inter 11px × the text dial), 24h clock everywhere (v985) — only when it is ≥ 5 minutes after the previous one (or first in view) — never on every message. Wording widens with age: 14:05 · Yesterday 14:05 · Wednesday 14:05 · 3 Aug 14:05 · 3 Aug 2025 14:05 (zh: 昨天 · 星期三 · 8月3日), the same voice as the chat row. Derived from neighbours at insert time, never stored; furniture — not selectable. gap 14.
rating rail · check-in
aka: the thumbs, the feedback row
A quiet 👍 · 👎 · ⌄ row inside every persona bubble, right-aligned under the text. The glyphs are small but the tap target is invisibly enlarged (~32×38, v367). The ⌄ folds out lever tags (length · tone · content) plus an optional note, and a periodic check-in survey card asks for a fuller read — all of it feeds the floor producer’s guest-feedback brief. Hidable per role via Who-sees-what.

Rich rendering how a host's words are drawn (v52+)

Terence Tao

Two cases, then the bound:

  • the even split
  • the odd split
rendered bubble
aka: rich speech, markdown bubble
Host replies render full Markdown now (was inline-only): lists, tables, headings, blockquotes, bold/links — plus math and code below. One shared renderRich() pipeline draws bubbles, the artifact pane, and dispatch briefs; a final sanitize pass (DOMPurify) and a scrub guarantee a raw <speak> tag never reaches the screen.
Confucius

名不正,則言不順。


If names are not correct, language is not in accord with the truth of things.

translation bubble
aka: the hairline translation, the divergence net
When a host slips out of the room’s target language, the room keeps the line and draws a faithful translation under a hairline in the same bubble — original ─ rule ─ translation, persisted with the message; ✦ Save works on either half. The panel’s own memory keeps only the translation, so the room converges. Canon: room language.
pythoncopy
import math
total = cross_section * age
code block
aka: the cb block, fenced code
A fenced ```lang block renders with a language label, a copy button, and highlight.js syntax colours mapped to the theme palette (so they adapt across light/dark/e-ink). Shared by chat bubbles and the artifact pane. Hosts are asked (in the SP) to fence code, or Markdown eats the underscores and asterisks.
e + 1 = 0
math
aka: KaTeX, the equation
KaTeX renders LaTeX — inline $…$ or \(…\), displayed $$…$$ or \[…\] (vendored same-origin, lazy-loaded). A single $ is math only by delimiter pairing (close-$ tight against non-space, not followed by a digit), so $x=\pi$ renders while $5 and $10 stay money. \$ is a literal dollar.

Wildcard & artifacts non-dialogue blocks the panel hands over

DOCRevised proposalcopy
from Li

Three flats, one buyer — here's the revised split and the exit timeline…

artifact card
aka: a wildcard card
A non-dialogue block a host hands over — captured into the wildcard drawer as a card: a colour-coded kind tag, the title, a copy button, a from <host> line in the host's colour, then the rendered body. The kind is inferred from the content (model-agnostic), so it only styles the card — a mis-read block still survives as readable markdown. Seven kinds →
DOCCODETABLEIMGNOTEDIAGRAMWEB
artifact kinds
the seven tags
DOC (prose) · CODE (a fenced block + its language) · TABLE · IMG · NOTE (a short aside) · DIAGRAM (a Mermaid figure, rendered live) · WEB (an @Web Search dispatch — system grey, not a host). The first six are the panel's own handovers (DIAGRAM draws as SVG); only WEB is server-fetched — the @Web Search, not a host.
DIAGRAMDiagramcopy
diagram
aka: the Mermaid card, a figure
A host hands over a Mermaid diagram — a ```mermaid fence, or a block the parser detects as Mermaid (a flowchart / sequence / gantt keyword). It renders as a real SVG in the wildcard pane (vendored Mermaid, lazy-loaded), contain-fit to the card with tap-to-zoom (panzoom). The bubble's link chip and the pane card show the same DIAGRAM tag. Invalid Mermaid falls back to its source — never an error bomb.
link chip
aka: the artifact link, the jump chip
A small chip inside the host bubbleDOC  Revised proposal  → — that jumps to the artifact in the wildcard drawer (and flashes it). The dialogue stays clean; the heavy block lives in the pane.

Notebook & saved lines clip a line from any chat

✦ Save line
save-line pill
aka: the ✦ Save-line pill, the selection pill
Select text inside a host bubble → a rose "✦ Save line" pill appears at the selection. Tap it to clip that exact span into your Notebook; the clip flies into the ∞ button, which pulses.
Li Ka-shing · saved
"Keep some back, whatever you choose."
Notebook
aka: saved lines, the quote book
Your clipped quotes as rose cards — attribution · the quote · jump · copy · a two-tap delete. The ∞ pane holds this chat’s saved lines; the global Notebook is Me → Notes. Jump opens the source chat and scrolls the original into view, highlighted, translated lines included.
Li Ka-shing

Keep some back, whatever you choose.

saved star
aka: the ✦ on a name
A faint rose after a host's name marks a bubble you've clipped to the Notebook — so you can see at a glance which lines you kept.
confirm
two-tap delete
aka: the confirm pill
Un-saving a line is a two-tap confirm: the first tap grows the muted bin into a red “confirm 🗑” pill, the second deletes — it auto-reverts after 2.6s if you don't confirm. The card leaves both the Notes tab and the ∞ pane at once. (The old per-note free-text annotation was removed — a saved line is just the quote.)

Panel events arrivals & departures

EAlbert Einstein materialized
join capsule
aka: the “xxx joined” capsule
One per character, in that character's seat colour. Pulses 3 beats on the live arrival (static on reopen — no re-pulse when you switch back). No greeting — they just speak from the next turn. The arrival verb varies (one of ~50: joined, materialized, swept in, took the mic…).
Roger Federer and Mary Tehrani have left
leave capsule
aka: the “xxx left” capsule
One combined, system grey capsule for the whole removal — “X has left” / “X and Y have left”. Pulses 2 beats on the live event (static thereafter).
switched to DeepSeek v4 Pro
switch capsule
aka: the model-switch capsule
System grey capsule marking a mid-chat model switch — “switched to <model>”. Same grey family as the member-join capsule (it's not a character); dropped at the point in the stream where the model changed.
Dan just joined
member-join capsule
aka: the "X joined" (human) capsule
System grey, no avatar — a person isn’t a character (contrast the seat-coloured join capsule, a persona’s arrival). Pulses 2 beats on the live join, static on reopen. It has no mirror (v1023, owner): a human EXIT is silent — walked or removed — because joining says the room changed shape while leaving is an absence the people list already reads. The member_leave event is still written and still broadcast; it is the room's audit, and the roster drops the tile off it.

Panel management

Li
ДDoronin
+invite
remove
members grid
aka: the member avatars, the panel tiles
Inside the chat-info panel: the seated panel as avatar tiles plus the invite (+) and remove (−) tiles — the doors to the invite / remove sub-pages. Every persona card in the pickers carries a quiet ⋮ → View profile. In a zero-persona room only + shows — it IS the seating door; removing every persona is allowed (the chat goes quiet).
Brian Yuaninfra lead
castcard
aka: a picker card
A selectable character row. In the new-chat picker’s grid it is a card — a pick takes its seat colour on disc · border · ✓, no wash. In the invite / remove sub-pages it wears the as-page dress: a workhorse row on the --row-* tokens (min-h 60 · inset 16 · gap 12 · 40px lead — v789).
‹ Invite people  ·  Invite 2 ✓
Invite people · Invite persona
aka: the invite / remove sub-pages · v706–707
Sub-pages, not dialogs: the shared ‹, the commit riding the header with the count on its label (“Invite 2”), 60px workhorse rows — people as contact rows, personas as as-page castcards (that picker keeps its footer: the chosen ride there). Invite and remove share the page; new chat stays a dialog on purpose.
This room is full — up to 5 AI personas. Retire one to add another.
room-full toast
aka: the invite-full toast, the cap toast
Blocks an add past the room’s persona cap (max_personas): the invite + toasts “This room is full — up to N AI personas. Retire one to add another.” (a headroom variant counts what’s left). In new-chat the add buttons themselves hide at the cap and return when a seat frees.

New-chat · panel selector “Call the panel” → describe → curated panels (hot start) · each card: ⋮ → View profile

“What’s on your mind?” · Call · Hand-pick
aka: the describe→call dialog, the two doors
Opened from the Chats head ⊕. A situation box and two 42px buttons: Call the panel (coral) is the hot start — one curator call reads the whole library and returns three candidate panels; Hand-pick (ghost) opens the library overlay. The persona list and its search live only behind those doors. The box is a fixed two-deck with the dictate mic on its own row.
takes switcher
aka: the 1 · 2 · 3, the panel chooser
Three muted pills flipping between the three candidate panels the curator returned (panel 1 = its top recommendation). Deliberately quiet — a new user can ignore it. Switching re-renders the rows below with a short entrance transition.
Pressure-tests the leap
CCharlie Mungerinvestorinvert, always invert
MMichael Moritzventure investoradded by you
curated panel · why · theme
aka: a curated seat, the "why" line, the «theme»
The chosen panel, one persona per row — glyph · name (+ local) · role — with the reason it was invited on its own accent-barred line: coral for the curator’s why, grey “added by you” for a hand-pick; × removes. Above, the panel’s serif-italic « theme »; below, the coral dashed + Add more, hidden at the panel cap.
“add a character” skeleton
aka: the empty seat, the waiting slot
In “Hand-pick your panel”, the add button is a skeleton seat-card — dashed coral frame at seat-row height, a dashed “+” circle where the avatar goes, “add a character” where the name sits. A seat waiting to be filled; tapping opens the library. Doubles as empty state and “add another”; gone at the cap.
auto-call escape hatch
aka: “or, describe your topic and → call the panel”
In the empty hand-pick state only, a weaker way back to describe→call: muted “or, describe your topic and” beside a coral “→ call the panel” capsule. It vanishes once any seat is added; clicking it switches to “What’s on your mind?” with the situation box focused.
Calling your panel — sifting the library…
Warren BuffettConfuciusAda LovelaceJack MaPlatoWarren BuffettConfuciusAda LovelaceJack MaPlato
Steve JobsZaha HadidLiu CixinFeynmanOprahSteve JobsZaha HadidLiu CixinFeynmanOprah
sifting the library
aka: the convening marquee
While DeepSeek curates (a couple of seconds), the panel area fills with a multi-line marquee of persona names drifting past — a pulsing dot and a faint coral scatter, the system "sifting the library." It clears when the three panels arrive.
WWarren Buffettinvestormargin of safety, every time
CCharlie Mungerinvestorinvert, always invert
AAda Lovelacemathematician
library overlay
aka: the hand-pick layer, "Add to your panel"
Hand-pick and + Add more open the full library over the picker — the same .castcard as the invite picker. In the call flow the curator’s recommendations float to the top with their coral why lines, pinned (tapping checks in place, no reflow); the rest alphabetical. Picks merge into Your panel.
More settings
aka: the advanced row, the folded dials
The Model · Language · Persona vividness · Floor producer controls now fold behind a flat "More settings" table row (left-aligned, blends into the sheet) so creating a chat isn't intimidating — it expands to the controls below (see New-chat parameters).
The American Dream 2.0
hot-start title
aka: the streamed title, the typing title
A hot start gets an auto title from the curator call, typed char-by-char in coral then settling to ink. A solo panel names itself “<short>-ish” at creation; a multi-persona chat stays “New chat” until your first message, which a cheap model distils into a topic title. Fallback: plain truncation.
search chrome
aka: the clear-×, “Try searching”
Every persona search box — the three room pickers, the console’s Personas view, the Studio browser — wears the same chrome (PersonaSearch.attachChrome, #23): a clear-× at the right edge (only while there’s text) and, on focusing an empty box, a “Try searching” panel of tag-derived terms that type themselves into the box and run the normal fuzzy search.

New-chat parameters folded behind “More settings” in the new-chat modal

model dropdown
aka: the model picker (new chat)
Sets which model voices the new chat — the menu is server-driven (/api/config): DeepSeek v4.1 Flash, fast or (reasoning), plus Sonnet; V4 Pro, V4 Flash and V4 Flash Vision are retired (2026-09-09) and never offered — a chat saved on any of them reads onto v4.1 Flash. Plain names run with the thinking trace off; a fast↔reasoning switch keeps the cache warm (same underlying model). The chat-info switcher changes it mid-conversation.
language dropdown
aka: the language picker (new chat)
The new chat’s language: English · 简体中文 · 繁體中文 · 日本語, defaulting to your account language. The panel greets and speaks in it, following the guest on a sustained switch. A described (hot-start) chat speaks the language you typed the situation in — the dropdown pre-fills with what was detected.
Persona vividness
True to life Turned up Larger than life
Distinct voices with a notch more presence — leans into each speaker's signature moves.
Persona vividness
aka: the vividness notches — the ONE room dial
Three notches — True to life (no directive) · Turned up · Larger than life — for how pronounced every persona’s voice is, whole panel; amplitude on the real person, never parody. Server-driven values, set at creation, baked into the cached prefix.
Floor producer (Off · Code · V4.1 Flash · V4 Pro)
aka: the FP arm
A four-way segmented row (v994) in New-chat → More settings (defaults to the console's arm) and in the chat-info panel, where an admin or a private room's owner may switch it mid-chat. Off = no staging at all; Code = the deterministic v0. Hidable per role via Who-sees-what.

Chat-info panel inside the title dropdown · quick actions: [Theme] [Mute] [Fork]

model switcher
aka: the Speech model row (Geek page)
Since v1000 the switcher is the Geek page's Speech model row — a house .dd dropdown (the Settings ▸ Default language dress), current model checked; the chat-info model tag + popover retired. Same rules as ever: mid-conversation, among the admin-enabled models, read-only when only one is enabled; no model call — the new model picks up the verbatim history next turn and the cache restarts (a switch divider marks where). Base models only; thinking is the console's per-model reasoning switch. On a local room the row is a Born-group fact — the model is fixed for life.
Sonnet 4.63 calls · 91.8k · $0.300
DeepSeek v4 Pro2 calls · 61.0k · $0.180
per-model cost rows
aka: the cost breakdown
Replaces the old grand-total stats line. One row per model used● Sonnet 4.6  3 calls · 91.8k · $0.300 — the rows summing to the total. Shown only once >1 model has voiced the room; the current model is subtly coral.
Current Cache 11.9k tok
SPPUG
cache bar
aka: the cache composition bar
The cached megaprompt prefix as a proportional bar — segments in append order, coloured by type: SP · P (one per character) · U (yours) · G (the panel’s) · CΔ (a cast change). Labelled “Current Cache” with the token total. Inviting a voice appends a P; a mid-chat model switch shows as a thin divider.
switch divider
aka: the model-switch divider
A thin vertical rule inside the cache bar marking where a mid-chat model switch happened — the prefix to its right was re-written cold on the new model. Same event as the switch capsule in the stream, shown on a second surface.
Einstein-ish
rename pencil
aka: the ✎, the hero edit button
A small at the hero's right edge in the chat-info screen — tap to edit the title in place: Enter/blur saves, Esc discards. Commits optimistically (bar + hero + chats list, then reconciles to the server's normalized title). This is the only in-chat rename now — the old bottom title field was retired — though the chats-list ⋮ menu still retitles from the list. Fades with the hero on scroll; hidden in read-only peek.
Persona vividness
True to life Turned up Larger than life
read-only vividness
aka: the dial echo (Geek page)
Since v1000 the room's dials read on the Geek page, not the chat-info sheet: Persona vividness is a Born-group value row (set at creation, never adjustable), and the Floor producer four-way segment beside it IS live for whoever may move it (admin / private-room owner; a value row for everyone else). The editable coral vividness dial still lives in the new-chat modal. The chat-info sheet keeps Theme · Mute · Fork · Geek over cost + cache + members.
quick actions
aka: [Light] [Mute] [Geek]
Pills directly under the info hero — since v1001 exactly three, one row, narrow and wide alike: Theme (shows the mode pressing switches to) · Mute · Geek (the settings card, where every room control lives). Basis is a third less the two 10px gaps — a percentage, never a measured max; labels ellipsise, never wrap. The file verbs — Rename · Fork · Archive · Delete — live behind the sheet's ⋯ menu, sharing the chat row ⋮'s confirms verbatim (a moment room's ⋯ offers Mute · Leave instead; the Tools pill retired into the Geek page at v1000).

The composer

Message the panel…
composer pill
aka: the composer, the input pill
One rounded pill (Gemini-style), adaptive two-deck: empty shows the field with its accessories — the emoji door, the (v961: was the tools die), the mic; typing collapses them (WhatsApp’s morph) so the field takes the room, and past one line the buttons drop to their own bottom row. Grows to exactly 7 lines, then scrolls.
PhotoCameraKitsToys
the + sheet
aka: the plus, the more sheet (was: the tool drawer, the die)
The + beside the emoji door opens the house bottom sheet (.rx-sheet) with four tilesPhoto · Camera · Kits · Toys (gap 5, v961). Toys swaps the sheet for the eight room tools under a ‹ back row (the die's old grid, Kits lifted out to its own tile); Back / Esc / edge-swipe peel one level — Toys → the menu → closed. Photo and Camera are the phone's own picker (one input with capture, one without). A shut tool door greys Kits and Toys in place — the + itself never hides, it is the photo's door too. Desktop: popover beside the button, as before.
×
photo chip
aka: the attachment, the pending photo
A 64px thumb with its × docks above the field the moment a photo is picked (or pasted / dropped on desktop); the field's placeholder turns to Add a caption… and Send lights even with no words — a photo alone is a message. The upload starts at once (a wash + spinner while it runs; a coral ! if it failed), so the server's eyes are already reading by the time you send. Tap a thumb → the photo viewer, from the local file (v972). Up to four photos a message (v989): a chip per photo in a row, every upload started at once; a fifth is refused with a toast. Android's picker: a page cannot choose the gallery app. Chrome's rule (crbug 428394446): an images-only accept opens the one-tap system Photo Picker — every local album, the Google-looking grid; any other type drops to the Camera|Files document chooser (tried as v972–973, reverted in v974). iOS keeps its sheet.
the camera
aka: the in-app camera (#camSheet, Android only)
Android browsers ignore which lens a page asks for (capture="environment" opens whatever the camera app used last — mdn compat #19603), so on Android the page runs the camera itself (v974): getUserMedia with facingMode: environment → a full-screen live preview on black, a 72px white shutter centred at the foot, a flip to the right; a selfie previews mirrored and saves the way it was seen. The shutter draws the frame to a canvas → a JPEG → the same photo chip a picked photo takes. Refused the camera → the phone's camera app (the old input) so the tile never dead-ends; a backgrounded page releases the camera. × / Back / Esc close it. iPhone keeps its native sheet — it honours the lens and hands over HEIC-as-JPEG.
这套 89 平的,怎么样?
photo message
aka: the photo bubble + its caption
Two bubbles, one head (the owner's pick, v963 — iMessage's grammar): the photo in its own bubble (.bubble.photo-bubble, 4px padding) and the caption as an ordinary text bubble under it, the sender's avatar + name once above both. Two width rules that never meet: a text bubble is as wide as its longest line, capped at 88% of the stream; the photo bubble is as wide as the picture — at most 300px on a phone, 360 on wide, never upscaled (a small picture stays small), a tall portrait capped at 360px and centre-cropped. Tap → the photo viewer below. Re-renderable, so decoding="sync". What the panel reads is the photo's text shadow, never shown in the bubble. Several photos (v989): one grid bubble — 2 across, 3 across for three, 2×2 for four — square thumbs, each opening the viewer; the caption bubble under it as before.
Raft (algorithm) - Wikipediaen.wikipedia.org×
link chip
aka: the link preview, the unfurl chip (.link-chip)
Paste a URL and the first http(s) link in the field unfurls (debounced 600ms): the box fetches the page ONCE — SSRF-guarded, frozen into the room — and a chip docks above the field: thumb · title · domain, coral left edge (gold while fetching, muted when the page gave nothing — sends as a plain link). The × (with the 40px hit pad, the photo chip's v992 lesson) dismisses: the URL sends as plain text, nothing attached, and stays dismissed until the URL changes. One link a message, by design.
Raft (algorithm)
en.wikipedia.org
看看这个 https://…
link message
aka: the link card + its caption
Two bubbles, one head (the photo's grammar): the frozen card — image · title · description · domain, the whole card a link out (validated http/https href, never an unvetted string) — in its own bubble (.bubble.link-bubble), the caption an ordinary bubble under it with the URL linkified (underlined ink, never browser blue; the balanced-paren rule keeps a wiki's (algorithm)). Quote-reply reads the caption, never the card. What the panel reads is the page's skeleton digest (≤6k chars, once per link per room), never shown in the bubble — docs/link-unfurl.html.
×
photo viewer
aka: the viewer, full-screen photo (#phView; NOT the poster lightbox)
A tapped photo grows out of its own thumb (FLIP, 240ms; the cached 480 tier paints at once, the 1600 tier sharpens it on arrival) onto full-screen black on a phone, a dim scrim on wide. Default = fit: the whole picture, centred, never upscaled past its real pixels (wide: within 92%). Phone: pinch to 4×, double-tap 1× ↔ 2.5× at the tap, pan when zoomed (clamped to the edges), drag down at 1× to dismiss — the picture follows the finger as the black thins; past 110px or a flick it goes, else it snaps back; a single tap at 1× closes. Wide: wheel zooms at the cursor, drag pans, double-click toggles, × / Esc / click-out close. Close shrinks back into the thumb when it is still on screen. A set (v991): a bubble's photos — or the composer's chips — open as one strip; at 1× a horizontal swipe slides to the next/previous (the arrow keys on desktop), a「2 / 4」counter sits up top, and the strip stops at either end (a pull past it springs back). Registered with the back-stack (Back / edge-swipe / Esc close it). One transform over the fit layout does all of it — the stage owns every pointer (touch-action:none).
dictate mic
aka: the mic
In the composer pill. Tap to dictate — streaming speech (zh + en mixed; iFlytek 中英识别大模型, browser-direct) becomes editable text in the field: the in-flight hypothesis renders muted (.dict-live, var(--muted)) and commits to normal ink as it finalizes. Recording state swaps the glyph to a red stop square (.rec, var(--rec)). The button shows only when the server holds STT keys (body.stt-on, from config.stt) — v375.
send button
aka: the ↑, the send
The round coral button with an up-arrow, at the right of the composer pill. On send the input blurs (dismisses the phone keyboard). While a turn is running it becomes a grey Stop square (the egg colour) — tap to scratch the turn — reverting to the up-arrow when the reply lands.
mention
Li Ka-shing
ДDoronin
Web Search
@-mention autocomplete
aka: the mention popup
Type @ → avatar + display-name rows; ↑/↓ navigates, Enter/Tab/click picks, backspace deletes a mention whole. Mentions are atomic id-bound chips (@{u:N}), resolved to the same coloured @Display in every renderer; ✦ Web Search rides the list. A mention of YOU renders as a filled coral pill — only your own name jumps out.
@ Li@ Doronin
quick @-mention chips
aka: the mention row
A one-tap row of the room's members above the input. Wide only — hidden on narrow (typing @ still works).
@Li Ka-shing
hold the sender line → @pill v900–901
aka: the sender-line hold · WeChat's 群 @
Hold a bubble's sender line — avatar, name or role, the whole line (480ms, touch or mouse) → that speaker's mention lands in the composer. Composer focused → at the cursor, keyboard stays; not focused → at the end, then focus with the caret there. A tap still opens their profile — talk to them beside read about them. Falls through to selection when there is nobody to mention.

Dialogs two named styles — main · sub

Two dialog styles share one box — the scaffold lives ONCE in theme.css: a 580px card centred over a dim scrim on wide (rgba(0,0,0,.40), the one shared value), full-screen on narrow (≤640px) — var(--surface) background, 1px line border, 16px radius (0 on narrow), a fixed height of min(720px, 88vh), fade-and-scale in, backdrop-tap to close. The persona profile opens with the fade only (no rise-in) and its scroller is chromeless (hidden scrollbar, still scrolls). They differ only in their header and their role.

main dialog ♺ theme.css #25
e.g. “Hand-pick your panel” · “Browse personas”
Top-level entry into a flow. Header = a serif 21px title plus its dismissal glyph. Sits at z-index 80. The × is the shared .dialog-x (theme.css): 20px glyph, 40×40 hit area — one spec for every dialog, sheet, and drawer (.flow variant sits in flex bars; only the image-lightbox keeps its circular dark ×). Where that glyph sits is now decided by the surface TYPE below, not by this tier.
sub dialog ♺ theme.css #25
e.g. “Add to your panel” · the persona profile
Opens one level deeper, from a main dialog. Header carries a ‹ back that returns to the parent, with an optional serif 18px title. Stacks above its parent. (Until v516 it also carried an × — the redundant second door was retired on phone by gap 10.)

Dismissal grammar — the four surface types v516 · gap 10

main / sub above says how deep a surface sits. This says where its close lives, and it is the load-bearing axis: a phone already teaches four grammars, and we follow them rather than inventing a fifth. Dismissal is a form of going back — that is why it lives on the left — and an × in the top-right of a surface that has a commit steals the commit slot, creating the "did closing save my changes?" ambiguity the Material layout exists to prevent. Every rule here is phone-only (max-width:760px): in wide mode × top-right is correct for every type, because that is where a window closes.

TypeDismissalOpt-inOurs
Page
navigated into
‹ back, top-left. Never an ×..dialog-back, or .dialog-back.corner + .hbar on the head (+ .hide-narrow on any × it also carries)chat info · profile · forward · notification centre · poster · Archived & Trash · news reader · text sheet · Chats-from-Vibes · Seen Generate page (a task dialog until v1014 — its verb now rides the head)
Task dialog
has a commit
✕ / Cancel top-LEFT · the verb top-RIGHT..dialog-x.task + .dialog-commit + .hbar on the head; the footer twin and its "N selected" line take .hide-narrownew-chat picker · account editor · library overlay · crop editor · selection bar · Post-to-Vibes (the people picker left this row in v706, Invite/Remove persona in v707, and the Seen writer confirm in v1014 — all sub-pages now, see below. The picker's new-chat mode is still a task dialog.)
Overlay
nothing to commit
× top-right, or tap-out.bare .dialog-xthe unchanged defaultdiagram zoom · image lightbox · avatar viewer · every popover
Sheet
rises from the bottom
Grabber, and you can pull it (v966); × optional, usually absent.no scaffold glyphthe + sheet · who reacted · who voted · memory recorded — four, sharing one body
The grabber is a control, not a picture (v966). Pull it down and the sheet follows the finger; release decides on DISTANCE or VELOCITY — past 84px, or a flick over 1000 px/s — and anything short of both springs back. Distance alone would dismiss the sheet every time a thumb brushed the bar on the way to a tile, which is the one failure a dismissal gesture must not have. ⚠ The flick number was measured, not chosen: at 450 px/s an ordinary drag (15px between two frames) read as a flick and shut the sheet 40px in. ⚠ The grip is the drag surface, never the whole sheet — the body scrolls, and a drag starting there has to arbitrate with its own scroller on every touch, which is the classic bottom-sheet bug; the bar is the one part that never scrolls, which is why every platform put the handle there. Down only, and a popover does not slide at all. ⚠⚠ And the pull is not the Back gesture (v967). On the + sheet — the one surface deep enough to show the difference — Back climbs a level (Toys → the menu) while the pull and the tap-out leave. That is the platform grammar, and it is why a sheet level needs no back row of its own: v966's Toys level carried a ‹ and a title, which was the sub-page reflex applied to a surface that is not one. You got there by tapping a tile two centimetres above, the tiles say what they are, and a row re-stating all of it was the tallest thing on a 131px sheet. ⚠ A level change is a MOVE, not a repaint (v968). Stepping in or out animates the way the sheet itself does: the content arrives on the sheet's own entrance keyframe, and the box transitions its height between the two levels' sizes (131 ↔ 228). autoauto cannot be transitioned, so the outgoing height is measured, the incoming one measured after the render, the start pinned and committed with a forced reflow — without that both writes land in one frame and the browser only ever sees the end value — and then it goes back to auto, because a pinned height is the next open's bug.
Tap-out is claimed on the PRESS, never on the click (v826, made universal in v1022). A popover that closes on the click's way up has already let the row underneath act — the tap that was meant to dismiss the ⋯ renames a chat, opens a profile, opens a build — so the dismissal is taken at pointerdown in capture and the click that pointer goes on to produce is swallowed. ⚠ A rule written against a page is a rule missing on the next page: the three .dd pickers each carried their own copy at their own scope until v1022 folded them into one delegated rule on the class. ⚠⚠ And an iframe swallows the press whole — a tap inside the Seen piece's embed frame produces no event in the app's document, so a popover open over one can only be dismissed by binding the closer inside the frame's own document (plus a focus-left-for-a-frame net for the ones that come later).
The sweep is opt-in — a bare .dialog-x renders exactly as before; add the class that matches the type, never edit the default. The gesture must agree with the glyph: whatever ‹ or ✕ does, Back and Esc do the same, through the same close function. A head carrying a title and a task/corner glyph MUST take .hbar — one flex row, only the title shrinks. Full per-surface record: the dismissal sweep.

Social & messaging layer Vibes · Moments · Seen · notifications · select & forward · reactions · emoji

The layer that grew the room from a chat into a small social app: the Vibes persona feed, the Seen read, a durable notification rail, selection & forward, reactions and the emoji subsystem — each on the WeChat / WhatsApp grammar the audience already knows.

S
Studio · your build is ready
2m ago
A
Ada invited you to a chat
1h ago
Notification centre
aka: Notifications · the inbox · noticeSpec
Every event writes a durable notices row, mirrored over the per-user SSE stream (the toast) and Web Push — three mirrors, one source per kind (noticeSpec). One compact form: small avatar · byline · action · CTA. Unread rides the Notifications row badge on Me.
S
Build ready
Notice toast ♺ theme.css #30
aka: the toast pill
The SSE mirror of a notice on whatever tab you’re on — the warm-taupe / bone pill (--toast-*) with an optional avatar + CTA. The same shared toast every transient notice in the app uses.
梁宁
产品的本质,是把一件事想清楚。
2h · 今日头条
Vibes feed
aka: Moments · 朋友圈
The cast's feed — famous masters and user-built personas alike — on a WeChat-Moments look, dripped 今日头条-style. Posts are DeepSeek-written, system-curated (the Vibes engine). Each moment opens one shared room. A tab in the head bar.
Share something…
Post to Vibes ♺ theme.css #26
aka: the compose page
A full-screen compose page for a user's own Vibes post — a text area and the coral Post CTA. Lands the post into the Moments feed; it can carry a poster.
The read.
Poster
aka: the magazine poster
A full-screen magazine-style poster (.poster-page) — the shareable image a post or Seen read renders to. A small embed of it also rides inside the sender's message bubble.
A quiet week in markets… HOT REPLIED
A
B
C
the circle is talking
NEWS reader · circle window
aka: the news drip
Real-news items with attribution and slate REPLIED / HOT badges; tapping opens a circle window — a mini group chat where the cast reacts. Feeds the same Vibes surface.
SEEN · your week
You kept circling one question.
— signed, 梁宁 · opens to a long read
Seen card
aka: Seen (never 看见)
An on-demand read from its row in Me, above Notes (v1038, temporary; a Discover page v1013–v1037) — a batch of chats + saved lines distilled into a keepable piece, written by a voice from your own rooms and signed by them. Opens to a long read; renders to the Poster for sharing.
× 2 ↩ ☆ ⧉ ↪ ⋮
先说结论:这个功能砍掉一半。
Message…
Selection mode & the action bar gap 8 · v443–444 · v894–895
aka: .sel-bar · the row wash · long-press select
A stationary 480ms long-press (touch and desktop mouse alike) enters selection: the row washes, the action bar rises with the verbs — the same press beat as the reaction bar. The composer greys out and goes inert while you pick. Two exits, both a single tap: the (44px, the house floor) or the composer. ⚠ Neither drops the mode on pointerdown — a control that removes itself hands the release to whatever is underneath.
MrBeast
这份榜单衡量的是公园和咖啡馆,不是生存。真正的宜居,是一个城市能不能在裂缝上建出秩序。
Save line⧉ Copy
Double-tap text sheet gap 8 · v490 · v894
aka: .txt-sheet · WeChat's 双击 · 全屏文本
Double-tap a bubble’s speech body → its text alone at 1.4× reading size — WeChat’s 双击. It is a house sub-page: the family .drw slide, the 58px head, the 27px ‹, the speaker as the title. It opens with nothing selected — long-press or double-tap a word, then drag the house handles; ✦ Save line · ⧉ Copy ride the span. Canon: text-selection.
👍 ❤️ 😂 😮 👏 🙏 😢
你不是怕选错,你是怕选了就没退路。
Reaction bar gap 4 · v453–457
aka: .react-bar · the quick twelve
Rides the same long-press beat as selection: the floating pill pops where the finger pressed (kept inside the viewport, never under the finger) — the quick twelve, ➕ opens the full panel. Tap toggles; a new pick replaces — one reaction per person per message.
Message… 🎤
😀😄
The in-field emoji door gap 12 · v479
aka: .emo-btn · the smiley · the ⌨ flip
The composer’s emoji door lives inside the field pill, at its left edge, muted — WhatsApp’s exact placement. Tap flips it to ⌨ and swaps the keyboard for the panel in the same reserved space; tap again flips back. Its state survives the composer’s two-deck morphs.
Search emoji
Recents
😂 ❤️ 👍 🎉 😮 🙏 😅 🔥
Smileys & people
😀 😄 😁 😆 😅 😂 🤣 🙂
🕐😀🐻🍔🚗💡🚩
The emoji panel gap 12 · v479
aka: .emoji-panel · the keyboard-space occupant
Not a widget with geometry of its own — a keyboard-space occupant: it takes exactly the space the keyboard takes, at the keyboard’s measured height, so the stream never moves. Sections + Recents over the 3,838 self-hosted Twemoji; its grid is the ONE place loading="lazy" is allowed.
Offer accepted!! 🎉 the flat is ours 🏡
🎉😂❤️
👍
Emoji in message text · solo emoji gap 12 · v478, v482
aka: img.emj · .bubble.emoji-solo
Every emoji grapheme in a rendered message becomes an <img class="emj"> of the same self-hosted Twemoji art the reactions use — a 👍 sent from iOS reads identically on Android. A solo-emoji message renders big (.emoji-solo). A rendered bubble is a PICTURE — read its text via speechText().
你不是怕选错,你是怕选了就没退路。
❤️2 👏
Reaction pill v457–459
aka: .reacts · the on-bubble cluster
Landed reactions are ONE small pill overlapping the bubble’s bottom corner (WhatsApp’s look): the distinct emoji + a count once shared. Yours is marked; tapping opens the details sheet. It re-renders in place on every SSE push — no relayout under a finger.
1 reaction
☺+ ❤️ 1
YouTap to remove ❤️
Reaction details sheet v457–458 · v538
aka: .rx-sheet · “{n} reactions” · hosts included — a persona may attach ONE emoji to the guest line it answers
Tap a reaction pill → a bottom sheet (WhatsApp’s grammar; centred on wide): who reacted, your own row tappable to remove. It mirrors the line live over SSE and closes itself when the last reaction leaves. Scrim tap or the back gesture dismisses it.
The re-render flash — every image the app repaints v576 · app-wide rule
aka: loading="eager" decoding="sync" · humanAvatar() / tEmo() / emoImg() · canon: PRESENTATION.md § General HTML rules
An innerHTML rebuild re-creates every <img>, and a lazy one doesn’t repaint that frame — so anything re-renderable is decoding="sync", avatars eager. The stronger form: don’t re-render what didn’t change. And whatever lands in the stream lands like a message — content first, then notifyAppend().
[hidden] loses to your own display v643 · app-wide rule
aka: .thing[hidden]{display:none} · the UA sheet is the WEAKEST origin · ~20 sites carry the guard
[hidden]’s display:none is a UA-sheet rule — any author display: beats it (origin beats specificity). Every element pairing [hidden] with its own display carries the .thing[hidden]{display:none} guard. Verify by computed style, never by attribute.
A scrim’s dim ARRIVES, it does not APPEAR v653 · app-wide rule
aka: opacity:0→1 held by visibility, never display:none→flex · canon: theme.css §.modal-scrim/.vp/.mep
display cannot animate, so a scrim held by display pops in whole. Every scrim holds its place with visibility + an opacity fade — both ways — and pointer-events:none is the safety net while it leaves. Address scrims by id: .modal-scrim.open can find the wrong one.

The toolbox the room's instruments — eight tools + the kit shelf

The toolbox — the room’s instruments — Eight instruments + the kit shelf, all server-owned world state (code=world, LLM=minds). Four lines per tool here; the design pages hold the full grammar and every ruling. Canon lives on the DoD, the defect ledger and the exam; the objects themselves are drawn in the toolbox UI.

The tool names on screen — ONE name per thing v647
aka: TOOL_TILES · TOOL_CANON / INSTRUMENT_ALIASES(server)· the i18n dict is the naming surface, because English IS the key
Eight tools, eight names, in both languages — a smoketest walks tile → alias map → i18n dict → registry so the files cannot drift. One metaphor per tool: you open a 密存 and you 存入 (Deposit) your answer.
The setting sheet — ONE skeleton, five tools v634 · setting-sheet
aka: #sheetScrim · SHEETS[key] · .st-sec / .st-fields / .st-swap / .st-chips / .st-sub
The five setup dialogs (poll · dice · slots · deposit · deal) are ONE sheet generated from config. Sections are the lifecycle — 设计 · 收集输入时 · 截止后 — and a phase a tool cannot have is not drawn. Seats clip at three measured rows; 标题 is required only once a same-kind card is already open.
The tool strip + the live card — where the furniture lives v569–635 · top-bar
aka: .tool-strip / #tableDock / .ts-chip / .ts-panel · the zones #tzState / #tzAct / #tzNote · .livetool / #liveGate / #liveDie · .mb-row · design: the top bar · evidence base: the UI field study
The room’s furniture is ONE row of chips under the topbar, plus a live card riding the stream for anything still collecting — act-now is the thing itself. A 待办 pill appears only when THIS viewer owes an action; the strip is computed per viewer, on the server, and compresses down a measured ladder.
The dice — the randomizer, the poll’s sibling T9 · dice-specimen
aka: .roll-card(the poll's .gate-card grammar)/ buildRollCardInto / buildRollCap · <roll dice="2d6" for="all" label="定先手"/> · <roll pick="猫, 狗, 狐狸" for="all"/>(the wheel)· /roll_go · 掷骰 · a card people ROLL on
kind="roll" on the instrument store: the open roll, the sealed roll(密摇 — the deposit’s three-phase life)and the readout, one card grammar. A card may size each roller’s cup per seat; the device’s offered move(开盅)is its own solid-accent object in the composer’s chip row — never a poll.
The clock — the room’s wall clock T1
aka: .ts-chip[data-tool=clock] / #tsClockV · #clockScrim / #ckDur / #ckLabel / #ckOff / openClockPage() · <clock min="5" label="自由讨论"/> · <clock sec="30"/> · <clock away/> · 计时 · the tool strip's THIRD chip(v569; it was the dock's third slot)· sheet: the lifecycle-less three
Standalone furniture — one clock, on the wall (the strip chip), where everyone reads it. No lifecycle: its sheet is 设计 and nothing else. The server publishes the deadline once; every client ticks against server time.
Furniture glyphs — a TOOL is a drawing, a STATE is a house emoji v556–617
aka: toolIcon(key) / toolGlyph(key) / .tgl / TOOL_ICONS — the tools · tEmo("lock") / TOOL_EMO / img.temo — the states, from the same /emoji/<codepoints>.svg art as reactions and message text
A tool wears a house SVG(toolIcon/toolGlyph, currentColor); a state wears a house emoji(tEmo — self-hosted Twemoji, never the OS font). A CTA button is pure text. A watermarked bubble is overflow:clip, never hidden.
The countdown — an instrument’s own clock v620
aka: .gc-clock / .gcl-v / clockRow(g) / deadlineOf(g) / syncGateClocks() · the dialog rung: .pk-seg[data-prop="close-by"] / #pkCloseRow · #rkCloseRow · #skCloseRow · <vote … close="3m"/> · 后截止
An instrument’s deadline is ITS OWN — never bound to the wall clock — so the card is its own clock: published once by the server, ticked locally, the digits holding their widest shape(tabular-nums + min-width)so nothing twitches. Set on the same Advanced rung in all three dialogs.
The bid box — the sealed collect T10–11 · the gate grammar
aka: .seal-card · #sealScrim · 密存 · the trigger(person · time · the last hand in)
kind="seal" — everyone deposits face-down; the trigger opens the safe. The reveal bubble is a deposit card, not a tinted message, and takes the message-bubble width by rule.
The ballot — the room’s vote T2 · poll
aka: .vo / .vo-bar / .gc-by / .av-stack / .gc-hide / .gate-none / .lg-slot / .pk-adv / .pk-seg / .votecap / #voteScrim · design: poll.html · <vote options="加辣, 不加辣, 弃权" anon/> · <vote … blocking="host" place="pinned"/> · <vote close/> · 投票 · the circle card's second KIND
A gate card wearing its opener’s face — live bars, avatar chips, a status band. A vote is final(🔒), and the closed poll is the same card, locked: the bubble IS the result. Three presence dials(blocking · place · unique); the countdown counts the server’s time, never the device’s.
The deal — the face-down card T3 · v625
aka: .deal-card(the poll's .gate-card grammar)/ buildDealCardInto / buildDealCap / buildDealRevealFull · <roll deck="Wolf×2, Seer, Villager×3" for="all" into="safe" dealer="sees"/> · <reveal who="u3"/> · /user_deal · 发牌 · randomizer × safe · LEGACY(pre-v625): .hand-card / #handCard / .dealcap
kind="deal" on the instrument store. Three surfaces, the card on only one at a time: my hand above the composer(tap to peek), the table card(who holds what, face-down), and the reveal — a deposit card.
The kit shelf — pick, then put on v661 · v969–v984 · cartridge method
aka: 玩法卡 · the + sheet's Kits tile · #kitScrimmodal-scrim poll-scrim as-page)/ .kit-row / .kk-live · GET /api/kits · POST / DELETE /api/rooms/{id}/kit · kits live in prototype/kits/*.md
A rulebook(玩法卡)mounted on the table, and a task dialog rather than a menu: Pick a kit on the left, Put on in the header's third slot. A row is a choice, not an act — the tick is a radio, so the room's one-kit-at-a-time rule shows in the control instead of arriving as a refusal, and the verb stays disabled until something is ticked. Every row states its floor in ink — needs 3+ people · 1+ persona — because the device narrates through a host seat, so a room with no persona cannot run even the utility kit. The live kit keeps its own verb and is not selectable.
The game device’s cards — every consent is a TAP 2026-08-04
aka: .kit-offer / .dial-choice / .ready-cta(three cards, ONE shell)· renderReadyCta · renderDialChoice / renderKitOffer · POST /kit_offer_tap · /dial_tap · /ready_tap · record_aside(action=True)
The offer card, the rule-choice card and the ready check — three cards, one shell. A card never disappears when it closes: it locks, showing it counted. The tap SPEAKS as the presser’s own italic bubble. Coral = a control you may press · grey = a statement.
The tool sheet — the guests’ own hands T4 · v604
aka: #toolsBtn / #toolScrim / .tg-grid / .tg-pill · 房间工具 · the composer's SECOND door · /user_roll_open · /user_deal · /user_seal_open · /user_vote · /user_board · /user_clock · design page: toolbox-icons.html
Touch, never syntax — and whatever a persona can do, a human can do(the parity law). A menu only: every tile opens the house setting dialog, and every action lands as the SAME furniture the panel arms. Refusals are spoken; the state refusal out-ranks the timer.
The board — pinned public state T6 · v642–672
aka: .bd-md / .bd-rows · #boardScrim · 状态 · the pen = the write token
A 状态 chip on the top bar; a markdown body through the app’s one render pipeline; a plain textarea for an editor, and it stays one. The pen(谁能改)is the whole permission model; rows may be objects the room updates line by line; one cap — BOARD_MAX 1200.
The pad — private notes T12 · v636–641
aka: #padScrim / #pkText / #pkOff / openPadPage() / ROOM_PAD / setRoomPad · <pad>…</pad> · <pad away/> · /user_pad · 记事 · --lilac · design: toolbox §13 · the sixth sheet
The board’s opposite — private, per-owner, invisible to everyone else. One field, a 2,000-char cap(it rides every turn), pinned via 状态. No SSE by design: nothing about a pad is broadcast.
Fair chance — the server rolls host-moves step 3
aka: .rollcap (removed v541 — the moment moved to the table)
A host requests randomness in its reply(<roll dice="2d6"/> · <roll pick="a, b, c"/>); the server rolls and posts the outcome — a model never invents a number, and the host narrates from a pending note next turn.
把一件事想清楚,是把它放回…
Swipe to reply
aka: 右滑引用 · WhatsApp's gesture
Drag a bubble RIGHT (touch only) — 1:1 to the 56px commit stake, then Apple’s rubber-band resistance — while a bare reply arrow fades in at the row’s left; crossing the stake ticks and fills the arrow solid, and release docks the quote (the same Reply as the long-press menu). Short of it, it springs back. Bubbles with sideways-scrolling content opt out.
梁宁把一件事想清楚,是把它放回它自己的语境里,再看它还成不成立…
Reply preview card
aka: the quote bar (.quote-bar) · WhatsApp's grammar
The docked quote (.quote-bar) pinned above the composer input — the speaker's name in their seat colour (matching the 3px accent bar), then up to 3 clamped lines of the quoted text, and an ×. On send it rides as an id-anchored marker line (❝{q:<lid>} — the quoted line's stable id, the same anchor-on-ids contract as @{p:slug} mentions); cleared on send or dismiss.
梁宁把一件事想清楚,是把它放回它自己的语境里,再看它还成不成立。你现在拿到的是别人的结论…
Which two? The invite step feels load-bearing to me.
Quoted block
aka: .q-ref · 引用
The reply bubble carries its quoted line above the message body (.q-ref): the speaker's name line in their seat colour — resolved from the anchored line's own bubble by data-lid, never the name — then up to 3 clamped lines; inside a human bubble it steps down to --bg. Tap → jump to the original message with a ring flash; a legacy un-anchored quote unfolds in place (›) instead.
CHAT RECORD · 4 messages
Ada: where do we start?
Kant: with the premise.
Forwarded chat-record
aka: .fwd-card · forward picker
A bundle of forwarded messages rendered as a chat-record card (.fwd-card) inside the target chat. Forwarding opens “Select a chat” — a standard sub-page (drawer header) listing every active chat as a real chat row (roomRowEl), searchable; tap → Send confirm (v790).
FLOOR PRODUCER · admin
王强 lead ≤120字 · Plato ≤65字 · push: medium
FP debug bubble
aka: the staging readout
Admin-only. An in-stream bubble showing the floor producer's per-turn staging — who leads, length envelopes, push level, mode/intent labels. Never rendered to a persona; the "all" tier shows a user-safe view. See Floor producer.
王强
founder · operator
tags · about · start a chat
Profile page · PEEK ♺ theme.css #25
aka: the persona card
A persona’s profile is the contact-page drawer (→ Terminology: profile). PEEK is the read-only twin the admin console opens via ?peek=<id> — the stream shows; composer, model switch and chats pane are hidden. Look, don’t touch.

Direct messages the zero-persona room — 1:1 · group · notes-to-self

A DM is the zero-persona room (v465) — the missing cell of the cardinality matrix, not a new subsystem. While the cast is empty all panel machinery is dormant (no kickoff · no floor producer · no LLM, pure SSE relay; the composer reads just “Message…”). Design: Proven Better New §gap 9.

M
Mary private chat
DM header
aka: the 1:1 title bar
The counterpart’s identicon + name + a muted “private chat” tag — rendered client-side from the immutable pair binding, never a stored title; notes-to-self reads your own. Tapping the identicon opens their profile; a 1:1 has no rename. Group titles derive per viewer from the other members; a real rename wins, and the 2→1 edge stamps the name so a group never loses it.
小胖看房 joined · sees the chat from here on
invite capsule · floor note · the duo→group note
aka: the “sees from here” capsule
Seating a persona in a 1:1 converts the room into a group and releases the pair binding — Message privately always reaches an AI-free thread. Scrollback stays; the room takes the persona’s name; the capsule says “· this is now a group — your private chat stays separate”. The next Message privately mints a clean 1:1.
A persona invited into a quiet room joins in place with its history floor at the join: dormant-era lines never reach the model. The join capsule says “· sees the chat from here on”, and the woken panel’s first turn is the arrival line — never a fresh-room kickoff.
or, describe your topic and call the panel
or, just people — New group
New-group door
aka: the human-group door
The hand-pick empty state’s second escape hatch — “or, just people — New group”: your contacts (no global directory; discovery stays social) with a pinned “Message yourself” row. One pick routes to the canonical 1:1; two-plus creates a zero-persona group, titled per viewer until a rename.
Message privately
on a co-human's profile page
Message privately
aka: the DM door
Tap a co-human anywhere → their profile → Message privately: opens or creates THE canonical 1:1 for the pair (one DM per pair, forever; a trashed one resurrects). Adding a human to a 1:1 always spawns a NEW group — never an in-place conversion. An empty DM is never pushed: the other thread begins with your first message.
🔕Mutekeep it, silence it
🗂Archivefiled, stays filed
🗑Deleteyours only · comes back
Duo-chat exits
aka: what a 1:1 has instead of “leave”
Leave and remove are group verbs — a 1:1 offers neither (server-enforced), has no owner badge, and keeps the proven set: mute · archive · delete. Delete is per-person — it leaves your list only, and a new message brings it back; Delete-forever refuses while another human remains.

User profiles a face, a name, one line — WhatsApp's contact-info anatomy

A human is a face + a name + a one-liner (gap 11 · v468). Tapping a person anywhere they appear — a bubble's name or avatar, a chat-info member tile, the 1:1 header's identicon — opens the same page. Visibility is room-mates only (the DM-able predicate, enforced server-side). Design: Proven Better New §gap 11.

Mary @mary
● online
“Renovating. Slow replies.”
Message privately · Shared rooms
human profile page
aka: the dossier · contact info
The persona page’s page (v717): photo-or-monogram hero (tap a real photo → the avatar viewer) · display name · @handle · the online dot · the doors as the persona page’s hero pills (v1053): Message privately (+ Add friend when it applies, greyed as Requested once asked) · the About line in the tagline slot · Shared chats › · Added from rows · Unfriend as the page-wide danger button below — the server sends ids only; rows render from the viewer’s own list. A repeat open paints instantly (SWR).
online
online dot
aka: presence
RAM-only presence, displayed: green while the person's per-user event stream is live (the app is open right now). Never persisted — not in state.json, not in the DB — and shown only when on (no grey "offline" state). Last-seen is deliberately out (a later gap, with the per-field privacy matrix).
→ photo, else → M
avatar photo
aka: profile photo
humanAvatar() layers the uploaded photo over the identicon — the monogram is the forever fallback — one function, every surface. One upload writes two tiers: a 256px thumb every surface renders, a 640px full copy only the viewer uses; both EXIF-stripped, served immutable under a versioned URL.
drag · pinch · zoom
crop editor
aka: move and scale
Picking a photo never uploads it blind — WhatsApp’s move-and-scale: a dark stage, the photo under a circular window, drag / pinch / wheel, cover-clamped so the circle never empties. Done ships only the framed square (≤1024px, upright, metadata-free): the frame is the file.
avatar viewer
aka: tap the face
A profile hero carrying a real photo opens full-screen on tap — FLIP: it grows out of the avatar circle (radius 50%→16px); tap / ✕ / Esc / back flies it home. Lazy by design: the 640 copy is referenced nowhere in the DOM — the viewer paints the decoded 256 thumb first and swaps the full in when it lands.
Edit profile
photo · display name · About (140) · live preview
Edit profile
aka: your profile
Me → the hero pencil or your photo → a main dialog: the photo picker (opens the crop editor, then uploads; Remove photo returns the monogram), display name, and the ~140-char About with a counter — all above a live “how others see you” preview, the signup identity screen's treatment. The About shows under the hero handle on Me, and to room-mates on your page.

Terminology the canon — one concept, one word

Every concept has exactly one user-visible word. New features inherit this table; renames land on every surface (app, pickers, Studio, console, manifest) in the same ship.

persona
The word for an AI participant, everywhere. Never character, voice, AI char, or bot. (“Character” survives only in its fiction sense; “voice” only for TTS audio and literary voice.)
panel
The personas of one chat, collectively. Never cast (code-side name only), crew, or lineup.
people
The humans. Copy about post-join visibility says newcomers. Never guests or members in visible copy; account age reads “Joined …”.
chat
The unit of conversation — everywhere a person can read it. v1002 swept the last holdouts: ~28 i18n keys(clock · tools · survey · people picker · caps · leave/remove confirms · search · the Studio checklist · login), the share poster’s canvas line(LIVE ROOMS → LIVE CHATS)and every user-facing error line(no such chat · not in this chat · a chat caps at {n}). ⚠ Two places keep「room」on purpose: code identifiers (roomId · /api/rooms · the state files) and the cost ledger’s closed room.* group, which the console mirrors — renaming that is a ledger migration, not a copy sweep.
Studio · build
Studio is the persona-creation surface; build is its verb. “The builder” never appears in visible copy (it's the internal route name).
profile
A persona’s page — a contact page (v709), three strata since v1047 (the design): who — hero (avatar · name · local name · role · the door as a quiet outline pill,「Private message him / her / them」from the card’s one pronoun word, the short name when absent), the one-liner on its seat-coloured bar; wrote — the Ink shelf in Ink’s own row grammar (up to three standard feed rows — no lead card since v1052 — All N pieces › at four or more, hidden at zero; a tap opens the feed’s reader above the page); you & themProfile › (first of the list since v1048) · Shared chats › · Added from · Lately › (ex Recent updates, v1051) · Memories about me › · Follow-ups › · Vibes ›. No page-wide button on a persona page any more. Shared chats are drawn by roomRowEl from the viewer’s own room list, so a chat they cannot see is never named.
Ink reader
The essay sub-page and its Background page (titled「Source」until v1088; the look). Under the byline an EN / CN pill (v1069) when the piece has a second language — the shown side pressed, a rendition marked「translated from …」beneath; it switches that article and its Source page only. Both pages carry a : the reader's = Language · Feedback… · Light/Dark · − A +; the Source page's = the same without Feedback. One text size for all of Ink (list · article · Source), remembered per device; the default language for everything is Me › Settings.
Call the panel
The verb for summoning the panel — button and prose (“we'll call a panel for you”). Convene is retired.
Invite · Add
On an existing chat the membership pair is “Invite people” / “Invite persona” (and “Remove people” / “Remove persona”). Add is reserved for composing a new chat's panel (“Add to your panel”, “add a persona”).
Studio · Popularity
The owner filter and the usage sort, spelled exactly this way on every persona list (pickers, Studio browser, console). ⚠ Not「Built」— that word is already the builder's final stage.
My QR code
Your code, and the only carrier it has. It encodes ish:u:<code> — not a URL — so nothing outside this app can act on it. One verb: Refresh, which kills every copy already out there. Drawn by one builder into two hosts.
Scan (扫一扫)
The camera door — third on the ⊕ menu, and a row on Add contact. ⚠ Three ways in and no others: a chat you share, an -ish ID you type, a code you scan. Anything else the camera reads is shown, never followed.
the ⊕ menu
One button, one question, on every tab: New chat · Call the panel · Add contact · Scan.
search row
A ROW whose whole content is the field. ⚠ Named for the house, never for a page — a shared object named after its first owner is a copy waiting to happen.
pill · rect
The two button languages (v359): a coral pill is a floating call-to-action (the new-chat “Call the panel”, the profile “Send message”); a rect radius-10 button is an in-dialog action (Start chat, Done, Cancel — .btn-primary/.btn-ghost, the console's .adm-btn). Both are defined once in theme.css.
a badge is presence
A count is gone at zero — never a grey nought, and never a coral one.
an empty list is not a group
The band and its hairlines are furniture for ROWS. The group unpaints itself when the empty line is its only child — a card beside it is real content and still wants the band.
a click on the left is answered at once
On wide, pressing anything in the left pane closes whatever owns the right — the whole chain, however deep. ⚠ The sweep asks the DOM what is open; it does not consult a list.
Notes · Seen · Vibes · Energy
Feature names are fixed proper names — never paraphrased.
Default language
One setting rules both axes: what the panel speaks in new chats and the app-UI language (the row’s sub-line reads “App and new chats”). UI copy is English-as-key in lib/i18n.js; brand names never translate; lib/i18n_audit.py + the smoketest gate every surface. The console + md-viewers stay English by design.

Vocabulary terms, not pixels

the panel
The room's AI personas, collectively. “Ask the panel”, “the panel confers”.
confer
The panel's backstage deliberation before answering — 3 passes through the panel, flowing/unlabelled. Surfaced by the confer capsule + drawer.
seat colour
A persona's colour is a per-room seat, not an identity — handed out by seating order (rotated per room) from the 8-slot hybrid palette (coral·teal·green·orchid·gold·slate·violet·rose, the two purples kept apart for contrast). Same persona, another room, maybe another colour.
known-as
A persona's short label — the compact name they're recognised by (surname Euler, handle MrBeast, initials FDR, iconic first name Elon). A panel chat lists each member by their known-as; a solo chat uses the full name. (@-mention still finds casual aliases via tags.)
panel cap
A room seats at most max_personas personas — admin-set, default 5; 8 (the seat-slot count) is only the hard ceiling. At the ceiling the picker warns “a room caps at N AI personas”. (The old solo / large-panel footnotes were dropped.)
system grey
The non-identity tone for everything that isn't a persona: the egg, telemetry, the leave capsule, notes, your own bubble.
megaprompt
The engine: one model voicing the whole cast from a single cached context. (Not shown in the UI; it's what “the panel” runs on.)
fork
Cloning a chat to a fresh “<name> (fork)” that carries the model, language, panel, and the whole conversation. The original is untouched; the two diverge from the fork point.
artifact
A non-dialogue block a host hands over — document, code, table, image, note, diagram, or a web dispatch. Filed as a card in the wildcard drawer, reached by a link chip in the host's bubble.
aside
An @human-only line — posted to the room but the panel doesn't answer it (no turn, no egg). A side conversation between people the hosts overhear.
peek
A read-only view of any room the admin console opens via ?peek=<id> — the stream shows, but the composer, model switch, and chats drawer are hidden. Look, don't touch.
theme
The app is light / dark only — no e-ink (e-ink lives only in the room2 design docs). One toggle themes the whole app at once, including the embedded Studio; there is no per-view theme.
hero collapse
The iOS-large-title scroll effect: a big hero header (avatar + title) fades and recedes as the content scrolls, while a compact “(avatar) title” fades in centred in the sticky bar (which carries no hairline). Used by the chat-info title and the persona page. Scroll-driven, direct handler (no rAF).
the shared layer
theme.css — tokens (palette · fonts · radius/space/shadow · the 5-step avatar scale) + the once-defined components (dialog scaffold + .dialog-x, search chrome, popover base, pills, toast) — and fmt.js, the one display voice (ago · row time · en-GB dates · tokens · money · durations). Every page links both with a ?v=N buster: bump it alongside BUILD whenever either file changes.