The brand was being drawn: a circled "Z" in Source Sans beside "ZURICH kotak"
in letter-spaced caps with "General Insurance" under it. That is a passable
imitation of a logo and not the logo — the real mark's proportions, its weight
contrast and its plate are all in the asset, which the login page has been
using all along.
So the masthead carries the artwork, at half the bar's height, and nothing is
set in type beside it: the asset already says both lines, and the wordmark
next to it was the same sentence twice. Bundled from src/assets rather than
public/, so Vite rewrites the URL relative and it resolves under the
/zurich-kotak/ mount.
On a phone the mark shrinks rather than shedding its second line — that line
is inside the artwork now, so it cannot be dropped on its own the way the type
lockup's could.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An "Ask the desk" panel over the console, for ops only. The desk is a
platform Support App bound to app 536: it reads the deployed config and the
live book through read-only tools and answers with evidence. It is a
different kind of thing from the five AI employees — they do the work and
write to the workflow; this one only reads, to answer a person's question.
No new auth. The desk is an ordinary interactive agent, a conversation is a
session and a question is a message on it, and the token this console
already holds is accepted on those routes as-is.
Ops only, deliberately: the desk reads the WHOLE book — every partner,
every premium, every commission — so a partner agent must not open it. The
platform enforces it per user; the button just isn't drawn for anyone else.
The panel renders the markdown the desk actually uses — headings, lists,
bold, code and tables. Tables earn their place: asked anything countable it
replies with one, and rendered as raw pipes a correct answer reads like a
broken one.
Verified end to end against the database: its stage counts matched all ten
states exactly, and its account of why ZK-2026-01040 sits with the
underwriter names both PAN numbers correctly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Opened at 390px the masthead wrapped inside its own 56px bar and clipped —
"ZURICH kotak", "Lead Desk / Lead to policy" and the user's name all on
three lines each — and the nine-item rail stacked above the content, so
every page began with a screenful of menu.
The masthead now keeps one line: the mark, the app name, the person, with
the secondary lines dropped below 900px and the app name below 560px,
where it cannot fit beside the user menu without truncating.
The rail becomes a drawer behind a button, over the page rather than above
it, closing on navigation and on a tap outside. Desktop is untouched: the
button is display:none, the rail is static, and the drawer rules live
entirely inside the media query.
Also: "Ops admin", not "OPS admin" — the acronym rule over-fired on a word.
Verified at 390x844 and 1440x900: no horizontal scroll, nothing clipped in
the masthead, the drawer closes on navigation, and the desktop rail keeps
its transform and its hidden button.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Implements the design in zurich-kotak-leaddesk across every screen.
Foundation: Source Sans 3 bundled from src/assets/fonts (no Google Fonts
request), 15px root, navy/blue/amber/teal/red palette where blue is the AI,
amber a person, teal the customer and red is reserved for risk; flat white
10px cards on a grey canvas, no shadows.
Shell: 56px navy masthead (Z wordmark, Lead Desk, user), 232px white nav
grouped Needs you / Scheduled / History with coloured count pills and the
closed stages folded.
Overview: the hero says who holds each open lead (stacked bar + legend)
beside the renewals-at-risk card; five KPIs incl. Closed to date; Waiting
on a person queue with owner dots and "N more with the AI"; Pipeline by
stage with owner-coloured bars; Renewal exposure with tabs, dropdown
filters and a Holding it column.
Lead: back button, id line, holder pill; one attention banner (amber your
turn / blocked, red stalled, blue with the AI, teal with the customer) that
carries its button; four facts with a confidence bar; navy steps rail
whose current step takes its holder's colour; timeline as an avatar
gutter with title, stage pill, by-line + AI tag and text-link actions
(See reasoning · N steps, Hear the call, Open thread); side column flows.
Lists: card with search + hide-closed toggle, columns Customer / Stage /
Renewal due / Holding it / Premium / Last activity, stalled rows get a red
inset, closed rows dim, person-gated queues split into Your turn / With the
AI group rows.
src/api/holder.js is the single rule for holder tone + label so the table
cells, the overview and the lead header cannot disagree.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Removing the node circles took the active-row marker with them — the tint alone
was too quiet to say which queue you were on. The active row (and Overview /
All leads) now carries a short accent bar on its left edge.
It appears only when active, so inactive rows stay clean and nothing reads as an
empty checkbox, which is what the circles did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five fixes to the confusing nav:
- The two per-row numbers had no key. The AI's share now reads "N with AI" in
quiet grey, so it stops competing with the pink "needs you" badge.
- Removed the outline node circles (and the rail line they hung on) — with one
queue per group they read as empty checkboxes, not a pipeline. Rows now align
flush and the active queue is shown by its tint alone.
- Dropped the section-header count that just repeated the single row beneath it.
- Lightened and tightened the group labels so the queues lead, not the headers.
- Closed now matches a queue row — same padding, hover and count badge — while
staying a foldable disclosure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous change over-corrected: left-anchoring a capped block pushed the
whole page to the left and left the right half of a wide monitor empty. Drop
the width cap so the content fills from the nav to the right edge, the way a
sidebar dashboard should.
- shell__main: no max-width, fills the width beside the nav.
- lead grid: story column takes the slack (1fr), facts rail fixed at its
content width (360-420px).
- metric tiles: auto-fit, so three or four tiles always fill the row instead
of leaving an empty hard-coded 4th cell on the right.
Reading comfort is still handled inside the feed (paragraphs cap their own
line length), so a wide panel does not sprawl the text.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The content block was capped at 1460px and centred with margin:0 auto, so on a
wide or zoomed-out monitor a large empty river opened between the sidebar and
the content, with the whole page floating in the middle.
Fix is the standard sidebar-dashboard layout: one governing width, anchored to
the nav instead of centred. shell__main caps at 1320 and left-aligns, so the
header, metric tiles, stage strip and the two-column grid all share that width
and line up. The grid's own columns cap at their natural content widths (the
760px story column at its reading width, the 340-440px facts rail at what its
key/value pairs need) so neither card stretches into internal whitespace. Any
spare width on a very wide screen falls to the right as margin, the way a
sidebar layout should read.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Visual and information-architecture pass across every screen. No API call,
payload, poll, permission rule or workflow mirror changes; forms still render
from the live schema and the timeline's merge/grouping is unchanged (only the
display order is reversible).
Design language
- Ground + white cards with one hairline and one radius; sentence-case
headings; shared primitives in index.css (.card .pill .btn .avatar .live).
- One inline icon set (components/Icon.jsx); no more filled navy/red tiles.
- Read-side labels in api/display.js: roles and enum tokens humanized,
customer initials/tones, money and date formatters. Nothing sent changes.
Shell
- 56px header with brand, global lead search (portfolio rows, "/" to focus)
and identity chip with a human role label.
- Sidebar rows carry icons and a clear active bar; AI roster at the foot,
each opening its remit card.
Login
- Operator copy instead of retail copy; centred sign-in card with show/hide
password; demo persona picker that fills the EMAIL only.
Overview
- "Who holds the work" as four quiet cards that warm only when owed.
- Pipeline flow bar with every stage named beneath it; KPI strip in a card.
- Action rows with chevron / view-only eye; expiry bands filter the table.
Queues
- Filter bar (search, product, channel, expiry band, stalled, hide closed)
and sortable columns, all client-side on the fetched page.
- Avatars, product/channel chips, stage chips for closed leads, tinted
stalled rows, count inline with the title.
Lead
- Avatar + chips in the header; metrics card with the stage rail inside,
and a branch node for Referred / Parked / Lost / Declined.
- Timeline newest-first by default (toggle), the worker's face as the node,
one footer row per entry, AI markdown rendered as bold.
- "AI team on this lead" panel with per-agent step counts; PDF tags on docs.
Forms & dialogs
- Required marked with an asterisk + legend; "Select…" placeholder; dd Mon
yyyy hint under date inputs; open activity form in a titled panel;
one-line dialog footnotes.
Fixes: horizontal overflow at 1024px, boxed "in N days" figure, raw tokens
(sme_underwriter, verbal_call, Rsa), findings cut at "1.".
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Text looked soft across the whole app. Cause: the entrance animation used
transform: translateY, and .shell__main (the entire content area) ran it with
fill-mode both. An element that animated transform stays on a GPU layer, and
text composited on a transformed layer renders blurry in Chrome on Linux — so
every word on the page was soft.
zk-rise is opacity-only now — it fades in without ever moving text onto a
fractional-pixel layer — and the page-wide container no longer animates at all
(a full-page fade on every navigation only softened text while it played, for
nothing). The remaining transforms are hover-only and transient.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The real problem was never the font or the shadows — it was the design LANGUAGE.
Floating white cards on a grey-blue ground is the generic admin-template look,
and no restyle escapes it. This ports the approved mockup: one canvas, sections
set off by hairlines and space, the activity feed as the hero.
- ONE CANVAS. The content ground is white; the sidebar is the only tinted
surface. Panels drop their border, radius and shadow and become plain
sections under their own headings.
- The metric strips (lead + overview) stop being caged tiles and become an
inline stat row between two hairlines — money at display size, the rest
quieter.
- The stage rail loses its card and becomes a slim inline stepper.
- The property rail flattens: Customer / Vehicle / Documents are hairline-
divided groups aligned to the edge, headings at label size — it is
reference, and should not shout over the story.
- "Who holds it" drops from a filled pill to a coloured dot and a line of text.
- Bricolage is gone. Display and body are both Zurich Sans, the brand face;
headings and figures lean on weight and tracking, not a second family. The
palette is the ZK blues throughout.
Same data, same functionality — only the surface. Revertible in one push.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Continues 009153f into the surfaces it had not reached.
The top bar was 66px with a hard rule under it — admin chrome. It is 72 now,
on the softer line, with the product name at 16px instead of 15. The sidebar
gets wider gutters and taller rows, so the queue list stops reading as a dense
menu and starts reading as the pipeline it is drawn to represent.
The audit trail is where people spend their time on this screen, so it gets the
most air: entries at 24px apart rather than 18, quoted text with 12/16 padding
instead of 8/12, and the activity name at semibold so an entry has a heading
rather than a first line.
The stage badge was 15px under a 12px caption. It is the single most important
fact about a lead and now reads at 16px semibold, which is legible from across
a desk — which is how it will actually be read in a demo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The app had FIFTY-FOUR distinct font sizes and forty of them sat between
0.58rem and 0.95rem — a three-pixel band. The most-used size was 0.78rem, so
body copy was 12.5px. That is not a hierarchy, it is a fog of small grey text,
and it is the main reason the screen read as a decade old: nothing could be
emphasised because everything was already the same size.
Nine steps now, with real jumps. 12px is the floor and is reserved for
uppercase eyebrows — every one of the 53 declarations sitting there that was
not an eyebrow has been lifted off it. The surfaces people actually read — the
trail's entries, WhatsApp bubbles, field values, table cells — are at body size
rather than meta size.
The hairline cages are gone with them. The lead metrics and the overview KPIs
were seven equal cells in a 1px grid, every value the same size as its label,
which left the reader to do the prioritising themselves on every lead. They are
separated by whitespace now, values at 22px against 12px labels, with premium
and commission larger again: size does the prioritising instead.
Panels breathe at 32px rather than 24, radii went 10/14 -> 12/16/20, borders
dropped to the softer line, and the stage rail, the queue table and the cards
were rebuilt on the same spacing rhythm.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"New lead" sat in the sidebar. A sidebar is for moving between places;
filing a lead is an action, and an action in a list of destinations is
the one item that does not behave like its neighbours — everything above
and below it changes what you are looking at, and it did something.
It now sits on the overview and on every queue, beside the lists it adds
to, which is also where somebody is standing when they realise they have
a lead to file. Same role gate as before: shown only to a user who holds
an entry door.
The sidebar's own button CSS went with it rather than being left for
someone to wonder about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
THE SIDEBAR NOW CARRIES COUNTS. It deliberately did not, on the argument
that a tally could only come from a second full list call that would then
disagree with the queue's own total. Right about the cost, wrong about
the conclusion: with no numbers the only way to learn whether anything
was waiting on you was to open all nine queues in turn, which is the
question navigation exists to answer.
There is no second call. The overview's existing fetch moved into a
PortfolioProvider that both surfaces read, so this is one call fewer than
before and the two agree by construction. Bounded at 200 rows, as the
overview always was; past that the honest answer is an aggregate
endpoint, not a bigger limit.
What the badge counts is what needs a PERSON — phaseOf again, so a lead
whose documents are in and whose AI chain is running is reported beside
the badge rather than inside it. An amber dot marks a queue holding a
renewal inside a week, which is the only reason to open one queue before
another and was previously invisible. Zero is shown rather than hidden:
"nothing here" is an answer, and a queue that disappears when it empties
makes the sidebar move under the cursor.
Closed is folded into a summary. Three of the twelve queues, opened about
once a week, and at equal weight they made the live ones harder to find.
REASONING OPENS IN A DIALOG, NOT INLINE. The audit rail is 320px wide
with one entry per step, and a 1,500-character rationale expanding in
place pushed a lead's whole history off screen to read one sentence of
it. Nobody reads a paragraph in a sidebar. The finding — the AI's own
first sentence, which is already the conclusion — stays on the line; the
working is one click away and one Escape back, headed by what is being
read ("Call — Log Contact") rather than by nothing. Escape closes, the
page behind does not scroll, and focus returns to the button that opened
it so a keyboard reader keeps their place.
Two headings that described the container rather than the contents:
Record → "Lead file", and the audit trail's subtitle now says what a
reader gets from it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four operator-reported issues, one commit because two of them are about
the same form.
ALL LEADS WAS GONE. The team's rewrite of the queue screen dropped the
/stage/all view, so finding a lead meant guessing which of nine queues
it sat in. Restored: no stage filter, closed leads dropped client-side,
one request for the whole open book. Linked under Overview in the
sidebar so it is one click from anywhere.
THE DOCUMENT FORM ASKED FOR WHAT THE LEAD ALREADY KNEW. Collect
Documents mirrors fourteen record fields — registration, make and model,
previous insurer, expiry, policy number, PAN — so the OCR has somewhere
to write. Rendered blank, they read as fourteen more things to type, on
a form whose whole point is uploading three files. They are now seeded
from the lead by base key: an agent uploading for KA01MF6618 sees
KA01MF6618 already there. Server prefill wins, then anything typed;
files and generated ids are never seeded, and an INIT form has no
record to seed from. SME-only fields were already hidden on a motor
lead by fieldLine — that half was working.
"SEND A REMINDER" IS THE SCHEDULER'S JOB, NOT A BUTTON. It fires at
24h, 72h and 120h on its own. Offering it as a loop action invited a
person to send a reminder the platform was already about to send. It
and Retry Call move to `force` — reachable behind "Lead not moving?"
for the day the schedule itself fails, and otherwise out of sight.
THE UPLOAD STAYED THE "NEXT STEP" AFTER THE UPLOAD. Once
documents_status is complete the lead remains in Document Pending while
Engage captures the risk and the quote is built — and the panel kept
saying "Upload documents", which reads as though nothing was received.
Now, when complete, the upload becomes a loop (a fourth document, a
correction) and the panel says what is actually happening: documents
received, Engage AI is capturing the risk and preparing the quote.
Permissions unchanged; ACTIONS still matches
tbl_wf_state_allowed_activities exactly.
Four things, and the first is the one that mattered.
ROLE-BASED ACCESS. The console showed every action to everyone on
purpose — "the refusal is the demo" — and the workflow refused
server-side. That is a defensible engineering position and a poor
product: an underwriter saw a row of six buttons, five of which 403.
src/api/permissions.js now mirrors tbl_wf_activity_permissions, and the
session already carries user.roles, so nothing new had to be fetched.
Action buttons, sidebar queues and the entry doors are all filtered.
It remains presentation only — the platform still refuses, and a role
added there but missing here hides a button that would have worked,
which is the failure mode to watch.
Each role's app now looks like their job. Ops sees the whole board and
still cannot clear a referral. An underwriter sees one queue and two
buttons. Agents see the three queues they work in. Reporting is NOT
gated: the overview counts the whole portfolio for everyone, because an
agent tracking leads they filed is reasonable and acting on them is not.
One deliberate divergence from the workflow, commented where it lives:
the two scheduled activities carry no roles at all, because that is the
only configuration under which the scheduler can perform them. Open to
the SYSTEM is not open to everyone signing in, so the UI narrows them —
otherwise an underwriter is offered "Send Document Reminder".
ANALYTICS. The overview was three lists. It is now a measured page:
open leads, pipeline value, written premium, conversion, commission —
then renewal exposure in three buckets, the queues needing a person with
the age of the oldest item in each, and stage distribution as bars.
Checked against the live book rather than assumed, which found a real
bug before it shipped: Policy Issued is written business AND not yet
terminal, so its premium was counted in pipeline and production both.
₹42,912 double counted on 37 leads. Pipeline now excludes anything
already on risk.
The lead page gained the same treatment: renewal countdown, lead age,
time in current stage, premium, commission, AI confidence. Age and
dwell are computed — neither is on the record, and they are the two
figures that answer "is this moving?", which no field could.
TERMINOLOGY. The previous pass over-corrected: fixing builder jargon
("the workflow decides, server-side") produced chat ("What you can do",
"The agents have it", "Nothing here right now"). Neither is how an
operations console reads. Now: Actions · Record · Audit trail · Action
required · Customer response · Automated · Scheduled · Document
collection · Underwriting referral · Premium confirmation.
LAYOUT. Prose subtitles cut to one line or removed. Measures render as
tabular figures in a bordered strip instead of label/value pairs. The
overview splits into work on the left and distribution on the right.
Stage bars replace a wall of equal-weight cards.
Pre-existing lint errors unchanged at 11; none in the new files.
Three problems, and the first one is the reason the other two were hard
to see.
THERE WAS NO DASHBOARD. "/" redirected to the underwriting queue — one
stage, usually empty, and belonging to somebody else. Every screen in
the app answered "show me this queue"; none answered "how is the book
doing, and is anything waiting on me?", which is the only question a
person has before they have picked a queue.
So: a Today page. Three blocks, in the order somebody cares about them.
What is waiting on a person, as three cards that colour only when they
have something in them. Renewals running out, worst first, capped at 30
days because a renewal six months away is not a thing to look at today.
Then every stage with its count, zeroes included — a stage quietly
receiving nothing is only visible if its zero is on screen.
It is one list call, counted in the browser. That is honest at this size
and it is the same call the queues already make. If the book outgrows it
the answer is a counts endpoint, not a bigger page.
THE WORDS WERE WRITTEN FOR WHOEVER BUILT THE WORKFLOW. "What happens
next" was followed by "these are the only activities this state allows
— but the workflow decides, server-side, whether you may". A failed list
said "this queue has no record view yet". Panels were called "Flow
details". A closed lead read "nothing runs from here". None of that is
wrong; all of it is addressed to the wrong reader.
Now: "What you can do", "Lead details", "History", "This queue could not
be loaded". Queue names say what they want — "Documents needed",
"Underwriter to review", "Payment to confirm". The sidebar group called
"Not yet" says "Not due yet", and "Running by itself" says "The agents
have it", which is the actual claim being made.
THE QUEUE HAD NINE COLUMNS AND LED WITH THE WRONG ONE. Two were internal
vocabulary: attribution status renders "clear" or "contested" and means
nothing to an operator, and the channel is background rather than
something anyone scans a queue for. Both fold into a subtitle under the
customer. That leaves six columns and puts the renewal countdown — the
number that decides whether to act today — second instead of fifth.
Also: the timeline printed a raw slug when a step came back without a
name. It is the screen this product is demonstrated on, so a
"zk-act-doc-reminder" in the middle of an otherwise readable story is
expensive. It now reads as English.
The pipeline could be looked at but not driven. Adds the three pieces
that make the machine walkable end to end from the UI.
- ActivityForm renders whatever /view/form-screens returns — labels,
types, select options, mandatory flags — and submits it back. No form
is defined in this repo, so a field added in Studio appears here with
no code change.
- AddLead presents the three doors. Each is a separate INIT activity with
its own permissions; the bank one lands further along because the bank
already did CKYC.
- Lead shows the file grouped in the order it was worked, ending with
what the sourcing agent earns, plus the activities this state allows
and who normally performs each.
The "who normally performs this" label is presentational only. Nothing
here enforces anything — the workflow refuses server-side and the form
reports what it said, including a note when a 403 is the platform
declining rather than the console misbehaving.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
RBAC is what this demo argues, so which persona you are signed in as
should never be a guess while presenting.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scaffold had the brand right and nothing behind it: the login was a
600ms setTimeout with a TODO, and three pieces of the frontgen deploy
contract were missing.
- Sign in against POST /usr/login (org_id as a STRING — a number is
rejected by the gateway) with the session in a provider; surface the
gateway's own message rather than a generic "invalid credentials",
because a wrong password and a user without access to this app look
identical from here and are not.
- Runtime config: the API URL is read from the config.js the server
writes at placement, never compiled in, and requireConfigValue throws
so a build with no config.js fails loudly instead of calling whichever
backend built it.
- base: './' plus a router basename taken from <base href>, so one build
serves any mount path.
- Move the fonts and logo from public/ into src/assets/ — Vite rewrites
bundled asset URLs to be relative, while a public/ file referenced as
"/fonts/..." stays absolute and 404s under the /zurich-kotak/ mount.
- API client for recordview / detailview / form-screens / start /
activity, and the pipeline shell whose sidebar is the state machine in
the order a lead moves.
Queue and lead-file data wait on the record and detail views.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>