A true leasing funnel, and a page that stops shouting. Scoped against the live code on origin/main, the 2026-08-19 standup, and the design language PropFlow already wrote down. Decisions at the bottom are answerable on this page.
The instinct that the prospects pages feel choppy and over-coloured is right, and it is already answered three times over inside PropFlow. Nothing here invents a look. The work is applying decisions the team has made and this page never adopted.
Twelve principles and a six-colour status system, written during the Renewals redesign. Colour communicates meaning, not decoration. Dots before pills. The number is the hero. Clara owns purple.
docs/brand/dashboards-and-charts.md §4.3 — no rainbow legends past four colours, no orange, never colour alone. §4.2 — shade for magnitude, hue for category. Funnels: never more than five stages.
DashboardCard already stopped painting chrome with its identity colour. The surface next door de-rainbowed itself and prospects didn't follow.
The tell. DashboardCard.tsx still accepts identityColor — and deliberately ignores it: “no longer paints the card chrome — the active/hover treatment is neutral now.” Meanwhile MetricStripCell on prospects renders a per-tile identity dot straight off LOGO_SPECTRUM. Same team, same month, opposite answer.
“Too many colours” is not a vibe here — it counts. The list strip paints six decorative hues, one per tile, off a five-stop brand ramp that wraps, so Vacant and Signed collide on the same violet. The detail page carries nine hue families, including two purples by design, two greens, two ambers and two reds.
Twelve values, nine families. The doctrine's ceiling is four.
Every activity-log dot and connector is a distinct interpolated hue — nodeColor(i) = spectrumAt(i/(total-1)). A twelve-row log paints twelve colours down one rail, and the colour encodes nothing: it is the row's index.
The meeting already rejected the rainbow, out loud. Sean proposed red / green / purple per stage. Fede: “I think that's going to look really bad.” Gera: “maybe different shades of purple.” That is precisely §4.2 — shade for magnitude, not hue for decoration.
The repo holds two incompatible definitions of the funnel, and the prospects page is on the wrong one for what the standup asked for.
countFunnelStages(). Counts who is sitting at a stage right now, so “Applications” means awaiting review only. It exists to guarantee Sean's tie-out from the 7/16 founders call: active === inquiries + tours + applications, enforced structurally so no caller can drift it.
/leasing hub usesapplicationsInWindow(). Counts everyone who reached a stage in the window, off the sticky appliedAt. Fede fixed this exact bug there on 7/21.
“Requiring stage === APPLIED here made the funnel row read 0 the moment every applicant advanced.”
— the code comment on the fix the standup is asking for again
Sean's “10, with 1 waiting” is flow semantics. So this is a semantics swap, not a label change — and under flow the tie-out mathematically cannot hold, because a signed applicant counts in Applications but is no longer Active. A funnel is supposed to work that way: each stage counts everyone who reached it. But the tie-out is a deliberate founders-call invariant, so it has to be retired on purpose, not quietly broken.
They weren't dropped for taste. FunnelView.tsx says the math mixed point-in-time snapshots with period counters and produced “nonsense numbers like 7425% convert.” Percentages only become honest once every stage is on flow. One decision, both asks.
Rendered with the real Camellia numbers from the call: 60 days, 52 inquiries → 23 tours → 10 applications (1 awaiting) → 9 signed.
What's wrong beyond the colour: Applications reads 1, not 10 — the standup's exact complaint. “avg — days” is dead text, because avgDaysVacant is hard-coded null. And the tiles are a row of counters, not a funnel: nothing shows that 52 became 9.
Keep the six-cell shape. Delete every identity dot, following the DashboardCard precedent. Fix the Applications semantics and the dead sub-line. Clicking a tile expands into a funnel instead of a line chart.
For: smallest diff, lowest risk, one PR.
Against: still a row of counters. Answers “stop shouting” but not “I want to see the funnel.”
Put the funnel on top, demote the strip to a secondary row underneath.
This is the trap, and we have the receipt: the renewals detail page ships three renderings of one state — a progress %, a four-node rail, and four phase badges. It is the top finding against that page. Two representations of the same four numbers, twelve inches apart, is how that happened.
One hue, four tints. Magnitude is carried by shade and by bar width, never by a second colour — literally what §4.2 prescribes and what Gera proposed in the meeting. The only other colour is the amber flag on the one conversion genuinely below benchmark, which is colour earning its place.
Occupancy leaves the funnel and becomes a footer line. Vacant units aren't part of the prospect flow — Gera and Fede both said so, and the five-stage ceiling says the same thing from the other side. It stays on the page; it stops pretending to be a stage.
Clicking a stage is the drill-down you asked for. FunnelView already lazily fetches the real people behind each count from /api/leasing/pipeline-members and lists them inline. The stage also filters the table below, so the funnel and the list can never disagree.
The conversion insight is the page's one insight. Tour→application at 43% against an industry 50–60% is the thing Fede called the reason an owner isn't blind. It sits at the point where the drop happens — not in a separate card.
Verdict: B. The only option that answers the actual ask — see the funnel, click into it — and it removes a component rather than adding one. A is the honest fallback if the colour fix should ship independently of the semantics decision; the two are separable, and A is a strict subset of B.
The renewals critique concluded that page is an incident page: a PM lands because one renewal is stuck, so it should answer what is wrong → where did it fail → what happened → what do I do.
The prospect detail page is not an incident page. A prospect is rarely blocked; they are simply somewhere in a pipeline. Leading with a blocked-banner and a resolution card would manufacture urgency that mostly isn't there. The question a PM actually arrives with is where is this person, and what has Clara already done? The structure that is right for renewals is the wrong import here — which is exactly why this needs its own pass rather than a reskin.
The page never shows the prospect's position in the funnel. There is a stepper, but it tracks the re-engagement cadence — five follow-up touches — not inquiry → tour → application → signed. A PM clicking a funnel stage lands on a page that doesn't say which stage they came from.
The generic Stepper primitive already exists with seven consumers. Wiring, not a new component.
stageDisplay() helper so the stepper, the header pill and the card badge can never disagree — the best structural idea from the renewals work, which minted exactly one status→{label, tone} table for that module.ReengagementCard's 93-line style block into tokens and the existing Stepper. This card alone is the biggest source of visual noise on the page.SignalTimeline's hard-coded hex, which break dark mode outright.Application #884213 is rendered in the activity log even though the code twelve lines up says it is intentionally not a key fact.Explicitly not doing: a blocked banner, a top-level resolution card, or a purple Clara Insight card. Those are renewals' answers to renewals' problem. If prospects earns one later — “this lead has gone quiet twice” — it gets exactly one, per the principle.
| What | Why it matters here | Handling |
|---|---|---|
| Issue #2880 Detail page crashes |
The prospect detail route dies to the app error boundary on Vercel preview against stage data — an unguarded Server Component throw. We cannot preview-verify a detail redesign on a page that doesn't render. | Lands first |
| Sean's tie-out Founders-call invariant |
Flow semantics break active === inquiries + tours + applications by construction. Needs a deliberate decision, not a silent regression. |
Gera + Sean |
| Issue #5551 No render pin |
Vacant Units is the only label in the strip with no render assertion behind it — and it is the one whose disappearance already took main down once. Removing it needs a pin added in the same PR. |
Known trap |
| Window mismatch 14 / 30 / 60 |
The default is 30. The link in the thread was 14. The entire standup discussion was 60. The funnel's window has to be picked, not inherited. | Question 3 |
| Households vs records | These tiles count households; the shared dashboard counters count records. Deliberate, but still open with Fede — and the funnel will make the divergence visible. | Flagged |
| Three principle docs | Your design language, docs/design/dashboard-style.md and docs/brand/design-standards.md broadly agree, but nothing reconciles them. The prospects page follows none. |
Pick one home |
Unblocks every visual check downstream. Small, isolated, its own PR.
Flow versus the tie-out. Blocks the funnel and the conversion percentages; blocks nothing else.
Identity dots out, one canonical stageDisplay() in, token bypasses retired, the .bk-* block folded away. Pure visual, no semantics — shippable while step 2 is still open.
Direction B. Stage click drills into members and filters the table. Occupancy moves to the footer line. Carries the Vacant Units render pin from #5551.
Position made visible, three status renderings collapsed to one, prep noise dropped.
Steps 3 and 4 are separable on purpose. If the semantics conversation takes a week, the page still stops shouting in the meantime.
Answer here and it saves for everyone — each person keeps their own answer, so Gera and Sean can disagree on the page rather than in a thread.
Right now the top of the page is six little boxes with six different colours, and none of them show that 52 people asked about a unit and 9 signed. Option B turns that into one shrinking bar chart you can click. Option A just tidies the boxes up.
Today “Applications: 1” means one is sitting on someone's desk. Sean wants it to mean ten people applied, and one still needs a look. Both are reasonable, but only the second one lets you show drop-off between stages. The catch: with the second, the three stage numbers stop adding up to “Active Prospects” — because someone who already signed still counts as having applied. That adding-up rule was a decision from a founders call, so it needs Sean to unmake it.
How far back the funnel looks. Leases take weeks, so a two-week window makes the last stage look emptier than the business really is.
Whether to also show which listing sites the leads came from. Useful, but it's a second question, and bolting it on now is how this pass gets slow.
Neither Slack message carried an image, so “that funnel bar chart” was read as the standup's concept. If a specific screenshot exists, this is the place to say so.