Compare commits

..

No commits in common. "ee57184fa2391663e1237b4a6003d9d6eb611208" and "e21e3949f50594a39bc700c3b25ff7895dcb532f" have entirely different histories.

5 changed files with 159 additions and 181 deletions

View File

@ -18,7 +18,7 @@
kept in the fallback stack, and Geist Mono carries refs and figures. --> kept in the fallback stack, and Geist Mono carries refs and figures. -->
<link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=Geist+Mono:wght@400;500&display=swap" /> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&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

@ -5,12 +5,11 @@
align-items: center; align-items: center;
gap: var(--sp-6); gap: var(--sp-6);
flex-wrap: wrap; flex-wrap: wrap;
margin: 0 0 var(--sp-5); margin: var(--sp-6) 0;
padding: var(--sp-6) var(--sp-7); padding: var(--sp-2) 0 var(--sp-5);
border: 1px solid var(--zk-line-soft); border: 0;
border-radius: var(--r-md); border-bottom: 1px solid var(--zk-line);
box-shadow: var(--sh-sm); background: transparent;
background: var(--zk-white);
} }
.rail ol { .rail ol {

View File

@ -22,69 +22,64 @@
} }
:root { :root {
/* PALETTE assured-inspired /* Brand palette lifted from zurichkotak.com */
A clean white ground, warm near-black ink, and ONE confident accent: --zk-blue: #2167ae;
a magenta-pink, reserved for action and active state. The token names --zk-blue-dark: #1a5490;
keep their history (the accent still lives under the --zk-blue-* names --zk-blue-deep: #1a528b;
the whole app references), but the values are the new scheme. Teal is --zk-navy: #23366f;
the customer, amber a person; the pink is "act / here", nothing else. */ --zk-blue-mid: #5495cf;
--zk-blue: #e5227f; /* THE ACCENT (was brand blue) — pink */ --zk-blue-light: #91bfe3;
--zk-blue-dark: #c30f68; --zk-cyan: #c7ebf9;
--zk-blue-deep: #c30f68; --zk-teal: #0e7c74;
--zk-navy: #2a1d3d; /* a deep plum-ink for the logo / darkest text */ --zk-teal-ink: #0b5f59;
--zk-blue-mid: #ee5fa0; --zk-teal-tint: #e3f4f2;
--zk-blue-light: #f6cfe3; /* pink hairline / ring */ --zk-teal-line: #a5d8d3;
--zk-cyan: #fdebf4; /* pink-soft */ --zk-good: #1f9d63;
--zk-teal: #0e9e8f;
--zk-teal-ink: #0a6b60;
--zk-teal-tint: #e2f5f2;
--zk-teal-line: #bfe6e1;
--zk-good: #18a558;
--zk-ink: #1c1626; /* warm near-black */ --zk-ink: #2b2e31;
--zk-muted: #645d72; --zk-muted: #5d6162;
--zk-grey: #a49dae; --zk-grey: #a6adaf;
--zk-line: #e3e1ea; --zk-line: #dde4e3;
--zk-line-soft: #ececf1; --zk-line-soft: #eceeef;
--zk-tint: #faf9fb; /* off-white section ground */ --zk-tint: #f4f9fc;
--zk-sunk: #f5f3f8; /* a touch deeper — hover / inset */ --zk-tint-blue: #ebf3fb;
--zk-tint-blue: #fdebf4; /* pink-soft (was tint-blue) */
--zk-white: #fff; --zk-white: #fff;
--zk-danger: #d64545; --zk-danger: #d32f2f;
--zk-amber: #c77d1a; /* Amber and red accents already used for people / lapsed renewals, named so
--zk-amber-ink: #9a5d10; they stop being magic hex literals scattered across four stylesheets. */
--zk-amber-tint: #fbf0dd; --zk-amber: #b5761f;
--zk-amber-line: #ecd3a3; --zk-amber-ink: #93500f;
--zk-danger-ink: #b23636; --zk-amber-tint: #fdf1e7;
--zk-danger-tint: #fbeaea; --zk-amber-line: #f0c69a;
--zk-danger-line: #f0c9c9; --zk-danger-ink: #a3261f;
--zk-danger-tint: #fdeceb;
--zk-danger-line: #f0c2bd;
/* Shape, depth, motion /* Shape, depth, motion
Rounder than before and softer shadows the assured cards read as One scale, used everywhere. The palette above is untouched; every shadow
objects that float a little, on a warm plum-ink low alpha rather than below is the brand navy at low alpha, so depth reads as part of the brand
generic grey haze. */ rather than as generic grey haze. */
--r-xs: 8px; --r-xs: 6px;
--r-sm: 10px; --r-sm: 8px;
--r-md: 14px; --r-md: 12px;
--r-lg: 18px; --r-lg: 16px;
--r-xl: 22px; --r-xl: 20px;
--r-pill: 999px; --r-pill: 999px;
--sh-xs: 0 1px 2px rgba(28, 22, 38, 0.04); --sh-xs: 0 1px 2px rgba(35, 54, 111, 0.05);
--sh-sm: 0 1px 2px rgba(28, 22, 38, 0.04), 0 4px 14px -8px rgba(28, 22, 38, 0.10); --sh-sm: 0 1px 2px rgba(35, 54, 111, 0.05), 0 2px 6px -2px rgba(35, 54, 111, 0.07);
--sh-md: 0 1px 2px rgba(28, 22, 38, 0.04), 0 8px 24px -10px rgba(28, 22, 38, 0.12); --sh-md: 0 1px 2px rgba(35, 54, 111, 0.05), 0 10px 24px -8px rgba(35, 54, 111, 0.14);
--sh-lg: 0 2px 6px rgba(28, 22, 38, 0.06), 0 24px 48px -16px rgba(28, 22, 38, 0.22); --sh-lg: 0 2px 6px rgba(35, 54, 111, 0.06), 0 20px 42px -14px rgba(35, 54, 111, 0.22);
--sh-accent: 0 6px 20px -6px rgba(229, 34, 127, 0.42);
--ring: 0 0 0 3px rgba(229, 34, 127, 0.16); --ring: 0 0 0 3px rgba(33, 103, 174, 0.16);
--ease: cubic-bezier(0.4, 0, 0.2, 1); --ease: cubic-bezier(0.4, 0, 0.2, 1);
--t-fast: 0.14s var(--ease); --t-fast: 0.14s var(--ease);
--t: 0.2s var(--ease); --t: 0.2s var(--ease);
--grad-blue: linear-gradient(135deg, var(--zk-blue) 0%, var(--zk-blue-dark) 100%); --grad-blue: linear-gradient(135deg, var(--zk-blue) 0%, var(--zk-blue-deep) 100%);
--grad-blue-hover: linear-gradient(135deg, var(--zk-blue-dark) 0%, #a80c59 100%); --grad-blue-hover: linear-gradient(135deg, var(--zk-blue-dark) 0%, var(--zk-navy) 100%);
/* TYPE SCALE /* TYPE SCALE
The app had FIFTY-FOUR distinct font sizes, and forty of them sat The app had FIFTY-FOUR distinct font sizes, and forty of them sat
@ -119,8 +114,6 @@
--fw-normal: 400; --fw-normal: 400;
--fw-medium: 500; --fw-medium: 500;
--fw-semi: 600; --fw-semi: 600;
--fw-bold: 700;
--fw-x: 800;
/* SPACING /* SPACING
One rhythm. Panels breathe at 24-28px rather than 14-18px, which is One rhythm. Panels breathe at 24-28px rather than 14-18px, which is
@ -138,8 +131,8 @@
/* Display and body are the SAME brand face Zurich Sans has no bold, /* Display and body are the SAME brand face Zurich Sans has no bold,
so headings and figures lean on weight 500 and tighter tracking, set per so headings and figures lean on weight 500 and tighter tracking, set per
use, not on a second family. */ use, not on a second family. */
--font-display: 'Plus Jakarta Sans', 'Geist', system-ui, sans-serif; --font-display: 'Geist', 'Zurich Sans', system-ui, sans-serif;
--font-zurich: 'Plus Jakarta Sans', 'Geist', system-ui, -apple-system, sans-serif; --font-zurich: 'Geist', 'Zurich Sans', system-ui, -apple-system, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace; --font-mono: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
font-family: var(--font-zurich); font-family: var(--font-zurich);

View File

@ -301,6 +301,81 @@ export default function Lead() {
const stage = phaseOf(STAGES.find((s) => s.name === stateName), row) const stage = phaseOf(STAGES.find((s) => s.name === stateName), row)
const holds = holdsOf(stage) const holds = holdsOf(stage)
/* WHAT IS HAPPENING NOW, lifted above the numbers. An operator's first
question about a lead is "what is going on / what do I do", not "what is
the premium" so the status strip (an alert when blocked or stalled, the
live state otherwise) sits directly under the header, before the metrics.
Rendered full-width there rather than inside the narrow work column. */
const statusStrip = (
blocked ? (
<div className="doing doing--blocked">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>Stopped {blocked.what}</strong>
<span>
{blocked.fix} The agents will pick the lead up again on their own once it
is there; nothing else is running in the meantime.
</span>
<button
type="button" className="doing__fix"
onClick={() => setOpen(blocked.via)}
>
Open Collect Documents
</button>
</div>
</div>
) : stalled ? (
<div className="doing doing--stalled">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>{stage.doing} nothing for {stalled.mins} minutes</strong>
<span>
{stage.by} has not come back. That is usually the model provider being
slow or dropping a request, not a problem with this lead the work so far
is safe and nothing has been lost.
{stalled.nudge
? ` Running ${stalled.nudge.label.toLowerCase()} wakes ${stalled.nudge.by} to try again.`
: ' There is no step to re-run from here; the actions below are the way on.'}
</span>
{stalled.nudge ? (
<button
type="button" className="doing__fix"
onClick={() => setOpen(stalled.nudge.uid)}
>
{stalled.nudge.label}
</button>
) : null}
</div>
</div>
) : stage?.kind === 'customer' && stage.doing ? (
/* Not an agent working, and not a task of yours either the lead
is with someone outside the business. Without this the screen
went blank at exactly the stage where an operator is most likely
to wonder whether something has broken. */
<div className="doing doing--waiting">
<span className="doing__wait" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Their answer comes back to this lead on its own Engage reads it and
records the acceptance. Nothing is waiting on you.
</span>
</div>
</div>
) : stage?.kind === 'auto' ? (
<div className="doing">
<span className="doing__pulse" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Handled by {stage.by}.
{stage.after ? ` The lead stays in ${stateName} until a quote exists — nothing is waiting on you.` : ''}
{' '}Typically completes within two minutes; this view refreshes automatically.
</span>
</div>
</div>
) : null
)
// 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.
@ -379,83 +454,6 @@ export default function Lead() {
// the way out rather than going on promising two minutes. // the way out rather than going on promising two minutes.
const stillFor = row.updated_at ? now - Date.parse(row.updated_at) : 0 const stillFor = row.updated_at ? now - Date.parse(row.updated_at) : 0
const stalled = !blocked && stage?.kind === 'auto' && stillFor > STALL_AFTER_MS const stalled = !blocked && stage?.kind === 'auto' && stillFor > STALL_AFTER_MS
/* WHAT IS HAPPENING NOW, lifted above the numbers. An operator's first
question about a lead is "what is going on / what do I do", not "what is
the premium" so the status strip (an alert when blocked or stalled, the
live state otherwise) sits directly under the header, before the metrics.
Rendered full-width there rather than inside the narrow work column. */
const statusStrip = (
blocked ? (
<div className="doing doing--blocked">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>Stopped {blocked.what}</strong>
<span>
{blocked.fix} The agents will pick the lead up again on their own once it
is there; nothing else is running in the meantime.
</span>
<button
type="button" className="doing__fix"
onClick={() => setOpen(blocked.via)}
>
Open Collect Documents
</button>
</div>
</div>
) : stalled ? (
<div className="doing doing--stalled">
<span className="doing__stop" aria-hidden="true" />
<div>
<strong>{stage.doing} nothing for {stalled.mins} minutes</strong>
<span>
{stage.by} has not come back. That is usually the model provider being
slow or dropping a request, not a problem with this lead the work so far
is safe and nothing has been lost.
{stalled.nudge
? ` Running ${stalled.nudge.label.toLowerCase()} wakes ${stalled.nudge.by} to try again.`
: ' There is no step to re-run from here; the actions below are the way on.'}
</span>
{stalled.nudge ? (
<button
type="button" className="doing__fix"
onClick={() => setOpen(stalled.nudge.uid)}
>
{stalled.nudge.label}
</button>
) : null}
</div>
</div>
) : stage?.kind === 'customer' && stage.doing ? (
/* Not an agent working, and not a task of yours either the lead
is with someone outside the business. Without this the screen
went blank at exactly the stage where an operator is most likely
to wonder whether something has broken. */
<div className="doing doing--waiting">
<span className="doing__wait" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Their answer comes back to this lead on its own Engage reads it and
records the acceptance. Nothing is waiting on you.
</span>
</div>
</div>
) : stage?.kind === 'auto' ? (
<div className="doing">
<span className="doing__pulse" aria-hidden="true" />
<div>
<strong>{stage.doing}</strong>
<span>
Handled by {stage.by}.
{stage.after ? ` The lead stays in ${stateName} until a quote exists — nothing is waiting on you.` : ''}
{' '}Typically completes within two minutes; this view refreshes automatically.
</span>
</div>
</div>
) : null
)
? { mins: Math.round(stillFor / 60000), nudge: nudgeFor(stage, row) } ? { mins: Math.round(stillFor / 60000), nudge: nudgeFor(stage, row) }
: null : null

View File

@ -10,7 +10,7 @@
.page__title { .page__title {
margin: 0; margin: 0;
font-size: var(--fs-2xl); font-size: var(--fs-2xl);
font-weight: var(--fw-x); font-weight: var(--fw-semi);
letter-spacing: -0.022em; letter-spacing: -0.022em;
line-height: 1.2; line-height: 1.2;
color: var(--zk-ink); color: var(--zk-ink);
@ -309,14 +309,14 @@
/* ---- panel ---- */ /* ---- panel ---- */
.panel { .panel {
/* CARD. The assured look is clean white cards that float a little on the /* FLAT. Was a shadowed white card; now a plain section on the white
off-white ground rounded, soft-shadowed, bounded. */ canvas, set off by its heading and by space, not by a box. */
background: var(--zk-white); background: transparent;
border: 1px solid var(--zk-line-soft); border: 0;
border-radius: var(--r-lg); border-radius: 0;
box-shadow: var(--sh-sm); box-shadow: none;
padding: var(--sp-6) var(--sp-7); padding: 0;
margin-bottom: var(--sp-5); margin-bottom: var(--sp-8);
} }
.panel__head { .panel__head {
@ -330,7 +330,7 @@
.panel__title { .panel__title {
margin: 0; margin: 0;
font-size: var(--fs-lg); font-size: var(--fs-lg);
font-weight: var(--fw-bold); font-weight: var(--fw-semi);
letter-spacing: -0.018em; letter-spacing: -0.018em;
line-height: var(--lh-tight); line-height: var(--lh-tight);
} }
@ -404,14 +404,12 @@
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 14px; gap: 14px;
margin-bottom: var(--sp-5); margin-bottom: 22px;
padding: var(--sp-4) var(--sp-5); padding: 16px 20px;
border-radius: var(--r-md); border-radius: var(--r-lg);
border: 1px solid var(--zk-teal-line); border: 1px solid var(--zk-blue-light);
background: var(--zk-teal-tint); background: var(--zk-tint-blue);
color: var(--zk-teal-ink);
} }
.doing strong { color: var(--zk-teal-ink); }
.doing div { .doing div {
display: flex; display: flex;
@ -620,9 +618,9 @@
/* Rail panels are tighter than main panels a 340px measure cannot afford /* Rail panels are tighter than main panels a 340px measure cannot afford
32px of padding on each side. */ 32px of padding on each side. */
.lead__side .panel { padding: var(--sp-5) var(--sp-6); margin-bottom: var(--sp-4); } .lead__side .panel { padding: 0; margin-bottom: var(--sp-6); }
.lead__side .panel > .panel__head { padding: 0; margin-bottom: var(--sp-4); } .lead__side .panel > .panel__head { padding: 0; margin-bottom: var(--sp-4); }
.lead__side .panel--crew { border-bottom: 0; } .lead__side .panel--crew { padding: 0 0 var(--sp-6); border-bottom: 1px solid var(--zk-line); }
/* rail headings sit at label size, not the big panel-title size the rail is /* rail headings sit at label size, not the big panel-title size the rail is
reference, and a 18px "Customer" heading would shout over the facts. */ reference, and a 18px "Customer" heading would shout over the facts. */
.lead__side .panel__title { .lead__side .panel__title {
@ -1109,26 +1107,16 @@
22px against 12px labels. Premium and commission the two numbers 22px against 12px labels. Premium and commission the two numbers
anyone actually came for are bigger again. */ anyone actually came for are bigger again. */
.lmetrics { .lmetrics {
display: grid; display: flex;
grid-template-columns: repeat(4, 1fr); flex-wrap: wrap;
gap: var(--sp-4); gap: var(--sp-6) var(--sp-8);
margin-bottom: var(--sp-5); padding: var(--sp-6) 0;
border-top: 1px solid var(--zk-line);
border-bottom: 1px solid var(--zk-line);
margin-bottom: 0;
} }
.lm { .lm { min-width: 0; }
min-width: 0;
background: var(--zk-white);
border: 1px solid var(--zk-line-soft);
border-radius: var(--r-md);
box-shadow: var(--sh-sm);
padding: var(--sp-5);
}
/* Money leads: the two figures a renewal is about sit in the accent tint. */
.lm--hero { background: var(--zk-tint-blue); border-color: var(--zk-blue-light); }
.lm--hero .lm__v { color: var(--zk-blue-dark); }
.lm--hero .lm__l { color: var(--zk-blue-dark); }
@media (max-width: 1040px) { .lmetrics { grid-template-columns: 1fr 1fr; } }
.lm__l { .lm__l {
display: block; display: block;
@ -1144,7 +1132,7 @@
display: block; display: block;
margin-bottom: var(--sp-1); margin-bottom: var(--sp-1);
font-size: var(--fs-xl); font-size: var(--fs-xl);
font-weight: var(--fw-x); font-weight: var(--fw-semi);
line-height: var(--lh-tight); line-height: var(--lh-tight);
color: var(--zk-navy); color: var(--zk-navy);
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
@ -1833,17 +1821,17 @@
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: var(--sp-3); gap: var(--sp-3);
padding: var(--sp-3) var(--sp-5) var(--sp-3) var(--sp-5); padding: var(--sp-3) var(--sp-4) var(--sp-3) var(--sp-5);
border: 0; border: 0;
border-radius: var(--r-md); border-radius: var(--r-md);
background: var(--zk-blue); background: var(--zk-amber);
color: #fff; color: #fff;
cursor: pointer; cursor: pointer;
box-shadow: var(--sh-accent); box-shadow: var(--sh-sm);
transition: background var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast); transition: background var(--t-fast), box-shadow var(--t-fast), transform var(--t-fast);
} }
.cta:hover { background: var(--zk-blue-dark); transform: translateY(-1px); } .cta:hover { background: var(--zk-amber-ink); box-shadow: var(--sh-md); }
.cta:focus-visible { outline: 2px solid var(--zk-blue-dark); outline-offset: 2px; } .cta:focus-visible { outline: 2px solid var(--zk-amber-ink); outline-offset: 2px; }
.cta__txt { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; } .cta__txt { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; }
.cta__txt small { .cta__txt small {
font-size: var(--fs-3xs); font-size: var(--fs-3xs);