Jev call copilot CurrencyTransfer research and architecture built 2026-09-25

Mockups

Click any image for the full-size render (1600 px wide). The design board shows the same images side by side.

D: live view (recommended, from your 25 September review)

D: review view

D: cadence sheet

A: Reference (earlier round)

B: Hero band (earlier round)

C: Transcript spine (earlier round)

States sheet

CT Copilot dashboard: design system

The spec every dashboard story (COPILOT-045 onwards) builds from. The dashboard has two screens. The live view is what a rep sees during a call: it is design G, "One list", approved on 2026-09-25, and its mockup in docs/design/shotgun/G/ is the literal layout spec. The review view is the audit ledger for replay and curation: it is variant D's review view (variant C's layout in plain English). Token values are literal, so lift them, never approximate them.

Two views, two jobs. The live view answers four questions at a glance: where are we in this call, what have we covered, what is worth saying right now, and what is still to come. It shows one list, the call plan, next to the transcript; it spends its one accent on "worth saying"; it carries no number, threshold, rule or confidence. The review view is an audit ledger: dense and quiet, warm paper in light mode and graphite in dark mode, every claim linked back to a transcript row (#i), every number monospaced with tabular figures. The live view is for the minute the rep is in; the review view is for checking the engine after the call. The same decision stream and the same call-plan object feed both.

Design constraints (non-negotiable)

  1. No probability, anywhere (INV-COPILOT-012). Do not build a closing-probability or activation-likelihood number, bar, gauge, chart or percentage hero. The onboarding hero is a count (x / y must-say items said) plus latched risk flags. The CS hero is a state (none → reported → owned → resolved) plus a health value on a 0 to 1 scale, shown as 0.64, never as 64%. The completeness tally is discrete: one cell per applicable item, coloured by that item's state. It is never a continuous fill. Only the talk-share bar uses %, because it is a word ratio computed in code. No DOM text matches /probability|likelihood/i.
  2. Status is never colour-only. Must-say items render icon + text label for all four states: filled check + "said", half-filled circle + "uncertain" (with its value), empty ring + "unsaid", dashed ring with a dash + "n/a" (with the reason). Signals use lamp shape as well as colour: filled = lit, dashed = uncertain, hollow = off, dotted = unknown (null, shown as "no answer", never 0.00). Risk uses a triangle icon + the word.
  3. Risk flags carry the offending utterance index (#38) and the value (0.71), plus the pseudonymised quote. Flags never auto-clear. A false-positive flag stays in the list, greyed and struck through, with "flagged as false positive".
  4. The next-best-move card shows title, what, and ≤ 3 approved lines, exactly one of them prefixed "say:". It also shows rule: <move> hidden because <reason> for suppressed[0], and "listening… (leaning X)" when gated. Every rep-facing string goes through renderRepText (approved line or verified rewrite). The UI cannot render anything else.
  5. Telemetry shows the model id with a drift warning when it ≠ model_expected, latency mean / p95, tokens (last in / out and total), budget degradation (questions dropped), cache hits, and cumulative cost at $0.042/M input.
  6. Keyboard: transcript rows are focusable (tabindex=0, aria-selected). Shift+↑/↓ extends the selection from the focused row, m opens the mark form, Esc clears. The focus ring is always visible: 2px --focus.
  7. No dead controls. Every button in the mockups maps to a protocol message or a route (see §Panels). Anything not yet wired renders disabled with microcopy that says why. There is no theme toggle, because the theme follows the OS (prefers-color-scheme).
  8. Solid colours only. No alpha in any surface token, so a panel never shows what is behind it in dark mode. The only translucency is the dialog scrim and shadow.

How the constraints apply to the live view (G) and the review view (D)

The constraints above were written for the audit ledger and hold in the review view as stated. In the live view:

Live view (G, "One list")

What a rep sees during a call. The same screen replays an uploaded or historical call (M1) and runs on live audio in M3 (the call audio and the rep's microphone). The literal layout spec is docs/design/shotgun/G/live.html with live.css; data.js holds the two drawn scenarios (onboarding mid-call with a concern open; customer success after the issue is fixed) and doubles as the fixture shape for the web tests; NOTES.md has the rationale (a), the data contract (b, folded into §Call plan contract) and the cadence rules (c, below). G's tokens (§Tokens, "Live view tokens (G)") replace D's tokens for the live view; the review view keeps §Tokens "Review view tokens (C and D)". Where this section and the G render differ, this section wins: the additions are the one-tag rule, the purpose control, the chips rules, the rate-pressure item and the replay top bar.

Principle: one list, one reading focus. The plan and the suggestions are one object, so nothing floats: the current item is a card that sits in its place in the list, between what is done and what is still to come. Where the call is, is answered by position, not by a stage label. The only large text on the screen after the item's title is the current item's 2 or 3 points.

Layout

Top bar

Left to right with 20px gaps, 24px side padding and a 1px --line rule under it: the brand "CT Copilot" (Bricolage Grotesque 700, 16px); the state pill; the call label in --ink 600 followed by the call kind in --ink-2 ("Onboarding call", "Customer success call"); a spacer; the view link; the clock (DM Mono 15px, --ink-2).

Transcript pane

Head: "Transcript" (Bricolage Grotesque 600, 20px) and "Newest at the bottom" (13px, --ink-3). Body: a --surface box with a 1px --line border and 10px radius that scrolls; its top edge fades into the surface (a sticky 24px gradient) so the first visible line reads as a live scroll rather than a cut. Auto-scroll keeps the newest line in view unless the rep has scrolled up.

Plan pane

Top to bottom:

  1. Head row: "Plan for this call" (Bricolage Grotesque 600, 20px) and, right-aligned, the tally in words, 13px --ink-3: "4 done, 5 to come, 1 not needed" ("to come" counts next and later items).
  2. Purpose line: the call purpose's approved label, 14px --ink-2 ("First call after sign-up"). It is a quiet button. Clicking it opens an inline list of this scenario's purposes under the heading "Change the purpose of this call"; choosing one sends set_purpose{purpose_id}, and the label is then followed by "Set by you" in --ink-3. Before a purpose is known the line is empty, never "unknown". This is the one control G adds to the render.
  3. Client facts chips (§Client facts chips).
  4. The list, which scrolls: items in plan order, 3px apart, then the "Not needed on this call" group at the foot.

Plan item rows

Every row is a button (aria-expanded) with a 22px mark, the title, and the state word right after the title (a 12px column gap plus 4px padding, so title and state read as one line; never pushed to the far edge). Row padding 5px 14px 5px 12px, radius 10px, hover fill --surface-2.

State Mark Title State word Notes
done tick in --accent 15.5px 500 --ink-2 "Done 02:58"; a client item reads "Client raised 00:03, done 03:47" the times are links
current 10px --accent dot the open card (§The current item) "Now"; a client item reads "Raised by the client 03:55" the only open thing on screen
next 8px hollow ring, --ink-4 15.5px 500 --ink "Next", --ink-2 600 one item, whenever anything is still to come
later 8px hollow ring, --ink-4 15.5px 400 --ink-3 "To come"; a client item reads "Client asked 01:12, to come"
not needed 8px dash, --ink-4 15.5px 400 --ink-3 its reason as a sentence ("The amount is well above the minimum") only in the foot group

State words are 12.5px --ink-3; their times are DM Mono 12px with a dotted --ink-4 underline, and clicking a time jumps to that line in the transcript and flashes it. Clicking a done or later row expands it: its hint (13px --ink-3) and its points, each with "Covered 03:44" when covered (14px --ink-2, rows ruled by --line-2). Marks are CSS shapes: no icon font, no hand-drawn SVG.

The current item

The one open card, in its place in the list: --surface fill, a 1px --accent-line border, --shadow, 10px radius and 4px margin above and below.

Client-added items

Not needed on this call

Template items that do not apply to this call sit in a dimmed group at the foot of the list, after a 1px --line rule and the label "Not needed on this call" (12px 600 uppercase at +0.06em, --ink-3, left padding 46px), each with its plain reason ("No document was asked for"). The sequence above is never interrupted by something that will not happen. An item that starts to apply again moves back up as "To come".

Client facts chips

The chips row sits in the plan head, under the purpose line and above the list, exactly as in G. The chips are the engine's client facts: a closed set of slots from the policy (§Call plan contract, client fact values), never free text, so the row stays bounded on a long call.

Caution

A caution appears in the transcript under the rep turn it refers to, within one decision of that turn: a --surface-2 box, radius 5px, padding 6px 10px 6px 8px, with a 16px ring icon holding "!" in --ink-2, then a single "Careful" tag, then the approved caution note in 13px 500 --ink ("That sounded like a rate prediction."). The note never repeats "Careful" or any other prefix; the tag says it once. Neutral ink, never a second accent colour, never a box in the plan.

It is timed from the turn, not from page load or message arrival: the element carries data-shown-at (the turn's time) and --elapsed (the clock minus the turn's time); it fades out between 12 s and 15 s after the turn (fade-away 3s delayed by 12s - elapsed), and under prefers-reduced-motion it hides at once at 15s - elapsed. A second caution replaces the first; they never stack. The clock is the call clock (the player in a replay, the call time in M3), so after a seek or in a replay the caution leaves at the same moment it would on a live page. The flag stays in the review view for good.

Listening and hold

"Listening" (an 8px --ink-4 dot pulsing, 13px --ink-3) shows at the transcript foot whenever Jev has nothing new: before the first decision, while no item is current, and while decisions are failing or skipped. Nothing blanks: an unknown decision (error, timeout, budget skipped, credits empty, catching up) keeps the plan and the chips as they are and never shows zeros.

Cadence rules

The engine decides what is true; the presenter in the page (web/src/presenter.ts, COPILOT-093) decides when the rep sees it. The engine's own rules run first (a challenger item must lead for two decisions in a row; at most one change of the current item per decision; §Call plan contract). These are G's rules as implemented (shotgun/G/NOTES.md (c)):

  1. Items change state only after a client turn ends (a client utterance's t_end; backchannels and acknowledgements excluded). The current card never moves while the rep is talking. A plan decided on a client turn is applied as it arrives (that turn has ended); a plan decided on a rep turn waits for the next client turn to end; after the call's last turn any pending plan is applied.
  2. The current card stays at least 20 s once opened; a challenger must lead for two decisions in a row before the card switches (the engine's hysteresis, reused). A client concern or rate-pressure item is inserted at the next change moment without waiting out the 20 s.
  3. A point ticks the moment the rep covers it (feedback on what the rep just did, applied as soon as that rep turn's decision arrives), turns from the tint to the grey covered line with "Covered just now, 04:44", and stays about 5 s after the next client turn ends; then it folds into the "Also covered at 08:16" line.
  4. At most one new point appears per decision; a new point settles in with a 0.5 s fade (settle: opacity 0 to 1 with a 3px rise), never a flash.
  5. A client concern is inserted as the current card at the current position on the client turn that raised it; the item it interrupted is "Next" and reopens when the concern collapses to done.
  6. A caution appears under its rep turn within one decision, in neutral ink, and fades between 12 s and 15 s after that turn, then is gone; a second caution never stacks on the first.
  7. "Listening" shows at the transcript foot whenever Jev has nothing new; nothing blanks, and the card is held on a failed decision.
  8. An updated chip shows "updated 04:12" for about 10 s of call time, then reads plainly.
  9. Under prefers-reduced-motion the fades collapse to instant changes and the pulses stop; the caution still leaves at 15 s.
Constant Value
LIVE_MIN_DWELL 20 s of call time
LIVE_COVERED_LINGER 5 s after the next client turn ends
LIVE_CAUTION_FADE_START / LIVE_CAUTION_GONE 12 s / 15 s after the turn
LIVE_CHIP_UPDATED 10 s
Point settle / pulse 0.5 s / 2.4 s

How it is built. The presenter is a pure function over the last presented state, the latest decision.plan with its decision index and unknown flag, the call clock and the utterances (speaker, kind, t_end from GET /api/calls/:id); the page runs it on every decision and every clock tick. It keeps the engine's latest plan as pending and applies it at the next change moment that satisfies the dwell; ticks and cautions are applied as they arrive and are never dropped. The same presenter drives a replay, where the clock is the player, and a seek resets it to the decision at the new clock. There are no timers in the engine or the Durable Object (INV-COPILOT-009).

Type, colour and motion

Removed from the live view (kept in the review view)

The signals grid; the stage strip and stage history; the separate concern panel; the next-best-move card as a panel of its own; the must-say checklist panel (its items are plan items now); the risk-flag box; every confidence number, threshold and legend; telemetry; Acknowledge and Dismiss; "shown since #i"; "rule: X hidden because Y"; #i references; the share-of-words bar; "say:".

Call plan contract

One object, the call plan, replaces the card, the must-say checklist and the stage on the live view. The engine builds it in step() (pure, INV-COPILOT-001) as Snapshot.plan; the Durable Object sends it in every decision (decision.d.plan); the live view renders it through the presenter; the review view's plan panel reads the same object. Stories: COPILOT-079 to COPILOT-091 (policy, engine, wire), COPILOT-092 (rate-pressure eval cases), COPILOT-093 and COPILOT-094 (presenter, chips), COPILOT-051 (the plan list) and COPILOT-049 (the review view's plan panel).

Shape

interface CallPlan {
  decision_id: string | null;     // `session_id:i`, set by the Durable Object on the wire (null inside the engine); binds a rewrite to the plan it was made for
  purpose: { id: string; label: RepFacingText; source: 'jev' | 'override'; at: Evidence } | null; // null until a purpose locks
  facts: FactChip[];              // filled slots only, in display order (group, importance, recency)
  items: PlanItem[];              // this call's plan, in order
  caution: Caution | null;        // the newest undismissed risk flag that has a caution note
}
interface Evidence { i: number; t: number }   // utterance index and its t_end in call seconds: the time link
interface PlanItem {
  id: string;                     // template item id, `concern:<type>:<start_i>`, `rate:<start_i>` or `ask:<move id>` (one id per concern episode)
  title: RepFacingText;           // template title, concern headline, "Rate pressure" or a move headline
  headline: RepFacingText | null; // rate pressure only: the situation's headline, the client's words ("Company X gives me a better rate.")
  hint: RepFacingText | null;     // why the item is on this plan; for rate pressure, the situation's hint
  state: 'done' | 'current' | 'next' | 'later' | 'not_needed';
  origin: 'plan' | 'client';      // 'client' = raised by the client (concern, rate pressure, client ask)
  raised_at: Evidence | null;     // the client turn that added it
  done_at: Evidence | null;       // the turn that met the last done condition, rep or client
  not_needed_reason: RepFacingText | null;
  ask_first: PlanPoint[];         // rate pressure only: qualify_the_quote's points when the situation has qualify_first, under one "Ask first" tag
  points: PlanPoint[];            // approved lines, under one tag: "Say" on a rate-pressure item, "Worth saying" elsewhere
}
interface PlanPoint { line: RepFacingText; covered_at: Evidence | null; fresh: boolean } // fresh = covered on this decision
interface FactChip {
  slot: string;
  group: 'client' | 'transfer' | 'how_they_pay' | 'status';
  label: RepFacingText;           // approved value label, or an approved template with code-formatted params
  at: Evidence;                   // the turn that established the value
  updated_at: Evidence | null;    // the turn that last replaced it
}
interface Caution { risk_id: string; note: RepFacingText; at: Evidence }

G's mockup data calls later "upcoming" and marks client items with origin: 'client'. Raised by the client is not a state of its own, because G shows client items as current ("Raised by the client 03:55"), later ("Client asked 01:12, to come") and done ("Client raised 00:03, done 03:47"). The tally and the words "Now", "Next" and "To come" are derived by the view.

RepFacingText gains one variant, for chips only: { kind: 'approved_template'; text_id; policy_version; params: Record<string, string> }, an approved template whose params are typed and formatted by code: an amount as digits with thousands separators (/^\d{1,3}(,\d{3})*$/), a currency as an ISO 4217 code from the policy's currency list; nothing else. Each param is checked when the chip is built and again in renderRepText. Everything else stays approved or a verified rewrite (INV-COPILOT-003).

Definitions (one each)

Plan templates (policy data)

policy/src/plan-<scenario>.json (plan-onboarding.json, plan-customer-success.json), built into the bundle as Policy.plan and hashed under weights_hash: no Jev question depends on it, so an edit is absorbed by recompute with zero Jev calls. It holds:

Template content decisions from Stevan's review (2026-09-25):

Seed templates (drafts for the red-pen; G's data.js draws both):

Call purpose

One Jev Choice per call over a closed taxonomy per scenario, asked on the early decision points until it locks:

The Choice lives in the scenario bank, so its answers are cached under bank_hash. It locks on the first answer at or above purpose_min_conf (with its confidence and evidence), never changes from Jev after that, and stops being asked once locked or after purpose_max_decisions decisions. A manual override (set_purpose, from the purpose control or the review view) replaces it, is audited, survives reconnects and seeks, and is never replaced by Jev.

Client fact values (extraction contract)

Rate pressure

From Stevan's rate-objection playbook (docs/source/ct-rate-objection.txt), encoded on 2026-09-25 as described in docs/policy/rate-objections.md (the id map): never defend the rate; change what is being compared.

Per-point said detection

One noul per visible point: covered::<text_id> ("In the rep's turns in recent_transcript, did the rep make this point, in any wording?" with the approved line), generated from the playbook (flat lines and situation points alike) like phrasing:: and cached with the move answers under playbook_hash. It is asked only on rep-turn decisions, for the uncovered points of the current item (the "Ask first" questions first) and, for the 4 decisions after a switch, of the item the view may still be showing during its dwell; at most 3 per request, least recently asked first, so a point left out on one turn is judged on the next while its turn is still in the window. A point is covered on the first decision at or above persist_fact, and stays covered.

Rep-facing text

Every string the live view shows from policy is approved text with a text_id, red-penned and loader-checked like the playbook lines: move headlines (headline.<move id>, the client's words, 8 words or fewer), concern headlines (concern.<topic>.headline), situation headlines and hints (situation.<id>.headline, situation.<id>.hint), caution notes per risk rule (caution.<risk id>, "That sounded like a rate prediction."), purpose labels, template titles, hints and not-needed reasons, the rate-pressure title, and fact labels and templates (plan.…). Lines and situation points keep their own status (draft, approved or retired; a retired line never reaches a bundle). The loader hard-rejects any of them that is not approved, exactly as it does lines (INV-COPILOT-003). No approved text starts with a prefix the view already shows as a tag ("say:", "Say", "Worth saying", "Ask first", "Careful"); the builder refuses one that does, and the encoded draft caution notes lose their leading "Careful:" (COPILOT-082).

What exists in src/engine today, and what is new

Piece Exists today New (story)
Approved lines CardState.lines (RepFacingText), card.ts linesOf (up to 3 per move) points per plan item (088); a retired line status (079)
Situations encoded in policy/src/playbook-*.json (7 rate situations with headline, hint, points, do_not, do_not_flags, qualify_first, core_text_id; docs/policy/rate-objections.md), not read by the engine situations as the points source for the card and the plan (083)
Question set the whole bank on every decision (the rate questions push the worst onboarding request to 13,596 estimated tokens) conditional question groups (081)
Applicability ChecklistItem.applies and hero.ts applies() (always, never, when_fact, when_amount_below_gbp, client_type) when_slot rules and not-needed reasons (087, 088)
Must-say locks facts.ts persistFacts (said_* on rep turns, with i); ChecklistEntry said / uncertain / unsaid / na done_at from the last lock an item needs (088)
Current move card.ts allowedMoves and pickCard hysteresis (switch_margin, confirm_updates, card_cooldown_utts) the current item from move, stage and inserts; one change per decision (088)
Concerns concern.ts open, re-raise, clear, expire; ConcernState.start_i; ConcernEpisode the inserted concern item (088); the rate episode from rate_objection_type is rate.ts SessionState.rate and rateView (086, done)
Stage stage.ts StageState.current with hysteresis the stage as a tie-break for the current item (088)
Risk flags risk.ts latched RiskFlag { id, i, t, value, label }; the five rate do-not flags with draft live_notes (encoded) approved caution notes (082), plan.caution (090, done)
Client facts persisted *_known facts; client_type (unknown unless obvious); amount.ts extractAmounts and gbpEquivalent fact slot values and chips (085)
Purpose none the purpose Choice, lock and override (084, 091)
Per-point coverage none (coverage is per must-say id) covered::<text_id> nouls and delivered (089, done: plan.ts coveredToAsk, PlanMemory.covered)
The plan none (one card at a time) templates (087, done: policy data only), the state machine (088, done: plan.ts updatePlan, SessionState.plan), Snapshot.plan (090, done: step() calls updatePlan after pickCard; recompute replays purpose events), decision.d.plan and set_purpose (091)
Timing the engine has no clock the presenter in the page times dwell, holds and cautions from the call clock (093)

Review view (variant D)

For Stevan now and the team later: replay, audit and curation. Mockups: mockups/D-review-{onboarding,cs}-{light,dark}.png. Source: review.html?s=onboarding|cs. It is variant C's layout and panels (§Layout, §Panels, §States) with plain-English labels, the risk flags as a calm panel of their own, the mark form docked under the transcript, and one new panel: What the rep saw. Kept as approved with D; the one change with G is that the must-say checklist panel is now The plan and reads from the same call-plan object as the live view (§Call plan contract). Numbers and #i links belong here. The live view (§Live view) links here with "Review view" and this view links back with "Live view"; both are routes.

Layout

Column Span Onboarding (top to bottom) Customer success (top to bottom)
Spine 4 Transcript (sticky; with the mark panel it fills calc(100vh - 128px), min 640px; scrolls internally) then Mark a moment Same
Centre 5 What the rep saw, The plan, Risk flags What the rep saw, The plan, Issue resolution, Risk flags
Rail 3 Call stage, Concerns, Signals, Jev usage Same

Top bar: brand, view switch (Review view current, Live view a route), call picker, scenario badge, Upload call, connection. The player bar is the full §Player (decision and risk ticks), with the source label "call recording". The debug drawer, alerts and upload states are unchanged (§States).

Panels (changes against C)

Label changes

Before (C) After (review view)
hero · onboarding onboarding call
5 / 9 must-say items said · k uncertain · m not applicable The plan: 4 done, 5 to come, 1 not needed; must-say values on their items
uncertain 0.52 / unsaid / n/a unsure 0.52 / not yet / not needed
amount ≥ £5k mentioned at #41 Until an amount of £5,000 or more is mentioned
▲ Risk flags · 1, value 0.71 Risk flags, meta 1 in this call, Jev's score 0.71
flag false positive Mark as false alarm
Call stage, confidence 0.71 Call stage (Jev's best guess), how sure 0.71
Open concern, bucket: money, since #34 · 3 decisions ago Concerns, Topic: money, Raised at #34, again at #39
clears when: The client accepts the comparison, or asks to see a quote. Closes when the client accepts, is ready to book or agrees a next step. It also closes after 8 of Jev's checks without coming up again.
Next best move, confidence 0.58 ≥ 0.35 What the rep saw, How sure Jev is 0.58 (a card shows from 0.35)
say: prefix none (the lead point is first)
rule: Agree the next step hidden because a concern is open (#34) Held back: Agree the next step, until the fees concern closes
Acknowledge / Dismiss / shown since #41 removed; How the plan changed lists events with #i
listening… (leaning X) live: Listening at the transcript foot; review: How the plan changed shows the leaning
Signals, lit ≥ 0.60 · uncertain 0.30 to 0.70 Signals with the legend in words
a dash (unknown signal) no answer
lock icon persisted kept for the rest of the call
Jev telemetry, 26 requests Jev usage, 26 checks so far
latency mean / p95 Response time, average / slowest 1 in 20
tokens last in / out, tokens total Last check, tokens sent / received, Tokens so far
budget: 0 questions dropped, cache hits 12 / 26, cost (cumulative) Questions dropped to fit the size limit, Answers reused 12 of 26, Cost so far
last request / response Show the last request and answer
talk-share rep 64% / client 36% Share of words rep 61% / client 39%
decision-point diamond (unexplained) ◆ Jev checked this line legend
Resolution, none → reported → owned → resolved, EMA Issue resolution, no issue / issue raised / owned by rep / resolved, smoothed
Mark moment verdicts model / acceptable / avoid Good example / Acceptable / Avoid (same values sent)
n of N utterances n of N lines

Tokens

Live view tokens (G)

Canonical file: docs/design/shotgun/G/live.css (its :root blocks). These replace D's tokens for the live view. COPILOT-045 copies them into web/app.css unchanged, scoped to [data-view="live"], with the same three blocks: light on the root, dark under @media (prefers-color-scheme: dark) guarded by :not([data-theme="light"]), and dark again under [data-theme="dark"].

/* light */
[data-view="live"] {
  --bg: #f2f3f5;  --surface: #fbfbfc;  --surface-2: #e9ebef;
  --ink: #15181d;  --ink-2: #4b5159;  --ink-3: #62696f;  --ink-4: #8a9099;
  --line: #dcdfe4;  --line-2: #eaecf0;
  --accent: #1f7a56;  --accent-ink: #176046;  --accent-tint: #e2f1e8;  --accent-tint-2: #eef6f1;  --accent-line: #b9dcc9;
  --shadow: 0 1px 2px rgba(21, 24, 29, .05), 0 12px 32px -16px rgba(21, 24, 29, .22);
  --r: 10px;  --r-s: 5px;
  --font-display: "Bricolage Grotesque", "Instrument Sans", sans-serif;
  --font-ui: "Instrument Sans", system-ui, sans-serif;
  --font-mono: "DM Mono", ui-monospace, monospace;
}
/* dark: under @media (prefers-color-scheme: dark) for :root:not([data-theme="light"]), and under :root[data-theme="dark"] */
[data-view="live"] {
  --bg: #101215;  --surface: #181b20;  --surface-2: #21252b;
  --ink: #e8eaed;  --ink-2: #aeb4bc;  --ink-3: #858c96;  --ink-4: #565d67;
  --line: #2a2f37;  --line-2: #23282f;
  --accent: #5fc795;  --accent-ink: #8ddbb6;  --accent-tint: #16291f;  --accent-tint-2: #141f1a;  --accent-line: #245c43;
  --shadow: 0 1px 2px rgba(0, 0, 0, .3), 0 12px 32px -16px rgba(0, 0, 0, .6);
}

Type scale (live view):

Use Font and size
Brand Bricolage Grotesque 700, 16px
Pane titles ("Transcript", "Plan for this call") Bricolage Grotesque 600, 20px, -0.01em
The current item's title Bricolage Grotesque 600, 23px/1.2, -0.012em
Points worth saying Instrument Sans 400, 19px/1.38
Plan row titles Instrument Sans 500 (done, next) or 400 (later, not needed), 15.5px
Body Instrument Sans 400, 15px/1.45
Transcript text Instrument Sans 400, 14.5px/1.42
Hints, purpose line Instrument Sans 400, 14px
Tally, sub-heads, caution Instrument Sans 13px
State words, chips, covered tags Instrument Sans 12.5px
Tags ("Worth saying", "Say", "Ask first", "Careful"), speaker labels, group label Instrument Sans 700, 11 to 12px, uppercase, +0.06em
Times DM Mono 12px (rows, transcript); the clock 15px

Review view tokens (C and D)

Canonical file: docs/design/mockups/src/tokens.css. COPILOT-045 copies it into web/app.css unchanged, scoped to [data-view="review"]. Fonts: IBM Plex Sans (400/500/600), IBM Plex Mono (400/500) and IBM Plex Sans Condensed (500/600, panel labels only). They are self-hosted woff2 (SIL OFL 1.1, mockups/src/fonts/OFL.txt), with no CDN.

Colour

:root {                      /* light: "paper" */
  --bg: #F3F1EC;  --surface: #FBFAF7;  --surface-2: #EEEBE4;  --surface-3: #E4E0D7;
  --line: #DCD7CD;  --line-strong: #BFB8AA;
  --ink: #1C1B18;  --ink-2: #4A4740;  --ink-3: #78736A;
  --accent: #2447A8;  --accent-ink: #FFFFFF;  --accent-bg: #E4EAF8;  --focus: #2447A8;
  --said: #1D7447;  --said-bg: #E1F0E6;
  --uncertain: #9A5800;  --uncertain-bg: #F8EAD3;
  --risk: #B42318;  --risk-bg: #FAE5E1;
  --rep: #2447A8;  --client: #0E6E66;  --selection-bg: #DDE5F7;
  --shadow-overlay: 0 12px 32px #1C1B1829;  --scrim: #1C1B1873;
}
@media (prefers-color-scheme: dark) { :root {   /* dark: "instrument" */
  --bg: #0E1012;  --surface: #15181B;  --surface-2: #1B1F23;  --surface-3: #242A2F;
  --line: #262B30;  --line-strong: #394048;
  --ink: #E8E6E0;  --ink-2: #ABA89F;  --ink-3: #75726B;
  --accent: #88A6FF;  --accent-ink: #0E1012;  --accent-bg: #1B2540;  --focus: #88A6FF;
  --said: #52C28C;  --said-bg: #13291E;
  --uncertain: #E9A546;  --uncertain-bg: #2E2312;
  --risk: #F27562;  --risk-bg: #331816;
  --rep: #88A6FF;  --client: #52C4B6;  --selection-bg: #1F2A45;
  --shadow-overlay: 0 12px 32px #000000A6;  --scrim: #000000A6;
} }

Colour meanings: --said for said / lit / done / resolution progress; --uncertain for the 0.30 to 0.70 band, catching-up, drift and budget warnings; --risk for risk flags, credits-empty, failures and destructive buttons; --accent for links (#i refs), the current utterance, selection and primary buttons; --rep and --client for speaker labels and the talk-share bar. The CS chart draws health in --ink (solid, 1.8px) and the EMA in --accent (dashed 4/3, 1.5px). Chart.js reads these through getComputedStyle and re-reads them on a prefers-color-scheme change.

Type scale

Token Size Use
--fs-label 11px, Plex Condensed 600, uppercase, +0.07em panel labels, subheads, speaker tags
--fs-meta 12px (Mono for values) timestamps, ids, values, chips, confidence
--fs-body-sm 13px transcript rows, list rows, alerts
--fs-body 14px card lines, body, form fields
--fs-title 16px / 600 card title, concern type, current stage, dialog title
--fs-head 20px Mono 600 CS health value
--fs-hero 32px / 600, −0.02em completeness x / y

Line heights: --lh-tight 1.25 (titles), --lh-body 1.45. font-feature-settings: "tnum" is set on body.

Spacing, radii, layout constants

Spacing is on a 4px base: --sp-1 2 · --sp-2 4 · --sp-3 8 · --sp-4 12 · --sp-5 16 · --sp-6 24 · --sp-7 32. Panel padding is 12px top and 16px sides / bottom. List rows are 5 to 7px vertical with a 1px --line rule between rows. Radii: --r-chip 2px (chips, badges, tally cells) · --r-control 4px (buttons, inputs, lines, risk box) · --r-panel 6px (panels, alerts, dialog). Grid: --grid-cols 12 · --grid-gap 12px · --page-pad 16px · --page-max 1680px.

Layout (review view)

A 12-column grid with a 12px gap and 16px page padding, max width 1680px. Designed at 1440 to 1600px wide, which is Stevan's laptop. Below 1200px the three columns collapse to two (the spine plus a single stack of centre and rail); below 800px they stack to one column. Every variant has the same two full-width rows on top:

  1. Top bar: brand · call picker · scenario badge · (spacer) · Upload call · connection status.
  2. Player bar: play/pause · clock mm:ss / mm:ss · scrubber (decision-point ticks; risk ticks in --risk) · speed segmented control 0.5×/1×/2×/5×/10×/20× · catching-up badge (only while pending > 0) · clock source (audio · mp3 or virtual).

Below them, variant C (Transcript spine) uses the layout below. The review view keeps it (§Review view); the live view has its own layout (§Live view).

Column Span Onboarding (top → bottom) Customer success (top → bottom)
Spine 4 Transcript (sticky, height: calc(100vh - 128px), min 560px, scrolls internally) Transcript
Centre 5 Hero-onboarding (checklist + risk flags) → Next best move → Mark moment form (when open) Hero-CS (resolution → health → what moved it) → Next best move → Mark moment form (when open)
Rail 3 Stage strip → Open concern → Signals (1 column) → Jev telemetry Stage strip → Open concern → Signals → Jev telemetry

Alerts (credits-empty, model-drift, call-replaced, budget-skipped) render as a full-width row between the player bar and the grid. The debug drawer docks full-width at the bottom of the page. The upload panel opens as a full-width row under the top bar.

Panels (review view)

Each subsection gives the content, the data source, and the controls with what each one sends. The exact visuals are in mockups/C-*.png. Components live in components.css. These are variant C's panels. The review view keeps them with the label changes in §Review view; the live view uses none of them (its transcript has its own form, §Live view).

Player

<audio> (/api/calls/:id/audio, Range) or a virtual clock. The play button is a 32px round --ink button with the icon in --surface. The clock is Mono 13px. The scrubber is a 3px rail with an --ink fill, a 13px head, 6px decision-point ticks and 8px --risk ticks at flagged utterances. Speed is a segmented control where the pressed segment is --ink on --surface. Above 4× the audio is muted and the virtual clock drives, and the source label switches to virtual. The scenario badge is uppercase Condensed on --surface-2. Scrubbing sends seek{i}; the clock sends utterance{i}. The catching-up badge is in §States.

Upload

Opens from Upload call in the top bar as a full-width panel titled "Upload call", with the meta line showing accepted types from /api/health upload_types (today "mp3 only"). It shows the filename + x / y MB (Mono), a 6px progress bar in --accent, and a step list: uploading → transcribing → ingesting (● current, ✓ done in --said, ✕ failed in --risk). The final status drives the panel: ready → Open replay (selects the call in the picker); processing → the call is listed but disabled in the picker; needs_review / needs_scenario → see §States. PUT /api/calls/upload (XHR progress), POST …/transcribe, then POST …/ingest.

Transcript

Header: "Transcript" plus n of N utterances. A talk-share bar sits under the header: rep % in --rep, client % in --client, a 6px bar. The rep-monologue warning (> 70 words) is an amber strip under the bar. Each row is a grid 38px | 12px | 1fr: Mono 11px time · decision-point diamond (--ink-3, only on decision points) · REP/CLIENT Condensed tag in the speaker colour + text (13px, textContent only) + #i in Mono --ink-3. Backchannels are 12px --ink-3 with tighter padding. Future rows (after the clock) are --ink-3. The current row has an --accent-bg fill and a 3px --accent left border and follows decision.i. Risk rows get an inline ▲ rate prediction in --risk, and marked rows get a ● marked pin in --accent. Clicking any #i elsewhere calls scrollToUtterance(i).

Hero-onboarding

In the review view this panel is The plan plus the Risk flags panel (§Review view); the icon, tally and flag-row details below still apply.

Label "Must-say checklist", meta "hero · onboarding". Top: completeness 5 / 9 (32px, denominator in --ink-3) with "must-say items said", "k uncertain · m not applicable" beside it, and the discrete tally under it (said = solid --said, uncertain = dashed --uncertain on --uncertain-bg, unsaid = --surface-3; n/a items are not counted). List rows 16px | name | state label (+value) | #i, ordered said → uncertain → unsaid → n/a, with the n/a reason in 11px (amount ≥ £5k mentioned at #41). Risk flags sit below in a bordered box (--risk border, --risk-bg fill) titled ▲ Risk flags · N. Each flag row shows name · #i · value · flag false positive, with the quote on the next line. The per-item and per-flag flag false positive link sends flag_false_positive{kind, id, i}. When there are no flags, a neutral box says "No risk flags in this call."

Hero-CS

Label "Resolution", meta "hero · customer success". A four-step track none → reported → owned → resolved: done nodes are filled --said with ✓, the current node is a --said ring with a halo, pending nodes are a hollow ring with "not yet". Under each name: the time (Mono) and #i. Then the health line: value 0.64 (20px Mono), "health · EMA 0.56 · last change +0.06", legend at right, and a 120px chart (Chart.js in the build) with gridlines at 0.0 / 0.5 / 1.0, health solid, EMA dashed, and a dotted --said marker labelled at each resolution transition. Then "What moved it": ≤ 3 rows ordered by |Δ|, name | #i | ±Δ (up --said, down --risk).

Stage strip

Review view only ("Call stage", §Review view).

Label "Call stage", meta confidence 0.71. The current stage is shown large (16px/600) with "since #i", then the history as chips separated by ›. Past chips are --ink-2 on --surface-2; the current chip has an --ink border on --surface.

Open concern

Review view only ("Concerns", §Review view); in the live view a concern is an inserted plan item.

Label "Open concern", meta confidence. Type (16px/600) or the parent bucket, then bucket: money and a since #i · n decisions ago / clears when … key-value list. Next come the topic's approved lines (same style as card lines, via renderRepText) and flag false positive (flag_false_positive{kind:'concern'}). Empty: "No open concern." plus "Last cleared: at #i".

Next best move

Superseded: the live view's current plan item (§Live view) and the review view's What the rep saw replace this panel. The rewrite slot and the gated state below still apply.

Label "Next best move", meta confidence 0.58 ≥ 0.35. The body has a 3px --said left rule: title (16px/600), what (13px --ink-2), and ≤ 3 lines on --surface-2. The highlighted line has a --said-bg fill, a --said border and a bold say: prefix. A verified rewrite renders as a fourth, dashed line labelled "tailored (verified)". Only {kind:'rewrite', verified:true} renders. Below: rule: <s>Move</s> hidden because <reason> in 12px --ink-3. Footer: Acknowledge (ack_card), Dismiss (dismiss_card), and shown since #i. The gated state is in §States.

Signals

Label "Signals", meta lit ≥ 0.60 · uncertain 0.30 to 0.70. A single column in the rail (two columns when a variant gives it ≥ 5 grid columns). Each row is lamp | name | [lock] value. The lock icon marks persisted facts. Score signals show their level (high / medium / low) instead of a number. Lamps are described in the constraints above.

Jev telemetry

Label "Jev telemetry", meta N requests. A two-column key/value grid: the model on its own line (jev-1.13.0 = expected, or the drift form in §States); latency mean / p95 ms; tokens last in / out; tokens total; cost (cumulative); budget (questions dropped); cache hits hits / decisions. At the bottom, the link last request / response opens the debug drawer. It is disabled with the title "open with ?debug=1" when the session was not loaded with debug:true.

Debug drawer

Only rendered when the page has ?debug=1 (load{debug:true}). It is full-width, docked at the bottom, with a 2px --ink top rule. Tabs: Last request · Last response · Weights · Rewrites (selected tab has an --ink underline). Request and response are pretty-printed JSON in Mono 11.5px on --surface-2 (keys --accent, strings --client, numbers --uncertain). Weights holds the thresholds form → set_weights → the recomputed diff and state drift: N of M decisions changed. Rewrites lists dropped candidates struck through, with the verifier reason and value in --risk Mono.

Mark moment

Opens in the centre column under the card, or with m when a span is selected. The meta line shows the span (#35 to #38 · 4 utterances). Fields: verdict as a segmented radio model | acceptable | avoid (selected segment --ink), tag (a select of moment kinds + free text), note (textarea). Mark moment sends mark_moment{start_i, end_i, verdict, tag, note}; Cancel / Esc closes it. On success an --ink toast shows "Marked #35 to #38 as avoid · risk" and the rows get a pin. error{note_rejected} gives the note a --risk border and the inline text "Remove names or numbers from the note."

States

All drawn in mockups/states-{light,dark}.png. Only the states listed here exist. Do not invent others.

Listening / gated card

Review view (What the rep saw). The live view shows "Listening" at the transcript foot instead (§Live view).

When next_move.confidence < 0.35, the card body becomes a dashed --line-strong box with a three-bar glyph and the text "listening… (leaning Explain that a booking is binding)". The meta line shows confidence 0.22 < 0.35, and a note says "No line is shown until the move is confident. Approved lines only." No lines, rule or buttons are shown.

Uncertain band

A value in 0.30 to 0.70 is never shown as off. Checklist rows use the half-filled icon + "uncertain 0.52" in --uncertain, and the tally cell is dashed. Signals use the dashed amber lamp with the value in --uncertain. Unknown (null) uses the dotted lamp and "no answer".

Catching-up badge

At high speed, while queue.pending > 0, an amber badge ● catching up (7 queued) sits after the speed control (--uncertain-bg fill, 1px --uncertain border). It disappears at 0.

Credits-empty alert

On alert{credits_empty}, a full-width --risk alert reads: "Jev credits are empty (402 · code 2021) The session stopped. Nothing after #i was judged; the last snapshot is held. Top up the Cloudflare account, then start a new session." The play button and speed control are disabled (grey play button). The last snapshot stays rendered.

Model-drift warning

On alert{model_drift} (once per session), an amber alert reads "Model drift: stub-1 answered, jev-1.13.0 expected Answers from a different model are not comparable with calibrated thresholds." with a Got it button that dismisses it. Telemetry keeps stub-1 ▲ expected jev-1.13.0 in --uncertain for the rest of the session.

Budget skipped

alert{budget_skipped} shows an amber note: "Decision #57 skipped: request over the 12,000-token cap. The previous snapshot is held; nothing is rendered as zero." A decision marked unknown keeps the previous panel values.

Call-replaced notice

error{call_replaced} shows a neutral (--surface-2) alert: "This call's transcript was replaced (rev 2 → 3) Decisions from this session are stale and playback is off. Reload to start a new session on the current transcript." Reload call starts a new session (new nonce, load without resume). Play is disabled.

Upload progress / failure / needs-scenario / needs-review

Invalidation confirmation dialog

Used when the speaker swap (or a scenario replacement) returns 409 call_has_derived_rows. A modal alertdialog over the --scrim: 460px wide, --r-panel, --shadow-overlay. Title "Swap speakers and discard derived work?". The body gives the rev change and a count table (decisions, moments, marks, labels, answers; Mono counts), then "Open sessions stop and reconnect on the new transcript. This cannot be undone." Actions: Cancel (default focus) and the destructive Delete N rows and swap (--risk fill), which re-posts with force:true and then reconnects with a new nonce.

Transcript selection (mouse and keyboard)

Mouse: shift-click or drag selects start_i..end_i. Keyboard: focus a row (visible 2px --focus ring), then Shift+↓/↑ extends from the focused row. Selected rows get the --selection-bg fill with a 1px --accent edge on the first and last rows. A hint line under the transcript reads Shift + ↑ ↓ extend · m mark · Esc clear · 4 utterances selected (#35 to #38). m opens the Mark moment form; Esc clears.

Empty / loading

Variants

All share the tokens of their view, the components and the data. A, B and C changed only the layout and the hero form; D split the product into two screens; round 3 (E, F, G and H, docs/design/shotgun/) redesigned the live screen around a living call plan.

G: One list (approved 2026-09-25, the live view)

From the round-3 brief (shotgun/BRIEF.md), written after Stevan's verdict on D: the card floated with no anchor, the caution outlived its moment, the progress dots had no labels, and above all the screen had no overall structure for the call at hand. G puts the transcript full height on the left and, on the right, the plan as one list: the current item open in place, done items collapsed with a tick and the time, what is to come greyed, items not needed at the foot with a reason, and a client concern inserted where the call is. Plan and suggestions are one object. For: nothing floats, reading order is call order, every state is a word, and the one accent means "worth saying". Against: it needs new engine pieces (purpose, templates, per-point detection, fact values) and new approved policy text before it can run on real calls (§Call plan contract). Full rationale: shotgun/G/NOTES.md (a).

E, F and H (round 3, not chosen)

E, "Agenda spine": the plan as a vertical stepper on the left, the transcript in the centre. F, "Plan strip and focus": a horizontal plan strip across the top and one large focus card. H, "Teleprompter": a one-line breadcrumb, one centred card and a three-line transcript ticker. Mockups in docs/design/shotgun/{E,F,H}/.

D: Live view and review view (review view kept, live view superseded by G)

From Stevan's review of A, B and C (FEEDBACK-2026-09-25.md). The rep sees one card during the call: a short situation headline, 2 or 3 green points worth mentioning now, ticks as they are covered, one caution line when a risk fires, and a quiet context line and progress strip. Everything with a number, a threshold or a rule (signals, stage history, confidence, telemetry, the checklist, risk values, #i links, the mark form) moves to the review view, which is C's layout in plain English. The cadence rules keep the card from moving faster than a rep can read while talking. For: it answers "what should a rep look at?" with one thing, and it keeps the audit ledger for the person who audits. Against: the one card floated with no structure for the call, which is what round 3 fixed. Its review view is kept; its live view is replaced by G.

A: Reference

This is the reference demo's layout, kept literally: the transcript where the video was (7 cols), the hero where the probability chart was (below it), the coaching column on the right (5 cols: stage rail, card, 2-col signals, concern), and telemetry as a header strip. The onboarding hero is split: the completeness numeral on the left, the checklist on the right, risk flags full-width under both. For: zero learning cost against the demo. Against: the hero is the last thing on the page (below the fold at 1440×900), which inverts the point of replacing the probability. The header strip has no room for drift, budget or cache.

B: Hero band

A full-width stage rail, then a full-width hero band (onboarding: numeral · 5×2 grid of item tiles · risk box; CS: resolution track · health chart · what moved it), then three columns: transcript 5 · card + concern 4 · signals + telemetry 3. For: the strongest "the headline is compliance" read, good for demos. Against: the band pushes the transcript and card below the fold. The tiles reflow whenever conditional items change applicability. The #i references in the band point at a transcript that is off screen.

C: Transcript spine

The transcript as a viewport-tall evidence column on the left (4), the verdict (hero) and the next move in the centre (5), and a context rail on the right (3: stage, concern, signals, telemetry). For: every #i in the hero, flags, concern and moved list points at a row that is always on screen. The hero is the first thing in the centre column and the card sits directly under it. Span selection and the mark form sit next to the evidence, which is Stevan's M1 job (audit the engine, mark moments). Against: signals get one narrow column, and the rail scrolls on smaller laptops.

Approved variant

13,546 words · Internal working documents. Do not share outside CurrencyTransfer.