diff --git a/state-hub/dashboard/src/components/help-tip.js b/state-hub/dashboard/src/components/help-tip.js
new file mode 100644
index 0000000..7f37d33
--- /dev/null
+++ b/state-hub/dashboard/src/components/help-tip.js
@@ -0,0 +1,167 @@
+// ABBR
+//
+// A custom element that shows a floating card on hover/focus.
+// Attributes:
+// label — bold title line in the card
+// description — body text
+// doc — optional URL; adds a "Learn more →" link
+//
+// The card is appended to document.body (position:fixed) so it escapes
+// any overflow:hidden or clipping ancestors (e.g. the TOC sidebar).
+
+const _STYLE_ID = "helptip-global-style";
+if (!document.getElementById(_STYLE_ID)) {
+ const s = document.createElement("style");
+ s.id = _STYLE_ID;
+ s.textContent = `
+help-tip {
+ cursor: help;
+ display: inline;
+}
+.helptip-card {
+ position: fixed;
+ z-index: 9999;
+ background: var(--theme-background, #fff);
+ border: 1px solid var(--theme-foreground-faint, #ddd);
+ border-radius: 9px;
+ padding: 0.6rem 0.85rem;
+ max-width: 270px;
+ min-width: 130px;
+ box-shadow: 0 6px 22px rgba(0,0,0,0.13), 0 1px 4px rgba(0,0,0,0.07);
+ font-size: 0.78rem;
+ line-height: 1.5;
+ color: var(--theme-foreground, #333);
+ opacity: 0;
+ transition: opacity 0.12s ease;
+ pointer-events: auto;
+}
+.helptip-card.helptip-visible { opacity: 1; }
+.helptip-card-label {
+ font-weight: 700;
+ font-size: 0.8rem;
+ margin-bottom: 0.3rem;
+ color: var(--theme-foreground, #222);
+}
+.helptip-card-desc {
+ color: var(--theme-foreground-muted, #555);
+}
+.helptip-card-link {
+ display: inline-block;
+ margin-top: 0.45rem;
+ font-size: 0.72rem;
+ color: var(--theme-foreground-focus, #3b82f6);
+ text-decoration: none;
+}
+.helptip-card-link:hover { text-decoration: underline; }
+`;
+ document.head.appendChild(s);
+}
+
+class HelpTip extends HTMLElement {
+ #card = null;
+ #showTimer = null;
+ #hideTimer = null;
+
+ connectedCallback() {
+ this.addEventListener("mouseenter", this.#onEnter);
+ this.addEventListener("mouseleave", this.#onLeave);
+ this.addEventListener("focusin", this.#onEnter);
+ this.addEventListener("focusout", this.#onLeave);
+ }
+
+ disconnectedCallback() {
+ clearTimeout(this.#showTimer);
+ clearTimeout(this.#hideTimer);
+ this.#clearCard();
+ this.removeEventListener("mouseenter", this.#onEnter);
+ this.removeEventListener("mouseleave", this.#onLeave);
+ this.removeEventListener("focusin", this.#onEnter);
+ this.removeEventListener("focusout", this.#onLeave);
+ }
+
+ #onEnter = () => {
+ clearTimeout(this.#hideTimer);
+ this.#showTimer = setTimeout(() => this.#showCard(), 80);
+ };
+
+ #onLeave = () => {
+ clearTimeout(this.#showTimer);
+ this.#hideTimer = setTimeout(() => this.#clearCard(), 200);
+ };
+
+ #showCard() {
+ if (this.#card) return;
+
+ const label = this.getAttribute("label") || "";
+ const desc = this.getAttribute("description") || "";
+ const doc = this.getAttribute("doc") || "";
+
+ const card = document.createElement("div");
+ card.className = "helptip-card";
+
+ if (label) {
+ const h = document.createElement("div");
+ h.className = "helptip-card-label";
+ h.textContent = label;
+ card.appendChild(h);
+ }
+ if (desc) {
+ const d = document.createElement("div");
+ d.className = "helptip-card-desc";
+ d.textContent = desc;
+ card.appendChild(d);
+ }
+ if (doc) {
+ const a = document.createElement("a");
+ a.className = "helptip-card-link";
+ a.textContent = "Learn more →";
+ a.href = doc;
+ card.appendChild(a);
+ }
+
+ // Keep card alive while mouse is over it
+ card.addEventListener("mouseenter", () => clearTimeout(this.#hideTimer));
+ card.addEventListener("mouseleave", this.#onLeave);
+
+ document.body.appendChild(card);
+ this.#card = card;
+
+ this.#position(card);
+ requestAnimationFrame(() => card.classList.add("helptip-visible"));
+ }
+
+ #position(card) {
+ const rect = this.getBoundingClientRect();
+ const cw = card.offsetWidth || 230;
+ const ch = card.offsetHeight || 80;
+ const gap = 8;
+ const vw = window.innerWidth;
+ const vh = window.innerHeight;
+
+ // Horizontal: align to left of trigger, clamp inside viewport
+ let left = rect.left;
+ if (left + cw + gap > vw) left = vw - cw - gap;
+ if (left < gap) left = gap;
+
+ // Vertical: prefer above; fall back to below
+ const top = (rect.top - ch - gap >= 0)
+ ? rect.top - ch - gap
+ : Math.min(rect.bottom + gap, vh - ch - gap);
+
+ card.style.left = `${left}px`;
+ card.style.top = `${top}px`;
+ }
+
+ #clearCard() {
+ if (this.#card) {
+ this.#card.remove();
+ this.#card = null;
+ }
+ }
+}
+
+if (!customElements.get("help-tip")) {
+ customElements.define("help-tip", HelpTip);
+}
+
+export { HelpTip };
diff --git a/state-hub/dashboard/src/workstreams.md b/state-hub/dashboard/src/workstreams.md
index dc1b20c..be7f3d3 100644
--- a/state-hub/dashboard/src/workstreams.md
+++ b/state-hub/dashboard/src/workstreams.md
@@ -134,6 +134,7 @@ const _domainBreakdown = [...new Set(openWs.map(w => _idToDomain[w.id] ?? "unkno
```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
+import "./components/help-tip.js";
// ── Live indicator ────────────────────────────────────────────────────────────
const _liveEl = html`
@@ -158,11 +159,11 @@ function _warnLevel(name, val) {
function _warnColor(lv) { return lv === 2 ? "#dc2626" : lv === 1 ? "#d97706" : "var(--theme-foreground-muted, #666)"; }
const _whiMetrics = [
- {name: "DD", val: _DD, fmt: v => v.toFixed(2), tip: "Dependency Density — average number of dependencies per open workstream; high values indicate a tightly coupled graph that is hard to parallelise."},
- {name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%", tip: "Blocked Ratio — share of open workstreams currently in a blocked state; directly reduces the work that can proceed right now."},
- {name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%", tip: "Single-Point Risk — share of open workstreams that are depended on by others but have no incoming dependencies themselves; losing one stalls everything downstream."},
- {name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%", tip: "Parallel Execution Potential — share of open workstreams with zero blocking dependencies that could start or continue immediately."},
- {name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%", tip: "Cross-Domain Dependency Ratio — share of dependency edges that cross domain boundaries; high values mean progress in one domain is gated on another team or project."},
+ {name: "DD", val: _DD, fmt: v => v.toFixed(2), label: "Dependency Density", desc: "Average number of dependencies per open workstream; high values indicate a tightly coupled graph that is hard to parallelise."},
+ {name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%", label: "Blocked Ratio", desc: "Share of open workstreams currently in a blocked state; directly reduces the work that can proceed right now."},
+ {name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%", label: "Single-Point Risk", desc: "Share of workstreams depended on by others but with no incoming dependencies themselves; losing one stalls everything downstream."},
+ {name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%", label: "Parallel Execution Potential", desc: "Share of open workstreams with zero blocking dependencies that could start or continue immediately."},
+ {name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%", label: "Cross-Domain Dependency Ratio", desc: "Share of dependency edges that cross domain boundaries; high values mean progress in one domain is gated on another team or project."},
];
const _whiBox = html`
@@ -179,8 +180,8 @@ const _whiBox = html`
${_whiMetrics.map(m => {
const lv = _warnLevel(m.name, m.val);
return html`
- ${m.name}
- ${m.fmt(m.val)}
+ ${m.name}
+ ${m.fmt(m.val)}
`;
})}
@@ -189,9 +190,12 @@ const _whiBox = html`
by domain
${_domainBreakdown.map(d => html`
- ${d.domain}
+ ${d.domain}
${(d.whi*100).toFixed(0)}%
- ${d.cpi === 1 ? html`⚠` : ""}
+ ${d.cpi === 1 ? html`⚠` : ""}
`)}
` : ""}
`}
@@ -323,7 +327,7 @@ if (wsWithDeps.length === 0) {
.whi-cycle-alert { background: #fef2f2; color: #dc2626; border-radius: 4px; padding: 0.2rem 0.45rem; font-size: 0.72rem; font-weight: 600; margin-bottom: 0.4rem; }
.whi-metrics { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; margin-bottom: 0.35rem; }
.whi-metric-row { display: flex; justify-content: space-between; padding: 0.16rem 0; }
-.whi-metric-name { font-family: monospace; font-size: 0.72rem; text-decoration: underline dotted; text-underline-offset: 2px; cursor: help; }
+.whi-metric-name { font-family: monospace; font-size: 0.72rem; }
.whi-metric-val { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.78rem; }
.whi-domains { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; }
.whi-domain-header { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--theme-foreground-faint, #aaa); margin-bottom: 0.2rem; }