diff --git a/static/calendar.js b/static/calendar.js
index aa5e360..a79fe4a 100644
--- a/static/calendar.js
+++ b/static/calendar.js
@@ -673,7 +673,7 @@ function renderTotals(visible) {
.map((r) => { const g = metric === "feels" ? feelsPick(r) : r[metric]; return g && g.c; })
.filter(Boolean);
buckets = SCALE_TEMP.map((t) =>
- [t.label, TIER_COLORS[t.c], classes.filter((c) => c === t.c).length]);
+ [t.label, TIER_COLORS[t.c], classes.filter((c) => c === t.c).length, t.c === "normal"]);
title = (TEMP_METRIC_INFO[metric] || {}).label || "value";
}
@@ -687,14 +687,26 @@ function renderTotals(visible) {
totEl.hidden = false;
return;
}
- const bar = buckets.filter((b) => b[2] > 0).map(([label, color, n]) =>
- ``).join("");
- const chips = buckets.filter((b) => b[2] > 0).map(([label, color, n]) =>
- `
- ${label}${pctText(n)}
`).join("");
+ // A compact deviation strip: one thin status-colored line per category, ordered
+ // low→high like the color scale, with each share printed above it in that
+ // category's own color. The median tier (the neutral "Normal" center of the
+ // diverging temperature scale) is the reference — its share is shown but it draws
+ // no line, just a faint baseline tick. Precip/dry-streak have no median, so every
+ // category there gets a line. Category names live in the color key below the grid,
+ // so this stays terse. Line heights scale to the tallest non-median category.
+ const maxLine = Math.max(1, ...buckets.filter((b) => !b[3]).map((b) => b[2]));
+ const LINE_MAX = 30; // px — height of the most common (non-median) category
+ const cols = buckets.map(([label, color, n, isMed]) => {
+ const h = isMed || !n ? 0 : Math.max(2, Math.round(LINE_MAX * n / maxLine));
+ const num = n ? pctText(n) : "0";
+ const line = h ? `` : "";
+ const mid = isMed ? `` : "";
+ return `` +
+ `${num}${line}${mid}
`;
+ }).join("");
totEl.innerHTML = `${head}
- ${bar}
- ${chips}
`;
+ ${cols}
`;
totEl.hidden = false;
}
diff --git a/static/style.css b/static/style.css
index b5edc16..d3baed5 100644
--- a/static/style.css
+++ b/static/style.css
@@ -736,14 +736,31 @@ main { max-width: 1200px; margin: 0 auto; padding: 20px 24px 60px; }
}
.comfort-row .hint { flex: 1 1 100%; margin: 0; font-size: 12px; color: var(--muted); }
-/* Category totals above the grid: stacked share bar + per-category chips. */
+/* Category totals above the grid: a compact deviation strip. Each category is a
+ column with its share printed at the top (in that category's own status color)
+ and a thin status-colored line rising from a shared baseline (height ∝ share).
+ The median tier draws no line — just a faint baseline tick as the neutral anchor. */
.cal-totals { max-width: 560px; margin: 0 0 18px; }
-.ct-bar {
- display: flex; height: 14px; border-radius: 7px; overflow: hidden;
- margin: 6px 0 8px; background: var(--surface-2);
+.ct-strip {
+ display: flex; align-items: flex-end; gap: 6px;
+ height: calc(var(--line-max, 30px) + 22px); margin: 8px 0 6px;
+}
+.ct-col { position: relative; flex: 1 1 0; min-width: 0; height: 100%; }
+.ct-num {
+ position: absolute; top: 0; left: 0; right: 0; text-align: center;
+ font-size: 11.5px; font-weight: 700; line-height: 1;
+ color: color-mix(in oklab, var(--c, var(--muted)), var(--text) 40%);
+}
+.ct-num-zero { font-weight: 600; opacity: .38; }
+.ct-line {
+ position: absolute; bottom: 0; left: 50%; transform: translateX(-50%);
+ width: 3px; border-radius: 2px 2px 0 0; background: var(--c);
+}
+.ct-col-median .ct-num { color: var(--muted); font-weight: 800; }
+.ct-mid {
+ position: absolute; bottom: 0; left: 50%; transform: translateX(-50%);
+ width: 7px; height: 3px; border-radius: 2px; background: var(--border);
}
-.ct-bar span { display: block; height: 100%; }
-.ct-pct { color: var(--muted); font-size: 12px; }
/* Days filtered out by the weather filter fade back so matches pop. */
.cal-cell.dim { opacity: .15; }