console: the surface pass — display face, depth, and whose move it is

The layout was right but the surface still read as a flat corporate form. Three
levers, all from the approved mockup:

- A DISPLAY FACE (Bricolage Grotesque) on the things the eye lands on — the
  lead name, panel headings, and every figure. Zurich Sans stays the body
  voice; this gives the numbers and titles the tighter, more current character
  the plain grotesque could not. Loaded from Google Fonts with Zurich Sans as
  the fallback, so a blocked CDN degrades to the brand face rather than breaking.

- DEPTH. Panels and the metric tiles lift from the barely-there --sh-sm to a
  real --sh-md with rounder corners, so cards read as objects on a surface
  rather than boxes drawn on paper.

- WHOSE MOVE IT IS. The header said "STAGE / Quote Presented" — a label. It now
  carries a coloured pill: "Right now · With the customer" (teal), "With the AI"
  (blue), "Waiting on a person" (amber). The stage name is in the rail below;
  the pill answers the question an operator actually opens a lead to ask.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yashas 2026-09-09 01:05:20 +05:30
parent 900f34d11e
commit 83b5795b22
4 changed files with 129 additions and 18 deletions

View File

@ -13,6 +13,13 @@
<!--BASE_HREF--> <!--BASE_HREF-->
<link rel="icon" type="image/webp" href="./brand/zurich_logo.webp" /> <link rel="icon" type="image/webp" href="./brand/zurich_logo.webp" />
<title>Zurich Kotak | Lead Desk</title> <title>Zurich Kotak | Lead Desk</title>
<!-- Display face for headings and figures. Zurich Sans (the brand face,
loaded from ./brand) stays the body voice; this gives the numbers and
titles the tighter, more current character the flat corporate grotesque
could not. Falls back to Zurich Sans if the CDN is blocked. -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700&display=swap" />
<!-- Written next to the artifact at placement; carries this environment's <!-- Written next to the artifact at placement; carries this environment's
VITE_ZINO_API_URL, which src/runtimeConfig.js reads off VITE_ZINO_API_URL, which src/runtimeConfig.js reads off
window.__RUNTIME_CONFIG__. Without it requireConfigValue throws and the window.__RUNTIME_CONFIG__. Without it requireConfigValue throws and the

View File

@ -30,6 +30,11 @@
--zk-blue-mid: #5495cf; --zk-blue-mid: #5495cf;
--zk-blue-light: #91bfe3; --zk-blue-light: #91bfe3;
--zk-cyan: #c7ebf9; --zk-cyan: #c7ebf9;
--zk-teal: #0e7c74;
--zk-teal-ink: #0b5f59;
--zk-teal-tint: #e3f4f2;
--zk-teal-line: #a5d8d3;
--zk-good: #1f9d63;
--zk-ink: #2b2e31; --zk-ink: #2b2e31;
--zk-muted: #5d6162; --zk-muted: #5d6162;
@ -119,6 +124,7 @@
--sp-8: 40px; --sp-8: 40px;
--sp-9: 56px; --sp-9: 56px;
--font-display: 'Bricolage Grotesque', 'Zurich Sans', 'Trebuchet MS', sans-serif;
--font-zurich: 'Zurich Sans', Arial, Helvetica, sans-serif; --font-zurich: 'Zurich Sans', Arial, Helvetica, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace; --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;

View File

@ -169,6 +169,25 @@ function fmt(k, v) {
return String(v) return String(v)
} }
/**
* Whose move it is, right now the question an operator opens a lead to
* answer, said as a coloured pill in the header. Derived from the stage's own
* kind, so it stays in step with the workflow: an AI-run stage is with the AI,
* a customer-gated one is with the customer, a person-gated one is waiting on
* someone, and a closed lead holds nobody.
*/
function holdsOf(stage) {
if (!stage) return null
switch (stage.kind) {
case 'auto': return { tone: 'ai', label: 'With the AI' }
case 'customer': return { tone: 'grey', pillTone: 'cust', label: 'With the customer' }
case 'needs': return { tone: 'person', label: stage.approval ? 'Awaiting your approval' : 'Waiting on a person' }
case 'waiting': return { tone: 'grey', label: 'In nurture' }
case 'end': return { tone: 'grey', label: 'Closed' }
default: return null
}
}
export default function Lead() { export default function Lead() {
const { instanceId } = useParams() const { instanceId } = useParams()
const { client, user } = useZino() const { client, user } = useZino()
@ -295,6 +314,7 @@ export default function Lead() {
// The state, then what is really happening inside it see phaseOf. Actions // The state, then what is really happening inside it see phaseOf. Actions
// are still looked up on the state's own uid; only the presentation moves. // are still looked up on the state's own uid; only the presentation moves.
const stage = phaseOf(STAGES.find((s) => s.name === stateName), row) const stage = phaseOf(STAGES.find((s) => s.name === stateName), row)
const holds = holdsOf(stage)
// Only what THIS user may run. The workflow refuses the rest server-side // Only what THIS user may run. The workflow refuses the rest server-side
// anyway; showing them a row of buttons that all 403 reads as a broken app // anyway; showing them a row of buttons that all 403 reads as a broken app
// rather than as a control. // rather than as a control.
@ -342,7 +362,6 @@ export default function Lead() {
}, 0) }, 0)
const isClosed = stage?.kind === 'end' const isClosed = stage?.kind === 'end'
const heldByOthers = !isClosed && actions.length === 0
// An automated stage that has stopped for a stated reason. Checked before // An automated stage that has stopped for a stated reason. Checked before
// the "in progress" strip below, which would otherwise keep promising that // the "in progress" strip below, which would otherwise keep promising that
// something is happening for as long as the lead is left alone. // something is happening for as long as the lead is left alone.
@ -384,20 +403,28 @@ export default function Lead() {
</p> </p>
</div> </div>
</div> </div>
<div className="lead__stage"> {/* WHOSE MOVE IT IS. The stage name is in the rail below; this says
<span className="lead__stagelabel">Stage</span> who holds the lead right now, which is the question the header is
<strong className={stage?.kind === 'end' ? 'is-closed' : undefined}>{stateName || '—'}</strong> really being asked. The customer tone reuses the teal cust palette. */}
{/* An end state runs no activity. That is worth one line next to the {holds ? (
stage, not a panel of its own where the actions would be. */} <div className={'holds holds--' + (holds.pillTone === 'cust' ? 'cust' : holds.tone)}>
{isClosed ? <span className="lead__closed">Closed no further activity</span> : null} <span className="holds__puck" aria-hidden="true">
{heldByOthers ? ( {holds.tone === 'ai' ? (
<span className="lead__closed"> <svg width="20" height="20" viewBox="0 0 20 20"><path d="M6 3a2 2 0 0 0-2 2 2 2 0 0 0-1 3.6A2 2 0 0 0 4 12a2 2 0 0 0 2 2M14 3a2 2 0 0 1 2 2 2 2 0 0 1 1 3.6A2 2 0 0 1 16 12a2 2 0 0 1-2 2M10 3.5v12" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
{stage?.kind === 'auto' ) : holds.pillTone === 'cust' ? (
? `In progress with ${stage.by}` <svg width="20" height="20" viewBox="0 0 20 20"><path d="M4 5h12v8h-6l-3 2.5V13H4z" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round"/></svg>
: `Assigned to ${stage?.by ?? 'another role'}`} ) : holds.tone === 'person' ? (
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="6.5" r="2.8" fill="none" stroke="currentColor" strokeWidth="1.5"/><path d="M4.5 15.5a5.5 5.5 0 0 1 11 0" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/></svg>
) : (
<svg width="20" height="20" viewBox="0 0 20 20"><path d="M10 5.5v4.5l3 2" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/><circle cx="10" cy="10" r="6.5" fill="none" stroke="currentColor" strokeWidth="1.5"/></svg>
)}
</span>
<span className="holds__txt">
<small>Right now</small>
<b>{holds.label}</b>
</span> </span>
) : null}
</div> </div>
) : null}
</header> </header>
{/* Measures, not fields. Age and dwell time are computed neither is on {/* Measures, not fields. Age and dwell time are computed neither is on

View File

@ -311,8 +311,8 @@
.panel { .panel {
background: var(--zk-white); background: var(--zk-white);
border: 1px solid var(--zk-line-soft); border: 1px solid var(--zk-line-soft);
border-radius: var(--r-lg); border-radius: var(--r-xl);
box-shadow: var(--sh-sm); box-shadow: var(--sh-md);
padding: var(--sp-7); padding: var(--sp-7);
margin-bottom: var(--sp-5); margin-bottom: var(--sp-5);
} }
@ -1091,8 +1091,8 @@
padding: var(--sp-6) var(--sp-7); padding: var(--sp-6) var(--sp-7);
background: var(--zk-white); background: var(--zk-white);
border: 1px solid var(--zk-line-soft); border: 1px solid var(--zk-line-soft);
border-radius: var(--r-lg); border-radius: var(--r-xl);
box-shadow: var(--sh-sm); box-shadow: var(--sh-md);
margin-bottom: var(--sp-5); margin-bottom: var(--sp-5);
} }
@ -1719,3 +1719,74 @@
color: var(--zk-grey); color: var(--zk-grey);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
/* modern surface pass
The layout was right but the surface still read as a flat corporate form.
Two levers close most of the gap: a display face on the things the eye
lands on the page title, panel headings and every figure and one warm,
coloured signal for who holds the lead right now. */
.page__title,
.panel__title,
.lead__stage strong,
.lm__v,
.kpi__v,
.now__v,
.fig .v {
font-family: var(--font-display);
letter-spacing: -0.02em;
}
.page__title { letter-spacing: -0.03em; }
/* WHO HOLDS THE LEAD, as a coloured pill the first thing the eye should
catch. The stage name lives in the rail below; this says whose move it is,
which is the question an operator actually opens a lead to answer. */
.holds {
flex: none;
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-2) var(--sp-5) var(--sp-2) var(--sp-2);
border-radius: var(--r-pill);
border: 1px solid var(--zk-teal-line);
background: var(--zk-teal-tint);
}
.holds__puck {
flex: none;
width: 40px; height: 40px;
border-radius: 50%;
display: grid; place-items: center;
background: var(--zk-teal); color: #fff;
}
.holds__txt { display: flex; flex-direction: column; line-height: 1.15; }
.holds__txt small {
font-size: var(--fs-3xs);
font-weight: var(--fw-semi);
letter-spacing: .09em;
text-transform: uppercase;
color: var(--zk-teal-ink);
opacity: .9;
}
.holds__txt b {
font-family: var(--font-display);
font-weight: var(--fw-semi);
font-size: var(--fs-md);
color: var(--zk-ink);
}
.holds--ai { background: var(--zk-tint-blue); border-color: var(--zk-blue-light); }
.holds--ai .holds__puck { background: var(--zk-blue); }
.holds--ai .holds__txt small { color: var(--zk-blue-dark); }
.holds--person { background: var(--zk-amber-tint); border-color: var(--zk-amber-line); }
.holds--person .holds__puck { background: var(--zk-amber); }
.holds--person .holds__txt small { color: var(--zk-amber-ink); }
.holds--grey { background: var(--zk-tint); border-color: var(--zk-line); }
.holds--grey .holds__puck { background: var(--zk-grey); }
.holds--grey .holds__txt small { color: var(--zk-muted); }
.holds--cust { background: var(--zk-teal-tint); border-color: var(--zk-teal-line); }
.holds--cust .holds__puck { background: var(--zk-teal); }
.holds--cust .holds__txt small { color: var(--zk-teal-ink); }