# Round 3 - full set, decides the sweep

Eleven live screens, each triple-bound: BINDING (what the mock claims and why),
DEMO (what is actually wired vs invented vs review scaffolding), and forks
(judgement calls this round made that need a ruling). React with the checkbox
per fork — check means "confirmed as recommended", leave unchecked and write
next to it for an override.

> # ⚠ THE TICKS BELOW WERE WITHDRAWN 2026-07-31. THIS FILE RULES NOTHING.
>
> Every fork box below was ticked on 2026-07-30 in one bulk acceptance, and the
> operator withdrew that acceptance on 2026-07-31: *"this is not something that I
> accepted intentionally and the agent didn't clearly state what I accepted to."*
> The 22 asks were buried across eleven sheets and seven of them were not
> cosmetic at all.
>
> **The boxes are reset to unticked so nothing here reads as consent.** The
> original ticked state is in git and in `docs/archive/DECISIONS-history.md`.
>
> **This file is now REASONING ONLY** — read it to learn what a fork actually
> proposed and why. **Status lives in exactly one place:
> `docs/DECISIONS.md` § Design & UI**, which carries all 22 in plain words with
> each one marked provisional, struck, or kept. If the two ever disagree,
> DECISIONS wins and this banner is what is stale.
>
> **RULED 2026-07-30 — part ② below still stands** (the screen verdict). Part ①
> did not survive.
>
> **① WITHDRAWN. The 22 forks are NOT accepted.** 21st.dev stays reference-only,
> which was separately ruled and is unaffected (`docs/DECISIONS.md` § Design &
> UI).
>
> **② Only two of the eleven screens are build-ready.** A ticked fork is not
> screen sign-off.
>
> - **BUILD — `login`** (drop the sample values) and **`inbox-per-channel`**
>   (minor changes expected after the build, not before).
> - **HOLD, in his words — `analytics` + `activity-filter-bar`** "horrendous" ·
>   **`contacts`** "filter rework" · **`agent-overview`** "I like it, maybe
>   explore it refactor and such" · **`knowledge-doc`** "completely went off
>   track and direction" · **`flows-canvas`** "still misses a ton, weird, lets
>   stick maybe with inspiring from manychat and it hardly did that" ·
>   **`settings`** "horrible, not a complete sweep, didn't consult in direction,
>   very cluttered" · **`home`** "rename to **dashboard**, literally might need a
>   full rework". `activity-detail-panel` was not named; treat it as held.
>
> **③ Why nine failed, and the method change it forces.** These eleven sheets
> were authored by parallel seats. *"making the mockups in workflow is like
> making different designers do different screens, there is no direction and the
> output is all over the place. and way more expensive too. orchestration should
> be done in case of a controlled output not creative."* A rework round is
> authored by ONE hand, screen by screen, against a direction agreed BEFORE the
> first file — see the amended sweep entry in `docs/DECISIONS.md` and the
> Spawning law in `KICKOFF-PROMPT.md`.

---

## analytics

`analytics.html`

### Binding

- CONCEPT REWORK: the landing is no longer five report blocks, it is a COVERAGE MAP of everything the workspace holds a record of. Eleven measures in four named groups (Conversations 2, The agent 4, Outreach 3, Delivery and cost 2), each derived from a real table in schema.prisma: Conversation/Message, AgentTurn, ReviewItem, AgentTurnKnowledge, ModerationAction, Campaign/CampaignRecipient, FlowSession/FlowRunEvent, Contact/Consent, MessageStatusEvent/QualitySnapshot, AgentTurn cost fields. Completeness is stated, not implied: a tally line reads "11 measures · 8 reporting today · 3 recorded but not yet reported" and each group header carries its own right-aligned count, so the operator can see the set is closed rather than assorted.
- EVERY ROW PREVIEWS ITS DESTINATION, not a title and a number. Each row carries three things: a plain noun name, one sentence stating the ACTUAL ANSWER in words and figures ("1,253 of 1,842 interactions closed without a person. 405 waited for review and 184 went to a human."), and an "Opens" line of inline chips naming the drill's real sections ("Handled, queued, escalated · Per situation · Silence reasons · Right and wrong labels"). This is ManyChat evidence 6 copied literally, the trigger-summary chips under a flow name, so you never open a view to learn what is in it. The Agent outcomes drill is drawn below the landing precisely so the chips can be checked against the sections they promise.
- NO TRAFFIC-LIGHT BARS anywhere, and no chart of any kind on either screen. Round 2's three-segment green/orange/red split bar is deleted, not recoloured. The outcomes distribution reads as a per-situation table with right-aligned tabular columns, scope='col' headers, a human-readable first column and a totals row; the silence reasons and the right/wrong labels share ONE bordered panel split by a single vertical hairline. Colour appears only on status words (Live, Thin today, Nothing yet, Not reported) and the accent, never as a magnitude encoding.
- ROWS IN ONE CONTAINER, not cards: a single bordered surface with hairline separators, the same look approved for the scope-13 Knowledge rows. Row height is uniform at 122px, all eleven headline figures right-align on one numeric spine in tabular numerals, and hierarchy comes from weight and position rather than from eleven boxes. Group headers reuse ManyChat evidence 9's one CountRow primitive: a small-caps label with its count right-aligned, the same row shape for all four groups.
- A MEASURE WITH NO REPORT IS LISTED BUT IS NOT A LINK. Reply speed, Flow runs and Contacts and consent are recorded in the database and countable today, so they hold their place in the map with an honest sentence saying the data exists and nothing reads it yet; their figure is the em dash null glyph, their chips read "Would hold" and are greyed, they carry no chevron, no hover fill and no place in the tab order, because there is nowhere to go. This is what makes the map a coverage map instead of a list of endpoints that happen to exist.
- COPY PASSES THE CONVENTIONS BAR, including the rule round 2 broke. Every row title is a plain noun phrase; the question-phrased headings from round 2 ("How much came in?", "What is it costing?") are gone, per the checklist line banning question-phrased headings. No em dash appears in prose, only as the null placeholder in data cells. Status is always a word plus a colour. A foot line names the provenance of every figure and the one third-party read (the WhatsApp quality rating, timestamped).
- FULLY INSIDE THE TOKEN SHEET. Round 2's demo-only --cm-fs-fig (30px) and --cm-fs-hero (42px) are removed: every figure on the landing and the drill headline sits on --cm-fs-xl (24px), so dossier fork 2 no longer has to be answered for this screen. Every colour, radius, spacing and size resolves to a --cm-* value; the only hexes in the file are the token definitions themselves. Light theme only, no prefers-color-scheme block. All hit targets are at least 36px, focus is a 2px accent-ink ring, motion is 140ms ease-out and suppressed under prefers-reduced-motion.
- ALL SIX STATES SHIP WITH THE SCREEN AND ARE HONEST. Loading is row-shaped skeletons with the map's structure already stable, never a spinner. Error is a full-width persistent banner naming the 30 second timeout and stating nothing was lost, with Try again. Partial says once at the top that Facebook is missing from every measure and why, instead of eleven times. Offline keeps the last figures with their age stated. Filtered-to-zero keeps the measure in the map, says which channel had nothing in which range, and never offers a create-first prompt. First run shows the full eleven measures with zero reporting, so what is coming is visible before any of it has data.

### Demo

- Every figure is invented but internally consistent and shaped like the real endpoint returns: 1,842 inbound = 1,253 handled + 405 queued + 184 escalated = 68%, the per-situation table sums to the same totals, 1,036 outbound sends with 4 failed = 99.6%. Real values come from getAnalyticsOverview; the three unreported measures have no endpoint at all yet.
- Two rows are frozen in a state so one screenshot shows both: Agent outcomes held in hover, Model spend held in keyboard focus. In the real screen only one row is ever in either, and only on pointer or keyboard.
- Wired for real: j and k move down and up the eight openable rows (correctly skipping the three that are not links), u returns to the top, and the channel and range segmented controls toggle their pressed segment. The single-key guard that stops shortcuts firing inside a text field is the same one useCanvasShortcuts.ts uses. Nothing else is wired.
- The sidebar brand mark is a flat gold stand-in for design/assets/logo-mark.png so the file opens with no asset path. The divider strips, the six-state gallery at the foot, and the drill stacked under the landing are review devices, not screen furniture. Only the landing map and the Agent outcomes drill are the deliverable; the other seven drills are not drawn this round.

### Forks

- [ ] A FOURTH availability state is introduced: "Not reported" (dashed outline, muted), alongside Live / Thin today / Nothing yet. It is what makes completeness provable, because it distinguishes "we count this and it is zero" from "we store this and nobody reads it". It is not in the server's QuestionAvailability union ('live' | 'thin' | 'empty'), so shipping it means either extending that union or computing the state in the web layer from a static list of unbuilt measures. **Recommend: extend the union**, so the honest state is server-computed like every other one rather than hardcoded in the UI.
- [ ] The eleven measures are my enumeration from schema.prisma, not an operator-approved list. The judgement calls: Reminders/PendingFollowup is excluded because Automation is now Flows; moderation sits under The agent rather than getting its own group; Contacts sits under Outreach as audience; shadow-versus-live moderation mode is folded into the moderation row instead of being its own measure. **Recommend: rule on the list itself this round**, since every later drill scope hangs off it. Anything missing, anything that should not be there?

---

## contacts

`contacts.html`

### Binding

- Round-2's direction is kept whole: real app shell (216px white sidebar with the true grouped nav, inlined logo mark, EB Garamond wordmark, sticky top bar), the channel segmented control carrying live counts plus a colour key dot per segment so the filter doubles as the legend for the row chips, quiet-by-default consent (dot plus word for Opted in / Unknown, a filled bad-soft pill only for Opted out, the one state that blocks a send), 28px neutral avatars, hairline rows, no vertical rules, tabular numerals on every count and date.
- State is folded into the name cell and the column set is final at six: select, Contact, Channel, Tags, Consent, Last inbound. Blocked is a bad-soft flag with an icon beside the name, Outbound is a quiet neutral flag on the same line, and the waId sits under the name in mono. The old 95%-em-dash State column and the Channel ID column are both gone; Last inbound (right-aligned, tabular, the active descending sort with aria-sort on the header) takes the freed width.
- ManyChat behaviour 9 is copied literally: ONE count-row primitive, label left and count right-aligned in tabular numerals, reused by every source that can slice the directory. The 206px rail runs All contacts, then Segments, then Tags, every row the same 36px component with hover, aria-pressed selection in accent-soft, and a zero-count row that stays clickable (Commercial 0) rather than being hidden. Selecting a row filters the table; the rail is sticky so the current slice is never carried in the head.
- ManyChat behaviour 10 is copied and it replaces round-2's Filters popover: the Filters control inserts a filter band at the head of the table box, attached to the data it filters, holding Consent, Last inbound, Blocked (the real option lists from Contacts.tsx) plus the outbound scope toggle, Save as segment and Done. Closing it resets the conditions and leaves no residual state. While the band is closed the active filters render as removable chips on the result line, so nothing is ever hidden behind the badge.
- Segments stop being a tab and become saved views in the rail (fork 1), which is the same channels-scope-3 one-concept-one-surface ruling with a cheaper mechanism: a segment is a saved filter over contacts, so it belongs where the filters live. Segments header carries Manage for create/edit/delete, and an active filter can be saved as a segment from the filter band. The section stays marketing+ only; a viewer sees a rail of Tags alone.
- Selection stays an inline accent-soft band above the table, never position:fixed, stating the count on this page and offering Select all 1,269 contacts explicitly so the silent partial-selection trap stays closed; Escape clears it, Ctrl/Cmd+A selects the page, and the header checkbox goes indeterminate. Actions are Send campaign, Add tag and Clear.
- The table is keyboard-operable to the §2 table: / focuses search, j/k and arrows move a roving row focus (APG grid, one row in the tab sequence at a time), x toggles the focused row, Escape closes the filter band then clears selection, and no single-key shortcut fires while the caret is in a field. Row click navigates to the record and never mutates; the whole 44px select cell is the checkbox target so nothing sits under 36px; row focus is a 2px accent-ink ring inset so the sticky header cannot obscure it.
- All six states are named and honest: loading is row-shaped skeletons, refreshing keeps the previous rows and says which minute they are from, offline is a persistent warn banner that names what is disabled, the error names the 30-second timeout and says nothing was lost, first run teaches the next step, and filtered-to-zero renders live inside the table with a reason that changes with the filter and a Clear filters action. All-removed is stated unreachable, because a contact is blocked and never deleted. Motion is 150ms ease-out and fully suppressed under prefers-reduced-motion.

### Demo

- Everything is wired: click a rail segment or tag to filter the 50 rows (Commercial 0 lands on the filtered-to-zero state, Clear filters restores), click Filters to insert the filter band and set Consent / Last inbound / Blocked, tick rows for the selection band, type in search or press / to focus it, and press j/k/x on the table. Verified in Chrome at 1568px: sticky header, sticky rail, in-table scroll, filter band open, zero state, states band.
- Row data, tag names and segment names are invented but internally consistent (902+231+136+0 = 1,269; tag and segment counts are directory-wide totals, which is why a facet count and the visible sample rows differ, exactly as the API would behave). Real field vocabulary and option lists come from Contacts.tsx and contacts-api.ts.
- Segment counts have no field on SegmentRow today; the rail count is the previewSegment rawCount, which is the one live number the API already returns for a saved segment.
- Two rows arrive selected and the sort is held on Last inbound so the selection band and the active-sort header are both visible in one still. Sub-768px collapse (rail becomes a horizontal strip above the list) is written but was not visually verified, the shared browser would not resize.

### Forks

- [ ] Segments moved out of the tab strip into the rail as saved views. The channels-scope-3 ruling said Segments is folded into Contacts, and it still is, but the mechanism changes from a tab to a count-row facet plus Manage. Keep the rail, or restore the Contacts | Segments tab strip? **Recommend: the rail**, it is the ManyChat pattern and it makes a segment behave like what it is, a saved filter.
- [ ] A contact row still navigates to the full /contacts/:id page. ManyChat opens the contact as an overlay (evidence 24) and CONVENTIONS quotes Stripe's default-to-context-view rule, which would argue for the 440px side panel over the list so list position and selection survive. Keep the full page, or open contacts in a side panel with an Open full profile link? **Recommend: keep the full page this round**, ContactDetail carries an edit form and the consent panel that a 440px panel would cramp.

---

## inbox-per-channel

`inbox-per-channel.html`

### Binding

- Plain-language reply-window wording on EVERY channel, WhatsApp included, with no Meta jargon left on the screen: the thread strip reads "Can reply now" plus "6h 12m left of the 24 hour reply window" in tabular numerals, list rows read "Can reply for 21h" or "Reply window closed", and the closed state names the fix where the operator acts rather than in the row (closed WhatsApp composer: "WhatsApp can still reach Vikram with an approved template" plus a Choose a template button; closed Instagram honestly offers no send at all). Round-2's 52px window meter is deleted: there is no bar of any kind on this screen.
- System and automation events render inline in the message stream as centered grey text with the triggering automation name as a link (ManyChat behaviour 7), so the audit trail sits in the same stream as the messages instead of a separate activity panel: "Ayesha messaged first, so the 24 hour reply window opened. Automation Property enquiry welcome started replying. 10:47".
- A per-contact Pause automation picker sits in the thread action row (ManyChat behaviour 8): 30 minutes / 1 hour / 3 hours / 6 hours / 12 hours / 1 day / Until I turn it back on, headed "For Ayesha Noor only" and footed "Every other conversation keeps running", so its scope is unmistakable against any global pause. It uses the token popover chrome (surface, line-strong, radius 6, shadow-raised, hairline-separated 36px rows), no confirm step, undone from the same menu, Escape closes it and focus returns to the trigger.
- Takeover stays a plain enabled "Send reply" plus the accent-soft banner above the composer, with no confirmation dialog. The banner also points at Pause automation as the time-boxed alternative, so the two controls read as one job with two shapes (send once and own the thread, or hold the automation for a set time) rather than two ways to do the same thing.
- Internal notes moved into the message stream on warn-soft with an explicit "Internal note" label plus author and time, and the composer gained Reply / Internal note tabs that remember the mode per conversation. The lead-context rail no longer carries a second notes list; it holds facts only, including a "Came from" line (Comment on the Al Reem reel, 12 Jul) and an "Automation: Running for this contact" line.
- Every option list is code-sourced: the channel filter offers only what channelsForCase("dm") returns today (All channels / WhatsApp / Instagram), so round-2's Facebook DM row is gone. No raw hex survives: avatars are a neutral tint with a hairline, and channel colour appears only in the avatar badge and the header chip. Every control clears the 36px house target floor (icon buttons, nav rows, selects, checkbox rows, popover rows).
- The keyboard vocabulary is visible, not folded away: a "/" key badge inside the search field, "j k move  ? keys" in the list foot, Ctrl+Enter sends in the composer hint, and j / k / Enter / u spelled out in the no-conversation-open state. One list row is drawn in its 2px accent-ink focus ring so the keyboard path is part of the picture.
- All six states ship in the file: loading (tail skeleton with a role=status line), partial (7 of 24 loaded), success (Read / Delivered), error (a flow send that failed, worded plainly as "Not delivered. Instagram refused the attachment. Send it again, or open Details for what Instagram said." with Send again and Details, plus a full-width list error carrying Try again), the three separate empties (no conversation open / no conversations yet / no conversation matches those filters, each action label echoing its title), and offline plus automation-paused.

### Demo

- All content is invented but shaped like real data (names, handles, the flow name "Property enquiry welcome", the truncated Instagram-scoped id, the 6h 12m figure). The 1000-character Instagram counter is stated as a real constraint and still needs confirming against the API before it ships.
- The pause picker is rendered already open and one list row is frozen in its keyboard-focus ring so both show in a single still; in the product only one of those is ever true at a time. The pre-typed draft, the unread badge, the failed send and the paused row co-exist here purely so those states are visible at once.
- The strip below the app shell is a review device, not screen furniture: it holds the states one still cannot hold at once (WhatsApp closed window, the three empties, error / offline / paused). The sidebar brand mark is a text stand-in for design/assets/logo-mark.png so the file opens standalone.
- Only the pause popover and the composer tabs are wired; everything else is visual. Verified in Chrome at 1568x772. The sub-768px layout is written but not visually verified, the shared browser would not resize.

### Forks

- [ ] Internal notes moved out of the lead-context rail and into the message stream (warn-soft plus an explicit label, with Reply / Internal note composer tabs). CONVENTIONS requires the composer to remember a note mode per conversation, and keeping the rail list too would be two surfaces for one job. **Recommend: notes live in the stream**, the rail keeps facts only. Confirm or put the rail list back.
- [ ] "Templates only", the real per-thread filter in Inbox.tsx, is not drawn on the header any more: it moves into the thread overflow menu as a rare control. **Recommend: overflow menu.** Confirm, or keep it inline on the thread header as it is in code today.

---

## activity-detail-panel

`activity-detail-panel.html`

### Binding

- The panel is INLINE in the two-pane split (round-2 fork 2 ruled): 440px column beside the list, never a scrim panel, so the page's own h1, Feed/Needs tabs and filter row stay on screen. Head (who, channel, held-for-approval, waiting time) and the decision foot are pinned; only the body scrolls. Verified at 1440x900: foot bottom 871 vs panel bottom 872 in every state that has one, body overflow true, no horizontal page scroll at 1440 / 1024 / 420.
- Six states ship, all reachable from the banner switch and each with its own foot: reviewing, already decided (success), hidden comment (the moderate kind), nothing selected, loading skeleton, and did-not-load. The error state is a full-width bad-soft banner naming the cause ("the server did not answer, or this computer is offline"), carrying Try again, with the decision buttons disabled and "Decisions are paused until this row loads." The hidden state carries the honest partial: "The full trace was not kept for this comment. Moderation runs before the model."
- History is gone as a separate block. System events now render inline in the same stream as the messages, centred and muted, with the rule name as a link back to itself ("09:19 The agent wrote a reply and held it — Price rule") and Undo sitting on the undoable line. This copies ManyChat evidence 7 literally and removes one whole section from the body.
- Destructive confirms, everything else undoes. Reject opens the four real REJECT_REASONS plus free text in a tray above the buttons and the Reject button becomes Cancel; Delete permanently opens the same tray shape with one filled confirm and becomes Cancel. Keeping a hidden comment and approving a reply both promise undo in the foot note instead of a confirm dialog. Escape unwinds exactly one layer at a time: popover, then tray, then selection, then focus back to the row.
- Focus contract for an inline pane, not a modal one: j/k move the selection and repaint the panel without stealing focus, Enter moves focus into the panel (the draft when there is one), Escape or u returns focus to the invoking row, and single-key shortcuts never fire while the caret is in a field. No focus trap, because the pane is not a dialog and the list must stay operable. Deviation from the APG dialog line in CONVENTIONS, stated here rather than reconciled silently.
- The list follows the identifier-first table rule: checkbox, Contact (name + channel chip + kind on line one, the comment on line two), Needs, then Waiting right-aligned and tabular. Channel is a chip with its name in text, never colour alone. Ticking a row opens an inline selection band above the table (never floating) that states the count, offers "Select all 5 waiting" explicitly, groups its five actions with hairlines, and clears on Escape.
- Third-and-beyond row actions live in one popover from the head (Open the conversation in Inbox, Open the post, Copy the trace reference, Hide this comment, and a hairline-separated destructive Delete), built to the token sheet's popover chrome. Uppercase eyebrows are down to one real field label ("Not sent, waiting on you"); the round-2 "ON THIS POST" kicker is now a plain sentence, and the type/channel eyebrow under each list name is gone.
- Every value is a --cm-* token, light theme only, no prefers-color-scheme, no traffic-light bar of any kind. The two raw hexes round 2 left (skeleton gradient, danger hover) are now color-mix on tokens. Hit targets are 36px on buttons, selects, nav rows and the checkbox label; the Open post link is padded to 29px. Motion is 140ms ease-out with a reduced-motion block. Verified: statusline/meta stay flex, the icon button stays grid, and the draft label stays inline-block across all six state switches.

### Demo

- Clicking any list row repaints the whole panel from that row's data: name, handle, channel chip, waiting, post line (Yusuf has no post, so the line reads "Not from a post" and Open post disappears), comment, draft, character count, the rule that held it, confidence and its note, knowledge hits, trigger and moderation. Priya's row switches the panel to the hidden-comment shape. j/k do the same from the keyboard.
- Typing in the draft flips the footer to "Edited by you", recounts characters live, and changes the primary button to "Send edited reply" — mirroring the real Activity.tsx behaviour.
- Reject opens the reason tray (Escape or Cancel closes it); on the hidden row, Delete permanently opens the confirm tray with the filled "Yes, delete it permanently". The head overflow button opens the popover; an outside click or Escape closes it and returns focus.
- The six-state segmented control in the black banner is review scaffolding, not product UI; in the app the state comes from the selected row and the fetch. All row content is representative (five plausible rows, 0.81 confidence, the two knowledge hits, the trace metrics), not live records.

### Forks

- [ ] The list's column set changed to satisfy the identifier-first and numbers-right rules: Waiting moved from first to last and right-aligned, and the Channel and Type columns folded into the Contact cell as a chip plus a lowercase kind word. It is a tighter, more scannable row, but the list is shared ground with the filter-bar seat, so it needs one yes or no. **Recommend: keep the new column set and hand it to the filter-bar seat.**
- [ ] Round 2's standalone History block is now inline system lines inside the thread (ManyChat 7). It removes a section and puts the audit trail where the eye already is, but it also means a long-lived row's older events are interleaved with messages rather than listed together. **Recommend: keep it inline**, and if a row ever accumulates more than about six events, collapse the older ones behind one "Show earlier activity" line rather than restoring the block.

---

## activity-filter-bar

`activity-filter-bar.html`

### Binding

- The filter bar is a ROW INSIDE the table container, sitting above the frozen header (ManyChat behaviour 10, filter-as-inserted-table-row), not a fly-out and not a card floating above the data: one border on the page, and the filters stay visually attached to the rows they cut. The whole screen renders in the real app shell (216px sidebar with the true grouped nav, Agent active and Activity active with its badge, sticky top bar).
- Two facets stay inline because they are the ones used on nearly every visit (Channel, Outcome); Source and Post collapse behind ONE "More filters" control carrying a badge of how many are active inside it. A filter set from inside that popover still surfaces as its own removable chip in the row, so nothing at all is hidden behind the badge, and "Clear all filters" appears only once at least one filter is set.
- Every count is faceted against the OTHER active filters (round-1 fork 1, recommended side taken): a set chip carries its own count, and every option in a popover carries the count that clicking it would actually yield, honest zeros included. The result readout on the right of the row reads "4 of 15 today" (or "of 6 waiting"), tabular, in an aria-live region.
- Post is a filter like every other one (round-1 fork 2 taken), no longer a separate button: it reads as the post's human name with its id on a second mono line, never a bare id, and it clears with the same gesture as the rest. "Search all posts" still hands off to the existing PostPicker.
- Feed and Needs-action run the same bar with the same chips, counts and clear gesture. Source is ABSENT on Needs, not disabled, and the More-filters popover states why in plain words; if a Source filter was set when the preset flips, the drop is announced in the row itself rather than done silently.
- Every option list is pulled from code, never hand-written: channels from playgroundChannels(), the six outcome words from OUTCOME_FILTER_OPTIONS in @channels/shared/activity-outcome, the three sources from the existing Source select. No engine word appears anywhere in the bar.
- Filtered-to-zero is its own state rendered inside the table: it states which filters were asked for, says today's activity is still there, and offers "Clear all filters", never a create-first action. First-run empty ("No agent activity yet") is a separate screen with its own action.
- All six states ship with the screen: loading is row-shaped skeletons with the filter row holding its shape so nothing jumps, error keeps the bar live and carries its recovery action, partial is the "Load older" pager (Feed only, Needs drains to zero), offline states that the counts stopped being true. Targets are 36px, focus is a 2px accent-ink ring, popovers close on Escape and return focus, arrows move between options, motion is 140ms and suppressed under prefers-reduced-motion.

### Demo

- The 15 rows are invented but shaped like FeedRow. Every chip count and every option count is COMPUTED from that array at render time, so the faceting maths is checkable by clicking (Instagram + Motor City post yields Replied 2, Flagged 1, Deleted 1, total 4); in production the counts come from the server, which is a real query per open facet.
- The screen opens mid-work (Channel Instagram, Post Motor City 2BR launch) so a set inline facet, an unset one, the badged control, an extras chip, Clear all and a live readout are all visible in one still. The real screen opens on whatever the URL carries.
- The gold SO square stands in for design/assets/logo-mark.png so the file opens standalone; the identity, role pill and Activity badge of 6 are sample shell state. The four panels under the dashed rule are review scaffolding for the other states, not screen furniture.
- Screenshots could not be captured this session (the shared browser's screenshot call fails on every tab with a CDP parameter error), so the layout was verified structurally in-page instead: 36px targets, popover z-index above the frozen header, faceted counts, the zero state, preset parity, Source dropped out loud, and Needs sorted worst-first.

### Forks

- [ ] ManyChat inserts its filter row on demand and removes it with no residual state. Here the row is permanent, because this console filters on nearly every visit and a toggled row would hide the active filters between visits. Keep it permanent, or make it toggle in like ManyChat's? **Recommend: permanent.**
- [ ] Options whose faceted count is 0 stay clickable (they read "0" and land on the filtered-to-zero state, which names the conflicting filters and offers Clear all). The alternative is disabling them, which prevents a dead end but forces the operator to clear another filter first. **Recommend: keep them clickable.**

---

## agent-overview

`agent-overview.html`

### Binding

- The screen answers "is the bot okay?" with ONE sentence that is computed, not written: a status word plus colour (Running / Drafting only / Off), a live count ("Answering on 2 of 4 surfaces"), and a plain-words sentence assembled from the real gate states. Flipping any gate rewrites all three. The four readouts (78% handled today · 7d 71%, 3 waiting for you, $2.14 cost today, gpt-4.1-mini bot model) are the exact StatTile set the live Overview.tsx renders, hairline-joined inside the SAME bordered surface as the verdict, per the token sheet's Stat tile spec. One border carries the whole answer.
- The four concept-groups the dossier flagged collapse to two. The per-channel status card and ChannelsTab's per-channel gate card MERGE into one row per surface (dossier fork 1, recommend: merge) inside one bordered container, hairline-separated, in the rows-not-cards shape the operator approved for scope-13 Knowledge and analytics round 2. ThresholdEditor is GONE from Overview (dossier fork 2, recommend: move it off now) - Overview reports, it does not tune.
- Primary-gate-inline split, kept and made literal: the reply gate is a segmented control in the row itself (direct manipulation, edit the row before opening a form about the row); every other gate is a read-only chip under the surface name (ManyChat evidence 6, the trigger summary as inline chips so you never open a surface to know what fires it); everything else lives behind one "Manage" link. Because the row holds controls it is NOT a whole-row link - the surface name is the link, so "row click navigates, never mutates" still holds.
- Only surfaces with a shipped editor get a control. Instagram DMs has no channel card in the codebase, so its gate renders as a read-only "Off" pill with "Set up", its turns cell is the em dash (the one sanctioned use of that glyph) and its note says its replies are not counted in today's figures. Honest partial state, not an invented control.
- Undo beats confirm: every gate change applies at once and raises an undo toast (Ctrl+Z also undoes, labelled in the toast). The ONLY modal confirm is the step up to Live, because messages already sent cannot be recalled - that confirm exists in WhatsAppChannelCard today and is kept. Native <dialog> gives focus-in, Escape and focus-return to the invoking button. The toast dismisses itself if a control that takes focus sits underneath it (WCAG 2.4.11).
- All six states ship in the file: loading is skeletons shaped like the surface with no gate controls rendered until the real mode is known; error is a full-width persistent banner naming the 10-second timeout, stating nothing changed, carrying Try again, with the last good reading stamped underneath; first run is "No channel is connected" with a matching "Connect a channel" action; offline freezes the figures and says how old they are; partial is the Instagram DMs row; success is the default plus the undo toast. Filtered-to-zero is unreachable - this screen has no filter.
- Copy fixes two live violations: the subtitle "Is the bot okay?" becomes a statement lede ("Where the bot is answering right now, what it handled today, and what is waiting for you") because question-phrased headings are banned, and the "Conversational AI" eyebrow is dropped because the top-bar crumb already carries it. Status is always a word plus a colour; the neutral pill is rebuilt from line + ink-2 (8.2:1) since muted on the hover fill measures 4.4:1, and both row meta lines step to ink-2 on row hover for the same reason.
- Token-pure and light only: no prefers-color-scheme block, no hex outside the inlined token declarations, no size above the 24px ceiling (no --cm-fs-fig / --cm-fs-hero, so analytics fork 2 stays untouched), semantic z-scale, 36px minimum hit targets, 2px accent-ink focus ring at 2px offset on every control, tabular numerals on every figure, 140-160ms ease-out motion suppressed under prefers-reduced-motion. Verified in Chrome at 1440, 760, 480 and 320px with no horizontal overflow and no console errors.

### Demo

- Click any Off / Shadow / Canary / Live segment: the verdict word, the surface count, the sentence and the row's chips all recompute, and an undo toast appears (Undo button or Ctrl+Z reverts it).
- Stepping a surface up to Live opens the confirm dialog with copy naming that surface; Cancel or Escape closes it, focus returns to the segment that opened it, and nothing changes.
- Figures are placeholders shaped like the real /api/agent/overview return and internally consistent: 32 turns today (24 + 6 + 2), 7 reviewed, so 25 handled = 78%; 3 of those 7 still open matches the "waiting for you" readout and the Activity badge. Facebook's "Live replies need the new Page token" note is a real constraint from FacebookChannelCard.tsx, not a payload field - confirm before it ships as row copy.
- Everything under the "Demo only, the other states" divider is review scaffolding so loading, error, first run, offline and the nothing-waiting variant can be seen in one still. The sidebar mark is a text stand-in for design/assets/logo-mark.png so the file opens standalone.

### Forks

- [ ] Gate commit pattern is inconsistent in code: WhatsApp uses a dirty-state "Save changes" button, Instagram and Facebook apply instantly with a toast. That is two patterns for one job. This mock applies instantly everywhere with an undo toast, keeping a confirm only for the step up to Live. **Recommend: instant plus undo everywhere, and drop WhatsApp's Save button.** Confirm or override.
- [ ] ThresholdEditor is removed from Overview per dossier fork 2, but the dossier never named its destination (it was parked "until the Moderation console re-homes them"). **Recommend: move it to Agent > Behaviour now**, the only shipped surface that owns how the bot decides, rather than leaving it parked on a landing screen. Or hold it until a Moderation console exists?

---

## login

`login.html`

### Binding

- One brand moment, once: the real logo asset (design/assets/logo.png, inlined as a --cm-logo token) sits at 52px beside the EB Garamond wordmark "Silver Oak Properties" on a single row, left-aligned like every other surface in the product, then a hairline, then the h1 "Channels Manager" at 24/650 sans. Round 1's centred stack of logo + wordmark + h1 + "Sign in to your workspace" tagline drops to two lines: the tagline said nothing the Sign in button does not. The navy/gold/DM Sans system that Login.tsx still ships is gone wholesale; the card is surface on bg, 1px line-strong, radius 6, 380px, and every colour, size and spacing value is a --cm-* token.
- A password reveal toggle sits inside the field at its right edge, 36x36, aria-pressed, with its own 2px accent-ink focus ring. Preventing the mistyped password beats catching it (CONVENTIONS §1.7, NN-H5); the current screen has no way to see what you typed and the only feedback is a failed sign-in.
- Five real, reachable states ship as designed screens, not just the two round 1 drew: empty (the default), partial with the password field in keyboard focus, signing in (both fields disabled, button aria-busy "Signing in…"), wrong email or password, sign-in locked after too many attempts, and server unreachable. Signed-in, loading and a separate empty state are named as unreachable with the reason stated on the page.
- The rate-limit state is new and is real: apps/api/src/auth/login-rate-limiter.service.ts throws 429 after 5 failed attempts on one IP+email inside 15 minutes, and toApiError surfaces its message verbatim today, so the live screen already renders "Too many login attempts. Please wait a few minutes and try again." in the same red box as a wrong password. Round 3 splits them: a lockout is warn-soft with a clock glyph and leaves the fields unmarked, a credential failure is bad-soft with an alert glyph and marks both fields aria-invalid. Different cause, different treatment, the word always carries the meaning.
- Every message names the cause and the way out on a second line: the 401 adds "Forgotten it? An admin can reset your password" (true, POST /users/:id/password is admin-only per users.controller.ts:79), the 429 adds "Sign-in locks after five failed tries in fifteen minutes", the network error adds "Your details were not sent anywhere". No error is a dead end.
- Message text is --cm-ink on the tinted fill, not --cm-muted: muted measures 4.25:1 on bad-soft and fails the 4.5:1 floor. Hierarchy inside the message comes from size (13 then 12), never from a lighter grey. The disabled submit sits at 0.85 opacity, not the reflexive 0.5, because anything dimmer drops white-on-accent under 4.5:1 (0.85 measures 4.65:1) and the "Signing in…" label already carries the state.
- Field chrome is the token sheet's one spec: surface, 1px line-strong, radius 4, padding 10/12, accent hover border, 2px accent-ink focus ring at 2px offset, 11px small-caps labels. Inputs are 41px, the button 40px, the reveal 36px, so nothing is under the 36px house floor. All motion is 140ms ease-out on colour only and is switched off under prefers-reduced-motion.
- Light theme only, self-contained: tokens copied inline from apps/web/src/styles/tokens.css, no prefers-color-scheme block, no data-theme override, no traffic-light bar of any kind, and the one media query cites --cm-bp-shell in a comment rather than inventing a breakpoint.

### Demo

- The screen is the top card only. Everything below the hairline (the "Every state this screen can be in" strip, the five labelled state cards, the notes above each and the unreachable-state list at the foot) is review scaffolding; in the product one state is on screen at a time and it comes from the sign-in attempt.
- Sample values only: ops@silveroak.ae, the masked password, and the visible "Marina2026!" in the signing-in card, which is shown in the clear purely to demonstrate the reveal toggle in its on state.
- Nothing is wired. The reveal toggle, the submit and the fields have native hover and focus behaviour but no script; the states are frozen with classes and attributes.
- The 401, 429 and network sentences are the strings the product already returns; the second, italic-free "fix" line under each is new copy that does not exist in Login.tsx yet.

### Forks

- [ ] Login renders as a standalone centred card, not inside the sidebar-and-top-bar shell the rest of the sweep uses. This follows the shell-basics dossier recommendation (a login screen is not the appshell and is not held to appshell IA), but that fork was never ruled on, and the round-3 brief asks every screen to sit in the shell. Keep the standalone card, or fold Login into the shell chrome? **Recommend: keep it standalone.**
- [ ] The reserved error slot is gone. Login.tsx holds 52px open under the fields so the button cannot jump on a failed sign-in, and the dossier praised that, but every real message here runs to three lines, so an honest reserve is either too short to stop the jump or a visible 60px hole in the default screen. Round 3 reserves nothing and lets the centred card grow. Accept the small shift, or put the reserve back and live with the void? **Recommend: accept the shift.**

---

## knowledge-doc

`knowledge-doc.html`

### Binding

- ONE door per document. The lightweight side-panel editor that could only see title/content/channels is retired: the row menu is Read it / Edit / Delete, and Edit always lands on the full page whatever the kind. This kills the dossier's blocking bug at the root (Edit showing an empty box for a structured document that visibly has content) instead of guarding it, and obeys "nothing introduces a second way to do a job that already has one". The row click itself opens the read panel and never mutates.
- The editor is ONE bordered surface with four hairline-separated NUMBERED parts (1 Kind of record, 2 The facts, 3 Where it is used, 4 What the bot gets), copying ManyChat's numbered wizard verbatim (EVIDENCE #10, "1 Post or Reel / 2 Trigger"). Numbering is earned here because this genuinely is a sequence; it is not an eyebrow on every section. Four parts, one border, not four cards.
- Part 4 rebuilds live as you type, so the operator never saves to find out what the bot got. Every field carries its own "1 fact" / "no fact yet" marker in the label row, and the read panel shows the same generated facts. This is the answer to the operator's round-2 complaint that a destination did not communicate what you would see when you clicked it: the destination IS the content.
- The library row gains a one-line summary under the name, rendered as inline chips (kind pill, fact count, updated date), copying ManyChat's trigger-summary-under-the-flow-name (EVIDENCE #6) so you know what is inside a document without opening it. Row shape, chips, hairlines and the dots menu are otherwise the scope-13 look, untouched. Not-in-use is a word pill plus colour, never colour alone.
- Read before edit (EVIDENCE #5, metrics-before-canvas): a row click opens the 440px right-hand side panel over a scrim showing the document's status, channels and every generated fact; editing is one click further behind Edit. Escape, the scrim and the close button all dismiss, focus moves in on open and returns to the invoker, and the page's own header, action row and tabs stay on screen.
- All six states ship: success (save toast that states how many facts the bot now reads, and says so honestly when the document is not in use), partial (unticking "The bot can quote this document" turns on a warn strip and rewrites part 4's hint), and loading / error / first-run / offline drawn in a clearly banded review strip. Empty copy states what is missing and its action echoes its title; the library empty and the no-facts empty are different screens.
- Validation prevents rather than repairs: Save is disabled with a reason while a required field is blank, error text sits with its field in a height-reserved slot so nothing jumps, changing an existing document's kind is behind a Change kind button with a written warning, Delete opens an in-place confirm tray naming the document and its live fact count, and Ctrl/Cmd+S saves from anywhere. Right-aligned action row in the page head, deliberately not sticky (WCAG 2.4.11).
- On-token end to end and verified: every hex in the file is a tokens.css value (the one derived shade is color-mix on --cm-bad), light theme only with no prefers-color-scheme block, zero em dashes, 36px minimum hit targets, 2px accent-ink focus rings, tabular numerals on every count, fields capped at a 74ch measure, and the fee row's four-column grid collapses by container query (verified at a 400px container, no overflow). No traffic-light bars anywhere.

### Demo

- All five documents are invented but shaped like the real endpoint returns (Dubai Marina area guide, Buying off plan process, Costs when buying fee table, Golden visa FAQ, handover delays free text). The fact strings are generated by the mock using the same cut the API makes, so counts and copy stay internally consistent (23 facts across 5 documents).
- Both routes are stacked on one canvas under dashed review bands so the door and its destination can be reviewed together. In the product these are two URLs; only the shell renders once.
- Live in the file: open any row menu, Read it opens the panel, Edit loads that document into the editor below, type in any field and part 4 plus the row summary rebuild, add or remove a repeatable row, tick the use box, Change kind, Delete, and Ctrl+S. Save is a 620ms fake round trip.
- The bottom states strip, the shell's user identity, the Activity badge of 3 and the admin pill are review scaffolding and sample shell state. The sidebar brand mark is a text stand-in for design/assets/logo-mark.png. Below 768px the collapsed shell is written but was not visually verified; the shared browser would not resize.

### Forks

- [ ] Retiring the lightweight EditPanel is a real behaviour change, not just chrome. It fixes the silent empty-box bug and gives one job one door, but editing a one-paragraph note now costs a page navigation instead of a side panel. The read panel still opens on a row click, so reading stays cheap. **Recommend: retire it, single door.** Alternative the dossier suggested: keep the panel for free-text kinds only and route structured kinds to the page, which leaves two editors alive.
- [ ] Delete confirms and offers no undo, because the API has no restore. CONVENTIONS 1.4 says ship undo anyway and reserve confirms for the irreversible, so today the screen is compliant only because delete really is irreversible. **Recommend: keep confirm-only for now** and open a work item for a soft delete with a restore window, rather than pretending an undo the backend cannot honour.

---

## flows-canvas

`flows-canvas.html`

### Binding

- Connector hover is the ManyChat three-part combo, verbatim in our tokens: the wire turns --cm-info, the destination step gets a 2px --cm-info outline, and a --cm-bad trash button fades in at the curve's midpoint. Wires are measured off the DOM (offsetLeft/offsetTop, so zoom never desyncs them) and each delete control is a real button that sits in the tab sequence and paints the same three-part highlight on focus, so nothing is hover-only (WCAG 2.1.1/2.4.7). Removing a link is undoable, so it offers Undo in a toast rather than a confirm.
- The message composer's floating toolbar appears only when the text box has focus and is anchored to the caret, measured with a hidden mirror of the field: Improve, emoji, {} contact detail, and a live tabular character count against the real 4096 limit. Emoji and contact details insert at the caret; the count turns --cm-bad past the cap; Escape closes a popover back to the field, Escape again closes the panel.
- A branch label is carried on the DOWNSTREAM step, not only on the wire: an --cm-info-soft strip above the node header holding the port's own word (Yes / If not / If no reply) plus the step it came from, and the same label repeats in the steps rail so a branch is readable without scrolling to it. The words are the engine's real port labels from flow-graph.ts, never invented ones.
- Add-step is ONE surface: a 44px accent FAB pinned to the canvas viewport (not the world, so panning never loses it), opening a categorized palette in ManyChat's order, Starting step then Content then AI then Logic then Extras. Every port row and the / key open the same panel with its title scoped to that port. Round-1's separate drag-from palette rail is deleted: two ways to add a step was two patterns for one job.
- A live flow opens on Key metrics, not the canvas: the real getFlowStats totals as four hairline-joined tiles in one bordered container, a per-step Reached/Errors table with scope=col headers and right-aligned tabular numerals, and the real published banner with Edit one click away. It carries the real STATS_NOTE, so no delivery, open or click number is invented.
- The flows list carries what fires each flow as chips under the name: channel swatch, the match verb in words, then individually removable keyword chips, with a +N more overflow. The separate Trigger column is gone (the line replaced it) and the State column keeps only the CONTROL that changes state, because the read-out is already the pill beside the name. A flow with nothing to fire it says so in --cm-warn text.
- The minimap round 1 shipped is replaced by a steps rail plus Fit, per CONVENTIONS 2 (Figma ships no minimap; zoom-to-fit plus a node list is the cheaper answer). The rail is a keyboard-reachable node list that selects and scrolls to a step; zoom is a real transform with a tabular percentage readout, stacked under the FAB.
- Step settings open in the product's existing right-side 400px panel inside the stage, not a scrim takeover, so the selected step keeps its 2px accent ring and stays visible while it is edited. All six states ship: canvas, first-run empty (title states what is missing, action label echoes it), skeleton loading with the controls visibly disabled, the real validation-problems list, the refused-save banner with its recovery actions, and an offline notice. The problems list names the step by its title; today's message interpolates the node id, which is engineering vocabulary.

### Demo

- Interactive, not a still: hover any connector for the three-part highlight, click the welcome Message step to open the panel with the caret toolbar, press the blue plus or / for the palette, type in the field to watch the counter and the autosave line move, delete a link and undo it from the toast.
- Content is representative, shaped like the real endpoints but not live records: flow names, keyword chips, the 342/268/6 totals and the per-step Reached/Errors figures. Only the Message step's panel is wired; other steps select and say so.
- The dark REVIEW HARNESS strip and its six view buttons are mockup scaffolding, not screen furniture. In the product the state comes from the route and the server.
- Card dragging, auto-arrange, publish and the trigger wizard are not wired here. The sub-768px fallback is written but could not be visually verified (the shared browser would not resize).

### Forks

- [ ] The floating toolbar copies all four ManyChat controls, but two have no backing today: there is no rewrite endpoint behind Improve, and message text is not interpolated at send time (verified: only CRM notes and webhook bodies run {{field}} through flow-crm.service.ts). Ship the toolbar whole and build those two small halves, or ship only emoji plus the counter now and add the other two later? **Recommend: ship it whole**, the toolbar is the point and both halves are days not weeks.
- [ ] Round 1's minimap is dropped for a steps rail plus Fit, on CONVENTIONS 2's verified-absence note. That is a deliberate change to the round-1 floor, so it needs an explicit yes: keep the steps rail, or put the minimap back alongside it? **Recommend: the steps rail**, it is keyboard-reachable and a minimap is not.

---

## settings

`settings.html`

### Binding

- Settings decomposes into a shell plus ten section PAGES, per the dossier's fork-1 recommendation (routes/settings/, one file per section). The shell is a 216px section nav on the page canvas holding the four real TAB_GROUPS (Platform / Contacts / Connections / Access) and nothing else; each entry is a real link with its own address, so the `role="tablist"` with no roving-tabindex that the dossier flagged is removed rather than patched. The page H1 becomes the section name ("Connections"), the top-bar crumb reads "Settings · Connections", and the old PageHeader eyebrow "Administration" is dropped (round 2 killed stacked uppercase eyebrows).
- The section nav copies ManyChat evidence 9 literally: ONE row primitive of label plus a right-aligned count, reused across every section that owns a list (Tags 12, Custom fields 6, Connections 7, Users 4). A section that is not a list carries no count at all rather than a fake zero, so the count means the same thing in every row.
- Connections is one bordered surface of hairline-separated record rows, not seven bordered cards: the scope-13 Knowledge row look the operator restored on 2026-07-28 (situations.css .cm-sit__kbrow) applied verbatim, with the add action in a foot row under the list. A row is name plus channel chips, the catalog's plain-word description, then a meta line (label, updated, and for ManyChat the keyword-list readout). Whole-row click opens the record in the right side panel over a scrim at min(440px,100%) per the token sheet; the row never mutates anything.
- Health is never claimed, only read. The list shows the stored on/off state and says so in its head ("Open one to read its live health. Nothing here is a saved claim about health."); token validity, subscribed fields, callback URL, last event received, 24-hour signature failures and processing errors, and the Facebook feed breakdown all live in the panel behind a live Re-check with its own checked-at time. Connections with no webhook get a Test with an honest "no webhook to read" line instead.
- The panel and the list are both rendered from one transcribed copy of the seeded presentation catalog (packages/shared/src/connections/presentation.ts): display name, powers, description, settings and secret field schemas, healthKind, testable, rotationNote. Adding a connector is a catalog entry, never a new component, exactly as the product is built. Every panel field is a real `<label for>` bound to its input with the field chrome from tokens.md (1px line-strong, radius 4, padding 10/12, accent hover border, 2px accent-ink focus ring at offset 2), closing the dossier's "zero htmlFor in 2974 lines" finding and its fork 2.
- The unsaved-changes guard survives the decomposition and is wired: editing a credential enables Save and flips the foot note to "Edited, not saved"; Escape, the close button, the scrim and Cancel all route through the guard, which raises the three-choice dialog (Save changes / Discard / Cancel) with the app's real copy. Escape closes the dialog first, then the panel; focus returns to the row that opened it.
- All six states ship and are honest: skeleton rows for loading, a persistent error banner naming the timeout and stating nothing was changed, a first-run empty using the real copy ("No connections yet" then "Add connection", the label echoing the title), a partial that renders a stored provider missing from the catalog as a visible disabled row instead of dropping it silently, and an offline state that timestamps the stale list and disables the controls that need the server. Filtered-to-zero is unreachable here: seven rows need no filter, so no filter exists.
- Contrast is measured, not assumed: 12px muted meta reads 4.40:1 on the hover fill and 4.26:1 on accent-soft, so the meta line steps to ink-2 whenever a row is hovered or selected; ok green at 11px reads 4.43:1 on accent-soft, so on a selected row the status word goes to ink while the dot keeps carrying the colour. Status is always a word plus a colour, the n8n row carries the plain-word caution that turning it off stops campaign sends, reminders and delayed follow-ups, and every hit target clears 36px.

### Demo

- Click the state segmented control (dashed border, review harness only) to swap the list through Loaded / Loading / Error / First run / Partial / Offline. In the app this comes from the server, never a control.
- Click any row to open its panel, rendered from that connection's catalog entry: WhatsApp and Social pages get the live webhook readout with a working Re-check, ManyChat gets the keyword-list block, the rest get Test connection. Type in any credential field to arm Save, then press Escape or click the scrim to raise the real unsaved-changes dialog. The Active switch in the panel writes straight back to the row.
- Values are shaped like the real returns but invented: the seven labels and updated timestamps, the token expiry, subscribed fields, callback URLs, the 0 failure counters, the feed breakdown, ManyChat's 42 keywords read 2 hours ago, and the nav counts. Two rows are frozen for a single still (WhatsApp in hover, n8n in keyboard focus); in the app only one row is ever in either.
- Not verified in a browser: this environment's screenshot bridge is broken (CDP clip.scale error), so geometry, overflow, the six states and every interaction were checked by measuring a live 1440/1180/900/430 render (no horizontal overflow at any width, shell collapsing below 768, panel body scrolling on its own) but no pixels were seen. The edit surface is drawn as a side panel only; no Add-connection or Models screen is in this file.

### Forks

- [ ] The brief said "the metadata cards", and this mock deliberately renders them as rows in one bordered container with the detail in the right side panel, replacing the seven stacked cards and the 680px edit Modal. Reason: the operator restored exactly this row look for scope-13 Knowledge on 2026-07-28 and rejected the five-card analytics landing in round 2, and CONVENTIONS says default to side-by-side context over a takeover. Surfacing rather than silently reconciling: keep rows plus side panel, or go back to cards plus modal? **Recommend rows plus side panel.**
- [ ] The seeded connection copy in presentation.ts carries em dashes (banned in prose by tokens.md) and one internal term: the email connection is described as alerts for the "red-tier", which is the dossier's fork 3 vocabulary. The mock rewrites both, for example "Sends the urgent alerts and the daily digest by email" and "Rotate the access token when sends start being refused" for "returning 401". Credential-field help is kept close to verbatim because it is setup instruction the operator copies from a developer console. Confirm the copy edits land in the seeded catalog, or keep the current strings?

---

## home

`home.html`

### Binding

- Channels stop being a 2-up card grid. One bordered surface, one hairline-separated row per channel: dot + name + channel chip + handle on line 1, ONE state pill plus a plain state sentence on line 2, gate chips as a labelled sub-line, and three right-aligned tabular columns (last inbound / last outbound / errors 24h) on a shared numeric spine under a small-caps header row. Same rows-in-one-container primitive the operator approved for the scope-13 Knowledge rows, and the same shape as ManyChat's flow row (name with a compact summary under it, EVIDENCE 6).
- The standing WhatsApp status is the row's state and it reads in info tone, per ORD-078: pill "Number disabled by Meta, alerts muted" plus the sentence "Silent since 3d 2h ago. Nothing arrives until Meta re-enables the number, so silence alerts stay off for this channel." The row dot matches the pill (info, not red) so the row reads known-and-parked rather than on-fire. This is a deliberate change from channelDot() in Home.tsx, which returns bad for an inactive connection.
- Worker, database and Redis health are a foot line INSIDE the health surface, never a stat tile: dot + word + tabular age ("Background jobs running, heartbeat 14s ago", "Database reachable", "Redis reachable") with the shared "checked" readout right-aligned. When background jobs die the foot line goes bad AND an attention row appears: the row is what you act on, the foot line is what you glance at.
- The page head is a status line under the title, not a stat row (ManyChat home, EVIDENCE section 1): "1 channel receiving, 1 disabled by Meta · 6 items need you · checked 30s ago". The item count is an in-page link to the attention surface, and "Check again" states what it is doing while it works, then updates both "checked" readouts so they never disagree.
- Dismiss is optimistic with undo, never a confirm (CONVENTIONS 1.4): the row goes at once and an accent-soft status line takes its place carrying Undo for 8 seconds; undo restores the row in its original position. "Dismiss all alerts" exists only at two or more dismissible alerts and its notice is correctly singular or plural. System-derived attention rows carry "go fix" only, never a dismiss.
- Today and Agent performance become the token sheet's actual Stat tile spec: 24px tabular figures with an 11px small-caps label under them, tiles joined by hairlines inside ONE bordered container. That removes two off-spec details in the shipped home.css: six separately bordered stat boxes, and the 3px border-left accent on the performance cells, which is the banned side-stripe border.
- Zone headings are sentence case 13px/600 with a muted count; small caps is reserved for real data labels (column headers, stat labels, "Agent gates", "Quality and tier"), per the round-2 activity ruling. No traffic-light bar, no split bar and no quota meter anywhere: quota is the sentence "0 of 1,000 business-initiated today" in tabular figures.
- All six states ship in the file: default; loading as row-shaped skeletons inside the same surface, never a spinner; error as a full-width persistent banner that names the timeout, says nothing was changed and carries "Try again"; empty as "All clear"; partial as "No events yet" with em dashes in the cells (the only use of that glyph); and the degraded-worker case. Every action target is at least 36px, nothing is hover-only, focus is a 2px accent-ink ring at 2px offset, motion is 140ms ease-out and is suppressed under prefers-reduced-motion.

### Demo

- Every figure is invented but internally consistent and shaped like the real GET /api/home payload. The gate values are the live ones from HANDOFF (Instagram comments shadow, moderation live, DMs off, comment-to-DM off), and the WhatsApp standing status is the real seeded alerting-config label.
- Three behaviours are wired: dismiss with undo, dismiss all with undo, and the "Check again" feedback. Everything else is native hover, focus and anchors. The all-clear branch is unreachable by clicking because the four system rows are not dismissible, which is correct; it is drawn in the appendix instead.
- The "same screen in its other states" block at the foot is a review device, not screen furniture. In the product only one of those states is ever on screen.
- The sidebar brand mark is a text stand-in for design/assets/logo-mark.png so the file opens standalone. The shared browser would not resize, so 860px and 380px were verified through a temporary iframe harness (reading order, no horizontal overflow) which was then deleted.

### Forks

- [ ] The phase-0 dossier verdict for Home was "keep, no structural issue", and this mock still converts the two channel cards into rows in one bordered surface. Surfacing rather than reconciling: keep the rows (**recommended**, it is the primitive the operator approved for Knowledge and it kills the 2-up card grid), or restore the channel cards?
- [ ] Round-6b locked "DB/Redis health surfaces only as attention rows, never a standing tile". This mock gives background jobs, database and Redis a standing foot LINE on the health surface (a line, not a tile), so a dead worker is visible before it becomes an alert, which is what "worker health honestly" asks for. **Recommend: keep the foot line.** Confirm, or revert to attention-rows-only.

---

## How to react

Tick a fork's checkbox to accept the recommendation as written. Leave it
unchecked and add a line underneath to override, e.g.:

```
- [ ] Keep the rail, or restore the tab strip? Recommend: the rail.
  → OVERRIDE: restore the tab strip, ops team still thinks in tabs.
```
