diff --git a/roadmap-planner/frontend/src/components/TeamAnalytics.css b/roadmap-planner/frontend/src/components/TeamAnalytics.css
index e9d703eb..a3013115 100644
--- a/roadmap-planner/frontend/src/components/TeamAnalytics.css
+++ b/roadmap-planner/frontend/src/components/TeamAnalytics.css
@@ -799,6 +799,95 @@
.ta-bars { display: flex; flex-direction: column; gap: 6px; }
+/* ---------- Chart help popovers ----------
+ * Small info-icon button next to a chart title that toggles a popover
+ * with prose explaining what the chart shows. The wrapper is positioned
+ * (relative) so the popover anchors to the icon. Panels use
+ * `overflow: hidden` for clean borders, so the popover is positioned
+ * relatively but uses a high z-index and a wide box; it can extend
+ * below the panel head over the chart body (panels are tall enough). */
+.ta-help {
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+ margin-left: 8px;
+ vertical-align: middle;
+}
+/* When a help popover is open, let it escape the panel's clipped frame
+ * (panels use overflow:hidden so their head background stays inside the
+ * rounded corners). :has() flips that off only for the open panel, so
+ * the popover can extend over the chart area below the head. */
+.ta-panel:has(.ta-help__pop) { overflow: visible; }
+.ta-help__btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ padding: 0;
+ border: 1px solid var(--border-soft);
+ border-radius: 999px;
+ background: var(--bg-elevated);
+ color: var(--fg-muted);
+ cursor: pointer;
+ transition:
+ color var(--dur-fast) var(--ease),
+ border-color var(--dur-fast) var(--ease),
+ background var(--dur-fast) var(--ease);
+}
+.ta-help__btn:hover,
+.ta-help__btn[aria-expanded="true"] {
+ color: var(--accent);
+ border-color: var(--accent);
+ background: var(--accent-tint);
+}
+.ta-help__pop {
+ position: absolute;
+ top: calc(100% + 8px);
+ left: 0;
+ z-index: 30;
+ width: min(420px, 80vw);
+ padding: 14px 16px;
+ background: var(--bg-elevated);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-default, 4px);
+ box-shadow: var(--shadow-plate);
+ font-family: var(--font-ui);
+ font-size: 12.5px;
+ line-height: 1.55;
+ color: var(--fg);
+ text-transform: none;
+ letter-spacing: 0;
+ font-weight: 400;
+ font-style: normal;
+}
+.ta-help__pop-title {
+ font-family: var(--font-mono);
+ font-size: 10.5px;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--fg-muted);
+ margin-bottom: 8px;
+}
+.ta-help__pop-body p { margin: 0 0 8px; }
+.ta-help__pop-body p:last-child { margin-bottom: 0; }
+.ta-help__pop-body ul {
+ margin: 0 0 8px;
+ padding-left: 18px;
+}
+.ta-help__pop-body li { margin-bottom: 4px; }
+.ta-help__pop-body strong { color: var(--fg); font-weight: 600; }
+.ta-help__pop-body em { color: var(--fg-subtle); font-style: italic; }
+
+/* KPI-strip hint row sits directly under the 5-tile grid and carries
+ * the help icon inline. Flex layout keeps the icon vertically centered
+ * against the small-caps hint text. */
+.ta-kpis-hint {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
/* Donut layout */
.ta-donut-row {
display: flex;
diff --git a/roadmap-planner/frontend/src/components/TeamAnalytics.jsx b/roadmap-planner/frontend/src/components/TeamAnalytics.jsx
index 24a14b16..3eeed9a7 100644
--- a/roadmap-planner/frontend/src/components/TeamAnalytics.jsx
+++ b/roadmap-planner/frontend/src/components/TeamAnalytics.jsx
@@ -34,7 +34,28 @@ import './TeamAnalytics.css';
// properties resolve at paint time against the active [data-theme] /
// [data-mode], so dark mode and the Atlas theme need no per-mode
// branching here.
-const PILLAR_PALETTE = ['var(--accent)', 'var(--ocean)', 'var(--amber)', 'var(--forest)', 'var(--crimson)'];
+// Categorical palette for pillar bands. The semantic theme tokens
+// (--accent / --ocean / …) don't work here: in the default light theme
+// --accent and --ocean both resolve to blue (first two slots collide),
+// and the project routinely has more pillars than the 5-token semantic
+// palette, so modulo wrapping causes explicit duplicates. These values
+// are tuned for perceptual distance (Tableau-10 inspired) and survive
+// the 0.85 opacity used by the stacked-area paint. Pillar colors carry
+// no theme semantics — they're purely categorical codes — so fixed
+// values are appropriate. Keep in sync with TeamAnalyticsDashboard.jsx.
+const PILLAR_PALETTE = [
+ '#4E79A7', // blue
+ '#F28E2B', // orange
+ '#59A14F', // green
+ '#E15759', // red
+ '#8E44AD', // purple
+ '#EDC948', // yellow
+ '#76B7B2', // teal
+ '#E377C2', // magenta
+ '#9C755F', // brown
+ '#17BECF', // cyan
+ '#7F7F7F', // gray
+];
const formatHours = (h) => (h == null ? '—' : `${(+h).toFixed(1)}h`);
const formatPct = (p) => (p == null || isNaN(p) ? '—' : `${Math.round(p)}%`);
diff --git a/roadmap-planner/frontend/src/components/TeamAnalyticsDashboard.jsx b/roadmap-planner/frontend/src/components/TeamAnalyticsDashboard.jsx
index 15ded8fa..e0ec2c2b 100644
--- a/roadmap-planner/frontend/src/components/TeamAnalyticsDashboard.jsx
+++ b/roadmap-planner/frontend/src/components/TeamAnalyticsDashboard.jsx
@@ -18,9 +18,47 @@
* — no hex literals, no theme branching here. Light/Dark/Atlas swap
* automatically.
*/
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
+import { HelpCircle } from 'lucide-react';
import { contributionsAPI } from '../services/api';
+/* ChartHelp — small info-icon button that toggles a popover with prose
+ * explaining what a chart shows, how its metric is derived, and how to
+ * read it. Click-outside or Escape closes. Anchored to the panel head
+ * so the popover doesn't get clipped by the panel's overflow:hidden. */
+function ChartHelp({ title, children }) {
+ const [open, setOpen] = useState(false);
+ const ref = useRef(null);
+ useEffect(() => {
+ if (!open) return undefined;
+ const onDown = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
+ const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
+ document.addEventListener('mousedown', onDown);
+ document.addEventListener('keydown', onKey);
+ return () => {
+ document.removeEventListener('mousedown', onDown);
+ document.removeEventListener('keydown', onKey);
+ };
+ }, [open]);
+ return (
+
+
+ {open && (
+
Click a metric tile to drive the panels below.
+Five clickable tiles, one per metric. Each tile shows the team‑wide total for the selected window and filter, plus a delta vs the prior half of the same window.
+The arrow + percentage compares the most recent half of the window to the earlier half (e.g. for 26 weeks: last 13 vs prior 13). (new) means there was zero activity in the prior half.
+Click any tile to make that metric the active metric — the Throughput trend highlights its line, and the Pillar mix, Donut, Top movers, and Ranking panels all switch to it.
+Weekly team‑wide totals for all five metrics on the same axis, so you can spot when activity surges, dips, or rotates between PR throughput, reviews, and Jira completion.
+Look for sustained slopes (capacity changes), notch‑shaped weeks (releases, holidays), and divergence between PRs opened and PRs merged (queue buildup vs catch‑up).
+Stacked area showing how the active metric is distributed across pillars, week by week. Each colored band is one pillar; the total height is the team aggregate for that week.
+Use it to see whether a single pillar is carrying total throughput, or whether the mix is rotating across teams over time.
+Paired weekly bars: PRs opened (left, purple) vs PRs merged (right, blue). This panel is intentionally fixed to opened‑vs‑merged — it answers "is the team merging what it's opening?" and the question doesn't translate to other metrics.
+Both bars respect the Window and Pillar filters but ignore the active metric selection.
+Donut chart of the active metric summed over the whole window, sliced by pillar. The number in the center is the team total; each legend row shows the pillar's count and percent of total.
+Where Pillar mix over time shows when a pillar contributes, this shows how much in aggregate. Use it to sanity‑check whether one pillar dominates the metric you're looking at.
+Per‑member delta on the active metric: the recent half of the window minus the prior half. Top 8 by absolute delta — biggest swings in either direction, not biggest totals.
+Best read together with the ranking panel below: a member can rank near the top of the team yet have a negative mover delta (great in absolute terms, slowing down), or vice‑versa.
+Every member who contributed at least one unit of the active metric in the window, sorted high to low. Bar length is the member's total; the number on the right is the raw count.
+This is a totals view. For who's accelerating vs slowing down, use the Top movers panel above.
+