ClientsFlow · Email-System Overhaul · W8 · EBO
FINAL · owner decisions CLOSED · awaiting signature · 2026-07-12. The target pipeline-board experience after W8 ships — a live push board, entrance/status animations, on-card AI reply, collapse arrow, a hamburger menu that now holds everything except the pipeline (owner ruling R1), and a consistency pass (not a redesign) over the existing board, light theme only. DECISIONS_FINAL.md (2026-07-12) outranks the owner comments, which outrank ANSWERS_ROUND3 and the handoff. Scenarios S1 … S10. The AMBER register is CLOSED — nothing in W8 may be guessed, defaulted or flagged amber.
prefers-reduced-motion suppression that touches these four animations is REMOVED (app/dash_styles.py:177 and the @media (prefers-reduced-motion) block in static/design/TOKENS.css). Elegance + hierarchy are the constraint, not motion-off.data-status → token map (static/design/TOKENS.css).ZZ-S1 Design Kft (design-system card) · ZZ-S2 Bounce Kft (arrives live) · ZZ-S3 Due Kft (needs action today) · ZZ-S3 Quiet Kft (no action today) · ZZ-S4 Seq Kft (armed sequence) · ZZ-S5 Reply Kft (unreplied inbound) · ZZ-S6 Overdue Kft · ZZ-S6 Today Kft · ZZ-S6 Future Kft (next-step proximity trio) · ZZ-S7 Fold Kft (collapse) · ZZ-S8 Menu Kft (nav/board card) · ZZ-S9 Fail Kft (failed send) · ZZ-S10 Wide Kft (responsive).$BASE (the staging board's base URL) and $DASH_TOKEN (the CURRENT live dash token — never computed from local flows.form_token("dash")) exported. The only curly placeholder bound by the walker is {lead_id}.| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás opens the pipeline board (loads the dashboard URL, Pipeline tab) — the seeded card ZZ-S1 Design Kft is on it |
The board he already knows — same columns, same card layout, same place for everything — but visually consistent: one spacing scale, one card corner radius, one border weight, one font/weight ladder, and every card carrying the same Material elevation-3 (light) shadow. Colour is disciplined: saturated only on decision states (needs-action · failed · approved), quiet tints on structural chrome | Copy: column titles and card fields unchanged · Look: uniform 8px spacing scale, one radius (14px cards), one border weight, one type ladder, elevation-3/light shadow on every card, quiet accent tints on column headers · Where: whole board | The dash loads the ClientsFlow token layer (static/design/TOKENS.css: status palette + --elev-3 + spacing/radius vars); card/tag/column styles read those tokens instead of the per-element ad-hoc values in app/dash_styles.py. No per-element inline colours anywhere |
Must NOT be a redesign — cards, columns and controls must NOT move, change shape, or be re-skinned beyond spacing/tags/fonts/radii/borders/shadows; must NOT paint any dark-theme surface even briefly; must NOT give structural chrome the same saturation as decision states (rainbow board); must NOT leave two shadow styles side by side | — |
| 2 | Mátyás scans across the columns | The same status always looks the same everywhere — a "Scheduled" tag, a "Due today" tag, a "Rejected" tag read identically in New leads and in Contract out. Every tag is colour + icon + English word, never colour alone | Copy (English only): "Scheduled" · "Due today" · "No action" · "Rejected" · "Failed" · "Approved" · Look: one pill style (.status-tag[data-status]), one colour + one Lucide icon per status, board-wide · Where: the card's tag row, every column |
Every status renders from a single state → token map (one data-status attribute per state); colours defined once in TOKENS.css, never per column, never inline |
Must NOT show ANY Hungarian text in the operator UI (no "Ütemezve" / "Ma teendő" / "Nincs teendő" / "Küldés"); must NOT let one status appear in two different colours in two columns; must NOT convey a status by colour alone (no icon, no word) | — |
| 3 | Mátyás keeps the board open while working (never presses Refresh) — and while a push lands, he has an editor OPEN on one card (a Draft-reply field with half-typed text) | The board stays current on its own — new leads, status flips, and sequence start/stop appear live, within seconds, with no refresh and no full-page reload. The card he is typing in is left alone: its update is held back and applied the moment he closes the editor, announced by a subtle "updated" flash on the card. His text is never touched (owner ruling D1) | Copy: unchanged; a brief "updated" flash on a card whose queued update just applied · Look: cards appear/update in place; the card with the open editor visibly does NOT change while he types · Where: live board | A server-push channel (SSE from the existing Modal web app — no new always-on service, $30 cap intact) replaces/supersedes the 12s pollEvents loop (app/dash_js.py:2449) as the update path; updates are applied per card, never by re-rendering the page. An update targeting a card with an open editor (Draft-reply field, any open inline panel, or window._gdirty set) is queued and flushed on editor close — settled behaviour, not a default |
Must NOT trigger a full-page auto-reload (it can discard an in-progress edit — the standing 2026-06-12 rule); must NOT repaint, re-render or reorder a card that currently has an open editor — a single character of his typing lost to a live push is the canonical fail; must NOT silently drop the queued update either (it applies on close, with the flash); must NOT go silently stale for minutes | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Nothing — Mátyás is just watching the board when a new lead (ZZ-S2 Bounce Kft) arrives |
A new card drops into the New-leads column and bounces to settle into place over about 2 seconds — playful, impossible to miss, and it lands exactly where it belongs. Then it is completely still | Copy: the lead's name + its first status tag · Look: bounce-to-place entrance (springy overshoot that settles), ~2s, .anim-entrance · Where: top of the New-leads column |
The push channel delivers the new lead to every open board; the card is inserted and the entrance class applied at insert time — transform-only (never opacity: an opacity fill pins opacity and defeats the dim/pulse overlays — the D6 bug in dash_styles.py:31) |
Must NOT require a refresh to see the lead; must NOT animate by fading opacity; must NOT drop the card into the wrong column; must NOT keep animating after it has landed; must NOT be suppressed by an OS reduced-motion preference (the animation always plays) | The new lead's history begins per W2's rules (first-contact entry). W8 draws attention to the card; it does not author the history entry. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás scans the board for what needs him today (seeded: ZZ-S3 Due Kft needs action, ZZ-S3 Quiet Kft does not) |
ZZ-S3 Due Kft stands out — full colour, a gentle pulse, a "Due today" tag. ZZ-S3 Quiet Kft is pushed back under a uniform darker dim overlay — the same darkness on every dimmed card, board-wide |
Copy: "Due today" tag on the active card · Look: needs-action = full colour + soft pulse (.anim-pulse); no-action = one consistent dark wash (.dim-today / today's .ucard.passive::before, dash_styles.py:420) · Where: every card, every column |
Each card's server-side "needs action today?" flag (d.dim, consumed at dash_js.py:1340) decides pulse vs dim; the dim is a non-opacity darkening wash (composes with the other layers), applied via one reusable class — the JS derives nothing |
Must NOT dim a card that actually needs action today; must NOT vary the dim darkness card to card (it must read as one uniform layer); must NOT use an opacity fade to dim (it breaks the other overlays) | — |
| 2 | Mátyás hovers a dimmed card, then moves away; later, a dimmed lead replies so the card becomes actionable | On hover the dim lifts instantly and the card reads at full strength, with a one-line reason ("No action today — next action: Jul 18") so he can audit why it was dimmed; moving away restores the dim. When the lead replies, the dim lifts for good and the card starts pulsing — live, without a refresh | Copy: hover reason line "No action today — next action: {date}" (English); card may gain a "Due today" tag · Look: hover → wash removed; on reply → wash removed permanently + pulse begins · Where: the card, in place | Hover only toggles the wash (no state write). The reply arrives through the push channel, flips the card's needs-action flag server-side; the dim class is removed and the pulse class added — the card is not re-rendered from scratch | Must NOT keep a card dimmed after it becomes actionable; must NOT require a refresh for the transition; must NOT hide the "why dimmed" reason behind a click; must NOT run pulse AND the rotating ring at full loudness on one card — owner ruling D2: the pulse outranks the ring (the pulse plays at full strength, the ring stays but calms — slower and thinner); must NOT drop the ring entirely just because the card pulses (the sequence is still armed and must still read as armed) | The underlying event (reply received) is logged per W2. W8 only flips the visual state. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás looks at ZZ-S4 Seq Kft, which has a scheduled sequence armed, and hovers its border |
The card wears a dashed border travelling clockwise around it in a calm cool colour (indigo) — a living "sequence running" halo. Hovering shows a tooltip: "Next scheduled: Jul 14, 09:00". It is unmistakably different from the failed-send ring, which is red and static — the two differ on BOTH colour and motion. If that same card ALSO needs action today, the pulse wins the loudness budget (owner ruling D2): the pulse plays at full strength and the ring keeps turning, but calmer — slower and thinner — so the card shouts once, not twice | Copy: "Scheduled" tag + hover tooltip "Next scheduled: {date}, {time}" (English) · Look: indigo (--status-scheduled) dashed ring rotating clockwise (.anim-sequence-ring); on a card that also pulses, the ring switches to its calm variant · Where: the card's outer border |
The card carries the "active sequence" flag (today's autoflow class, dash_js.py:1344); the rotating ring is layered additively over the static dashed border, so a browser without the animation still shows a dashed indigo border. Animation precedence is one settled rule in one place: needs-action pulse > sequence ring (D2) — never a per-card judgment call |
Must NOT show the ring on a card with no armed sequence; must NOT replace the static dashed fallback; must NOT be red, warm, or static (that vocabulary belongs to failure — S9); must NOT rotate counter-clockwise; must NOT hide the "next scheduled" readout behind a click; must NOT run the ring at full loudness alongside the pulse on one card, and must NOT suppress the ring altogether when the pulse takes precedence (D2: it calms, it does not vanish) | The sequence's scheduled steps live in the touchpoint history / outbox (W2). The ring reflects that a sequence is armed; it adds no history of its own. |
| 2 | The sequence ends because the lead replies (or it is stopped / completes) | The rotating ring disappears — and in the same beat the card picks up the needs-action pulse (a reply means it's his turn now). "Autopilot ended" reads visually as "your turn", one continuous story, not two disconnected changes | Copy: "Scheduled" tag removed; "Due today" tag appears · Look: ring gone → pulse begins on the same card, no gap · Where: the card | The push channel clears the "active sequence" flag and (for a reply-stop) sets the needs-action flag in the same update; the ring class is removed and the pulse class added in one paint | Must NOT keep spinning after the sequence stopped (a false "still sending" signal); must NOT require a refresh to clear; must NOT let a reply-stopped card go silent (ring gone, nothing gained) — the hand-off to the pulse is mandatory | The stop/complete/reply event is logged by W1/W2. W8 only swaps the ring for the pulse. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás looks at ZZ-S5 Reply Kft, whose latest email from the lead is unreplied |
The lead's latest email is right there on the card face — not behind a click: the first ~2 lines are printed, with a "more" expander that opens the full text inline. Under it sits one primary button: "Draft reply" | Copy: the lead's email text (2-line preview + "more") + button "Draft reply" (English) · Look: quoted email block on the card + a primary button · Where: card body, only while the latest email is unreplied | The card reads the latest-inbound + reply-status from the same lead state W2 maintains; nothing is generated and nothing is sent at this point | Must NOT hide the unreplied email behind a click/hover; must NOT print the full email by default (a long inbound would blow up the card height and wreck the column scan); must NOT show the block after a reply has been sent; must NOT show a stale/older email as "latest"; must NOT label the button anything other than "Draft reply" | The inbound email is already a touchpoint (W2). W8 surfaces its text on the card face; showing it creates no new entry. |
| 2 | Mátyás clicks "Draft reply" (and mashes it a second time to be sure) | Within a blink the button disables and shows a spinning loader with the label "Drafting…" — so the click clearly registered. Then an editable reply field appears right on the card, pre-filled with the AI's draft: proper Hungarian greeting on top (client-facing copy), the body written from this lead's history, the canonical sign-off at the bottom. The second click did nothing — no second draft | Copy: button → "Drafting…" (English) while working; draft body = Hungarian client copy, {{greeting}} resolved (e.g. "Kedves János,") + canonical sign-off · Look: Lucide loader-circle spinning in the disabled button (visible <300ms), then an inline textarea + "Send" / "Cancel" buttons · Where: in place, on the card |
One AI generation request fires (the button is disabled synchronously on the first click — a double-fire cannot happen and cannot double-bill Gemini); the draft is generated from the touchpoint history, with greeting + sign-off applied by the existing copy rules (W2/W6 own copy correctness — W8 renders the field). Nothing sends | Must NOT auto-send the draft; must NOT fire two generations on a double-click; must NOT sit visually dead for 1–2s with no spinner; must NOT show a raw {{token}} in the draft; must NOT drop the canonical sign-off; must NOT lose the draft if a live update lands on the card underneath; must NOT put Hungarian on any BUTTON or label (only inside the draft body) |
No entry yet — a draft is not a send. The touchpoint is written only when he actually sends (step 3), owned by W1/W2. |
| 3 | Mátyás edits the draft and clicks "Send" | The reply goes out and the card frees immediately: the unreplied block + draft field disappear (it's answered now) and the sent reply lands in the card's history. If the send FAILS: the field stays open with his text intact, the failure is shown inline in the field, and the card gains the red dashed failed-send ring — a failure can never look like a success | Copy: on success — card returns to normal (no unreplied block); on failure — inline English failure line + "Failed" tag · Look: success = field closes; failure = field stays open + red dashed ring (S9) · Where: the card | The send fires through the W1 send path (the click IS the human gate); the touchpoint history gains the sent reply (W2) with the exact text he sent — 100% parity. On failure W1 raises the failed-send state and W8 renders it; nothing is silently swallowed | Must NOT send without the explicit "Send" click; must NOT keep showing the unreplied block after a successful send; must NOT close the field or clear his text on a failure; must NOT report success when the send failed; the recorded history copy must match what was sent exactly (parity) | NEW ENTRY (owned by W2) The sent reply is logged as an outbound touchpoint with the resolved greeting + sign-off — 100% parity with what left. W8 triggers the send; W1/W2 own the record. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás reads the three seeded cards — ZZ-S6 Overdue Kft, ZZ-S6 Today Kft, ZZ-S6 Future Kft |
Each card carries a single uniform "next step" tag in the same slot — "Next: call Jul 14" · "Next: scheduled email Jul 15" · "Next: awaiting reply" — and the tag's tint tells him the urgency: overdue = red, today = bright, future = calm. He knows when before he reads the date | Copy (English): "Next: {step} {date}" · Look: one tag style, colour by proximity from the shared token scale (overdue → --status-failed, today → saturated, future → quiet tint) · Where: the card's tag row, same slot on every card |
The next step is derived from the lead's scheduled/expected next action (the same data feeding the outbox/history, W2); tag colour comes from the state→token map (S1) with a proximity modifier — the same source that drives needs-action/dim, never a second rule | Must NOT render the next step as free prose that varies card to card; must NOT use a different colour for the same step-type in different columns; must NOT show a next-step tag that contradicts the card's status tag (e.g. "No action" + an overdue red next-step); must NOT invent a second urgency rule that disagrees with the dim/pulse logic | The next scheduled step lives in W2's data. The tag reflects it; it authors no history. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás clicks the small arrow at the top-right of ZZ-S7 Fold Kft |
The card folds to a slim name-only row with a quick, cool bouncy animation (~1s) — a springy shrink that feels professional, not a jerky snap. What survives the fold: the lead name · the status colour dot · the next-scheduled-contact date · the sequence icon (if armed) · the failed-send marker (if failed). The arrow now points the other way | Copy: name + next-contact date survive · Look: full card → one-line strip in ~1s using the proven spring easing (cubic-bezier(.34,1.56,.64,1), easeOutBack — the same easing the entrance uses, already in TOKENS.css); arrow flips · Where: the card, top-right arrow |
The card's collapsed flag is set; the collapsed layout renders only the survivor field set. Height animates via the shared easing token — no bespoke hand-rolled curve per card | Must NOT drop the name or any "needs attention" signal (a collapsed card that is failing or needs action today must STILL show it — a collapsed card must still be able to shout); must NOT reflow the whole column jarringly; must NOT take noticeably longer than ~1s; must NOT look janky/linear (it must use the spring easing) | — |
| 2 | Mátyás clicks the (now-flipped) arrow on the collapsed card, then reloads the page | The card bounces back open to its full self (same ~1s spring) — unreplied email block, tags, buttons all return exactly as before, including a draft he had in progress. After the reload, the cards he had collapsed are still collapsed (owner ruling D5 — the fold is a preference, not a session accident); newly pushed cards arrive expanded | Copy: full card content returns · Look: strip → full card, spring easing; arrow flips back; after a reload the previously folded cards render folded immediately (no expand-then-collapse flicker) · Where: the card | The collapsed flag is cleared and the full layout renders. Settled (D5): the collapsed flag is persisted per card (client-side, keyed by lead id) so it survives a reload and is applied at first paint; a live-pushed new card has no stored flag and therefore renders expanded | Must NOT lose card content or an in-progress draft across a collapse/expand; must NOT expand a different card than the one clicked; must NOT re-expand everything on reload (D5 is settled, not a default); must NOT flash the full card before folding it on reload; must NOT collapse a newly arrived card | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás loads the dashboard (the board behind it holds ZZ-S8 Menu Kft) |
The top bar holds exactly one tab — Pipeline — plus a single ☰ hamburger drawn in the same design language as the rest of the board (same radius, border, elevation-3 shadow, Lucide menu icon). Everything else — including Today/Focus, Calls and Outbox — lives behind the ☰. The board is the page |
Copy (English): "Pipeline" + ☰, nothing else · Look: a one-item nav; one ☰ button styled from the token set, unmistakably a menu (icon + hit area, not a hidden gesture) · Where: dashboard top nav | The nav's split is re-declared (W7 owns the list; dash_js.py:31-35 PRIMARY/TOOLS): PRIMARY becomes [pipeline], every other panel is a ☰ item — relocated, never deleted. Settled by R1 — not a default |
Must NOT leave a second tab in the top bar (Focus/Today, Calls, Outbox included — R1 is literal); must NOT delete or break any relocated tab; must NOT leave a tab reachable from two places (old Tools overflow AND the new ☰); must NOT make the ☰ itself easy to miss now that his daily tools are inside it; must NOT style the ☰ off-system (different radius/shadow/colour than the board) | — |
| 2 | Mátyás clicks ☰ and looks for the tools he uses every day (Today/Focus, Calls, Outbox), then clicks one, then presses Esc and returns to the board (ZZ-S8 Menu Kft still there) |
The menu opens as a clean panel listing every relocated tab — his daily items first, in a stable order, visually separated from the rarely-used rest (a "Daily" group above the others, each with its Lucide icon + English name). One click opens the tab exactly as before; Esc (or a click outside) closes the menu cleanly. He finds Outbox at a glance, without reading a list of eighteen | Copy: the relocated tab names (English), grouped: Today · Calls · Outbox · Done first, then the rest · Look: dropdown panel under the ☰ — same radius/border/elevation-3 as the board, one icon + one label per row, a hairline separator between the daily group and the rest, clear hover highlight · Where: under the ☰ button | Each item deep-links to its existing tab/panel — no behaviour of those tabs changes, only their entry point moved (W7 verifies each still works). The ordering is declared once beside the item list; the menu never reorders itself between opens | Must NOT change what a relocated tab does; must NOT bury the daily items among the dormant ones or below the fold (they are the reason the menu now matters); must NOT reorder the items between opens (his muscle memory is the whole point); must NOT require more than one click to reach any tab; must NOT trap focus so the menu can't be closed with Esc or an outside click | — |
| 3 | Mátyás looks for the old "Create Task" button on a deal card | It's gone — removed from the interface entirely (Q15=A: this is the ONLY removal; every other control merely relocates) | Copy: — · Look: the Create-Task button no longer rendered · Where: the deal-card action row (today dash_js.py:1279 / 1283 / 1287) |
The Create-Task control is removed from the UI (coordinated with W7 — removed ONCE, cleanly; not guarded in one workstream and hidden in the other) | Must NOT leave a dead/stub Create-Task button that does nothing; must NOT leave any link or hint pointing at a control that no longer exists; must NOT accidentally remove any OTHER control | — |
| 4 | Mátyás reaches the menu from the keyboard: Tab to the ☰, press Enter, arrow down through the items, press Enter on one; then reopens it and presses Esc | The ☰ takes focus with a clearly visible focus ring, Enter opens it, the arrow keys walk the items with the highlighted item plainly visible on screen, Enter opens the highlighted tab, and Esc closes the menu and hands focus back to the ☰ — his daily tools stay a two-keystroke reach even though they left the top bar | Copy: unchanged · Look: a visible focus ring on the ☰ and a visible highlight on the active menu item (token colour; the ring is never styled away) · Where: the ☰ + its panel | The ☰ is a real focusable control (Basecoat's dropdown vocabulary): Enter/Space opens, arrows move the active item, Enter activates, Esc closes and restores focus. No new routing — the same deep links the mouse uses | Must NOT have an invisible focus state (a keyboard user who can't see where he is has no quick access at all); must NOT leave focus stranded inside a closed menu; must NOT open a different tab than the highlighted one; must NOT require the mouse to reach any item | — |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Nothing — a scheduled send on ZZ-S9 Fail Kft fails behind the scenes (W1 detects it) |
The card gains a static red dashed ring and a "Failed" tag — loud, urgent, and unmistakably different from the moving indigo sequence ring (red + still vs cool + moving). Clicking the ring / the "Failed" tag takes him straight to W1's failure detail for that card | Copy: "Failed" status tag (English) · Look: static red dashed ring (.anim-failed-ring, --status-failed) — no rotation · Where: card border |
W1 raises the failed-send state; W8 renders the ring from that state (styling only, never suppressible by a later cosmetic override). The click-through wires the card affordance to W1's failure entry — the behaviour behind it is W1's | Must NOT let the red ring be silently overridden by another card style; must NOT rotate or look cool-coloured (that vocabulary means "sequence running"); must NOT clear the ring until W1 says the failure is resolved; must NOT be the only failure signal on a collapsed card (the failed marker survives the fold — S7) | The failure is logged by W1/W2. W8 styles the card; it authors no failure record. |
| # | You do | You should see | Element that changes copy · look · where |
What changes underneath | Must NOT happen | 🕓 Touchpoint history |
|---|---|---|---|---|---|---|
| 1 | Mátyás works the board at desktop width (1280) — the real target; ZZ-S10 Wide Kft is on screen |
All columns side by side; cards at full richness — entrance bounce, pulse, dim, rotating ring, tags, the Draft-reply field and the collapse arrow all render cleanly and legibly, with the uniform elevation-3 shadow and one spacing rhythm | Copy: full, English · Look: multi-column board, full card chrome · Where: whole board | Full layout; no horizontal page scroll (the column strip scrolls inside its own container — the existing .kanban scroll-lock at dash_js.py:687-692 must keep working) |
Must NOT overflow the page horizontally; must NOT overlap cards or clip tags; must NOT lose the column scroll position when a live update lands | — |
| 2 | Mátyás glances at the board at tablet width (768) — ZZ-S10 Wide Kft still on screen |
Nothing is broken: columns scroll horizontally, cards stay legible, tags don't clip, the ☰ and the collapse arrow are still there and still work. It doesn't have to be optimised — it has to not be embarrassing | Copy: full · Look: horizontally scrollable columns, same card chrome · Where: board + nav | The existing responsive breakpoints reflow the column strip; no new mobile-specific layout is introduced | Must NOT clip or overlap any tag/button; must NOT hide the ☰; must NOT push content off-screen; must NOT introduce a phone-specific layout (out of scope by owner ruling) | — |
Each build work item derived for W8, and the scenario steps that prove it works when clicked through on the staging board. This table is the acceptance contract: /ebo-live-build walks exactly these steps after each item ships to staging, and every step again in the full pass before merge.
| Work item | What it delivers | Proven by (scenario · step) |
|---|---|---|
| WI-1 · Live push channel | The board updates live (new leads, needs-action flips, sequence start/stop, failures) with no manual refresh and no full-page reload — SSE off the existing Modal web app, no new service (≤$30 cap). | S1·3 · S2·1 · S3·2 · S4·2 |
| WI-2 · Entrance animation | New-lead bounce-to-place, ~2s, transform-only, always plays (no reduced-motion suppression). | S2·1 |
| WI-3 · Card state visuals | Needs-action pulse · uniform dim overlay with hover-audit ("why dimmed") · clockwise indigo sequence ring with a "next scheduled" tooltip · the ring→pulse hand-off when a sequence ends. | S3·1 · S3·2 · S4·1 · S4·2 |
| WI-4 · Draft-reply on card | Always-visible unreplied email (2-line preview + "more") → "Draft reply" button with a disabled spinner state → editable AI draft (Hungarian greeting + canonical sign-off) → human-gated "Send" with copy parity and a non-swallowing failure path. | S5·1 · S5·2 · S5·3 |
| WI-5 · Next-step tags | One uniform, token-coloured next-step tag on every card in every column, tinted by proximity (overdue/today/future). | S6·1 |
| WI-6 · Collapse arrow | Top-right arrow folds a card to name + status dot + next-contact date + sequence icon + failed marker, and expands it back — ~1s spring easing, preserving in-progress drafts. | S7·1 · S7·2 |
| WI-7 · Hamburger visuals + Create-Task removal | Top-level nav reduced to Pipeline only (R1) with the ☰ holding everything else — including his daily tools: the button + menu panel drawn in the board's design language, daily items grouped first in a stable order, one click to any tab, keyboard-openable with a visible focus ring, Esc-closable (W7 supplies the item list and proves each tab still works); the Create-Task button removed entirely. | S8·1 · S8·2 · S8·3 · S8·4 |
| WI-8 · Design-system consistency pass | Uniform spacing · tags · fonts · radii · borders · Material elevation-3/light shadow; one data-status→token colour map; English-only operator strings; light theme only. NOT a redesign. | S1·1 · S1·2 · S10·1 · S10·2 |
| WI-9 · Failed-send ring styling | Render W1's failed-send state as a static red dashed ring + "Failed" tag, click-through to W1's failure detail; unmistakably distinct from the sequence ring. | S9·1 |