@booboo-brain/panel 0.5.3 → 0.5.5

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.
package/dist/index.js CHANGED
@@ -1,73 +1,182 @@
1
1
  // src/Panel.tsx
2
2
  import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { orgBootSlice } from "@booboo-brain/spec";
3
+ import { orgBootSlice, relTime } from "@booboo-brain/spec";
4
4
 
5
5
  // src/panel-css.ts
6
6
  var PANEL_CSS = String.raw`
7
+ /* GENERATED from design/tokens.json by scripts/gen-tokens.mjs — DO NOT EDIT */
8
+ .pnl, .pnl-fatal {
9
+ --bg: #06080e;
10
+ --bg-lift: #0a0d15;
11
+ --card: #0f131c;
12
+ --line: #1c2130;
13
+ --line-hi: #2b3243;
14
+ --text: #E8DCC4;
15
+ --dim: #8a8268;
16
+ --faint: #585240;
17
+ --gold: #c9a04a;
18
+ --gold-hi: #E8C877;
19
+ --teal: #4ECDC4;
20
+ --violet: #a78bd0;
21
+ --green: #5fae7e;
22
+ --amber: #d6a23e;
23
+ --red: #d05a5a;
24
+ --wine: #0b0509;
25
+ --wine2: #140a11;
26
+ --plate: #1c1119;
27
+ --plate-lift: #241624;
28
+ --brass: #d9a05b;
29
+ --brass-hi: #efc389;
30
+ --brass-deep: #a9753a;
31
+ --mint: #8be9b0;
32
+ --cream: #f4eee2;
33
+ --cream-dim: #b3a897;
34
+ --cream-faint: #7c7365;
35
+ --verb-reports-to: #c9a04a;
36
+ --verb-declares: #E8C877;
37
+ --verb-amends: #E8C877;
38
+ --verb-inherits: #8a8268;
39
+ --verb-owns: #4ECDC4;
40
+ --verb-reads: #3a7a74;
41
+ --verb-escalates-to: #d05a5a;
42
+ --verb-covers: #5fae7e;
43
+ --verb-supplies: #a8815a;
44
+ --verb-audits: #a78bd0;
45
+ --verb-spine: #29242f;
46
+ --display: Fraunces, ui-serif, Georgia, serif;
47
+ --ui: 'DM Sans', ui-sans-serif, system-ui, sans-serif;
48
+ --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace;
49
+ --ease-swift: cubic-bezier(0.2, 0.7, 0.25, 1);
50
+ --ease-settle: cubic-bezier(0.16, 1, 0.3, 1);
51
+ --z-canvas: 0;
52
+ --z-hud: 10;
53
+ --z-dossier: 20;
54
+ --z-palette: 30;
55
+ --z-toast: 40;
56
+ }
57
+
7
58
  /* Booboo Organigram — alive, simple, for everyone. One accent (verdigris,
8
59
  the dachshund's collar), a slow aurora breathing behind everything, cards
9
60
  that lift and settle, numbers that count up. Dark by default. */
10
61
 
11
- :root {
12
- --bg: #0b0c10;
13
- --bg-1: #10131a;
14
- --bg-2: #171b24;
15
- --line: #242a36;
16
- --ink: #e9edf3;
17
- --ink-2: #9aa7b6;
18
- --ink-3: #5d6776;
19
- --accent: #4fb8a0;
20
- --accent-2: #7fd4c0;
21
- --accent-dim: rgba(79, 184, 160, 0.13);
22
- --warn: #e0b45f;
23
- --font: -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
24
- --mono: ui-monospace, "SF Mono", Menlo, monospace;
62
+ /* Scoped to the panel's own roots, NEVER :root — see scripts/gen-tokens.mjs.
63
+ A custom property on :root is shadowed by any ancestor declaring the same
64
+ name, so a host whose shell declares --bg-1/--line/--gold silently repaints
65
+ the board. Declaring them here means nothing can sit between these names and
66
+ the elements that read them. */
67
+ .pnl, .pnl-fatal {
68
+ /* THE STAFF BOARD — a master-key rack, not a dashboard.
69
+ Structure comes from two real artefacts: a master-key hierarchy (passe
70
+ général → passe partiel → cylinder, which is literally how a hotel is
71
+ keyed, and exactly how authority cascades here) and a ministry organigram
72
+ (rank is NAMED in column headers, never inferred from indentation).
73
+ Material comes from the approved FHQ hero designs — wine-black ground,
74
+ brass rails, real vendor marks on white chips.
75
+ Raw values are generated into app/tokens.css from design/tokens.json
76
+ (scripts/gen-tokens.mjs); these are the panel's semantic aliases over them.
77
+ RANK READS THROUGH FINISH, NEVER HUE. The old board coloured each card by
78
+ hash(agent.id) — nine departments, nine arbitrary neon hues — which is the
79
+ loudest "generated by a dev tool" signal there is, and it disagreed with
80
+ the cosmos's own gold/silver/bronze ladder. */
81
+ /* LIGHT IS THE DEFAULT (Jesse, 2026-07-19: "design for everyone — a white
82
+ background, very light bubbles, beautiful refined display"). A whole hotel
83
+ uses this, not a room of developers; a stranger should not land in a
84
+ terminal. Dark remains, one toggle away, below.
85
+ The ground is a WARM white, never flat #fff — a lit room, not a sheet of
86
+ paper. Depth comes from layered shadow, so ink stays the only dark thing
87
+ on screen and brass drops from material to ~5% accent. */
88
+ --bg: #fcfbf9;
89
+ --bg-1: #fffefb; /* tinted toward the brass hue, never flat #fff */
90
+ --bg-2: #f6f4f0;
91
+ --line: #e9e5df;
92
+ --line-soft: #f0ece6;
93
+ --line-2: #d9d3c9;
94
+ --surface-tray: rgba(255, 255, 255, .5);
95
+ /* the ground-plane dot. White-on-white rendered NOTHING on the light board
96
+ — the texture was there in the stylesheet and absent on screen. */
97
+ --dot: rgba(27, 24, 21, 0.055);
98
+ --surface-lift: linear-gradient(180deg, #fffdf8, #ffffff);
99
+ --surface-lift-edge: #e8d9bd;
100
+ --ink: #1b1815;
101
+ --ink-2: #6b6459;
102
+ --ink-3: #9a9287;
103
+ --accent: #a9753a;
104
+ --accent-2: #c9954f;
105
+ --accent-dim: #f7f0e4;
106
+ --warn: #b8862c;
107
+ --warn-wash: #fbf3e3;
108
+ --ok: #3f9c6d;
109
+ --ok-wash: #eaf5ef;
110
+ --fail: #c0553f;
111
+ --fail-wash: #fbeeea;
112
+ /* one shadow set, three depths — the only source of elevation */
113
+ --sh-1: 0 1px 2px rgba(27, 24, 21, .04), 0 1px 1px rgba(27, 24, 21, .03);
114
+ --sh-2: 0 2px 4px rgba(27, 24, 21, .04), 0 6px 16px -6px rgba(27, 24, 21, .10);
115
+ --sh-3: 0 4px 8px rgba(27, 24, 21, .05), 0 16px 34px -12px rgba(27, 24, 21, .16);
116
+ /* motion — slight, never showy. Everything eases on these two. */
117
+ --ease: cubic-bezier(.2, .7, .25, 1);
118
+ --settle: cubic-bezier(.16, 1, .3, 1);
119
+ --font: "DM Sans", -apple-system, "Segoe UI", Inter, system-ui, sans-serif;
120
+ --mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
121
+ --display: "Fraunces", ui-serif, Georgia, serif;
122
+ --brass-line: linear-gradient(180deg, var(--brass-hi), var(--brass) 55%, var(--brass-deep));
25
123
  }
26
124
 
27
- /* LIGHT theme same verdigris accent, swapped surfaces. Toggle in the top bar. */
28
- :root[data-theme="light"] {
29
- --bg: #f5f7f9;
30
- --bg-1: #ffffff;
31
- --bg-2: #eef1f4;
32
- --line: #dbe1e8;
33
- --ink: #182029;
34
- --ink-2: #55636f;
35
- --ink-3: #8b97a3;
36
- --accent: #1e8f79;
37
- --accent-2: #12796a;
38
- --accent-dim: rgba(30, 143, 121, 0.12);
39
- --warn: #b57b1e;
40
- }
41
- [data-theme="light"] .bar,
42
- [data-theme="light"] .tabs { background: rgba(255, 255, 255, 0.82); }
43
- [data-theme="light"] .btn.primary { color: #ffffff; }
44
- [data-theme="light"] .pnl-aurora { opacity: 0.32; }
45
- [data-theme="light"] .doss-rep.tap:hover { background: rgba(0, 0, 0, 0.03); }
46
- [data-theme="light"] .bk-card:hover,
47
- [data-theme="light"] .mem-row:hover { box-shadow: 0 8px 22px rgba(20, 40, 60, 0.08); }
48
- /* surfaces that hardcode a dark tint — retint for the light theme */
49
- [data-theme="light"] .oc-row.wrap { background: rgba(20, 40, 60, 0.04); }
50
- [data-theme="light"] .doss { background: rgba(255, 255, 255, 0.9); }
51
- [data-theme="light"] .ag {
52
- background: linear-gradient(180deg, hsl(var(--h) 42% 93% / 0.9), var(--bg-1) 60%);
53
- border-color: hsl(var(--h) 30% 78%);
54
- }
55
- [data-theme="light"] .tl-ava { background: hsl(var(--h) 45% 90% / 0.9); border-color: hsl(var(--h) 40% 75% / 0.5); }
56
- [data-theme="light"] .oc-tray { background: linear-gradient(180deg, #e7ebef, #eef1f4); box-shadow: inset 0 3px 7px rgba(20, 40, 60, 0.08); }
57
- [data-theme="light"] .oc-mac { background: rgba(20, 40, 60, 0.05); border-color: rgba(20, 40, 60, 0.1); }
125
+ /* DARKthe wine-and-brass board, now one toggle away instead of the default.
126
+ Same structure, same shadows-as-depth logic, inverted ground.
127
+ Scoped to the panel's own root (not :root) for two reasons: the panel must
128
+ not read a HOST's data-theme as its own, and it must not write one either. */
129
+ .pnl[data-theme="dark"], .pnl-fatal[data-theme="dark"] {
130
+ --bg: var(--wine);
131
+ --bg-1: var(--plate);
132
+ --bg-2: var(--wine2);
133
+ --line: rgba(244, 238, 226, 0.13);
134
+ --line-soft: rgba(244, 238, 226, 0.08);
135
+ --line-2: rgba(244, 238, 226, 0.22);
136
+ --surface-tray: rgba(11, 5, 9, .4);
137
+ --dot: rgba(255, 255, 255, 0.035);
138
+ --surface-lift: linear-gradient(168deg, #3a2a16, #1d1410);
139
+ --surface-lift-edge: rgba(239, 195, 137, .4);
140
+ --ink: var(--cream);
141
+ --ink-2: var(--cream-dim);
142
+ --ink-3: var(--cream-faint);
143
+ --accent: var(--brass);
144
+ --accent-2: var(--brass-hi);
145
+ --accent-dim: rgba(217, 160, 91, 0.12);
146
+ --warn: var(--brass);
147
+ --warn-wash: rgba(217, 160, 91, 0.10);
148
+ --ok: var(--mint);
149
+ --ok-wash: rgba(139, 233, 176, 0.10);
150
+ --fail: #e07b6a;
151
+ --fail-wash: rgba(224, 123, 106, 0.10);
152
+ --sh-1: 0 1px 2px rgba(0, 0, 0, .5);
153
+ --sh-2: 0 2px 4px rgba(0, 0, 0, .5), 0 16px 34px -18px rgba(0, 0, 0, .85);
154
+ --sh-3: 0 4px 8px rgba(0, 0, 0, .5), 0 22px 48px -18px rgba(0, 0, 0, .9);
155
+ }
156
+ /* The [data-theme="dark"] body rule that used to live here is GONE. A mounted
157
+ component has no business repainting its host's <body> — in Dionisos OS it
158
+ turned the cockpit's body white while the cards stayed dark, which is half of
159
+ what "split-brain" looked like. The standalone app paints its own body in
160
+ app/index.html, which is the page that actually owns it. */
161
+ .pnl[data-theme="dark"] .ag { background: linear-gradient(168deg, var(--plate-lift), #180e16); }
162
+ .pnl[data-theme="dark"] .bar, .pnl[data-theme="dark"] .tabs { background: rgba(11, 5, 9, 0.72); }
163
+ .pnl[data-theme="dark"] .rack .wt { background: #fff; border-color: transparent; }
164
+ .pnl[data-theme="dark"] .pnl-stars { opacity: .45; }
58
165
 
59
- * { box-sizing: border-box; }
60
- body { font-family: var(--font); color: var(--ink); background: var(--bg); overflow-x: hidden; }
166
+ .pnl, .pnl-fatal, .pnl *, .pnl-fatal * { box-sizing: border-box; }
61
167
 
168
+ .pnl, .pnl-fatal { font-family: var(--font); color: var(--ink); background: var(--bg); }
62
169
  .pnl { display: flex; flex-direction: column; position: relative; overflow: hidden; }
63
170
 
64
- /* the breatha slow aurora drifting behind everything */
171
+ /* the groundwarm wine light pooling from the top-left, exactly as the hero
172
+ grades it. NO GRAIN/NOISE LAYER: the mockup carried a feTurbulence overlay
173
+ and it read as a blurry filter smeared over everything (Jesse, 2026-07-19).
174
+ The atmosphere is gradient + constellation only. */
65
175
  .pnl-aurora {
66
- position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity: 0.5;
176
+ position: fixed; inset: -30%; pointer-events: none; z-index: 0; opacity: 1;
67
177
  background:
68
- radial-gradient(38% 30% at 24% 30%, rgba(79, 184, 160, 0.10), transparent 70%),
69
- radial-gradient(30% 26% at 78% 64%, rgba(90, 130, 200, 0.08), transparent 70%),
70
- radial-gradient(24% 22% at 55% 15%, rgba(79, 184, 160, 0.06), transparent 70%);
178
+ radial-gradient(55% 45% at 12% 0%, rgba(201, 149, 79, .055), transparent 62%),
179
+ radial-gradient(45% 40% at 95% 100%, rgba(63, 156, 109, .035), transparent 62%);
71
180
  animation: aurora 48s ease-in-out infinite alternate;
72
181
  }
73
182
  @keyframes aurora {
@@ -87,12 +196,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
87
196
  /* top bar */
88
197
  .bar {
89
198
  position: relative; z-index: 2; display: flex; align-items: center; gap: 22px;
90
- padding: 14px 26px; border-bottom: 1px solid var(--line);
91
- background: rgba(16, 19, 26, 0.82); backdrop-filter: blur(10px);
199
+ padding: 14px 26px; border-bottom: 1px solid var(--line-soft);
200
+ background: rgba(252, 251, 249, 0.82); backdrop-filter: blur(10px);
92
201
  flex-wrap: wrap;
93
202
  }
94
- .bar-brand { font-size: 15px; }
95
- .bar-brand b { font-weight: 650; }
203
+ .bar-brand { font-size: 19px; font-family: var(--display); font-weight: 600; letter-spacing: -0.01em; }
204
+ .bar-brand b { font-weight: 600; color: var(--accent-2); }
96
205
  .bar-stats { display: flex; gap: 18px; color: var(--ink-2); font-size: 12.5px; }
97
206
  .bar-stats b { color: var(--ink); font-variant-numeric: tabular-nums; font-size: 14px; }
98
207
  .bar-stats .tap { cursor: pointer; transition: color 0.15s; }
@@ -106,22 +215,23 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
106
215
  .btn {
107
216
  border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
108
217
  border-radius: 9px; padding: 8px 15px; font-size: 12.5px; cursor: pointer;
109
- transition: border-color 0.15s, transform 0.1s;
218
+ transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
110
219
  }
111
220
  .btn:hover { border-color: var(--accent); }
112
221
  .btn:active { transform: scale(0.97); }
113
- .btn.primary { background: var(--accent); border-color: var(--accent); color: #08110e; font-weight: 650; }
222
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 650; }
114
223
  .btn.primary:disabled { opacity: 0.5; cursor: wait; }
115
224
  .btn.ghost { background: transparent; color: var(--ink-2); }
116
225
 
117
226
  /* tabs */
118
227
  .tabs {
119
228
  position: relative; z-index: 2; display: flex; gap: 4px; padding: 0 22px;
120
- border-bottom: 1px solid var(--line); background: rgba(16, 19, 26, 0.7); backdrop-filter: blur(8px);
229
+ border-bottom: 1px solid var(--line); background: rgba(252, 251, 249, 0.7); backdrop-filter: blur(8px);
121
230
  overflow-x: auto;
122
231
  }
123
232
  .tab {
124
233
  background: none; border: none; border-bottom: 2px solid transparent; color: var(--ink-2);
234
+ transition: color .18s var(--ease), border-color .22s var(--ease);
125
235
  font-size: 12.5px; padding: 12px 15px 11px; cursor: pointer; white-space: nowrap;
126
236
  transition: color 0.15s, border-color 0.2s;
127
237
  }
@@ -197,7 +307,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
197
307
 
198
308
  /* rules */
199
309
  .rule-list { display: flex; flex-direction: column; gap: 9px; }
200
- .rule-card { background: var(--bg-1); border: 1px solid var(--line); border-left: 2px solid var(--accent); border-radius: 11px; padding: 12px 15px 10px; animation: settle 0.35s ease both; transition: border-color 0.15s; }
310
+ .rule-card { background: var(--bg-1); border: 1px solid var(--line); border-radius: 11px; padding: 12px 15px 10px; animation: settle 0.35s ease both; transition: border-color 0.15s; }
201
311
  .rule-card:hover { border-color: var(--accent); }
202
312
  .rule-top { display: flex; align-items: baseline; gap: 10px; margin-bottom: 5px; }
203
313
  .rule-ref { font-family: var(--mono); font-size: 12.5px; }
@@ -229,19 +339,17 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
229
339
  /* the machine rack — a recessed slot flush under the owner's card */
230
340
  .oc-tray {
231
341
  display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;
232
- max-width: 210px; margin-top: -6px; padding: 10px 8px 7px;
233
- background: linear-gradient(180deg, #0a0c11, #0d1017);
234
- border: 1px solid var(--line); border-top: 0; border-radius: 0 0 14px 14px;
235
- box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.55);
342
+ max-width: 210px; margin-top: -4px; padding: 10px 8px 7px;
343
+ background: transparent; border: 0; padding: 8px 0 0;
236
344
  }
237
- .oc-tray.fail { border-color: rgba(224, 123, 106, 0.55); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.55), 0 6px 18px -8px rgba(224, 123, 106, 0.3); }
238
- .oc-tray.warn { border-color: rgba(224, 180, 95, 0.45); }
345
+ .oc-tray.fail { border-color: rgba(224, 123, 106, 0.5); box-shadow: inset 0 3px 8px rgba(0, 0, 0, 0.5), 0 6px 18px -8px rgba(224, 123, 106, 0.28); }
346
+ .oc-tray.warn { border-color: rgba(217, 160, 91, 0.42); }
239
347
  .oc-mac {
240
- display: inline-flex; align-items: center; gap: 5px;
241
- background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.05);
242
- border-radius: 6px; padding: 4px 8px; cursor: pointer;
243
- font-family: var(--mono); font-size: 10px; color: var(--ink-2);
244
- transition: border-color 0.15s, transform 0.1s;
348
+ display: inline-flex; align-items: center; gap: 6px;
349
+ background: var(--bg-2); border: 1px solid var(--line);
350
+ border-radius: 8px; padding: 5px 9px; cursor: pointer;
351
+ font-family: var(--mono); font-size: 9.5px; color: var(--ink-2);
352
+ transition: border-color .18s var(--ease), transform .12s var(--ease), box-shadow .2s var(--ease);
245
353
  }
246
354
  .oc-mac:hover { border-color: var(--accent); color: var(--ink); transform: translateY(-1px); }
247
355
  .oc-mac.sel { border-color: var(--accent); background: var(--accent-dim); color: var(--ink); }
@@ -249,6 +357,21 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
249
357
  .oc-mac.dragging { opacity: 0.45; cursor: grabbing; }
250
358
  .oc-mac.more:hover { color: var(--accent); }
251
359
  .mac-emoji { font-size: 11px; }
360
+ /* real vendor marks, tinted to the house so twelve logos read as one system.
361
+ Full brand colour on hover — recognition when you want it, calm when you
362
+ don't. */
363
+ /* the hero's treatment: a real vendor mark on a WHITE chip. Twelve tinted
364
+ monochrome logos read as a sticker album; twelve white chips read as a rack
365
+ of real systems — and it is the one thing on the old board that already
366
+ looked genuine, so everything else was raised to it rather than it lowered. */
367
+ .mac-brand {
368
+ width: 24px; height: 24px; flex: 0 0 24px; display: block;
369
+ background: var(--bg-2); border: 1px solid var(--line); border-radius: 7px;
370
+ padding: 4px; box-sizing: border-box;
371
+ transition: transform .2s ease, box-shadow .2s ease;
372
+ }
373
+ .oc-mac:hover .mac-brand,
374
+ .oc-mac.sel .mac-brand { transform: scale(1.06); box-shadow: 0 5px 14px -5px rgba(0, 0, 0, 0.85); }
252
375
  .mac-name { white-space: nowrap; max-width: 76px; overflow: hidden; text-overflow: ellipsis; }
253
376
  .mac-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 7px; display: inline-block; }
254
377
  .mac-dot.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
@@ -285,10 +408,35 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
285
408
  .bk-agents.unassigned { color: var(--ink-3); font-style: italic; }
286
409
 
287
410
  .pending {
288
- position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap;
289
- padding: 10px 26px; border-bottom: 1px solid var(--line); background: rgba(224, 180, 95, 0.06);
411
+ position: relative; z-index: 2; display: flex; gap: 8px; flex-wrap: wrap; align-items: center;
412
+ padding: 10px 26px; border-bottom: 1px solid var(--line); background: var(--warn-wash);
413
+ }
414
+ .pending-lead {
415
+ font-family: var(--mono); font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
416
+ color: var(--warn); margin-right: 2px;
417
+ }
418
+ /* every pending change is its own undo button. A global discard makes one
419
+ mis-drop cost every other edit, so people stop dragging entirely. */
420
+ .pending-item {
421
+ display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
422
+ font-size: 11.5px; font-family: var(--mono); color: var(--ink-2);
423
+ background: var(--bg-1); border: 1px solid var(--line); border-radius: 100px;
424
+ padding: 4px 6px 4px 12px;
425
+ transition: border-color .18s var(--ease), color .18s ease, transform .16s var(--ease);
426
+ }
427
+ .pending-item i {
428
+ font-style: normal; font-size: 8.5px; letter-spacing: .12em; text-transform: uppercase;
429
+ color: var(--warn); border: 1px solid rgba(184,134,44,.3); border-radius: 100px; padding: 2px 7px;
430
+ transition: background .18s ease, color .18s ease;
431
+ }
432
+ .pending-item:hover { border-color: var(--warn); color: var(--ink); transform: translateY(-1px); }
433
+ .pending-item:hover i { background: var(--warn); color: var(--bg-1); }
434
+
435
+ /* the lane lights up as a drop target, so a near-miss is impossible */
436
+ .oc-row.lanes > .oc-child.lane-over {
437
+ border-color: var(--accent); background: var(--accent-dim);
438
+ box-shadow: 0 0 0 3px var(--accent-dim), var(--sh-2);
290
439
  }
291
- .pending-item { font-size: 11.5px; color: var(--warn); font-family: var(--mono); }
292
440
  .pnl-err { position: relative; z-index: 2; padding: 10px 26px; color: #e07b6a; font-size: 12.5px; }
293
441
 
294
442
  /* layout — 100% so a HOST container decides the height (embedded panel);
@@ -301,17 +449,59 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
301
449
  + a verdigris pool anchoring the eye at the root. Static — capture-safe. */
302
450
  background:
303
451
  radial-gradient(900px 420px at 50% 120px, rgba(79, 184, 160, 0.05), transparent 70%),
304
- radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.035) 1px, transparent 1.5px);
452
+ radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 1.5px);
305
453
  background-size: auto, 28px 28px;
306
454
  }
307
- .tree-hint { color: var(--ink-3); font-size: 11.5px; margin: 0 0 20px; text-align: center; }
455
+ .tree-hint { color: var(--ink-3); font-family: var(--mono); font-size: 10px; letter-spacing: .05em; margin: 0 0 14px; text-align: center; }
456
+
457
+ /* ── RANK HEADERS (from the ministry organigram) ───────────────────────────
458
+ Rank is NAMED, never inferred from indentation. Roman numerals + a rule
459
+ under each, spanning the columns the cascade actually occupies. */
460
+ /* four headers over the four real cascade columns: law · gm · departments ·
461
+ staff. Widths mirror .cascade so a header sits over the column it names —
462
+ headers labelling columns that do not exist is worse than no headers. */
463
+ .ranks {
464
+ display: grid;
465
+ grid-template-columns: 232px var(--rail-w, 56px) 268px var(--rail-w, 56px) minmax(232px, 300px) var(--rail-w, 56px) 1fr;
466
+ margin: 0 0 16px; padding: 0 16px; flex: 0 0 auto;
467
+ }
468
+ .rank:nth-child(1) { grid-column: 1; }
469
+ .rank:nth-child(2) { grid-column: 3; }
470
+ .rank:nth-child(3) { grid-column: 5; }
471
+ .rank:nth-child(4) { grid-column: 7; }
472
+ .rank {
473
+ font-family: var(--mono); font-size: 9px; letter-spacing: .2em; text-transform: uppercase;
474
+ color: var(--ink-3); padding-bottom: 8px; border-bottom: 1px solid rgba(217, 160, 91, 0.16);
475
+ position: relative; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
476
+ }
477
+ .rank b { color: var(--brass); font-weight: 500; margin-right: 7px; }
478
+ .rank::after { content: ""; position: absolute; left: 0; bottom: -1px; width: 24px; height: 1px; background: var(--brass); }
479
+
480
+ /* the constellation — the hero's own atmosphere, drawn once behind the rack.
481
+ Gradient + points only; NO grain/noise overlay (it read as a blurry filter
482
+ smeared across the board). */
483
+ .pnl-stars { position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: 0; width: 100%; height: 100%; }
308
484
 
309
485
  /* THE COMPANY CHART — root on top, branches fanning beneath. Connectors are
310
486
  crafted: curved elbows at the row's ends, branch-hued drops, a glowing
311
487
  junction node where the parent's line meets the bar. Pure CSS. */
312
488
  /* the measuring viewport — flex:1 + min-height:0 lets it consume the height
313
489
  .tree gives it; align/justify center the scaled block for narrow orgs. */
314
- .chart-fit { position: relative; flex: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: center; overflow: hidden; }
490
+ /* Always scrollable. It used to be overflow:hidden unless the user manually
491
+ zoomed — safe only while auto-fit guaranteed the whole board fit one screen.
492
+ A lane board grows downward, so that assumption sealed every lane below the
493
+ fold behind an invisible wall. */
494
+ .chart-fit { position: relative; flex: 1; min-height: 0; display: flex; align-items: flex-start; justify-content: center; overflow: auto; }
495
+ /* transform:scale() does not change layout size, so the scaled block gets a
496
+ spacer sized to its VISUAL footprint — otherwise the scroll extent is the
497
+ unscaled height and you scroll into empty space. */
498
+ /* overflow:hidden clips the UNSCALED layout box of the absolutely-positioned
499
+ chart, which otherwise adds its full natural height to the scroll extent —
500
+ ~900px of empty scroll past the last lane. The visual (scaled) content is
501
+ exactly the spacer's size, so nothing real is clipped. */
502
+ .chart-scaled { position: relative; flex: 0 0 auto; overflow: hidden; }
503
+ .chart-scaled > .chart { position: absolute; top: 0; left: 50%; transform-origin: top center; }
504
+ .chart-fit.zoomed .chart-scaled > .chart { left: 0; transform-origin: top left; }
315
505
  /* user-zoomed: native scroll is the pan; top-left origin keeps the scaled
316
506
  overflow fully reachable (top/center origin would clip the left half). */
317
507
  .chart-fit.zoomed { overflow: auto; justify-content: flex-start; }
@@ -329,35 +519,252 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
329
519
  .zoomer button:hover { background: var(--accent-dim); color: var(--ink); }
330
520
  .zoomer button.on { color: var(--accent); }
331
521
  .zoomer-pct { min-width: 40px; text-align: center; color: var(--ink-3); }
522
+ .zoomer-sep { width: 1px; height: 16px; background: var(--line); margin: 0 2px; }
523
+
524
+ /* ── the ledger shelf: every bucket as a pigeonhole, hover a role to see its
525
+ reach. Docked under the chart, always visible — the memory half of the
526
+ two pillars (structure above, memory below), never a separate tab. */
527
+ .ledger-shelf {
528
+ position: relative; z-index: 2; flex: 0 0 auto; margin-top: 14px;
529
+ padding: 10px 16px 12px; border-radius: 10px;
530
+ border: 1px solid var(--line); background: var(--bg-1); box-shadow: var(--sh-2);
531
+ width: min(1180px, 92vw);
532
+ }
533
+ .shelf-label {
534
+ display: block; font-family: var(--mono); font-size: 9px; letter-spacing: 0.14em;
535
+ text-transform: uppercase; color: var(--brass); margin-bottom: 8px;
536
+ }
537
+ .shelf-slots { display: flex; flex-wrap: wrap; gap: 6px; }
538
+ .shelf-slot {
539
+ font-family: var(--mono); font-size: 10px; color: var(--ink-3);
540
+ background: var(--bg-2); border: 1px solid var(--line); border-radius: 5px;
541
+ padding: 4px 9px; transition: border-color .18s ease, color .18s ease, box-shadow .25s ease, transform .18s ease;
542
+ }
543
+ .shelf-slot.lit {
544
+ color: var(--brass-hi); border-color: var(--brass); transform: translateY(-1px);
545
+ box-shadow: 0 0 0 1px rgba(201, 160, 74, 0.3), 0 6px 16px -6px rgba(201, 160, 74, 0.5);
546
+ background: rgba(201, 160, 74, 0.1);
547
+ }
548
+ .shelf-slot.sealed { color: var(--ink-3); font-style: italic; border-style: dashed; opacity: 0.75; }
549
+ .shelf-slot.sealed.lit { color: var(--ink-3); border-color: var(--line); background: var(--bg-2); box-shadow: none; transform: none; }
550
+ @media (prefers-reduced-motion: reduce) { .shelf-slot { transition: none; } }
551
+
552
+ /* the law toggle in the top bar */
553
+ .law-toggle { font-family: var(--mono); letter-spacing: 0.02em; }
554
+ .law-toggle.on { color: var(--brass-hi); border-color: var(--brass); background: rgba(201, 160, 74, 0.12); }
332
555
  /* min-width:max-content keeps connectors geometrically correct at natural size
333
556
  BEFORE scaling; the measured --fit shrinks the whole block toward its head. */
557
+ /* transform is set inline by Panel.tsx (see the note there) — only origin,
558
+ sizing and the easing live here. */
334
559
  .chart {
335
560
  min-width: max-content; padding: 8px 14px 20px;
336
- transform: scale(var(--fit, 1)); transform-origin: top center;
561
+ transform-origin: top center; position: relative; /* the rail plane's origin */
337
562
  transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1); will-change: transform;
338
563
  }
564
+ /* plates sit above the rail plane */
565
+ .chart .cascade { position: relative; z-index: 1; }
566
+
567
+ /* rank I — the law. Brass-edged like the GM but unfilled: it is a document,
568
+ not an operator. */
569
+ .law-plate { width: 232px; cursor: pointer; }
570
+ .law-plate .ag-ava { font-family: var(--display); color: var(--accent); }
571
+ .law-plate .ag-name { color: var(--accent); font-size: 13px; }
572
+ .law-plate .ag-role { word-break: break-all; }
573
+ /* ── THE CASCADE (master-key structure) ───────────────────────────────────
574
+ Rank runs LEFT TO RIGHT, one column each, exactly as a master-key chart
575
+ reads: THE STANDARD → GM → departments → staff. Two nested grids give the
576
+ four columns; the brass elbows between them are drawn by CascadeRails as
577
+ one measured SVG (orthogonal geometry between arbitrary boxes is not a
578
+ thing CSS pseudo-elements can do honestly). */
579
+ .cascade {
580
+ display: grid;
581
+ grid-template-columns: max-content var(--rail-w, 56px) 1fr;
582
+ /* START, not centre. Centring is right for a 3-branch key chart; with nine
583
+ departments the stack is ~2,200px tall, so it buried the master key and the
584
+ GM below the fold — the two plates that must be seen first. Top-aligned
585
+ also matches the ministry chart's reading order. */
586
+ align-items: start;
587
+ }
588
+ .cascade > .law-plate { grid-column: 1; }
589
+ .cascade > .ocn { grid-column: 3; }
590
+
591
+ /* rank II–IV live inside the root .ocn, which is itself a cascade */
592
+ .cascade > .ocn {
593
+ display: grid;
594
+ /* the lanes column is BOUNDED so staff wrap inside their lane. Left at 1fr
595
+ under .chart's max-content sizing it grew to fit every staff plate on one
596
+ row — 2,400px natural width, which fit-to-width then scaled to 52% and made
597
+ every plate unreadable. Rank IV wraps; the board stays legible. */
598
+ grid-template-columns: max-content var(--rail-w, 56px) minmax(420px, 940px);
599
+ grid-template-areas: "gm rail lanes";
600
+ align-items: start;
601
+ }
602
+ .cascade > .ocn > .casc-head { grid-area: gm; display: flex; flex-direction: column; align-items: stretch; }
603
+ .cascade > .ocn > .oc-down { grid-area: rail; }
604
+ .cascade > .ocn > .oc-row.lanes { grid-area: lanes; }
605
+
606
+ /* the SVG rail plane — sits under the plates, never intercepts a click */
607
+ .rails { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: visible; width: 100%; height: 100%; }
608
+
339
609
  .ocn { display: flex; flex-direction: column; align-items: center; }
340
- .oc-down { width: 1.5px; height: 18px; position: relative;
341
- background: linear-gradient(180deg, hsl(165 40% 45% / 0.5), var(--line)); }
610
+ /* the connective tissue reads brass everywhere — a company chart drawn with
611
+ the house's own material, not a generic diagram line. */
612
+ .oc-down { width: 2px; height: 18px; position: relative; background: var(--brass-line); opacity: 0.75; }
342
613
  .oc-down::after { content: ""; position: absolute; bottom: -2px; left: 50%;
343
614
  width: 5px; height: 5px; transform: translateX(-50%); border-radius: 50%;
344
- background: var(--accent); box-shadow: 0 0 8px rgba(79, 184, 160, 0.5); }
615
+ background: var(--brass-hi); box-shadow: 0 0 8px rgba(201, 160, 74, 0.55); }
345
616
  .oc-row { display: flex; align-items: flex-start; }
346
- .oc-child { position: relative; padding: 18px 5px 0; display: flex; flex-direction: column; align-items: center; }
347
- .oc-child::before { content: ""; position: absolute; top: 0; left: 50%; width: 1.5px; height: 18px;
348
- background: linear-gradient(180deg, var(--line), hsl(var(--h, 165) 32% 34%)); }
349
- .oc-child::after { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 16px;
350
- border-top: 1.5px solid var(--line); }
351
- .oc-child:first-child::after { left: 50%; border-left: 1.5px solid var(--line); border-top-left-radius: 14px; }
352
- .oc-child:last-child::after { right: 50%; border-right: 1.5px solid var(--line); border-top-right-radius: 14px; }
353
- .oc-child:first-child::before, .oc-child:last-child::before { display: none; }
354
- .oc-child:only-child::after { display: none; }
355
- .oc-child:only-child::before { display: block; }
356
617
 
357
- /* the branch wakes when you hover its parent */
358
- .ocn:has(> .ag:hover) > .oc-down,
359
- .ocn:has(> .ag:hover) > .oc-row > .oc-child::before { background: hsl(165 45% 45% / 0.6); }
360
- .ocn:has(> .ag:hover) > .oc-row > .oc-child::after { border-color: hsl(165 45% 45% / 0.6); }
618
+ /* ── THE STAFF BOARD: departments as lanes, stacking DOWNWARD ──────────────
619
+ Nine departments fanned sideways forced a canvas nobody could read at 17%
620
+ zoom. Each lane is a row of the board: a brass rail down the left, the
621
+ head's card, then its people flowing right and wrapping inside the lane.
622
+ The board scrolls like a document; every card stays at legible size. */
623
+ /* the lanes are rank III+IV: one shelf per department, stacked. Width now comes
624
+ from the cascade's 1fr column, not a hardcoded viewport clamp. */
625
+ .oc-row.lanes {
626
+ flex-direction: column;
627
+ align-items: stretch;
628
+ gap: 14px;
629
+ padding-top: 0;
630
+ width: 100%;
631
+ }
632
+ /* a lane is a SHELF on the rack — a hairline tray, not a coloured panel. The
633
+ old lane had a hash-hue left border and tint per department; the rack is one
634
+ material throughout and the department is named, not coloured. */
635
+ .oc-row.lanes > .oc-child {
636
+ flex: 1 1 auto;
637
+ position: relative;
638
+ border: 1px solid var(--line);
639
+ border-radius: 18px;
640
+ background: var(--surface-tray);
641
+ padding: 16px 18px 18px;
642
+ transition: border-color .25s ease, box-shadow .35s var(--ease);
643
+ }
644
+ .oc-row.lanes > .oc-child:hover { border-color: var(--line-2, #d9d3c9); box-shadow: var(--sh-2); }
645
+ @media (prefers-reduced-motion: reduce) {
646
+ .oc-row.lanes > .oc-child:hover { transform: none; }
647
+ }
648
+ /* lanes own their connectors — kill the fan's drop-lines inside them */
649
+ .oc-row.lanes > .oc-child::before,
650
+ .oc-row.lanes > .oc-child::after { display: none; }
651
+ .oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
652
+
653
+ /* inside a lane: head card top-left, its service tray beneath the head, a
654
+ brass rail column, its people filling the right. Explicit areas — an
655
+ implicit 2-column grid put the tray in the staff slot the moment every
656
+ department gained services, which shunted the people into a one-card
657
+ column. The rail column is the elbow HANDOFF.md flagged as hidden: the
658
+ department head commanding its people, drawn once, orthogonally. */
659
+ .oc-row.lanes > .oc-child > .ocn {
660
+ display: grid;
661
+ grid-template-columns: minmax(278px, 300px) var(--rail-w, 56px) 1fr;
662
+ grid-template-areas:
663
+ "head rail staff"
664
+ "macs rail staff";
665
+ grid-template-rows: auto 1fr;
666
+ align-items: start;
667
+ gap: 10px 0;
668
+ }
669
+ .oc-row.lanes > .oc-child > .ocn > .ag { grid-area: head; }
670
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray { grid-area: macs; }
671
+ .oc-row.lanes > .oc-child > .ocn > .oc-row { grid-area: staff; margin-left: 4px; }
672
+
673
+ /* the rail: a vertical brass spine for the department, capped where it meets
674
+ the head, with one orthogonal branch reaching into the staff block. This
675
+ is the ".oc-down" element — hidden below (line ~490 in the old cascade),
676
+ here it's re-purposed as the rail instead of the plain tree drop-line. */
677
+ .oc-row.lanes > .oc-child > .ocn > .oc-down {
678
+ display: block; grid-area: rail; position: relative; width: 100%; height: 100%;
679
+ min-height: 44px;
680
+ /* the base .oc-down is a 2px-wide FILLED brass drop-line. Widening it to the
681
+ rail column without clearing its background painted a solid brass slab down
682
+ every lane — the rail is drawn by ::before/::after, not by the box itself. */
683
+ background: none;
684
+ }
685
+ /* No ::before/::after brass here any more — the SVG rail plane draws every
686
+ connector from measured box positions. Leaving these would double each rail. */
687
+
688
+ /* semantic zoom lever: the rail itself is the fold/unfold control at the
689
+ department level — house → department → role, one click at a time. */
690
+ .oc-down.foldable { cursor: pointer; }
691
+ .oc-down.foldable:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; border-radius: 4px; }
692
+ .oc-fold-glyph {
693
+ position: absolute; left: 50%; top: 22px; transform: translate(-50%, -50%);
694
+ font-style: normal; font-size: 10px; line-height: 1; width: 15px; height: 15px;
695
+ display: grid; place-items: center; border-radius: 50%;
696
+ background: var(--bg-1); border: 1px solid var(--line-2); color: var(--accent);
697
+ transition: transform .15s ease, background .15s ease;
698
+ }
699
+ .oc-down.foldable:hover .oc-fold-glyph { background: rgba(201, 160, 74, 0.18); transform: translate(-50%, -50%) scale(1.15); }
700
+ .oc-down.folded .oc-fold-glyph { color: var(--ink-2); border-color: var(--line); }
701
+
702
+ /* the folded department: head visible, staff summarised to one line — the
703
+ "department" rung of house → department → role. */
704
+ .oc-folded-summary {
705
+ grid-area: staff; align-self: center; justify-self: start; text-align: left;
706
+ background: none; border: 1px dashed var(--line); border-radius: 8px;
707
+ padding: 9px 13px; margin-left: 4px; cursor: pointer;
708
+ font-family: var(--mono); font-size: 11px; color: var(--ink-3);
709
+ transition: border-color .15s ease, color .15s ease;
710
+ }
711
+ .oc-folded-summary:hover { border-color: var(--brass); color: var(--brass-hi); }
712
+ /* "show the law": the SAME rails, re-read as authority. The SVG plane swaps to
713
+ a travelling dashed brass so the law is seen flowing down the cascade. */
714
+ .rail-line { stroke: var(--line-2, #d9d3c9); stroke-width: 1.5; stroke-linecap: round; fill: none; }
715
+ .rail-dash { stroke: var(--accent-2); stroke-width: 1; fill: none; stroke-dasharray: 2 9; opacity: 0; }
716
+ .law-on .rail-line { stroke: var(--brass-hi); stroke-width: 1.9; opacity: .9; }
717
+ .law-on .rail-dash { opacity: .95; animation: rail-flow 700ms linear infinite; }
718
+ @keyframes rail-flow { to { stroke-dashoffset: -11; } }
719
+ @media (prefers-reduced-motion: reduce) { .law-on .rail-dash { animation: none; } }
720
+
721
+ /* the tray is the department's stack of real services — one per line, so the
722
+ vendor marks read as a list of systems rather than a scatter of chips */
723
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray {
724
+ display: flex; flex-direction: column; align-items: stretch;
725
+ gap: 4px; margin: 0; padding: 0; max-width: 232px;
726
+ }
727
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac { justify-content: flex-start; width: 100%; }
728
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray > .oc-mac .mac-name { max-width: none; }
729
+
730
+ @media (max-width: 820px) {
731
+ .oc-row.lanes > .oc-child > .ocn {
732
+ grid-template-columns: 1fr;
733
+ grid-template-areas: "head" "macs" "staff";
734
+ }
735
+ /* no rail column on a single-column stack — the rail has nothing to span */
736
+ .oc-row.lanes > .oc-child > .ocn > .oc-down { display: none; }
737
+ .oc-row.lanes > .oc-child > .ocn > .oc-tray { max-width: none; flex-direction: row; flex-wrap: wrap; }
738
+ }
739
+ /* inside a lane the people always flow right and wrap — the ".wrap" grid
740
+ (which grows downward) would otherwise stack a big department into a
741
+ one-card-wide column. Specificity here deliberately beats .oc-row.wrap. */
742
+ .oc-row.lanes > .oc-child > .ocn > .oc-row,
743
+ .oc-row.lanes > .oc-child > .ocn > .oc-row.wrap {
744
+ display: flex;
745
+ flex-direction: row;
746
+ flex-wrap: wrap;
747
+ justify-content: flex-start;
748
+ gap: 8px;
749
+ margin: 0;
750
+ padding: 0;
751
+ max-width: none;
752
+ border: 0;
753
+ border-radius: 0;
754
+ background: none;
755
+ align-content: flex-start;
756
+ }
757
+ .oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child { padding: 0; flex: 0 0 auto; }
758
+ .oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::before,
759
+ .oc-row.lanes > .oc-child > .ocn > .oc-row > .oc-child::after { display: none; }
760
+
761
+ @media (max-width: 820px) {
762
+ .oc-row.lanes > .oc-child > .ocn { grid-template-columns: 1fr; gap: 12px; }
763
+ }
764
+ /* Connectors are drawn by the SVG rail plane now, not by pseudo-elements. The
765
+ old fan drop-lines/elbows here would double every rail. */
766
+ .oc-child { position: relative; padding: 0; display: flex; flex-direction: column; align-items: center; }
767
+ .oc-child::before, .oc-child::after { display: none; }
361
768
 
362
769
  /* wide sibling sets — grouped subtrees that wrap to natural width (never crush a
363
770
  branch into a fixed cell — that overflowed and collided with the next branch) */
@@ -370,78 +777,171 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
370
777
  .oc-row.wrap > .oc-child { padding: 8px 4px 0; flex: 0 0 auto; }
371
778
  .oc-row.wrap > .oc-child::before, .oc-row.wrap > .oc-child::after { display: none; }
372
779
 
373
- /* agent cards company badges with a rank system (size + saturation recede
374
- with depth) and a light-from-above material model. */
780
+ /* ── THE PLATE ────────────────────────────────────────────────────────────
781
+ An engraved plate on a key rack. Left edge is milled brass; rank reads
782
+ through FINISH and SIZE (polished → satin → dulled), never hue. Radius 4px
783
+ per CRAFT §0 (radii 2/3/6) — the old 15px rounded rect is what made this
784
+ read as a SaaS card. Content is left-aligned like a name plate, not
785
+ centred like a business card. */
786
+ /* ══ THE SOURCE CARD (variant B — "the spine") ══════════════════════════
787
+ This board is not a picture of the org: it IS the org file booboo_boot
788
+ reads. Rules inherit down it, bucket reach derives from it, every agent
789
+ boots against it. So the card states its CONSEQUENCE, not just its
790
+ attributes — what it declares, who it binds, what it boots on.
791
+ Health is the card's own left edge, so scanning nine stacked departments
792
+ you find the amber one peripherally, before reading a single word. */
375
793
  .ag {
376
- position: relative; display: flex; flex-direction: column; align-items: center; gap: 6px;
377
- width: calc(160px - min(var(--d, 0), 2) * 12px); /* 160 / 148 / 136 by rank */
378
- padding: 13px 11px 10px; text-align: center;
379
- background: linear-gradient(180deg, hsl(var(--h) 30% 14% / 0.55), var(--bg-1) 55%);
380
- border: 1px solid rgba(255, 255, 255, 0.06); border-radius: 15px; cursor: grab; overflow: hidden;
381
- filter: saturate(calc(1 - min(var(--d, 0), 2) * 0.15));
382
- box-shadow:
383
- inset 0 1px 0 rgba(255, 255, 255, 0.05),
384
- inset 0 0 0 1px hsl(var(--h) 40% 40% / 0.07),
385
- 0 1px 2px rgba(0, 0, 0, 0.5),
386
- 0 16px 32px -18px hsl(var(--h) 55% 40% / 0.28);
387
- transition: border-color 0.15s, transform 0.15s, box-shadow 0.2s, opacity 0.15s, filter 0.2s;
388
- animation: settle 0.4s ease both;
794
+ position: relative; display: flex; flex-direction: column; align-items: stretch;
795
+ width: calc(300px - min(var(--d, 0), 2) * 22px); /* 300 / 278 / 256 by rank */
796
+ padding: 15px 18px 13px 21px; text-align: left;
797
+ background: var(--bg-1);
798
+ border: 1px solid var(--line); border-radius: 14px; cursor: grab; overflow: hidden;
799
+ box-shadow: var(--sh-2);
800
+ transition: box-shadow .22s var(--ease), transform .22s var(--ease), border-color .18s ease, opacity .18s ease;
801
+ animation: settle .45s var(--settle) both;
802
+ }
803
+ /* the spine health as the card's own edge */
804
+ .ag::before {
805
+ content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
806
+ background: var(--ink-3); transition: background .2s ease;
389
807
  }
390
- .ag::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
391
- height: calc(4px - min(var(--d, 0), 2) * 1px); /* lanyard thins with rank */
392
- background: linear-gradient(90deg, hsl(var(--h) 55% 55% / 0.9), hsl(var(--h) 55% 45% / 0.25)); }
393
- @keyframes settle { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
394
- .ag:hover { border-color: hsl(var(--h) 45% 50% / 0.6); transform: translateY(-2px); filter: saturate(1);
395
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 2px 4px rgba(0, 0, 0, 0.5), 0 24px 44px -18px hsl(var(--h) 55% 45% / 0.4); }
396
- .ag.sel { border-color: hsl(var(--h) 55% 55%); filter: saturate(1);
397
- box-shadow: 0 0 0 2px hsl(var(--h) 55% 55% / 0.18), 0 10px 28px hsl(var(--h) 50% 45% / 0.2); }
808
+ .ag.h-ok::before { background: var(--ok); }
809
+ .ag.h-warn::before { background: var(--warn); }
810
+ .ag.h-fail::before { background: var(--fail); }
811
+ /* an unhealthy card also carries a wash, so the state survives a greyscale
812
+ print and never rests on hue alone */
813
+ .ag.h-warn { background: linear-gradient(180deg, var(--warn-wash), var(--bg-1) 52%); border-color: #ecd9ae; }
814
+ .ag.h-fail { background: linear-gradient(180deg, var(--fail-wash), var(--bg-1) 52%); border-color: #edc9bf; }
815
+ @keyframes settle { from { opacity: 0; transform: translateY(8px) scale(.985); } to { opacity: 1; transform: none; } }
816
+ .ag:hover { transform: translateY(-2px); box-shadow: var(--sh-3); border-color: var(--line-2); }
817
+ .ag:active { transform: translateY(0) scale(.995); transition-duration: .08s; }
818
+ .ag.sel { border-color: var(--accent); box-shadow: var(--sh-3), 0 0 0 3px var(--accent-dim); }
398
819
  .ag:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
399
- .ag.over { border-color: var(--accent-2); box-shadow: 0 0 0 3px var(--accent-dim), 0 12px 30px rgba(79, 184, 160, 0.2); transform: scale(1.03); }
400
- .ag.dragging { opacity: 0.45; cursor: grabbing; }
820
+ .ag.over { border-color: var(--accent-2); box-shadow: var(--sh-3), 0 0 0 4px var(--accent-dim); transform: scale(1.02); }
821
+ .ag.dragging { opacity: 0.4; cursor: grabbing; }
822
+
823
+ /* WHERE IT BELONGS — reads first, so a card lifted out of its lane still
824
+ knows its own place. */
825
+ .ag-eyebrow {
826
+ font-family: var(--mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
827
+ color: var(--accent); display: block; margin-bottom: 7px;
828
+ }
829
+ /* WHAT FLOWS OUT — the consequence line. Plain English, never glyphs. */
830
+ .ag-flows {
831
+ display: block; font-size: 11.5px; color: var(--ink-3); margin-top: 10px;
832
+ font-variant-numeric: tabular-nums;
833
+ }
401
834
 
402
- .ag-light { position: absolute; top: 10px; right: 10px; width: 7px; height: 7px; border-radius: 50%; }
403
- .ag-light.ok { background: var(--accent); box-shadow: 0 0 6px rgba(79, 184, 160, 0.7); }
404
- .ag-light.warn { background: var(--warn); box-shadow: 0 0 6px rgba(224, 180, 95, 0.7); }
405
- .ag-light.fail { background: #e07b6a; box-shadow: 0 0 8px rgba(224, 123, 106, 0.9); animation: pulse 1.4s ease-in-out infinite; }
835
+ /* ── THE HEALTH LIGHT ──────────────────────────────────────────────────────
836
+ A real lamp: a lit core inside a soft halo, with a slow breath so a healthy
837
+ board is visibly ALIVE rather than merely static. The halo is a separate
838
+ ::after ring that expands and fades one pulse every 2.6s, offset per
839
+ bubble so nine departments never blink in unison (a synchronised board
840
+ reads as a screensaver; a staggered one reads as a building). */
841
+ .ag-light {
842
+ position: absolute; top: 13px; right: 14px; width: 8px; height: 8px; border-radius: 50%;
843
+ background: var(--ink-3);
844
+ }
845
+ .ag-light::after {
846
+ content: ""; position: absolute; inset: -3px; border-radius: 50%;
847
+ border: 1px solid currentColor; opacity: 0;
848
+ animation: lamp 2.6s var(--ease) infinite;
849
+ animation-delay: var(--lamp-delay, 0s);
850
+ }
851
+ .ag-light.ok { background: var(--ok); color: var(--ok); box-shadow: 0 0 0 3px var(--ok-wash); }
852
+ .ag-light.warn { background: var(--warn); color: var(--warn); box-shadow: 0 0 0 3px var(--warn-wash); }
853
+ .ag-light.fail { background: var(--fail); color: var(--fail); box-shadow: 0 0 0 3px var(--fail-wash); }
854
+ /* a failing lamp beats faster — urgency in the rhythm, not just the hue */
855
+ .ag-light.fail::after { animation-duration: 1.2s; }
856
+ @keyframes lamp {
857
+ 0% { transform: scale(.7); opacity: .85; }
858
+ 70%, 100% { transform: scale(1.9); opacity: 0; }
859
+ }
860
+ @media (prefers-reduced-motion: reduce) { .ag-light::after { animation: none; } }
406
861
 
407
- .ag.root { cursor: default; width: 200px; padding: 16px 13px 11px;
408
- border-color: rgba(79, 184, 160, 0.35); filter: none;
409
- box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 0 0 1px rgba(79, 184, 160, 0.25), 0 24px 60px -20px rgba(79, 184, 160, 0.35); }
410
- .ag.root .ag-ava { width: 46px; height: 46px; font-size: 23px; }
411
- .ag.root .ag-name { font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
412
- .ag.root::after {
413
- content: ""; position: absolute; inset: 0; border-radius: 15px; pointer-events: none;
414
- box-shadow: inset 0 0 26px rgba(79, 184, 160, 0.08); animation: ring 3.2s ease-in-out infinite;
415
- }
416
- @keyframes ring { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
862
+ /* rank I·II — the GM. Polished brass: the only plate that is lit rather than lit-upon. */
863
+ .ag.root { cursor: default; width: 268px; padding: 15px 17px;
864
+ background: var(--surface-lift);
865
+ border-color: var(--surface-lift-edge); box-shadow: var(--sh-3); }
866
+ .ag.root .ag-name { font-size: 14.5px; }
417
867
 
868
+ /* plate header: the mark sits BESIDE the name, not above it — a name plate,
869
+ not a business card. Staff plates carry no mark at all (see Panel.tsx): a
870
+ brass plaque does not have a cartoon on it, and 51 identical 🤖 was the
871
+ cheapest token on the board. */
872
+ .ag-head { display: flex; align-items: center; gap: 9px; min-width: 0; }
418
873
  .ag-ava {
419
- width: 38px; height: 38px; display: grid; place-items: center; font-size: 19px; margin-top: 2px;
420
- border-radius: 13px; background: linear-gradient(140deg, hsl(var(--h) 42% 26% / 0.85), hsl(var(--h) 45% 14% / 0.9));
421
- border: 1px solid hsl(var(--h) 45% 45% / 0.35);
422
- box-shadow: inset 0 1px 0 hsl(var(--h) 50% 60% / 0.25), 0 4px 10px -4px hsl(var(--h) 50% 30% / 0.6);
423
- }
424
- .ag-name { font-size: 13px; font-weight: 650; letter-spacing: -0.01em; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
425
- .ag-role { font-size: 11px; color: var(--ink-2); line-height: 1.35;
426
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
427
- .ag-meta { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 2px; }
428
- .ag-meta em { font-style: normal; font-size: 10.5px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
874
+ width: 26px; height: 26px; flex: 0 0 26px; display: grid; place-items: center; font-size: 14px;
875
+ border-radius: 9px; background: var(--accent-dim); border: 1px solid #eadcc3;
876
+ }
877
+ .ag.root .ag-ava { width: 30px; height: 30px; flex-basis: 30px; font-size: 16px; border-color: rgba(239, 195, 137, 0.45); }
878
+
879
+ /* the name engraved: a dark groove under, a hairline of light above */
880
+ .ag-name {
881
+ font-family: var(--display); font-size: 18px; font-weight: 600; letter-spacing: -0.015em;
882
+ line-height: 1.18; color: var(--ink);
883
+ white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis;
884
+ }
885
+ /* staff plates: quieter, lighter weight — rank in the type as well as the metal */
886
+ .ag.staff .ag-name { font-family: var(--font); font-size: 15px; font-weight: 600; letter-spacing: -0.01em; color: var(--ink); }
887
+
888
+ .ag-role {
889
+ font-size: 12.5px; line-height: 1.5; color: var(--ink-2); margin-top: 6px;
890
+ display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
891
+ }
892
+ .ag-meta { display: flex; align-items: center; gap: 9px; margin-top: 7px; }
893
+ .ag-meta em { font-style: normal; font-family: var(--mono); font-size: 9px; color: var(--ink-3); font-variant-numeric: tabular-nums; white-space: nowrap; }
429
894
  .ag-kids {
430
- font-size: 10px; background: var(--accent-dim); color: var(--accent); border-radius: 99px;
431
- padding: 2px 8px; font-variant-numeric: tabular-nums; font-weight: 650; text-transform: uppercase; letter-spacing: 0.06em;
895
+ font-family: var(--mono); font-size: 8.5px; color: var(--brass); border: 1px solid rgba(217, 160, 91, 0.28);
896
+ border-radius: 3px; padding: 2px 7px; font-variant-numeric: tabular-nums; text-transform: uppercase; letter-spacing: 0.1em;
897
+ }
898
+
899
+ /* ── the plate face: health · bucket reach · rule count · last report ──────
900
+ Replaces a role line that used to just repeat the name ("Lift Engineer" /
901
+ "Lift Engineer — Engineering"). Four facts, left-aligned, mono, no chrome. */
902
+ .ag-facts {
903
+ display: flex; align-items: center; gap: 10px;
904
+ margin-top: 12px; padding-top: 11px; border-top: 1px solid var(--line);
432
905
  }
906
+ .ag-fact { font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--ink-3); }
907
+ .ag-fact-health.ok { color: var(--ok); }
908
+ .ag-fact-health.warn { color: var(--warn); }
909
+ .ag-fact-health.fail { color: var(--fail); }
910
+ .ag-fact-health.none { color: var(--ink-3); font-weight: 500; }
911
+ .fact-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--ink-3); flex: 0 0 auto; }
912
+ .ag-fact-health.ok .fact-dot { background: var(--ok); box-shadow: 0 0 6px rgba(139, 233, 176, 0.75); }
913
+ .ag-fact-health.warn .fact-dot { background: var(--brass); box-shadow: 0 0 6px rgba(217, 160, 91, 0.85); }
914
+ .ag-fact-health.fail .fact-dot { background: var(--fail); box-shadow: 0 0 7px rgba(224, 123, 106, 0.85); }
915
+ .ag-fact-health.ok, .ag-fact-health.warn, .ag-fact-health.fail { color: var(--ink-2); }
916
+ .ag-fact-report { margin-left: auto; color: var(--ink-3); font-weight: 400; font-family: var(--mono); font-size: 10px; }
917
+ /* the expected beat, quieter than the fact it qualifies — it is the ruler, not
918
+ the measurement, so it must never out-shout "reported 4d ago". */
919
+ .ag-cadence { font-weight: 400; color: var(--ink-3); opacity: .62; }
920
+
921
+ /* "show the law": the boot-order chain, printed on the plate only while the
922
+ toggle is on — the product's core idea, made literal per plate. */
923
+ .ag-law { display: none; }
924
+ .law-on .ag-law {
925
+ display: block; margin-top: 7px; padding-top: 7px; border-top: 1px dashed rgba(217, 160, 91, 0.32);
926
+ font-family: var(--mono); font-size: 8.5px; letter-spacing: 0.03em; color: var(--brass-hi);
927
+ line-height: 1.6; word-break: break-word;
928
+ }
929
+ .ag-law-arrow { font-style: normal; color: var(--brass); margin: 0 3px; opacity: 0.85; }
930
+ .law-on.ag { box-shadow: 0 0 0 1px rgba(217, 160, 91, 0.3), 0 16px 34px -18px rgba(0, 0, 0, 0.85); }
433
931
 
434
932
  /* the dossier */
435
933
  .doss {
436
934
  width: 340px; flex: 0 0 340px; border-left: 1px solid var(--line);
437
- background: rgba(16, 19, 26, 0.75); backdrop-filter: blur(10px);
935
+ background: var(--bg-1); backdrop-filter: blur(10px);
438
936
  padding: 26px 24px 40px; overflow-y: auto; animation: slidein 0.25s ease both;
439
937
  }
440
938
  @keyframes slidein { from { opacity: 0; transform: translateX(14px); } to { opacity: 1; transform: none; } }
441
- .doss-head { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
442
- .doss-emoji { font-size: 30px; }
443
- .doss h2 { margin: 0; font-size: 18px; }
444
- .doss-role { margin: 3px 0 0; color: var(--ink-2); font-size: 12.5px; }
939
+ .doss-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; }
940
+ .doss-emoji { font-size: 30px; flex: none; }
941
+ .doss h2 { margin: 0; font-size: 18px; min-width: 0; }
942
+ /* full panel width: a body of four seats is a list, not a subtitle squeezed
943
+ beside three buttons. Middots become the wrap points. */
944
+ .doss-role { margin: 0 0 14px; color: var(--ink-2); font-size: 12.5px; line-height: 1.55; }
445
945
 
446
946
  .doss-chain { font-size: 11px; color: var(--ink-3); margin-bottom: 18px; line-height: 1.7; }
447
947
  .doss-chain i { font-style: normal; margin: 0 5px; }
@@ -462,8 +962,12 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
462
962
  .doss-rules em { color: var(--ink-3); font-style: normal; font-size: 9.5px; margin-left: 5px; }
463
963
  .doss-chips { display: flex; gap: 6px; flex-wrap: wrap; }
464
964
  .doss-reps { display: flex; flex-direction: column; gap: 7px; }
965
+ /* No side-stripe. It was a 2px brass border-left that said nothing the
966
+ status dot doesn't already say properly, and a coloured side-stripe on a
967
+ card is a banned pattern in the house design laws. Full 1px border; the
968
+ whole edge answers on hover. */
465
969
  .doss-rep {
466
- background: var(--bg-1); border: 1px solid var(--line); border-left: 2px solid var(--accent);
970
+ background: var(--bg-1); border: 1px solid var(--line);
467
971
  border-radius: 9px; padding: 9px 11px; animation: settle 0.35s ease both;
468
972
  }
469
973
  .doss-rep-top { display: flex; align-items: baseline; gap: 9px; }
@@ -471,7 +975,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
471
975
  .doss-rep-label { font-size: 12px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
472
976
  .doss-rep-sum { margin: 5px 0 0; font-size: 11.5px; color: var(--ink-2); line-height: 1.55; }
473
977
  .doss-rep.tap { cursor: pointer; transition: border-color .12s ease, background .12s ease; }
474
- .doss-rep.tap:hover { border-left-color: var(--warn); background: rgba(255, 255, 255, 0.03); }
978
+ .doss-rep.tap:hover { border-color: var(--accent); background: var(--accent-dim); }
475
979
  .doss-rep.tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
476
980
  .doss-rep-caret { margin-left: auto; font-size: 10px; color: var(--ink-2); align-self: center; }
477
981
  .rep-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; align-self: center; }
@@ -479,7 +983,7 @@ body { font-family: var(--font); color: var(--ink); background: var(--bg); overf
479
983
  .rep-ok { color: var(--accent); } .rep-warn { color: var(--warn); } .rep-fail { color: #e07b6a; }
480
984
  .doss-rep-detail { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--line); font-size: 11px; line-height: 1.7; color: var(--ink-2); font-family: var(--mono); word-break: break-word; }
481
985
  .doss-rep-detail .doss-l { display: inline-block; min-width: 74px; opacity: .6; text-transform: uppercase; letter-spacing: .04em; font-size: 9.5px; }
482
- .chip { font-size: 11px; background: var(--accent-dim); color: var(--accent-2); border-radius: 7px; padding: 4px 10px; }
986
+ .chip { transition: transform .16s var(--ease), background .18s ease; font-size: 11px; background: var(--accent-dim); color: var(--accent-2); border-radius: 7px; padding: 4px 10px; }
483
987
  .chip.alt { background: var(--bg-2); color: var(--ink-2); }
484
988
  .doss-boot { font-size: 12px; color: var(--ink-2); line-height: 1.6; margin: 0; white-space: pre-wrap; }
485
989
  /* contract — one click to open, edit in place, save flows to the org file */
@@ -554,17 +1058,6 @@ var TABS = [
554
1058
  { id: "rules", glyph: "\xA7", label: "rules" },
555
1059
  { id: "graph", glyph: "\u25C9", label: "graph" }
556
1060
  ];
557
- function relTime(iso) {
558
- if (typeof iso !== "string") return "";
559
- const t = new Date(iso).getTime();
560
- if (!t) return "";
561
- const s = (Date.now() - t) / 1e3;
562
- if (s < 60) return "just now";
563
- if (s < 3600) return `${Math.floor(s / 60)}m ago`;
564
- if (s < 86400) return `${Math.floor(s / 3600)}h ago`;
565
- if (s < 172800) return "yesterday";
566
- return `${Math.floor(s / 86400)}d ago`;
567
- }
568
1061
  function nodeAt(n) {
569
1062
  const d = n.data ?? {};
570
1063
  for (const k of ["at", "ts", "time", "created_at", "date", "bst", "ts_bst", "finished_at"]) {
@@ -581,13 +1074,14 @@ function timeMs(at) {
581
1074
  function buildHealthMap(nodes) {
582
1075
  const m = /* @__PURE__ */ new Map();
583
1076
  for (const r of nodes) {
584
- if (!r.cluster) continue;
1077
+ const who = reportAgentId(r);
1078
+ if (!who) continue;
585
1079
  const d = r.data ?? {};
586
1080
  if (typeof d.status !== "string") continue;
587
1081
  const status = d.status;
588
1082
  const at = nodeAt(r);
589
1083
  const atMs = timeMs(at);
590
- const cur = m.get(r.cluster) ?? { lastAt: "", lastMs: -Infinity, lastStatus: "", n: 0, fails: 0 };
1084
+ const cur = m.get(who) ?? { lastAt: "", lastMs: -Infinity, lastStatus: "", n: 0, fails: 0 };
591
1085
  cur.n++;
592
1086
  if (status === "fail") cur.fails++;
593
1087
  if (Number.isFinite(atMs) && atMs >= cur.lastMs) {
@@ -595,7 +1089,7 @@ function buildHealthMap(nodes) {
595
1089
  cur.lastAt = at;
596
1090
  cur.lastStatus = status;
597
1091
  }
598
- m.set(r.cluster, cur);
1092
+ m.set(who, cur);
599
1093
  }
600
1094
  return m;
601
1095
  }
@@ -609,6 +1103,15 @@ function pulseFor(a, health) {
609
1103
  }
610
1104
  return best;
611
1105
  }
1106
+ function everyN(h) {
1107
+ if (h <= 1) return "hourly";
1108
+ if (h < 24) return `every ${h}h`;
1109
+ if (h === 24) return "daily";
1110
+ if (h === 168) return "weekly";
1111
+ if (h >= 672 && h <= 744) return "monthly";
1112
+ if (h % 24 === 0) return `every ${h / 24} days`;
1113
+ return `every ${h}h`;
1114
+ }
612
1115
  function lightFor(a, health) {
613
1116
  const p = pulseFor(a, health);
614
1117
  if (!p || !p.lastAt) return "none";
@@ -631,6 +1134,10 @@ function nodeSummary(n) {
631
1134
  }
632
1135
  return "";
633
1136
  }
1137
+ function reportAgentId(n) {
1138
+ const d = n.data ?? {};
1139
+ return n.cluster || (typeof d.agent === "string" ? d.agent : "") || "";
1140
+ }
634
1141
  async function fetchReports(api, cluster, limit = 500) {
635
1142
  const q = async (t) => {
636
1143
  try {
@@ -656,23 +1163,31 @@ function slugify(s) {
656
1163
  return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
657
1164
  }
658
1165
  var REDUCED = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches;
1166
+ var THEME_KEY = "booboo-theme-v2";
659
1167
  function readTheme() {
660
1168
  try {
661
- return localStorage.getItem("booboo-theme") === "light" ? "light" : "dark";
1169
+ if (new URLSearchParams(window.location.search).get("embed")) return "light";
1170
+ return localStorage.getItem(THEME_KEY) === "dark" ? "dark" : "light";
662
1171
  } catch {
663
- return "dark";
1172
+ return "light";
664
1173
  }
665
1174
  }
666
- function useTheme() {
1175
+ function useTheme(pinned) {
667
1176
  const [theme, setTheme] = useState(readTheme);
1177
+ const chosen = useRef(false);
668
1178
  useEffect(() => {
669
- document.documentElement.setAttribute("data-theme", theme);
1179
+ if (pinned) return;
1180
+ if (!chosen.current) return;
670
1181
  try {
671
- localStorage.setItem("booboo-theme", theme);
1182
+ localStorage.setItem(THEME_KEY, theme);
672
1183
  } catch {
673
1184
  }
674
- }, [theme]);
675
- return [theme, () => setTheme((t) => t === "dark" ? "light" : "dark")];
1185
+ }, [theme, pinned]);
1186
+ if (pinned) return [pinned, null];
1187
+ return [theme, () => {
1188
+ chosen.current = true;
1189
+ setTheme((t) => t === "dark" ? "light" : "dark");
1190
+ }];
676
1191
  }
677
1192
  function useCountUp(target, ms = 900) {
678
1193
  const [v, setV] = useState(REDUCED ? target : 0);
@@ -720,8 +1235,216 @@ function bucketHue(name) {
720
1235
  for (const c of name) h = (h * 31 + c.charCodeAt(0)) % 360;
721
1236
  return h;
722
1237
  }
1238
+ var SEALED_BUCKET = "guest-registry";
1239
+ function LedgerShelf({ org, lit }) {
1240
+ const buckets = useMemo(() => {
1241
+ const s = /* @__PURE__ */ new Set();
1242
+ for (const a of org.agents) for (const b of a.buckets ?? []) s.add(b);
1243
+ return [...s].sort();
1244
+ }, [org]);
1245
+ return /* @__PURE__ */ jsxs("div", { className: "ledger-shelf", role: "list", "aria-label": "the ledger \u2014 hover a role to see what it reaches", children: [
1246
+ /* @__PURE__ */ jsx("span", { className: "shelf-label", children: "the ledger" }),
1247
+ /* @__PURE__ */ jsxs("div", { className: "shelf-slots", children: [
1248
+ buckets.map((b) => /* @__PURE__ */ jsx("span", { className: `shelf-slot${lit.has(b) ? " lit" : ""}`, role: "listitem", children: b }, b)),
1249
+ /* @__PURE__ */ jsxs("span", { className: "shelf-slot sealed", role: "listitem", title: "ledger:guest-registry \u2014 sealed by wall. Visible, never emitted.", children: [
1250
+ "\u{1F512} ",
1251
+ SEALED_BUCKET
1252
+ ] })
1253
+ ] })
1254
+ ] });
1255
+ }
1256
+ function Constellation() {
1257
+ const svg = useMemo(() => {
1258
+ const W = 1600, H = 1e3, N = 58;
1259
+ let s = 20260719;
1260
+ const rnd = () => (s = s * 1103515245 + 12345 & 2147483647) / 2147483647;
1261
+ const pts = [];
1262
+ for (let i = 0; i < N; i++) pts.push([rnd() * W, rnd() * H]);
1263
+ let d = "";
1264
+ for (let i = 0; i < N; i++) {
1265
+ for (let j = i + 1; j < N; j++) {
1266
+ const dist = Math.hypot(pts[i][0] - pts[j][0], pts[i][1] - pts[j][1]);
1267
+ if (dist < 200) d += `<line x1="${pts[i][0].toFixed(1)}" y1="${pts[i][1].toFixed(1)}" x2="${pts[j][0].toFixed(1)}" y2="${pts[j][1].toFixed(1)}" stroke="rgba(217,160,91,${((1 - dist / 200) * 0.14).toFixed(3)})" stroke-width="1"/>`;
1268
+ }
1269
+ }
1270
+ for (const [x, y] of pts) d += `<circle cx="${x.toFixed(1)}" cy="${y.toFixed(1)}" r="${(rnd() * 1.4 + 0.6).toFixed(2)}" fill="rgba(239,195,137,.45)"/>`;
1271
+ return d;
1272
+ }, []);
1273
+ return /* @__PURE__ */ jsx("svg", { className: "pnl-stars", viewBox: "0 0 1600 1000", preserveAspectRatio: "xMidYMid slice", "aria-hidden": true, dangerouslySetInnerHTML: { __html: svg } });
1274
+ }
1275
+ function LawPlate({ org, selected, onSelect }) {
1276
+ const root = org.agents.find((a) => a.id === org.root);
1277
+ const rules = root?.rules ?? [];
1278
+ if (!rules.length) return null;
1279
+ return /* @__PURE__ */ jsxs(
1280
+ "div",
1281
+ {
1282
+ className: `ag law-plate${selected ? " sel" : ""}`,
1283
+ "data-rail": "law",
1284
+ tabIndex: 0,
1285
+ title: "the law every agent in the house boots against",
1286
+ onClick: (e) => {
1287
+ e.stopPropagation();
1288
+ if (root) onSelect(root.id);
1289
+ },
1290
+ onKeyDown: (e) => {
1291
+ if ((e.key === "Enter" || e.key === " ") && root) {
1292
+ e.preventDefault();
1293
+ onSelect(root.id);
1294
+ }
1295
+ },
1296
+ children: [
1297
+ /* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
1298
+ /* @__PURE__ */ jsx("span", { className: "ag-ava", children: "\xA7" }),
1299
+ /* @__PURE__ */ jsx("span", { className: "ag-name", children: "The House Standard" })
1300
+ ] }),
1301
+ /* @__PURE__ */ jsx("span", { className: "ag-role", children: rules[0] }),
1302
+ /* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
1303
+ /* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
1304
+ "binds ",
1305
+ org.agents.length
1306
+ ] }),
1307
+ /* @__PURE__ */ jsxs("em", { className: "ag-fact", children: [
1308
+ rules.length,
1309
+ " clause",
1310
+ rules.length === 1 ? "" : "s"
1311
+ ] })
1312
+ ] })
1313
+ ]
1314
+ }
1315
+ );
1316
+ }
1317
+ function boxIn(el, root) {
1318
+ let x = 0, y = 0;
1319
+ let cur = el;
1320
+ while (cur && cur !== root) {
1321
+ x += cur.offsetLeft;
1322
+ y += cur.offsetTop;
1323
+ cur = cur.offsetParent;
1324
+ }
1325
+ return { x, y, w: el.offsetWidth, h: el.offsetHeight, cy: y + el.offsetHeight / 2 };
1326
+ }
1327
+ function elbow(x1, y1, x2, y2) {
1328
+ if (Math.abs(y2 - y1) < 1.5) return `M${x1},${y1.toFixed(1)} H${x2}`;
1329
+ const mx = x1 + (x2 - x1) * 0.45;
1330
+ const r = Math.min(9, Math.abs(y2 - y1) / 2);
1331
+ const dir = y2 > y1 ? 1 : -1;
1332
+ return `M${x1},${y1.toFixed(1)} H${(mx - r).toFixed(1)} Q${mx},${y1.toFixed(1)} ${mx},${(y1 + r * dir).toFixed(1)} V${(y2 - r * dir).toFixed(1)} Q${mx},${y2.toFixed(1)} ${(mx + r).toFixed(1)},${y2.toFixed(1)} H${x2}`;
1333
+ }
1334
+ function CascadeRails({ chartRef, version }) {
1335
+ const [paths, setPaths] = useState([]);
1336
+ useLayoutEffect(() => {
1337
+ const root = chartRef.current;
1338
+ if (!root) return;
1339
+ const measure = () => {
1340
+ const pick = (sel) => Array.from(root.querySelectorAll(sel));
1341
+ const law = root.querySelector('[data-rail="law"]');
1342
+ const gm = root.querySelector('[data-rail="gm"]');
1343
+ const depts = pick('[data-rail="dept"]');
1344
+ const staff = pick('[data-rail="staff"]');
1345
+ const out = [];
1346
+ const gmB = gm ? boxIn(gm, root) : null;
1347
+ if (law && gmB) {
1348
+ const lawB = boxIn(law, root);
1349
+ out.push(elbow(lawB.x + lawB.w, lawB.cy, gmB.x, gmB.cy));
1350
+ }
1351
+ const deptBox = /* @__PURE__ */ new Map();
1352
+ for (const d of depts) {
1353
+ const b = boxIn(d, root);
1354
+ deptBox.set(d.dataset.id ?? "", b);
1355
+ if (gmB) out.push(elbow(gmB.x + gmB.w, gmB.cy, b.x, b.cy));
1356
+ }
1357
+ for (const s of staff) {
1358
+ const parent = deptBox.get(s.dataset.parent ?? "");
1359
+ if (!parent) continue;
1360
+ const b = boxIn(s, root);
1361
+ out.push(elbow(parent.x + parent.w, parent.cy, b.x, b.cy));
1362
+ }
1363
+ setPaths((prev) => prev.length === out.length && prev.every((p, i) => p === out[i]) ? prev : out);
1364
+ };
1365
+ measure();
1366
+ const ro = new ResizeObserver(measure);
1367
+ ro.observe(root);
1368
+ return () => ro.disconnect();
1369
+ }, [chartRef, version]);
1370
+ return /* @__PURE__ */ jsxs("svg", { className: "rails", "aria-hidden": true, children: [
1371
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsxs("linearGradient", { id: "railGrad", x1: "0", y1: "0", x2: "1", y2: "0", children: [
1372
+ /* @__PURE__ */ jsx("stop", { offset: "0", stopColor: "var(--brass)", stopOpacity: "0.3" }),
1373
+ /* @__PURE__ */ jsx("stop", { offset: "1", stopColor: "var(--brass-hi)", stopOpacity: "0.85" })
1374
+ ] }) }),
1375
+ paths.map((d, i) => /* @__PURE__ */ jsxs("g", { children: [
1376
+ /* @__PURE__ */ jsx("path", { className: "rail-line", d }),
1377
+ /* @__PURE__ */ jsx("path", { className: "rail-dash", d })
1378
+ ] }, i))
1379
+ ] });
1380
+ }
1381
+ function Lane({ laneId, isLane, dragId, onDropOn, children }) {
1382
+ const [over, setOver] = useState(false);
1383
+ if (!isLane) return /* @__PURE__ */ jsx("div", { className: "oc-child", children });
1384
+ return /* @__PURE__ */ jsx(
1385
+ "div",
1386
+ {
1387
+ className: `oc-child${over && dragId && dragId !== laneId ? " lane-over" : ""}`,
1388
+ onDragOver: (e) => {
1389
+ if (dragId && dragId !== laneId) {
1390
+ e.preventDefault();
1391
+ setOver(true);
1392
+ }
1393
+ },
1394
+ onDragLeave: (e) => {
1395
+ if (!e.currentTarget.contains(e.relatedTarget)) setOver(false);
1396
+ },
1397
+ onDrop: (e) => {
1398
+ e.preventDefault();
1399
+ setOver(false);
1400
+ if (dragId && dragId !== laneId) onDropOn(laneId);
1401
+ },
1402
+ children
1403
+ }
1404
+ );
1405
+ }
1406
+ var HEALTH_WORD = { ok: "Healthy", warn: "Needs a look", fail: "Failing", none: "No reports yet" };
1407
+ function consequenceOf(a, org, slice) {
1408
+ if (!slice) return "";
1409
+ const descendants = (id) => {
1410
+ const kids = org.agents.filter((x) => x.parent === id);
1411
+ return kids.reduce((n, k) => n + 1 + descendants(k.id), 0);
1412
+ };
1413
+ const declares = a.rules?.length ?? 0;
1414
+ const below = descendants(a.id);
1415
+ if (below === 0) return "";
1416
+ const parts = [];
1417
+ if (declares) parts.push(`declares ${declares} rule${declares === 1 ? "" : "s"}`);
1418
+ parts.push(`binds ${below} below`);
1419
+ return parts.join(" \xB7 ");
1420
+ }
1421
+ function AgentFacts({ a, org, health, showLaw }) {
1422
+ const slice = useMemo(() => orgBootSlice(org, a.id), [org, a.id]);
1423
+ const consequence = useMemo(() => consequenceOf(a, org, slice), [a, org, slice]);
1424
+ const pulse = pulseFor(a, health);
1425
+ const light = lightFor(a, health);
1426
+ if (!slice) return null;
1427
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1428
+ consequence && /* @__PURE__ */ jsx("span", { className: "ag-flows", title: "what flows out of this node \u2014 the org file is the source booboo_boot reads", children: consequence }),
1429
+ /* @__PURE__ */ jsxs("span", { className: "ag-facts", children: [
1430
+ /* @__PURE__ */ jsx("em", { className: `ag-fact ag-fact-health ${light}`, children: HEALTH_WORD[light] }),
1431
+ /* @__PURE__ */ jsxs("em", { className: "ag-fact ag-fact-report", title: typeof a.cadence === "number" ? `${pulse?.lastAt ? `last report filed ${relTime(pulse.lastAt)}` : "no report filed yet"} \xB7 expected ${everyN(a.cadence)}; amber past about twice that` : pulse?.lastAt ? `last report filed ${relTime(pulse.lastAt)}` : "no report filed yet", children: [
1432
+ pulse?.lastAt ? `reported ${relTime(pulse.lastAt)}` : "",
1433
+ typeof a.cadence === "number" && a.kind !== "automation" && /* @__PURE__ */ jsxs("b", { className: "ag-cadence", children: [
1434
+ " \xB7 ",
1435
+ everyN(a.cadence)
1436
+ ] })
1437
+ ] })
1438
+ ] }),
1439
+ showLaw && /* @__PURE__ */ jsx("span", { className: "ag-law", title: "inheritance in boot order", children: slice.chain.map((c, i) => /* @__PURE__ */ jsxs("span", { children: [
1440
+ i > 0 && /* @__PURE__ */ jsx("i", { className: "ag-law-arrow", children: "\u2193" }),
1441
+ c.name
1442
+ ] }, c.id)) })
1443
+ ] });
1444
+ }
723
1445
  function AgentCard({
724
1446
  a,
1447
+ org,
725
1448
  isRoot,
726
1449
  depth,
727
1450
  order,
@@ -731,16 +1454,19 @@ function AgentCard({
731
1454
  onDragStart,
732
1455
  onDropOn,
733
1456
  childCount,
734
- light = "none"
1457
+ light = "none",
1458
+ health = null,
1459
+ onHover,
1460
+ showLaw = false
735
1461
  }) {
736
1462
  const [over, setOver] = useState(false);
737
- const nBuckets = a.buckets?.length ?? 0;
738
1463
  const nSkills = a.skills?.length ?? 0;
1464
+ const parentName = a.parent ? org.agents.find((x) => x.id === a.parent)?.name : null;
739
1465
  return /* @__PURE__ */ jsxs(
740
1466
  "div",
741
1467
  {
742
- className: `ag${isRoot ? " root" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}`,
743
- style: { ["--h"]: bucketHue(a.id), ["--d"]: depth, animationDelay: `${Math.min(depth * 70 + order * 45, 600)}ms` },
1468
+ className: `ag${isRoot ? " root" : ""}${depth >= 2 ? " staff" : ""}${selected ? " sel" : ""}${over ? " over" : ""}${dragId === a.id ? " dragging" : ""}${showLaw ? " law-on" : ""}${light !== "none" ? " h-" + light : ""}`,
1469
+ style: { ["--d"]: depth, animationDelay: `${Math.min(depth * 70 + order * 45, 600)}ms` },
744
1470
  draggable: !isRoot,
745
1471
  tabIndex: 0,
746
1472
  onKeyDown: (e) => {
@@ -769,16 +1495,28 @@ function AgentCard({
769
1495
  setOver(false);
770
1496
  onDropOn(a.id);
771
1497
  },
1498
+ onMouseEnter: () => onHover?.(a.id),
1499
+ onMouseLeave: () => onHover?.(null),
1500
+ "data-rail": isRoot ? "gm" : depth === 1 ? "dept" : "staff",
1501
+ "data-id": a.id,
1502
+ "data-parent": a.parent ?? "",
772
1503
  children: [
773
- light !== "none" && /* @__PURE__ */ jsx("i", { className: `ag-light ${light}`, title: `fleet health: ${light}` }),
774
- /* @__PURE__ */ jsx("span", { className: "ag-ava", children: a.emoji || "\u{1F916}" }),
775
- /* @__PURE__ */ jsx("span", { className: "ag-name", children: a.name }),
776
- /* @__PURE__ */ jsx("span", { className: "ag-role", children: a.role || " " }),
777
- /* @__PURE__ */ jsxs("span", { className: "ag-meta", children: [
778
- nBuckets > 0 && /* @__PURE__ */ jsxs("em", { title: "memory buckets", children: [
779
- "\u25A4 ",
780
- nBuckets
781
- ] }),
1504
+ light !== "none" && /* @__PURE__ */ jsx(
1505
+ "i",
1506
+ {
1507
+ className: `ag-light ${light}`,
1508
+ style: { ["--lamp-delay"]: `${bucketHue(a.id) % 26 / 10}s` },
1509
+ title: `health: ${light === "ok" ? "healthy" : light === "warn" ? "needs a look" : "failing"}`
1510
+ }
1511
+ ),
1512
+ /* @__PURE__ */ jsx("span", { className: "ag-eyebrow", children: depth === 0 ? "The house \xB7 rank II" : depth === 1 ? "Department \xB7 rank III" : parentName ?? "Staff" }),
1513
+ /* @__PURE__ */ jsxs("div", { className: "ag-head", children: [
1514
+ depth <= 1 && a.emoji && /* @__PURE__ */ jsx("span", { className: "ag-ava", children: a.emoji }),
1515
+ /* @__PURE__ */ jsx("span", { className: "ag-name", children: a.name })
1516
+ ] }),
1517
+ a.role && /* @__PURE__ */ jsx("span", { className: "ag-role", children: a.role }),
1518
+ /* @__PURE__ */ jsx(AgentFacts, { a, org, health, showLaw }),
1519
+ (nSkills > 0 || childCount > 0) && /* @__PURE__ */ jsxs("span", { className: "ag-meta", children: [
782
1520
  nSkills > 0 && /* @__PURE__ */ jsxs("em", { title: "skills", children: [
783
1521
  "\u2726 ",
784
1522
  nSkills
@@ -799,7 +1537,8 @@ function ChartNode({
799
1537
  order,
800
1538
  ...cardProps
801
1539
  }) {
802
- const kids = org.agents.filter((c) => c.parent === a.id && c.kind !== "automation");
1540
+ const kidsRaw = org.agents.filter((c) => c.parent === a.id && c.kind !== "automation");
1541
+ const kids = depth === 0 ? [...kidsRaw].sort((x, y) => x.id.localeCompare(y.id)) : kidsRaw;
803
1542
  const machines = (() => {
804
1543
  const out = [];
805
1544
  const walk = (pid) => {
@@ -814,17 +1553,20 @@ function ChartNode({
814
1553
  })();
815
1554
  const lights = machines.map((m) => lightFor(m, cardProps.health));
816
1555
  const trayLight = worst(lights);
1556
+ const isDept = depth === 1 && kids.length > 0;
1557
+ const folded = isDept && cardProps.collapsed?.has(a.id);
817
1558
  const TRAY_MAX = 8;
818
1559
  const hidden = machines.length - TRAY_MAX;
819
1560
  const hiddenBad = machines.slice(TRAY_MAX).reduce(
820
1561
  (n, m) => n + (lightFor(m, cardProps.health) !== "ok" && lightFor(m, cardProps.health) !== "none" ? 1 : 0),
821
1562
  0
822
1563
  );
823
- return /* @__PURE__ */ jsxs("div", { className: "ocn", children: [
1564
+ const cardAndRack = /* @__PURE__ */ jsxs(Fragment, { children: [
824
1565
  /* @__PURE__ */ jsx(
825
1566
  AgentCard,
826
1567
  {
827
1568
  a,
1569
+ org,
828
1570
  isRoot: a.id === org.root,
829
1571
  depth,
830
1572
  order,
@@ -834,7 +1576,10 @@ function ChartNode({
834
1576
  onDragStart: cardProps.onDragStart,
835
1577
  onDropOn: cardProps.onDropOn,
836
1578
  childCount: kids.length,
837
- light: trayLight
1579
+ light: trayLight,
1580
+ health: cardProps.health,
1581
+ onHover: cardProps.onHover,
1582
+ showLaw: cardProps.showLaw
838
1583
  }
839
1584
  ),
840
1585
  machines.length > 0 && /* @__PURE__ */ jsxs("div", { className: `oc-tray ${trayLight}`, children: [
@@ -855,7 +1600,7 @@ function ChartNode({
855
1600
  },
856
1601
  children: [
857
1602
  /* @__PURE__ */ jsx("i", { className: `mac-dot ${lightFor(m, cardProps.health)}${unstableFor(m, cardProps.health) ? " unstable" : ""}` }),
858
- /* @__PURE__ */ jsx("span", { className: "mac-emoji", children: m.emoji || "\u2699\uFE0F" }),
1603
+ /* @__PURE__ */ jsx(BrandMark, { agent: m }),
859
1604
  /* @__PURE__ */ jsx("span", { className: "mac-name", children: m.name })
860
1605
  ]
861
1606
  },
@@ -879,13 +1624,78 @@ function ChartNode({
879
1624
  ]
880
1625
  }
881
1626
  )
882
- ] }),
1627
+ ] })
1628
+ ] });
1629
+ return /* @__PURE__ */ jsxs("div", { className: "ocn", children: [
1630
+ depth === 0 ? /* @__PURE__ */ jsx("div", { className: "casc-head", children: cardAndRack }) : cardAndRack,
883
1631
  kids.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
884
- /* @__PURE__ */ jsx("div", { className: "oc-down" }),
885
- /* @__PURE__ */ jsx("div", { className: `oc-row${kids.length > 3 ? " wrap" : ""}`, children: kids.map((k, i) => /* @__PURE__ */ jsx("div", { className: "oc-child", style: { ["--h"]: bucketHue(k.id) }, children: /* @__PURE__ */ jsx(ChartNode, { org, a: k, depth: depth + 1, order: i, ...cardProps }) }, k.id)) })
1632
+ /* @__PURE__ */ jsx(
1633
+ "div",
1634
+ {
1635
+ className: `oc-down${isDept ? " foldable" : ""}${folded ? " folded" : ""}`,
1636
+ role: isDept ? "button" : void 0,
1637
+ tabIndex: isDept ? 0 : void 0,
1638
+ title: isDept ? folded ? `${kids.length} staff \u2014 click to expand (semantic zoom: department \u2192 role)` : "click to fold this department to head-only (semantic zoom: role \u2192 department)" : void 0,
1639
+ onClick: isDept ? (e) => {
1640
+ e.stopPropagation();
1641
+ cardProps.onToggleCollapse?.(a.id);
1642
+ } : void 0,
1643
+ onKeyDown: isDept ? (e) => {
1644
+ if (e.key === "Enter" || e.key === " ") {
1645
+ e.preventDefault();
1646
+ cardProps.onToggleCollapse?.(a.id);
1647
+ }
1648
+ } : void 0,
1649
+ children: isDept && /* @__PURE__ */ jsx("i", { className: "oc-fold-glyph", children: folded ? "\u25B8" : "\u25BE" })
1650
+ }
1651
+ ),
1652
+ folded ? /* @__PURE__ */ jsxs(
1653
+ "button",
1654
+ {
1655
+ type: "button",
1656
+ className: "oc-folded-summary",
1657
+ onClick: (e) => {
1658
+ e.stopPropagation();
1659
+ cardProps.onToggleCollapse?.(a.id);
1660
+ },
1661
+ title: "click to expand \u2014 semantic zoom: department \u2192 role",
1662
+ children: [
1663
+ kids.length,
1664
+ " staff \xB7 folded to department level \u2014 click to expand"
1665
+ ]
1666
+ }
1667
+ ) : /* @__PURE__ */ jsx("div", { className: `oc-row${depth > 0 && kids.length > 3 ? " wrap" : ""}${depth === 0 ? " lanes" : ""}`, children: kids.map((k, i) => /* @__PURE__ */ jsx(
1668
+ Lane,
1669
+ {
1670
+ laneId: k.id,
1671
+ isLane: depth === 0,
1672
+ dragId: cardProps.dragId,
1673
+ onDropOn: cardProps.onDropOn,
1674
+ children: /* @__PURE__ */ jsx(ChartNode, { org, a: k, depth: depth + 1, order: i, ...cardProps })
1675
+ },
1676
+ k.id
1677
+ )) })
886
1678
  ] })
887
1679
  ] });
888
1680
  }
1681
+ function BrandMark({ agent }) {
1682
+ const brand = (agent.data ?? {}).brand;
1683
+ const [failed, setFailed] = useState(false);
1684
+ if (typeof brand !== "string" || !brand || failed) {
1685
+ return /* @__PURE__ */ jsx("span", { className: "mac-emoji", children: agent.emoji || "\u2699\uFE0F" });
1686
+ }
1687
+ return /* @__PURE__ */ jsx(
1688
+ "img",
1689
+ {
1690
+ className: "mac-brand",
1691
+ src: `https://cdn.simpleicons.org/${brand}/000`,
1692
+ alt: "",
1693
+ loading: "lazy",
1694
+ draggable: false,
1695
+ onError: () => setFailed(true)
1696
+ }
1697
+ );
1698
+ }
889
1699
  function Chip({ children, tone, onClick }) {
890
1700
  return /* @__PURE__ */ jsx("span", { className: `chip${tone ? ` ${tone}` : ""}${onClick ? " tap" : ""}`, onClick, children });
891
1701
  }
@@ -938,8 +1748,19 @@ function Dossier({
938
1748
  )
939
1749
  ).then((counts) => setMemCount(counts.reduce((s, n) => s + n, 0)));
940
1750
  fetchReports(api, id, 100).then(({ total, nodes }) => {
941
- setRepCount(total);
942
- setReports(nodes.slice(0, 4));
1751
+ if (total > 0) {
1752
+ setRepCount(total);
1753
+ setReports(nodes.slice(0, 4));
1754
+ return;
1755
+ }
1756
+ return fetchReports(api, null, 2e3).then(({ nodes: all }) => {
1757
+ const mine = all.filter((n) => reportAgentId(n) === id);
1758
+ setRepCount(mine.length);
1759
+ setReports(mine.slice(0, 4));
1760
+ });
1761
+ }).catch(() => {
1762
+ setRepCount(0);
1763
+ setReports([]);
943
1764
  });
944
1765
  }, [id, hasSnapshot, slice, api]);
945
1766
  const mem = useCountUp(memCount ?? 0);
@@ -978,13 +1799,10 @@ function Dossier({
978
1799
  return /* @__PURE__ */ jsxs("aside", { className: "doss", onClick: (e) => e.stopPropagation(), children: [
979
1800
  /* @__PURE__ */ jsxs("div", { className: "doss-head", children: [
980
1801
  /* @__PURE__ */ jsx("span", { className: "doss-emoji", children: a.emoji || "\u{1F916}" }),
981
- /* @__PURE__ */ jsxs("div", { children: [
982
- /* @__PURE__ */ jsxs("h2", { children: [
983
- a.name,
984
- " ",
985
- a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
986
- ] }),
987
- a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role })
1802
+ /* @__PURE__ */ jsxs("h2", { children: [
1803
+ a.name,
1804
+ " ",
1805
+ a.kind === "automation" && /* @__PURE__ */ jsx("span", { className: "auto-badge", children: "automation" })
988
1806
  ] }),
989
1807
  /* @__PURE__ */ jsxs("div", { className: "doss-head-actions", children: [
990
1808
  !edit && /* @__PURE__ */ jsx("button", { className: "doss-3d", title: "edit this agent", onClick: startEdit, children: "\u270E edit" }),
@@ -992,6 +1810,7 @@ function Dossier({
992
1810
  /* @__PURE__ */ jsx("button", { className: "doss-close", title: "close the dossier", "aria-label": "close the dossier", onClick: onClose, children: "\u2715" })
993
1811
  ] })
994
1812
  ] }),
1813
+ a.role && /* @__PURE__ */ jsx("p", { className: "doss-role", children: a.role }),
995
1814
  edit && /* @__PURE__ */ jsxs("div", { className: "doss-edit", children: [
996
1815
  /* @__PURE__ */ jsxs("div", { className: "doss-edit-row2", children: [
997
1816
  /* @__PURE__ */ jsxs("label", { children: [
@@ -1044,9 +1863,8 @@ function Dossier({
1044
1863
  p.n
1045
1864
  ] }),
1046
1865
  typeof a.cadence === "number" && /* @__PURE__ */ jsxs("span", { children: [
1047
- "expected every ",
1048
- a.cadence,
1049
- "h"
1866
+ "reports ",
1867
+ everyN(a.cadence)
1050
1868
  ] })
1051
1869
  ] });
1052
1870
  })(),
@@ -1217,7 +2035,8 @@ function OrgScreen({
1217
2035
  hasSnapshot,
1218
2036
  onUpdate,
1219
2037
  onAdd,
1220
- onRemove
2038
+ onRemove,
2039
+ showLaw
1221
2040
  }) {
1222
2041
  const api = useApi();
1223
2042
  const [health, setHealth] = useState(null);
@@ -1226,10 +2045,27 @@ function OrgScreen({
1226
2045
  fetchReports(api, null, 1e4).then(({ nodes }) => setHealth(buildHealthMap(nodes))).catch(() => {
1227
2046
  });
1228
2047
  }, [hasSnapshot, api]);
2048
+ const [hoverId, setHoverId] = useState(null);
2049
+ const litBuckets = useMemo(
2050
+ () => new Set(hoverId ? orgBootSlice(draft, hoverId)?.buckets ?? [] : []),
2051
+ [hoverId, draft]
2052
+ );
2053
+ const [collapsed, setCollapsed] = useState(() => /* @__PURE__ */ new Set());
2054
+ const depts = useMemo(() => draft.agents.filter((a) => a.parent === draft.root), [draft]);
2055
+ const toggleCollapse = useCallback((id) => {
2056
+ setCollapsed((s) => {
2057
+ const n = new Set(s);
2058
+ n.has(id) ? n.delete(id) : n.add(id);
2059
+ return n;
2060
+ });
2061
+ }, []);
2062
+ const allFolded = depts.length > 0 && depts.every((d) => collapsed.has(d.id));
2063
+ const railVersion = `${draft.agents.length}:${[...collapsed].sort().join(",")}:${showLaw}:${health ? 1 : 0}:${selected ?? ""}`;
1229
2064
  const fitRef = useRef(null);
1230
2065
  const chartRef = useRef(null);
1231
2066
  const [fit, setFit] = useState(1);
1232
2067
  const [zoom, setZoom] = useState(null);
2068
+ const [nat, setNat] = useState({ w: 0, h: 0 });
1233
2069
  const eff = zoom ?? fit;
1234
2070
  useLayoutEffect(() => {
1235
2071
  const vp = fitRef.current, chart = chartRef.current;
@@ -1239,15 +2075,18 @@ function OrgScreen({
1239
2075
  const natW = chart.scrollWidth, natH = chart.scrollHeight;
1240
2076
  const availW = vp.clientWidth - PAD * 2, availH = vp.clientHeight - PAD * 2;
1241
2077
  if (natW <= 0 || natH <= 0) return;
1242
- const k = Math.min(1, availW / natW, availH / natH);
1243
- setFit(k > 0 ? k : 1);
2078
+ setNat((p) => p.w === natW && p.h === natH ? p : { w: natW, h: natH });
2079
+ const kW = availW / natW;
2080
+ const kH = availH / natH;
2081
+ const k = natH > availH * 1.35 ? kW : Math.min(kW, kH);
2082
+ setFit(Math.min(1, k > 0 ? k : 1));
1244
2083
  };
1245
2084
  measure();
1246
2085
  const ro = new ResizeObserver(measure);
1247
2086
  ro.observe(vp);
1248
2087
  ro.observe(chart);
1249
2088
  return () => ro.disconnect();
1250
- }, [draft, health, selected]);
2089
+ }, [draft, health, selected, collapsed]);
1251
2090
  const effRef = useRef(eff);
1252
2091
  effRef.current = eff;
1253
2092
  useEffect(() => {
@@ -1264,24 +2103,74 @@ function OrgScreen({
1264
2103
  }, []);
1265
2104
  const root = draft.agents.find((a) => a.id === draft.root);
1266
2105
  return /* @__PURE__ */ jsxs("div", { className: "body", onClick: () => setSelected(null), children: [
1267
- /* @__PURE__ */ jsxs("main", { className: "tree", onClick: (e) => e.stopPropagation(), children: [
1268
- /* @__PURE__ */ jsx("p", { className: "tree-hint", children: "drag an agent \u2014 or a tray machine \u2014 onto its new parent \xB7 click for its dossier \xB7 machine trays show live health" }),
1269
- /* @__PURE__ */ jsx("div", { className: `chart-fit${zoom !== null ? " zoomed" : ""}`, ref: fitRef, children: /* @__PURE__ */ jsx("div", { className: "chart", ref: chartRef, style: { ["--fit"]: eff }, children: root && /* @__PURE__ */ jsx(
1270
- ChartNode,
2106
+ /* @__PURE__ */ jsxs("main", { className: `tree${showLaw ? " law-on" : ""}`, onClick: (e) => e.stopPropagation(), children: [
2107
+ /* @__PURE__ */ jsx("p", { className: "tree-hint", children: showLaw ? "the rails now trace authority \u2014 brass flows from the House Standard through every SOP to every role" : "drag a plate \u2014 or a machine \u2014 onto its new parent \xB7 click for its dossier \xB7 machine racks show live health" }),
2108
+ /* @__PURE__ */ jsxs("div", { className: "ranks", children: [
2109
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2110
+ /* @__PURE__ */ jsx("b", { children: "I" }),
2111
+ "The standard"
2112
+ ] }),
2113
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2114
+ /* @__PURE__ */ jsx("b", { children: "II" }),
2115
+ "The executive"
2116
+ ] }),
2117
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2118
+ /* @__PURE__ */ jsx("b", { children: "III" }),
2119
+ "Departments"
2120
+ ] }),
2121
+ /* @__PURE__ */ jsxs("div", { className: "rank", children: [
2122
+ /* @__PURE__ */ jsx("b", { children: "IV" }),
2123
+ "Staff & machines"
2124
+ ] })
2125
+ ] }),
2126
+ /* @__PURE__ */ jsx("div", { className: `chart-fit${zoom !== null ? " zoomed" : ""}`, ref: fitRef, children: /* @__PURE__ */ jsx("div", { className: "chart-scaled", style: { width: nat.w * eff, height: nat.h * eff }, children: /* @__PURE__ */ jsxs(
2127
+ "div",
1271
2128
  {
1272
- org: draft,
1273
- a: root,
1274
- depth: 0,
1275
- order: 0,
1276
- selected,
1277
- dragId,
1278
- health,
1279
- onSelect: setSelected,
1280
- onDragStart: setDragId,
1281
- onDropOn: dropOn
2129
+ className: "chart",
2130
+ ref: chartRef,
2131
+ style: {
2132
+ ["--fit"]: eff,
2133
+ transform: zoom !== null ? `scale(${eff})` : `translateX(-50%) scale(${eff})`
2134
+ },
2135
+ children: [
2136
+ /* @__PURE__ */ jsx(CascadeRails, { chartRef, version: railVersion }),
2137
+ /* @__PURE__ */ jsxs("div", { className: "cascade", children: [
2138
+ /* @__PURE__ */ jsx(LawPlate, { org: draft, selected: selected === draft.root, onSelect: setSelected }),
2139
+ root && /* @__PURE__ */ jsx(
2140
+ ChartNode,
2141
+ {
2142
+ org: draft,
2143
+ a: root,
2144
+ depth: 0,
2145
+ order: 0,
2146
+ selected,
2147
+ dragId,
2148
+ health,
2149
+ onSelect: setSelected,
2150
+ onDragStart: setDragId,
2151
+ onDropOn: dropOn,
2152
+ onHover: setHoverId,
2153
+ showLaw,
2154
+ collapsed,
2155
+ onToggleCollapse: toggleCollapse
2156
+ }
2157
+ )
2158
+ ] })
2159
+ ]
1282
2160
  }
1283
2161
  ) }) }),
1284
2162
  /* @__PURE__ */ jsxs("div", { className: "zoomer", children: [
2163
+ /* @__PURE__ */ jsx(
2164
+ "button",
2165
+ {
2166
+ type: "button",
2167
+ className: allFolded ? "on" : "",
2168
+ title: allFolded ? "unfold every department \u2014 see roles again" : "fold every department to head-only \u2014 house \u2192 department",
2169
+ onClick: () => setCollapsed(allFolded ? /* @__PURE__ */ new Set() : new Set(depts.map((d) => d.id))),
2170
+ children: allFolded ? "\u2302 house" : "\u25BE roles"
2171
+ }
2172
+ ),
2173
+ /* @__PURE__ */ jsx("span", { className: "zoomer-sep" }),
1285
2174
  /* @__PURE__ */ jsx("button", { type: "button", title: "zoom out (ctrl+wheel)", onClick: () => setZoom(Math.max(0.2, eff / 1.25)), children: "\u2212" }),
1286
2175
  /* @__PURE__ */ jsxs("span", { className: "zoomer-pct", children: [
1287
2176
  Math.round(eff * 100),
@@ -1289,7 +2178,8 @@ function OrgScreen({
1289
2178
  ] }),
1290
2179
  /* @__PURE__ */ jsx("button", { type: "button", title: "zoom in (ctrl+wheel)", onClick: () => setZoom(Math.min(2.5, eff * 1.25)), children: "\uFF0B" }),
1291
2180
  /* @__PURE__ */ jsx("button", { type: "button", className: zoom === null ? "on" : "", title: "fit the whole org in view", onClick: () => setZoom(null), children: "fit" })
1292
- ] })
2181
+ ] }),
2182
+ /* @__PURE__ */ jsx(LedgerShelf, { org: draft, lit: litBuckets })
1293
2183
  ] }),
1294
2184
  selected && /* @__PURE__ */ jsx(
1295
2185
  Dossier,
@@ -1418,8 +2308,8 @@ function ReportsScreen({ org, hasSnapshot }) {
1418
2308
  if (!hasSnapshot) return;
1419
2309
  fetchReports(api, null, 1e3).then(({ nodes }) => setRows(nodes)).catch(() => setRows([]));
1420
2310
  }, [hasSnapshot, api]);
1421
- const agents = useMemo(() => [...new Set((rows ?? []).map((r) => r.cluster ?? ""))].filter(Boolean), [rows]);
1422
- const shown = (rows ?? []).filter((r) => !who || r.cluster === who);
2311
+ const agents = useMemo(() => [...new Set((rows ?? []).map(reportAgentId))].filter(Boolean), [rows]);
2312
+ const shown = (rows ?? []).filter((r) => !who || reportAgentId(r) === who);
1423
2313
  const total = useCountUp(shown.length);
1424
2314
  if (!hasSnapshot)
1425
2315
  return /* @__PURE__ */ jsxs("div", { className: "screen", children: [
@@ -1450,15 +2340,16 @@ function ReportsScreen({ org, hasSnapshot }) {
1450
2340
  ")."
1451
2341
  ] }) : /* @__PURE__ */ jsxs("div", { className: "timeline", children: [
1452
2342
  shown.slice(0, 100).map((r) => {
1453
- const a = r.cluster ? nameOf.get(r.cluster) : void 0;
2343
+ const filer = reportAgentId(r);
2344
+ const a = filer ? nameOf.get(filer) : void 0;
1454
2345
  const when = relTime(nodeAt(r));
1455
2346
  const sum = nodeSummary(r);
1456
- return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(r.cluster ?? "x") }, children: [
2347
+ return /* @__PURE__ */ jsxs("div", { className: "tl-row", style: { ["--h"]: bucketHue(filer || "x") }, children: [
1457
2348
  /* @__PURE__ */ jsx("div", { className: "tl-dot" }),
1458
2349
  /* @__PURE__ */ jsxs("div", { className: "tl-body", children: [
1459
2350
  /* @__PURE__ */ jsxs("div", { className: "tl-top", children: [
1460
2351
  /* @__PURE__ */ jsx("span", { className: "tl-ava", children: a?.emoji ?? "\u{1F916}" }),
1461
- /* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ?? r.cluster ?? "unknown" }),
2352
+ /* @__PURE__ */ jsx("span", { className: "tl-agent", children: a?.name ?? filer ?? "unknown" }),
1462
2353
  when && /* @__PURE__ */ jsx("span", { className: "tl-when", children: when })
1463
2354
  ] }),
1464
2355
  /* @__PURE__ */ jsx("p", { className: "tl-sum", children: sum || r.label })
@@ -1543,7 +2434,7 @@ function GraphScreen({ hasSnapshot }) {
1543
2434
  ] });
1544
2435
  return /* @__PURE__ */ jsx("div", { className: "graph-wrap", children: /* @__PURE__ */ jsx("iframe", { className: "graph-frame", src: "/view/?file=/snapshot.json", title: "Booboo 3D brain" }) });
1545
2436
  }
1546
- function App() {
2437
+ function App({ theme: pinnedTheme }) {
1547
2438
  const api = useApi();
1548
2439
  const [tab, param] = useRoute();
1549
2440
  const [saved, setSaved] = useState(null);
@@ -1558,7 +2449,8 @@ function App() {
1558
2449
  api("/org").then((o) => {
1559
2450
  setSaved(o);
1560
2451
  setDraft(o);
1561
- setSelected(o.root);
2452
+ const embed = new URLSearchParams(window.location.search).get("embed");
2453
+ if (!embed) setSelected(o.root);
1562
2454
  }).catch(() => setErr("Can't load the organigram \u2014 is `booboo panel --org` running?"));
1563
2455
  api("/stats").then(setStats).catch(() => setStats(null));
1564
2456
  Promise.all([
@@ -1576,15 +2468,32 @@ function App() {
1576
2468
  for (const a of draft.agents) {
1577
2469
  const b = before.get(a.id);
1578
2470
  if (!b) {
1579
- out.push(`\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}`);
2471
+ out.push({ key: `add:${a.id}`, id: a.id, kind: "added", label: `\uFF0B ${a.name} under ${a.parent ? name(a.parent) : "root"}` });
1580
2472
  continue;
1581
2473
  }
1582
- if ((b.parent ?? null) !== (a.parent ?? null)) out.push(`${a.name} \u2192 now under ${a.parent ? name(a.parent) : "root"}`);
1583
- if (JSON.stringify(noParent(b)) !== JSON.stringify(noParent(a))) out.push(`\u270E ${a.name} edited`);
2474
+ if ((b.parent ?? null) !== (a.parent ?? null)) out.push({ key: `move:${a.id}`, id: a.id, kind: "moved", label: `${a.name} \u2192 now under ${a.parent ? name(a.parent) : "root"}` });
2475
+ if (JSON.stringify(noParent(b)) !== JSON.stringify(noParent(a))) out.push({ key: `edit:${a.id}`, id: a.id, kind: "edited", label: `\u270E ${a.name} edited` });
1584
2476
  }
1585
- for (const b of saved.agents) if (!after.has(b.id)) out.push(`\u2212 ${b.name} removed`);
2477
+ for (const b of saved.agents) if (!after.has(b.id)) out.push({ key: `rm:${b.id}`, id: b.id, kind: "removed", label: `\u2212 ${b.name} removed` });
1586
2478
  return out;
1587
2479
  }, [saved, draft]);
2480
+ const revertOne = useCallback((c) => {
2481
+ if (!saved || !draft) return;
2482
+ const was = saved.agents.find((a) => a.id === c.id);
2483
+ setDraft((d) => {
2484
+ if (!d) return d;
2485
+ if (c.kind === "added") return { ...d, agents: d.agents.filter((a) => a.id !== c.id) };
2486
+ if (c.kind === "removed" && was) return { ...d, agents: [...d.agents, was] };
2487
+ if (!was) return d;
2488
+ return {
2489
+ ...d,
2490
+ agents: d.agents.map((a) => {
2491
+ if (a.id !== c.id) return a;
2492
+ return c.kind === "moved" ? { ...a, parent: was.parent } : { ...was, parent: a.parent };
2493
+ })
2494
+ };
2495
+ });
2496
+ }, [saved, draft]);
1588
2497
  const updateAgent = useCallback((id, patch) => {
1589
2498
  setDraft((d) => d ? { ...d, agents: d.agents.map((a) => a.id === id ? { ...a, ...patch } : a) } : d);
1590
2499
  }, []);
@@ -1656,11 +2565,13 @@ function App() {
1656
2565
  const nodeCount = useCountUp(stats?.nodes ?? 0, 1200);
1657
2566
  const memTotal = useCountUp(totals?.mem ?? 0, 1100);
1658
2567
  const repTotal = useCountUp(totals?.rep ?? 0, 1300);
1659
- const [theme, toggleTheme] = useTheme();
1660
- if (err && !draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal", children: err });
1661
- if (!draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal calm", children: "waking the organigram\u2026" });
1662
- return /* @__PURE__ */ jsxs("div", { className: "pnl", children: [
2568
+ const [theme, toggleTheme] = useTheme(pinnedTheme);
2569
+ const [showLaw, setShowLaw] = useState(false);
2570
+ if (err && !draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal", "data-theme": theme, children: err });
2571
+ if (!draft) return /* @__PURE__ */ jsx("div", { className: "pnl-fatal calm", "data-theme": theme, children: "waking the organigram\u2026" });
2572
+ return /* @__PURE__ */ jsxs("div", { className: "pnl", "data-theme": theme, children: [
1663
2573
  /* @__PURE__ */ jsx("div", { className: "pnl-aurora", "aria-hidden": true }),
2574
+ /* @__PURE__ */ jsx(Constellation, {}),
1664
2575
  /* @__PURE__ */ jsxs("header", { className: "bar", children: [
1665
2576
  /* @__PURE__ */ jsxs("div", { className: "bar-brand", children: [
1666
2577
  "\u{1F43E} ",
@@ -1685,7 +2596,17 @@ function App() {
1685
2596
  ] })
1686
2597
  ] }),
1687
2598
  /* @__PURE__ */ jsxs("div", { className: "bar-actions", children: [
1688
- /* @__PURE__ */ jsx("button", { className: "btn ghost theme-toggle", title: "light / dark", "aria-label": "toggle light or dark theme", onClick: toggleTheme, children: theme === "dark" ? "\u2600" : "\u263E" }),
2599
+ tab === "org" && /* @__PURE__ */ jsx(
2600
+ "button",
2601
+ {
2602
+ className: `btn ghost law-toggle${showLaw ? " on" : ""}`,
2603
+ title: "trace rule inheritance \u2014 House Standard \u2192 SOP \u2192 role",
2604
+ "aria-pressed": showLaw,
2605
+ onClick: () => setShowLaw((v) => !v),
2606
+ children: "\u2696 show the law"
2607
+ }
2608
+ ),
2609
+ toggleTheme && /* @__PURE__ */ jsx("button", { className: "btn ghost theme-toggle", title: "light / dark", "aria-label": "toggle light or dark theme", onClick: toggleTheme, children: theme === "dark" ? "\u2600" : "\u263E" }),
1689
2610
  changes.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
1690
2611
  /* @__PURE__ */ jsxs("span", { className: "bar-draft", children: [
1691
2612
  changes.length,
@@ -1702,7 +2623,23 @@ function App() {
1702
2623
  " ",
1703
2624
  t.label
1704
2625
  ] }, t.id)) }),
1705
- changes.length > 0 && tab === "org" && /* @__PURE__ */ jsx("div", { className: "pending", children: changes.map((c) => /* @__PURE__ */ jsx("span", { className: "pending-item", children: c }, c)) }),
2626
+ changes.length > 0 && tab === "org" && /* @__PURE__ */ jsxs("div", { className: "pending", children: [
2627
+ /* @__PURE__ */ jsx("span", { className: "pending-lead", children: "unapplied" }),
2628
+ changes.map((c) => /* @__PURE__ */ jsxs(
2629
+ "button",
2630
+ {
2631
+ type: "button",
2632
+ className: "pending-item",
2633
+ title: "undo just this change",
2634
+ onClick: () => revertOne(c),
2635
+ children: [
2636
+ c.label,
2637
+ /* @__PURE__ */ jsx("i", { "aria-hidden": "true", children: "undo" })
2638
+ ]
2639
+ },
2640
+ c.key
2641
+ ))
2642
+ ] }),
1706
2643
  err && /* @__PURE__ */ jsx("div", { className: "pnl-err", children: err }),
1707
2644
  /* @__PURE__ */ jsxs("div", { className: "content", children: [
1708
2645
  tab === "org" && /* @__PURE__ */ jsx(
@@ -1717,7 +2654,8 @@ function App() {
1717
2654
  hasSnapshot: !!stats,
1718
2655
  onUpdate: updateAgent,
1719
2656
  onAdd: addAgent,
1720
- onRemove: removeAgent
2657
+ onRemove: removeAgent,
2658
+ showLaw
1721
2659
  }
1722
2660
  ),
1723
2661
  tab === "buckets" && /* @__PURE__ */ jsx(BucketsScreen, { org: draft, param, hasSnapshot: !!stats }),
@@ -1727,17 +2665,17 @@ function App() {
1727
2665
  ] }, tab + (param ?? ""))
1728
2666
  ] });
1729
2667
  }
1730
- function Panel({ api = defaultApi } = {}) {
2668
+ function Panel({ api = defaultApi, theme } = {}) {
1731
2669
  return /* @__PURE__ */ jsxs(ApiCtx.Provider, { value: api, children: [
1732
2670
  /* @__PURE__ */ jsx("style", { children: PANEL_CSS }),
1733
- /* @__PURE__ */ jsx(App, {})
2671
+ /* @__PURE__ */ jsx(App, { theme })
1734
2672
  ] });
1735
2673
  }
1736
2674
 
1737
2675
  // package.json
1738
2676
  var package_default = {
1739
2677
  name: "@booboo-brain/panel",
1740
- version: "0.5.3",
2678
+ version: "0.5.5",
1741
2679
  description: "Booboo panel \u2014 the organigram. Run your agents like a company: a drag-drop hierarchy your agents boot from, with buckets, reports, rules and the 3D graph, over one org file + snapshot.",
1742
2680
  license: "MIT",
1743
2681
  repository: {