← gallery

DESIGN.md — the ruling ledger

This file is the authority on any design question. It is not a description of the mocks; it is the set of decisions the mocks and the client code both follow. When this file and a mock disagree, this file wins and the mock is stale. When this file and the client code disagree, the code is a bug.

What this file is not: the authority on behaviour. That lives in ../../docs/project/. If a ruling here implies a mechanic the spec contradicts, the spec wins — report the ruling as the thing to fix.

The concept is Voice Studioa practice room, not a game. Warm paper, five identity pastels each holding one fixed job, black as the only action colour, and a verdict palette that stands on neutral ground and never lies about coverage.

The screens beside this file are not a mood board. They are the reference set every other screen is derived from, so a value here must be usable as written. If a mock needs a mental correction table before you can build from it ("the type is small because it's a gallery"), it has stopped being a reference. Everything in this file is stated at production scale.

Lineage

ElementCame fromWhy it won
The whole visual systemvoice-studio concept, v2Founder's pick, 2026-07-29, after the Manhua direction drew repeated "boring · hardcore · too classic · looks like a mock". Voice Studio reads as competence rather than as a game, which is the position an app whose edge is speaking should occupy.
Three verdict colours + a deliberate absencedocs/architecture/speech-scoring.md wire enum `green \amber \red \uncertain \na`The absence is not cosmetic — it is the difference between "you got it" and "we never heard it". na is a slot that does not exist rather than a verdict; see §2.2.
Magenta #E8368F as CLOSEManhua ledger, 2026-07-20, carried forward unchangedRetired amber twice over: it went brown on warm paper, and amber/red collapse under red-green colour blindness on precisely the distinction the product's trust rests on.
Verdict values #25845F / #D44749Voice Studio v2, 2026-07-29Supersede the Manhua ledger's #12A150 / #E5252A, which were tuned for pure white under black ink outlines and read acidic on warm paper.
data-grade / data-roll attribute mechanicVoice Studio v1One attribute drives pinyin, diacritic, grade bar, verdict word and character together, so a screen cannot disagree with itself.
Path geometry — one node size, six-node wave, v1 spacingFounder ruling, 2026-07-29Size-based hierarchy read as slightly bigger; a star reads as a destination.
Retired directions (manhua, voice-studio v1, the rejected 2026-07-28 rebuild)../archive/Not canon. Do not mine them for look. An agent that reads the archive for "the design" will confidently rebuild a rejected direction.

Reference build: the screens beside this file. When a number here is ambiguous, open the screen.


1. The position

It looks like a practice room, not a game. Duolingo-shaped design promises fun; this promises competence. That matches an app whose entire edge is speaking, and it is a position almost nobody in the category occupies.

The failure mode to design against is not "boring" — that one is fixed. It is too calm. A product that never celebrates is as untrustworthy as one that celebrates nothing real. The answer is not to scatter colour; it is to spend it in one place (§5.4).

Four principles, unchanged in substance from the first direction:

  1. 1. Path is home. The goal and the next useful practice are visible on arrival — visible, not

present below the fold.

  1. 2. Context, not decoration. Photography appears where a person or place makes practice feel real,

and nowhere else. A principle used on one screen out of eight is not a principle.

  1. 3. Focused black. Navigation and primary actions turn solid black only when active. Black is the

action colour; a pastel never is.

  1. 4. Adaptive speech presentation. Short speaking targets break into parts; conversation stays

continuous.


2. Colour

Colour carries exactly two kinds of meaning here, and they must never be confused: which thing this is, and how you did. Everything below follows from keeping those apart.

2.1 Identity pastels — which

Each pastel has one job across the entire product and keeps it on every screen, forever. This is what makes forty screens derivable from eight.

TokenValueMeans, everywhere
peach#F6D9B2goals and the path — the journey
rose#F2BDC3speaking — your voice, the hard modality
lilac#CECBF7conversation and people
sky#D7E8F6listening
mint#BCEBD9the library — study sets, saved things
paper / panel#FBF8F3 / #F0ECE4structure, ground, anything neutral

A pastel may never be chosen because a card needed a colour. If four study-set cards are four different pastels for variety, the system is already broken — set categories are identified by icon and text (§2.5, rule 6), not by hue.

Consequence, and it is a feature: home becomes colourful honestly. It shows a goal (peach) and a route made of speaking, conversation and listening steps (rose, lilac, sky). It earns its colour by showing several kinds of activity, not by being decorated.

2.2 Verdicts — how you did

Three colours, and one deliberate absence of colour. The absence is the one that gets forgotten, and forgetting it is expensive.

StateStrongSoftMeaning
CLEAR#25845F#D9F0E6the judged part landed
CLOSE#E8368F#FDE2EFrecognisable, a repair remains
FIX#D44749#FDE4E4a different sound or contour came out
not scoredink #111015locked #F1EFE9no verdict — not said, not picked up, or no model output

Ink is not "correct". Ink is "we did not score this." The learner skipped the syllable, the mic missed it, or the model returned nothing. A cleared part must never render the same as an unsaid one, or a dropout reads as a perfect syllable — which is the worst lie this screen could tell, because it is a false green, and it is invisible.

The unjudged part therefore keeps ink type and leaves its bar on locked, the empty track. The absence has to be visible as an absence; a grade colour of any kind would hide it.

The wire has five values, and they do not map one-to-one to the four rows

Grade is a closed enum: green · amber · red · uncertain · na. Three of them are the three colours (amber is the wire name; CLOSE magenta is the colour — the enum is the contract, the colour is a design ruling, and neither gets "fixed" to match the other). The other two are not a fourth and fifth colour, and the difference between them is load-bearing:

— a take with one uncertain part is 12 of 13 scored, and that missing thirteenth is exactly what the coverage rule exists to disclose.

have no initial to score, so there is nothing there to grade, disclose, or colour. It is excluded from the denominator — which is the whole reason the canonical sentence is five syllables · 13 parts and not 15.

Collapsing those two would be a real defect in both directions: counting na as a miss would invent a hole the learner cannot fix, and rendering uncertain as absent would hide one they need to know about.

There is a sixth state that is not a wire value: the pre-score state the mocks call unjudged, worn by every part of a sentence before a take has been judged at all. It renders identically to uncertain — ink type, locked bar — because at that moment it means the same thing.

The parser fails closed to uncertain on anything unrecognised. An unknown grade must never become a colour.

Close is magenta, not amber. The #A86B00 amber read as brown on warm paper — muddy where it most needed to be unmistakable. Worse, amber and red collapse toward the same dark yellow-brown under red-green colour blindness (~8% of men), and CLOSE-versus-FIX is precisely the distinction the product's trust rests on: a learner who cannot separate them gets the false-red experience this app exists to avoid. Magenta separates from red on the blue axis and survives. It also dissolves the peach≈Close collision the previous direction had already flagged, and it agrees with the Manhua ledger, which had already retired amber by name in 2026-07-20 — so this is a value carried forward, not a new one.

Green is quiet — but quiet is an emphasis decision, not a hue decision. Colouring eleven good syllables green to mark two problems buries the two problems. The tempting fix — make CLEAR ink — is wrong, because §2.2's fourth state already owns ink: a cleared part and an unsaid one would become indistinguishable, and the product would be quietly claiming credit for syllables the mic never heard.

So green stays, and the emphasis is bought elsewhere: on a good syllable the bar and the verdict word go green while the character stays ink. Only a part with work left in it colours its character. The eye lands on the two problems because they are the only characters wearing colour, and nothing has to lie about coverage to achieve it.

The character joins the green in exactly one situation: every part judged and clear. That is not a verdict on a component, it is the aggregate over all of them — so a perfect take is the one time the whole target lights up, and it earns the moment (§5.4). A take with a dropped syllable has not earned it, and is excluded.

Green also carries aggregate success outside the analyzer — goal complete, session complete, progress fill — where the message is genuinely "you did well".

2.3 Operational alert — outside both layers

Tools need to say something went wrong, and it is not about pronunciation. CAPTURE FAILED — STOP THE SESSION must not borrow FIX red; a verdict colour appearing outside a verdict destroys the one rule that makes the analyzer readable.

TokenValueMeans
alert#B4261Ean operational failure — deep, desaturated, unmistakably not the analyzer's red

Reserved for tool surfaces and system failure. Never on a learner's score.

2.4 Grades stand on neutral ground

A verdict colour only ever appears on paper or white. Never on a pastel.

This is the single largest visual correction from the first direction, where the analyzer's tokens sat on a rose panel: green vibrated against pink, red lost its alarm inside its own colour family, and amber went muddy. In conversation it was worse — the learner's scored bubble was mint and the graded pinyin inside it was green, so the good result was the least readable thing on screen.

An identity pastel may frame, head, or border an analyzer region. It may not be the ground beneath a graded syllable.

2.5 Rules

  1. 1. Pastels identify regions and content types. Verdict colours identify verdicts. Neither borrows.
  2. 2. A decorative surface does not change colour because an answer became right or wrong.
  3. 3. Analyzer interiors are white or paper, always.
  4. 4. Every verdict colour is accompanied by a word or another non-colour channel. Colour is never the

only carrier.

  1. 5. Verdict colours are never reused for ratings, categories, navigation, or charts. A ★4.9 rating is

ink — painting a good rating in the "needs work" colour is the exact error this rule exists to stop.

  1. 6. Study Set categories are identified by icon and text, never by pastel alone.
  2. 7. Black — never a pastel — is the active-action colour.
  3. 8. At most two dominant pastel regions in a viewport.

3. Type

The previous mocks ran 7–9.5px because they were drawn to fit a phone inside a gallery. On a 390pt frame those are literal point sizes, well under Apple's 11pt floor — and the single most valuable sentence in the product, "You said s. In 是, sh needs the tongue slightly behind the ridge", was set at 9.5. That is a magazine's type ratio applied to an app.

The mocks now carry the production scale. This costs density and the trade is correct.

RoleSizeWeight
Display / onboarding headline28–34500–600
Root page title26–30500–600
Lesson title18–22600
Section heading18–20600–700
Standard body15–16500–600
Supporting body14500–600
Metadata / caption12 floor600–700
Button label14–15700
Pinyin in a scored target15–16700
Hanzi in an analyzer28–34600
Hanzi in an answer choice20–24600

Pinyin is learning content, not metadata. Chinese and pinyin never shrink to preserve a no-scroll layout — the screen grows instead.

Small, grey, and heavy is the one combination to avoid. Much of the old micro-copy was ~8px at weight 800 in muted grey on a pastel, which is the hardest legibility recipe there is. Below 14, drop to weight 600 and use ink at reduced opacity rather than muted on colour.

Hierarchy comes from proportion and moderate weight. Weight 900 is not the default voice here — that belongs to the retired direction.

Faces: Hanken Grotesk for Latin UI and pinyin, Noto Sans SC for Chinese. Human and editorial for the interface, dense and dependable for the characters.


4. Shape, elevation, spacing

Three surface levels, and nothing else:

  1. 1. flat pastel panels for structural regions — no shadow;
  2. 2. white cards with a hairline and one subtle low blur for contained content;
  3. 3. black-filled controls for the current or committed action.

Radii: 15–18 controls and small cards · 20–22 standard panels · full pills for filters and compact status · circles for icon and voice controls.

Spacing: 4 · 8 · 12 · 16 · 18 · 24 · 32. The mobile gutter is 18. Exploratory 7s, 9s and 13s do not become tokens.

Expressive geometry is allowed in exactly two places. Onboarding's rotated, asymmetric, overlapping tiles are the most memorable image in the set, and in the first direction they appeared once and then every subsequent screen became an axis-aligned rectangle. That composition now also carries the home goal card, so the brand's most distinctive gesture survives past sign-in. Analyzer panels, tone graphs, articulation diagrams and dense controls stay regular — always.


5. The learner surfaces

5.1 The route is the best asset — and it is also the pitch

The curve is load-bearing. A serpentine route with alternating nodes reads as a journey; the same five items in a straight column read as a checklist, and a checklist is what every other app has. This was flattened once, to win vertical space, and the trade was wrong: the space bought was worth less than the feeling sold. Keep the curve.

Home is a conversion surface, not just navigation (founder, 2026-07-29). People open the app and read the path to work out what this thing can eventually do for them. So a screen showing five steps toward one goal undersells the product — "Goal 1 of 8" asserts an arc the screen never shows, and the user has to take the other seven on faith.

So the route does not stop at the current goal. It keeps going, all the way to the last one:

What is drawn
Current goalits lessons, named and tappable, then the goal
Every later goalthe goal, at exactly the same size and weight, with GOAL n · m STEPS stating what is inside it

Geometry — the four rules (founder, 2026-07-29)

  1. 1. One node size for everything. Lessons and goals are the same disc. v1's ~58px, v1's spacing,

v1's connector weight — that combination read better than anything since and is the reference.

  1. 2. Current is marked by a ring, never by size. Nothing is bigger because it is next.
  2. 3. A six-node wave: the trail sweeps one way for three nodes and back for three. Node 1 sits at an

extreme, so the pattern is legible from the first screen rather than emerging over a scroll.

  1. 4. 13 nodes: the current goal's five lessons plus its goal, then the seven remaining goals. Five

lessons plus a goal is exactly one wave cycle, so the rhythm carries meaning instead of being decoration.

Size is no longer available to say "milestone", so the mark must. A goal is a star on peach; a lesson is its activity icon on that activity's pastel. That single substitution carries the whole distinction — a star reads as a destination in every game ever made — and it survives at one size, which size-based hierarchy would not have.

Completed goals stay on the path, above the current one, at the same size, filled with a check. The learner should see the ground they have covered; a path that always restarts at goal 1 is a menu. (Not drawn in the reference mock, which shows the honest first-run state where nothing is behind you.)

Rejected: drawing every lesson of every goal (~48 nodes, ~6 screens). It serves the daily-return job well and destroys the conversion job — the eight capability titles end up six screens down — and it cannot be honest until all forty lesson titles exist. Duolingo affords that shape because its home screen is not its conversion surface. Ours is.

Nothing fades and nothing shrinks (founder, 2026-07-29). An earlier draft receded the far goals to smaller, lighter type, which reads as "it gets vague out here" — and that is a lie. The path is pre-authored. We know exactly what is in it, including the last goal, so drawing uncertainty we do not have is dishonest in the one place the user is deciding whether to trust us. A distance ramp is also a convention borrowed from apps whose far content genuinely is unwritten; ours is not.

What legitimately changes with distance is detail, not emphasis: later steps are not unknown, they are merely not actionable yet, so they are not drawn — and the step count is stated instead, which discloses rather than hides. Drawing all forty nodes is the other honest option and was rejected for a product reason: it buries the destinations the user is scrolling to find.

One continuous scroll, one gesture, no second view that can drift from the first. No zoom mode, no map screen. If a jump is ever wanted it is a "see all 8 goals" link on the goal card, and it waits for evidence that someone wants it.

Goal titles are therefore capability claims, not lesson numbers — "Order food without pointing" rather than "Unit 4". Down the scroll they are the product's promise, so they carry the weight of it.

A track is a goal sequence, not a course (founder, 2026-07-29 — keep the chips). Recommended · HSK · TOCFL · Traveling stay on home. They are curated orderings of goals over one shared atom library, resolved by the one lesson assembler — not four parallel curricula. This matters because it is the difference between four playlists and four products: the expensive assets are atoms and assembly, and all four tracks draw on the same ones.

Two consequences worth stating so they are not rediscovered:

learner who switches from Recommended to HSK keeps every atom they have strengthened. Only goal completion is per-sequence. Switching is therefore cheap and safe, and the UI should never warn about it.

failure on the exact surface we have just decided is the conversion surface. A chip ships when its sequence exists.

This supersedes docs/project/learning_overview.md's "one course per L1 only" and needs propagating there, with the goal-sequence definition above, so the line is not read as permission to author four separate courses.

Nodes are differentiated by icon, not by hanzi. The steps were told apart by 说 / 聊 / 听 / 呢 / ★ — characters our learner, a Vietnamese beginner at HSK 1, cannot tell apart by definition. That is the whole reason the app exists. The icon (microphone, speech bubbles, ear, star) is primary; a character may remain as small secondary texture, never as the sole differentiator. Each node takes its activity pastel from §2.1, so the route also reads as what kind of work is coming.

The goal card, not the goal node, is what delivers "path is home." The peach card at the top states the goal and its progress on arrival. The route's terminal node may therefore sit below the fold — that is not a failure, it is the invitation to scroll, and past it lies the rest of the arc.

5.2 The speaking screen

The syllable token — pinyin over hanzi over verdict word, one grade bar — is the best component in the system. It looks like an instrument rather than a card. Keep it exactly.

Corrections: the token row stands on paper, not on rose (§2.4). The score is the screen's headline number and is not buried bottom-left beneath the tokens, sharing a row with two lines of grey micro-copy. Two large saturated panels no longer stack hard against the tab bar.

data-grade / data-roll remain the mechanism: one attribute tints a syllable's pinyin and diacritic together and drives its grade bar, verdict word and character, so a screen cannot disagree with itself.

5.3 Charts encode or leave

The profile's seven pastel bars had no axis, no scale, and colours that meant nothing — decoration shaped like a chart, sitting two screens from the tone-contour graph, which is beautiful precisely because every mark carries information. Single series, one neutral fill, a real baseline, readable values. Multi-colour pastel bars that encode nothing are retired.

5.4 One win moment

A practice room that never celebrates reads flat, and this direction runs that risk more than most — across a lesson there is roughly one moment that feels like a win. The celebration is motion and scale plus green reaching the characters, spent at the all-clear take and at goal completion, and nowhere else. One real moment beats eight decorated ones — and because the characters are otherwise ink on a good syllable (§2.2), the moment costs nothing to set up.

5.4b Onboarding is the demo, not a form (ruled 2026-07-29)

Eight screens, two of them real games, no placement test. The order is the ruling:

language → why now → Game 1 → login → mic → Game 2 → where you're starting from → path-day-one

Game 1 is unfailable and pre-auth — for a vi learner it is Read the character with the Hán-Việt reveal, which says you already know more of this than you think; for en it is Match. Game 2 is the learner's own voice scored at word grain, on the real analyzer. A rendering of the analyzer is what every competitor's landing page shows; running it on their voice inside two minutes is the thing only we can do. It is §7's "show the evidence before making the claim" at the cheapest possible price.

The login precondition — raised, and met (founder, 2026-07-29). The gate was that login could not move to slot 4 while it was an email address plus a six-digit code, because an out-of-app round trip cannot sit immediately before the first speaking moment. Identity is now four paths — Apple, Google, Facebook, and continue with email — so one tap reaches slot 5 and the condition is satisfied. Login sits at slot 4. Game 1's assets stay bundled/static so the pre-auth screen needs no network.

Three rulings on the front door follow from that, and they are visible on login.html:

  1. 1. One-tap leads; email is the fallback beneath a divider. Order is Apple · Google · Facebook —

Apple first on the platform that requires it to be offered at all once third-party sign-in exists, Facebook last because it carries the most permission friction, not because it matters least. For a Vietnamese beachhead it may well be the most-used of the three.

  1. 2. Brand glyphs are ink monochrome. Google's mark is four brand colours and Facebook's is a blue;

either would be a sixth and seventh hue in a palette where every colour already holds a fixed job. A brand glyph does not get to spend the product's colour.

  1. 3. No black anywhere on this screen. The email button was drawn black and it beat three one-tap

options sitting above it — a full-black fill outweighs position, so the screen read "sign in with email, or else these three", which inverts the intent. There is no committed action on a front door: every path is a first tap. Black is the action colour and it returns the moment there is an action to mark. This is the one screen in the product that legitimately has none.

5.5 No clock while the mic is open (ruled 2026-07-29)

Capture screens show no elapsed time — learner or teacher. Not in a banner, not on the control, not as a bar filling toward a ceiling. This holds for every recording state in both flows.

The distinction that makes it precise: duration is a fact about a take you have, not a display while you are making one. Listen back · 2.4s is correct and stays everywhere it appears — it labels which recording is about to play, and on the teacher side it is part of the saved receipt. A running counter is a different object wearing the same units. It sets a budget nobody set, it invites watching a number instead of finishing the sentence, and on a three-second utterance it measures nothing anyone can act on. Nobody in this category ships one; the products that do are voice memos and messages, where the length of the artifact is the point.

Two consequences worth naming:

The ceiling is a failure boundary, not a budget. A cap exists, it sits far above any real utterance — well past the 9.8s stress sentence — and reaching it means something went wrong (mic left open, phone in a bag), not that the learner talked too long. So it is never displayed, never warned toward, and never counted down. It auto-stops and the take is handled like any other.

Duration is never used as feedback. v1 scores segmentals and tone; it does not score speed. A take-to-take duration delta would be feedback about a dimension we do not measure, and it points the wrong way — a rushed take flattens exactly the tones the screen is trying to clear. speaking-retry.html compares what was repaired, not how fast it was said.

And say the state once. Recording is the one state a screen can prove rather than assert, so it is said on the element that proves it: the level meter, which goes still when the mic is dead, the phone is face-down, or the learner is inaudible. A banner above it saying "Recording now" is a claim that stays true while the meter is lying, which makes it worse than absent. One announcement, on the measurement — the control is a control, not a second status line.

This is also where the mock format leaks into the design and has to be watched. A still frame cannot transition, so state gets written down to be legible in the gallery — and that compensation survives into the spec looking like a decision. In the running app the transition announces itself. When a screen carries the same fact in three places, ask which one of them the build would need.

6. The tool register — teacher, rater, admin (ruled 2026-07-29)

The teacher, rater and admin surfaces are derived from this system, not designed separately. Inventing a second visual language for the tools is the failure this section exists to prevent.

6.1 What the tools actually are

Not a teacher product. They are the internal shell for the recording study: under ~20 people, all personally reachable, on builds we control. CLAUDE.md invariant #11 governs every decision here — protect the corpus, forgive the room. What reaches the corpus fails closed and gets a mechanical check. What goes wrong in the room gets a retry and a sentence, not machinery. No recovery choreography, no self-service account flows, no offline prefetch, no graceful-degradation paths.

Three capacities, and they are an independent set, not a hierarchyoperator, rater, and admin are separate claims one person can hold together. There is no role picker: you sign in at the one front door, the server returns your claims, and the client routes silently.

6.2 Two densities, one system

comfortable is the learner default. compact is the tool register: the same tokens, the same components, tighter spacing, a higher information rate. An operator reads their screen fast, in a bright room, with a student waiting.

LearnerTool
Page groundpaperpanel — a structural tint, so a tool never reads as a learner screen
Content unita card, one thing at a timea row, many things scannable at once
Gutter1814
Pastellarge regions, identitya tint for zoning only, never a large field
Type15–16 body14–15 body — the 12px floor does not move

A tool screen is built by switching density and reaching for the same primitives. The four vocabularies the learner screens never exercised — the persistent tool header, the dense data row, the field/form, and the operational notice — live in src/app.css, authored once.

One learner screen legitimately borrows the row: settings.html. It is the only learner surface that is genuinely a list of changeable facts rather than one thing at a time, so it takes .row instead of growing a local twin. Nothing else about the tool register comes with it — it keeps paper, the tab bar, and learner type. Borrowing a single primitive because the content is shaped like it is fine; reading this as licence to build learner screens out of tool rows is not.

6.3 The tools are marked by structure, not by a colour of their own

The retired direction gave staff surfaces a deep slate-blue accent nobody else could use. That is dropped. A sixth colour with one job is a sixth thing to keep straight, and it was solving a problem that does not exist — a roster of student names is not mistakable for a learner screen. The mark is the panel ground plus a header that states the mode in words. Structure is cheaper than a token and it survives being photographed in a bright classroom, which is how these screens are actually debugged.

6.4 No verdict colour on a tool surface — with one principled exception

Capture is not scoring. Errors are data, never marked. The sentence a student is reading renders in the shared analyzer tokens uncoloured, always. The scoring result that comes back is not represented on an operator screen at all.

So what carries "saved" and "failed"?

aggregate success outside the analyzer. A durability receipt is exactly that. It says the bytes landed; it says nothing about the speech.

take that did not upload is an operational failure, and borrowing the analyzer's red for it would destroy the one rule that makes the analyzer readable.

screen must never say Saved while the audio behind it is still on a device.

6.5 The language on screen follows the reader

The operator and rater are Vietnamese teachers, so their screens are drawn in Vietnamese — one language on screen, no bilingual hedging. The English source lives in the ARB.

Admin is the exception, and it is English, because its only reader is the founder. That is a deliberate split, not an oversight: a surface localised for an audience of one is cost with no reader. docs/2026-07-26-build-handover-admin.md §5 currently says admin is Vietnamese "like every other surface" — that line needs correcting, and it is listed in §13.

And the copy never uses research vocabulary. Never data, AI, model, research, corpus, participant, subject. Say student, saved / failed, "the app needs to hear…". The teacher is a colleague running a lesson, not a research instrument.

6.6 What the tools may never do

false-start takes are corpus data, never operator-discardable.

trusted collaborator; the product reminds, it does not gate.

not in the corpus, not in a queue, not in an export.

capture, take, participant, session or catalog. Destructive corpus operations stay deliberate SQL: slow, visible, and requiring intent. An admin UI that can mutate corpus rows is a corpus-integrity hazard with a comfortable interface on it.


7. Doctrine that carries forward

Show the evidence before making the claim. A score appears after audio the learner can play back. Rewards name what was actually done.

Name the exact component. Feedback identifies initial, final or tone — never "your pronunciation was wrong."

Be honest about coverage. When a dimension is not judged, the screen says so rather than implying a verdict. Uncalibrated is stated, not hidden.

Preserve learner agency. Every score can be replayed, retried, and reported as wrong.

Copy is plain and specific. Vietnamese is the interface language: it runs ~25–30% longer than English and its diacritics need vertical room, so every component is built to wrap rather than to fit one measured string. No component may depend on a text length.

Motion arrives, it does not perform. Content settles in with a short stagger; prefers-reduced-motion is honoured everywhere.

Accessibility floors. Text floor 12. Every verdict carries a non-colour channel. Contrast is checked on the ground the element actually sits on — which, for a verdict, is always paper or white (§2.4), and for a tool notice is panel, not paper.

Touch targets are 44, and the mocks draw the visual, not the hit area. .pill is 34px tall because a 44px chip is a badge the size of a button, and most pills in this system are labels. Where a pill is genuinely tappable — a filter tab, a track chip — the built control gets a 44px hit area from padding around the same 34px visual. The mock is not wrong; it is drawing the smaller of the two boxes. Say so in the client, or someone will "fix" the mock by making every chip enormous.


8. What is retired

The Manhua direction, entire (founder, 2026-07-29). Black ink outlines, hard zero-blur offset shadows, dot-screen paper, heavy Inter at weight 900, imperial yellow #FFC91F as the signal colour, press physics. It drew boring · hardcore · too classic · looks like a mock often enough to be a verdict rather than a mood. The set is in ../archive/manhua-2026-07-29/.

What survived the retirement, so it is not re-litigated: the analyzer grammar (pinyin above hanzi, initial and final as independently graded spans, the drawn tone mark carrying tone's own state, the aggregate bar under the character), magenta CLOSE, the coverage contract, the vocabulary, the anti-features, and the demo cast. The look did not survive. Reskin freely; keep the mechanic exactly.

Also retired:

on warm paper. #25845F / #D44749 replace them everywhere, including in client code.

instead. (Making CLEAR ink outright was considered and rejected: ink is the fourth state.)

alone own — they are marked by density (§6), which is what a teacher actually notices.

8.1 State machines in a mock (ruled 2026-07-29)

A mock may not animate itself into a second state. Three of the original screens carried a JS state machine — speaking.html swapped its coaching panel on a token tap and played a whole retry sequence; conversation.html simulated recording and scoring; choice.html painted answers right and wrong. All three are gone.

The reason is not purity. Every one of them had drifted, and in the same direction: the retry labelled its take 2.6s where the ledger says 2.2, the conversation turn ended on 92% where its own number is 96, and choice.html's wrong-answer copy told the learner to "listen once more" — sending them back to the audio with the same wrong model of what they heard, when the miss was comprehension, not hearing. None of it was caught, because a state you have to click into is a state nobody reviews. The gallery is scrolled, not clicked.

So: those states became files — speaking-recording, speaking-tone, speaking-retry, conversation-feedback, choice-wrong — and what remains in a mock's script is affordance feedback only: a play button showing it was pressed, a copy button confirming, a control navigating to the sibling that owns the next state.

One consequence worth stating. speaking.html coaches one repair — 是's initial — and merely names the second: "still to do after this: 南's rising tone stalled halfway", linking to the screen that teaches it. Two coaching panels on one screen is two instructions, and the learner acts on one. That is "one change at a time" applied to layout, not just to copy.

9. Open, and deliberately so

it as a defined slot: an image may be the prompt on a speaking exercise, generated onto reviewed sentences only (content_model_overview.md §2 rule 6). Mocks draw real sample images from public sources rather than grey boxes. One trade is accepted knowingly: a screen carrying a remote image no longer opens fully offline from file://, which is why lesson-picture.html says so in a comment and no other screen depends on the network.

flow is not yet decided; the learner screens currently do not use it.

operator chrome are on Voice Studio; mobile/test/theme/app_tokens_test.dart parses this system's app.css and fails the build if the two drift. What remains is per-screen judgement, tracked in ../../docs/2026-07-29-design-pivot-plan.md.

Two things about that migration are worth knowing before touching the client. signal and signalSoft still exist and are retired — they were Manhua's imperial yellow, they are parked on ink and panel so ~30 call sites keep compiling, and each site gets a real answer as its screen is visited. Do not use either on anything new. And the widget names did not changeSheet, Well, InkChip are Voice Studio primitives now; 19 screens import them, and renaming would have turned a skin change into a sweep.

after 2026-07-29 — deliberately deprioritised below the client reskin, since the Flutter widgets they would inform already exist and are being re-skinned rather than written fresh, so the fork-it-three-times risk this was guarding against is smaller than it looked)*. Each now appears on three or more screens, which is the threshold this system uses. They are listed here rather than promoted in the same pass that drew fifty screens, because folding them in means editing every screen that carries a twin, and that is a sweep with its own risk — worth doing deliberately, once, rather than under the same clock as the drawing.

WantedWhere it already exists
.verdict-word — the non-colour channel §2.5 rule 4 mandates on every graded thingthe four marked exercise screens, plus choice.html's style map
.answer — a marked answer row, border-good bg-good-soft / the fix twin, with its letter chipfour copies across the exercise screens
.take-row — play control, label, value; the evidence row a score always followseight instances across the speaking and completion screens
.stat-tile — icon square, number, defined labelprofile, lesson-complete, goal-complete
.check-disc — the green aggregate checksix on goal-complete alone
the pre-speak token — ink type, bar on locked, part-count in the verdict word's slotevery pre-speak and failure state

uses panel fill with a muted label for you have not done the thing yet; the speaking waits use black at reduced fill for the action is committed and the app is finishing it. That distinction is real and probably correct, but it is currently a convention rather than a ruling.


10. Demo data — the state catalog

Every mock draws from this one cast, and the numbers must reconcile across screens. This is what stops fifty mocks from quietly disagreeing about how long the streak is. Carried forward from the Manhua ledger where the value was content rather than look.

The learner

FieldValueAppears on
NameAn Nguyễn — "An" in greetings, 安 in Chineseprofile, path, conversation
JourneyVietnamese → Mandarin · learning since January · near zero at startprofile
Speak-streak12 days (only counts if you produced scored speech that day)path, profile
Spoken in total3.5 h — the learner's own voice only; model playback and reading time are not countedprofile
Parts cleared412profile
Words to review12path
Speaking minutes by monthJan 14 · Feb 19 · Mar 26 · Apr 31 · May 33 · Jun 39 · Jul 48profile
Next useful repairThird tone — dip before rising. Shows up in 好 hǎo · 你 nǐ · 我 wǒprofile, progress
Saved recordings48 · study sets 3profile
Day-1 zero stateno streak number, no chart, route at 0 of 5, step 1 is the only doorpath-day-one

The canonical sentence

FieldValue
Hanzi我是越南人。
Pinyinwǒ shì Yuènán rén
Gloss"Tôi là người Việt Nam." — Vietnamese in every locale; it is stimulus, not interface copy
PromptMei asks: 你是哪国人?
Take 189% · all 13 parts scored · 2 to repair. 是 — initial sh said as sFIX. 南 — tone 2 stalled halfway → CLOSE. Everything else CLEAR. Take 2.4s.
Why 13 and not 15five syllables × three parts = 15, minus the two zero-initial syllables 我 and 越, which have no initial to score.
Take 296% · 2.3s. 是 came back CLEAR; only 南's tone is still CLOSE. This is the middle of the loop and the reason speaking.html only names the second repair — the tone gets its own screen once the first repair has landed.
The retry100% · all parts judged and clear → data-all-clear, the one win moment. Take 2.2s.
Uncertain case南's tone returns no verdict → 92% · 12 of 13 parts scored. Ink mark, bar left on locked. No celebration on a partial.
Below-floor case是 wholly unscored (dropout, 3 parts) → 10 of 13 = 77%, below the 80% floor → no score at all, an honest note, and the retry is free.

The stress sentence — 22 characters

我昨天晚上跟朋友一起去了一家很好吃的中国饭馆 · wǒ zuótiān wǎnshang gēn péngyou yìqǐ qùle yì jiā hěn hǎochī de zhōngguó fànguǎn · "Last night I went with friends to a really good Chinese restaurant." 98% · all 61 parts scored · 2 to repair — 吃 initial FIX (c for ch), 馆 tone CLOSE (the dip flattened). Take 9.8s. The screen it lives on proves the token grid wraps and the score never hides.

The goal ladder — 8 goals, and they are capability claims

GoalSteps
1Introduce yourself naturally. Say where you're from, catch the reply, and ask someone back.5 · in progress, 8%
2Order food without pointing.6
3Ask for directions and understand the answer.5
4Handle prices, numbers and bargaining.5
5Make small talk about work and family.6
6Get through a phone call.4
7Explain a problem — and be understood.5
8Hold your own in a real conversation.7

Goal 1's five steps, in order, each with its activity pastel: Speak your answer (rose) · Use it in conversation (lilac) · Listen and choose (sky) · Lock in the words (mint) · Ask someone back (rose) — then the goal conversation, on peach.

Tracks: Recommended · HSK · TOCFL · Traveling. About 24 min for goal 1.

The cast

WhoRole
Mei · 美玲 Měilíngthe conversation partner in the tea-house scene. 你好!我叫美玲。
An · 安the learner

Conversation demo: a tea house, 4 turns, currently turn 2 of 4. The learner's scored turn is 你好,我叫安。at 96% · 12 of 13 parts clear — 好 CLOSE (tone 3 stayed flat), take 2.1s.

Study sets

SetOriginNumbers
Restaurant survival · 点菜verified · Travel32 phrases · 18 spoken clearly · 56% · picks up at 辣
The first 150 words · 词official · HSK 1 · Speaking150 words · 9,530 learners · ★4.9
Getting around Shanghai · 行verified · Travel · Conversation64 phrases · updated this week
Introduce yourself at work · 聊official · Work · Conversation38 phrases · 1,463 learners · ★4.8 · 12 min
Ordering without pointing · 茶verified · Food · Listening52 phrases · ★5.0 · 18 min

Ratings are ink, never a verdict colour (§2.5 rule 5), and they live in the card footer, never in the origin slot. Discovery shows only official and verified sets.

The tool cast — the recording study

The learner cast above does not appear on a tool surface, and that is the point: these are different people doing different work.

WhoWhat
cô Hườngthe operator. Runs the sittings, remotely, without the founder present.
Chauthe founder. Who you message when the app stops on purpose.
R0 · R1 · R2 · R3the rater slots. A rater is a slot, never a name — R0 is Chau's own.

A student is a name in the roster and a code of two unaccented Vietnamese wordssong-xanh, nui-cao, tre-mem, gio-nam, dat-lanh. Not p01; that scheme was replaced on 2026-07-26 and survives only in two printed paper sheets that still need correcting. The app draws the code; the teacher types the name. Names live in the roster and nowhere else — never in the corpus, never on a rating surface, never in an export.

The roster on tool-home.html: Nguyễn Thu Hà (song-xanh, B, Bắc, 12/23) · Lê Bảo Ngọc (tre-mem, Z, Trung, 23/23 with 3 still uploading) · Phạm Quốc Anh (gio-nam, C, Bắc, 2 failed) · Vũ Hải Đăng (dat-lanh, B, Bắc, 5/23) · Trần Minh Khôi (nui-cao, B, Nam, complete).

A sitting is 23 clean sentences + a 5-sentence noise block = 28 takes, each student walking their own server-shuffled order.

One thread runs through the tool set, and it is deliberate. The console flags Lê Bảo Ngọc as 23 of 23 with three takes still uploading; tool-record-pending.html is that phone mid-sitting; and admin-sitting.html is the founder opening that same sitting — 25 saved of 28, three still on the phone, completion refused. The screens are not three unrelated demos of the same feature, they are one afternoon. A code bound to two different names across two screens would be the exact corpus error these tools exist to prevent, so the cast reconciles or the mock is wrong.

The rating stimulus is study content, not learner content. tool-rating-item.html uses 你好,我叫小明 · nǐ hǎo, wǒ jiào Xiǎo Míng, judging 你 — chosen because it carries the 3-3 sandhi (a third tone before another third tone is read as a second), which is the case the protocol most needs a rater to get right, and which the learner cast's sentence does not contain.

The meter and the money

20 scored speaking attempts per day on free — the smallest number that lets a free learner finish one whole lesson including retries. Pro $14.99/mo · $99.99/yr, and the annual charge is the primary figure with $8.33/mo as the equivalent. Not $7.08 — that was developer-net math and must never appear as a user-facing monthly price. Lifetime is out of v1. Trial is 14 days, annual SKU only, started from the paywall, and never before the learner has completed a lesson.

Three rules the paywall screen must obey, all from subscription_overview.md:

and creating sets, reference audio, progress, before/after, the tone board.

finishes; corrective retries on the active prompt are always included. Toni never says a sound is wrong and then asks for money before showing the repair. The wall sits between sessions, never inside one.

preserved, the reset time — then the offer. A coach, not a toll booth.

Unscorable attempts and reconnections consume nothing; the placement test is never charged.


11. Vocabulary in the UI

Use the product's words, no synonyms. From ../../docs/project/content_model_overview.md:

atom (one word) · sentence (that atom in context, per band) · exercise (one screen of practice) · lesson (6–10 exercises, always ending on a speaking exercise) · study set · band (beginner / intermediate / advanced) · goal · track · origin (official / verified / user).

Screen-level names: Speaking lesson · Conversation lesson (scripted) · Speak-streak · Toni. A lesson objective is a can-do statement — "I can tell someone who I am" — never "Unit 3 Lesson 2" and never a grammar point.

The nine exercise names are reserved words (ratified 2026-07-29). Seven families carry them; learning_overview.md holds the catalog. On screen they are used for exactly the exercise they name and nowhere else:

FamilyName(s)
Speak a known targetSay it
Comprehend, then speakYour turn
Assemble, then speakBuild & say
Listen and chooseWhich one? · Which tone? · Catch it
Cross-form vocabularyWord check
The character as an objectRead the character
Pair upMatch

"Your turn" is the loose one to watch. It reads like a generic "your go", and it was already being used that way as an eyebrow on speaking-prompt.html and on the image-prompt screen — both of which are Say it. If an exercise hands the learner the target sentence, it is not Your turn; Your turn is the one where you must understand a question and choose a reply before you speak.

Never: card, deck, quiz, level (say band), assessment (say attempt), credits, hearts, XP (say what was actually done — "412 parts cleared", not "340 XP").

Anti-features — do not helpfully add one: no hearts or lives · no content gating on free · no handwriting or stroke-order production · no leagues, leaderboards or social feed · no free-response conversation · no dark theme.


12. Scope

V1 ships: the speaking lesson and every one of its states · the home path · the other exercise types · the conversation lesson · study sets including paste-your-own · profile and progress · the metered paywall · the teacher tools.

V2 deferred: the boss-gate / unit-test mechanic (drawn as a goal node, no pass-threshold spec exists) · a web surface — mobile is the product.

Explicitly out: dark theme · hearts and lives · handwriting production · leagues and leaderboards.


13. Mocks that contradict the spec

The spec wins. Each of these is a bug in the mock, to be fixed when the screen is next touched — do not implement them.

MockShowsSpec says
sets.htmlStar ratings (★4.9) on study setsstudy_sets_overview.md:36-45 rules "No ratings. There is no rating field in the model, and none ships — discovery ranks on real LearnerCount/ForkCount only." The founder kept the rating in the UI anyway (2026-07-29, "keep the rating for study set"), so this is a spec gap to close, not a mock to change: the spec needs a rating field, plus a ruling on who may rate, how a cold-start set renders without one, and how it is moderated. Until then the mock is ahead of the model.
sets.htmlNothing — this row records a fixed violationThe same ruling also says user-made sets are never discoverable (private/unlisted, shareable by link only). The discovery list was corrected on 2026-07-29 to show official and verified only. Do not reintroduce a "learner-made" shelf.
path.htmlFour tracks — Recommended · HSK · TOCFL · Travelinglearning_overview.md:137 says one course per L1. §5.1 rules a track is a goal sequence over one shared atom library, not a parallel course — which reconciles them, and needs propagating into the spec so the line is not read as permission to author four curricula.
conversation.htmlNothing — this row records a closed contradictionThe content model had no conversation layer. It now does: content_model_overview.md §4b defines ConversationScene — ordered turns, prompt audio, canonical reply targets, review state — reusing the speaking engine at turn grain, with no branching, memory or repair state. The mock is buildable as drawn. (2026-07-29-review-adjudication-lesson-games.md)
profile.htmlSeven months of speaking minutes to a free learnersubscription_overview.md tiers progress history as current week on free and full history on Pro. One of the two is wrong and it is a product decision, not a mock fix: either the chart gets a tier clamp, or the spec drops the clamp because a seven-month proof-of-progress chart is the single best retention argument the product has and paywalling it is self-defeating. progress.html deliberately carries no tier chrome pending the call.
admin-sitting.html, admin-teachers.htmlEnglish copydocs/2026-07-26-build-handover-admin.md §5 says the admin surface is Vietnamese "like every other surface". §6.5 rules it English — its only reader is the founder, and localising for an audience of one is cost with no reader. The handover line is the thing to correct, not the mocks.
set-paste.html6 lines → 5 practice targets → 8 wordsstudy_sets_overview.md illustrates the same list as 9 words, but a real segmentation of 我想去中国 gives four (我 · 想 · 去 · 中国), so the total is 8. The screen shows its own segmentation and is internally consistent; the doc's worked example is the thing to correct.

14. Building a new screen — agent checklist

  1. 1. Link the shared system: <link rel="stylesheet" href="studio.css"> and

<script src="shared.js" defer></script>. Wear the chrome (<studio-status>, and the tab bar or the tool header). Never re-copy tokens into a screen; never hand-write a status bar.

  1. 2. One HTML file per screen, Tailwind utilities in the markup, so the screen reads top to bottom

in one file. Anything appearing on three or more screens is promoted into src/app.css instead.

  1. 3. One dominant job, phrased as a question, and one state. A state that needs a tap to reach

is a sibling file, not a hidden display:none — the gallery is reviewed by scrolling, not by clicking (founder, 2026-07-29). Two jobs means two screens.

  1. 4. Wrap in the phone frame (.device-stage.phone.app, 390×844) and wire every href,

including the tab bar's active state.

  1. 5. Demo fidelity: real hanzi, real copy, real numbers from §10. No lorem ipsum, no grey boxes.
  2. 6. Animation ideas go in an HTML comment (<!-- MOTION: … -->), never in JS — and must obey §17.
  3. 7. Spec-check the behaviour against ../../docs/project/ before drawing

it. A new contradiction goes in §13; it does not go in silently.

  1. 8. Rebuild: cd .. && npm run all. A utility class that was not in the markup at build time does

not exist in studio.css, so an un-rebuilt screen silently drops the style.

  1. 9. Give it an ORDER slot, a BLURBS job line and an APP_GROUPS section in

../scripts/build-gallery.py.

  1. 10. Open it from file:// and confirm it stands alone.

15. Where the per-screen job statements live

Every screen has one dominant job, phrased as plainly as possible, and that line lives in BLURBS in ../scripts/build-gallery.pynot here. It is single-sourced on purpose: the gallery is the surface those lines are actually read on, and a second copy in this file would be a second thing to keep true. If you cannot write the line, the screen probably does two jobs and should be two screens.

This file rules the decisions; the gallery states the jobs. When they disagree, this file wins.


16. Handover to implementation

Three sources of truth, by question:

QuestionSource
How should it look?this file + the screens beside it
How should it behave?../../docs/project/the spec wins
What is the data shape?the generated OpenAPI DTOs — never a hand-written model

17. Motion (ruled 2026-07-29)

Every screen carries a <!-- MOTION: … --> comment saying what moves on it. Those are the specifics; these are the rules they all obey. An implementer who reads only this section will not be wrong.

1 · Arrival is a stagger, not an entrance. Content arrives in reading order, 50–70ms apart, on --animate-arrive (.42s, cubic-bezier(.2,.7,.2,1), 8px rise). One pass, on first render. It is how the eye is given an order to read in — nothing more.

2 · Nothing loops or fills that is not measuring something. The level meter moves because it is reporting the microphone. A spinner reports nothing, and a progress bar that fills on a timer reports a guess. If it moves continuously, point at the quantity it is tracking, or delete it.

3 · A state hands over; it does not get replaced. Between two states of the same screen the shared card must not re-lay-out — the change is a 250ms cross-fade inside a frame that holds still, so the take reads as being passed along rather than swapped for a different screen.

4 · An absence does not perform. The ghosted bar, the unscored syllable, the empty track: these are the product saying we do not know, and animating them would dress a hole as an event.

5 · Celebration is spent once. Scale and green reaching the characters belong to the all-clear take and to goal completion (§5.4), never looped, never on anything smaller. An exercise that congratulates itself devalues the speaking win, which is the only one that was hard.

6 · Nothing shakes, flashes, or bounces. A wrong answer is information, not a failure event. On a correction the learner's choice settles first and the truth follows ~100ms behind, so it reads as an answer being given rather than two results arriving at once.

7 · No spinner where the wait is short. A spinner on a fast operation advertises a delay the user had not noticed. Show the thing that is actually happening, or show nothing.

8 · Playback says two things, and they go in two places. A listen-back control that is playing has to answer what does my next tap do and how far along is it, and one indicator cannot carry both. The icon is the affordance: play becomes stop, because while audio is running a tap stops it, and an affordance the user has to remember is one the control failed to state. The fill is the measurement: the pill fills left to right with the playhead, which is a real quantity, so it is the one thing here rule 2 licences to move. The label keeps the take's total duration and never becomes a running counter — §5.5 rules that duration is a fact about a take you have, and the fill already carries position. When the length is genuinely unknown — a stream still arriving — the icon changes and nothing fills: an indeterminate bar in that slot would be the guess rule 2 forbids, dressed as the measurement beside it. The end of playback needs no separate state; the fill runs out and the icon returns, which is the whole event.

9 · prefers-reduced-motion draws the end state, complete. Every animation here has a legible final frame; that frame is the fallback. Nothing in this product is only understandable in motion.