karajan-code 2.19.0 → 2.19.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.
Files changed (53) hide show
  1. package/README.md +4 -0
  2. package/package.json +9 -1
  3. package/packages/hu-board/README.md +69 -0
  4. package/packages/hu-board/package.json +23 -0
  5. package/packages/hu-board/public/app.js +4038 -0
  6. package/packages/hu-board/public/index.html +55 -0
  7. package/packages/hu-board/public/pipeline.css +132 -0
  8. package/packages/hu-board/public/pipeline.html +48 -0
  9. package/packages/hu-board/public/pipeline.js +200 -0
  10. package/packages/hu-board/public/styles.css +918 -0
  11. package/packages/hu-board/src/auth.js +92 -0
  12. package/packages/hu-board/src/boot-info.js +16 -0
  13. package/packages/hu-board/src/cleanup-zombies.js +142 -0
  14. package/packages/hu-board/src/command-runner.js +134 -0
  15. package/packages/hu-board/src/config-yaml.js +285 -0
  16. package/packages/hu-board/src/db.js +739 -0
  17. package/packages/hu-board/src/ephemeral-cleaner.js +198 -0
  18. package/packages/hu-board/src/event-bus.js +58 -0
  19. package/packages/hu-board/src/format.js +83 -0
  20. package/packages/hu-board/src/hu-zombie-reaper.js +103 -0
  21. package/packages/hu-board/src/journal-parser.js +180 -0
  22. package/packages/hu-board/src/plan-mutations.js +422 -0
  23. package/packages/hu-board/src/preflight.js +285 -0
  24. package/packages/hu-board/src/routes/api.js +1271 -0
  25. package/packages/hu-board/src/routes/pipeline.js +45 -0
  26. package/packages/hu-board/src/run-tracker.js +121 -0
  27. package/packages/hu-board/src/server.js +435 -0
  28. package/packages/hu-board/src/sync.js +626 -0
  29. package/packages/hu-board/src/token-store.js +57 -0
  30. package/packages/hu-board/src/zombie-reaper.js +207 -0
  31. package/src/audit/webperf-input.js +4 -7
  32. package/src/checks/dir-setup.js +5 -3
  33. package/src/cli/register-pipeline.js +1 -0
  34. package/src/cli/register-plan.js +1 -0
  35. package/src/commands/plan/generate.js +15 -0
  36. package/src/commands/run.js +19 -1
  37. package/src/commands/webperf.js +4 -6
  38. package/src/config/role-resolver.js +3 -1
  39. package/src/mcp/handlers/direct-handlers.js +3 -0
  40. package/src/mcp/handlers/run-handler.js +5 -0
  41. package/src/mcp/tools.js +4 -2
  42. package/src/prompts/spec-reviewer.js +66 -0
  43. package/src/roles/perf-role.js +4 -4
  44. package/src/roles/spec-reviewer-role.js +106 -0
  45. package/src/sonar/api.js +17 -3
  46. package/src/sonar/token-recovery.js +67 -0
  47. package/src/spec-review/refine-loop.js +58 -0
  48. package/src/spec-review/run-spec-review.js +70 -0
  49. package/src/utils/board-prompt-bridge.js +4 -5
  50. package/src/utils/display/spec-findings.js +44 -0
  51. package/src/utils/paths.js +15 -0
  52. package/src/utils/run-registry.js +4 -4
  53. package/templates/roles/spec-reviewer.md +38 -0
@@ -0,0 +1,918 @@
1
+ /* ============================================
2
+ Karajan HU Board - Dark Theme
3
+ ============================================ */
4
+
5
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
6
+
7
+ :root {
8
+ --bg-primary: #0a0e1a;
9
+ /* --bg-secondary was referenced from app.js (modal backgrounds, inputs,
10
+ code blocks) but never declared, so it fell back to `transparent` and
11
+ made the prompt modal show cards bleeding through. Anchored to a
12
+ slightly lighter shade than --bg-card so layered surfaces stay
13
+ visually distinct. */
14
+ --bg-secondary: #131a30;
15
+ --bg-card: #0f1425;
16
+ --bg-card-hover: #141a30;
17
+ --border: #1e2740;
18
+ --border-active: #2d3a5c;
19
+ --text-primary: #e2e8f0;
20
+ --text-secondary: #94a3b8;
21
+ --text-muted: #64748b;
22
+ --accent-purple: #7c3aed;
23
+ --accent-purple-dim: #6d28d9;
24
+ --accent-teal: #2dd4bf;
25
+ --accent-teal-dim: #14b8a6;
26
+ --color-green: #34d399;
27
+ --color-red: #f87171;
28
+ --color-yellow: #fbbf24;
29
+ --color-blue: #60a5fa;
30
+ --color-orange: #fb923c;
31
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
32
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
33
+ --radius: 8px;
34
+ --radius-sm: 4px;
35
+ --shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
36
+ }
37
+
38
+ *, *::before, *::after {
39
+ box-sizing: border-box;
40
+ margin: 0;
41
+ padding: 0;
42
+ }
43
+
44
+ html {
45
+ font-size: 16px;
46
+ scroll-behavior: smooth;
47
+ }
48
+
49
+ body {
50
+ font-family: var(--font-sans);
51
+ background: var(--bg-primary);
52
+ color: var(--text-primary);
53
+ line-height: 1.6;
54
+ min-height: 100vh;
55
+ }
56
+
57
+ a {
58
+ color: var(--accent-purple);
59
+ text-decoration: none;
60
+ }
61
+
62
+ a:hover {
63
+ color: var(--accent-teal);
64
+ }
65
+
66
+ /* ---- Header ---- */
67
+ .header {
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ padding: 16px 24px;
72
+ border-bottom: 1px solid var(--border);
73
+ background: var(--bg-card);
74
+ position: sticky;
75
+ top: 0;
76
+ z-index: 100;
77
+ }
78
+
79
+ .header__logo {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 12px;
83
+ }
84
+
85
+ .header__logo svg {
86
+ width: 32px;
87
+ height: 32px;
88
+ }
89
+
90
+ .header__title {
91
+ font-size: 1.25rem;
92
+ font-weight: 700;
93
+ color: var(--text-primary);
94
+ }
95
+
96
+ .header__subtitle {
97
+ font-size: 0.75rem;
98
+ color: var(--text-muted);
99
+ font-family: var(--font-mono);
100
+ }
101
+
102
+ .header__nav {
103
+ display: flex;
104
+ gap: 8px;
105
+ align-items: center;
106
+ }
107
+
108
+ .nav-btn {
109
+ background: transparent;
110
+ border: 1px solid var(--border);
111
+ color: var(--text-secondary);
112
+ padding: 6px 14px;
113
+ border-radius: var(--radius-sm);
114
+ cursor: pointer;
115
+ font-family: var(--font-sans);
116
+ font-size: 0.85rem;
117
+ transition: all 0.2s;
118
+ }
119
+
120
+ .nav-btn:hover,
121
+ .nav-btn.active {
122
+ background: var(--accent-purple);
123
+ color: white;
124
+ border-color: var(--accent-purple);
125
+ }
126
+
127
+ .project-select {
128
+ background: var(--bg-primary);
129
+ border: 1px solid var(--border);
130
+ color: var(--text-primary);
131
+ padding: 6px 12px;
132
+ border-radius: var(--radius-sm);
133
+ font-family: var(--font-sans);
134
+ font-size: 0.85rem;
135
+ cursor: pointer;
136
+ }
137
+
138
+ .project-select:focus {
139
+ outline: none;
140
+ border-color: var(--accent-purple);
141
+ }
142
+
143
+ .header__controls {
144
+ display: flex;
145
+ gap: 6px;
146
+ margin-left: 8px;
147
+ }
148
+
149
+ .control-btn {
150
+ background: transparent;
151
+ border: 1px solid var(--border);
152
+ border-radius: 6px;
153
+ padding: 4px 8px;
154
+ font-size: 0.9rem;
155
+ cursor: pointer;
156
+ transition: all 0.15s;
157
+ color: var(--text-secondary);
158
+ }
159
+
160
+ .control-btn:hover {
161
+ border-color: var(--accent-purple);
162
+ background: var(--bg-card-hover);
163
+ }
164
+
165
+ .control-btn:disabled {
166
+ opacity: 0.5;
167
+ cursor: wait;
168
+ }
169
+
170
+ /* ---- Main Content ---- */
171
+ .main {
172
+ max-width: 1400px;
173
+ margin: 0 auto;
174
+ padding: 24px;
175
+ }
176
+
177
+ /* ---- Stats Grid ---- */
178
+ .stats-grid {
179
+ display: grid;
180
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
181
+ gap: 16px;
182
+ margin-bottom: 32px;
183
+ }
184
+
185
+ .stat-card {
186
+ background: var(--bg-card);
187
+ border: 1px solid var(--border);
188
+ border-radius: var(--radius);
189
+ padding: 20px;
190
+ text-align: center;
191
+ transition: border-color 0.2s;
192
+ }
193
+
194
+ .stat-card:hover {
195
+ border-color: var(--border-active);
196
+ }
197
+
198
+ .stat-card__value {
199
+ font-size: 2rem;
200
+ font-weight: 700;
201
+ color: var(--accent-teal);
202
+ font-family: var(--font-mono);
203
+ }
204
+
205
+ .stat-card__value--purple {
206
+ color: var(--accent-purple);
207
+ }
208
+
209
+ .stat-card__value--yellow {
210
+ color: var(--color-yellow);
211
+ }
212
+
213
+ .stat-card__value--green {
214
+ color: var(--color-green);
215
+ }
216
+
217
+ .stat-card__label {
218
+ font-size: 0.8rem;
219
+ color: var(--text-muted);
220
+ margin-top: 4px;
221
+ text-transform: uppercase;
222
+ letter-spacing: 0.05em;
223
+ }
224
+
225
+ /* ---- Section Headers ---- */
226
+ .section-header {
227
+ display: flex;
228
+ align-items: center;
229
+ justify-content: space-between;
230
+ margin-bottom: 16px;
231
+ padding-bottom: 8px;
232
+ border-bottom: 1px solid var(--border);
233
+ }
234
+
235
+ .section-header__title {
236
+ font-size: 1.1rem;
237
+ font-weight: 600;
238
+ color: var(--accent-purple);
239
+ }
240
+
241
+ .section-header__count {
242
+ font-size: 0.8rem;
243
+ color: var(--text-muted);
244
+ font-family: var(--font-mono);
245
+ }
246
+
247
+ /* ---- Kanban Board ---- */
248
+ .kanban {
249
+ display: grid;
250
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
251
+ gap: 16px;
252
+ margin-bottom: 32px;
253
+ }
254
+
255
+ .kanban__column {
256
+ background: var(--bg-card);
257
+ border: 1px solid var(--border);
258
+ border-radius: var(--radius);
259
+ padding: 16px;
260
+ min-height: 200px;
261
+ }
262
+
263
+ .kanban__column-header {
264
+ display: flex;
265
+ align-items: center;
266
+ justify-content: space-between;
267
+ margin-bottom: 12px;
268
+ padding-bottom: 8px;
269
+ border-bottom: 1px solid var(--border);
270
+ }
271
+
272
+ .kanban__column-title {
273
+ font-size: 0.85rem;
274
+ font-weight: 600;
275
+ text-transform: uppercase;
276
+ letter-spacing: 0.05em;
277
+ }
278
+
279
+ .kanban__column--pending .kanban__column-title { color: var(--color-yellow); }
280
+ .kanban__column--certified .kanban__column-title { color: var(--accent-teal); }
281
+ .kanban__column--needs-context .kanban__column-title { color: var(--color-red); }
282
+ .kanban__column--done .kanban__column-title { color: var(--color-green); }
283
+
284
+ .kanban__column-count {
285
+ font-size: 0.75rem;
286
+ color: var(--text-muted);
287
+ background: var(--bg-primary);
288
+ padding: 2px 8px;
289
+ border-radius: 10px;
290
+ font-family: var(--font-mono);
291
+ }
292
+
293
+ /* ---- Story Card ---- */
294
+ .story-card {
295
+ background: var(--bg-primary);
296
+ border: 1px solid var(--border);
297
+ border-radius: var(--radius-sm);
298
+ padding: 12px;
299
+ margin-bottom: 8px;
300
+ cursor: pointer;
301
+ transition: all 0.2s;
302
+ }
303
+
304
+ .story-card:hover {
305
+ border-color: var(--accent-purple);
306
+ background: var(--bg-card-hover);
307
+ }
308
+
309
+ .story-card__id {
310
+ font-family: var(--font-mono);
311
+ font-size: 0.7rem;
312
+ color: var(--text-muted);
313
+ margin-bottom: 4px;
314
+ }
315
+
316
+ .story-card__title {
317
+ font-size: 0.85rem;
318
+ color: var(--text-primary);
319
+ margin-bottom: 8px;
320
+ line-height: 1.4;
321
+ display: -webkit-box;
322
+ -webkit-line-clamp: 3;
323
+ -webkit-box-orient: vertical;
324
+ overflow: hidden;
325
+ }
326
+
327
+ .story-card__meta {
328
+ display: flex;
329
+ align-items: center;
330
+ justify-content: space-between;
331
+ gap: 8px;
332
+ flex-wrap: wrap;
333
+ }
334
+
335
+ .story-card__score {
336
+ font-family: var(--font-mono);
337
+ font-size: 0.75rem;
338
+ }
339
+
340
+ .story-card__score--good { color: var(--color-green); }
341
+ .story-card__score--ok { color: var(--color-yellow); }
342
+ .story-card__score--bad { color: var(--color-red); }
343
+
344
+ .quality-bar {
345
+ display: inline-flex;
346
+ gap: 1px;
347
+ vertical-align: middle;
348
+ margin-left: 4px;
349
+ }
350
+
351
+ .quality-bar__segment {
352
+ width: 6px;
353
+ height: 10px;
354
+ border-radius: 1px;
355
+ background: var(--border);
356
+ }
357
+
358
+ .quality-bar__segment--filled {
359
+ background: var(--accent-teal);
360
+ }
361
+
362
+ .story-card__ac {
363
+ font-size: 0.7rem;
364
+ color: var(--text-muted);
365
+ }
366
+
367
+ .story-card__antipattern {
368
+ font-size: 0.7rem;
369
+ color: var(--color-orange);
370
+ margin-top: 4px;
371
+ }
372
+
373
+ .story-card__status {
374
+ font-size: 0.7rem;
375
+ font-weight: 500;
376
+ padding: 2px 6px;
377
+ border-radius: 3px;
378
+ }
379
+
380
+ .status--pending { background: rgba(251, 191, 36, 0.15); color: var(--color-yellow); }
381
+ .status--certified { background: rgba(45, 212, 191, 0.15); color: var(--accent-teal); }
382
+ .status--needs_context { background: rgba(248, 113, 113, 0.15); color: var(--color-red); }
383
+ .status--done { background: rgba(52, 211, 153, 0.15); color: var(--color-green); }
384
+ .status--coding { background: rgba(124, 58, 237, 0.15); color: var(--accent-purple); }
385
+
386
+ /* KJC-TSK-0394 step 2: badge ortogonal al status que pinta el último
387
+ `result` conocido (pass / fail / partial). Convive con el status:
388
+ p. ej. status=done + result=pass = "terminó y todo OK"; status=done
389
+ + result=fail = "terminó pero tests fallaron"; status=pending +
390
+ result=fail = "está en cola para reintentar (último run falló)". */
391
+ .story-card__result {
392
+ font-size: 0.7rem;
393
+ font-weight: 700;
394
+ padding: 2px 6px;
395
+ border-radius: 3px;
396
+ font-family: var(--font-mono, monospace);
397
+ }
398
+ .result--pass { background: rgba(52, 211, 153, 0.15); color: var(--color-green); }
399
+ .result--fail { background: rgba(248, 113, 113, 0.15); color: var(--color-red); }
400
+ .result--partial { background: rgba(234, 179, 8, 0.15); color: var(--color-yellow); }
401
+
402
+ .story-card__time {
403
+ font-size: 0.65rem;
404
+ color: var(--text-muted);
405
+ margin-top: 4px;
406
+ }
407
+
408
+ /* ---- Story Detail Modal ---- */
409
+ .modal-backdrop {
410
+ position: fixed;
411
+ inset: 0;
412
+ background: rgba(0, 0, 0, 0.7);
413
+ z-index: 200;
414
+ display: flex;
415
+ align-items: center;
416
+ justify-content: center;
417
+ padding: 24px;
418
+ }
419
+
420
+ .modal-backdrop.hidden {
421
+ display: none;
422
+ }
423
+
424
+ .modal {
425
+ background: var(--bg-card);
426
+ border: 1px solid var(--border);
427
+ border-radius: var(--radius);
428
+ max-width: 700px;
429
+ width: 100%;
430
+ max-height: 85vh;
431
+ overflow-y: auto;
432
+ padding: 24px;
433
+ box-shadow: var(--shadow);
434
+ }
435
+
436
+ .modal__header {
437
+ display: flex;
438
+ align-items: flex-start;
439
+ justify-content: space-between;
440
+ margin-bottom: 16px;
441
+ }
442
+
443
+ .modal__title {
444
+ font-size: 1.1rem;
445
+ font-weight: 600;
446
+ }
447
+
448
+ .modal__close {
449
+ background: transparent;
450
+ border: none;
451
+ color: var(--text-muted);
452
+ font-size: 1.5rem;
453
+ cursor: pointer;
454
+ line-height: 1;
455
+ padding: 0 4px;
456
+ }
457
+
458
+ .modal__close:hover {
459
+ color: var(--text-primary);
460
+ }
461
+
462
+ .modal__section {
463
+ margin-bottom: 16px;
464
+ }
465
+
466
+ .modal__section-title {
467
+ font-size: 0.8rem;
468
+ color: var(--accent-purple);
469
+ text-transform: uppercase;
470
+ letter-spacing: 0.05em;
471
+ margin-bottom: 6px;
472
+ font-weight: 600;
473
+ }
474
+
475
+ .modal__field {
476
+ margin-bottom: 8px;
477
+ }
478
+
479
+ .modal__field-label {
480
+ font-size: 0.75rem;
481
+ color: var(--text-muted);
482
+ margin-bottom: 2px;
483
+ }
484
+
485
+ .modal__field-value {
486
+ font-size: 0.9rem;
487
+ color: var(--text-primary);
488
+ }
489
+
490
+ .modal__quality-grid {
491
+ display: grid;
492
+ grid-template-columns: repeat(3, 1fr);
493
+ gap: 8px;
494
+ }
495
+
496
+ .modal__quality-dim {
497
+ background: var(--bg-primary);
498
+ border: 1px solid var(--border);
499
+ border-radius: var(--radius-sm);
500
+ padding: 8px;
501
+ text-align: center;
502
+ }
503
+
504
+ .modal__quality-dim-label {
505
+ font-size: 0.65rem;
506
+ color: var(--text-muted);
507
+ text-transform: uppercase;
508
+ }
509
+
510
+ .modal__quality-dim-value {
511
+ font-size: 1.1rem;
512
+ font-weight: 600;
513
+ font-family: var(--font-mono);
514
+ color: var(--accent-teal);
515
+ }
516
+
517
+ .modal__ac-list {
518
+ list-style: none;
519
+ }
520
+
521
+ .modal__ac-item {
522
+ background: var(--bg-primary);
523
+ border-left: 3px solid var(--accent-purple);
524
+ padding: 8px 12px;
525
+ margin-bottom: 6px;
526
+ font-size: 0.85rem;
527
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
528
+ }
529
+
530
+ .modal__ac-item code {
531
+ font-family: var(--font-mono);
532
+ color: var(--accent-teal);
533
+ font-size: 0.8rem;
534
+ }
535
+
536
+ /* ---- Sessions Timeline ---- */
537
+ .sessions-list {
538
+ display: flex;
539
+ flex-direction: column;
540
+ gap: 12px;
541
+ }
542
+
543
+ .session-card {
544
+ background: var(--bg-card);
545
+ border: 1px solid var(--border);
546
+ border-radius: var(--radius);
547
+ padding: 16px;
548
+ cursor: pointer;
549
+ transition: border-color 0.2s;
550
+ }
551
+
552
+ .session-card:hover {
553
+ border-color: var(--accent-purple);
554
+ }
555
+
556
+ .session-card__header {
557
+ display: flex;
558
+ align-items: center;
559
+ justify-content: space-between;
560
+ margin-bottom: 8px;
561
+ }
562
+
563
+ .session-card__id {
564
+ font-family: var(--font-mono);
565
+ font-size: 0.8rem;
566
+ color: var(--accent-purple);
567
+ }
568
+
569
+ /* Human-readable session title (project + HH:MM) — replaces the
570
+ cryptic id as the card's primary header. The id stays available
571
+ as `idChip` at the bottom of the card for kj resume <id>. */
572
+ .session-card__title {
573
+ font-weight: 600;
574
+ font-size: 0.95rem;
575
+ color: var(--text-primary);
576
+ }
577
+
578
+ .session-card__id-chip {
579
+ font-family: var(--font-mono);
580
+ font-size: 0.7rem;
581
+ color: var(--text-muted);
582
+ margin-top: 8px;
583
+ padding-top: 8px;
584
+ border-top: 1px dashed var(--border);
585
+ word-break: break-all;
586
+ }
587
+
588
+ .modal__subtitle {
589
+ margin-top: 4px;
590
+ margin-bottom: 8px;
591
+ }
592
+
593
+ .session-card__status {
594
+ font-size: 0.75rem;
595
+ font-weight: 500;
596
+ padding: 2px 8px;
597
+ border-radius: 3px;
598
+ }
599
+
600
+ .session-status--approved { background: rgba(52, 211, 153, 0.15); color: var(--color-green); }
601
+ .session-status--failed { background: rgba(248, 113, 113, 0.15); color: var(--color-red); }
602
+ .session-status--stopped { background: rgba(251, 191, 36, 0.15); color: var(--color-yellow); }
603
+ .session-status--running { background: rgba(124, 58, 237, 0.15); color: var(--accent-purple); }
604
+
605
+ .session-card__task {
606
+ font-size: 0.85rem;
607
+ color: var(--text-primary);
608
+ margin-bottom: 8px;
609
+ line-height: 1.4;
610
+ display: -webkit-box;
611
+ -webkit-line-clamp: 2;
612
+ -webkit-box-orient: vertical;
613
+ overflow: hidden;
614
+ }
615
+
616
+ .session-card__meta {
617
+ display: flex;
618
+ gap: 16px;
619
+ font-size: 0.75rem;
620
+ color: var(--text-muted);
621
+ font-family: var(--font-mono);
622
+ }
623
+
624
+ /* ---- Session Detail Timeline ---- */
625
+ .timeline {
626
+ position: relative;
627
+ padding-left: 24px;
628
+ margin: 16px 0;
629
+ }
630
+
631
+ .timeline::before {
632
+ content: '';
633
+ position: absolute;
634
+ left: 8px;
635
+ top: 0;
636
+ bottom: 0;
637
+ width: 2px;
638
+ background: var(--border);
639
+ }
640
+
641
+ .timeline__item {
642
+ position: relative;
643
+ margin-bottom: 12px;
644
+ padding: 8px 12px;
645
+ background: var(--bg-primary);
646
+ border: 1px solid var(--border);
647
+ border-radius: var(--radius-sm);
648
+ }
649
+
650
+ .timeline__item::before {
651
+ content: '';
652
+ position: absolute;
653
+ left: -20px;
654
+ top: 14px;
655
+ width: 10px;
656
+ height: 10px;
657
+ border-radius: 50%;
658
+ background: var(--border);
659
+ }
660
+
661
+ .timeline__item--ok::before { background: var(--color-green); }
662
+ .timeline__item--fail::before { background: var(--color-red); }
663
+ .timeline__item--info::before { background: var(--color-blue); }
664
+
665
+ .timeline__stage {
666
+ font-family: var(--font-mono);
667
+ font-size: 0.8rem;
668
+ color: var(--accent-purple);
669
+ font-weight: 500;
670
+ }
671
+
672
+ .timeline__detail {
673
+ font-size: 0.8rem;
674
+ color: var(--text-secondary);
675
+ margin-top: 2px;
676
+ }
677
+
678
+ .timeline__time {
679
+ font-size: 0.7rem;
680
+ color: var(--text-muted);
681
+ font-family: var(--font-mono);
682
+ float: right;
683
+ }
684
+
685
+ /* ---- Projects Grid ---- */
686
+ .projects-grid {
687
+ display: grid;
688
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
689
+ gap: 16px;
690
+ margin-bottom: 32px;
691
+ }
692
+
693
+ .project-card {
694
+ position: relative;
695
+ background: var(--bg-card);
696
+ border: 1px solid var(--border);
697
+ border-radius: var(--radius);
698
+ transition: all 0.2s;
699
+ }
700
+
701
+ .project-card:hover {
702
+ border-color: var(--accent-purple);
703
+ background: var(--bg-card-hover);
704
+ }
705
+
706
+ .project-card__body {
707
+ padding: 20px;
708
+ cursor: pointer;
709
+ }
710
+
711
+ .project-card__delete {
712
+ position: absolute;
713
+ top: 8px;
714
+ right: 8px;
715
+ background: transparent;
716
+ border: 1px solid transparent;
717
+ border-radius: 6px;
718
+ padding: 4px 8px;
719
+ font-size: 0.95rem;
720
+ cursor: pointer;
721
+ opacity: 0.55;
722
+ transition: all 0.15s;
723
+ z-index: 2;
724
+ }
725
+
726
+ .project-card:hover .project-card__delete,
727
+ .project-picker__card:hover .project-card__delete {
728
+ opacity: 0.85;
729
+ }
730
+
731
+ .project-card__delete:hover {
732
+ opacity: 1 !important;
733
+ background: rgba(255, 80, 80, 0.15);
734
+ border-color: rgba(255, 80, 80, 0.4);
735
+ }
736
+
737
+ /* is_test toggle (KJC-TSK-0371). Sits to the LEFT of the delete
738
+ * button, same affordances. Visible at low opacity by default
739
+ * because — unlike delete — it conveys live state (is the
740
+ * cleaner watching this project?). Hovering brightens it. */
741
+ .project-card__is-test {
742
+ position: absolute;
743
+ top: 8px;
744
+ right: 44px;
745
+ background: transparent;
746
+ border: 1px solid transparent;
747
+ border-radius: 6px;
748
+ padding: 4px 8px;
749
+ font-size: 0.85rem;
750
+ cursor: pointer;
751
+ opacity: 0.55;
752
+ transition: all 0.15s;
753
+ z-index: 2;
754
+ }
755
+
756
+ .project-card:hover .project-card__is-test {
757
+ opacity: 0.85;
758
+ }
759
+
760
+ .project-card__is-test:hover {
761
+ opacity: 1 !important;
762
+ background: rgba(160, 160, 255, 0.15);
763
+ border-color: rgba(160, 160, 255, 0.4);
764
+ }
765
+
766
+ .project-card__name {
767
+ font-size: 1rem;
768
+ font-weight: 600;
769
+ color: var(--accent-purple);
770
+ margin-bottom: 8px;
771
+ }
772
+
773
+ .project-card__stats {
774
+ display: grid;
775
+ grid-template-columns: repeat(3, 1fr);
776
+ gap: 8px;
777
+ }
778
+
779
+ .project-card__stat {
780
+ text-align: center;
781
+ }
782
+
783
+ .project-card__stat-value {
784
+ font-size: 1.2rem;
785
+ font-weight: 600;
786
+ font-family: var(--font-mono);
787
+ color: var(--text-primary);
788
+ }
789
+
790
+ .project-card__stat-label {
791
+ font-size: 0.65rem;
792
+ color: var(--text-muted);
793
+ text-transform: uppercase;
794
+ }
795
+
796
+ .project-card__activity {
797
+ margin-top: 8px;
798
+ font-size: 0.7rem;
799
+ color: var(--text-muted);
800
+ }
801
+
802
+ /* ---- Empty State ---- */
803
+ .empty-state {
804
+ text-align: center;
805
+ padding: 60px 20px;
806
+ color: var(--text-muted);
807
+ }
808
+
809
+ .empty-state__title {
810
+ font-size: 1.1rem;
811
+ color: var(--text-secondary);
812
+ margin-bottom: 8px;
813
+ }
814
+
815
+ .empty-state__text {
816
+ font-size: 0.85rem;
817
+ max-width: 400px;
818
+ margin: 0 auto;
819
+ }
820
+
821
+ .empty-state__path {
822
+ font-family: var(--font-mono);
823
+ font-size: 0.8rem;
824
+ color: var(--accent-purple);
825
+ background: var(--bg-card);
826
+ padding: 4px 8px;
827
+ border-radius: var(--radius-sm);
828
+ display: inline-block;
829
+ margin-top: 8px;
830
+ }
831
+
832
+ /* ---- Loading ---- */
833
+ .loading {
834
+ text-align: center;
835
+ padding: 40px;
836
+ color: var(--text-muted);
837
+ }
838
+
839
+ .loading__spinner {
840
+ display: inline-block;
841
+ width: 24px;
842
+ height: 24px;
843
+ border: 2px solid var(--border);
844
+ border-top-color: var(--accent-purple);
845
+ border-radius: 50%;
846
+ animation: spin 0.8s linear infinite;
847
+ }
848
+
849
+ @keyframes spin {
850
+ to { transform: rotate(360deg); }
851
+ }
852
+
853
+ /* ---- Responsive ---- */
854
+ @media (max-width: 768px) {
855
+ .header {
856
+ flex-direction: column;
857
+ gap: 12px;
858
+ align-items: flex-start;
859
+ }
860
+
861
+ .header__nav {
862
+ width: 100%;
863
+ flex-wrap: wrap;
864
+ }
865
+
866
+ .kanban {
867
+ grid-template-columns: 1fr;
868
+ }
869
+
870
+ .stats-grid {
871
+ grid-template-columns: repeat(2, 1fr);
872
+ }
873
+
874
+ .modal {
875
+ margin: 8px;
876
+ max-height: 90vh;
877
+ }
878
+
879
+ .modal__quality-grid {
880
+ grid-template-columns: repeat(2, 1fr);
881
+ }
882
+
883
+ .main {
884
+ padding: 16px;
885
+ }
886
+ }
887
+
888
+ /* ---- Scrollbar ---- */
889
+ ::-webkit-scrollbar {
890
+ width: 8px;
891
+ height: 8px;
892
+ }
893
+
894
+ ::-webkit-scrollbar-track {
895
+ background: var(--bg-primary);
896
+ }
897
+
898
+ ::-webkit-scrollbar-thumb {
899
+ background: var(--border);
900
+ border-radius: 4px;
901
+ }
902
+
903
+ ::-webkit-scrollbar-thumb:hover {
904
+ background: var(--border-active);
905
+ }
906
+
907
+ /* ---- Scoped-project mode (URL: /p/<slug>) ---- */
908
+ /* Hide multi-project affordances when the board is scoped to a single
909
+ * project — the page only makes sense as "this project's Kanban".
910
+ * Multi-project UX (the All Projects selector, the Sessions tab, the
911
+ * Pipeline page link) adds noise and broke the user's mental model
912
+ * during dogfooding. */
913
+ body.scoped-mode .project-select,
914
+ body.scoped-mode .nav-btn[data-view="sessions"],
915
+ body.scoped-mode .nav-btn[data-view="dashboard"],
916
+ body.scoped-mode .header__nav a[href="/pipeline.html"] {
917
+ display: none;
918
+ }