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/ ../../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 Studio — a practice room, not a game. A neutral near-white ground, five identity pastels each holding one fixed job, black as the only action colour, and a verdict palette that stands on that 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.
| Element | Came from | Why it won | ||||
|---|---|---|---|---|---|---|
| The whole visual system | voice-studio concept, v2 | Founder'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 absence | docs/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 CLOSE | Manhua ledger, 2026-07-20, carried forward unchanged | Retired 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 / #D44749 | Voice Studio v2, 2026-07-29 | Supersede 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 mechanic | Voice Studio v1 | One 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 spacing | Founder ruling, 2026-07-29 | Size-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. | ||||
Neutral ground — paper #FAFAF9, panel #F2F2F0, locked #EFEFED | Founder ruling, 2026-08-07, on Stepo's 2026-07-12 temperature precedent ("neutral app, warm human moments") | The warm cream (#FBF8F3 / #F0ECE4 / #F1EFE9) is the default an image generator reaches for, and it made the app read as AI-made on arrival. Canvas and chrome go neutral; warmth is now earned, not ambient — it lives only in the identity pastels, the illustrations and the verdict grounds. Exactly three tokens moved; nothing else in the palette changed. The magenta CLOSE ruling survives this. One of its two reasons (amber went brown on warm paper) no longer applies, but the other — amber and red collapsing under red-green colour blindness on the CLOSE-vs-FIX distinction — is ground-independent. Amber stays retired; do not re-propose it because the paper changed. |
Reference build: the screens beside this file. When a number here is ambiguous, open the screen.
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:
present below the fold.
and nowhere else. A principle used on one screen out of eight is not a principle.
action colour; a pastel never is.
continuous.
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.
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.
| Token | Value | Means, everywhere |
|---|---|---|
peach | #F6D9B2 | goals and the path — the journey |
peach-deep | #EDB87C | peach's accent for a peach child on a peach field — a mark that must stay visibly path when its ground is already peach (born on the plan beat's done-coin, 2026-08-13; never a second meaning, only depth) |
rose | #F2BDC3 | speaking — your voice, the hard modality |
lilac | #CECBF7 | conversation and people |
sky | #D7E8F6 | listening |
mint | #BCEBD9 | the library — study sets, saved things |
paper / panel | #FAFAF9 / #F2F2F0 | structure, 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.
Three colours, and one deliberate absence of colour. The absence is the one that gets forgotten, and forgetting it is expensive.
| State | Strong | Soft | Meaning |
|---|---|---|---|
| CLEAR | #25845F | #D9F0E6 | the judged part landed |
| CLOSE | #E8368F | #FDE2EF | recognisable, a repair remains |
| FIX | #D44749 | #FDE4E4 | a different sound or contour came out |
| not scored | ink #111015 | locked #EFEFED | no 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.
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:
uncertain is the ink row. We tried and got no confident answer. It stays in the denominator— a take with one uncertain part is 12 of 13 scored, and that missing thirteenth is exactly what the coverage rule exists to disclose.
na is not a state of a part; it is a part that does not exist. The zero-initial slot. 我 and 越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 load-bearing reason is colour blindness: 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. There was a second reason, and it is now history: the #A86B00 amber read as brown on the warm cream ground the app used until 2026-08-07. That ground is gone; the ruling is not. Amber does not come back because the paper changed — the colour-blindness reason never depended on the ground.
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".
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.
| Token | Value | Means |
|---|---|---|
alert | #B4261E | an operational failure — deep, desaturated, unmistakably not the analyzer's red |
Reserved for tool surfaces and system failure. Never on a learner's score.
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.
only carrier.
ink — painting a good rating in the "needs work" colour is the exact error this rule exists to stop.
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.
| Role | Size | Weight |
|---|---|---|
| Display / onboarding headline | 28–34 | 500–600 |
| Root page title | 26–30 | 500–600 |
| Lesson title | 18–22 | 600 |
| Section heading | 18–20 | 600–700 |
| Standard body | 15–16 | 500–600 |
| Supporting body | 14 | 500–600 |
| Metadata / caption | 12 floor | 600–700 |
| Button label | 14–15 | 700 |
| Pinyin in a scored target | 15–16 | 700 |
| Hanzi in an analyzer | 28–34 | 600 |
| Hanzi in an answer choice | 20–24 | 600 |
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.
Three surface levels, and nothing else:
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.
The syllable token has one width, and it is a fifth of the row (founder, 2026-08-04: "the two cards 面条 are too wide — design as if five cards fit the width, and with only two you get what 2/5 looks like"). The analyzer's token is the same object whether it is drawing a five-syllable sentence or a two-syllable word, so it stands in the same five-slot track (grid-cols-5, 6px gaps) at every length; short words leave the rest of the row empty. Fixed pixel widths chosen to look right at one length are the failure this replaces — 92px was invented for single-word screens and made 面条 read as a bigger, different component than the same word inside a sentence. The one exception is a single token paired with an explanation column (lesson-say-wo, onboarding-mic): that is a two-column composition, the token is a specimen being annotated before the learner speaks, and it keeps its 92px. A row of tokens is never that, and neither is a scored take (§5.2b).
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.
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 goal | its lessons, named and tappable, then the goal |
| Every later goal | the goal, at exactly the same size and weight, with GOAL n · m STEPS stating what is inside it |
v1's connector weight — that combination read better than anything since and is the reference.
extreme, so the pattern is legible from the first screen rather than emerging over a scroll.
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). Recommended · HSK · TOCFL · Traveling 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.
The track lives in the header eyebrow, and the four chips are retired (ruled 2026-08-13). A 34px pill cannot carry a promise, three of four said nothing, and the strip spent the row above the goal card — the conversion row — on a control the learner touches roughly never. Every home surface now names its track in the eyebrow, in ink, with a down chevron — "Recommended track ⌄", "HSK track ⌄" — opening path-picker.html, where one rich card per track carries its promise, one real detail, and the feature set that track has and the others do not.
Three consequences worth stating so they are not rediscovered:
AtomProgress is keyed (UserId, AtomId), so alearner 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.
(A1, 2026-08-13). Both halves are one rule read correctly: the build serves only tracks whose sequences exist, and the mock draws the target state, all four live and equal. Dimmed cards and COMING SOON tags are the language-shelf defect on a third surface; rollout order is an ops fact.
published track the chevron and the tap are absent — not dimmed, not disabled: the affordance promises a choice, and a picker holding only the route the learner is already standing on is a door onto the room they are in. The name stays, because naming the route is true either way. The count is the server's (PathDto.trackCount), so the door appears the moment a second route is published, with no app release. This is the same rule as the bullet above read from the other end: availability is never drawn, and a control for an unavailable choice is drawing it.
nothing on the screen may claim HSK progress. The one number that is allowed is the word meter — "you can already say 28 of 300 New HSK 1 words" — because it is a fact about her library, not her position, and it is honest only because the track's spine can eventually reach all 300.
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.1 rules the route's geometry; this is what each node is allowed to say, settled once the client drew them for real.
A completed node's mark is a check in place of its activity icon — ratified as designed-in-code. Same disc, same size, no fade, no second badge and no label beside it. §5.1's nothing fades and nothing shrinks governs history exactly as it governs distance: what a step was does not change by having been done, so done is a mark, not a dimming.
A distant goal states its planned step count and invents nothing to fill it. GOAL n · m STEPS takes m from the count the route declares (plannedStepCount) — never from counting the lesson rows a goal happens to carry, and never by authoring placeholder lesson titles so the arithmetic comes out. A placeholder row would wear the ledger's authority while naming nothing, and §5.1's promise is that the far end of the path is known: stating the size is the disclosure, inventing its contents is the lie.
**A step that is not built yet reads Đang dựng, and that state is internal.** It exists so an internal build cannot draw Start over a step with nothing to start — the navigation lie §8.2 rules against, drawn on the home screen. A learner release does not ship a path with unbuilt nodes, so the state is inherently a staging safeguard rather than a learner-facing node state, and no learner screen or mock draws it.
An unbuilt step is transparent: it neither opens nor walls the steps behind it, and the forward CTA skips what cannot start and offers the next step that can. (Ratified 2026-08-01 under engineering necessity — an unbuildable step 2 would have sealed every step after it, making the round's new content unreachable. The same-day review concurred on exactly this scope: good for staging, never a production curriculum rule — a published path hides what is unpublished, or the learning path becomes a visible release plan. Founder ratification still open: it is a product rule made to unblock a build, recorded so it is visible rather than ambient.) It does not trip the locking tripwire §10 records, because Đang dựng is staging-only: nothing a learner sees is un-startable, so a fully coloured node still means available.
The quiet-review row is hidden at 0 due. A learner with no queue has no fact to state here, and 0 words to review draws an absent history as a failing score. At more than 0 due the row is live and opens the quiet session (built 2026-08-13) — the row and its number ship together with the session, because a row that says 12 words promises twelve.
Above 20 due, the greeting's review line is replaced by an offer — never joined by one. The standalone Review lesson is a real metered lesson over due words (learning_overview.md ../../docs/project/learning_overview.md), so past that threshold the count stops being a fact stated in passing and becomes one mint card under the goal card and above the route: secondary in every channel, because the goal card stays the primary door, and carrying no verdict colour, because due is a fact about the clock. At 20 or fewer the home is unchanged, and the threshold is the server's (reviewLessonAvailable), never an arithmetic the client did. Drawn: path-review-due.html.
An interest pack — the pre-authored lessons keyed to the driver a learner answered in onboarding — arrives on the route as a bonus stop, drawn beside the trail rather than in it. It was first drawn as an ordinary node on the spine while the goal badge refused to count it, on the grounds that a pack is injected rather than authored; authored-vs-injected is an internal distinction, and a stop standing in the route's own line that the route's own count ignores is a screen disagreeing with itself.
The contract, and every clause of it is checkable on the drawing:
disc. Nothing downstream waits on it — it can never block — and the trail runs past it unbroken.
the count is right and nothing on the line is uncounted. Route minutes count the spine; a bonus stop wears its own +4 min on its card, where the learner is deciding whether to take it.
other card on the route promises something the learner will be able to do, and a stop that names a topic instead is an advertisement in the route's clothes.
Order food goal.
work. There is no "personalised" hue and there is never a peach one — peach is a goal.
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.
Every speaking exercise ends in the same result: the §5.2 token row, wrapping in fifths, over the percentage. Say it, Your turn and Build & say are one card in the build, and a study set deals the same card the path does — so there is exactly one layout to get right, and it is the one speaking.html already draws.
What this replaces, and why the earlier reading was wrong. From 2026-08-01 a scored take was one row per syllable: the token, and beside it a chip naming each part the scorer judged. The argument was §7's name the exact component — a screen that can only say this syllable cannot say which part. That argument was sound about a bare token and wrong about this one. The token is not silent about parts: data-grade tints the initial, the final and the drawn tone mark independently, which is the whole reason 是 can show a green sh under a magenta tone in the same glyph. The chips were restating in words what the token was already saying in colour — and they cost the layout its scaling. At one syllable the row fits. At the four or five syllables a pasted class phrase actually carries, the result becomes a column of stacked verdicts that has to be scrolled to be finished, and the percentage — the thing the learner came back for — is pushed off the bottom.
So: one row, wrapping, tokens always a fifth of the row wide (§5.2), percentage under it. The component is still named — in the token's own spans, and in the one-change-at-a-time card that follows, which says which part to move in a sentence rather than in a chip.
Absence still reads as absence. A part that does not exist has no span to tint (the zero-initial slot, na in §2.2), and a syllable nothing could be judged on carries an unfilled grade bar — never a grey verdict word, which would report a measurement that was never taken.
One token paired with an explanation column survives, and is not this. lesson-say-wo and onboarding-mic annotate a specimen before the learner speaks; that composition keeps its 92px per §5.2. A scored take is never a specimen.
Listen-back appears only when the take's audio is held. The take is kept in memory for the exercise on screen and forgotten when the lesson closes — nothing is written to disk — so the control is present when there is something to play and absent when there is not, never a dead button on a take that has already gone. §7's show the evidence before making the claim is what the control is for; a button that plays nothing makes the claim without the evidence. The retention rule itself is behaviour and lives in speaking_overview.md ../../docs/project/speaking_overview.md.
Every speaking card opens with one line naming what is being asked for, above the tiles that take it apart. What that line holds depends on whether the tiles are the whole thing.
| Grain | The line | Why |
|---|---|---|
| Word | 越南 · Yuènán · “Việt Nam” | the tiles are syllables, so this is the only place the word stands whole — the repetition is the point (lesson-say-yuenan.html) |
| Sentence | the meaning alone — “Tôi là người Việt Nam.” | the tiles are the sentence, in its own hanzi and its own spelling, at the size the card celebrates it at |
At sentence grain the three-way line printed the utterance twice and its spelling twice, joined by · into a bold run-on that wrapped to three lines the moment a sentence ran longer than 越南. The meaning is the one fact the tiles cannot carry, so it is what the line is left holding.
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.
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.
The mechanism is drawn, not implied — and it is the star, not the mascot (founder, 2026-07-31). At goal completion the celebration is the star medallion at hero scale — the route's goal-1 node, filled, on a white coin with a green halo and a burst of green sparks. At the all-clear take it is the green win panel with a modest spark burst beside the sentence. A mascot celebrate state was tried in both slots and overruled; the win element is always the star/green mechanism, never her. She may ATTEND a completion (goal-complete seats the encouraging pose at 168px under §5.4c's floor idiom, arriving only after the coin's stamp) but she never replaces, scales, or moves the win by proxy — enlarging the coin so the mascot could be bigger was drawn and rejected on exactly that rule — and she renders identically whatever the numbers were (score-blindness, §5.4c). Two rules make the end frame honest without motion: a win surface never wears repair pink — the panel that says "You fixed both" in the fix colour tells the eye something went wrong — and green must be visible in the final frame, because prefers-reduced-motion (§17.9) and every screenshot see only that frame. A completion element styled identically to an unstarted one — the same chip, the same grey tile, a check the only difference — is listed, not achieved, and fails this rule.
Seven moments, one of them the learner's own voice, no placement test. The order is the ruling:
login → language (1/4) → mic (2/4) → the take → why now (3/4) → where you're starting from (4/4) → the plan → path-day-one
The rhythm is two questions, the aha, two questions, the receipt, the reveal (founder, 2026-08-13). The four dots promise four asks; anything else in the middle of them is felt as an extra step rather than a break, so the flow now runs its asks in two clean pairs with the take — the one thing only this product can do — between them, and closes on a receipt. The Hán-Việt game is cut (onboarding-hanviet-ask → onboarding-hanviet, retired to ../archive/): wedged between why-now and the band it made a four-dot walk read as eight steps. Its content is not lost — Word check is the second exercise of the prescribed first lesson and the Hán-Việt bridge lands on the Từ mới cards that open it (learning_overview.md ../../docs/project/learning_overview.md, the first-lesson table), which is where a learner now meets it. The plan beat's wall does the there-is-more-than-one-game work in one glance.
The language shelf is five, and it is drawn as done (founder, 2026-08-13). Ask 1 offers English · Tiếng Việt · Español · Bahasa Indonesia · 한국어, every row live, equally tappable, each in its own native name with its sub-line already written in that language. There is no dimmed row, no SOON tag and no coming-soon state on this screen. The mock draws the target state — the design assumes the capability is finished, exactly as every other surface in this gallery is drawn — and the build decides which of the five renders on a given day. Rollout order is an ops fact and never appears on the screen. (The track picker's never offer what is not authored rule is the same rule read correctly: it governs which rows the app renders, and was never an instruction to draw a half-finished shelf.) A first draft of this round shipped six dimmed SOON rows and was replaced the same night: a row that names a learner's language and then refuses it is worse than a shelf that has not reached them yet.
The consequence panel hangs off the answer that caused it (ruled 2026-08-13; extended to ask 1 the same day). On all three asks that carry a consequence — language, why now and where you're starting from — the panel used to sit at the foot of the option list, which put the answer and its meaning at opposite ends of a scroll. It now sits directly under the selected row, inside one container: the black border and the lift move from the button to a wrapper, the row rides on white at the top of it, the pastel fills the bottom, overflow-hidden clips the corners, and the unselected rows continue underneath as separate cards. Four things follow and they are the idiom:
the pastel inside it is an identity colour, never a verdict.
attach peach, because a route's order is the path. The language screen attaches mint: the Hán-Việt head start is vocabulary the learner already owns before opening the app, which is the library. Attachment is a layout rule, never a colour rule.
reading I fly in three weeks — and "Because you chose Tiếng Việt" under the row reading Tiếng Việt — is the tap read back a centimetre later; the attachment does that pointing now, so all three eyebrows say what the panel is for: "What that changes".
already on screen was cut on the reflow, not preserved — the goal screen lost the prose summary of its four goal rows, the language screen lost "中国 sounds like Trung Quốc", which is the two tiles above it read aloud.
And the panel is per-selection, which is what attachment makes checkable. Detached at the foot of a list, a consequence can quietly outlive the answer that produced it; hanging off the row, it cannot. The mocks each draw one state (§8.2) — onboarding-language draws the Tiếng Việt state, and the Hán-Việt head start is that row's consequence and no other row's.
The exam driver forks the walk, and both walks are complete (ruled 2026-08-13; the block codex returned the packet on). A branch that can be reasoned about but not read is not designed, so the gallery carries two chains and every href on each lands where its label promises:
| ask 3 | ask 4 | the receipt | the reveal | |
|---|---|---|---|---|
| A · every other driver | onboarding-goal | onboarding-band | onboarding-plan | path-day-one |
| B · the exam | onboarding-goal-hsk | onboarding-band-hsk | onboarding-plan-hsk | path-day-one-hsk |
Each file draws one selected state (§8.2), so an unselected option row is an <a> into its own sibling rather than a radio nobody can turn (§14 r3). The walk is the unit under review: a plan that names a track the goal screen did not choose is the incoherence walking catches and per-screen review does not.
Walk B is the same question — where does the route start — with a different consequence, because the second question should visibly have heard the first. Its rows report knowledge, not certificates: New to Chinese · I know basic words and short sentences · I know 1,000+ words and can handle a simple conversation. Certificate rows over-credited the papers in circulation — sittings still run the old HSK, where a level-2 pass is roughly half the vocabulary the new syllabus attaches to the same label — and a learner read against the wrong word list starts one band too high, which is the placement error the amber canon exists to prevent. It is not a fifth ask, and no "which HSK are you sitting" question exists anywhere in the flow.
And the exam driver gets no interest pack. A pack carries a driver's distinctive residue; the exam answer swaps the whole sequence, so the syllabus is already the content. onboarding-plan-hsk therefore assembles two checklist lines rather than three — inventing a row to keep the rhythm would be the one dishonest line on a screen whose whole claim is this was assembled for you.
A self-reported band is only safe if being wrong is symmetrical (ruled 2026-08-13). Both band screens carry "Too easy? Jump ahead in one tap" and, in the same card and the same quiet register, its counterpart: "Placed too high? Step back the same way." The over-placed learner is the one the amber canon protects, and until this round her only recourse was Profile.
Sign in first, then the take ahead of the questions (founder, 2026-07-31). The take sat at moment 6, behind why-now; it is the one thing only this product can do, so it runs as early as it can — after only the door and the two things a take needs, the language and the mic. The door is the first screen, for simplicity: everything behind it runs on an account, so no anonymous take, no attach-at-sign-in machinery, no pre-auth surface exists anywhere. Why-now and the band come after the learner has heard themselves scored.
The take is a sentence, and the sentence is 你好,我来了 (founder-picked, 2026-08-13 — was 你好 alone, before that 越南). Nǐ hǎo, wǒ lái le — "Hi — I'm here!", an arrival and not a departure. Five syllables, fourteen parts, and the choice is four rulings at once:
is indistinguishable from every competitor's confetti. So the reveal leads with the per-part map and the percent is the caption, never the headline — a number invites comparison, a diagnosis invites trust. onboarding-first-take.html draws that order: the five-token strip at full card width, the score at caption size beneath it, one named observation in the panel below.
Chinese nearly everyone already owns — and 我来了 is easy, popular, and half-guessable straight off the pinyin by someone who has never studied. That is a difficulty ramp with a floor.
English-first market needs (founder, 2026-07-31). The identity hook the old words carried is goal 1 step 1's job — the three words of lesson one are 我 · 是 · 越南, and only 我 is shared.
that is the story worth sharing out of minute three.
The cast is the first-ten-minutes success floor, drawn: a mixed map, mostly green, the ones that are not green are amber and never red, and exactly one observation is named — 你 CLOSE (two third tones meet and the first lifts; the learner dipped both), 好 · 我 · 来 CLEAR, 了 CLOSE (it came out heavy, named in the second-repair slot rather than taught). The accounting line names the grain, not the count (adjudicated 2026-08-13): the learner sees five boxes and no arithmetic that reaches fourteen, so it reads "Every sound and tone scored · 2 to repair" beside the 76%, and every screen that quotes the take back quotes that form. We never block on score: completion is the evidence of attempt.
The why-now list carries the exam (ruled 2026-07-31, HSK-handover amendment A1). "I have an HSK exam coming" sits second among the reasons; a learner who picks it lands on the HSK-selected route reveal (path-day-one-hsk.html — the same moment as path-day-one.html, drawn in its other state), whose reveal carries the one specific claim: the oral exam is compulsory from HSK 3, and its level-3 paper opens with listen-and-repeat — the exercise scored here every day. (Level-scoped on the learning-team gate, 2026-07-31: from HSK 4 the oral opener is retelling, and every level-4 candidate can check.) The 2.0/3.0 explainer still stays off this screen — it lives on the track surface (path-hsk.html), where the track is actually picked.
A moment is not a screen, and the take takes three (2026-07-29, on the drawing). The take needs its ask and its answer drawn separately or the answer is a claim with no evidence — it went straight from the microphone explanation to a finished 94% result, which codex called conjured and was right to. The take is onboarding-take-prompt → onboarding-take-recording → onboarding-first-take: three files, one moment, and it is the only moment in the flow that is more than one screen.
The take is unfailable, and it is the learner's own voice scored at word grain, on the real analyzer. Unfailable has always been a ruling about the scored take and nothing else. 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 plan is the closing moment, and it is a receipt performed as assembly (founder, 2026-08-13; re-ruled the same day). onboarding-plan.html comes after the last ask and before the reveal, and it asks nothing: the learner's own answers come back assembled — their track, their pack, the shape of a day — with the capability wall beneath them. It carries no stepper, because a fifth dot at the moment the product stops taking and starts giving would read as one more form. Its promise binds the next screen: if path-day-one ever stops opening on what the plan says it opens on, the plan is the lie (§8.2).
Asking nothing is not enough; the screen has to LOOK like it asks nothing. The first draft drew the three pinned answers as white cards with icon chips and a check disc — the exact costume the two asks before it had just spent four minutes teaching means pick one — and the founder read it as a form: "feels like I need to select something I don't understand." The rows are now checklist lines directly on the peach: a done-tick, the answer as running text, no card, no chip, no fill, no border, no radius. The section head is "Building your plan", so the panel reads as the assembly's result rather than a set of choices, and the twelve wall tiles flattened onto panel with the house lift removed — once the rows above them went flat, twelve raised white cells in a three-column grid under a CTA read as an app-icon grid, which is a thing people tap. The expected action on this screen is: watch for about a second, then tap the button. If any part of it invites a decision, it has broken.
The done-tick carries the domain colour (founder, 2026-08-13: the checklist is right but should "look polished, feel completed/success"). Each tick sits in a 24px pastel disc with the check in ink — peach-deep on the track line, mint on the words line, rose on the lessons-and-speak-streak line. This is §2.1 doing real work at last: the colour is on the completion mark, saying which part of the product just got assembled, where the retired 36px icon chip only captioned a row the learner could not act on. Three constraints bind it, and they were settled on renders, not in argument:
on the one screen whose whole failure mode is looking like a form. A paper ring and an ink hairline ring were both drawn and both read as an empty control with a tick in it. What reads done is a solid disc with no border, defined by a 1.5px lift — a coin on the panel, not a box waiting to be filled. The check goes to stroke 3.4 to stay the loudest thing inside it.
border-ink/12 rule — the samehairline the goal screen uses under its arithmetic — turns three floating sentences into a ruled ledger, which is what a finished receipt looks like. A full-bleed rule has no left or right edge, so unlike a card boundary it cannot suggest a row to tap (§2.5, §8.2).
bg-peach on a bg-peachpanel was pixel-identical to its field — the coin was drawn only by its shadow, and an emboss delivers no colour. Every edge treatment read as a radio, an ink veil read as disabled, and a white coin isn't peach — so §2.1 gained peach-deep and the track line's coin wears it (ruled 2026-08-13, founder-directed). The rule generalises: depth, never an outline, is how a mark keeps its domain colour on a ground that already has it.
Mai sits right on the plan beat, and the asset is mirrored (founder-authorized, 2026-08-13: "maybe mirror flip her"). She was seated left because mai-presenting's palm extends image-right; the cost was that her 118px column pushed the eyebrow and headline 108px in from the screen's left margin while the sub-line beneath them started at 18px, and the founder read the result as broken layout rather than as a figure. She takes path.html's greeting seat — text left, figure right, the product's default — and transform: scaleX(-1) on her container turns the palm back at the headline. Mirroring a mascot asset is not a standing licence: it flips any asymmetry the character carries, and the default answer is to ask the mascot seat for a facing variant. It was allowed here because the founder named it, the pose carries no lettering or one-sided prop, and — per the crops rule — the transform lives in the container and is never baked into the asset.
No paywall in onboarding, v1 (founder, 2026-08-13). This is a freemium meter, not a trial gate: the high-conversion moment is the server's 402 wall on the day a learner wants a third lesson (paywall.html, §5.10) — a learner who has felt the product's limit — never the end of onboarding, where the learner has spent four minutes and owns nothing yet. The plan beat's "Two lessons a day, free" row is the reserved slot a trial offer will one day sit beside; nothing before that row mentions price, plans, or what runs out.
Identity is four paths — Apple, Google, Facebook, and continue with email (founder, 2026-07-29). With the door as the opening screen, one-tap is what keeps it from being a wall: the three social rows land inside in one tap, and the six-digit email round trip is the fallback below the divider, never the opening act.
Three rulings on the front door follow from that, and they are visible on login.html:
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.
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.
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.
docs/2026-08-03-founder-ruling-mai-presence-by-default.md; Toni retired 2026-08-01)MaiSay has a mascot: Mai, a chibi girl with big round glasses — the app-premise embodied, "a girl who teaches you the language." Her canon is the accepted-image corpus plus its prompt manifests in product/mascot/assets/mai/ (see product/mascot/README.md and docs/fable-brief-mascot-design.md). Her register is the founder's: companion — you are not studying alone; safe and calm. Not a cheerleader, not a grader, not a narrator of chrome.
The rule: a learner-facing screen carries Mai by default, and absence needs a named exclusion, per screen, in writing. The design question is not "has this room earned her?" but "what, specifically, keeps her out?" — and the answer must be one of:
or "n of m scored". A face beside a verdict reads as the grader's mood, and the honesty surface cannot afford one. This covers aggregates of verdicts too: progress.html's per-part repair boards are diagnosis, not decoration.
error state. An error carries no vibe. (A no-score completion* still celebrates the act, and she is on it.)
ruling Mai plays the role ../../docs/2026-08-04-founder-ruling-mai-plays-the-role.md; most of what this exclusion used to cover was Mei, and Mei is retired.) · The voice gate — cast 2026-08-04, and it lifts per surface as the audio becomes hers. Mai's voice is cmn-CN-Chirp3-HD-Sulafat (ruling ../../docs/2026-08-04-founder-ruling-mai-voice-sulafat.md), chosen by measured tone accuracy across fourteen candidates and cast by the founder for character. Casting is not the trigger — the audio is. A surface keeps the gate down until the recording it plays has actually been regenerated in her voice; until then, unattributed model audio may be beside her and she is never shown producing it — drawn static, no speaking loop, no open-mouth frame. Once a surface plays her voice the composition becomes literal and the loop turns on, and nothing is redrawn: mai-speaking-loop was drawn against this gate and is the same file on either side of it. The role-play seats (mai-role-asking, -greeting, -tea) stay mouth-closed as specified — they are the static frames of a seat that can later animate without recomposition. · Learner playback is never hers. A take belongs to the learner, and her face never attends the learner's own recording — the same wall as where the mic is live, she is not*, from the other side. What is NO LONGER in this exclusion: the conversation family, the Your-turn family, catch-it and catch-scene. She is the speaker in those scenes now (playing 美玲, framed on the scene card), so the face already follows the recording — that is the rule satisfied, not waived. Exclusion 1 still governs the verdict on a scene she played: conversation-feedback and every ✓ 96% bubble stay hers to be absent from.
never disappointed, pleading, alarmed, or gesturing toward the purchase.
onboarding pickers; profile.html, where every region is evidence). A screen with no honest seat waits for its redesign rather than taking a sticker; the exclusion comment names what would reopen it.
And one boundary that is not an exclusion at all: the operator console is a different species (2026-08-04). Every tool- / admin- screen is an internal instrument for under twenty teachers (CLAUDE.md invariant 11), speaking Vietnamese to its operators, whose jobs are correct corpus rows and fast recovery. Presence-by-default governs learner surfaces — it is about a learner not studying alone — so it never reaches the console, and no new pose reopens it. The family rule is written once, at tool-home.html; those files do not each repeat it.
Composition laws (the sweep's craft, 2026-08-03/04):
her (the voice panel's narrowed claim column; the path header's 275→228px greeting). Leftover slack is not a room.
overflow:hidden makes the cut — never a re-framed asset. A seat that animates shares ONE framing between its static and its loop. Drawn art is different from cropping: the composed-cut ink-arc busts are artwork and belong to static seats only (voice panels, calm-wait); the lesson cue-room family, which animates, takes the canonical master cut by the panel edge everywhere.
sets.html vssets-empty.html hold one seat each; the route-end seat died to this law).
ink hair is the highest-contrast mass; sets.html to the pixel). Edge-cut idiom: the cut through hair reads as authored (the door hero; measured deviations like match's −22 are recorded in-file).
pattern printed on her.
regardless of every number. Never-guilt, never-disappointed is the emotional spine, and the greeting cadence is where it is drawn rather than asserted — five sibling files, not a switch. The clock is: path.html morning → at the desk (mai-lofi-study) · path-afternoon.html → reading on the floor (mai-studying) · path-evening.html → headphones, eyes closed (mai-music) · path-night.html past about ten → the yawn (mai-sleepy) · path-return.html a long gap → the wave (mai-greeting, because a return is an arrival). Four of the five carry a verb; sleepy is the one that does not, and it holds the one slot where the hour is the whole point — it lost the evening to mai-music because at the hour a learner is deciding whether to do one more lesson, a companion yawning at them argues the wrong way. The words change with the greeting, not with the hour: the late file still reads "Good evening" — a "good night" at the moment someone is opening the app is a goodbye, and telling them it is late is a comment on their day wearing a clock. Two laws hold across them: the swap is time-of-day, never activity-conditional — the evening screen renders identically at streak 12 and streak 0 — and a broken streak is neither printed nor zeroed, nor is elapsed time ("3 weeks away" is the same guilt wearing arithmetic). There is no sad, tired-at-you, or disappointed variant of her anywhere in the product.
vest, white collared shirt, black short skirt with a visible waist break where the vest ends, bare legs, small black shoes, white socks. The vest never continues below the waist and the lower garment is never lilac, white or any pastel: it is the one dark mass in the outfit besides the hair and the shoes, and it is what stops the figure reading as a single lilac blob at small sizes. This was canon by corpus consensus and not by specification until it drifted — mai-canonical's own prompt describes only the blouse — which is exactly how mai-walking (its prompt says "lilac skirt" outright) and mai-path-companion (its prompt says nothing below the waist, so the model continued the vest downward) ended up in a lilac pinafore. Both are queued for re-cut: docs/2026-08-04-request-to-mascot-team-wardrobe-canon.md. A consistency break of this kind does not read as a costume change; it reads as separately generated art, which is the one impression a mascot cannot afford.
the home path instead of a full body for nothing — companion study … users like it so much")*. Where she keeps the learner company, the pose carries a verb: at the desk with a notebook, reading on the floor, listening with headphones. A figure standing at ease with its hands behind its back reads as someone waiting for the learner rather than working alongside them, and the difference is the entire feeling users responded to. mai-path-companion — her first full body, drawn for exactly this seat — lost the home-path header to mai-lofi-study on those grounds. Consequences: the daytime path pose is a study pose (mai-lofi-study or mai-studying, a curation choice, never a poseless figure); the pose law is user-global, not track-dependent — path-hsk seats the same one, because the route changes with the track and who is sitting beside it does not; and a scene pose gets its own walked size — 132px on the header rather than a figure's 96, because a scene shrunk to a figure's width keeps the character and loses the desk. It buys its width back from its own transparent margins (−15 left, −11 right), not from the headline, which stays on one line.
the glasses survive) may sign a surface a companion could never stand on — but only in a brand row, beside the wordmark, never adjacent to a number. Its first home is complete-share-card.html, which is marketing by construction: the learner publishes it, and in a feed of wordmarks the face is what makes a thumbnail ours. A mark placed beside a section heading to be friendly is decoration, not a room, and was rejected on the sets rail.
contrast her wardrobe within the pastel the surface's semantics already assign.
judgment reads as anxiety, but the calm companion (eyes closed, no hands) walked and lived on speaking-arming.html (the hold before the tone, floor idiom — a bottom cut through a white sleeve on a white card reads as fading, not as an authored edge), speaking-sending.html (the upload — a wait, not an error) and speaking-scoring.html (the score pass, now with the mai-calm-breath loop). The EXPERIMENTAL flag and the kill criterion travel with the idiom: if any one of the three reads as worry on a walk, all three go faceless together.
speaking run she is present at the prompt (the Example bubble is hers), at the hold, and at both waits — and absent for the take itself and for every scored frame after it. Two different reasons converge, which is what makes it a rule rather than a coincidence: while the mic is open the learner's own voice is the subject and a face becomes an audience (the meter is an instrument, not a room), and once the score exists exclusion 1 governs. Absence during recording is therefore designed, not an oversight, and no new pose reopens it. THE RULE IS ABOUT THE METER, NOT THE FRAME (founder, 2026-08-04: "all three need to have Mai, put her in same place"). It was first applied as "absent for the whole take", which made her vanish for two frames of a three-frame motion. Corrected: she stays in the card head across prompt → recording → result — one seat, one position, on all three — and the level meter is what she is never inside. That is the original reason intact (an instrument is not a room; a face in the level display makes an audience of the learner's own voice) without the disappearance it was accidentally buying. The onboarding trio is the worked example; the result frame seats her in its head and still keeps her away from the score itself. Method note worth keeping: where a file explains its own emptiness by pointing at a seat somewhere else, check the pointer. Three files claimed she "arrives with the open mic" long after that seat was cut, and one of them was empty only because of the false pointer.
"She appears not necessarily by feature now. She can just be a companion learning, so the user doesn't feel alone. Redesign the learning and card elements as needed so she is in naturally." And the evidence behind it: "we got very positive feedback by her appearance — users straight love it, they directly request to see her more. Feature-link is our solution for more naturalness, but not the limit." Three things follow, and they change how a screen is argued: · Company is a sufficient reason. She no longer has to own the cue, the voice, or the recording to be on a screen. "The learner should not feel alone here" is now a complete justification, and it applies to ordinary learning surfaces by default. · Feature-linking is a TECHNIQUE, not a gate. Tying her to something the screen actually does (she offers the example, she speaks the scene) is still the best way to make a seat feel inevitable rather than pasted — keep reaching for it first. It is no longer the price of entry. · GEOMETRY STOPS BEING A REFUSAL AND BECOMES A TASK. Exclusion 5 — "no honest room" — was the most-cited exclusion in the sweep, and it is now the wrong answer on a learning surface. The instruction is to redesign the card so she fits, the way lesson-word-check's cue panel was composed around her rather than handed its leftover slack. A file may still say a layout does not yet have room; it may no longer say that as a conclusion. What did NOT widen, and should be assumed intact until Chau says otherwise: exclusion 1 (verdict surfaces — the founder's own hill), exclusion 2 (consequential errors), the voice gate, and exclusion 4 (the offer row, pending the paywall A/B). This ruling is about whether she needs a reason; it is not a licence to stand beside a score.
not a door into verdict or score imagery.
Toni stays retired (founder, 2026-08-01): product/archive/toni-2026-08-01/ is history on disk, not canon. The copy still speaks as MaiSay, not as Mai: "MaiSay's voice", MaiSay Pro — Mai is the face the app wears, not a narrator; the speaker role (her voice on learner surfaces) stays gated on casting the canonical Mai voice.
A lesson ends in an adaptive sequence of two to four beats, one claim per screen, in fixed order: score → voice → progress, with the goal-finishing run swapping the medallion pair in (goal-complete.html, one viewport, then goal-recap.html as the final surface — measured numbers, no Mai) and the first-ever take swapping the activation beat. Rules that hold across every variant:
null` → omitted) and has four variants by score shape; it is the verdict surface, and it is Mai-free (exclusion 1).
complete-voice.html), is grade-blind by construction: nopercentage, no verdict word or colour; the 41% run and the 100% run render it identically. It never says "saved" — SpeechAttemptService retains nothing for learner practice; the listen-back rows are the run's own session audio. Mai listens here (the composed-cut bust, 190px): the beat celebrates the act, and listening says "I heard you" where a thumbs-up would be a verdict.
1 of 5 steps is the same object as goal-complete's 5/5 steps, a completion count. Under the presence ruling the beat is Mai's, and the peach panel was rebuilt into a room for her: two bands, the bar and the forward sentence re-flowed into a 202px column, the path companion standing in the right one with her feet on the panel's bottom edge (complete-progress.html, measured in-file). The beat is never told the run's score, so score-blindness is mechanical.
sequence, not per beat.
progress — speaking-skip.html; a scorer failure states the operational fact — complete-no-score.html, which is a completion, not an error, and keeps her).
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.
A lesson is a modal flow, not a tab. From the first exercise to the last pixel of the completion screen, the learner surfaces wear no tab bar — <studio-nav> is absent from every exercise, every speaking state, every conversation turn, and from lesson-complete / goal-complete. It returns when the learner is back on a home surface, which is one tap later.
The completion screen was briefly ruled the place the bar comes back, on the theory that it is the door out. Rendering it settled the question: the bar costs the peach claim card (§5.8c) its space and pushes what the lesson won below the fold, which is the one win moment (§5.4) traded for navigation the screen already provides in words. Both completion screens name their own exits — the header X and a Your path button. A third route to the same place, paid for with the payoff, is not an exit; it is a tax.
Three reasons, in the order they matter:
Nothing on that bar is where they are.
which is exactly where the action bar and the last line of the prompt are fighting for room. Several screens were clipped by it. Removing it is the cheapest layout fix in the system.
the take.
The exit already exists and is the right one: the back chevron and the progress bar in the header.
The bar belongs to tab ROOTS, and to nothing else (extended 2026-08-10, founder). The original ruling drew the line at the lesson and left it on the drill-ins — set-detail, set-paste, progress, settings, paywall. That was the same lie one step quieter: while a learner is inside Restaurant survival the bar lights Study sets, which is not where they are, it is where they came from. So the surfaces that wear it are exactly the ones you can reach by tapping a tab — path and sets today, profile when it is built. Everything reached from them pushes over the bar and covers it: a set, the paste door, focus, add words, due today, settings, progress, the paywall.
Two things fall out and both are wanted. The set page ends in its own action bar (Pick up at 辣) with nothing under it, so the primary action is the last thing on the screen instead of the second-last. And the back chevron becomes the only way out of a drill-in, which is what makes the back gesture and the header agree — a bar that could jump you sideways out of a half-finished paste was a second exit nobody designed.
sets was rebuilt three times on 2026-08-10, and the arc matters because each dead form is worth not re-proposing. The underlined tab strip (For you · My sets) died first: a second tab metaphor over the bottom bar's own Study sets tab, in a form that exists nowhere else in the app. A My study sets section above the library died next: it buried discovery. A My study sets row (door to a separate full-shelf screen) lived for an hour before the founder named the final shape: the primary split belongs in the chip row, wearing the house switch, with the learner's shelf as the first chip and a whole state of the screen — not a strip, not a section, not a door.
The chip row: My sets · Popular · HSK · Travel · Work · Food.
| Chip | The state it opens |
|---|---|
My sets (sets.html) | The learner's collection: due row (what is owed outranks everything; due is a fact about sets you have) → continue card (the shelf's hottest item, the one dominant mint) → Made by you → Learning. The groups split on the relationship — you can add words to a set you made and not to one you joined — and the heading carries it so no card wears an ownership badge. A group with nothing is absent. The make-your-own tile closes Made by you: creating is a fact about your own collection. Forty chapter-sets just make the state long — the library is one chip away, never buried. |
Popular (sets-popular.html) | The library's front shelf, Netflix-shaped in house materials: a spotlight rail — most-loved certified sets, 218px cards, horizontal swipe, the second card cut by the column edge as the swipe affordance — then the grid, with the tile closing it. The shelf ranks on rating combined with the badge (founder, 2026-08-11) — but still no rank numerals: the stars already carry the fact, and an ordinal dresses a shelf as a leaderboard. |
| HSK · Travel · Work · Food | Category shelves of the same grid. Inert in the mocks until the library can fill them. |
Popular replaced "For you" outright (founder: "let not have for you chip anymore"). Personalisation needs a model and a history and pretends to have both on day one; popular is a number the library already owns, and it is what a stranger actually wants to know.
The order never moves under the learner; only the landing varies. My sets is first in the row and is the landing whenever the learner has anything, owned or joined. A learner with nothing lands on Popular — an empty shelf cannot be a landing — but My sets keeps its seat at the head of the row.
The rail is a fact; the grid is a suggestion. A set the learner joined stays on the spotlight rail (hiding the platform's most-loved set from the shelf that proves it would make the rail lie) but leaves the recommendation grid (re-recommending what they already took is noise). A spotlight repeating a grid card is deliberate — that is how a front shelf works.
What held across all three rebuilds, each with its own ruling: the create action is the labelled + New set ink pill in the header of every state (a bare icon circle read as stray chrome; the old one scrolled to a door — a treasure hunt, not a button); search sits above the chips and interrogates the library (sets-empty is its zero-result state, on the Popular chip); ratings are ink and a below-threshold set shows an empty slot, never a zero; only cards whose set page is actually drawn are live links (§8.2 — everything else is an inert <div>); and Mai keeps the shelf-room seat — floor on the first grid's top edge, score-blind — on whichever state's body starts a grid, and the two states hold two verbs (founder, 2026-08-10): on My sets she writes beside the lists the learner wrote (mai-writing; its loop is commissioned — docs/2026-08-10-request-to-mascot-team-writing-loop.md), on Popular she reads beside the lists we published (mai-studying + loop), and the empty state keeps the browsing pose. Same seat law, same floor, different hand. One Mai per state, always.
Screens: set-detail.html (browse) · set-detail-studying.html (studying). Behaviour is study_sets_overview.md; this section is what the page owes.
Two things were living on one page and the split is the ruling. The set — title, category, band, the phrase list, the rating aggregate with its count, provenance, share and copy — is a catalog item, the same object for every viewer. The learner's study of it — progress, due, the resume point, Focus, and one day the lesson-customisation controls — is a relationship, and it belongs in one place: a single Your study panel under the hero. Before this ruling the study state was scattered over the hero's bar, a three-tile tally, a Focus card and an Add-words card, and the page read as a learning dashboard a browser had wandered into.
Browse is the base state and it is what a Popular tap opens. No bar, no tally, no due, no Focus, no rating input — a rating requires enrolment plus one scored item (the spec's brigading defence), so the browse state shows the aggregate only. The footer is one button: Start learning. The listening sidekick belongs to a set with a history and stays on the studying state.
Study begins and exactly one thing is added. The Your study panel: the one progress figure with its bar (§5.3 — the 18/6/8 tile row is dead; the per-phrase pills are the breakdown), the due-today fact, the resume point, and the Focus row with its Change. Nothing study-shaped may appear outside the panel, and anything study-shaped the build grows (the Flutter page's stray due line) folds into it. Future controls on how this learner studies this set — which exercise families, lesson length — land inside this panel, never as new cards.
The hero never carries progress again. Set facts only, in both states — the tile, the claim, ★ with its count and learners. One mint per screen, and it is the shelf item.
Add words is an owner's door; the copy is everyone else's. The server refuses add-words on a set you didn't write, so the door is a small + Add words link in the phrase-list header — beside the list it appends to — drawn only on a set the learner owns. For every other set the answer to "how would I add to this?" is Save a copy (the fork the spec already prices: keeps the words, loses the badge, yours to edit). No third system. And an edit costs the owner nothing (founder, 2026-08-11): the badge is granted by the team and stays until the team revokes it, and ratings are permanent — so the weekly add on a verified set carries no fine print, which is what lets the core ritual run on the set's best state.
Cast: browse draws 茶 Ordering without pointing — ★ 5.0 from 76 ratings · 890 learners, 52 phrases — which makes the 茶 cards the Popular shelf's live links (§8.2). Studying draws Restaurant survival with §10's interlocked numbers. On due-today, only the Restaurant survival row is live for the same reason.
A conversation has several scored turns; a lesson exercise has one. That difference decides the container.
Turn feedback opens as a bottom sheet over the transcript. The conversation stays visible and scrolled to the turn behind it, dismissing returns the learner exactly where they were, and the next scored turn is one tap away rather than two navigations. Reviewing four turns must not cost four round-trips through a full-screen page.
Full-screen feedback stays correct where there is one score to read — speaking.html after a Say it exercise. The rule is not "sheets are better"; it is the container matches how many of these the learner will open in a row.
The sheet carries the same content, in the same order, at the same size: listen-back with the score, the part strip, the one coaching point, and the actions. It is the screen with a paper edge and a grab handle at ~88% height, not a reduced version.
A gallery of mechanics is not a demonstration of a lesson. Every exercise screen was drawn as a perfect specimen of its family and each one invented its own surroundings, so the set carried three incompatible journeys at once — Lock in the words · 4 of 5 with an internal n / 6, Speaking lesson · 1 of 5 on a five-syllable sentence, and Speaking lesson · 2 of 6 on the picture prompt. Walked end to end, the first ten minutes did not exist. The screens proved the components and left the product unproven, which is the exact failure the drawing was supposed to close.
§10 gains a first-lesson cast, and every screen in the run draws from it. One objective, three atoms, one counter running 1 / 6 → 6 / 6, and lesson-complete summarising that run and no other. A screen may not invent a lesson title, a denominator, or a word.
The later sentence-repair lesson stays and is labelled as later. Say the whole sentence on 我是越南人 is a real second run and the whole speaking-state catalog hangs off it. It is not lesson one, it is not reachable in the first ten minutes, and nothing in the first run may borrow its sentence grain.
The counter is two levels, and that was never the bug. The eyebrow carries the step within the goal — Say where you're from · 1 of 5 — and the badge carries the exercise within the lesson — 3 / 6. That scheme is right and stays. What broke was that six unrelated screens shared one invented objective (Lock in the words) while disagreeing about everything under it, so the two levels described two different products.
There are exactly three cast lessons, and everything else is a specimen.
| Lesson | Eyebrow | Badge | |
|---|---|---|---|
| Goal 1 · step 1 | six exercises, three atoms | Say where you're from · 1 of 5 | n / 6 |
| Goal 1 · step 2 | the scripted conversation | Conversation · 2 of 5 | n / 4 (turns) |
| Goal 1 · step 3 | the sentence, one clear repair | Say the whole sentence · 3 of 5 | n / 5 |
A conversation is a cast lesson, not a specimen, because it is one of only two lesson formats that ship and the turn-by-turn run has to be walkable. Its badge counts turns, which is its own shape and collides with nothing.
And goal 1's ladder was inverted, which is why the first node opened the wrong lesson. As drawn, step 1 asked a brand-new learner to speak the five-syllable 我是越南人 and "Lock in the words" came at step 4 — vocabulary after production, the sequence rule 8 of learning_overview.md ../../docs/project/learning_overview.md exists to forbid. The ladder:
| Step | Objective | Opens |
|---|---|---|
| 1 | Say where you're from — three words, out loud | lesson-match.html (1 / 6) |
| 2 | Use it in conversation — a guided 4-turn exchange | conversation.html |
| 3 | Say the whole sentence — 我是越南人。 | speaking.html (1 / 5) |
| 4 | Catch the reply — the word inside someone else's stream | lesson-catch-it.html |
| 5 | Ask someone back | lesson-your-turn.html |
| — | the goal · introduce yourself in 4 turns | goal-complete.html |
The whole speaking-state catalog belongs to step 3 and carries its header. Those screens are moments inside one take, not slots in a run.
Step 5's drawing is lesson-your-turn.html and stays so. What ships behind it today is a substitution built from families that already have a render — recorded as an open deviation in §9, not as a change to this ladder.
A screen belonging to none of the three is a specimen: it drops both counters and the objective and carries Practice · <family name>. A specimen may look completely real; what it may not do is claim a slot in a run it is not part of. The 22-character stress sentence is a specimen — it proves the token grid wraps, and it is nobody's step.
One rule, found three times in one round: an exercise may not show, before it is answered, the thing it is asking for. It reads as helpfulness and it is the cheapest way to make a lesson prove nothing — the learner clears the board and no evidence was produced.
Match reveals progressively. The word strip shows only the pairs already matched, growing by one as each pair is cleared, keyed by atom id; before the first pair there is no strip at all. The teaching note waits for the cleared board. The authored full strip was an answer key printed under the board — every pairing the exercise asks for, listed in order, on the screen that asks for it.
Match speaks: a hanzi tile plays its atom on tap, and a cleared pair plays it once more (ruled 2026-08-06). No toggle and no hint framing — in a speaking-first app the sound is the word, and a learner has not met a word until they have heard it; Match is untimed and unfailable, so the audio costs nothing. This does not breach the answer-key rule above: the exercise asks for hanzi ↔ gloss, and the audio is a third face of the atom — it reveals no pairing the screen holds. Gloss tiles stay silent (Vietnamese TTS is not the product), and playback is one channel, latest-wins — a new tap cuts off the previous clip, so fast players never queue a backlog.
Match's feedback grammar (ruled 2026-08-06, same round): a cleared pair gets a brief green tint and a light success haptic, and the word itself is the correct-sound — nothing is played in front of it, because a chime before the word delays the thing prefetch just made instant. A mispairing gets a shake, a soft neutral thud and a gentle double haptic — never red: Match is deliberately unfailable and excluded from the lesson score, and red is the scorer's verdict currency — spending it on "not these two tiles" imports failure into the one exercise designed to have none and cheapens the colour everywhere it is earned. The board-clear chime stays.
Lesson audio is prefetched at lesson open (ruled 2026-08-06): the run snapshot's every audio source is background-warmed — engine, session and files — when the lesson opens, so a first tap plays from memory. The library is pre-generated statics on the CDN; the only latency was the lazy first-tap fetch, and it is spent before the learner reaches a tile.
The atom speaks at the marking moment on every recognition exercise whose cue was not audio (ruled 2026-08-06): Word check in its non-audio directions and Read the character play the atom's recording as the verdict lands — correct or wrong, since the reveal already discloses the answer, and on a wrong pick the true sound is the correction. Never before the answer: on hanzi → pinyin the question is literally "how does this sound," so audio ahead of the pick is the answer-key rule broken in sound. Audio-cued exercises (Which one, Which tone, audio → …) do not replay at marking — there the sound was the question and has just played.
Word check is one exercise with a cue → answer direction, never sibling exercise types. audio → gloss, hanzi → pinyin, audio → hanzi and the rest are one exercise carrying a direction; each is a different render of the atom, which is precisely the thing that ships freely, and a second exercise type per direction would multiply the catalog by its own axis and demand a new reserved name (§11) for each. Two consequences on screen: an option carries its label and nothing else — a reveal line under one option is the answer key drawn small — and the lesson's word strip does not render before marking under any non-gloss direction, because there it names the very words the question is about.
Read the character differences words, not spellings (drawn 2026-08-07, lesson-read-character.html). The cue is the character and nothing that sounds — no play control, no pinyin, and no Hint, since the peek reveals the word's sound and here the sound is the answer. Where hanzi → pinyin differences four spellings of one syllable, this family's four options are the true readings of four words the learner knows — every distractor is another atom's voice, which is the family's claim: the character is an object with an identity. Below the options sits the object card — radical, stroke count, the parts — because structure is the one true fact about a character that does not leak its sound, so it may stand before the answer as identity evidence, not a hint. At the marking moment the atom speaks (the 2026-08-06 ruling above), and for a vi learner the reveal carries the Hán-Việt reading — the bridge is shown at the answer, never before it.
Which tone? serves a toneless spelling as its cue, and its five options stand in scale order. The cue is the spelling with no tone mark on it; the options are the five tones, first through fifth, unshuffled. An option that names its tone has no secret position to protect, so shuffling buys nothing — and the tones are an ordered series, so a reordered scale is simply unreadable. The exercise refuses a multi-syllable word until per-syllable audio exists: which tone asked about a word carrying two of them is a question with two answers.
Before the goal is complete, the card headlines the LESSON's objective. The unit's can-do statement appears nowhere until it is earned — it belongs to goal-complete.html, where it is true. The position line reads "1 of 5 toward <goal title>", and toward takes the goal's title, not its can-do: naming the can-do before it has been earned is exactly the claim this rule forbids, and doing it in the toward slot is doing it while admitting there are four steps left.
This supersedes lesson-complete.html's peach card, which headlines "I can tell someone where I'm from." one step into five. §5.4's win moment and §18's one warm slot are unchanged: the card still gets a sentence anchored to something the learner actually did — what moves is whose claim it is.
When the machine could not judge every eligible exercise, the card says so: "Đã chấm n/m — máy chấm chưa đủ chắc chắn với phần còn lại." The uncertainty is the machine's, always, and never the learner's — the sentence names what the scorer could not do, not what the learner failed to do. This is §7's be honest about coverage at lesson grain, the sibling of 12 of 13 parts scored at take grain (§5.9 keeps both as allowed state labels). Silence here would be a quiet percentage printed over a hole, which is the false green §2.2 exists to prevent, one level up.
Found on the device, in the goal-1 conversation: answering made the screen jump — the question panel compressed, the untouched replies collapsed to one line, and the verdict word pushed into the text column. Each was a deliberate trade for a shorter scroll to the next phase, and the founder walked it and ruled the jump the worse cost.
The rule: from the moment an exercise is on screen, the question and the answer options keep their exact geometry through pick and verdict. What a marking may do: change colour, change a border (with the width difference given back in padding, so content does not shift), and set words into space that already exists. What it may not do: resize, collapse, compress, or insert anything above or between the question and the options. Everything the marking adds — teaching lines, pairing cards, the speaking phase — arrives below the options, or floats (the verdict word sits in the row's corner without consuming layout).
And the rule outlives the take (founder, 2026-08-04: "the rule of an exercise is always keep the original question/challenge — not shift, not layout change"). Every state an exercise can reach — marked, scored, unscored, mismatched, out of attempts — draws the challenge block unchanged: same elements, same sizes, same order, same place. An element may not be dropped on the argument that it has "no job left" once there is an answer; the challenge is the one thing on the card that is never spent, because it is what the learner is still being asked to do. States append — the note, the score, the retry, the listen-back all arrive below — and they may retitle a control whose truth changed, which is not a layout change.
A hint is a toggle, and a toggle does not move (founder, 2026-08-04: "Show the character is a toggle button, not tap-and-hide"). Anything a learner can reveal is revealed by one control in one place whose label flips — Show the character ↔ Hide the character — never by a tap that opens and a second, differently-placed control that closes. Each level of hint owns its own toggle next to the thing it acts on (the character toggle under the gloss; the pinyin toggle under the tokens), and the icon carries the state: eye and eye-off, never a chevron, because a chevron promises navigation and a hint reveals in place. A disclosure that only opens is a trapdoor — the learner who looked and now wants to test themselves properly has nothing to press.
Found on lesson-say-memory-mismatch.html, which had dropped the photograph and floated the gloss to the top of the card after a failed match: the learner came back from a take to a card that had rebuilt itself, and had to re-read the exercise to find out whether the question had changed too.
Consequences already applied: Your turn keeps its question panel at full size and its three replies at full height forever; its marking line moved below the replies; Word check's marked note moved below its options. Owed, for the design-round sweep: Word check's cue-reveal currently grows the cue panel in place — the revealed word needs a home below the options (mock first), and lesson-your-turn-correct.html + lesson-word-check.html owe re-renders to match this rule, which supersedes the collapse both mocks currently draw.
Found on the device, minutes after 5.8d: with the jump fixed, the deeper shape was still wrong — after the pick, the learner scrolled down to find the microphone. Ruled: Your turn's two phases are two screens of one exercise. The pick screen ends at its marking and pairing card and closes with the one continue; the speaking screen opens with the question still readable, the repair line where one is owed, and the speaking card — conversation-recording.html was already this screen for the conversation case, and now the plain case matches it. The exercise, the outcome, and the server contract are untouched: one exercise, one ordinal, two rooms.
The bottom bar follows the screen: pick screen — waiting label, then continue; speaking screen — the one speaking ladder. lesson-your-turn-correct.html owes its re-render under both this rule and 5.8d.
Room 1's card comes through only if it is a question (founder, 2026-08-15). Both two-phase families draw a card above the tray or the options, and only one of them survives into the speaking room. Your turn's is a question — a reply is an answer to something, and the something has to stay readable while it is being said. Build & say's “You were asked to build” is a task recap, and by room 2 the task is over: the tray is spent, the sentence is assembled, and the rail already says Built · CLEAR. Carried through, it stopped being a cue and became the third printing of a sentence that is already the largest thing on the screen. So it is dropped, and lesson-build-say-speaking.html is the room without it.
The meaning still does not leave the room — that half of the recap's job is kept, once, on the line above the plate (5.2c).
docs/2026-08-11-adjudication-how-a-sentence-teaches.md verdict 8)The conversation rail's withdrawn-gloss contract (conversation-known.html) extends into the lesson screens as one primitive: a tapped word takes a 2px dotted rule in ink/32 and prints its meaning beneath in muted ink at the size the screen already prints glosses. The tap is costless, recorded, and never printed back at the learner (§5.9); nothing on the screen moves when a gloss opens (reserved space, 5.8d's spirit); the rest state carries no per-word decoration — a reading line with twelve underlines is a page of links, and the affordance is the one open gloss.
Where it lives, and where it deliberately does not:
| Surface | Tappable | Why |
|---|---|---|
| Fill it in — the frame words | every non-blank word | the frame is stimulus; the payload is words (lesson-fill.html) |
| Fill it in — the gap and the four options | never | the gap is the question; the options carry their printed gloss already — that is the pick mechanic, not a tap surface |
| Build & say — the tray tiles | a spent tile's tap asks the meaning; a waiting tile's one tap places it, and cannot also gloss (ruled 2026-08-12 on the build agent's escalation) | the tray IS the sentence's words (lesson-build-say.html); the placement gesture owns the waiting tap, and a second affordance would be a new mechanic no mock teaches |
| Build & say — the syllable strip | never | 越 and 南 have no meaning apart from each other; a per-syllable gloss would invent one |
| Build & say — the built plate | never | it is the learner's own output and the build's verdict surface; meaning lives one row up, on the tray tile |
Copy: Fill it in's idle button reads Pick a word (was Tap a word — with frame words tappable that label named two different taps).
The screens read as interactive design documentation. They explain, mid-exercise, why the game cannot be failed, why there is no timer, what is and is not scored, what stays free, that the scorer already knows the target, and that this is not an exam simulation. Every one of those is a sound ruling and none of them is the learner's business while they are playing.
The line, and it is mechanical:
happened to this take. Keep it.
screen's "this take did not use one of today's 20", the free-retry note, an offline explanation. Something went wrong and the sentence is repair.
"Colours belong to this take" appears on onboarding-first-take.html and nowhere else — the only screen where a learner is seeing a colour verdict on their own voice for the first time. It bounds a claim rather than defending the product: the alternative reading of a red mark is you are bad at this, and §7's "amber is where trust is won or lost" is the same instinct one screen earlier. After that the scoping is carried structurally, by This take · scored in the eyebrow and This take · 89% on the playback row, on every scored screen.
> The first ruling kept the sentence on all seven scored screens, and the sweep falsified it: > nine instances in four different strings — Colors / Colours, with and without Score and > — which is copy drift wearing the costume of a trust intervention. A line repeated in four > spellings is not one intervention; it is the thing §5.9 exists to cut, and the two labels above > were already saying it.
that catches the case above before it spreads: before defending a line as necessary, find what already states it. Nine repetitions is the evidence, not the exception.
"the pick is not scored, only the take is", "everything non-speaking stays free, forever", "what HSK-style means here", and above all "this is a scripted conversation — MaiSay already knows the target", which tells a learner mid-scene that the scene is fake.
Where the banned copy goes: the one-time explainer moves to the surface that owns it — HSK-style to track selection, the billing rule to the paywall and settings, the scoring contract to help — and appears once, not on every exercise.
Roughly a third of the visible copy on the game screens goes. A learner should know what to do in five seconds. Design rationale belongs in an HTML comment, where reviewers read it and learners never do; that is what the comments are for and they stay.
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.
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 hierarchy — operator, 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.
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.
| Learner | Tool | |
|---|---|---|
| Page ground | paper | panel — a structural tint, so a tool never reads as a learner screen |
| Content unit | a card, one thing at a time | a row, many things scannable at once |
| Gutter | 18 | 14 |
| Pastel | large regions, identity | a tint for zoning only, never a large field |
| Type | 15–16 body | 14–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.
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.
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"?
good its second job:aggregate success outside the analyzer. A durability receipt is exactly that. It says the bytes landed; it says nothing about the speech.
alert #B4261E, never FIX red. This is precisely what §2.3 created alert for. Atake 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.
locked — the empty track. A take on the phone has not landed, and thescreen must never say Saved while the audio behind it is still on a device.
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 said Vietnamese; corrected 2026-08-05.)
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.
false-start takes are corpus data, never operator-discardable.
A labelled, reasoned, block-level skip is allowed, and is not that (ruled 2026-07-29). tool-record-noise.html lets an operator skip the whole noise block with a mandatory reason and a button that stays dead until one is chosen. The banned thing is a per-take escape hatch that lets a bad take vanish; this is a fact about the room — no quiet corner, no time left — recorded rather than hidden, and it fails closed. Naming it here so it is a ruling rather than something the next reader has to infer from the absence of a complaint.
The admin entry is a row, not a third door (ruled 2026-08-05). An account carrying the admin claim sees one quiet row beside the account door — "Admin · Sittings · grading · teachers" — and nobody else sees anything. The doors are the teacher's work; admin is not work a teacher has, so it never competes with them. The row is English on a Vietnamese screen because it is drawn only for admin's one reader (§6.5).
the teacher for opposite actions: a guard failure — too short, too quiet, no lead-in — is fixed by re-recording, and a server or harness fault is not (tool-record-fault.html says so in words: "thu lại cũng không sửa được"). The console badge counts both, so the line beneath it carries the instruction: thu lại N câu or lỗi máy chủ · đã báo. One badge over both kinds would send a teacher re-recording a sentence the server refuses every time, losing the sitting one take at a time — the failure invariant 11 tolerates in the room, made permanent by a label.
trusted collaborator; the product reminds, it does not gate.
not in the corpus, not in a queue, not in an export.
Admin holds the roster↔code join, and that is the one place it renders (ruled 2026-07-29). admin-sitting.html shows "Lê Bảo Ngọc" beside "tre-mem", which is correct and necessary: somebody has to know who to pay, and the console's only reader is the founder. The rule above is about the surfaces where a judgement is made — a rater who can see whose voice this is, is a rater whose labels carry a person. Admin makes no judgement. The join may render on admin and nowhere else, and it never travels — not into the corpus, not into a queue, not into 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.
roster row began as name + one muted line, with the count and the receipt stacked in a narrow right-hand column. On a real 390px phone that column had about eleven characters: 23 / 23 · ồn: xong clipped, the noise block shrank to the word ồn, and group, region and age band could not be shown at all. Nothing was missing from the data — the layout was hiding it, which on the screen a teacher reads between two students is the same thing. The fixed shape:
| Line | Job | Styling |
|---|---|---|
| 1 | Who — name, plus the single most important number or receipt, right-aligned | 15px ink; the receipt keeps its stat pill |
| 2 | Which — code · group · region · age band; reference the reader scans, never acts on | 12px, text-ink/55 |
| 3 | What to do — the instruction, in whole words | 13px, text-ink/80 — deliberately larger and darker than line 2 |
Line 3 outranking line 2 is the point: the old row gave identity and instruction the same weight, so "lỗi máy chủ · đã báo" read as decoration. .row carries min-height only, so it grows — a taller row needs no new CSS. Every third line must fit one line at 390px; a row that wraps to four has failed the rule, and the copy gets shortened rather than the rule bent. Applies to tool-home.html, tool-setup-duplicate.html, admin-records.html and admin-cohort.html.
admin-cohort.html — "The 24" — is the fifth admin screen and the only one that compares the corpus against a target. Three things it must never do. It counts learners, not sittings: one student with two finished sittings is one learner, and the day the roster bug produced exactly that is the day this distinction stopped being pedantic. It counts finished ledgers only — a student halfway through is not recruited, and a screen that counted them would tell the founder to stop looking. And it holds headcount apart from readiness: what blocks a queue build today (unmeasured noise status, takes still on a phone, server faults) is filed separately from the target, because recruiting thirteen more people fixes none of it. An unconfirmed group is filed apart from both — it costs sampling balance, never correctness, and must not read as a blocker.
No dry-run category exists (founder, 2026-08-10). An earlier draft of admin-records.html carried a "dry run" chip on the founder's own sitting; nothing in the database backs one, and a sitting of Chau reading at group A is a group-A learner that counts. The chip is deleted. A mock that invents a corpus fact is the one failure these screens exist to prevent.
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 shelf 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.
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:
#A86B00 amber CLOSE, and any yellow or orange grade.#12A150 / #E5252A — tuned for pure white under ink outlines, acidicon 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.)
--op #2B4B8F operator accent. The tools are no longer marked by a colour theyalone own — they are marked by density (§6), which is what a teacher actually notices.
onboarding-hanviet-ask → onboarding-hanviet, cut forrhythm 2026-08-13 (§5.4b). Not the mechanic: Word check in its audio → gloss direction, with the Hán-Việt reveal, is exercise 2 of the prescribed first lesson. The two files are in ../archive/.
The gallery visually contained more than the ratified seven families, because two screens from the retired six-type enum kept exercise chrome and an exercise counter.
lesson-order.html is deleted. Standalone sentence-ordering, marked exact-only, is *Build &say with its speech phase amputated — and lesson-build-say.html already draws both phases ("You built" → "Now say it"*). Ordering without the speaking is the format we replaced, not a format we ship.
lesson-words.html stops being an exercise and becomes the word-detail surface. *"Word 1 of 3in this lesson", character-by-character decomposition, and a Got it · check me* action is a reference surface — it is the atom rendered for reading, reachable from a study set or a review, and it is genuinely useful there. It loses the exercise counter and the lesson header. **The 1-of-4 Word check mechanic gets its own drawing** and takes the name. (Not "study card" — §11 bans card in the product's vocabulary, and a ledger that uses a banned word to name a screen will put it on the screen eventually.)
A screen carrying n / 6 is claiming a slot in the lesson. Only the six in §10 have one.
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.
A mock is a set of still frames, and inconsistency between frames is the point — path-day-one and day-12 path.html are both true, of different learners, on the same route. That is showcase, and it is allowed (founder, 2026-07-30).
The defect is narrower and it is a lie, not an inconsistency: a screen stating a fact about the tap that just brought the learner there. §8.1 removed state machines; this is the rule for what the static destination is allowed to say. Three instances, all found by walking:
| It said | The learner had done | Fix |
|---|---|---|
| "You picked Hàn Quốc" on the Hán-Việt reveal | tapped any of four; all four land here | the reveal names the answer, never the pick |
| a 12-day streak and goal 2, on backing out of lesson one | started today | a positioned screen exits to the path state it is positioned in |
| Restaurant survival opening from The first 150 words | tapped a different set | one drawn set, reached from the card that carries its identity |
Two mechanical rules fall out, and both are greppable:
· 3 of 5 — exitsto the path state that position belongs to (path-day-one.html for goal 1). A screen carrying Practice · <family> is a specimen, belongs to no day, and exits to path.html.
discovery cards on sets.html are <div>s. A dead card is honest; a card that opens someone else's set makes every rating on the screen unreadable, because the reviewer cannot tell which set a number belonged to.
Why this matters more than it looks. Ratings, counters and progress are all claims about a named object. The moment a tap can land on a different object than the one it named, none of them can be reviewed at all — which is how a walk turns a design question into an unanswerable one.
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 does not open fully offline from file://. Eight do — lesson-picture (the picture prompt), conversation / conversation-feedback / goal-complete (the tea-house scene), path / path-day-one (the scene photography §5.1 calls the best asset), and profile / settings (the learner's avatar). Every other screen renders standalone.
— see §5.4c)*. The Toni character sheet and commissioning brief are history, not canon. Mai's canon and pipeline live in product/mascot/; §5.4c rules her presence (default) and the exclusions that keep her off a screen.
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 ../../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 change — Sheet, Well, InkChip are Voice Studio primitives now; 19 screens import them, and renaming would have turned a skin change into a sweep.
src/app.css and are still hand-written *(still openafter 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.
| Wanted | Where it already exists |
|---|---|
.verdict-word — the non-colour channel §2.5 rule 4 mandates on every graded thing | the 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 chip | four copies across the exercise screens |
.take-row — play control, label, value; the evidence row a score always follows | eight instances across the speaking and completion screens |
.stat-tile — icon square, number, defined label | profile, lesson-complete, goal-complete |
.check-disc — the green aggregate check | six on goal-complete alone |
the pre-speak token — ink type, bar on locked, part-count in the verdict word's slot | every pre-speak and failure state |
2026-08-01). The designed step 5 is Your turn — understand a question, choose a reply, then* speak it (§11) — and that family has no assembler render and no client view. The step as built is assembled from families that do (Match, Word check, Read the character, Say it) and still reaches the ruled objective: the learner ends the step saying 你呢? out loud. This is an open deviation, not a superseding ruling. §5.8's ladder is unchanged, lesson-your-turn.html remains step 5's drawing, and the substitution stands only until the family is built.
choice.htmluses 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.
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.
| Field | Value | Appears on |
|---|---|---|
| Name | An Nguyễn — "An" in greetings, 安 in Chinese | profile, path, conversation |
| Journey | Vietnamese → Mandarin · learning since January · near zero at start | profile |
| Speak-streak | 12 days (only counts if you produced scored speech that day) | path, profile |
| Spoken in total | 3.5 h — the learner's own voice only; model playback and reading time are not counted | profile |
| Parts cleared | 412 | profile |
| Words to review | 12 | quiet-review — and the path row that states it, once the review session ships (§5.1b) |
| Speaking minutes by month | Jan 14 · Feb 19 · Mar 26 · Apr 31 · May 33 · Jun 39 · Jul 48 | profile |
| Next useful repair | Third tone — dip before rising. Shows up in 好 hǎo · 你 nǐ · 我 wǒ | profile, progress |
| Saved recordings | 48 · study sets 4 made, 1 joined — Chapter 5 is finished and sits in the Made-by-you grid, full bar and count, never a checkmark | profile, sets |
| Her shelf, by group (§5.6b) | Made by you — Restaurant survival 点菜 · 32 phrases · 18 learned · 4 due, and it is verified: a set she pasted that we reviewed and promoted, which is why it alone carries ★4.7 from 212 ratings and 1,204 learners while the others carry none · Chapter 7 · this week 麻烦 · 20 · 12 learned · 8 due · Chapter 6 便宜 · 18 · 18 learned · 6 due · Chapter 5 课本 · 20 · 20 learned · finished, full bar. Learning — The first 150 words 词, the official HSK 1 set she joined from the Popular shelf: 150 phrases, not started, so it has no due count and draws no bar. 8+6+4 is the 18 due across 3 sets stated on the due row and on due-today. | sets, due-today, set-detail, profile |
| Day-1 zero state | no streak number, no chart, route at 0 of 5, step 1 is the only door | path-day-one |
This is the run the first ten minutes ends in, and it is the only lesson a new learner can reach. Goal 1, step 1. Objective on every screen: "Say where you're from". Counter: 1 / 6 → 6 / 6. Three atoms and no fourth.
| Atom | Pinyin | Gloss |
|---|---|---|
| 我 | wǒ | I / tôi |
| 是 | shì | am / là |
| 越南 | Yuènán | Vietnam / Việt Nam |
| Exercise | Content | |
|---|---|---|
| 1 | Match | the three atoms — six tiles, hanzi ↔ gloss |
| 2 | Word check | audio → gloss, one of the three |
| 3 | Say it | 我 — atom grain, one syllable |
| 4 | Which one? | 是 shì vs 四 sì — the sh/s initial |
| 5 | Say it | 是 |
| 6 | Say it | 越南 — the climax |
The lesson ladders into the canonical sentence on purpose. Its three atoms are three of the five syllables of 我是越南人, and step 4 drills exactly the initial the learner later gets wrong on it — 是 said as s is the FIX on take 1 below. The climax is 越南, the learner's own country — the first thing they say in Mandarin that is about them. Onboarding's take is 你好,我来了 (§5.4b), so 我 is the only one of the three the learner has said before, and 是 and 越南 are genuinely new. The improvement callback on lesson-say-yuenan is take 1 → take 2 of that exercise, not a callback to onboarding — 越南 does not appear in the onboarding take at all. Nothing here is a coincidence and none of it should be re-cast without re-casting all of it.
Atom grain means one word on screen. A five-syllable sentence at step 3 is the wrong floor and belongs to the later lesson.
Say the whole sentence — one sentence, one clear repair. A later session, 1 of 5, and the origin of the whole speaking-state catalog. Not reachable in the first ten minutes.
| Field | Value |
|---|---|
| Hanzi | 我是越南人。 |
| Pinyin | wǒ 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 |
| Prompt | You are asked: 你是哪国人? |
| Take 1 | 89% · all 13 parts scored · 2 to repair. 是 — initial sh said as s → FIX. 南 — tone 2 stalled halfway → CLOSE. Everything else CLEAR. Take 2.4s. |
| Why 13 and not 15 | five syllables × three parts = 15, minus the two zero-initial syllables 我 and 越, which have no initial to score. |
| Take 2 | 96% · 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 retry | 100% · all parts judged and clear → data-all-clear, the one win moment. Take 2.2s. |
| The failed repair | 93% · 2.3s. 是's initial lands as s a second time → FIX again; 南's tone came good, so the screen has exactly one problem. This is the state where Skip appears (speaking-skip.html): repair first, Skip after a failed retry, and the SRS comeback said plainly on screen (ruled 2026-07-31). |
| 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 different word | From-memory mismatch (lesson-say-memory-mismatch.html): the take fails part alignment → below the floor by construction → NOT SCORED, no red anywhere, "That wasn't 面条" and a free retry. A mismatch is a content failure, not a mouth failure; lending FIX red to it would make every real red unreadable (ruled 2026-07-31). |
| Quiet session | quiet-review.html twin cast numbers: 12 words due (one number wherever it appears — the path row states the same 12 once the session ships, §5.1b) · 18 exercises across Match, Word check and Which tone? · atoms drawn from goals 1 and 2. Not a lesson: no counter, no streak, no celebration, and the end state hands back to speaking (ruled 2026-07-31). |
我昨天晚上跟朋友一起去了一家很好吃的中国饭馆 · 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.
| Goal | Steps | |
|---|---|---|
| 1 | Introduce yourself naturally. Say where you're from, catch the reply, and ask someone back. | 5 |
| 2 | Order food without pointing. | 6 |
| 3 | Ask for directions and understand the answer. | 5 |
| 4 | Handle prices, numbers and bargaining. | 5 |
| 5 | Make small talk about work and family. | 6 |
| 6 | Get through a phone call. | 4 |
| 7 | Explain a problem — and be understood. | 5 |
| 8 | Hold your own in a real conversation. | 7 |
Goal 1's five steps, in order, each with its activity pastel: 词 Say where you're from (mint) · 聊 Use it in conversation (lilac) · 说 Say the whole sentence (rose) · 听 Catch the reply (sky) · 呢 Ask someone back (rose) — then ★ the goal conversation, on peach. The pastel belongs to the activity, so when §5.8 re-ordered the ladder each colour moved with its step.
Tracks: Recommended · HSK · TOCFL · Traveling. About 24 min for goal 1.
| Who | Role |
|---|---|
| Mai | the one character. Companion on the path, and the player of every scene part — in the tea house she plays 美玲 Měilíng: 你好!我叫美玲。 |
| An · 安 | the learner |
美玲 is a part, not a cast member (founder ruling 2026-08-04, Mai plays the role ../../docs/2026-08-04-founder-ruling-mai-plays-the-role.md). Mei was retired as a character; Mai wears the face, the avatar and the voice, and the scene card names the role before the first bubble — "I'll be 美玲 — someone you just met." The role keeps its name on purpose: goal 1 exists to teach 我叫…, so the learner has to hear a self-introduction modelled, and a nameless part would have deleted the model sentence. The stranger fiction survives because a part is being played — a companion who greets you by name on the path cannot literally be the person you introduce yourself to. Lilac is still conversation's colour: it belonged to the activity, never to Mei — the same rule that survived §5.8's pastel re-order.
ONE ROLE PER SCENE IS HERS; INCIDENTAL PARTS STAY FACELESS (ruled 2026-08-04, surfaced by lesson-catch-scene). A scene may carry more than one non-learner speaker — the noodle shop has a waiter as well as 美玲 — and one Mai per screen forbids her wearing both. So a scene names exactly one role as hers, and every other part keeps the glyph disc it already had (服 for the waiter): a hanzi in a plain circle, no face, no name. This is not a shortfall to be filled by a second mascot pose — an incidental voice should read as scenery. She is the person the learner is practising WITH; a waiter is a thing that happens in a restaurant. Consequence for titles: a scene question names the role, never the player. What did 美玲 order? is right; "What did Mai order?" would be asking about the companion rather than about the part she is playing, which is the whole distinction this ruling rests on.
AND SHE IS NOT CAST IN EVERY ROLE (founder, 2026-08-04). Mai plays generic conversation partners — a stranger over tea, someone at a counter, the person you are practising with. She does not play roles that are specifically somebody else: 老师 the teacher, 爸爸 and 妈妈, and the other named relations low-HSK content leans on. Those are their own characters, to be authored when the content needs them, and until they exist such a part keeps a faceless glyph disc rather than borrowing her face. The test is whether the role is a person the learner has a relationship with — a teacher and a parent are; a stranger in a tea house is not. Forcing one character into all of them would make the cast read as a costume rack, and "Mai as your mother" is the sentence that shows why.
Conversation demo: a tea house, 4 learner turns, currently turn 2 of 4. Two turns are cast:
| Turn | Said | Score |
|---|---|---|
| 1 · history | 你好,我叫安。 | 96% · 12 of 13 clear — 好 CLOSE (tone 3 stayed flat) · 2.1s |
| 2 · the one being spoken | 我是越南人。 | the canonical sentence's take 1 — 89%, 是's sh said as s → FIX, 南's tone 2 stalled → CLOSE · 2.4s |
Turn 2 is take 1 of the canonical sentence, and the repeat is the point. Goal 1 is one story about one sound: step 1 drills 是 shì vs 四 sì, step 2 is where the sh collapses to s under real pressure in the scene, and step 3 — Say the whole sentence — is the repair lesson that exists because of that turn. So speaking.html opening on the same 89% is not a duplicate cast: a repair lesson does not hand you a fresh first attempt, it hands you back the take that showed the problem. Say so on the screen; do not re-roll the numbers to make it look new.
| Set | Origin | Numbers |
|---|---|---|
| Restaurant survival · 点菜 | verified · Travel | 32 phrases · 18 learned · 56% · picks up at 辣 · ★4.7 from 212 ratings · 1,204 learners |
| The first 150 words · 词 | official · HSK 1 · Speaking | 150 words · 9,530 learners · ★4.9 |
| Getting around Shanghai · 行 | verified · Travel · Conversation | 64 phrases · updated this week |
| Introduce yourself at work · 聊 | official · Work · Conversation | 38 phrases · 1,463 learners · ★4.8 · 12 min |
| Ordering without pointing · 茶 | verified · Food · Listening | 52 phrases · ★5.0 from 76 ratings · 890 learners · 18 min — the browse state of the set page (§5.6c) |
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.
Where a rating appears, and where it deliberately does not. Three surfaces, three jobs:
| Surface | Shows | Why |
|---|---|---|
Discovery card (sets.html) | the average, in one fixed slot | you are choosing; the average is the signal and there is no room for more |
The set page (set-detail.html · set-detail-studying.html) | the average with its count — and your own stars once you have studied it (§5.6c: the input row is the studying state's; browse shows the aggregate only) | you are judging, and ★5.0 from three people is a different claim from ★4.7 from 212 — the count is what separates a signal from a decoration |
| The continue card | neither — nor a learner count | you already chose this set; it shows progress, and a rating on it is noise at the moment of resuming |
So the continue card and the set page showing different things about the same set is intentional, not a contradiction. The rating count appears on the set page only, never on a card.
Ratings ship (founder, 2026-07-30, replacing the 2026-07-21 "no ratings" call). A signed-in learner rates a set from the set page — one rating per person per set, changeable, and that is the whole feature. Stars only; no written reviews, ever — a number cannot libel an author or need a takedown policy, free text can do both, and the review box is what turns a rating into a moderation product. A set below the display threshold shows no rating at all — not a zero, not three grey stars, not "no ratings yet" in the slot where a number goes. The cold-start rule that hides thin learner counts governs here identically: a figure appears when it is true.
path.html is not day one and must stop borrowing its numbers. An has a 12-day streak, so she has history, and nothing in the product currently draws what history looks like: every route node on every screen is in its untouched state, and there is no cleared-node treatment anywhere.
| Field | Value |
|---|---|
| Goal 1 · Introduce yourself naturally | complete — all 5 steps, and it is the first goal to show what a finished goal looks like in the 8-goal ladder |
| Goal 2 · Order food without pointing | in progress, 2 of 6 steps · the current route |
| Route | steps 1–2 cleared, step 3 current, 4–6 ahead — three node states on one screen |
| Lessons completed | 12 — and the split matters: 8 on the path (goal 1's five plus its goal conversation, plus two of goal 2) and 4 from study sets, because An is 56% through Restaurant survival. Both rails, one total. It lives on profile.html where both are already counted, not on the route — three 12s on one line beside a 12-day streak and 12 words to review reads as a copy-paste defect, and a printed total invites an arithmetic the route cannot show. |
**Goal 2 — Order food without pointing, 6 steps.** Cast so it stops drifting; the shape deliberately mirrors goal 1 (vocabulary → conversation → whole utterance → listening → production unprompted):
| Step | Content | |
|---|---|---|
| 1 | Say what you want | 我要 · 这个 · 一杯 |
| 2 | Order with a waiter | a guided 4-turn exchange |
| 3 | Say the whole order | 我要一杯茶。 |
| 4 | Catch the price | the number inside the reply |
| 5 | Ask for the bill | 买单。 |
| 6 | Order without the menu | production with no prompt |
| — | the goal | Order a full meal · table to bill, no pointing · about 26 min |
No goal-2 exercise screen exists, and that is fine. Each node points at the screen that draws its family — the content behind it is goal 1's. A route node is a promise about a kind of practice; the mock proves the kind. Do not draw twelve more screens to make a route honest.
The three node states carry no new hue and no verdict colour, and that division of labour is ratified: the trail carries sequence — joined up behind her, stepping stones ahead, and where the line becomes dots is where she is — while the node carries activity and current position, by a check replacing the activity icon on a cleared disc and a double ring plus Continue on the current one. Legible without a legend.
**The condition on it (codex, 2026-07-30): a node ahead is fully coloured, so it reads as available — and that is only true while "ahead" means unlocked.** The moment a step can be locked, colour is carrying two meanings and the dotted trail cannot separate them, because a locked step and a merely-future step sit in the same place on the same line. Locking needs a state on the node itself, in form rather than hue, before it ships. Nothing today locks, so nothing today is wrong; this is the tripwire on the change that would break it.
path-day-one has a primary CTA — Start step 1 — and day 12 lost it: the screen opens on three completed things and the current node sits below the fold. The home surface of a returning learner is the conversion surface (§5.1), and a learner who opens the app to a wall of finished work with no visible next action has been shown their history instead of their next five minutes.
The goal card carries the action. Scrolling to find it is not an answer, and auto-scrolling to it is a state machine (§8.1).
The badge and the bar are the same fact and must not both carry a number — printing 0 of 5 steps beside an 8% bar is how they came to disagree. The badge counts steps; the bar draws it. Same instinct as §5.3.
The learner cast above does not appear on a tool surface, and that is the point: these are different people doing different work.
| Who | What |
|---|---|
| cô Hường | the operator. Runs the sittings, remotely, without the founder present. |
| Chau | the founder. Who you message when the app stops on purpose. |
| R0 · R1 · R2 · R3 | the 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 words — song-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.
Two lessons a day on free, spent the moment the first take is scored, and inside a lesson speaking is unlimited (founder, 2026-07-29). 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 ../../docs/project/subscription_overview.md:
audio, progress, before/after, the tone board — none of it counts.
and not at completion, which would make abandoning the last exercise free. Once spent, the lesson finishes whenever the learner likes, so the wall never lands inside one. MaiSay never says a sound is wrong and then asks for money before showing the repair.
preserved, the reset time — then the offer. A coach, not a toll booth.
Nothing in onboarding is charged, and nothing in onboarding sells — the take is the learner's first scored take, and metering the moment the product exists to prove would be self-defeating. The same reasoning bars the wall itself from the flow (§5.4b, founder 2026-08-13): the offer belongs to the day the meter runs out, not to minute four.
The old counter was 20 scored takes, and switching the unit deleted a whole register of copy. The gate was always a session gate underneath — quota reserved at run start, the wall forbidden inside a repair loop — so the take counter was a translation layer, and every sentence explaining it existed only to reconcile the two: this take did not use one of today's 20 · model playback never uses an attempt · a take that never reaches the scorer costs nothing. All of it goes. There is nothing left to explain, which is §5.9 achieved by deleting the cause instead of the copy.
A small floating banner at the top of the home surfaces — path, sets, progress, profile — reading the lessons left today. Absent from every lesson, exercise, speaking state, conversation turn and completion screen, which is the same boundary §5.6 draws for the tab bar: while the learner is working, the surface belongs to the work.
Showing the count is only possible because the unit changed. 2 lessons left today is a fact a learner can plan the evening around; 17 attempts left was a number that meant nothing until it was zero, which is why it was never on screen and why the wall always arrived as a surprise. The banner exists to make the wall predictable, so it must never read as pressure: it states the number and nothing else, it does not count down inside a session, and it does not turn a warning colour. On Pro it is not there at all — an unlimited meter is a banner that only advertises.
Screens: due-today.html · nothing-due.html · set-add-words.html. Behaviour is docs/project/study_sets_overview.md ../../docs/project/study_sets_overview.md; this is what the surfaces owe.
One button, not a set. A learner studying alongside a class holds several hundred phrases across eight chapter-sets, and choosing which one to open in the morning is the friction. The sets surface carries one due count across every enrolment and starts the session directly. due-today.html is the browse affordance, not the default route — a summary screen between the button and the lesson is the choosing this rule removes.
"Due" is a fact about the clock, never a grade. No verdict colour appears on a queue surface. The per-phrase colours a learner has earned live on the set page, on white (§2.2). A count of what is owed is not a score, and colouring it would make the calendar look like an opinion of the learner.
An empty queue is answered, never filled. When nothing is due the product says so, offers real doors — drill a phrase, quiet review, add this week's words — and puts no primary action in the footer. Serving a filler lesson so a metered one gets spent is the worst available use of the free tier: it charges for our own inability to say that's everything. It is equally not a congratulation — the product has exactly two win moments (§5.4) and finishing a queue is neither, because nothing was spoken.
The weekly add is its own surface, and the duplicate is not an error. set-add-words.html is the import preview plus the one fact it cannot know: what was already there. A class re-teaching its own vocabulary is the normal case, so a repeated phrase sits on locked — the empty track, "we made nothing of this" (§2.2) — never on red, and the row answers the only question a learner actually has there: your progress on it stays.
A resolution the machine made is stated on the line it happened to. A stripped trailing gloss and a traditional form mapped to simplified are both facts about our reading of the learner's text, and each is said once, in place, against the actual characters (§18 r1) — not summarised above the list and not left silent.
Screens: sets.html · set-detail.html · sets-empty.html.
The engine was already right; the labels were not. A study-set lesson is Match → a discriminating look (Which tone? / Read the character / Word check) → Say it, drawn from the same catalog the path uses. Every surface around it, though, counted only the last rung: the card said 18 of 32 phrases spoken clearly, the tiles said spoken clearly · nearly there · not spoken yet, and the shelf's eyebrow said Built for spoken Mandarin. A learner who had matched a word, picked its tone and spoken it once for an unjudgeable take read not spoken yet — the app telling them they had done nothing. The word for the rail is learn.
The three states are Not started · Learning · Learned. Not started is never in a lesson — the empty track on locked, and never a grade. **Learning is met, not amber**: it counts every phrase that has been taught and practised, including a repaired take and one we could not judge. Learned is every word in the phrase said cleanly first try.
Learned still requires clean speech, and that is the point of the split. Recognition earns Learning honestly and cannot buy the top rung — a tapped tile is a one-in-three guess, and on this product knowing a word means being able to say it. Widening the middle state is what makes the top one strict without being cruel.
Learning is not borrowed from the verdict family. This retires a standing awkwardness: the middle tile could not say close, because CLOSE is a verdict word with a colour behind it (§2.2) and printing it as a grey noun taught the eye a second, colourless meaning for the one word the analyzer needs unambiguous. A learn word has no such collision.
What does not change. Review is still speaking — a returning word comes back as Say it, always, because AtomProgress only ever advances on a spoken take — so due-today.html and nothing-due.html keep their speaking language and are correct to. The rail is a learn rail; the review queue inside it is not.
Use the product's words, no synonyms. From ../../docs/project/content_model_overview.md ../../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. 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 ten exercise names are reserved words (ratified 2026-07-29; Fill it in joined 2026-07-31). Seven families carry them; learning_overview.md holds the catalog. On screen they are used for exactly the exercise they name and nowhere else:
| Family | Name(s) |
|---|---|
| Speak a known target | Say it |
| Comprehend, then speak | Your turn |
| Assemble, then speak | Build & say |
| Listen and choose | Which one? · Which tone? · Catch it · Fill it in |
| Cross-form vocabulary | Word check |
| The character as an object | Read the character |
| Pair up | Match |
Word check's directions do not get names (ruled 2026-08-01). audio → gloss, hanzi → pinyin, audio → hanzi are a parameter of one exercise (§5.8b), not siblings of it. Naming a direction on screen would mint an eleventh reserved word for something the catalog does not have, and the reserved list is closed for exactly that reason.
"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.
A phrase in a study set is Not started · Learning · Learned (founder, 2026-08-04, §5.11b) — three learn words, on the pill, on the tiles and on the card's n-of-m line. **Never Clear, Close or Not yet on a set surface**: the first two are verdict words the analyzer owns (§2.2), and reusing them for progress teaches a second, colourless meaning for the one word that must not have one.
Never: card, deck, quiz, level (say band), assessment (say attempt), credits, hearts, XP (say what was actually done — "412 parts cleared", not "340 XP"). One carve-out (ruled 2026-07-31): HSK 1–6 are the exam's own published names and appear as-is on the HSK track surfaces — a proper noun, not our vocabulary. Level stays banned for our bands. A named level says New HSK (adjudicated 2026-08-13): both syllabuses reuse the labels 1–6, so "HSK 3" alone is a claim about a word list the learner cannot check. The track is explicitly the New HSK (3.0) route, every user-facing level reads New HSK n, and no surface claims to prepare a learner for both.
Never on a learner surface — pipeline words (ruled 2026-07-30): uncalibrated, coverage, calibration, decode, cache, model playback (say "MaiSay's voice" or "the example"), onboarding (a flow name, not a thing the learner did), and atom — the learner word is word. These are the bench describing itself; the 2026-07-30 sweep found them on 14 screens, led by a score strip reading Full coverage · uncalibrated beside a 100%. A learner cannot act on any of them.
One control, one name. The button that plays the reference pronunciation is Example — ▶ Example on the take card, "Hear the example" where the footer carries it, and "Hear 越南" where naming the word itself is shorter. Never Model: that is the analyzer's word for the thing it compares against, and a learner three minutes in has not been told there is a model, let alone that hearing it is free.
One unit, one name. The scored unit is part, everywhere — "412 parts cleared" is the canon example above. set-paste.html calling the same unit a practice target was the counterexample: two internal names for one thing, both learner-visible. On a study-set surface the countable is a phrase; what scoring counts is a part; nothing is ever a target.
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.
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.
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.
| Mock | Shows | Spec says |
|---|---|---|
sets.html | Nothing — this row records a fixed violation | The 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-picker.html | Four tracks — Recommended · HSK · TOCFL · Traveling | learning_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.html | Nothing — this row records a closed contradiction | The 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.html | Seven months of speaking minutes to a free learner | subscription_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-*.html | Nothing — this row records a fixed violation | The admin handover's §5 said the admin surface is Vietnamese "like every other surface"; §6.5 rules it English. The handover line was corrected on 2026-08-05, when the admin set grew admin-records.html and admin-grading.html under the same ruling. |
set-paste.html | 6 lines → 5 practice targets → 8 words | study_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. |
<link rel="stylesheet" href="studio.css"> and<script src="shared.js" defer></script>. Wear the chrome: <studio-status> always; then the tool header on a tool surface, <studio-nav> on a home surface, and nothing inside a lesson (§5.6). Never re-copy tokens into a screen; never hand-write a status bar. 1b. Never invent a class name. Every class in the markup is either a Tailwind utility or defined in src/app.css. A class that is in neither is dead text and the browser ignores it — which is how conversation.html shipped a footer with class="action" that had no layout at all, dropped its mic glyph, and read to the founder as "the mic button is missing." The house action bar is <footer class="shrink-0 basis-[70px] flex gap-2 px-[17px] pt-2 pb-3 bg-paper"> with control-icon / control-label inside .button; copy it, don't re-name it.
On a flex footer, the basis is the button height — they stretch. basis-[70px] gives the 52px control every screen shares; onboarding-first-take carried basis-[102px] and rendered two 80px slabs, which is what the founder saw. A taller footer is only correct when it is not flex and the extra height carries a caption below a normal-height button — onboarding-mic (102) and paywall (86) are both that shape and both correct. Height belongs to the content, never to the controls.
1c. A brand-new arbitrary value silently renders as nothing until npm run build. studio.css is compiled Tailwind, so a utility like -mt-[584px] that no screen has used before is not in the stylesheet yet — the markup looks right, the class name is legitimate, and the element renders at offset 0. This is rule 1b's failure mode wearing a valid class name, which makes it worse: there is nothing to grep for. Rebuild before you judge a layout, and if you are mid-pass on a shared file, use an inline style for measured geometry the way profile.html already does. And judge it from the PNG — this is exactly the class of bug that reading the HTML cannot catch.
in one file. Anything appearing on three or more screens is promoted into src/app.css instead.
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.
.device-stage → .phone → .app, 390×844) and wire every href,including the tab bar's active state.
<!-- MOTION: … -->), never in JS — and must obey §17.../../docs/project/ ../../docs/project/ before drawingit. A new contradiction goes in §13; it does not go in silently.
cd .. && npm run all. A utility class that was not in the markup at build time doesnot exist in studio.css, so an un-rebuilt screen silently drops the style.
ORDER slot, a BLURBS job line and an APP_GROUPS section in../scripts/build-gallery.py ../scripts/build-gallery.py.
file:// and confirm it stands alone.Every screen has one dominant job, phrased as plainly as possible, and that line lives in BLURBS in ../scripts/build-gallery.py ../scripts/build-gallery.py — not 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.
Three sources of truth, by question:
| Question | Source |
|---|---|
| How should it look? | this file + the screens beside it |
| How should it behave? | ../../docs/project/ ../../docs/project/ — the spec wins |
| What is the data shape? | the generated OpenAPI DTOs — never a hand-written model |
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.
Rule 8 is written for the listen-back, and the rater's take control is the one place its toggle does not reach (ruled 2026-08-05). The premise — while audio is running a tap stops it — holds where a take is long enough that stopping it is worth a button. On the rating item the stimulus is under three seconds and is replayed hundreds of times a sitting, so the honest answer to what does my next tap do is always play it again: a restart costs a rater one tap where a stop costs two. That control has no stop state, and its progress rides a ring drawn at rest and empty, so an arriving measurement cannot resize it. The rest of rule 8 binds it exactly — nothing sweeps when the length is unknown, and the end of playback is still not a state, because the ring empties and the icon was never anything but play.
What both halves of rule 8 are really protecting is a control that does not move. The rater's did: it swapped its label between Nghe lại and Thử lại, grew a stop icon while playing, and sat beside a second, untappable black disc that teachers tapped instead — three states, three widths, and two controls for one sound. One control per sound, and one size for every state it has.
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.
10 · The route reveal is the product's one big animation (founder, 2026-08-13). Rules 1 and 5 keep motion small everywhere else, which is exactly what buys this one moment its size: on first arrival at path-day-one, the route line sweeps down, the nodes pop in on a stagger all the way to the last goal, the camera settles back on step 1, the goal card slides in, and the CTA breathes once. That is the whole choreography and it is spent in one pass — it is the learner's map of the next three weeks drawing itself, which is a real quantity being shown, not a flourish. One time only: every later arrival at home draws the settled frame instantly, because an animation the learner has to sit through twice is a toll. prefers-reduced-motion gets the settled frame directly, and it loses nothing — the end state is the map.
11 · The plan assembles itself, one line at a time (ruled 2026-08-13). On arrival at onboarding-plan, the three checklist lines tick in sequence, ~300ms apart — deliberately slower than rule 1's 50–70ms reading stagger, because this is not the eye being given an order to read in, it is three facts being confirmed one at a time, and a stagger fast enough to read as layout says nothing. The text of each line and its hairline rule are already there; the pastel disc and its tick land together, as one object — a coloured disc sitting empty while it waits for a check to arrive is a radio button mid-animation, which is the read this screen exists to avoid, so the coin is never on screen unticked. Once the third has landed, the wall fades up as one block, never tile by tile — twelve staggered tiles would be a performance the learner sits through, and the wall's message is its mass, which only exists once it is all there. Under a second and a half, one pass, nothing loops or fills (rule 2 — the ticks are discrete events, not a bar reporting a guess), and nothing celebrates: assembling a plan is not an achievement (rule 5), so no scale, no green, no spark. prefers-reduced-motion gets the all-ticked end state instantly, which is exactly the static mock.
Early viewers (three, independently agreeing, and codex's fun not proven from another angle) read the app as dry, formal, and text-heavy — "like a linguistics research paper." A visual sweep of all 48 learner screens confirmed it and found the causes. This section is the ruling; the founder delegated the register call (2026-07-30). The §1 position stands — practice room, not a game — but §1 also names the failure mode this section exists to fix: too calm.
Two voices, split by slot.
self-description. This half was already right and does not move.
sì, not 是 shì."* is the house sentence: narrative, warm, anchored to something the learner actually did. Warmth is licensed by truth — a warm sentence must contain a measurement or an event; an exclamation with no fact under it is banned, and so is a mascot doing a bit. When the learner's feedback language is Vietnamese, warmth is written in Vietnamese — warmth translated is warmth lost.
Five greppable rules, each the cure for a defect the sweep found on ten or more screens:
slots, not four paraphrases — the retired onboarding pick screen said listen, then pick five times. One slot carries the instruction; the others do their own jobs or go quiet. (This is §5.9's repeat test applied to instructions.)
rule, what HSK-style means: each has one home surface where it is said once. Any other surface gets at most a chip (Not timed), never the paragraph again. The rule admits no "but this screen makes the same point in its own words" exception — that exception is what put never what you can reach on both onboarding-goal and onboarding-band.
qualifies a number. 89% beside 2 to repair is complete; Full coverage · uncalibrated was the bench talking to itself in front of a learner.
is-computed collapse to one plain line, with the why behind a tap. The learner glancing gets a verdict and a colour; the learner who asks why gets the full answer. A lecture that is default-visible before the learner has even spoken (onboarding-mic.html) is furniture, not teaching.
The tap is a component, not a corner annotation. The disclosure is a full-width row under a hairline at the foot of the panel it opens: the contextual question in ink on the left — "Why are these different?", "How the five tones move", "What verified means" — and a chevron on the right. It is inert in a mock (§8.2). What it replaced was Why · tap set in 12px uppercase in the panel's corner, which read as a footnote about the panel rather than a control on it — the learner has to be able to see that there is more, and what more there is, without tapping to find out.
final frame, never repair pink on a win). Voice's half of the rule: the completion sentence names what was won — "You held all four turns over tea" — not the count of steps it took.
What this section does not license: streaks as pressure, red as scolding, jokes at the verdict, or copy that argues (§5.9). The app is a coach who was there — it saw the take, it names the win, and it never fills silence with policy.