agentdynamics 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ /* Minimal SVG chart kit: columns (stacked), line/area, hbars, 100% stack, sparkline. */
2
+ const C = (() => {
3
+ const NS = "http://www.w3.org/2000/svg";
4
+ const css = (v) => getComputedStyle(document.documentElement).getPropertyValue(v).trim();
5
+ const SERIES = ["--s1", "--s2", "--s3", "--s4", "--s5", "--s6", "--s7", "--s8"];
6
+ // Phase colors follow the entity, never rank.
7
+ const PHASE = { explore: "--s1", edit: "--s2", verify: "--s3", execute: "--s4", plan: "--s7", delegate: "--s5",
8
+ vcs: "--s6", communicate: "--s8", respond: "--neutral", other: "--neutral" };
9
+ const color = (i) => `var(${SERIES[i % SERIES.length]})`;
10
+ const phaseColor = (p) => `var(${PHASE[p] || "--neutral"})`;
11
+ const tipEl = () => document.getElementById("tip");
12
+
13
+ function tip(html, ev) {
14
+ const t = tipEl();
15
+ if (!html) { t.style.display = "none"; return; }
16
+ t.innerHTML = html;
17
+ t.style.display = "block";
18
+ const w = t.offsetWidth, h = t.offsetHeight;
19
+ let x = ev.clientX + 14, y = ev.clientY + 14;
20
+ if (x + w > innerWidth - 8) x = ev.clientX - w - 14;
21
+ if (y + h > innerHeight - 8) y = ev.clientY - h - 14;
22
+ t.style.left = x + "px"; t.style.top = y + "px";
23
+ }
24
+ const hideTip = () => tip(null);
25
+
26
+ function el(tag, attrs = {}, parent) {
27
+ const e = document.createElementNS(NS, tag);
28
+ for (const k in attrs) e.setAttribute(k, attrs[k]);
29
+ if (parent) parent.appendChild(e);
30
+ return e;
31
+ }
32
+ function niceMax(v) {
33
+ if (!v || v <= 0) return 1;
34
+ const p = Math.pow(10, Math.floor(Math.log10(v)));
35
+ for (const m of [1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10]) if (m * p >= v) return m * p;
36
+ return 10 * p;
37
+ }
38
+ // top-rounded rect path anchored to baseline
39
+ function colPath(x, y, w, h, r) {
40
+ r = Math.min(r, w / 2, h);
41
+ if (h <= 0) return "";
42
+ return `M${x},${y + h}V${y + r}Q${x},${y} ${x + r},${y}H${x + w - r}Q${x + w},${y} ${x + w},${y + r}V${y + h}Z`;
43
+ }
44
+
45
+ /** columns(el, rows, {x, keys:[{key,label,color}], fmt, height, xfmt}) — stacked when several keys */
46
+ function columns(host, rows, o) {
47
+ host.innerHTML = "";
48
+ host.classList.add("chart");
49
+ const H = o.height || 200, W = host.clientWidth || 600;
50
+ const m = { l: 48, r: 8, t: 8, b: 24 };
51
+ const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, height: H }, host);
52
+ const keys = o.keys;
53
+ const totals = rows.map((r) => keys.reduce((s, k) => s + (+r[k.key] || 0), 0));
54
+ const max = niceMax(Math.max(...totals, 0));
55
+ const iw = W - m.l - m.r, ih = H - m.t - m.b;
56
+ for (let i = 0; i <= 4; i++) {
57
+ const y = m.t + ih - (ih * i) / 4;
58
+ el("line", { x1: m.l, x2: W - m.r, y1: y, y2: y, class: "gridline" }, svg);
59
+ el("text", { x: m.l - 6, y: y + 4, "text-anchor": "end" }, svg).textContent = o.fmt((max * i) / 4);
60
+ }
61
+ if (!rows.length) return;
62
+ const bw = iw / rows.length, gap = Math.max(1, Math.min(6, bw * 0.25)), w = Math.max(1, bw - gap);
63
+ const every = Math.ceil(rows.length / Math.max(1, Math.floor(iw / 70)));
64
+ rows.forEach((r, i) => {
65
+ const x = m.l + i * bw + gap / 2;
66
+ let base = m.t + ih;
67
+ keys.forEach((k, ki) => {
68
+ const v = +r[k.key] || 0;
69
+ const h = (v / max) * ih;
70
+ if (h <= 0) return;
71
+ const top = ki === keys.length - 1 || keys.slice(ki + 1).every((kk) => !(+r[kk.key]));
72
+ const hh = Math.max(0, h - (ki ? 2 : 0));
73
+ const p = top ? colPath(x, base - h, w, hh, 3) : `M${x},${base - h}h${w}v${hh}h${-w}Z`;
74
+ el("path", { d: p, fill: k.color }, svg);
75
+ base -= h;
76
+ });
77
+ if (i % every === 0) el("text", { x: x + w / 2, y: H - 6, "text-anchor": "middle" }, svg).textContent = (o.xfmt || ((d) => d))(r[o.x]);
78
+ const hit = el("rect", { x: m.l + i * bw, y: m.t, width: bw, height: ih, fill: "transparent" }, svg);
79
+ hit.addEventListener("mousemove", (ev) => {
80
+ let h = `<b>${(o.xfmt || ((d) => d))(r[o.x])}</b>`;
81
+ [...keys].reverse().forEach((k) => {
82
+ if (keys.length > 1 && !(+r[k.key])) return;
83
+ h += `<div class="tr"><span><i style="background:${k.color}"></i>${k.label}</span><b>${o.fmt(+r[k.key] || 0)}</b></div>`;
84
+ });
85
+ if (keys.length > 1) h += `<div class="tr"><span>Total</span><b>${o.fmt(totals[i])}</b></div>`;
86
+ tip(h, ev);
87
+ });
88
+ hit.addEventListener("mouseleave", hideTip);
89
+ if (o.onClick) { hit.style.cursor = "pointer"; hit.addEventListener("click", () => o.onClick(r)); }
90
+ });
91
+ if (keys.length > 1) legend(host, keys);
92
+ }
93
+
94
+ function legend(host, keys) {
95
+ const lg = document.createElement("div");
96
+ lg.className = "legend";
97
+ lg.innerHTML = keys.map((k) => `<span><i style="background:${k.color}"></i>${k.label}</span>`).join("");
98
+ host.appendChild(lg);
99
+ }
100
+
101
+ /** line(el, pts:[{x,y,...}], {fmt, xfmt, height, area, color, label, tipExtra, refLine}) */
102
+ function line(host, pts, o) {
103
+ host.innerHTML = "";
104
+ host.classList.add("chart");
105
+ const H = o.height || 180, W = host.clientWidth || 600;
106
+ const m = { l: 52, r: 10, t: 10, b: 24 };
107
+ const svg = el("svg", { viewBox: `0 0 ${W} ${H}`, height: H }, host);
108
+ const iw = W - m.l - m.r, ih = H - m.t - m.b;
109
+ const max = niceMax(Math.max(...pts.map((p) => p.y), o.refLine || 0, 0));
110
+ const xs = pts.map((p) => p.x);
111
+ const x0 = Math.min(...xs), x1 = Math.max(...xs);
112
+ const sx = (v) => m.l + (x1 === x0 ? iw / 2 : ((v - x0) / (x1 - x0)) * iw);
113
+ const sy = (v) => m.t + ih - (v / max) * ih;
114
+ for (let i = 0; i <= 4; i++) {
115
+ const y = m.t + ih - (ih * i) / 4;
116
+ el("line", { x1: m.l, x2: W - m.r, y1: y, y2: y, class: "gridline" }, svg);
117
+ el("text", { x: m.l - 6, y: y + 4, "text-anchor": "end" }, svg).textContent = o.fmt((max * i) / 4);
118
+ }
119
+ if (!pts.length) return;
120
+ const col = o.color || "var(--s1)";
121
+ const d = pts.map((p, i) => `${i ? "L" : "M"}${sx(p.x)},${sy(p.y)}`).join("");
122
+ if (o.area) el("path", { d: `${d}L${sx(pts.at(-1).x)},${m.t + ih}L${sx(pts[0].x)},${m.t + ih}Z`, fill: col, opacity: 0.12 }, svg);
123
+ if (o.refLine) {
124
+ el("line", { x1: m.l, x2: W - m.r, y1: sy(o.refLine), y2: sy(o.refLine), stroke: "var(--critical)", "stroke-dasharray": "4 4", "stroke-width": 1 }, svg);
125
+ el("text", { x: W - m.r, y: sy(o.refLine) - 4, "text-anchor": "end" }, svg).textContent = o.refLabel || "";
126
+ }
127
+ el("path", { d, fill: "none", stroke: col, "stroke-width": 2, "stroke-linejoin": "round" }, svg);
128
+ const ticks = Math.min(6, pts.length);
129
+ for (let i = 0; i < ticks; i++) {
130
+ const p = pts[Math.round((i * (pts.length - 1)) / Math.max(1, ticks - 1))];
131
+ el("text", { x: sx(p.x), y: H - 6, "text-anchor": "middle" }, svg).textContent = (o.xfmt || ((v) => v))(p.x);
132
+ }
133
+ const cross = el("line", { y1: m.t, y2: m.t + ih, stroke: "var(--text-3)", "stroke-width": 1, opacity: 0 }, svg);
134
+ const dot = el("circle", { r: 4, fill: col, stroke: "var(--surface)", "stroke-width": 2, opacity: 0 }, svg);
135
+ const hit = el("rect", { x: m.l, y: m.t, width: iw, height: ih, fill: "transparent" }, svg);
136
+ hit.addEventListener("mousemove", (ev) => {
137
+ const r = svg.getBoundingClientRect();
138
+ const mx = ((ev.clientX - r.left) / r.width) * W;
139
+ let best = pts[0];
140
+ for (const p of pts) if (Math.abs(sx(p.x) - mx) < Math.abs(sx(best.x) - mx)) best = p;
141
+ cross.setAttribute("x1", sx(best.x)); cross.setAttribute("x2", sx(best.x)); cross.setAttribute("opacity", 0.5);
142
+ dot.setAttribute("cx", sx(best.x)); dot.setAttribute("cy", sy(best.y)); dot.setAttribute("opacity", 1);
143
+ tip(`<b>${(o.xfmt || ((v) => v))(best.x)}</b><div class="tr"><span>${o.label || "value"}</span><b>${o.fmt(best.y)}</b></div>${o.tipExtra ? o.tipExtra(best) : ""}`, ev);
144
+ });
145
+ hit.addEventListener("mouseleave", () => { hideTip(); cross.setAttribute("opacity", 0); dot.setAttribute("opacity", 0); });
146
+ if (o.onClick) hit.addEventListener("click", (ev) => {
147
+ const r = svg.getBoundingClientRect(); const mx = ((ev.clientX - r.left) / r.width) * W;
148
+ let best = pts[0]; for (const p of pts) if (Math.abs(sx(p.x) - mx) < Math.abs(sx(best.x) - mx)) best = p;
149
+ o.onClick(best);
150
+ });
151
+ }
152
+
153
+ /** hbars(el, items:[{label, value, color?, sub?, href?}], {fmt}) */
154
+ function hbars(host, items, o = {}) {
155
+ const max = Math.max(...items.map((i) => i.value), 0) || 1;
156
+ host.innerHTML = items.map((i) => `
157
+ <div class="hbar" ${i.title ? `title="${i.title}"` : ""}>
158
+ <div class="k">${i.href ? `<a href="${i.href}">${i.label}</a>` : i.label}</div>
159
+ <div class="track"><div class="fill" style="width:${(100 * i.value) / max}%;background:${i.color || "var(--s1)"}"></div></div>
160
+ <div class="v">${(o.fmt || ((v) => v))(i.value)}</div>
161
+ </div>`).join("") || `<div class="empty">No data</div>`;
162
+ }
163
+
164
+ /** stack100(el, parts:[{key,label,value,color}]) with legend */
165
+ function stack100(host, parts, o = {}) {
166
+ const tot = parts.reduce((s, p) => s + p.value, 0) || 1;
167
+ const bar = parts.filter((p) => p.value > 0).map((p) =>
168
+ `<div style="width:${(100 * p.value) / tot}%;background:${p.color}" data-l="${p.label}" data-v="${p.value}"></div>`).join("");
169
+ host.innerHTML = `<div class="stack100">${bar}</div>` + (o.legend === false ? "" :
170
+ `<div class="legend">${parts.filter((p) => p.value > 0).map((p) => `<span><i style="background:${p.color}"></i>${p.label} ${Math.round((100 * p.value) / tot)}%</span>`).join("")}</div>`);
171
+ host.querySelectorAll(".stack100 > div").forEach((d) => {
172
+ d.addEventListener("mousemove", (ev) => tip(`<b>${d.dataset.l}</b><div class="tr"><span>share</span><b>${Math.round((100 * d.dataset.v) / tot)}%</b></div>${o.fmt ? `<div class="tr"><span>value</span><b>${o.fmt(+d.dataset.v)}</b></div>` : ""}`, ev));
173
+ d.addEventListener("mouseleave", hideTip);
174
+ });
175
+ }
176
+
177
+ function sparkline(values, w = 90, h = 22, col = "var(--s1)") {
178
+ if (!values.length) return "";
179
+ const max = Math.max(...values) || 1;
180
+ const pts = values.map((v, i) => `${values.length === 1 ? w / 2 : (i * w) / (values.length - 1)},${h - 2 - (v / max) * (h - 4)}`).join(" ");
181
+ return `<svg width="${w}" height="${h}" viewBox="0 0 ${w} ${h}"><polyline points="${pts}" fill="none" stroke="${col}" stroke-width="1.5" stroke-linejoin="round"/></svg>`;
182
+ }
183
+
184
+ return { columns, line, hbars, stack100, sparkline, color, phaseColor, tip, hideTip, css, PHASE };
185
+ })();
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>AgentDynamics</title>
7
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M2 13h4l3-8 4 14 3-9 2 3h4' fill='none' stroke='%232a78d6' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E">
8
+ <link rel="stylesheet" href="/style.css">
9
+ </head>
10
+ <body>
11
+ <div class="app">
12
+ <aside class="side">
13
+ <div class="brand">
14
+ <svg width="22" height="22" viewBox="0 0 24 24"><path d="M2 13h4l3-8 4 14 3-9 2 3h4" fill="none" stroke="var(--accent)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>
15
+ AgentDynamics
16
+ </div>
17
+ <nav id="nav"></nav>
18
+ </aside>
19
+ <div class="main">
20
+ <div class="topbar">
21
+ <label>Project <select id="f-project"><option value="">All projects</option></select></label>
22
+ <label id="f-env-wrap" hidden>Env <select id="f-env"></select></label>
23
+ <div class="seg" id="f-days">
24
+ <button data-v="1">24h</button><button data-v="7">7d</button><button data-v="30">30d</button><button data-v="">All</button>
25
+ </div>
26
+ <label title="Include delegated subagent runs as separate tasks"><input type="checkbox" id="f-sub"> Subagents as tasks</label>
27
+ <div class="spacer"></div>
28
+ <span class="small muted" id="refreshed"></span>
29
+ <button id="btn-refresh" title="Re-scan sources now">Refresh</button>
30
+ <button id="btn-theme" title="Toggle theme" aria-label="Toggle theme">◐</button>
31
+ </div>
32
+ <div id="page" class="page"><div class="loading">Loading…</div></div>
33
+ </div>
34
+ </div>
35
+ <div class="tip" id="tip"></div>
36
+ <div class="toast" id="toast"></div>
37
+ <script src="/charts.js"></script>
38
+ <script src="/app.js"></script>
39
+ </body>
40
+ </html>
@@ -0,0 +1,244 @@
1
+ :root {
2
+ color-scheme: light;
3
+ --bg: #f4f4f2;
4
+ --surface: #fcfcfb;
5
+ --surface-2: #f0efec;
6
+ --border: #e2e1dc;
7
+ --text: #0b0b0b;
8
+ --text-2: #52514e;
9
+ --text-3: #7d7c77;
10
+ --accent: #2a78d6;
11
+ --accent-soft: #e3eefb;
12
+ --good: #0ca30c; --warning: #fab219; --serious: #ec835a; --critical: #d03b3b;
13
+ --good-text: #006300; --warning-text: #8a5a00; --critical-text: #b02a2a;
14
+ --s1: #2a78d6; --s2: #eb6834; --s3: #1baf7a; --s4: #eda100; --s5: #e87ba4; --s6: #008300; --s7: #4a3aa7; --s8: #e34948;
15
+ --neutral: #b4b2ab;
16
+ --grid: #ebeae6;
17
+ --shadow: 0 1px 2px rgba(0,0,0,.05), 0 2px 8px rgba(0,0,0,.04);
18
+ --mono: ui-monospace, "Cascadia Code", Consolas, monospace;
19
+ }
20
+ @media (prefers-color-scheme: dark) {
21
+ :root:not([data-theme="light"]) {
22
+ color-scheme: dark;
23
+ --bg: #121211; --surface: #1a1a19; --surface-2: #232321; --border: #2f2f2c;
24
+ --text: #ffffff; --text-2: #c3c2b7; --text-3: #8d8c85;
25
+ --accent: #3987e5; --accent-soft: #1b2b40;
26
+ --good-text: #4cc94c; --warning-text: #fab219; --critical-text: #ef7070;
27
+ --s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --s5: #d55181; --s6: #008300; --s7: #9085e9; --s8: #e66767;
28
+ --neutral: #5d5c57; --grid: #2a2a28;
29
+ --shadow: none;
30
+ }
31
+ }
32
+ :root[data-theme="dark"] {
33
+ color-scheme: dark;
34
+ --bg: #121211; --surface: #1a1a19; --surface-2: #232321; --border: #2f2f2c;
35
+ --text: #ffffff; --text-2: #c3c2b7; --text-3: #8d8c85;
36
+ --accent: #3987e5; --accent-soft: #1b2b40;
37
+ --good-text: #4cc94c; --warning-text: #fab219; --critical-text: #ef7070;
38
+ --s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --s5: #d55181; --s6: #008300; --s7: #9085e9; --s8: #e66767;
39
+ --neutral: #5d5c57; --grid: #2a2a28;
40
+ --shadow: none;
41
+ }
42
+
43
+ * { box-sizing: border-box; }
44
+ html, body { margin: 0; height: 100%; }
45
+ body {
46
+ background: var(--bg); color: var(--text);
47
+ font: 14px/1.45 "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
48
+ font-variant-numeric: tabular-nums;
49
+ }
50
+ a { color: var(--accent); text-decoration: none; }
51
+ a:hover { text-decoration: underline; }
52
+ h1 { font-size: 20px; margin: 0; font-weight: 650; letter-spacing: -.01em; }
53
+ h2 { font-size: 14px; margin: 0 0 10px; font-weight: 600; }
54
+ h3 { font-size: 13px; margin: 0 0 6px; font-weight: 600; color: var(--text-2); }
55
+ code, .mono { font-family: var(--mono); font-size: 12px; }
56
+
57
+ .app { display: grid; grid-template-columns: 220px 1fr; min-height: 100vh; }
58
+ .side {
59
+ background: var(--surface); border-right: 1px solid var(--border); padding: 16px 10px;
60
+ position: sticky; top: 0; height: 100vh; overflow-y: auto;
61
+ }
62
+ .brand { display: flex; align-items: center; gap: 8px; padding: 0 8px 16px; font-weight: 700; font-size: 15px; }
63
+ .brand svg { flex: none; }
64
+ .nav-group { margin-bottom: 14px; }
65
+ .nav-group > div { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--text-3); padding: 4px 10px; }
66
+ .nav-group a {
67
+ display: flex; justify-content: space-between; align-items: center;
68
+ padding: 6px 10px; border-radius: 6px; color: var(--text-2); font-size: 13.5px;
69
+ }
70
+ .nav-group a:hover { background: var(--surface-2); text-decoration: none; color: var(--text); }
71
+ .nav-group a.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
72
+ .nav-group a small { color: var(--text-3); font-size: 11px; font-weight: 400; }
73
+
74
+ .main { min-width: 0; }
75
+ .topbar {
76
+ display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
77
+ padding: 10px 24px; background: var(--surface); border-bottom: 1px solid var(--border);
78
+ position: sticky; top: 0; z-index: 5;
79
+ }
80
+ .topbar .spacer { flex: 1; }
81
+ .topbar label { font-size: 12px; color: var(--text-3); display: flex; gap: 6px; align-items: center; }
82
+ select, input[type=text], input[type=number], textarea {
83
+ background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
84
+ padding: 5px 8px; font: inherit; font-size: 13px;
85
+ }
86
+ button, .btn {
87
+ background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px;
88
+ padding: 5px 12px; font: inherit; font-size: 13px; cursor: pointer;
89
+ }
90
+ button:hover { background: var(--surface-2); }
91
+ button.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
92
+ .seg { display: inline-flex; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; }
93
+ .seg button { border: 0; border-radius: 0; padding: 5px 10px; }
94
+ .seg button.on { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
95
+ .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; background: var(--good); }
96
+
97
+ .page { padding: 20px 24px 60px; max-width: 1500px; }
98
+ .page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
99
+ .page-head p { margin: 4px 0 0; color: var(--text-2); font-size: 13px; max-width: 780px; }
100
+
101
+ .grid { display: grid; gap: 14px; }
102
+ .g2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
103
+ .g3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
104
+ .g-3-2 { grid-template-columns: 3fr 2fr; }
105
+ .g-2-1 { grid-template-columns: 2fr 1fr; }
106
+ .span2 { grid-column: span 2; }
107
+ @media (max-width: 1100px) { .g2, .g3, .g-3-2, .g-2-1 { grid-template-columns: 1fr; } .span2 { grid-column: auto; } }
108
+
109
+ .card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; box-shadow: var(--shadow); min-width: 0; }
110
+ .card-head { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 8px; }
111
+ .card-head h2 { margin: 0; }
112
+ .card-head .sub { color: var(--text-3); font-size: 12px; }
113
+
114
+ .kpis { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; margin-bottom: 14px; }
115
+ .kpi { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 12px 14px; box-shadow: var(--shadow); }
116
+ .kpi .label { font-size: 12px; color: var(--text-2); display: flex; align-items: center; gap: 6px; }
117
+ .kpi .value { font-size: 24px; font-weight: 650; margin-top: 2px; letter-spacing: -.02em; }
118
+ .kpi .hint { font-size: 11.5px; color: var(--text-3); margin-top: 2px; }
119
+
120
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
121
+ th { text-align: left; font-weight: 600; color: var(--text-3); font-size: 11.5px; text-transform: uppercase; letter-spacing: .04em;
122
+ padding: 6px 8px; border-bottom: 1px solid var(--border); white-space: nowrap; cursor: default; }
123
+ th.sortable { cursor: pointer; }
124
+ td { padding: 7px 8px; border-bottom: 1px solid var(--grid); vertical-align: top; }
125
+ tr.click { cursor: pointer; }
126
+ tr.click:hover td { background: var(--surface-2); }
127
+ td.num, th.num { text-align: right; white-space: nowrap; }
128
+ .truncate { max-width: 460px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
129
+ .table-wrap { overflow-x: auto; }
130
+
131
+ .pill { display: inline-flex; align-items: center; gap: 5px; padding: 1px 8px; border-radius: 999px; font-size: 11.5px;
132
+ font-weight: 600; background: var(--surface-2); color: var(--text-2); white-space: nowrap; }
133
+ .pill::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--neutral); }
134
+ .pill.normal::before, .pill.satisfied::before, .pill.completed::before, .pill.ok::before { background: var(--good); }
135
+ .pill.warning::before, .pill.tolerating::before, .pill.rework::before, .pill.info::before { background: var(--warning); }
136
+ .pill.serious::before { background: var(--serious); }
137
+ .pill.critical::before, .pill.frustrated::before, .pill.interrupted::before, .pill.failed::before { background: var(--critical); }
138
+ .pill.in-progress::before { background: var(--accent); }
139
+ .pill.info::before { background: var(--accent); }
140
+ .tag { display: inline-block; font-size: 11px; padding: 0 6px; border-radius: 4px; background: var(--surface-2); color: var(--text-2); margin-right: 4px; border: 1px solid var(--border); }
141
+ .tag.bad { color: var(--critical-text); border-color: color-mix(in srgb, var(--critical) 40%, transparent); }
142
+ .tag.warn { color: var(--warning-text); border-color: color-mix(in srgb, var(--warning) 50%, transparent); }
143
+
144
+ .muted { color: var(--text-3); }
145
+ .small { font-size: 12px; }
146
+ .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
147
+ .between { display: flex; justify-content: space-between; align-items: center; gap: 10px; }
148
+ .empty { color: var(--text-3); padding: 24px; text-align: center; }
149
+ .loading { color: var(--text-3); padding: 40px; text-align: center; }
150
+
151
+ /* charts */
152
+ .chart { position: relative; width: 100%; }
153
+ .chart svg { display: block; width: 100%; overflow: visible; }
154
+ .chart text { fill: var(--text-3); font-size: 11px; }
155
+ .chart .gridline { stroke: var(--grid); stroke-width: 1; }
156
+ .chart .axis { stroke: var(--border); }
157
+ .legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--text-2); margin-top: 6px; }
158
+ .legend span { display: inline-flex; align-items: center; gap: 6px; }
159
+ .legend i { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
160
+ .tip {
161
+ position: fixed; z-index: 50; pointer-events: none; background: var(--surface); color: var(--text);
162
+ border: 1px solid var(--border); border-radius: 8px; padding: 8px 10px; font-size: 12px; box-shadow: 0 4px 16px rgba(0,0,0,.14);
163
+ max-width: 360px; display: none;
164
+ }
165
+ .tip b { font-weight: 600; }
166
+ .tip .tr { display: flex; justify-content: space-between; gap: 16px; }
167
+ .tip .tr i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; margin-right: 6px; }
168
+
169
+ .hbar { display: grid; grid-template-columns: minmax(90px, 160px) 1fr 70px; gap: 8px; align-items: center; font-size: 12.5px; margin: 5px 0; }
170
+ .hbar .track { height: 10px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
171
+ .hbar .fill { height: 100%; border-radius: 0 4px 4px 0; background: var(--s1); }
172
+ .hbar .v { text-align: right; color: var(--text-2); }
173
+ .hbar .k { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
174
+
175
+ .stack100 { display: flex; height: 14px; border-radius: 4px; overflow: hidden; gap: 2px; background: var(--surface); }
176
+ .stack100 > div { height: 100%; }
177
+
178
+ .score-row { display: grid; grid-template-columns: 110px 1fr 40px; gap: 8px; align-items: center; margin: 6px 0; font-size: 13px; }
179
+ .score-row .track { height: 8px; background: var(--surface-2); border-radius: 4px; overflow: hidden; }
180
+ .score-row .fill { height: 100%; border-radius: 4px; }
181
+ .big-score { font-size: 40px; font-weight: 700; letter-spacing: -.03em; line-height: 1; }
182
+
183
+ /* waterfall */
184
+ .wf { font-size: 12px; }
185
+ .wf-row { display: grid; grid-template-columns: 34px 170px 1fr 80px 70px; gap: 8px; align-items: center; padding: 3px 0; border-bottom: 1px solid var(--grid); cursor: pointer; }
186
+ .wf-row:hover { background: var(--surface-2); }
187
+ .wf-row.sel { background: var(--accent-soft); }
188
+ .wf-track { position: relative; height: 14px; }
189
+ .wf-bar { position: absolute; top: 2px; height: 10px; border-radius: 3px; min-width: 2px; }
190
+ .wf-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
191
+ .wf-detail { background: var(--surface-2); border-radius: 8px; padding: 10px 12px; margin: 4px 0 8px; font-size: 12px; white-space: pre-wrap; word-break: break-word; font-family: var(--mono); max-height: 320px; overflow: auto; }
192
+
193
+ .insight { border-left: 3px solid var(--neutral); padding: 10px 14px; background: var(--surface); border-radius: 0 10px 10px 0; border-top: 1px solid var(--border); border-right: 1px solid var(--border); border-bottom: 1px solid var(--border); }
194
+ .insight.warning { border-left-color: var(--warning); }
195
+ .insight.ok { border-left-color: var(--good); }
196
+ .insight.info { border-left-color: var(--accent); }
197
+ .insight .metric { font-size: 18px; font-weight: 650; margin: 2px 0 4px; }
198
+ .insight .advice { font-size: 12.5px; color: var(--text-2); margin-top: 6px; }
199
+
200
+ .flow-wrap { display: grid; grid-template-columns: 1fr 300px; gap: 14px; }
201
+ @media (max-width: 1100px) { .flow-wrap { grid-template-columns: 1fr; } }
202
+ .flow svg { width: 100%; display: block; }
203
+ .flow .node rect { fill: var(--surface); stroke: var(--border); stroke-width: 1.5; }
204
+ .flow .node:hover rect { stroke: var(--accent); }
205
+ .flow .node.sel rect { stroke: var(--accent); stroke-width: 2.5; }
206
+ .flow .node text { fill: var(--text); font-size: 12px; }
207
+ .flow .node text.sub { fill: var(--text-3); font-size: 10.5px; }
208
+ .flow .edge { fill: none; stroke: var(--neutral); opacity: .7; }
209
+ .flow .edge.err { stroke: var(--critical); }
210
+ .flow .elabel { fill: var(--text-3); font-size: 10px; }
211
+ .flow .col-title { fill: var(--text-3); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }
212
+
213
+ .prompt-box { background: var(--surface-2); border-radius: 8px; padding: 10px 12px; white-space: pre-wrap; word-break: break-word; max-height: 180px; overflow: auto; font-size: 13px; }
214
+ details summary { cursor: pointer; color: var(--text-2); font-size: 12.5px; }
215
+ .code-block { background: var(--surface-2); border: 1px solid var(--border); border-radius: 8px; padding: 12px; overflow-x: auto; font-family: var(--mono); font-size: 12px; white-space: pre; }
216
+ .rule-row input[type=number] { width: 90px; }
217
+ .toast { position: fixed; bottom: 18px; right: 18px; background: var(--text); color: var(--bg); padding: 8px 14px; border-radius: 8px; font-size: 13px; opacity: 0; transition: opacity .2s; z-index: 60; }
218
+ .toast.show { opacity: 1; }
219
+
220
+ @media (max-width: 760px) {
221
+ .app { grid-template-columns: 1fr; }
222
+ .side { position: static; height: auto; display: flex; overflow-x: auto; gap: 4px; padding: 8px; }
223
+ .brand { display: none; }
224
+ .side nav { display: flex; gap: 2px; }
225
+ .nav-group { display: flex; margin: 0; }
226
+ .nav-group > div { display: none; }
227
+ .nav-group a { white-space: nowrap; }
228
+ .page { padding: 16px; }
229
+ .topbar { padding: 8px 16px; }
230
+ .wf-row { grid-template-columns: 28px 110px 1fr 60px; }
231
+ .wf-row > :last-child { display: none; }
232
+ }
233
+ .spacer { flex: 1; }
234
+
235
+ .stack-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 10px; }
236
+ .stack { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; text-align: left; padding: 10px 12px; border-radius: 8px; }
237
+ .stack span { font-size: 12px; color: var(--text-3); }
238
+ .stack.on { border-color: var(--accent); background: var(--accent-soft); }
239
+ .stack.on b { color: var(--accent); }
240
+ .live { display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-radius: 8px; background: var(--surface-2); font-size: 14px; }
241
+ .live.ok { background: color-mix(in srgb, var(--good) 14%, var(--surface)); }
242
+ .spinner { width: 14px; height: 14px; border-radius: 50%; border: 2px solid var(--border); border-top-color: var(--accent); animation: spin 0.9s linear infinite; }
243
+ @keyframes spin { to { transform: rotate(360deg); } }
244
+ @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }