Feature docs

Leads

IN DESIGN — design-ahead of the app

One pipeline, two ways to work it. The Leads screen flips between the kanban board and a full data table in place — same header, same quick filters, same deals — so working a lead and scanning the book are one surface, not two features.

The board is the work view: untouched-first triage, effort stats, and an honest heat signal on every card, with Compact cards and in-place column collapse. The table is the scan view: every deal is a row, every field a column, sorted globally from one menu. A single two-state chip owns the flip; the incumbent board stays the default until the table earns it (ds_v264).

Listen to this page
0:00/11:21

Rulings ledger

DateRuling
07-08Tiers are population dials — service-derived rank buckets, re-tuned each retrain; never hardcode thresholds client-side.
07-08Funnel bar KILLED post-GSM call — measure EFFORT, not pipeline shape.
07-08UNTOUCHED = deal created + zero outbound since; red-02 surface; a quick-filter chip; the stat renders red when >0.
07-08One lead source per deal — the source IS the intent ("Credit Application", "Trade-In Form"); intent-vs-origin duplication is dead.
07-08Multi-assignee cluster + qa footer per the customer-card conventions; hover-hidden buttons are dead.
07-09Score = EMBER GRID top-right, ago beside the name; percentiles in the tooltip only.
07-29Deals can start without a vehicle — a credit application or trade-in with no vehicle still opens a deal (an empty deal); the first vehicle the customer touches attaches to it, and a customer holds at most one empty deal at a time.
07-23Incumbent-default (ds_v264) — until the table view is validated, the incumbent view is the default everywhere — Board for Leads, List for Contacts. Supersedes the v262 "default = Table" note; the files author Board as state-a.
07-23Compact is board-only for now (ds_v260) — the chip hides in table view. The planned table Compact = a reduced column set (Name · Stage · Score · Vehicle · Assigned); wiring pending.
07-23Bottom bar killed (ds_v260→v261) — .sb-dtfoot and the .lt-frame card are gone — the table fills edge-to-edge. Sort re-homed to the control band: desktop = labeled "⇅ Manual Order ⌄" dropdown right-aligned under Add; mobile = icon just left of Filters. Open: mobile lost the "28 leads" count — re-home or drop.
07-22Board⇄Table is one screen, flipped in place (ds_v262) — a single two-state chip owns body.table-mode; the header band never changes, only content swaps. No separate table route.
07-22Per-cell hover and click, not whole-row (ds_v259) — every populated cell is its own target; em-dash cells and the Stage dropdown are excluded. Click destinations are v1 read-and-open.
07-22Honesty caret (ds_v256) — a quiet, non-interactive caret marks the ONE sorted column only. Defaults that aren't real sorts — Manual Order here, Recently Updated / Unseen Activity on Contacts — show no caret anywhere.
07-22Sort is global and menu-driven — never a header-click (ds_v255) — one four-option menu (Manual Order default · Lead Score · Most Stale · Vehicle) sorts the whole table.
07-22Stage is the one full-cell tint exception (ds_v253) — 14% status hue across the whole cell; everywhere else we color the value, never the cell. Tint base hardcodes white — dark-mode base unresolved.
07-09Collapse rail hugs content — pad · letter · gap · eye · pad; full column height, in place.
07-09Rename Desk → Leads (design-ahead; the shared topbar atom keeps Desk until the app ships it).
07-09The card is ONE recipe — screens instance it, never re-implement it.
07-10Lead score — the metric name — never "deal score"; tier words exactly Hot / Warm / Cool / Cold; directional labels rejected (direction is the grid's shape only).
07-10Sort by Lead Score added to the column sort menu (high→low; unscored cards sink to the bottom).
07-10Tier-banded opacity — per-tier alpha bands (Hot .90–1.00 · Warm .65–.85 · Cool .40–.60 · Cold .20–.35); raw probability never maps to pixels.
07-10Stat info icons — circled-i on every effort-stat label; hover shows the definition tooltip, click pins it — no navigation.
07-10Stat thresholds — Untouched red when >0; Avg First Touch: <15m neutral · 15–60m orange · >60m red — calibrated on live first-touch data.
07-10Score explorations page removed — the decision trail lives in the repo plan doc.
07-10Score tooltip designed — replaces the non-native gradient line-chart hover; contained bars on the shared band table, calibrated % headline with adaptive precision, SHAP why-rows, no gradients / no uncontained lines.

Anatomy map

Board screen (desktop)Full-bleed interactive board — compact toggle + column collapse live. Board screen (mobile)Scroll-snap column pager with adjacent-column peek. Deal cardThe Stacklight v5 recipe and every card state. Board chromeEffort stats, quick filters, column headers, collapse rail. Ember grid specElement-level engineering spec: anatomy, data contract, states. Status chipThe .sb-statusdd enum — colors and variants. Table primitive.sb-datatable frame, cells, controls. Leads table screen — desktopThe shipped Board/Table flip screen. Leads table screen — mobileEdge-to-edge table on the phone.

The card

The canonical Stacklight card — its final row order, and the rule that keeps every screen honest.

1 · Full card — vehicle present · source CarGurus
Devon Brooks4mWarm
2023 Honda Accord Sport$28,750·#48213
CarGurus
VIPHotRepeat Buyer
PRESCRIPTIVE — CARD v5: (Taylor ruling: score TOP-RIGHT, time NEXT TO NAME): R1 = name + ago inline (left) · EMBER GRID + tier word pinned TOP-RIGHT. Stack: [user] status DROPDOWN chip · [car] Vehicle of Interest (or "None" + plus) · [source icon] the SINGLE lead source · [tags] COLORED label chips + bare add · footer: cluster + always-exposed .qa actions. Tiers are service-derived buckets off within-pipeline rank (model team’s D1) — scale-coupled, re-tuned each retrain; never hardcode thresholds client-side. The grid = the last 7 daily trend points, one hue per current tier, opacity per day; empty-left = young deal, empty-right = going quiet (scoring stops after 45d silence). All absolute numbers live in the tooltip. Flag-gated (#1827); needs ≥2 trend points, else tier word only. Rule: the status chip ALWAYS carries a caret; label chips NEVER do.
ENG: the card is ONE recipe (components.css) — screens must instance it, never re-implement it.

The score

What the Ember Grid is, the payload that drives it, and the rules engineering must not break.

Ember Grid — .sb-embergrid · component-level engineering spec (PRESCRIPTIVE — design-ahead of the app · flag #1827)
What it is: the lead-score element on the Leads deal card (top-right of row 1): 7 daily squares + the current tier word. It answers two questions at a glance — WHERE the deal stands (the color) and WHERE IT CAME FROM (the opacity over days). The atom CSS in components.css is the source of truth; this page is the deep-link spec — features reference it, never copy it. Card context: comp-deal-card. Decision trail: the repo plan doc (autopilot/DESK_PLAN.md).
0 · Locked terminology (PM ruling 2026-07-10)
The metric's user-facing name is the LEAD SCORE. The page is Leads — "deal score" never appears in user-facing or spec copy. Tier vocabulary is EXACTLY four words: Hot / Warm / Cool / Cold. "New" is the unscored label (no tier claimed); the stale microtext ("scored Nd ago") replaces the tier word. Directional labels are REJECTED — Cooling / Heating / Rising / Going Quiet never appear as UI labels; direction is expressed by the grid's shape only. Directional words are allowed in spec NARRATIVE prose (like this page's state names), never in the UI.
2 · Data contract — payload → pixels (two worked examples)
// Example A — active hot deal · 7 contiguous points, rising // trend day = {d, t, pos, p} — p is TOOLTIP-DISPLAY-ONLY; pixels never derive from p { "tier": "hot", "tier-source": "pClose via service buckets (D1)", "updatedAt": "2026-07-09T13:05Z", "trend": [ {"d":"2026-07-03", "t":"cool", "pos":0.3, "p":0.09}, // cool band: 0.40 + 0.3×0.20 → alpha 0.46 {"d":"2026-07-04", "t":"cool", "pos":0.8, "p":0.14}, // cool → 0.56 {"d":"2026-07-05", "t":"warm", "pos":0.2, "p":0.16}, // warm band: 0.65 + 0.2×0.20 → 0.69 {"d":"2026-07-06", "t":"warm", "pos":0.7, "p":0.22}, // warm → 0.79 {"d":"2026-07-07", "t":"hot", "pos":0.1, "p":0.27}, // hot band: 0.90 + 0.1×0.10 → 0.91 {"d":"2026-07-08", "t":"hot", "pos":0.5, "p":0.30}, // hot → 0.95 {"d":"2026-07-09", "t":"hot", "pos":0.9, "p":0.31} // hot → 0.99 (today) ] }
Elise Tran22mHot
7 contiguous dates → 7 filled squares; each day's alpha from ITS OWN tier's band; hue = current tier (hot).
// Example B — cool tier · going quiet (2 missing days) { "tier": "cool", "updatedAt": "2026-07-07T02:00Z", "trend": [ {"d":"2026-07-03", "t":"warm", "pos":0.5, "p":0.24}, // warm → 0.75 {"d":"2026-07-04", "t":"cool", "pos":0.6, "p":0.19}, // cool → 0.52 // 2026-07-05 ABSENT → empty gray-06 square {"d":"2026-07-06", "t":"cool", "pos":0.2, "p":0.11}, // cool → 0.48 {"d":"2026-07-07", "t":"cold", "pos":0.7, "p":0.06} // cold band: 0.20 + 0.7×0.15 → 0.31 // 07-08, 07-09 ABSENT → empty, empty (going quiet) ] }
Marcus Lee6dCool
Missing dates render as empty squares; the two empty squares at the RIGHT edge are the going-quiet warning.
Mapping rules: take the LAST 7 CALENDAR DAYS from trend (stores ≤30 daily points; dates are NOT contiguous). Each day arrives as {d, t, pos, p}: day present → alpha from THAT DAY's tier band interpolated by pos (intra-tier percentile — cuts stay service-side dials, never client math). Day absent → empty gray-06 square. p is tooltip-display-only — pixels never derive from p. Hue from the CURRENT tier, which is SERVICE-DERIVED (rank-based buckets per model-team decision D1) — NEVER hardcode tier cutoffs client-side; they are scale-coupled and re-tuned each retrain.
2b · Tier-banded opacity (PM ruling 2026-07-10 — replaces the single quartile ladder)
WHY: raw probability must NEVER map linearly to alpha. The calibrated median pClose is under 1%, so a linear mapping renders nearly everything invisible — and even a rare 30% pClose (one of the hottest leads in the pipeline) would draw at 30% alpha and read cold. Probability space and perceptual alpha space are different axes. THE SYSTEM: each trend day arrives from the service with its own tier t + intra-tier position pos (0–1, percentile within that tier's population — cuts stay service-side dials, never client math). Grid HUE = current tier (unchanged law). Each day square's alpha = linear interpolation inside ITS OWN tier's fixed band. Formula: alpha = band.min + pos × (band.max − band.min), rounded to 2dp.
Day tierAlpha band
Hot0.90 – 1.00floor 0.90 — always unmistakably hot
Warm0.65 – 0.85
Cool0.40 – 0.60
Cold0.20 – 0.35floor 0.20 — stays above invisibility
No scoresolid var(--gray-06)absence is a DIFFERENT channel — never the hue at low alpha
aBands are disjoint with deliberate gaps — a hot day can never render dimmer than a warm day.
bHot floor 0.90: a 30% and a 60% pClose hot lead differ by only a few alpha points — both unmistakably hot.
cTier changes read as visible steps; intra-tier drift is subtle BY DESIGN — level first, micro-trend second.
dCold floor 0.20 stays above invisibility; empty gray-06 is visually distinct from any cold alpha even when the current tier is Cold (gray-02 hue is light — verified below).
eGrayscale-safe — the band ladder survives colorblindness.
Cold Cold-tier specimen (enlarged): cold alphas on the gray-02 hue vs solid gray-06 empties — the two channels stay distinct.
2c · The score tooltip (PM ruling 2026-07-10 — replaces the non-native gradient line-chart hover)
HARD AESTHETIC RULES: NO gradients anywhere. NO uncontained line charts — if a chart, it lives inside a hairline-bordered box with a baseline. Flat token fills only. Utilitarian — every row earns its place. The panel is the rich-content instance of the locked tooltip pattern (~260px, r-5, gray-06 border, body-level engine; hover shows, click/tap pins).
1Label row: "LEAD SCORE" 10px uppercase gray-03 letter-spaced · right microtext "scored 2h ago" gray-04.
2Headline: the calibrated close probability as plain fact — number 20px 700 gray-00, "chance of closing" 12.5px gray-02. Formatting rule: ≥10% integer ("33%") · 1–10% one decimal ("4.2%") · <1% shows "<1%" — honest but readable (the median calibrated pClose is under 1%).
3Context line: within-pipeline percentile (model-team D1) · middot · the tier word in its tier color. 12px.
4Trend, CONTAINED: a micro BAR chart — last 30 daily bars (2px wide, 1px gap), flat tier-hue fill, inside a hairline gray-06 box with a gray-07 baseline, ~36px tall, 8px padding. BAR HEIGHT RULE: heights reuse THE SAME BAND TABLE as square opacity — a day maps its (t, pos) to height%: Hot 90–100 · Warm 65–85 · Cool 40–60 · Cold 20–35 · missing day = no bar (gap). One perceptual mapping, two channels: alpha on the card grid, height in the tooltip chart. NEVER raw probability as height (the same anti-linear principle). Bars are full opacity — the chart encodes level by HEIGHT; opacity stays the card grid's channel.
5Delta line, plain facts: "7 days ago 21% → today 33%" 11.5px gray-02 (numbers gray-00). Terminology law: no directional WORDS — the numbers speak.
6WHY: up to 3 reason rows from the model's real per-deal reasons (SHAP): "+" green-01 or "−" red-01 (11px 700) + reason text 11.5px gray-01, under a "WHY" 9.5px uppercase gray-04 label.
STALE (>7d): headline dimmed gray-02, "scored 9d ago" orange-01 microtext + neutral note "Score may be outdated".
NEW / UNSCORED: "Not scored yet" gray-02 + "needs 2+ days of activity" — no chart, no headline.
Data note: everything shown already exists in the score payload — calibrated pClose, within-pipeline percentile, the 30d trend (as {d, t, pos}), SHAP reasons, updatedAt. No new backend fields. Flag-gated #1827 like the grid.
5 · Rules — engineering checklist
One recipe (components.css) — INSTANCE it, never re-implement it.
≥2 trend points required, else render the tier word only (no grid).
Never hardcode tier thresholds — tiers are service-derived, scale-coupled, re-tuned each retrain.
Never linear-map probability → alpha — the calibrated median is under 1%; use the tier bands.
Day alpha from the DAY's tier band (t + pos); hue from the CURRENT tier. p is tooltip-only.
No absolute % on the card — the tooltip owns every number.
Stale microtext REPLACES the tier word — never both.
The grid is decorative for a11y — the tier word is the accessible text (aria-label pattern: "Hot — scored 2 hours ago").
Works in grayscale by design — the opacity ramp survives colorblindness.

Effort signals

How the board surfaces neglected leads: the untouched state, lead origins, and the manager's stat row.

5 · UNTOUCHED — flagship effort signal · source "Vehicle Inquiry"
Jordan Whitaker1hNew
2024 Subaru Outback Touring$39,940·#SA-1177
Vehicle Inquiry
UNTOUCHED: deal created → ZERO outbound since; clears on the first outbound touch. Background red-02 = the same "needs attention" surface language as the showroom unconfirmed rows; all rows normal (fresh ago — usually brand-new; no score yet). AA note: green-01 fresh-ago and the gray-06 .qa buttons were contrast-checked against red-02 — both pass AA on the light red wash; no token change needed.
9 · Source family map (eng documentation · PRESCRIPTIVE)
WebsiteOwn site / platform flows"Vehicle Inquiry", "Trade-In Form", "Credit Application", dealer websites
PortalThird-party marketplace — label = the BRANDCarGurus, Cars.com, AutoTrader, CarFax, KBB, Capital One
Walk-inPhysical arrival"Walk In", "Drive By", "Retail"
Phone LeadInbound call"Phone Up", "App: Phone Call"
ChatChat — text = the VENDOR ("Podium"), never uuidsPodium, Gubagoo, "Space Auto: Website Chat"
Social/AdsPaid + social — text = the SPECIFIC channel ("Facebook")Facebook, FB Marketplace, Television
ReferralHuman referral / repeat"Referral", "App: Repeat", "App: Current Customer"
OEMManufacturer 3rd-party programsNissan / GM / Ford programs
DMS ImportLabeled row — icon-only rule retired"DRIVECENTRIC DATA IMPORT", "List Import", "DMS Sync", Dealertrack
External LeadExternal/unspecified handoffs"External Lead"
OtherUnmatched dealer free-text, shown verbatim(verbatim raw string)
LABEL RULE (Taylor): the icon carries the FAMILY, the text carries the SPECIFIC source — megaphone + "Facebook" (not "Social"), store + "CarGurus", comment + "Podium"; generic text only when no recognizable brand exists (globe "Website", door "Walk-in", phone "Phone Lead", database "DMS Import"). Three-pass mapping: exact "Space Auto: <flow>" map → brand/keyword match → "App:" prefix-strip. Display names are friendly ("Vehicle Inquiry" was Space Auto: Vehicle · "Trade-In Form" was Space Auto: Trade App · "Credit Application" was Space Auto: Credit App). The raw leadSource string always rides in the tooltip; trafficSource is 88% null so it never renders on the card. The icon-only import rule is RETIRED — imports read quietly as fa-database + "DMS Import" in the labeled source row.
1 · Mini effort-stats row (PRESCRIPTIVE)
28New Leads
6Untouched
41Touchpoints
12mAvg First Touch
12mAvg First Touch
32mAvg First Touch
1h 24mAvg First Touch
0Untouched
6Untouched
Threshold states: Avg First Touch — gray-00 under 15m · orange-01 15–60m · red-01 over 60m; Untouched — neutral at 0, red-01 whenever >0. New Leads + Touchpoints are always neutral (volume gauges, no judgment). Data-grounded: calibrated against 60-day first-touch distributions at three live stores — medians 2–5 min, p75 5–25 min, so an average past an hour is unambiguous systemic trouble. Definitions (the info-icon tooltips, verbatim): New Leads = "Leads created in the current window (defaults to today)." · Untouched = "Leads created in the window with zero outbound messages since — email, SMS, or call. Red whenever the count is above zero." · Touchpoints = "Outbound messages (email, SMS, call) sent in the window." · Avg First Touch = "Average time from lead creation to the first outbound message, over leads created in the window. Under 15m healthy · 15–60m drifting · over 60m red." Each label carries a circled-i (gray-04, gray-01 on hover): hover shows the definition via the locked sb-tooltip engine; CLICK pins/unpins the same tooltip (touch devices have no hover). It navigates nowhere — definitions are self-contained. PRESCRIPTIVE — pipeline EFFORT, not shape. The board opens filtered to leads CREATED TODAY by default; this row answers the GSM triage loop — did we touch them all · how many untouched · how much outreach today · how fast do we touch. A shorter variant of the locked showroom stat-card recipe (bare white card, gray-06 border, r5; value t-paragraph-sb, not the big showroom number); one-line nowrap labels — the whole row is scoped to the board's default created-today view, so no per-card 'Today' sublines. The Untouched value renders red-01 whenever >0 (gray-00 at 0 — alert semantics). Data: Touchpoints ≈ outreachStats and Avg First Touch ≈ first-response stats already on POST /v2/dashboard/sales; the Untouched count needs a NEW backend aggregate (design-ahead).
2 · Mobile mini stats (~390px · PRESCRIPTIVE)
28New Leads
6Untouched
41Touchpoints
12mAvg First Touch
Mobile = a 2×2 grid per the app's established mobile stats pattern — all four metrics retained (they are the manager triage loop); equal card widths, no horizontal scroll, no clipping.
3 · Quick filters + the Untouched chip (PRESCRIPTIVE)
Definition: a lead is UNTOUCHED when the customer + deal were created and ZERO outbound messages have been sent since deal creation; it clears on the first outbound touch. The chip uses the locked quick-filter recipe + selected state, with its live count.

Board chrome

The controls wrapped around the columns: per-column sort and hide, the in-place collapse rail, and the compact toggle.

4 · Column header v2 — all five statuses (PRESCRIPTIVE)
Fresh1.9K
Working48
Finance3
Close21.1K
Lost1.1K
PRESCRIPTIVE. Gospel look kept (status-02 tinted container · status-01 title · gray-00 count) + the right icon cluster: sort + hide are per-column. The shipped app's + here was "create deal in this stage" — REMOVED per PM ruling (no adding columns; creation lives elsewhere). Icons are always visible — board chrome, not hover-revealed. Idiom rule: Sort + Hide are the DESKTOP column-header cluster. On the MOBILE pager (one status column per swipe screen) Sort persists (per-page sort) but Hide is ABSENT — a one-column-per-screen pager has no side-by-side columns to collapse. General principle: a desktop affordance that manages a side-by-side spatial condition does not exist in the pager idiom.
5 · Column sort menu — open (PRESCRIPTIVE)
Working48
PRESCRIPTIVE. Locked dropdown-menu recipe. Manual Order = default (the kanban drag order); sort is per-column and session-sticky. Lead Score (ruling 07-10) sorts high→low by the current score rank; unscored cards sink to the bottom.
6 · Collapsed-column rail — .sb-leadrail (PRESCRIPTIVE)
Working48
Supersedes the restore pill — ONE in-place affordance: a hidden column collapses WHERE IT LIVES to a slim content-hugging full-height rail (8px · letter · 8px · eye · 8px) (same status-02 tint + radius as the column container): first letter in status-01 over a small eye; the whole rail is one click target (tooltip "Show Fresh"). Spatial continuity — no affordance teleports to the board edge. Shown beside an expanded column fragment.
7 · Compact toggle — quick-filter chip (PRESCRIPTIVE)
PRESCRIPTIVE — no new atom: the locked quick-filter chip recipe (the All/Mine/Unassigned family) with its locked selected state. Lives in the page header next to the quick filters (Taylor ruling); toggles the whole board between full Stacklight cards and the 56px compact cards (see comp-deal-card).

The table view

The table is built bottom-up on the shared .sb-datatable primitive — these are the live atoms, from the empty frame to the shipped screens.

One screen, two views. A single #viewToggle chip (data-role="two-state" data-body-class="table-mode") flips the page between Board and Table IN PLACE: the pagehead and quick-filter band stay constant; only the content region swaps (body:not(.table-mode) shows the board and hides .sb-datatable; body.table-mode does the reverse and additionally hides the ministat effort strip). View-specific utilities swap with it: Sort is table-only, Compact is board-only. Default = BOARD (authored data-state="a", no body class): per the incumbent-default ruling the incumbent view ships as default everywhere until the table is validated — Board for Leads, List for Contacts. Authoring rule: the state-a (incumbent) content MUST exist statically in the file — the two-state chip only writes the body class on init/click, so a page whose default content waits on JS renders blank until first click.
Empty frame — header + 4 rows (hairlines · em-dash empties · row hover)
Name
Status
Vehicle
Source
Score
The shell. Column widths are declared once on .sb-datatable (grid-template-columns via --dt-cols); each row is a subgrid spanning those tracks, so every cell + the header align to the same column in every row — no stagger, even with variable row heights. Cells have 16px horizontal padding and a 1px gray-06 vertical separator between columns (last cell in a row has none). Text cells wrap (rows grow to fit); atom + number cells (.sb-dtcell--nowrap, --num) stay single-line. Per-cell hover = gray-07 on populated cells; em-dash, Stage, and Status cells are excluded. Empty cells render a muted em-dash. Reused atoms drop into .sb-dtcell.
Typed cells — each cell is an existing atom in a .sb-dtcell
Cell type
Atom in the cell
Name
Devon Brooks
Assigned
KSTBJM+2
Score
Warm4d
Labels
VIPHotRepeat+1
Vehicle
2023 Honda Accord Sport#48213$28,750
Source
CarGurus
Last activity
3h ago
Price
$28,750
Each typed cell is an existing atom vertically centered in a .sb-dtcell — no new cell styling. Name = the .nm/.sub truncation stack (NO idx gutter, NO customer avatar — customers never have avatars), sticky-left. Assigned = .sb-avgroup s24 STAFF avatars + "+N" (staff, so avatars are fine). Score = the .sb-embergrid element dropped flex:none as-is. Labels = .sb-tag--* chips + "+N" overflow. Vehicle = .sb-dealrow (the short deal row — NOT the 80px .sb-veh thumb row). Source = .ibx family icon + .srcname (raw leadSource in the tooltip). Last activity = the .ago colored age. Price = right-aligned .sb-dtcell--num. Nullable cells → the em-dash.
Control bar — two-state view toggle (Board|Table)
View + sort controls. The table's controls live in the shared header band, never in the table itself. The Table⇄Board view switch is a two-state .sb-msel chip in the quick-filter .sb-mselbar (fa-table-columns/fa-table-cells; the chip always names the CURRENT view — blue .on only in table state). Sort is GLOBAL and menu-driven — there is no header-click sorting. Desktop: a labeled .sb-msel dropdown (⊕ ⌄, wrapped in span.sb-sortwrap) right-aligned in the band, under the pagehead Add. Mobile: the same control as an icon-only chip, just left of Filters. Sort renders in table view only (body:not(.table-mode) .sb-sortwrap{display:none}). The menu is a standard .sb-popover (min-width 184px, gray-06 bg, gray-05 hairlines and hover) with four .sb-menuitem entries, verbatim and in order: Manual Order (fa-arrow-down-arrow-up, the default, .is-selected) · Lead Score (fa-fire) · Most Stale (fa-hourglass-half) · Vehicle (A-Z) (fa-car). Every item carries a .ck check, hidden except on .is-selected. The active sort echoes as the quiet .sortcaret on that one column header; Manual Order is not a real sort, so it shows no caret anywhere.
Composed row — a row is a deal
Name
Stage
Score
Last activity
Assigned
Vehicle
Price
Source
Labels
Email
Phone
Alex Chen
Hot0d
20m ago
JM
2024 Subaru Outback#90455
$38,200
Vehicle Inquiry
VIP
alex.chen@outlook.com
(415) 555-0177
Devon Brooks
Warm4d
3h ago
KSTB
2023 Honda Accord Sport#48213
$28,750
CarGurus
VIPHot+1
devon.brooks@gmail.com
(415) 555-0132
Priya Nair
Warm1d
2h ago
SDKSTB
2022 Tesla Model 3#77120
$41,900
CarGurus
Repeat
priya.nair@gmail.com
(415) 555-0161
Marcus Lee
Cold6d
6d ago
2019 Toyota RAV4 LE#31902
$22,400
Website
Trade-In
marcus.lee.longaddress@dealershipmail.com
(415) 555-0140
Rob Delgado
12m ago
KS
None
Credit Application
rob.delgado@outlook.com
(415) 555-0188
A row is a deal — default order Name(sticky) · Stage · Score · Last activity · Assigned · Vehicle · Price · Source · Labels · Email · Phone (triage front-loaded, contact detail last). Every non-empty, non-Stage cell has its own hover (gray-07 + cursor:pointer) and is its own click target (v1 destinations = read-and-open); em-dash cells and the Stage cell (which keeps its .sb-statusdd dropdown) do not hover. Stage is the ONE full-cell tinted column (documented exception to "color the value, never the cell"): a full-bleed tint of the deal processingStatus hue at ~14% with the picker at full chroma; nullable → untinted em-dash. sort is global and menu-driven from the control band (see the controls nugget); the quiet .sortcaret marks the one sorted column, and the authored default (Manual Order) shows none. Text cells wrap; atom + number cells nowrap; Labels is the sole 1fr column; Assigned caps at 3 + "+N".
Compact view — fewer fields (Name · Stage · Score · Vehicle · Assigned)
Name
Stage
Score
Vehicle
Assigned
Alex Chen
Hot0d
2024 Subaru Outback
JM
Devon Brooks
Warm1d
2023 Honda Accord Sport
TBRK
Marcus Lee
Cold6d
2022 Toyota RAV4
JM
Compact currently lives on the board only — the chip hides in table view. The reduced-column table (Name · Stage · Score · Vehicle · Assigned) is spec’d here but not yet wired.
The Leads table screen. Every row is a DEAL — the same DealItem payload the board cards render, one row per deal. Eleven columns, widths declared once on .sb-datatable (rows are CSS subgrid — no stagger), in order: Name (sticky left, min 200px) · Stage (full-cell 14%-tinted .sb-statusdd, caret pushed to the right cell edge) · Score (.sb-embergrid with live scoretip) · Last activity · Assigned (overlapping avatars + watcher eye-chip) · Vehicle · Price (right-aligned tabular) · Source (normalized name; raw feed value in the tooltip) · Labels · Email · Phone. Absent values render an em-dash cell that takes no hover. The five stages map 1:1 to DealProcessingStatus with the shipped hues: New/orange, Working/indigo, Finance/blue, Close/green, Lost/red. The whole .sb-datatable is the two-axis scroll container; the header row is sticky-top, Name is sticky-left, and the header-Name corner cell wins at z-4. Per-cell hover (gray-07 + pointer) marks every clickable cell; empty cells and the Stage dropdown (which owns its own interaction) are excluded, and destinations are v1 read-and-open. Twin flags for engineering: Last-activity heat colors are currently scoped to card selectors so table cells render neutral — intended behavior is the card's exact recency mapping; the Stage cell label reads 'New' where the board lane reads 'Fresh' (same status — reconcile to one label); Working/Close cells reuse the generic --indigo/--sold statusdd variants.
Mobile table. The identical .sb-datatable organism ships on the phone — no reduced redesign. It runs edge-to-edge (no card frame) and the table itself is the two-axis scroll surface. Mobile adjustments: Name narrows to minmax(140,150) and truncates; there is no pagehead — Filters and a primary "+" Add chip join the compact quick-filter band (.sb-mselbar--compact), where every chip is icon-only with a data-tip tooltip and Sort sits just left of Filters; the view toggle is icon-only too. Everything else — sticky header, sticky Name, column order, Stage tint, per-cell hover — is identical to desktop.

Screens

The combined Table/Board pages (comp-leads-table-desktop / -mobile) are the shipped screens; the board-only pages remain the board's atomic source.

INTERACTIVE (PRESCRIPTIVE — Leads-board evolution). The Compact chip toggles the whole board between full Stacklight cards and 56px compact cards (each card carries both canonical instances; a board-level class flips visibility — no reload). Click a column's eye to collapse it in place to its .sb-leadrail; click the rail to bring it back — multiple columns can collapse. Columns scroll vertically inside their tint containers. Board opens filtered to leads CREATED TODAY; untouched red-02 cards sort first in Fresh (deal created → zero outbound since; clears on first outbound touch). The "Leads" tab is a design-ahead rename — the shared topbar atom keeps "Desk" until the app ships it.
INTERACTIVE (PRESCRIPTIVE). One board column per swipe page — a REAL scroll-snap pager with ~20px tinted PEEK slivers of the adjacent columns on both edges (Working's indigo peeking right). The Compact chip toggles the page between full and 56px compact cards — identical recipe + mechanism as desktop (both instances rendered; a pager-level class flips visibility). Fresh page: 7 varied cards, 2 untouched red-02 first + the credit-app "None +" card. Board opens on leads created today. "Leads" tab rename is design-ahead (app keeps Desk until shipped).

Deals without a vehicle

A deal doesn't always start with a vehicle. Credit applications and trade-in submissions routinely arrive before the shopper has picked anything out, and that person is a lead worth working the moment they raise their hand. So a submission with no vehicle still opens a deal — an empty deal: somewhere for the credit app or trade-in to live, and a card on the board straight away.

The empty deal is a placeholder that fills itself in. The first vehicle that customer interacts with attaches to it rather than spawning a second deal, and a customer holds at most one empty deal at a time — so the placeholder becomes an ordinary deal as soon as a vehicle enters the picture, and no one accumulates a drawer of vehicle-less duplicates.

On the board and in the table an empty deal renders like any other deal, with an explicit “no vehicle yet” state where the vehicle would be. That state has to read as deliberate and stay clearly distinct from a deal whose vehicle reference is broken or stale — two different problems that must never look alike. The vehicle-less card and cell states are not yet drawn in the twin.

Open questions. What counts as “interacts with a vehicle” — does browsing a vehicle page or favoriting count, or only a lead / retailing / credit-app submission that names a vehicle? Does an empty deal expire or auto-archive if the customer never engages a vehicle, which stage does it sit in, and does it count toward board totals? And can an empty deal enter desking, or is a vehicle required first?

Engineering scope

Design-ahead of the backend — each item below needs engineering work before the board ships.

Full decision ledger: autopilot/DESK_PLAN.md · autopilot/TABLEVIEW_PLAN.md (repo). This page is the first composed feature doc under the Atomic documentation LAW — the template every future feature copies: narrative + rulings ledger + anatomy map + @dsEmbed transclusions (build inlines them with provenance links) + eng scope notes.