spectoflow 0.13.0 → 0.13.2

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.
@@ -1,5 +1,5 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" data-theme="dark">
2
+ <html lang="en" data-theme="dark" data-design="control-room">
3
3
  <head>
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -10,6 +10,7 @@
10
10
  <body>
11
11
  <header class="topbar">
12
12
  <div class="brand">
13
+ <button class="nav-toggle" id="navToggle" aria-label="Toggle menu" aria-expanded="false"><span></span><span></span><span></span></button>
13
14
  <a class="brand-logo" href="/board" data-route="board" aria-label="spectoflow home">
14
15
  <img class="brand-logo-img is-dark" src="logo-white.png" alt="spectoflow" />
15
16
  <img class="brand-logo-img is-light" src="logo-dark.png" alt="spectoflow" />
@@ -17,6 +18,7 @@
17
18
  <div class="brand-text">
18
19
  <div class="brand-line">
19
20
  <span class="brand-name">spectoflow</span>
21
+ <span class="brand-ver" id="brandVer" hidden></span>
20
22
  <span class="brand-sep">/</span>
21
23
  <span class="brand-project" id="projectName">—</span>
22
24
  </div>
@@ -35,38 +37,12 @@
35
37
  <button class="tab" data-tab="team"><span class="tab-ico" data-icon="agents"></span><span class="tab-label">Agents &amp; Skills</span></button>
36
38
  <button class="tab" data-tab="chat"><span class="tab-ico" data-icon="chat"></span><span class="tab-label">Chat</span></button>
37
39
  <button class="tab" data-tab="info"><span class="tab-ico" data-icon="info"></span><span class="tab-label">Info</span></button>
40
+ <button class="tab" data-tab="settings"><span class="tab-ico" data-icon="settings"></span><span class="tab-label">Settings</span></button>
38
41
  </nav>
39
42
  <div class="top-right">
40
- <span class="meta-chip" id="agentChip" title="Active agent">claude</span>
41
- <span class="meta-chip" id="langChip" title="Output language">en</span>
42
- <span class="mode-chip" id="modeChip" title="Autonomy mode">semi</span>
43
43
  <span class="sync" id="sync"><span class="sync-dot"></span><span id="syncLabel">live</span></span>
44
44
  <button class="btn primary run-btn" id="runQuickBtn" title="Open the run chat"><span class="tab-ico" data-icon="run"></span><span>Run</span></button>
45
- <button class="icon-btn" id="settingsBtn" aria-label="Settings" title="Settings"><span class="tab-ico" data-icon="settings"></span></button>
46
- <button class="theme-toggle" id="themeToggle" aria-label="Toggle theme"><span class="theme-ico"></span></button>
47
- </div>
48
-
49
- <!-- settings popover (mode + language) -->
50
- <div class="settings-pop" id="settingsPop" hidden role="dialog" aria-label="Settings">
51
- <div class="settings-head">Settings</div>
52
- <label class="settings-row"><span>Autonomy mode</span>
53
- <select id="setMode">
54
- <option value="autopilot">autopilot</option>
55
- <option value="semi">semi</option>
56
- <option value="manual">manual</option>
57
- </select>
58
- </label>
59
- <label class="settings-row"><span>Output language</span>
60
- <select id="setLang">
61
- <option value="en">en · English</option>
62
- <option value="fr">fr · Français</option>
63
- <option value="es">es · Español</option>
64
- <option value="de">de · Deutsch</option>
65
- <option value="pt">pt · Português</option>
66
- <option value="it">it · Italiano</option>
67
- </select>
68
- </label>
69
- <p class="settings-note">Writes <code>.spectoflow/config.json</code> — the agent picks it up on its next run.</p>
45
+ <button class="theme-toggle" id="themeToggle" aria-label="Toggle light / dark theme" title="Toggle light / dark"><span class="tab-ico" data-icon="theme"></span></button>
70
46
  </div>
71
47
  </header>
72
48
 
@@ -174,7 +150,7 @@
174
150
  <section class="panel" data-panel="workflow">
175
151
  <div class="wf-wrap">
176
152
  <h2 class="panel-title">Active workflow</h2>
177
- <p class="panel-sub">Click a step to enable or disable it. This edits <code>.spectoflow/workflow.md</code>.</p>
153
+ <p class="panel-sub">The pipeline your requests flow through — from idea to shipped, spec-driven. <strong>Click a step</strong> to see what it does and turn it on or off (this edits <code>.spectoflow/workflow.md</code>). Enabled steps run in order; the orchestrator gates each one by your <em>mode</em> and <em>policy</em>. Optional steps stay off until you switch them on.</p>
178
154
  <div class="wf-diagram" id="wfDiagram"></div>
179
155
  </div>
180
156
  </section>
@@ -224,8 +200,61 @@
224
200
  <div class="info-grid" id="infoGrid"></div>
225
201
  </div>
226
202
  </section>
203
+
204
+ <!-- SETTINGS (change autonomy mode + output language → config.json) -->
205
+ <section class="panel" data-panel="settings">
206
+ <div class="settings-wrap">
207
+ <h2 class="panel-title">Settings</h2>
208
+ <p class="panel-sub">Change how spectoflow runs. Writes <code>.spectoflow/config.json</code> — the agent picks it up on its next run.</p>
209
+ <div class="settings-card">
210
+ <label class="settings-field">
211
+ <span class="settings-field-label">Dashboard design</span>
212
+ <select id="setDesign" class="settings-select"></select>
213
+ </label>
214
+ <label class="settings-field">
215
+ <span class="settings-field-label">Autonomy mode</span>
216
+ <select id="setMode" class="settings-select">
217
+ <option value="autopilot">autopilot — run freely, ask only on policy gates</option>
218
+ <option value="semi">semi — autopilot + policy approvals</option>
219
+ <option value="manual">manual — approve every step</option>
220
+ </select>
221
+ </label>
222
+ <label class="settings-field">
223
+ <span class="settings-field-label">Output language</span>
224
+ <select id="setLang" class="settings-select">
225
+ <option value="en">en · English</option>
226
+ <option value="fr">fr · Français</option>
227
+ <option value="es">es · Español</option>
228
+ <option value="de">de · Deutsch</option>
229
+ <option value="pt">pt · Português</option>
230
+ <option value="it">it · Italiano</option>
231
+ </select>
232
+ </label>
233
+ <div class="settings-saved" id="settingsSaved" hidden>✓ saved</div>
234
+ </div>
235
+ <div class="settings-readonly" id="settingsReadonly"></div>
236
+ </div>
237
+ </section>
227
238
  </main>
228
239
 
240
+ <footer class="app-footer">
241
+ <div class="footer-left">
242
+ <a class="footer-logo" href="/board" data-route="board" aria-label="spectoflow">
243
+ <img class="brand-logo-img is-dark" src="logo-white.png" alt="spectoflow" />
244
+ <img class="brand-logo-img is-light" src="logo-dark.png" alt="spectoflow" />
245
+ </a>
246
+ <span class="footer-name">spectoflow</span>
247
+ <span class="footer-ver" id="footerVer"></span>
248
+ <span class="footer-sep">·</span>
249
+ <span class="footer-lic">MIT License</span>
250
+ </div>
251
+ <div class="footer-right">
252
+ <a href="https://github.com/georgesmomo/spectoflow" target="_blank" rel="noopener">GitHub</a>
253
+ <a href="https://www.npmjs.com/package/spectoflow" target="_blank" rel="noopener">npm</a>
254
+ <button class="footer-link" id="footerSettings">Settings</button>
255
+ </div>
256
+ </footer>
257
+
229
258
  <!-- CHAT WIDGET (floating agent runner; entry point to the group-chat) -->
230
259
  <button class="chat-fab" id="chatFab" aria-label="Open run chat" title="Run an agent">💬</button>
231
260
  <div class="chat" id="chat" aria-hidden="true" role="dialog" aria-label="Run an agent">
@@ -257,6 +286,9 @@
257
286
  <!-- floating chart tooltip layer — positioned on mousemove over [data-tip] hit targets -->
258
287
  <div id="tooltip" class="tooltip" hidden></div>
259
288
 
289
+ <!-- workflow step popover — anchored to the clicked step, holds its details + toggle -->
290
+ <div id="wfPop" class="wf-pop" hidden></div>
291
+
260
292
  <!-- shared gradient used by SpectoCharts.ring's foreground arc (stroke="url(#grad)") -->
261
293
  <svg width="0" height="0" style="position:absolute" aria-hidden="true">
262
294
  <defs>
@@ -270,6 +302,7 @@
270
302
  <script src="stats.js"></script>
271
303
  <script src="charts.js"></script>
272
304
  <script src="icons.js"></script>
305
+ <script src="designs.js"></script>
273
306
  <script src="app.js"></script>
274
307
  </body>
275
308
  </html>
@@ -1,23 +1,25 @@
1
- /* spectoflow control plane. Direction: an engineering control room — calm, dense, precise.
2
- One signature: the amber "signal" for what is live/active. Mono carries ids and statuses. */
1
+ /* spectoflow control plane. Direction: a calm, dense control room.
2
+ Signature: a violet "signal" for what is live/active; green for delivered; teal secondary.
3
+ The amber brand logo is the one deliberate warm accent. Mono carries ids and statuses. */
3
4
  :root {
4
- --bg: #10151b; --surface: #171e26; --surface-2: #1e2732; --line: #2a3641;
5
- --ink: #e9edf2; --muted: #94a2af; --faint: #63727e;
6
- --signal: #e6a54b; --cool: #5fb2cc;
7
- --s-todo: #7d8f97; --s-in_progress: #e6a54b; --s-to_validate: #5fb2cc;
8
- --s-to_analyze: #ab8cd9; --s-done: #5fb67e; --s-blocked: #db7268;
9
- --radius: 14px; --shadow: 0 10px 30px rgba(0,0,0,.38);
5
+ --bg: #0f1116; --surface: #171922; --surface-2: #1e2130; --line: #2a2e3d;
6
+ --ink: #e9edf4; --muted: #9aa3b5; --faint: #6a7289;
7
+ --signal: #7c5cff; --cool: #4bb3a7; --on-accent: #ffffff;
8
+ --s-todo: #7d8695; --s-in_progress: #e6a54b; --s-to_validate: #4bb3a7;
9
+ --s-to_analyze: #ab8cd9; --s-done: #4caf72; --s-blocked: #e0788a;
10
+ --radius: 14px; --shadow: 0 12px 34px rgba(0,0,0,.42);
10
11
  --mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
11
12
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
13
+ --display: var(--sans); /* headings / big numbers — designs may swap in a display face */
12
14
  }
13
- [data-theme="light"] { --bg:#f6f3ee; --surface:#fffdfa; --surface-2:#efe9e0; --line:#ddd4c6; --ink:#211b12; --muted:#6b6255; --faint:#948a7b; --signal:#c17d20; --cool:#2c7f99; --s-todo:#6b7680; --s-in_progress:#c17d20; --s-to_validate:#2c7f99; --s-to_analyze:#8a6bb0; --s-done:#3f8f5c; --s-blocked:#c14c3f; }
15
+ [data-theme="light"] { --bg:#f4f5f9; --surface:#ffffff; --surface-2:#eef0f6; --line:#dde0ea; --ink:#1b1e28; --muted:#5c6478; --faint:#8b93a6; --signal:#6d54e0; --cool:#2f8e82; --on-accent:#ffffff; --s-todo:#6b7280; --s-in_progress:#c17d20; --s-to_validate:#2f8e82; --s-to_analyze:#8a6bb0; --s-done:#3f9f63; --s-blocked:#c85a6e; }
14
16
  * { box-sizing: border-box; }
15
17
  html,body { margin:0; height:100%; }
16
- body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; }
18
+ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-size:14px; line-height:1.5; -webkit-font-smoothing:antialiased; overflow-x:hidden; }
17
19
  @media (prefers-reduced-motion: reduce){ *{ animation:none!important; transition:none!important; } }
18
20
 
19
21
  /* topbar — dense engineering-control-room header: brand+meter (left) / icon tabs (centre) / status+run (right) */
20
- .topbar { display:grid; grid-template-columns:1fr auto 1fr; align-items:center; column-gap:18px; min-height:60px; padding:8px 18px; border-bottom:1px solid var(--line);
22
+ .topbar { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; column-gap:14px; min-height:60px; padding:8px 16px; border-bottom:1px solid var(--line);
21
23
  background:color-mix(in srgb,var(--surface) 92%,transparent); position:sticky; top:0; z-index:6; backdrop-filter:blur(8px); }
22
24
  .brand { justify-self:start; display:flex; align-items:center; gap:11px; font-weight:600; min-width:0; }
23
25
  .brand-mark { width:16px; height:16px; border-radius:4px; background:conic-gradient(from 210deg,var(--signal),var(--cool),var(--signal)); box-shadow:0 0 0 1px var(--line) inset; flex-shrink:0; }
@@ -27,13 +29,14 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
27
29
  .brand-sub { font-family:var(--mono); font-size:9.5px; text-transform:uppercase; letter-spacing:.08em; color:var(--faint); }
28
30
  .progress-meter { width:76px; height:5px; border-radius:999px; background:var(--surface-2); overflow:hidden; flex-shrink:0; margin-left:4px; }
29
31
  .progress-meter-fill { height:100%; width:0%; background:linear-gradient(90deg,var(--cool),var(--signal)); border-radius:999px; transition:width .5s ease; }
30
- .tabs { justify-self:center; display:flex; gap:2px; }
32
+ .tabs { justify-self:center; display:flex; gap:2px; min-width:0; max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; }
33
+ .tabs::-webkit-scrollbar { display:none; }
31
34
  .tab { display:flex; align-items:center; gap:6px; font:inherit; font-size:12.5px; padding:7px 11px; border:0; background:transparent; color:var(--muted); cursor:pointer; border-radius:7px; white-space:nowrap; }
32
35
  .tab-ico { width:16px; height:16px; flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; }
33
36
  .tab-ico svg { width:100%; height:100%; display:block; }
34
37
  .tab:hover { color:var(--ink); background:var(--surface-2); }
35
38
  .tab.is-active { color:var(--ink); background:var(--surface-2); box-shadow:inset 0 -2px 0 var(--signal); }
36
- .top-right { justify-self:end; display:flex; align-items:center; gap:9px; }
39
+ .top-right { justify-self:end; display:flex; align-items:center; gap:9px; min-width:0; }
37
40
  .meta-chip,.mode-chip { font-family:var(--mono); font-size:11px; text-transform:uppercase; letter-spacing:.06em; padding:3px 8px; border:1px solid var(--line); border-radius:999px; color:var(--muted); }
38
41
  .mode-chip { color:var(--signal); border-color:color-mix(in srgb,var(--signal) 40%,var(--line)); }
39
42
  .sync { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--muted); }
@@ -137,7 +140,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
137
140
  .chip-list { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
138
141
  .fchip { font-family:var(--mono); font-size:11px; text-transform:uppercase; letter-spacing:.05em; padding:5px 11px; border-radius:999px; border:1px solid var(--line); background:var(--surface); color:var(--muted); cursor:pointer; transition:all .15s; }
139
142
  .fchip:hover { color:var(--ink); border-color:var(--cool); }
140
- .fchip.active { color:#10151b; border-color:transparent; font-weight:700; background:var(--muted); }
143
+ .fchip.active { color:var(--on-accent); border-color:transparent; font-weight:700; background:var(--muted); }
141
144
  .fchip[data-status="all"].active { background:var(--signal); }
142
145
  .fchip[data-status="todo"].active { background:var(--s-todo); }
143
146
  .fchip[data-status="in_progress"].active { background:var(--s-in_progress); }
@@ -255,7 +258,8 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
255
258
 
256
259
  /* workflow diagram */
257
260
  .wf-wrap,.team-wrap { padding:26px 28px 60px; max-width:1000px; }
258
- .wf-diagram { display:flex; flex-wrap:wrap; align-items:center; gap:0; margin-top:8px; }
261
+ .wf-wrap { max-width:1340px; } /* wider so the horizontal pipeline fits its 9 steps on one line */
262
+ .wf-diagram { display:block; margin-top:8px; }
259
263
  .wf-step { display:flex; align-items:center; }
260
264
  .wf-node { display:flex; align-items:center; gap:8px; padding:11px 15px; border:1px solid var(--line); border-radius:var(--radius); background:var(--surface); cursor:pointer; transition:border-color .18s,transform .15s,box-shadow .15s; user-select:none; }
261
265
  .wf-node:hover { border-color:var(--cool); transform:translateY(-1px); box-shadow:var(--shadow); }
@@ -311,27 +315,39 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
311
315
  [data-theme="light"] .md-body pre code { color:var(--ink); }
312
316
  .md-body strong { font-weight:700; }
313
317
  .status-btn { font-family:var(--mono); font-size:11px; padding:4px 9px; border:1px solid var(--line); border-radius:999px; background:var(--surface-2); color:var(--muted); cursor:pointer; }
314
- .status-btn.active { color:#10151b; border-color:transparent; font-weight:600; }
318
+ .status-btn.active { color:var(--on-accent); border-color:transparent; font-weight:600; }
315
319
  .comments { display:flex; flex-direction:column; gap:8px; }
316
320
  .comment { background:var(--surface-2); border:1px solid var(--line); border-radius:8px; padding:8px 10px; font-size:13px; }
317
321
  .c-box { display:flex; flex-direction:column; gap:8px; margin-top:8px; }
318
322
  .c-box textarea { width:100%; min-height:60px; resize:vertical; background:var(--bg); color:var(--ink); border:1px solid var(--line); border-radius:8px; padding:8px 10px; font:inherit; }
319
323
  .c-actions { display:flex; gap:8px; }
320
324
  .btn { font:inherit; font-size:13px; padding:7px 12px; border-radius:8px; cursor:pointer; border:1px solid var(--line); background:var(--surface-2); color:var(--ink); }
321
- .btn.primary { background:var(--signal); color:#241705; border-color:transparent; font-weight:600; }
325
+ .btn.primary { background:var(--signal); color:var(--on-accent); border-color:transparent; font-weight:600; }
322
326
  .empty { color:var(--faint); font-style:italic; font-size:12.5px; }
323
327
  :focus-visible { outline:2px solid var(--cool); outline-offset:2px; }
328
+ /* Below ~1180px the 8 labelled tabs no longer fit alongside brand + right cluster,
329
+ so tabs go icon-only (and scroll horizontally inside their own strip if still tight). */
330
+ @media (max-width:1180px){ .tabs .tab{ padding:7px 8px; } .tab-label{ display:none; } }
324
331
  @media (max-width:900px){
325
332
  .panel[data-panel="board"].is-active { grid-template-columns:1fr; }
326
333
  .side { border-left:0; border-top:1px solid var(--line); flex-direction:row; flex-wrap:wrap; gap:16px 28px; }
327
334
  .side .rail-block { min-width:200px; flex:1; }
328
335
  .kpi-row { grid-template-columns:repeat(2,1fr); }
336
+ /* secondary status chips are noise on a small header — keep Run + settings + theme */
337
+ .meta-chip, .mode-chip { display:none; }
338
+ }
339
+ @media (max-width:640px){
340
+ .brand-text { display:none; } /* logo alone carries the brand */
341
+ .run-btn span:last-child { display:none; } /* Run → icon only */
342
+ .run-btn { padding:6px 9px; }
343
+ .sync span:last-child { display:none; } /* just the pulsing dot */
329
344
  }
330
345
  @media (max-width:520px){
331
346
  .kpi-row { grid-template-columns:1fr; }
332
347
  .donut-row { flex-direction:column; align-items:flex-start; }
348
+ .overview { padding:16px; }
349
+ .app-footer { flex-direction:column; align-items:flex-start; gap:8px; }
333
350
  }
334
- @media (max-width:760px){ .tabs .tab{ padding:7px 8px;} .tab-label{ display:none; } }
335
351
 
336
352
  /* chat widget (floating agent runner) — same surface/radius/shadow tokens as the card system.
337
353
  Bubble/log styles below (.msg, .bubble, .chat-idle, .approval…) are shared, unscoped classes
@@ -351,7 +367,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
351
367
  .chat-log { flex:1; overflow-y:auto; padding:16px; display:flex; flex-direction:column; gap:11px; }
352
368
  .chat-idle { color:var(--faint); font-style:italic; font-size:12.5px; }
353
369
  .msg { display:flex; } .msg.you { justify-content:flex-end; }
354
- .msg.you .bubble { background:var(--signal); color:#241705; border-radius:12px 12px 3px 12px; padding:8px 11px; font-size:13px; max-width:82%; font-weight:500; }
370
+ .msg.you .bubble { background:var(--signal); color:var(--on-accent); border-radius:12px 12px 3px 12px; padding:8px 11px; font-size:13px; max-width:82%; font-weight:500; }
355
371
  .msg.agent { margin:0; align-self:flex-start; max-width:92%; background:#05080b; border:1px solid var(--line); border-radius:12px 12px 12px 3px; padding:10px 12px; font-family:var(--mono); font-size:12px; line-height:1.5; color:#cdd9de; white-space:pre-wrap; word-break:break-word; overflow-wrap:anywhere; }
356
372
  [data-theme="light"] .msg.agent { background:#10151b; }
357
373
  .msg.agentmsg { flex-direction:column; align-items:flex-start; gap:3px; max-width:88%; }
@@ -361,10 +377,11 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
361
377
  .msg.agentmsg.k-status .bubble { border-color:color-mix(in srgb,var(--s-done) 35%,var(--line)); }
362
378
  .msg.agentmsg.k-question .msg-role { color:var(--signal); }
363
379
  .msg.agentmsg.k-handoff .msg-role { color:var(--s-to_analyze); }
364
- .chat-input { display:flex; gap:8px; padding:12px 14px 4px; }
365
- .chat-ta { flex:1; min-height:40px; max-height:120px; resize:vertical; font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line); border-radius:9px; padding:9px 11px; transition:border-color .15s; }
380
+ /* Composer: textarea on its own full-width row, compact actions right-aligned below. */
381
+ .chat-input { display:flex; flex-wrap:wrap; justify-content:flex-end; align-items:center; gap:8px; padding:12px 14px; }
382
+ .chat-ta { flex:1 1 100%; min-height:44px; max-height:140px; resize:vertical; font:inherit; background:var(--bg); color:var(--ink); border:1px solid var(--line); border-radius:9px; padding:9px 11px; transition:border-color .15s; }
366
383
  .chat-ta:focus { outline:2px solid var(--cool); outline-offset:1px; }
367
- .chat-send { align-self:stretch; }
384
+ .chat-send { flex:0 0 auto; align-self:auto; padding:8px 16px; }
368
385
  .chat-warn { font-size:10.5px; color:var(--signal); margin:6px 12px 10px; }
369
386
  .approval { display:flex; flex-direction:column; gap:8px; padding:10px 12px; background:var(--surface-2); border:1px solid color-mix(in srgb,var(--signal) 40%,var(--line)); border-radius:var(--radius); box-shadow:var(--shadow); }
370
387
  .approval .c-actions { display:flex; gap:8px; }
@@ -394,7 +411,7 @@ body { background:var(--bg); color:var(--ink); font-family:var(--sans); font-siz
394
411
  re-renders (which rebuild these nodes) then don't replay them — this is what stops the
395
412
  dashboard from flickering on every agent action. */
396
413
  /* cards/panels rise in on render */
397
- @keyframes rise { from{ opacity:0; transform:translateY(8px);} to{ opacity:1; transform:translateY(0);} }
414
+ @keyframes rise { from{ opacity:0; transform:translateY(5px);} to{ opacity:1; transform:translateY(0);} }
398
415
  body.booting .kpi, body.booting .ocard, body.booting .card { animation:rise .4s cubic-bezier(.2,.8,.2,1) both; }
399
416
  body.booting .kpi-row .kpi:nth-child(1){ animation-delay:.02s; } body.booting .kpi-row .kpi:nth-child(2){ animation-delay:.06s; }
400
417
  body.booting .kpi-row .kpi:nth-child(3){ animation-delay:.1s; } body.booting .kpi-row .kpi:nth-child(4){ animation-delay:.14s; }
@@ -407,7 +424,7 @@ body.booting .area-dot, body.booting .area-value { opacity:0; animation:fadein .
407
424
  @keyframes fadein { to{ opacity:1; } }
408
425
 
409
426
  /* donut arcs pop in, staggered by --i */
410
- @keyframes pop { from{ opacity:0; transform:scale(.85); } to{ opacity:1; transform:scale(1); } }
427
+ @keyframes pop { from{ opacity:0; } to{ opacity:1; } }
411
428
  body.booting .seg-anim { transform-origin:center; transform-box:fill-box; opacity:0; animation:pop .45s cubic-bezier(.2,.8,.2,1) forwards; animation-delay:calc(var(--i,0)*.08s); }
412
429
 
413
430
  /* phase bars grow + the count-up badge is driven from JS (countUp in app.js) */
@@ -452,7 +469,7 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
452
469
  .settings-note { font-size:10.5px; color:var(--faint); margin:0; line-height:1.4; }
453
470
 
454
471
  /* tab badge (open attention count) */
455
- .tab-badge { display:inline-grid; place-items:center; min-width:16px; height:16px; padding:0 4px; margin-left:5px; border-radius:999px; background:var(--signal); color:#241705; font-size:10px; font-weight:700; }
472
+ .tab-badge { display:inline-grid; place-items:center; min-width:16px; height:16px; padding:0 4px; margin-left:5px; border-radius:999px; background:var(--signal); color:var(--on-accent); font-size:10px; font-weight:700; }
456
473
  .tab-badge[hidden] { display:none; }
457
474
 
458
475
  /* attention tab */
@@ -492,7 +509,7 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
492
509
  .wf-card.off { opacity:.5; }
493
510
  .wf-card-head { display:flex; align-items:center; gap:8px; }
494
511
  .wf-num { display:grid; place-items:center; width:20px; height:20px; border-radius:999px; background:var(--surface-2); color:var(--muted); font-family:var(--mono); font-size:11px; flex-shrink:0; }
495
- .wf-card:not(.off) .wf-num { background:var(--signal); color:#241705; }
512
+ .wf-card:not(.off) .wf-num { background:var(--signal); color:var(--on-accent); }
496
513
  .wf-card-name { font-weight:600; font-size:13.5px; }
497
514
  .wf-opt { font-family:var(--mono); font-size:9px; text-transform:uppercase; letter-spacing:.05em; color:var(--faint); border:1px solid var(--line); border-radius:4px; padding:1px 4px; }
498
515
  .wf-card-meta { display:flex; flex-wrap:wrap; gap:6px; }
@@ -506,3 +523,253 @@ body.booting .ring-svg circle:last-of-type { transform-origin:center; animation:
506
523
  .wf-conn { flex:0 0 auto; align-self:center; width:26px; height:2px; background:linear-gradient(90deg,var(--cool),var(--signal)); margin:0 2px; border-radius:2px; }
507
524
  .wf-conn.off { background:var(--line); }
508
525
  @media (max-width:720px){ .wf-track { flex-direction:column; align-items:stretch; } .wf-conn { width:2px; height:18px; margin:2px 0 2px 19px; } }
526
+
527
+ /* ================= v0.13.2 — reference re-skin (violet), settings tab, footer ================= */
528
+
529
+ /* KPI colored left border (accent set inline per card) */
530
+ .kpi.has-accent { border-left-width:3px; border-left-style:solid; }
531
+
532
+ /* Settings tab */
533
+ .settings-wrap { max-width:720px; margin:0 auto; padding:20px 22px; }
534
+ .settings-card { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:18px; display:flex; flex-direction:column; gap:16px; margin:14px 0; }
535
+ .settings-field { display:flex; flex-direction:column; gap:6px; }
536
+ .settings-field-label { font-size:11px; text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
537
+ .settings-select { font-family:inherit; font-size:14px; padding:9px 11px; border:1px solid var(--line); border-radius:9px; background:var(--surface-2); color:var(--ink); cursor:pointer; }
538
+ .settings-select:focus { outline:2px solid var(--signal); outline-offset:1px; }
539
+ .settings-saved { align-self:flex-start; font-size:12px; color:var(--s-done); font-family:var(--mono); }
540
+ .settings-saved[hidden] { display:none; }
541
+ .settings-readonly { background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; }
542
+ .settings-ro-row { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 15px; border-bottom:1px solid var(--line); }
543
+ .settings-ro-row:last-child { border-bottom:none; }
544
+ .settings-ro-k { color:var(--muted); font-size:13px; }
545
+ .settings-ro-v { font-family:var(--mono); font-size:12.5px; color:var(--ink); }
546
+
547
+ /* App footer — professional, always at the bottom of the content */
548
+ .app-footer { display:flex; align-items:center; justify-content:space-between; gap:16px; flex-wrap:wrap; padding:14px 22px; border-top:1px solid var(--line); background:var(--surface); color:var(--muted); font-size:12.5px; }
549
+ .footer-left { display:flex; align-items:center; gap:9px; }
550
+ .footer-logo { display:inline-flex; height:18px; text-decoration:none; }
551
+ .footer-logo .brand-logo-img { height:18px; width:auto; }
552
+ .footer-name { font-weight:700; color:var(--ink); }
553
+ .footer-ver { font-family:var(--mono); font-size:11.5px; color:var(--signal); }
554
+ .footer-sep { color:var(--faint); }
555
+ .footer-lic { color:var(--muted); }
556
+ .footer-right { display:flex; align-items:center; gap:16px; }
557
+ .footer-right a, .footer-link { color:var(--muted); text-decoration:none; background:none; border:none; font:inherit; cursor:pointer; padding:0; }
558
+ .footer-right a:hover, .footer-link:hover { color:var(--signal); }
559
+
560
+ /* framework version chip in the header brand line */
561
+ .brand-ver { font-family:var(--mono); font-size:9.5px; color:var(--signal); border:1px solid color-mix(in srgb,var(--signal) 40%,var(--line)); border-radius:999px; padding:1px 6px; flex-shrink:0; }
562
+ .brand-ver[hidden] { display:none; }
563
+ @media (max-width:640px){ .brand-ver { display:none; } }
564
+
565
+ /* ---- mobile hamburger nav + FAB-safe footer (v0.13.2) ---- */
566
+ .nav-toggle { display:none; width:34px; height:34px; border:1px solid var(--line); border-radius:8px; background:var(--surface-2); cursor:pointer; padding:0; flex-direction:column; align-items:center; justify-content:center; gap:4px; flex-shrink:0; }
567
+ .nav-toggle span { display:block; width:16px; height:2px; background:var(--ink); border-radius:2px; transition:transform .2s, opacity .2s; }
568
+ body.nav-open .nav-toggle span:nth-child(1){ transform:translateY(6px) rotate(45deg); }
569
+ body.nav-open .nav-toggle span:nth-child(2){ opacity:0; }
570
+ body.nav-open .nav-toggle span:nth-child(3){ transform:translateY(-6px) rotate(-45deg); }
571
+
572
+ /* keep the footer's right-side links clear of the floating chat FAB (bottom-right) */
573
+ .app-footer { padding-right:92px; }
574
+
575
+ @media (max-width:900px){
576
+ .nav-toggle { display:flex; }
577
+ .brand-text { display:none; } /* logo + hamburger carry the brand on mobile */
578
+ .tabs { position:absolute; top:100%; left:0; right:0; flex-direction:column; align-items:stretch; gap:2px; max-width:none;
579
+ background:var(--surface); border-bottom:1px solid var(--line); box-shadow:var(--shadow); padding:8px;
580
+ max-height:0; overflow:hidden; opacity:0; pointer-events:none; transition:max-height .22s ease, opacity .18s; }
581
+ body.nav-open .tabs { max-height:82vh; overflow-y:auto; opacity:1; pointer-events:auto; }
582
+ .tabs .tab { justify-content:flex-start; padding:12px 14px; border-radius:9px; font-size:14px; }
583
+ .tabs .tab.is-active { box-shadow:inset 3px 0 0 var(--signal); }
584
+ .tab-label { display:inline !important; } /* full labels inside the dropdown */
585
+ }
586
+
587
+ /* ---- Workflow: vertical pipeline timeline (v0.13.2) ---- */
588
+ .wf-legend { display:flex; align-items:center; gap:8px; margin-bottom:18px; }
589
+ .wf-legend-dot { width:8px; height:8px; border-radius:50%; background:var(--signal); box-shadow:0 0 0 4px color-mix(in srgb,var(--signal) 18%,transparent); }
590
+ .wf-legend-txt { font-family:var(--mono); font-size:11.5px; color:var(--muted); }
591
+ .wf-flow { max-width:600px; }
592
+ .wf-step { display:grid; grid-template-columns:40px 1fr; column-gap:14px; }
593
+ .wf-rail { position:relative; display:flex; justify-content:center; }
594
+ .wf-rail::before { content:""; position:absolute; top:0; bottom:0; width:2px; background:var(--line); border-radius:2px; }
595
+ .wf-step.on .wf-rail::before { background:linear-gradient(var(--signal),color-mix(in srgb,var(--signal) 55%,var(--cool))); }
596
+ .wf-step.first .wf-rail::before { top:20px; }
597
+ .wf-step.last .wf-rail::before { bottom:auto; height:20px; }
598
+ .wf-node2 { position:relative; z-index:1; margin-top:10px; width:30px; height:30px; border-radius:50%; display:grid; place-items:center; font-family:var(--mono); font-size:12px; font-weight:700; background:var(--surface); border:2px solid var(--line); color:var(--faint); flex-shrink:0; transition:background .18s,border-color .18s,color .18s,box-shadow .18s; }
599
+ .wf-step.on .wf-node2 { background:var(--signal); border-color:transparent; color:var(--on-accent); box-shadow:0 0 0 4px color-mix(in srgb,var(--signal) 16%,transparent); }
600
+ .wf-body { margin:6px 0; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:13px 15px; cursor:pointer; transition:border-color .15s,transform .12s,box-shadow .15s; }
601
+ .wf-body:hover { border-color:color-mix(in srgb,var(--signal) 45%,var(--line)); transform:translateX(3px); box-shadow:var(--shadow); }
602
+ .wf-body:focus-visible { outline:2px solid var(--signal); outline-offset:2px; }
603
+ .wf-step.off .wf-body { opacity:.62; }
604
+ .wf-body-head { display:flex; align-items:center; gap:10px; }
605
+ .wf-step-name { font-weight:600; font-size:14px; letter-spacing:-.01em; }
606
+ .wf-body-meta { display:flex; flex-wrap:wrap; gap:6px; margin-top:9px; }
607
+ .wf-switch { margin-left:auto; width:36px; height:20px; border-radius:999px; background:var(--surface-2); border:1px solid var(--line); position:relative; transition:background .16s,border-color .16s; flex-shrink:0; }
608
+ .wf-switch.on { background:var(--signal); border-color:transparent; }
609
+ .wf-knob { position:absolute; top:2px; left:2px; width:14px; height:14px; border-radius:50%; background:var(--muted); transition:transform .18s cubic-bezier(.2,.8,.2,1),background .18s; }
610
+ .wf-switch.on .wf-knob { transform:translateX(16px); background:#fff; }
611
+
612
+ /* theme toggle now renders the moon icon */
613
+ .theme-toggle { color:var(--muted); }
614
+ .theme-toggle:hover { color:var(--ink); border-color:var(--cool); }
615
+
616
+ /* ---- Workflow: horizontal icon pipeline (v0.13.2) ---- */
617
+ .wf-pipeline { display:flex; align-items:flex-start; gap:0; overflow-x:auto; padding:24px 6px 12px; scrollbar-width:thin; }
618
+ .wf-pipeline::-webkit-scrollbar { height:8px; }
619
+ .wf-pipeline::-webkit-scrollbar-thumb { background:var(--line); border-radius:8px; }
620
+ .wf-step2 { position:relative; display:flex; flex-direction:column; align-items:center; flex:0 0 auto; width:118px; cursor:pointer; }
621
+ .wf-step2:focus-visible { outline:none; }
622
+ .wf-step2:focus-visible .wf-circle { outline:2px solid var(--signal); outline-offset:3px; }
623
+ .wf-circle { position:relative; width:60px; height:60px; border-radius:50%; display:grid; place-items:center; border:2px solid var(--line); background:var(--surface); color:var(--muted); transition:transform .18s,border-color .18s,color .18s,background .18s,box-shadow .18s; }
624
+ .wf-circle svg { width:26px; height:26px; }
625
+ .wf-step2:hover .wf-circle { transform:translateY(-3px); border-color:var(--cool); color:var(--ink); }
626
+ .wf-step2.on .wf-circle { border-color:var(--signal); color:var(--signal); box-shadow:0 0 0 4px color-mix(in srgb,var(--signal) 14%,transparent); }
627
+ .wf-step2.on:hover .wf-circle { background:var(--signal); color:var(--on-accent); }
628
+ .wf-step2.off .wf-circle { border-style:dashed; opacity:.65; }
629
+ .wf-step2.on .wf-circle::after { content:""; position:absolute; inset:-2px; border-radius:50%; animation:wf-ring 2.8s ease-out infinite; }
630
+ @keyframes wf-ring { 0%{ box-shadow:0 0 0 0 color-mix(in srgb,var(--signal) 32%,transparent);} 70%{ box-shadow:0 0 0 9px transparent;} 100%{ box-shadow:0 0 0 0 transparent;} }
631
+ .wf-caption { margin-top:13px; font-size:12px; font-weight:600; text-align:center; color:var(--ink); line-height:1.3; max-width:108px; }
632
+ .wf-step2.off .wf-caption { color:var(--muted); font-weight:500; }
633
+ .wf-opt2 { display:block; font-family:var(--mono); font-size:8.5px; text-transform:uppercase; letter-spacing:.06em; color:var(--faint); margin-top:3px; }
634
+ .wf-link { flex:1 1 26px; min-width:26px; height:2px; align-self:flex-start; margin-top:30px; border-radius:2px; background:var(--line); }
635
+ .wf-link.on { background-color:transparent; background-image:repeating-linear-gradient(90deg, color-mix(in srgb,var(--signal) 85%,transparent) 0 6px, transparent 6px 12px); background-size:12px 2px; background-repeat:repeat-x; animation:wf-flow-dash .6s linear infinite; }
636
+ @keyframes wf-flow-dash { to { background-position:12px 0; } }
637
+ body.booting .wf-step2 { opacity:0; animation:rise .4s cubic-bezier(.2,.8,.2,1) both; animation-delay:calc(var(--i,0)*.06s); }
638
+
639
+ /* ---- Workflow: selected-step highlight + inline detail zone (v0.13.2) ---- */
640
+ .wf-step2.is-selected .wf-circle { border-color:var(--signal); border-style:solid; color:var(--signal); box-shadow:0 0 0 5px color-mix(in srgb,var(--signal) 22%,transparent); }
641
+ .wf-step2.is-selected.off .wf-circle { opacity:1; }
642
+ .wf-step2.is-selected .wf-caption { color:var(--ink); font-weight:700; }
643
+ .wf-detail { margin-top:26px; max-width:720px; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); padding:18px 20px; }
644
+ .wf-detail-head { display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
645
+ .wf-detail-num { display:grid; place-items:center; width:26px; height:26px; border-radius:50%; background:var(--signal); color:var(--on-accent); font-family:var(--mono); font-size:12px; font-weight:700; flex-shrink:0; }
646
+ .wf-detail-name { font-size:17px; font-weight:700; letter-spacing:-.01em; }
647
+ .wf-detail-status { font-family:var(--mono); font-size:10px; text-transform:uppercase; letter-spacing:.05em; padding:2px 8px; border-radius:999px; }
648
+ .wf-detail-status.on { color:var(--s-done); border:1px solid color-mix(in srgb,var(--s-done) 40%,var(--line)); }
649
+ .wf-detail-status.off { color:var(--muted); border:1px solid var(--line); }
650
+ .wf-detail-btn { margin-left:auto; }
651
+ .wf-detail-desc { color:var(--ink); font-size:13.5px; line-height:1.55; margin:13px 0 15px; }
652
+ .wf-detail-grid { display:grid; grid-template-columns:auto 1fr; gap:9px 18px; }
653
+ .wf-detail-row { display:contents; }
654
+ .wf-detail-k { color:var(--muted); font-size:13px; }
655
+ .wf-detail-v { color:var(--ink); font-family:var(--mono); font-size:12.5px; }
656
+ .wf-detail-skill { margin-top:15px; padding-top:14px; border-top:1px solid var(--line); color:var(--muted); font-size:12.5px; line-height:1.55; }
657
+ .wf-detail-skill b { color:var(--ink); font-weight:600; }
658
+
659
+ /* ---- Workflow: click-popover, connector arrows, mobile reflow (v0.13.2) ---- */
660
+ .wf-pop { position:fixed; z-index:40; width:min(360px,92vw); background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); box-shadow:var(--shadow); padding:16px 18px; }
661
+ .wf-pop[hidden] { display:none; }
662
+ .wf-pop::before { content:""; position:absolute; top:-8px; left:var(--caret-x,50%); transform:translateX(-50%); border-left:8px solid transparent; border-right:8px solid transparent; border-bottom:8px solid var(--surface); filter:drop-shadow(0 -1px 0 var(--line)); }
663
+ .wf-pop.above::before { top:auto; bottom:-8px; border-bottom:0; border-top:8px solid var(--surface); filter:drop-shadow(0 1px 0 var(--line)); }
664
+ .wf-pop-actions { margin-top:15px; display:flex; }
665
+ .wf-pop .wf-detail-desc { margin:12px 0 14px; }
666
+
667
+ /* directional arrowhead at the end of each connector */
668
+ .wf-link { position:relative; }
669
+ .wf-link::after { content:""; position:absolute; right:-1px; top:50%; transform:translateY(-50%); width:0; height:0; border-left:6px solid var(--line); border-top:4px solid transparent; border-bottom:4px solid transparent; }
670
+ .wf-link.on::after { border-left-color:var(--signal); }
671
+
672
+ /* mobile: reflow into a centered wrap — no horizontal scroll (connectors hidden) */
673
+ @media (max-width:760px){
674
+ .wf-wrap { max-width:none; }
675
+ .wf-pipeline { flex-wrap:wrap; overflow-x:visible; justify-content:center; gap:8px 4px; padding:16px 0 8px; }
676
+ .wf-link { display:none; }
677
+ .wf-step2 { width:33.3%; max-width:120px; }
678
+ }
679
+ @media (max-width:420px){ .wf-step2 { width:50%; } }
680
+
681
+ /* ================= Design skins (data-design) ================= */
682
+ /* control-room = the bare :root defaults above. Add new designs as scoped blocks like "ocean" below.
683
+ Each design should override the full token set for BOTH dark and its [data-theme="light"] variant. */
684
+
685
+ /* --- Obsidian Ops: near-black mission-control, electric lime + cyan, mono precision --- */
686
+ :root[data-design="obsidian"] {
687
+ --bg:#0a0b0d; --surface:#131417; --surface-2:#1b1d21; --line:#23262b;
688
+ --ink:#e8e6e0; --muted:#8b909a; --faint:#5b6068;
689
+ --signal:#9be15d; --cool:#00e5ff; --on-accent:#0a0b0d;
690
+ --s-todo:#5b6068; --s-in_progress:#f2c94c; --s-to_validate:#56ccf2; --s-to_analyze:#a78bfa; --s-done:#3fb950; --s-blocked:#f2748a;
691
+ --radius:10px; --shadow:0 10px 26px rgba(0,0,0,.55);
692
+ }
693
+ /* mission-control precision: mono labels, tighter, a flat bordered KPI row with dividers.
694
+ NB: no hardcoded topbar colour — it inherits the token-based base so it follows light/dark. */
695
+ :root[data-design="obsidian"] .kpi-label,
696
+ :root[data-design="obsidian"] .rail-title,
697
+ :root[data-design="obsidian"] .panel-sub,
698
+ :root[data-design="obsidian"] .kpi-num { font-family:var(--mono); }
699
+ :root[data-design="obsidian"] .kpi-num { font-weight:800; letter-spacing:-.02em; }
700
+ :root[data-design="obsidian"] .brand-sub { color:var(--signal); }
701
+ :root[data-design="obsidian"] .kpi-row { gap:0; background:var(--surface); border:1px solid var(--line); border-radius:var(--radius); overflow:hidden; }
702
+ :root[data-design="obsidian"] .kpi { background:transparent; border:0; border-left:1px solid var(--line) !important; border-radius:0; }
703
+ :root[data-design="obsidian"] .kpi:first-child { border-left:0 !important; }
704
+ :root[data-design="obsidian"] .kpi:hover { box-shadow:none; background:color-mix(in srgb,var(--surface-2) 60%,transparent); }
705
+ :root[data-design="obsidian"] .wf-mini .dot, :root[data-design="obsidian"] .wf-strip .dot { box-shadow:0 0 8px color-mix(in srgb,var(--signal) 60%,transparent); }
706
+ :root[data-design="obsidian"] .btn.primary,
707
+ :root[data-design="obsidian"] .fchip.active[data-status="all"] { color:#0a0b0d; }
708
+
709
+ :root[data-design="obsidian"][data-theme="light"] {
710
+ --bg:#f0eee9; --surface:#ffffff; --surface-2:#e7e9e4; --line:#dcdcd4;
711
+ --ink:#1b1d21; --muted:#5b6068; --faint:#8b909a;
712
+ --signal:#4c9a2a; --cool:#0891b2; --on-accent:#ffffff;
713
+ --s-todo:#6a6f78; --s-in_progress:#b8860b; --s-to_validate:#0891b2; --s-to_analyze:#7c5cff; --s-done:#3fb950; --s-blocked:#d15168;
714
+ }
715
+
716
+ /* ================= Self-hosted fonts (offline, zero external dependency) ================= */
717
+ @font-face{font-family:'Space Grotesk';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/space-grotesk-400.woff2) format('woff2');}
718
+ @font-face{font-family:'Space Grotesk';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/space-grotesk-500.woff2) format('woff2');}
719
+ @font-face{font-family:'Space Grotesk';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/space-grotesk-700.woff2) format('woff2');}
720
+ @font-face{font-family:'Sora';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/sora-400.woff2) format('woff2');}
721
+ @font-face{font-family:'Sora';font-style:normal;font-weight:600;font-display:swap;src:url(fonts/sora-600.woff2) format('woff2');}
722
+ @font-face{font-family:'Sora';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/sora-700.woff2) format('woff2');}
723
+ @font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/ibm-plex-sans-400.woff2) format('woff2');}
724
+ @font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/ibm-plex-sans-500.woff2) format('woff2');}
725
+ @font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(fonts/ibm-plex-sans-600.woff2) format('woff2');}
726
+ @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:400;font-display:swap;src:url(fonts/jetbrains-mono-400.woff2) format('woff2');}
727
+ @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:500;font-display:swap;src:url(fonts/jetbrains-mono-500.woff2) format('woff2');}
728
+ @font-face{font-family:'JetBrains Mono';font-style:normal;font-weight:700;font-display:swap;src:url(fonts/jetbrains-mono-700.woff2) format('woff2');}
729
+
730
+ /* headings / big numbers use the active design's display face */
731
+ .brand-name, .panel-title, .kpi-num, .wf-detail-name, .stat-tile-val { font-family:var(--display); }
732
+
733
+ /* Obsidian Ops fonts: JetBrains Mono (mono + display) + IBM Plex Sans (body) */
734
+ :root[data-design="obsidian"] { --sans:'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; --mono:'JetBrains Mono', ui-monospace, monospace; --display:'JetBrains Mono', ui-monospace, monospace; }
735
+
736
+ /* --- Neon Command: glassmorphism, aurora violet + cyan, control-room --- */
737
+ :root[data-design="neon-command"] {
738
+ --bg:#0d0b1a; --surface:#171233; --surface-2:#1f1940; --line:#2e2857;
739
+ --ink:#eae6ff; --muted:#a99fd6; --faint:#6f689e;
740
+ --signal:#7c4dff; --cool:#00e5ff; --on-accent:#0d0b1a;
741
+ --s-todo:#6f689e; --s-in_progress:#ff9f45; --s-to_validate:#00e5ff; --s-to_analyze:#c47dff; --s-done:#3ddc97; --s-blocked:#ff6b8a;
742
+ --radius:16px; --shadow:0 18px 50px rgba(0,0,0,.55);
743
+ --sans:'Sora', system-ui, sans-serif; --display:'Space Grotesk', system-ui, sans-serif;
744
+ }
745
+ :root[data-design="neon-command"] body {
746
+ background:
747
+ radial-gradient(1200px 600px at 15% -5%, rgba(124,77,255,.22), transparent 60%),
748
+ radial-gradient(1000px 520px at 100% 8%, rgba(0,229,255,.14), transparent 55%),
749
+ var(--bg);
750
+ background-attachment:fixed;
751
+ }
752
+ :root[data-design="neon-command"] .kpi,
753
+ :root[data-design="neon-command"] .ocard,
754
+ :root[data-design="neon-command"] .card,
755
+ :root[data-design="neon-command"] .phase,
756
+ :root[data-design="neon-command"] .settings-card,
757
+ :root[data-design="neon-command"] .settings-readonly,
758
+ :root[data-design="neon-command"] .wf-pop,
759
+ :root[data-design="neon-command"] .approval {
760
+ background:linear-gradient(160deg, color-mix(in srgb,var(--surface) 80%,transparent), color-mix(in srgb,var(--surface-2) 50%,transparent));
761
+ border-color:color-mix(in srgb,var(--signal) 22%,var(--line));
762
+ backdrop-filter:blur(10px);
763
+ }
764
+ :root[data-design="neon-command"] .topbar { background:color-mix(in srgb,var(--bg) 68%,transparent); border-bottom-color:color-mix(in srgb,var(--signal) 22%,var(--line)); }
765
+ :root[data-design="neon-command"] .btn.primary,
766
+ :root[data-design="neon-command"] .run-btn { background:linear-gradient(120deg,var(--signal),var(--cool)); color:#fff; border-color:transparent; box-shadow:0 6px 20px color-mix(in srgb,var(--signal) 45%,transparent); }
767
+ :root[data-design="neon-command"] .brand-name { background:linear-gradient(120deg,var(--signal),var(--cool)); -webkit-background-clip:text; background-clip:text; color:transparent; }
768
+ :root[data-design="neon-command"] .tab.is-active { background:color-mix(in srgb,var(--signal) 20%,transparent); box-shadow:inset 0 -2px 0 var(--cool); }
769
+ :root[data-design="neon-command"] .wf-circle { backdrop-filter:blur(6px); }
770
+ :root[data-design="neon-command"][data-theme="light"] {
771
+ --bg:#f2f0fb; --surface:#ffffff; --surface-2:#eceafa; --line:#ddd8f2;
772
+ --ink:#1a1440; --muted:#5a5486; --faint:#8b86b3;
773
+ --signal:#6d3bff; --cool:#0891b2; --on-accent:#ffffff;
774
+ --s-done:#0f9d63; --s-to_validate:#0891b2; --s-in_progress:#c2801e; --s-blocked:#d15168;
775
+ }
@@ -15,10 +15,22 @@ const orchestrator = require('./orchestrator');
15
15
  const PORT = process.env.SPECTOFLOW_PORT ? Number(process.env.SPECTOFLOW_PORT) : 4319;
16
16
  const PUBLIC = path.join(__dirname, 'public');
17
17
  const ROOT = process.env.SPECTOFLOW_ROOT || path.resolve(__dirname, '..', '..');
18
- const MIME = { '.html':'text/html; charset=utf-8', '.css':'text/css; charset=utf-8', '.js':'application/javascript; charset=utf-8', '.png':'image/png', '.svg':'image/svg+xml', '.ico':'image/x-icon' };
18
+ const MIME = { '.html':'text/html; charset=utf-8', '.css':'text/css; charset=utf-8', '.js':'application/javascript; charset=utf-8', '.png':'image/png', '.svg':'image/svg+xml', '.ico':'image/x-icon', '.woff2':'font/woff2', '.woff':'font/woff' };
19
19
  const clients = new Set();
20
20
 
21
- function project(){ return store.readProject(ROOT); }
21
+ // Installed framework version: the manifest records it at init/update time. Fallback to the kit's
22
+ // own package.json — only reachable (and only used) when the server is run straight from templates/
23
+ // (dev/preview), never from an installed project whose sibling package.json belongs to the user.
24
+ function frameworkVersion(){
25
+ try { return JSON.parse(fs.readFileSync(path.join(ROOT, '.spectoflow', '.manifest.json'), 'utf8')).version; } catch {}
26
+ try { const pk = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')); if (pk.name === 'spectoflow') return pk.version; } catch {}
27
+ return null;
28
+ }
29
+ function project(){
30
+ const p = store.readProject(ROOT);
31
+ const v = frameworkVersion(); if (v) p.version = v;
32
+ return p;
33
+ }
22
34
  function sendJSON(res,code,obj){ res.writeHead(code,{'Content-Type':'application/json; charset=utf-8'}); res.end(JSON.stringify(obj)); }
23
35
  function body(req){ return new Promise(r=>{ let b=''; req.on('data',c=>b+=c); req.on('end',()=>{ try{r(JSON.parse(b||'{}'));}catch{r({});} }); }); }
24
36
  function emit(obj){ const line='data: '+JSON.stringify(obj)+'\n\n'; for(const res of clients) res.write(line); }
@@ -30,6 +42,7 @@ function writeConfig(patch){
30
42
  const cp = configPath(); const cfg = JSON.parse(fs.readFileSync(cp, 'utf8'));
31
43
  if (patch.mode && ['autopilot','semi','manual'].includes(patch.mode)) cfg.mode = patch.mode;
32
44
  if (typeof patch.language === 'string' && patch.language.trim()) cfg.language = patch.language.trim();
45
+ if (typeof patch.design === 'string' && /^[a-z0-9-]{1,40}$/.test(patch.design)) cfg.design = patch.design;
33
46
  fs.writeFileSync(cp, JSON.stringify(cfg, null, 2) + '\n');
34
47
  return cfg;
35
48
  }