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.
1 · Anatomy
Warm (enlarged for annotation — real size is 6px squares)
1Squares: 6px, 1px radius, 3px gap. Oldest day LEFT → today RIGHT (always the last 7 calendar days).
2Hue = CURRENT tier: hot red-01 · warm orange-01 · cool blue-01 · cold gray-02. One hue per deal — never per-day colors.
3Per-day alpha = the DAY's tier band: Hot 0.90–1.00 · Warm 0.65–0.85 · Cool 0.40–0.60 · Cold 0.20–0.35, interpolated by that day's intra-tier position (see section 2b).
4Unscored day: solid gray-06 square (honest gap — no carry-forward; absence is a different channel, never the hue at low alpha).
5Tier word: t-tiny weight 600, tier color, 6px after the grid.
6Placement: deal-card row 1 right side, ≥12px clearance from the name group (name + 8px + .ago).
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.
3 · State machine
NEW New Deal <1d or <2 trend points: empty grid + "New" gray — NO tier claimed (the model's weakest read).
ACTIVE Warm Grid + tier word. Scored on every deal event + the daily cron.
GOING QUIET Cool Empties from the RIGHT as scoring stops — the visible warning before stale. (Narrative name — the UI label stays "Cool".)
STALE scored 9d ago updatedAt >7d: strip dimmed + "scored Nd ago" microtext REPLACES the word (never both).
REVIVED Warm Any deal event restarts scoring; the strip relights from the RIGHT.
THE LAW:  EMPTY-LEFT = YOUNG  ·  EMPTY-RIGHT = GOING DARK
4 · Refresh behavior — when the data moves
Scored on every deal event (create · status change · credit app · appointment · inbound message) + a daily cooling cron while the deal is active. Deals silent >45 days exit the daily refresh — their strip empties from the right, then goes stale — and re-enter on any event. Scores FREEZE when a deal closes or archives. The hover tooltip (.sb-dealscore-tip) carries ALL absolute numbers: percentile-first title ("Hot — top 4% of your pipeline") · model-estimate fine print ("~31% close probability") · sparkline · SHAP reasons · scored-ago + model version.
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.
6 · Canonical markup — the exact instance pattern
<!-- active (modifier: --hot / --warm / --cool / --cold) · alphas from the day-tier bands --> <span class="sb-embergrid sb-embergrid--warm" data-tip="Lead score"> <span class="strip"> <span style="opacity:0.46"></span><span style="opacity:0.69"></span><span class="empty"></span> <span style="opacity:0.75"></span><span style="opacity:0.79"></span><span style="opacity:0.83"></span><span style="opacity:0.85"></span> </span> <span class="tword">Warm</span> </span> <!-- stale: microtext replaces the word --> <span class="sb-embergrid sb-embergrid--stale">…<span class="microage">scored 9d ago</span></span> <!-- new: 7 empty squares, no tier claimed --> <span class="sb-embergrid sb-embergrid--new">…<span class="tword">New</span></span>
PRESCRIPTIVE — design-ahead of the app (flag #1827). Card context: comp-deal-card.