PropFlow · Leasing · Scope & direction

Making the prospects page quiet

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.

2026-08-24 Targets /leasing/prospects + /leasing/prospects/[id] No code changed yet
01

This is not a new direction

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.

Your design language

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.

The chart doctrine

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.

The dashboard precedent

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.

02

The loudness, measured

“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.

Detail page, today

Twelve values, nine families. The doctrine's ceiling is four.

The single worst offender

Every activity-log dot and connector is a distinct interpolated huenodeColor(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.

Meaning isn't stable either

  • “Signed” is green in the header pill and violet in the application card badge — on the same prospect.
  • “Tour” is purple in the header pill, green in the confirmed strip, yellow in the proposed strip.
  • The header says “Application Review” while the card two inches below says “Under review” — two components restating one fact.
  • ReengagementCard injects a 93-line <style> block of .bk-* classes — its own type scale, radii, pills, a pulsing dot and a double glow ring. A second design system inside one card, and the loudest thing on the page.
  • The “Not Interested” banner is hard-coded hex, so it does not respond to dark mode at all.

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.

03

The one thing that gates everything

The repo holds two incompatible definitions of the funnel, and the prospects page is on the wrong one for what the standup asked for.

Current-stage — what prospects uses

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.

Flow — what the /leasing hub uses

applicationsInWindow(). 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.

The same decision unlocks the conversion percentages

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.

Two data caveats worth stating in the UI

  • appliedAt is stamped when the sync poller notices an application, not when it was submitted. Measured drift against AppFolio: 6 hours to 59 days. A 60-day window is approximate at its edge.
  • approvedAt is written in memory and dropped before it reaches DynamoDB — absent in prod. There can be no honest “Approved” stage today. Four stages is also exactly the doctrine's ceiling, so this costs us nothing.
04

List page — three directions

Rendered with the real Camellia numbers from the call: 60 days, 52 inquiries → 23 tours → 10 applications (1 awaiting) → 9 signed.

Today — six tiles, six decorative huesShips now
Vacant
19
avg — days
Active
34
in the pipeline
Inquiries
23
 
Tours
10
1 upcoming
Applications
1
awaiting review
Signed
9
last 30 days

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.

A · Quiet the stripFallback

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.”

C · Funnel plus stripRejected

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.

B · The funnel is the headerRecommended
52
Inquiries
last 60 days
44% continue to a tour
23
Tours
1 upcoming
43% go on to apply · below the 50–60% benchmark
10
Applications
1 awaiting review
90% are approved and sign
9
Signed leases
 
19 vacant · 4 on notice · 23 available

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.

05

Detail page — and why the renewals answer is the wrong one to copy

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.

Already right — don't touch it

  • Header is lean, ~124px, no gradient hero. Renewals has a dual-radial wash, a 5xl gradient name and a pinging halo. Prospects has none of it.
  • The conversation is already a link, not an inline transcript. Renewals still renders every bubble.
  • The focal slot renders exactly one of four cards. That is progressive disclosure, already built.

The real gap

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.

The pass, in priority order

  1. Add the funnel stepper. Four nodes, current position marked, fed by one canonical 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.
  2. Collapse the three status representations into one. Today: header pill, application-card badge, re-engagement pill — one fact, three times, three colours.
  3. Kill the interpolated activity rail. Dots become semantic: done, current, upcoming. An index is not a meaning.
  4. Fold 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.
  5. Retire the token bypasses — the “Not Interested” banner and SignalTimeline's hard-coded hex, which break dark mode outright.
  6. Drop the prep noise. “Synced to PropFlow” is an internal ingestion event with no PM action, and it only appears for legacy-id rows. 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.

06

Blockers, in the order they bite

WhatWhy it matters hereHandling
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
07

Sequence

01
Fix #2880, so the detail page renders in preview

Unblocks every visual check downstream. Small, isolated, its own PR.

02
Settle the semantics with Sean

Flow versus the tie-out. Blocks the funnel and the conversion percentages; blocks nothing else.

03
De-rainbow both pages

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.

04
Replace the strip with the funnel

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.

05
Detail page: the funnel stepper and the collapse

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.

08

Decisions

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.

1 · Direction for the list page
In plain terms

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.

2 · How the funnel counts — needs Sean
In plain terms

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.

3 · The funnel's window
In plain terms

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.

4 · Lead source — “where are our leads coming from”
In plain terms

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.

Anything else
In plain terms

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.

PropFlow Docs