← back to Agent Cabinet

pyramid.html

971 lines

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Cabinet — Pyramid &amp; Timelines</title>
<style>
  :root{
    --bg:#0b0e14; --panel:#11151f; --ink:#e7ecf3; --dim:#8a94a6; --line:#1f2735;
    --op:#60a5fa; --com:#34d399; --mkt:#f472b6; --dir:#fbbf24; --eng:#a78bfa; --res:#22d3ee; --cmp:#fb7185;
  }
  *{box-sizing:border-box}
  html,body{margin:0;background:var(--bg);color:var(--ink);font:13px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Helvetica,Arial,sans-serif}
  header{padding:18px 24px 10px;border-bottom:1px solid var(--line);position:sticky;top:0;background:linear-gradient(180deg,#0b0e14 70%,rgba(11,14,20,.0));z-index:30}
  h1{margin:0;font-size:20px;letter-spacing:.2px;font-weight:600}
  h1 small{color:var(--dim);font-weight:400;font-size:13px;margin-left:10px}
  .legend{display:flex;gap:14px;flex-wrap:wrap;margin-top:10px;align-items:center}
  .legend .lg{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--dim)}
  .legend .dot{width:11px;height:11px;border-radius:3px}
  .legend .key{margin-left:auto;display:flex;gap:14px;font-size:11.5px;color:var(--dim)}
  .legend .key b{color:var(--ink);font-weight:600}
  .scroller{overflow:auto;padding:26px 24px 40px}
  #pyramid{position:relative;min-width:1180px}
  svg#lines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:1}
  svg#xlines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:1}
  .toolbar{display:flex;gap:16px;align-items:center;margin-top:11px;flex-wrap:wrap}
  .toolbar label{display:flex;align-items:center;gap:6px;font-size:12px;color:#aeb8c8;cursor:pointer;user-select:none}
  .toolbar input{accent-color:#6d34f4;cursor:pointer}
  .tog-line{display:inline-block;width:22px;border-top-width:2px;height:0;vertical-align:middle}
  .tb-btn{flex:none;background:#161d2c;border:1px solid #2b3650;color:#bcd0f0;border-radius:7px;padding:5px 12px;font-size:12px;cursor:pointer;margin-left:auto}
  .tb-btn:hover{background:#1b2435}
  .tier{position:relative;z-index:2}
  .tier-label{position:absolute;left:-2px;transform:translateX(-100%);color:var(--dim);font-size:10px;letter-spacing:.12em;text-transform:uppercase;white-space:nowrap;padding-right:14px;opacity:.65}

  /* President */
  .president{display:flex;justify-content:center;margin-bottom:54px}
  .pres-node{background:linear-gradient(180deg,#1b2233,#141a28);border:1px solid #2b3650;border-radius:14px;padding:12px 26px;text-align:center;box-shadow:0 6px 26px rgba(0,0,0,.4)}
  .pres-node .t{font-size:16px;font-weight:700}
  .pres-node .s{color:var(--dim);font-size:11.5px;margin-top:2px}

  /* Officers row */
  .officers{display:flex;gap:18px;align-items:flex-start;justify-content:space-between}
  .officer-col{flex:1 1 0;min-width:150px;display:flex;flex-direction:column;align-items:center}
  .officer-node{width:100%;border-radius:11px;padding:10px 12px;text-align:center;cursor:default;
    background:#121826;border:1px solid var(--oc,#2b3650);box-shadow:0 4px 18px rgba(0,0,0,.35);position:relative}
  .officer-node::before{content:"";position:absolute;left:0;top:0;right:0;height:3px;border-radius:11px 11px 0 0;background:var(--oc)}
  .officer-node .nm{font-weight:700;font-size:13px;color:var(--ink)}
  .officer-node .dm{color:var(--dim);font-size:10.5px;margin-top:3px;line-height:1.3;min-height:26px}
  .officer-node .ct{margin-top:6px;font-size:10.5px;color:var(--oc);font-weight:600}

  .group-label{align-self:flex-start;margin:16px 0 6px;font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);opacity:.7}
  .agents,.skills{display:flex;flex-wrap:wrap;gap:5px;width:100%;justify-content:center}
  .agent{font-size:10.5px;padding:4px 7px;border-radius:7px;background:#0f1623;border:1px solid var(--oc);color:#dfe6f2;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
  .skill{font-size:9.5px;padding:2.5px 6px;border-radius:20px;background:#0c1019;border:1px solid var(--line);color:var(--dim);white-space:nowrap}
  .skill.scr{border-color:var(--oc);color:#cfe;cursor:help;font-weight:600}

  /* Workflows */
  .tier-workflows{margin-top:58px}
  .wf-band{border:1px dashed #2b3650;border-radius:12px;padding:14px 16px;background:rgba(20,26,40,.4)}
  .wf-title{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--dim);margin-bottom:10px}
  .wf-row{display:flex;gap:10px;flex-wrap:wrap}
  .wf{min-width:150px;flex:1 1 150px;border-radius:10px;background:#101725;border:1px solid var(--wc,#2b3650);padding:9px 11px}
  .wf .wn{font-weight:700;font-size:12px}
  .wf .wd{color:var(--dim);font-size:10.5px;margin-top:2px}
  .wf .wt{font-size:10px;color:var(--wc);margin-top:5px;font-weight:600}

  /* Timeline */
  .timeline-wrap{margin:8px 24px 50px;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px 18px 22px}
  .timeline-wrap h2{margin:0 0 4px;font-size:15px;font-weight:600}
  .timeline-wrap .sub{color:var(--dim);font-size:11.5px;margin-bottom:16px}
  .tl{position:relative;margin-left:178px;border-left:1px solid var(--line)}
  .tl-axis{position:relative;height:20px;margin-bottom:4px}
  .tl-axis .hr{position:absolute;top:0;height:100%;border-left:1px solid #1a2230}
  .tl-axis .hr span{position:absolute;top:2px;left:3px;font-size:9.5px;color:var(--dim)}
  .lane{position:relative;height:26px;border-top:1px solid #141b28}
  .lane:hover{background:rgba(255,255,255,.02)}
  .lane .lab{position:absolute;left:-178px;width:172px;top:0;height:100%;display:flex;align-items:center;gap:7px;font-size:11px;color:var(--ink);padding-right:8px;justify-content:flex-end;text-align:right}
  .lane .lab .d{width:9px;height:9px;border-radius:2px;flex:none}
  .lane .lab .o{color:var(--dim);font-size:9.5px}
  .mark{position:absolute;top:50%;transform:translate(-50%,-50%);z-index:2}
  .mark.pt{width:11px;height:11px;border-radius:50%;border:2px solid #0b0e14}
  .mark.pt .tip,.bar .tip{display:none}
  .bar{position:absolute;top:50%;transform:translateY(-50%);height:8px;border-radius:5px;opacity:.55;background-image:repeating-linear-gradient(90deg,transparent,transparent 5px,rgba(0,0,0,.25) 5px,rgba(0,0,0,.25) 9px)}
  .mark:hover .tip,.bar:hover .tip{display:block}
  .tip{position:absolute;bottom:16px;left:50%;transform:translateX(-50%);background:#0b0e14;border:1px solid var(--line);border-radius:6px;padding:5px 8px;font-size:10.5px;white-space:nowrap;color:var(--ink);z-index:9;box-shadow:0 4px 14px rgba(0,0,0,.5)}
  .foot{color:var(--dim);font-size:11px;padding:0 24px 36px}
  .foot code{background:#101725;border:1px solid var(--line);border-radius:4px;padding:1px 5px}

  /* clickable nodes */
  [data-kind],[data-gap]{cursor:pointer}
  .agent:hover,.skill:hover,.wf:hover{outline:1px solid #4b5b7a;filter:brightness(1.18)}
  .officer-node[data-kind]:hover{filter:brightness(1.12)}
  /* keyboard focus — every interactive surface gets a visible ring */
  [data-kind]:focus-visible,[data-gap]:focus-visible,.gap-h:focus-visible,.sug-h:focus-visible,
  .dr-btn:focus-visible,.dr-x:focus-visible,.mini:focus-visible,.tb-btn:focus-visible,
  .modal-x:focus-visible,.m-btn:focus-visible,.route-chip:focus-visible,.idea-card:focus-visible,
  .toolbar input:focus-visible{outline:2px solid #8ab4ff;outline-offset:2px;border-radius:8px}
  .dr-btn:disabled{opacity:.55;cursor:wait;filter:saturate(.6)}

  /* drawer */
  #drawer{position:fixed;top:0;right:0;height:100vh;width:min(560px,94vw);background:#0d1119;border-left:1px solid #243049;
    box-shadow:-14px 0 50px rgba(0,0,0,.6);transform:translateX(102%);transition:transform .22s cubic-bezier(.4,0,.2,1);z-index:100;display:flex;flex-direction:column}
  #drawer.open{transform:translateX(0)}
  .dr-head{padding:16px 18px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px}
  .dr-kind{font-size:10px;font-weight:800;letter-spacing:.1em;padding:2px 7px;border-radius:5px}
  .k-agent{background:#2a2150;color:#c4b5fd;border:1px solid #6d34f4}
  .k-skill{background:#0f2a1c;color:#86efac;border:1px solid #16a34a}
  #dr-title{font-size:16px;font-weight:700;flex:1;word-break:break-word}
  .dr-x{background:transparent;border:1px solid var(--line);color:var(--dim);border-radius:6px;width:28px;height:28px;cursor:pointer;font-size:15px}
  .dr-x:hover{color:#fff;border-color:#4b5b7a}
  .dr-actions{display:flex;gap:9px;padding:13px 18px;border-bottom:1px solid var(--line)}
  .dr-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;padding:10px;border-radius:9px;font-size:13px;font-weight:700;cursor:pointer;border:1px solid}
  .dr-btn.run{background:#10231a;color:#6ee7b7;border-color:#16a34a}
  .dr-btn.run:hover{background:#16321f}
  .dr-btn.open{background:#161d2c;color:#bcd0f0;border-color:#2b3650}
  .dr-btn.open:hover{background:#1b2435}
  #dr-body{padding:14px 18px;overflow:auto;flex:1}
  .dr-path{font:11px ui-monospace,monospace;color:#6b7280;margin-bottom:8px;word-break:break-all}
  .dr-src{background:#0a0e16;border:1px solid var(--line);border-radius:8px;padding:12px;font:11.5px/1.5 ui-monospace,monospace;color:#c8d2e0;white-space:pre-wrap;word-break:break-word;margin:0}
  .dr-note{color:var(--dim);font-size:12px;background:#12161f;border:1px dashed #2b3650;border-radius:8px;padding:11px;margin-bottom:10px}
  .dr-load{color:var(--dim);font-size:12px;padding:8px}
  .scr-row{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:6px 8px;border-bottom:1px solid #141b28}
  .scr-n{font:11.5px ui-monospace,monospace;color:#cfe;word-break:break-all}
  .mini{background:#161d2c;border:1px solid #2b3650;color:#bcd0f0;border-radius:6px;padding:3px 8px;font-size:11px;cursor:pointer;margin-left:5px}
  .mini:hover{background:#1b2435}
  .sug-hit{cursor:pointer}
  #sug-handles{position:absolute;inset:0;pointer-events:none;z-index:6}
  .sug-h{position:absolute;transform:translate(-50%,-50%);pointer-events:auto;cursor:pointer;width:30px;height:30px;border-radius:50%;
    border:2px solid #0b0e14;background:#fbbf24;color:#3a2a05;font-size:15px;font-weight:900;line-height:1;display:flex;align-items:center;justify-content:center;
    box-shadow:0 0 0 2px rgba(251,191,36,.35),0 3px 9px rgba(0,0,0,.55);transition:transform .12s}
  .sug-h:hover,.sug-h:focus-visible{transform:translate(-50%,-50%) scale(1.3);z-index:8}
  .sug-h.approved{background:#34d399;color:#062a1c;box-shadow:0 0 0 2px rgba(52,211,153,.4),0 3px 9px rgba(0,0,0,.55)}
  /* hover/focus label on suggestion + gap handles */
  .sug-h::after,.gap-h::after{content:attr(data-act);position:absolute;left:50%;bottom:calc(100% + 7px);transform:translateX(-50%);
    white-space:nowrap;font-size:11px;font-weight:700;letter-spacing:.02em;padding:3px 8px;border-radius:6px;border:1px solid;
    opacity:0;pointer-events:none;transition:opacity .12s;box-shadow:0 4px 14px rgba(0,0,0,.5);z-index:9}
  .sug-h::after{background:#33280a;color:#fde68a;border-color:#fbbf24}
  .gap-h::after{background:#2a1414;color:#fca5a5;border-color:#b91c1c}
  .sug-h.approved::after,.gap-h.approved::after{background:#0f2a1c;color:#86efac;border-color:#16a34a}
  .sug-h:hover::after,.sug-h:focus-visible::after,.gap-h:hover::after,.gap-h:focus-visible::after{opacity:1}
  .rel-flow{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:12px;margin-bottom:4px}
  .rel-end{background:#161d2c;border:1px solid #2b3650;border-radius:6px;padding:3px 9px;font-weight:700}
  .rel-to{border-color:#fbbf24;color:#fde68a}
  .rel-mid{color:#fbbf24;font:11px ui-monospace,monospace}
  #dr-body code{background:#101725;border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-size:11px}
  #toast{position:fixed;bottom:22px;left:50%;transform:translateX(-50%) translateY(20px);background:#0a0e16;border:1px solid #2b3650;color:#e7ecf3;
    padding:10px 16px;border-radius:9px;font-size:12.5px;opacity:0;transition:.2s;z-index:120;box-shadow:0 8px 30px rgba(0,0,0,.5);pointer-events:none}
  #toast.show{opacity:1;transform:translateX(-50%) translateY(0)}

  /* ideas modal */
  .modal-overlay{position:fixed;inset:0;background:rgba(4,6,11,.72);backdrop-filter:blur(3px);z-index:200;display:none;align-items:center;justify-content:center;padding:24px}
  .modal-overlay.open{display:flex}
  .modal-box{background:#0d1119;border:1px solid #2b3650;border-radius:16px;width:min(720px,96vw);max-height:88vh;overflow:auto;box-shadow:0 24px 70px rgba(0,0,0,.6);position:relative}
  .modal-x{position:absolute;top:13px;right:14px;background:transparent;border:1px solid var(--line);color:var(--dim);border-radius:7px;width:30px;height:30px;cursor:pointer;font-size:15px;z-index:2}
  .modal-x:hover{color:#fff;border-color:#4b5b7a}
  .m-pad{padding:22px 24px}
  .m-h{font-size:19px;font-weight:700;margin:0 0 4px;display:flex;align-items:center;gap:10px}
  .m-sub{color:var(--dim);font-size:13px;margin-bottom:16px;line-height:1.5}
  .idea-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px}
  .idea-card{background:#101725;border:1px solid #2b3650;border-radius:12px;padding:14px;cursor:pointer;transition:transform .12s,border-color .12s}
  .idea-card:hover{transform:translateY(-3px);border-color:#6d34f4}
  .idea-card .ic{font-size:26px}
  .idea-card .t{font-weight:700;font-size:13.5px;margin:7px 0 4px}
  .idea-card .p{color:var(--dim);font-size:11.5px;line-height:1.45}
  .idea-card .tag{display:inline-block;margin-top:8px;font-size:9.5px;font-weight:800;letter-spacing:.08em;padding:2px 7px;border-radius:5px}
  .tag.live{background:#0f2a1c;color:#86efac;border:1px solid #16a34a}
  .tag.prop{background:#33280a;color:#fbbf24;border:1px solid #fbbf24}
  .m-actions{display:flex;gap:9px;flex-wrap:wrap;margin-top:18px;border-top:1px solid var(--line);padding-top:16px}
  .m-btn{padding:9px 15px;border-radius:9px;font-size:13px;font-weight:700;cursor:pointer;border:1px solid}
  .m-btn.primary{background:#241a52;color:#c4b5fd;border-color:#6d34f4}
  .m-btn.primary:hover{background:#2d2168}
  .m-btn.ghost{background:#161d2c;color:#bcd0f0;border-color:#2b3650}
  .m-btn.ghost:hover{background:#1b2435}
  .m-btn.queue{background:#10231a;color:#6ee7b7;border-color:#16a34a}
  .m-btn.queue:hover{background:#16321f}
  .ri{display:flex;gap:8px;margin-bottom:12px}
  .ri input{flex:1;background:#0a0e16;border:1px solid #2b3650;color:#e7ecf3;border-radius:8px;padding:9px 11px;font-size:13px}
  .route-off{background:#101725;border:1px solid var(--rc,#2b3650);border-radius:10px;padding:11px;margin-bottom:9px}
  .route-off .ro-h{font-weight:700;font-size:13px;color:var(--rc)}
  .route-chip{display:inline-flex;align-items:center;gap:5px;background:#0c1019;border:1px solid var(--line);border-radius:7px;padding:3px 8px;margin:5px 5px 0 0;font-size:11px;cursor:pointer}
  .route-chip:hover{border-color:#6d34f4;color:#fff}
  .m-list{font-size:12px}
  .m-list .row{display:flex;justify-content:space-between;gap:10px;padding:6px 2px;border-bottom:1px solid #141b28}
  .m-quote{font-style:italic;color:#9fb0c9;border-left:3px solid #6d34f4;padding:6px 12px;margin:10px 0;font-size:12px;background:rgba(109,52,244,.07)}
  /* health + cost overlays */
  .officer-node.h-up{border-color:#16a34a !important}
  .officer-node.h-warn{border-color:#fbbf24 !important}
  .officer-node.h-down{border-color:#ef4444 !important}
  .health-dot{position:absolute;top:7px;right:8px;width:11px;height:11px;border-radius:50%;box-shadow:0 0 0 2px #0b0e14}
  .health-dot.up{background:#34d399}.health-dot.warn{background:#fbbf24}.health-dot.down{background:#ef4444}.health-dot.na{background:#4b5563}
  .cost-badge{position:absolute;top:7px;left:8px;font:800 9.5px ui-monospace,monospace;color:#fde68a;background:#3a2a05;border:1px solid #b8860b;border-radius:5px;padding:1px 5px}
  .cost-bar{position:absolute;left:0;bottom:0;height:3px;background:linear-gradient(90deg,#f59e0b,#ef4444)}
  #clear-overlay{position:fixed;bottom:20px;left:20px;z-index:140;background:#161d2c;border:1px solid #2b3650;color:#bcd0f0;border-radius:9px;padding:8px 14px;font-size:12px;cursor:pointer;display:none;box-shadow:0 6px 20px rgba(0,0,0,.5)}
  #clear-overlay.show{display:block}
  .hstat{display:inline-flex;align-items:center;gap:6px;font-size:12px;padding:3px 0}
  .hstat .d{width:10px;height:10px;border-radius:50%}
  .bar-row{display:flex;align-items:center;gap:8px;font-size:11.5px;margin:3px 0}
  .bar-row .nm{width:170px;color:#cfd8e6;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .bar-row .bar{height:9px;border-radius:5px;background:linear-gradient(90deg,#6d34f4,#a78bfa)}
  .bar-row .v{color:#fde68a;font:11px ui-monospace,monospace;min-width:52px;text-align:right}

  /* ── Gaps overlay (proposed officers + orphan adoptions) ── */
  svg#glines{position:absolute;inset:0;width:100%;height:100%;pointer-events:none;overflow:visible;z-index:1}
  #gap-handles{position:absolute;inset:0;pointer-events:none;z-index:6}
  .gap-col{opacity:.95}
  .officer-node.gap-node{border-style:dashed;border-color:#f87171;
    background:repeating-linear-gradient(135deg,#16131a,#16131a 9px,#1a1014 9px,#1a1014 18px)}
  .officer-node.gap-node::before{background:#f87171}
  .officer-node.gap-node.gap-approved{border-style:solid;border-color:#34d399}
  .officer-node.gap-node.gap-approved::before{background:#34d399}
  .gap-tag{position:absolute;top:7px;right:8px;font-size:8px;font-weight:800;letter-spacing:.1em;
    background:#b91c1c;color:#fff;padding:2px 6px;border-radius:4px}
  .gap-node.gap-approved .gap-tag{background:#16a34a}
  .gap-chip{border-style:dashed !important;opacity:.85}
  .gap-orphans{margin-top:9px;border-top:1px dashed #3a2530;padding-top:7px}
  .gap-orphans::before{content:"orphans · click + to adopt";display:block;width:100%;font-size:8.5px;
    letter-spacing:.08em;text-transform:uppercase;color:#9a7480;opacity:.8;margin-bottom:4px}
  .orphan-chip{background:#1a1410;border:1px dashed #b91c1c;color:#fca5a5;cursor:pointer}
  .orphan-chip:hover{filter:brightness(1.25);border-color:#f87171}
  .orphan-chip.gap-approved{background:#0f2a1c;border-color:#34d399;color:#86efac}
  .gap-hit{cursor:pointer}
  .gap-h{position:absolute;transform:translate(-50%,-50%);pointer-events:auto;cursor:pointer;width:30px;height:30px;border-radius:50%;
    border:2px solid #0b0e14;background:#f87171;color:#3a0a0a;font-size:17px;font-weight:900;line-height:1;display:flex;align-items:center;justify-content:center;
    box-shadow:0 0 0 2px rgba(248,113,113,.35),0 3px 9px rgba(0,0,0,.55);transition:transform .12s}
  .gap-h:hover,.gap-h:focus-visible{transform:translate(-50%,-50%) scale(1.3);z-index:8}
  .gap-h.approved{background:#34d399;color:#062a1c;box-shadow:0 0 0 2px rgba(52,211,153,.4),0 3px 9px rgba(0,0,0,.55)}
  /* the ghost (proposed) officer node itself reads as actionable */
  .officer-node.gap-node{cursor:pointer;transition:filter .12s,box-shadow .12s}
  .officer-node.gap-node:hover,.officer-node.gap-node:focus-visible{filter:brightness(1.22);box-shadow:0 0 0 2px rgba(248,113,113,.4),0 6px 22px rgba(0,0,0,.5)}
  .orphan-chip:focus-visible{outline:2px solid #f87171;outline-offset:2px}
</style>
</head>
<body>
<header>
  <h1>The Cabinet <small>President → Officers → Agents → Skills → Workflows · live structure from cabinet.yaml</small></h1>
  <div class="legend" id="legend"></div>
  <div class="toolbar" id="toolbar"></div>
</header>

<div class="scroller">
  <div id="pyramid">
    <svg id="lines"></svg>
    <svg id="xlines"></svg>
    <svg id="glines"></svg>
    <div id="sug-handles"></div>
    <div id="gap-handles"></div>
    <div class="tier president"><div class="tier-label" style="top:14px">President</div>
      <div class="pres-node" id="pres"><div class="t">Steve</div><div class="s">Chief Executive · steve@designerwallcoverings.com</div></div>
    </div>
    <div class="tier officers" id="officers"><div class="tier-label" style="top:0">Officers · Agents · Skills</div></div>
    <div class="tier tier-workflows" id="wftier">
      <div class="tier-label" style="top:14px">Workflows</div>
      <div class="wf-band"><div class="wf-title">Recurring orchestration &amp; council workflows</div><div class="wf-row" id="wfrow"></div></div>
    </div>
  </div>
</div>

<div class="timeline-wrap">
  <h2>Daily timeline — when the fleet fires</h2>
  <div class="sub">Real launchd / cron cadence. ● = point-in-day job · ▭ = interval job (runs all day). Colored by owning officer.</div>
  <div id="tl"></div>
</div>

<div class="foot">
  Source of truth: <code>~/Projects/agent-cabinet/cabinet.yaml</code> · regenerate with the <code>agent-architect</code> agent.
  Agents = subagents an officer delegates to · Skills = leaf skill bundles · Workflows = multi-agent orchestrations.
  <b style="color:#86efac">Click any node</b> to open its source &amp; run it.
</div>

<div id="drawer">
  <div class="dr-head">
    <span class="dr-kind" id="dr-kind">SKILL</span>
    <span id="dr-title">—</span>
    <button class="dr-x" onclick="closePanel()">✕</button>
  </div>
  <div class="dr-actions">
    <button class="dr-btn run" id="dr-run">▶ Run in new session</button>
    <button class="dr-btn open" id="dr-open">📂 Open in editor</button>
  </div>
  <div id="dr-body"></div>
</div>
<div id="toast"></div>
<div id="modal" class="modal-overlay"><div class="modal-box"><button class="modal-x" onclick="closeModal()">✕</button><div id="modal-content" class="m-pad"></div></div></div>
<button id="clear-overlay" onclick="clearOverlays()">⊘ Clear overlay</button>

<script>
const OFF = [
  { id:'vp-operations', nm:'VP Operations', v:'op', col:'#60a5fa',
    dm:'Infra · monitoring · secrets · domains · DNS · watchdogs',
    agents:['domain-name-agent','onboard-domain-agent','secrets-manager','pm2-crash-watcher','deployment-engineer','devops-engineer','analytics-agent'],
    skills:['dw-yolo-loop','secrets','domain-setup','cloudflare-manager','open-firewalls','process-hawk'] },
  { id:'vp-dw-commerce', nm:'VP DW Commerce', v:'com', col:'#34d399',
    dm:'Catalog · vendor scrapers · Shopify · storefronts',
    agents:['aichat','front-page-steward'],
    skills:['dw-site-build','dw-shopify-theme-optimizer','dw-marketing-copy','dw-seo-optimizer','dw-purchasing-vendor','dw-sku-analyst','dw-legal-compliance','room-setting-generator','commercial-room-setting-generator','shopify-vendor-bulk-updater','weekend-csv-products','tiktok','instagram-post-template','instagram-account-manager','instagram-post-scheduler'],
    scrapers:['1838-scraper-manager','arteriors-scraper-manager','atomic50-scraper-manager','bnwalls-scraper-manager','bespoke-scraper-manager','brewster-scraper-manager','colefax-fowler-colorway-scraper','colouranddesign-scraper-manager','command54-scraper-manager','contrado-scraper-manager','cowtan-colorway-scraper','dedar-scraper-manager','denovowall-scraper-manager','designtex-scraper-manager','fabricut-scraper-manager','folia-scraper-manager','franquemont-scraper-manager','hygge-scraper-manager','jim-dultz-scraper-manager','justin-david-scraper','justindavid-scraper-manager','knoll-scraper-manager','maya-romanoff-scraper-manager','mindthegap-scraper-manager','muralsource-scraper-manager','naturale54-scraper-manager','novasuede-updater','osborne-scraper-manager','reid-witlin-scraper-manager','romo-scraper-manager','scalamandre-catalog-scraper','schumacher-dwsw-updater','stout-scraper-manager','timorous-scraper-manager','wallquest-scraper-manager','york-scraper-manager','york-wallcoverings-scraper-manager','arc-com-colorway-scraper','arte-international-colorway-scraper'] },
  { id:'vp-dw-marketing', nm:'VP DW Marketing', v:'mkt', col:'#f472b6',
    dm:'Command center · social · video · creative · ad-intel · GA4',
    agents:['dw-instagram','dw-tiktok','dw-linkedin','dw-facebook','dw-pinterest','dw-youtube','dw-x-twitter','seo-analyzer','analytics-agent','product-strategist'],
    skills:['dw-marketing-copy','dw-seo-optimizer','reels-producer','reviewed-demo-video','app-demo-video','what-landed-video','session-debrief','video-gallery','room-setting-generator','commercial-room-setting-generator','canvas-design','four-horsemen','four-horsemen-ui-orchestrator','stampede','logo-agent','ad-social-tracker','advertising-signals','collection-creator','new-arrivals-rotator','pairs-well-with','dw-vendor-landing'] },
  { id:'vp-directories', nm:'VP Directories', v:'dir', col:'#fbbf24',
    dm:'Lawyer · doctor · NPH · animals · lacountyeats · ventura',
    agents:['lawyer-build-agent','doctor-agent','la-research-agent'],
    skills:['ad-social-tracker','advertising-signals'] },
  { id:'vp-engineering', nm:'VP Engineering', v:'eng', col:'#a78bfa',
    dm:'Backend · frontend · DB · review · security · perf · test',
    agents:['backend-architect','frontend-developer','fullstack-developer','database-architect','database-optimizer','api-documenter','code-reviewer','architect-review','security-auditor','performance-engineer','test-engineer','error-detective','debugger','javascript-pro','typescript-pro','python-pro','ios-developer','mobile-developer','ai-engineer','prompt-engineer','mcp-expert'],
    skills:['dw-agent-builder','prompt-cache-optimization','claude-api','simplify','review','refactor-agent','mcp-builder'] },
  { id:'vp-research-content', nm:'VP Research & Content', v:'res', col:'#22d3ee',
    dm:'Web research · LA records · video · mockups · design · voice',
    agents:['exa-agent','search-specialist','technical-researcher','data-analyst','data-scientist','website-analysis','seo-analyzer','ui-ux-designer','graphic-designer','product-strategist','debate-team-fast','task-decomposition-expert'],
    skills:['site-audit','peer-survey','competitors','mockups','tools-pack','info-hub','extension','reviewed-demo-video','app-demo-video','session-debrief','video-gallery','clone-voice','use-voice','reels-producer','four-horsemen','four-horsemen-ui-orchestrator','stampede','canvas-design','artifacts-builder','prism','gamify','claude-control-center'] },
  { id:'vp-compliance-policy', nm:'VP Compliance & Policy', v:'cmp', col:'#fb7185',
    dm:'Outbound-comms compliance · pre-flight · legal posture',
    agents:['comms-compliance','best-practices-reviewer'],
    skills:['best-practices'] },
];

const WF = [
  { n:'officer-idea-council', d:'7 officers brainstorm → top-5', t:'daily 7:00am', col:'#cbd5e1' },
  { n:'coordinating-agents', d:'morning standup + cross-help', t:'daily 6:45am', col:'#cbd5e1' },
  { n:'officer-council-replies', d:'apply Steve’s emailed decisions', t:'few ×/day', col:'#cbd5e1' },
  { n:'george-reply-ingest', d:'reply-to-act on any Claude email', t:'every 20 min', col:'#60a5fa' },
  { n:'idea-loop', d:'generate + debate-score ideas', t:'every 30 min', col:'#22d3ee' },
  { n:'morning-review', d:'overnight findings approve panel', t:'daily 8:30am', col:'#22d3ee' },
  { n:'fleet-refactor-sweep', d:'parallel code-health pass', t:'on demand', col:'#a78bfa' },
  { n:'deep-research', d:'fan-out web research + verify', t:'on demand', col:'#22d3ee' },
];

// Daily timeline jobs. start/end in fractional hours (0-24). type pt|interval.
const JOBS = [
  { lab:'dw-uptime-probe', owner:'operations', col:'#60a5fa', type:'interval', every:'15 min' },
  { lab:'dw-canary-meta-watchdog', owner:'operations', col:'#60a5fa', type:'interval', every:'hourly' },
  { lab:'george-reply-ingest', owner:'operations', col:'#60a5fa', type:'interval', every:'20 min' },
  { lab:'idea-loop', owner:'research', col:'#22d3ee', type:'interval', every:'30 min' },
  { lab:'dw-leak-guard', owner:'commerce', col:'#34d399', type:'pt', at:3.5, note:'nightly leak scan' },
  { lab:'dw-backup-canary', owner:'operations', col:'#60a5fa', type:'pt', at:4.0, note:'Sun 04:00 weekly' },
  { lab:'dw-scraper-canary', owner:'commerce', col:'#34d399', type:'pt', at:5.0, note:'nightly 05:00' },
  { lab:'dw-map-auditor', owner:'commerce', col:'#34d399', type:'pt', at:5.5, note:'daily 05:30' },
  { lab:'dw-leak-scan (fleet)', owner:'compliance', col:'#fb7185', type:'pt', at:6.0, note:'Mon 06:00 weekly' },
  { lab:'sample-split-resume', owner:'commerce', col:'#34d399', type:'pt', at:6.25, note:'daily 06:15' },
  { lab:'coordinating-agents', owner:'cabinet', col:'#cbd5e1', type:'pt', at:6.75, note:'standup 06:45' },
  { lab:'officer-idea-council', owner:'cabinet', col:'#cbd5e1', type:'pt', at:7.0, note:'daily 07:00' },
  { lab:'morning-review', owner:'research', col:'#22d3ee', type:'pt', at:8.5, note:'daily 08:30' },
];

const childRefs = []; // {childEl, parentEl, kind}
const allNodes = [];  // {el, kind, id, officer}
let SHARED = [];
let SUG_STATE = {};   // key -> 'approved' | 'removed'
const sKey = (g) => g.kind+':'+g.id+':'+g.from+':'+g.to;

// Curated cross-officer SUGGESTIONS — a task that should ALSO live under another
// officer (drawn as amber arrows). Distinct from "shared now" which is computed
// from cabinet.yaml duplicates. {id, kind, from, to, reason}
const SUGGEST = [
  { id:'dw-legal-compliance', kind:'skill',    from:'vp-dw-commerce',      to:'vp-compliance-policy', reason:'Settlement / vendor-contract review is also a compliance gate' },
  { id:'security-auditor',    kind:'subagent', from:'vp-engineering',      to:'vp-compliance-policy', reason:'Security posture (OWASP/auth) overlaps policy & compliance' },
  { id:'best-practices',      kind:'skill',    from:'vp-compliance-policy',to:'vp-engineering',       reason:'Standing-rules pre-flight before any code / prod op' },
  { id:'la-research-agent',   kind:'subagent', from:'vp-directories',      to:'vp-research-content',  reason:'LA public-records work IS a core research capability' },
  { id:'analytics-agent',     kind:'subagent', from:'vp-operations',       to:'vp-research-content',  reason:'GA4 traffic data feeds site audits & competitor analysis' },
  { id:'comms-compliance',    kind:'subagent', from:'vp-compliance-policy',to:'vp-dw-marketing',      reason:'Every mailer / social send must clear CAN-SPAM first' },
  { id:'dw-vendor-landing',   kind:'skill',    from:'vp-dw-marketing',     to:'vp-dw-commerce',       reason:'Vendor landings are storefront surfaces commerce owns' },
  { id:'collection-creator',  kind:'skill',    from:'vp-dw-marketing',     to:'vp-dw-commerce',       reason:'Collection/merchandising lives in the Shopify catalog' },
  { id:'new-arrivals-rotator',kind:'skill',    from:'vp-dw-marketing',     to:'vp-dw-commerce',       reason:'Auto-rotating Shopify collections are catalog ops' },
  { id:'pairs-well-with',     kind:'skill',    from:'vp-dw-marketing',     to:'vp-dw-commerce',       reason:'Per-SKU recommendations are a product-page feature' },
  { id:'exa-agent',           kind:'subagent', from:'vp-research-content', to:'vp-directories',       reason:'Web research enriches directory listings' },
  { id:'logo-agent',          kind:'skill',    from:'vp-dw-marketing',     to:'vp-research-content',  reason:'Tournament brand-mark picker is a design/research tool' },
];

// ── Org GAPS (mirror server.js GAPS) — the structural gaps Steve flagged, drawn
// as red dotted lines you click to APPROVE. new-vp = stand up a whole officer;
// adopt-orphan = file an unregistered agent under an existing officer. Approving
// writes it into cabinet.yaml + auto-commits (git-reversible).
const GAPS = [
  { id:'vp-security', type:'new-vp', label:'VP Security', col:'#ef4444',
    domain:'Security · incident response · secrets · firewalls · monitoring',
    reason:'No single owner for security today — it is split across vp-engineering (security-auditor), vp-operations (secrets-manager, open-firewalls) and the orphaned security-dashboard (:9889) + Kamatera security-monitor.sh. This stands up one incident commander.',
    directors:[
      {kind:'subagent', name:'security-auditor', note:'cross-officer (primary: vp-engineering)'},
      {kind:'subagent', name:'secrets-manager', note:'cross-officer (primary: vp-operations)'},
      {kind:'subagent', name:'security-dashboard', note:'monitoring fleet :9889'},
      {kind:'skill', name:'open-firewalls', note:'cross-officer (primary: vp-operations)'},
      {kind:'skill', name:'secrets', note:'cross-officer (primary: vp-operations)'},
      {kind:'subagent', name:'comms-compliance', note:'cross-officer (primary: vp-compliance-policy)'},
    ] },
  { id:'vp-special-projects', type:'new-vp', label:'VP Special Projects', col:'#14b8a6',
    domain:'Non-DW / side / product-builder projects',
    reason:'wallpapersback.com, apartmentwallpaper, site-factory, small-business-builder and the Abrams portfolio have no officer — and their agents (wallpapersback-agent, apartment-wallpaper-agent, seam-debug-agent) are not even in the cabinet.',
    directors:[
      {kind:'subagent', name:'wallpapersback-agent', note:'wallpapersback.com storefront + gen pipeline'},
      {kind:'subagent', name:'apartment-wallpaper-agent', note:'apartmentwallpaper.com'},
      {kind:'subagent', name:'seam-debug-agent', note:'wallpapersback.com seam/joint healer'},
      {kind:'skill', name:'site-factory', note:'multi-site Next.js generator'},
      {kind:'skill', name:'small-business-builder', note:'SDCC analysis + rebuild'},
    ] },
  { id:'agent-architect',  type:'adopt-orphan', kind:'subagent', to:'vp-operations',  reason:'It owns cabinet.yaml + this viewer — belongs under Operations.' },
  { id:'abramstasks-agent', type:'adopt-orphan', kind:'subagent', to:'vp-operations',  reason:'Plan→parallel-Claudes orchestrator — an ops/orchestration utility.' },
  { id:'context-manager',  type:'adopt-orphan', kind:'subagent', to:'vp-engineering', reason:'Multi-agent context management — an engineering utility.' },
  { id:'dfm',              type:'adopt-orphan', kind:'subagent', to:'vp-operations',  reason:'Autonomous task executor — confirm purpose, file under Operations.' },
];
const gapKey = (g) => g.type==='new-vp' ? 'vp:'+g.id : g.kind+':'+g.id+':'+g.to;
let GAP_STATE = {};   // key -> 'approved' | 'removed'

function legend(){
  const L = document.getElementById('legend');
  OFF.forEach(o=>{
    const s=document.createElement('span'); s.className='lg';
    s.innerHTML = `<span class="dot" style="background:${o.col}"></span>${o.nm.replace('VP ','')}`;
    L.appendChild(s);
  });
  const k=document.createElement('span'); k.className='key';
  const totA = OFF.reduce((a,o)=>a+o.agents.length,0);
  const totS = OFF.reduce((a,o)=>a+o.skills.length+(o.scrapers?o.scrapers.length:0),0);
  k.innerHTML = `<span><b>7</b> officers</span><span><b>${totA}</b> agents</span><span><b>${totS}</b> skills</span><span><b>${WF.length}</b> workflows</span>`;
  L.appendChild(k);
}

function build(){
  const row = document.getElementById('officers');
  OFF.forEach(o=>{
    const col=document.createElement('div'); col.className='officer-col';
    const node=document.createElement('div'); node.className='officer-node'; node.style.setProperty('--oc',o.col);
    node.dataset.kind='subagent'; node.dataset.id=o.id; node.dataset.label=o.nm;
    const sc = o.scrapers?o.scrapers.length:0;
    node.innerHTML = `<div class="nm">${o.nm}</div><div class="dm">${o.dm}</div>
      <div class="ct">${o.agents.length} agents · ${o.skills.length+sc} skills</div>`;
    col.appendChild(node); o._node=node; allNodes.push({el:node,kind:'officer',id:o.id,officer:o.id});

    const aL=document.createElement('div'); aL.className='group-label'; aL.textContent='Agents'; col.appendChild(aL);
    const aWrap=document.createElement('div'); aWrap.className='agents';
    o.agents.forEach(a=>{ const e=document.createElement('div'); e.className='agent'; e.style.setProperty('--oc',o.col); e.textContent=a;
      e.dataset.kind='subagent'; e.dataset.id=a; e.dataset.label=a; aWrap.appendChild(e); childRefs.push({c:e,p:node,kind:'agent',col:o.col});
      allNodes.push({el:e,kind:'agent',id:a,officer:o.id}); });
    col.appendChild(aWrap);

    const sL=document.createElement('div'); sL.className='group-label'; sL.textContent='Skills'; col.appendChild(sL);
    const sWrap=document.createElement('div'); sWrap.className='skills';
    o.skills.forEach(s=>{ const e=document.createElement('div'); e.className='skill'; e.textContent=s;
      e.dataset.kind='skill'; e.dataset.id=s; e.dataset.label=s; sWrap.appendChild(e); childRefs.push({c:e,p:node,kind:'skill',col:o.col});
      allNodes.push({el:e,kind:'skill',id:s,officer:o.id}); });
    if(o.scrapers){ const e=document.createElement('div'); e.className='skill scr'; e.style.setProperty('--oc',o.col);
      e.textContent='Vendor Scrapers ×'+o.scrapers.length; e.title='Click to open all '+o.scrapers.length+' scraper skills';
      e.dataset.kind='scrapers'; e.dataset.scrapers=JSON.stringify(o.scrapers);
      sWrap.appendChild(e); childRefs.push({c:e,p:node,kind:'skill',col:o.col}); }
    col.appendChild(sWrap);

    row.appendChild(col);
    childRefs.push({c:node,p:document.getElementById('pres'),kind:'officer',col:o.col});
  });

  const wfrow=document.getElementById('wfrow');
  WF.forEach(w=>{ const e=document.createElement('div'); e.className='wf'; e.style.setProperty('--wc',w.col);
    e.dataset.kind='skill'; e.dataset.id=w.n; e.dataset.label=w.n;
    e.innerHTML=`<div class="wn">${w.n}</div><div class="wd">${w.d}</div><div class="wt">${w.t}</div>`; wfrow.appendChild(e); });

  buildGaps();
}

// Ghost officer columns (new-vp) appended to the officers row, + dashed orphan
// chips appended under their target officer. Both carry data-gap → openGap().
function buildGaps(){
  const row=document.getElementById('officers');
  GAPS.filter(g=>g.type==='new-vp').forEach(g=>{
    const col=document.createElement('div'); col.className='officer-col gap-col';
    const node=document.createElement('div'); node.className='officer-node gap-node'; node.style.setProperty('--oc',g.col);
    node.dataset.gap=gapKey(g);
    node.innerHTML=`<div class="gap-tag">PROPOSED</div><div class="nm">${g.label}</div><div class="dm">${g.domain}</div><div class="ct">${g.directors.length} directors · GAP</div>`;
    col.appendChild(node); g._node=node;
    const aL=document.createElement('div'); aL.className='group-label'; aL.textContent='Proposed directors'; col.appendChild(aL);
    const aWrap=document.createElement('div'); aWrap.className='agents';
    g.directors.forEach(d=>{ const e=document.createElement('div'); e.className=(d.kind==='skill'?'skill':'agent')+' gap-chip';
      e.style.setProperty('--oc',g.col); e.textContent=d.name; e.title=d.note||''; aWrap.appendChild(e); });
    col.appendChild(aWrap);
    row.appendChild(col);
  });
  GAPS.filter(g=>g.type==='adopt-orphan').forEach(g=>{
    const target=OFF.find(o=>o.id===g.to); if(!target||!target._node) return;
    const col=target._node.parentElement;
    let host=col.querySelector('.gap-orphans');
    if(!host){ host=document.createElement('div'); host.className='agents gap-orphans'; col.appendChild(host); }
    const e=document.createElement('div'); e.className='agent gap-chip orphan-chip'; e.style.setProperty('--oc',target.col);
    e.dataset.gap=gapKey(g); e.textContent=g.id; e.title='Orphan — click to adopt under '+target.nm;
    host.appendChild(e); g._chip=e; g._target=target;
  });
}

function center(el, root){
  const r=el.getBoundingClientRect(), b=root.getBoundingClientRect();
  return { x:r.left-b.left+r.width/2, y:r.top-b.top, yb:r.top-b.top+r.height, h:r.height };
}
function drawLines(){
  const root=document.getElementById('pyramid');
  const svg=document.getElementById('lines');
  svg.setAttribute('width', root.scrollWidth); svg.setAttribute('height', root.scrollHeight);
  svg.setAttribute('viewBox',`0 0 ${root.scrollWidth} ${root.scrollHeight}`);
  let html='';
  childRefs.forEach(ref=>{
    const c=center(ref.c,root), p=center(ref.p,root);
    const x1=p.x, y1=p.yb, x2=c.x, y2=c.y;            // parent bottom → child top
    const my=(y1+y2)/2;
    const op = ref.kind==='officer'?0.9 : ref.kind==='agent'?0.5 : 0.22;
    const w  = ref.kind==='officer'?1.8 : ref.kind==='agent'?1.1 : 0.7;
    html += `<path d="M${x1},${y1} C${x1},${my} ${x2},${my} ${x2},${y2}" fill="none" stroke="${ref.col}" stroke-width="${w}" stroke-opacity="${op}"/>`;
  });
  svg.innerHTML=html;
}

function timeline(){
  const W=860; // px for 24h track
  const tl=document.getElementById('tl');
  const axis=document.createElement('div'); axis.className='tl-axis tl';
  for(let h=0;h<=24;h+=2){ const t=document.createElement('div'); t.className='hr'; t.style.left=(h/24*W)+'px';
    t.innerHTML=`<span>${String(h).padStart(2,'0')}</span>`; axis.appendChild(t); }
  tl.appendChild(axis);
  JOBS.forEach(j=>{
    const lane=document.createElement('div'); lane.className='lane tl';
    const lab=document.createElement('div'); lab.className='lab';
    lab.innerHTML=`<span>${j.lab}</span><span class="o">${j.owner}</span><span class="d" style="background:${j.col}"></span>`;
    lane.appendChild(lab);
    if(j.type==='pt'){
      const m=document.createElement('div'); m.className='mark pt'; m.style.left=(j.at/24*W)+'px'; m.style.background=j.col;
      m.innerHTML=`<div class="tip">${j.lab} — ${j.note}</div>`; lane.appendChild(m);
    } else {
      const b=document.createElement('div'); b.className='bar'; b.style.left='0px'; b.style.width=W+'px'; b.style.background=j.col;
      b.innerHTML=`<div class="tip" style="left:60px">${j.lab} — every ${j.every}</div>`; lane.appendChild(b);
    }
    tl.appendChild(lane);
  });
}

// ── Runnable / openable: click a node → drawer with source + Run + Open ──
let _toastT;
function toast(msg){ const t=document.getElementById('toast'); t.textContent=msg; t.classList.add('show'); clearTimeout(_toastT); _toastT=setTimeout(()=>t.classList.remove('show'),2600); }
// ── Drawer focus management (P0): remember what opened the drawer, focus the
// primary action on open, restore focus to the trigger on close, trap Tab. ──
let _drawerReturn=null, _drawerWasOpen=false;
function closePanel(){
  const dr=document.getElementById('drawer'); dr.classList.remove('open');
  // focus restore is handled centrally by the MutationObserver below
}
// Disable both drawer actions while an approve/dismiss write is in flight so a
// double-click can't fire two cabinet.yaml writes (P0). Re-render re-enables.
function setDrawerBusy(busy,label){
  const run=document.getElementById('dr-run'), open=document.getElementById('dr-open');
  [run,open].forEach(b=>{ if(b) b.disabled=busy; });
  if(!run) return;
  if(busy){ if(run._prevTxt==null) run._prevTxt=run.textContent; run.textContent=label||'Working…'; }
  else if(run._prevTxt!=null){ run.textContent=run._prevTxt; run._prevTxt=null; }
}
async function runNode(kind,id){
  toast('Launching '+(kind==='skill'?'/':'@')+id+' …');
  try{ const r=await(await fetch('/api/run?kind='+kind+'&name='+encodeURIComponent(id))).json();
    toast(r.ok?('▶ '+id+' — running in a new terminal tab'):('Run failed: '+(r.error||'?'))); }
  catch(e){ toast('Run failed — is the server (:9766) up & are you on http not file://?'); }
}
async function openNode(kind,id){
  try{ const r=await(await fetch('/api/open?kind='+kind+'&name='+encodeURIComponent(id))).json();
    toast(r.ok?('📂 opened '+id+' in editor'):(r.error||'no on-disk source')); }
  catch(e){ toast('Open failed — server down?'); }
}
async function openPanel(kind,id,label){
  const dr=document.getElementById('drawer'); dr.classList.add('open');
  document.getElementById('dr-title').textContent=label||id;
  const k=document.getElementById('dr-kind'); k.textContent=kind==='skill'?'SKILL':'AGENT'; k.className='dr-kind '+(kind==='skill'?'k-skill':'k-agent');
  const run=document.getElementById('dr-run'), open=document.getElementById('dr-open');
  run.disabled=false; open.disabled=false; run._prevTxt=null;
  run.style.display=''; open.style.display='';
  run.textContent='▶ Run in new session'; open.textContent='📂 Open in editor';
  run.onclick=()=>runNode(kind,id); open.onclick=()=>openNode(kind,id);
  const body=document.getElementById('dr-body'); body.innerHTML='<div class="dr-load">Loading source…</div>';
  try{
    const r=await(await fetch('/api/source?kind='+kind+'&name='+encodeURIComponent(id))).json();
    if(r.exists){
      body.innerHTML='<div class="dr-path">'+r.path+'</div><pre class="dr-src"></pre>';
      body.querySelector('.dr-src').textContent=r.content||'(empty)';
    } else {
      body.innerHTML='<div class="dr-note">No on-disk source found — likely a plugin/MCP skill or a generic library agent. You can still <b>Run</b> it.</div><div class="dr-path">looked: '+r.path+'</div>';
    }
  }catch(e){ body.innerHTML='<div class="dr-note">Couldn’t reach the server. Open this page at <b>http://localhost:9766/pyramid</b> (not file://) so Run/Open work.</div>'; }
}
function openScrapers(list){
  const dr=document.getElementById('drawer'); dr.classList.add('open');
  document.getElementById('dr-title').textContent='Vendor Scrapers ×'+list.length;
  const k=document.getElementById('dr-kind'); k.textContent='SKILLS'; k.className='dr-kind k-skill';
  document.getElementById('dr-run').style.display='none'; document.getElementById('dr-open').style.display='none';
  const body=document.getElementById('dr-body');
  body.innerHTML='<div class="dr-note">Each vendor scraper is its own runnable skill.</div>'+
    list.map(n=>'<div class="scr-row"><span class="scr-n">/'+n+'</span><span>'+
      '<button class="mini" onclick="runNode(\'skill\',\''+n+'\')">▶ Run</button>'+
      '<button class="mini" onclick="openNode(\'skill\',\''+n+'\')">📂 Open</button></span></div>').join('');
}
function onNodeClick(e){
  const el=e.target.closest('[data-kind]'); if(!el) return;
  const kind=el.dataset.kind;
  if(kind==='scrapers'){ openScrapers(JSON.parse(el.dataset.scrapers||'[]')); return; }
  openPanel(kind, el.dataset.id, el.dataset.label);
}

// ── Cross-officer analysis: SHARED-now (computed) + SUGGESTED (curated) ──
function hashHue(s){ let h=0; for(let i=0;i<s.length;i++) h=(h*31+s.charCodeAt(i))>>>0; return h%360; }
function midC(el,root){ const c=center(el,root); return { x:c.x, y:c.y + c.h/2 }; }
function computeShared(){
  const byId={};
  allNodes.forEach(n=>{ if(n.kind!=='agent'&&n.kind!=='skill') return; (byId[n.id]=byId[n.id]||[]).push(n); });
  const out=[];
  Object.keys(byId).forEach(id=>{ const insts=byId[id]; const offs=[...new Set(insts.map(x=>x.officer))]; if(offs.length>1) out.push({id,insts,officers:offs}); });
  return out.sort((a,b)=>b.officers.length-a.officers.length);
}
function drawCross(){
  const root=document.getElementById('pyramid'), svg=document.getElementById('xlines');
  svg.setAttribute('width',root.scrollWidth); svg.setAttribute('height',root.scrollHeight); svg.setAttribute('viewBox',`0 0 ${root.scrollWidth} ${root.scrollHeight}`);
  let sh='', sg='', hh='';
  SHARED.forEach(s=>{
    const pts=s.insts.map(n=>midC(n.el,root)).sort((a,b)=>a.x-b.x);
    const hue=hashHue(s.id);
    let d=`M${pts[0].x},${pts[0].y}`;
    for(let i=1;i<pts.length;i++){ const mx=(pts[i-1].x+pts[i].x)/2, top=Math.min(pts[i-1].y,pts[i].y)-46; d+=` Q${mx},${top} ${pts[i].x},${pts[i].y}`; }
    sh+=`<path d="${d}" fill="none" stroke="hsl(${hue} 82% 66%)" stroke-width="1.4" stroke-opacity=".9" stroke-dasharray="5 4"/>`;
    pts.forEach(p=>{ sh+=`<circle cx="${p.x}" cy="${p.y}" r="2.6" fill="hsl(${hue} 82% 66%)"/>`; });
  });
  SUGGEST.forEach(g=>{
    const key=sKey(g), dec=SUG_STATE[key];
    if(dec==='removed') return;
    const src=allNodes.find(n=>n.id===g.id && n.officer===g.from);
    const dst=OFF.find(o=>o.id===g.to);
    if(!src||!dst||!dst._node) return;
    const a=midC(src.el,root), b=center(dst._node,root), bx=b.x, by=b.y, my=(a.y+by)/2;
    const d=`M${a.x},${a.y} C${a.x},${my} ${bx},${my} ${bx},${by}`;
    const ap=dec==='approved';
    sg+=`<path d="${d}" fill="none" stroke="${ap?'#34d399':'#fbbf24'}" stroke-width="${ap?1.9:1.3}" stroke-opacity="${ap?.95:.82}" ${ap?'':'stroke-dasharray="1.5 4.5"'} marker-end="url(#${ap?'arrG':'arr'})"/>`;
    sg+=`<path class="sug-hit" data-key="${key}" d="${d}" fill="none" stroke="transparent" stroke-width="16" pointer-events="stroke" style="cursor:pointer"/>`;
    const hx=(a.x+bx)/2, hy=(a.y+by)/2;
    hh+=`<button class="sug-h${ap?' approved':''}" data-key="${key}" data-act="${ap?'Approved ✓':'Review move'}" style="left:${hx}px;top:${hy}px" title="${offName(g.from)} → ${offName(g.to)}  ·  ${g.kind==='skill'?'/':'@'}${g.id}  (click to review / approve)">${ap?'✓':'◆'}</button>`;
  });
  svg.innerHTML=`<defs>`+
    `<marker id="arr" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#fbbf24"/></marker>`+
    `<marker id="arrG" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#34d399"/></marker>`+
    `</defs><g id="g-shared">${sh}</g><g id="g-suggest">${sg}</g>`;
  const hc=document.getElementById('sug-handles'); if(hc) hc.innerHTML=hh;
  applyToggles();
}
// ── Gaps: red dotted lines (President→ghost officer, or orphan chip→officer) ──
function drawGaps(){
  const root=document.getElementById('pyramid'), svg=document.getElementById('glines');
  svg.setAttribute('width',root.scrollWidth); svg.setAttribute('height',root.scrollHeight); svg.setAttribute('viewBox',`0 0 ${root.scrollWidth} ${root.scrollHeight}`);
  const pres=document.getElementById('pres');
  let gp='', hh='';
  GAPS.forEach(g=>{
    const key=gapKey(g), dec=GAP_STATE[key];
    if(dec==='removed') return;
    const ap=dec==='approved';
    let a,b;
    if(g.type==='new-vp'){
      if(!g._node) return;
      g._node.classList.toggle('gap-approved',ap);
      const tag=g._node.querySelector('.gap-tag'); if(tag) tag.textContent=ap?'APPROVED':'PROPOSED';
      const p=center(pres,root), c=center(g._node,root); a={x:p.x,y:p.yb}; b={x:c.x,y:c.y};
    } else {
      if(!g._chip||!g._target||!g._target._node) return;
      g._chip.classList.toggle('gap-approved',ap);
      const s=center(g._chip,root), t=center(g._target._node,root); a={x:s.x,y:s.y+s.h/2}; b={x:t.x,y:t.yb};
    }
    const my=(a.y+b.y)/2, col=ap?'#34d399':'#f87171';
    gp+=`<path d="M${a.x},${a.y} C${a.x},${my} ${b.x},${my} ${b.x},${b.y}" fill="none" stroke="${col}" stroke-width="${ap?1.9:1.4}" stroke-opacity="${ap?.95:.85}" ${ap?'':'stroke-dasharray="2 5"'} marker-end="url(#${ap?'gapArrG':'gapArr'})"/>`;
    gp+=`<path class="gap-hit" data-key="${key}" d="M${a.x},${a.y} C${a.x},${my} ${b.x},${my} ${b.x},${b.y}" fill="none" stroke="transparent" stroke-width="16" pointer-events="stroke"/>`;
    const hx=(a.x+b.x)/2, hy=(a.y+b.y)/2;
    const ttl=g.type==='new-vp'?('Stand up '+g.label):('Adopt @'+g.id+' → '+g._target.nm);
    const act=ap?'Approved ✓':'Approve gap';
    hh+=`<button class="gap-h${ap?' approved':''}" data-key="${key}" data-act="${act}" style="left:${hx}px;top:${hy}px" title="${ttl}  (click to approve / dismiss)">${ap?'✓':'+'}</button>`;
  });
  svg.innerHTML=`<defs>`+
    `<marker id="gapArr" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#f87171"/></marker>`+
    `<marker id="gapArrG" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#34d399"/></marker>`+
    `</defs><g id="g-gaps">${gp}</g>`;
  const ghc=document.getElementById('gap-handles'); if(ghc) ghc.innerHTML=hh;
  applyToggles();
}
function applyToggles(){
  const on=id=>{ const e=document.getElementById(id); return e?e.checked:true; };
  document.getElementById('lines').style.display = on('tg-hier')?'':'none';
  const gs=document.getElementById('g-shared'), gg=document.getElementById('g-suggest');
  if(gs) gs.style.display = on('tg-shared')?'':'none';
  if(gg) gg.style.display = on('tg-sug')?'':'none';
  const hc=document.getElementById('sug-handles'); if(hc) hc.style.display = on('tg-sug')?'':'none';
  const gx=document.getElementById('g-gaps'); if(gx) gx.style.display = on('tg-gap')?'':'none';
  const ghc=document.getElementById('gap-handles'); if(ghc) ghc.style.display = on('tg-gap')?'':'none';
}
// Calmer default — Hierarchy + Gaps on, Shared + Suggested overlays off until the
// user opts in. Persisted to localStorage so the choice survives reload.
const TOGGLE_DEFAULTS={'tg-hier':true,'tg-shared':false,'tg-sug':false,'tg-gap':true};
const TOGGLE_LS='cabinet.toggles';
function loadToggles(){ try{ const s=JSON.parse(localStorage.getItem(TOGGLE_LS)||'{}'); return {...TOGGLE_DEFAULTS,...s}; }catch(e){ return {...TOGGLE_DEFAULTS}; } }
function saveToggles(){ const o={}; ['tg-hier','tg-shared','tg-sug','tg-gap'].forEach(id=>{ const e=document.getElementById(id); if(e) o[id]=e.checked; }); try{ localStorage.setItem(TOGGLE_LS,JSON.stringify(o)); }catch(e){} }
function buildToolbar(){
  SHARED=computeShared();
  const t=loadToggles(), ck=id=>t[id]?' checked':'';
  document.getElementById('toolbar').innerHTML =
    `<label><input type="checkbox" id="tg-hier"${ck('tg-hier')}> Hierarchy</label>`+
    `<label><input type="checkbox" id="tg-shared"${ck('tg-shared')}><span class="tog-line" style="border-top:2px dashed #cbd5e1"></span>Shared now (${SHARED.length})</label>`+
    `<label><input type="checkbox" id="tg-sug"${ck('tg-sug')}><span class="tog-line" style="border-top:2px dotted #fbbf24"></span>Suggested moves (${SUGGEST.length})</label>`+
    `<label><input type="checkbox" id="tg-gap"${ck('tg-gap')}><span class="tog-line" style="border-top:2px dotted #f87171"></span>Gaps (${GAPS.length})</label>`+
    `<button class="tb-btn" onclick="openIdeasGallery()">💡 Ideas</button>`+
    `<button class="tb-btn" style="margin-left:0" onclick="location.href='/activity'">🕓 Activity</button>`+
    `<button class="tb-btn" style="margin-left:0" onclick="showAnalysis()">▤ List</button>`;
  ['tg-hier','tg-shared','tg-sug','tg-gap'].forEach(id=>document.getElementById(id).addEventListener('change',()=>{ saveToggles(); applyToggles(); }));
}
function offName(id){ const o=OFF.find(x=>x.id===id); return o?o.nm.replace('VP ',''):id; }
function showAnalysis(){
  const dr=document.getElementById('drawer'); dr.classList.add('open');
  document.getElementById('dr-title').textContent='Cross-officer analysis';
  const k=document.getElementById('dr-kind'); k.textContent='SHARES'; k.className='dr-kind k-agent';
  document.getElementById('dr-run').style.display='none'; document.getElementById('dr-open').style.display='none';
  let h=`<div class="dr-note"><b style="color:#cbd5e1">Shared now (${SHARED.length})</b> — already wired to ≥2 officers (dashed colored lines). A task serving many officers.</div>`;
  h+=SHARED.map(s=>`<div class="scr-row"><span class="scr-n" style="color:hsl(${hashHue(s.id)} 82% 70%)">${s.id}</span><span style="font-size:10.5px;color:#8a94a6;text-align:right">${s.officers.map(offName).join(' · ')}</span></div>`).join('');
  h+=`<div class="dr-note" style="margin-top:16px"><b style="color:#fbbf24">Suggested moves (${SUGGEST.length})</b> — should also belong to another officer (amber arrows).</div>`;
  h+=SUGGEST.map(g=>{ const key=sKey(g), dec=SUG_STATE[key], sc=dec==='approved'?'#86efac':dec==='removed'?'#fca5a5':'#fbbf24';
    return `<div class="scr-row" style="display:block"><div><span class="scr-n">${g.kind==='skill'?'/':'@'}${g.id}</span> <span style="color:#fbbf24;font-size:10.5px">${offName(g.from)} → ${offName(g.to)}</span>${dec?` <span style="color:${sc};font-size:10px">● ${dec}</span>`:''}</div><div style="font-size:10.5px;color:#8a94a6;margin:3px 0;line-height:1.4">${g.reason}</div><div><button class="mini" onclick="decide('${key}','approve')">✓ Approve</button><button class="mini" onclick="decide('${key}','remove')">✕ Remove</button><button class="mini" onclick="decide('${key}','reset')">↺</button></div></div>`; }).join('');
  document.getElementById('dr-body').innerHTML=h;
}

// ── Click a dotted suggestion line → relationship + Approve / Remove ──
async function loadSugState(){ try{ const r=await(await fetch('/api/suggestions')).json(); if(r&&r.decisions) SUG_STATE=r.decisions; }catch(e){} }
function openSuggestion(key){
  const g=SUGGEST.find(x=>sKey(x)===key); if(!g) return;
  const dec=SUG_STATE[key];
  document.getElementById('drawer').classList.add('open');
  document.getElementById('dr-title').textContent=(g.kind==='skill'?'/':'@')+g.id;
  const k=document.getElementById('dr-kind'); k.textContent=dec?dec.toUpperCase():'SUGGESTED';
  k.className='dr-kind'; k.style.cssText=dec==='approved'?'background:#0f2a1c;color:#86efac;border:1px solid #16a34a':'background:#33280a;color:#fbbf24;border:1px solid #fbbf24';
  const run=document.getElementById('dr-run'), open=document.getElementById('dr-open');
  run.disabled=false; open.disabled=false; run._prevTxt=null;
  run.style.display=''; open.style.display='';
  run.className='dr-btn run'; run.textContent=dec==='approved'?'✓ Approved':'✓ Approve';
  open.className='dr-btn'; open.style.cssText='flex:1;display:flex;align-items:center;justify-content:center;gap:7px;padding:10px;border-radius:9px;font-size:13px;font-weight:700;cursor:pointer;background:#2a1414;color:#fca5a5;border:1px solid #b91c1c';
  open.textContent=dec==='removed'?'✕ Removed':'✕ Remove';
  run.onclick=()=>decide(key,'approve'); open.onclick=()=>decide(key,'remove');
  document.getElementById('dr-body').innerHTML=
    `<div class="rel-flow"><span class="rel-end">${offName(g.from)}</span><span class="rel-mid">${g.kind==='skill'?'/':'@'}${g.id} should also serve →</span><span class="rel-end rel-to">${offName(g.to)}</span></div>`+
    `<div class="dr-note" style="margin-top:8px">${g.reason}</div>`+
    (dec?`<div style="margin-top:10px;font-size:12px;color:${dec==='approved'?'#86efac':'#fca5a5'}">Status: <b>${dec}</b>${dec==='approved'?' — added under '+offName(g.to)+' in cabinet.yaml':''} &nbsp;<button class="mini" onclick="decide('${key}','reset')">↺ Reset</button></div>`:'')+
    `<div class="dr-note" style="margin-top:14px;font-size:11px">Approve appends <code>- ${g.kind}: ${g.id}</code> under <b>${g.to}</b>'s directors in cabinet.yaml (idempotent, git-reversible). Remove just dismisses it.</div>`;
}
async function decide(key,action){
  setDrawerBusy(true, action==='approve'?'Approving…':action==='remove'?'Removing…':'Resetting…');
  try{
    const r=await(await fetch('/api/suggest?action='+action+'&key='+encodeURIComponent(key))).json();
    if(!r.ok){ toast('failed: '+(r.error||'?')); setDrawerBusy(false); return; }
    SUG_STATE=r.decisions||SUG_STATE; drawCross();
    const g=SUGGEST.find(x=>sKey(x)===key)||{};
    toast(action==='approve'?(r.cabinet&&r.cabinet.already?'already in cabinet.yaml':('✓ approved — committed '+(r.commit||'(no hash)')+' · added under '+offName(g.to))):action==='remove'?'✕ suggestion removed':'↺ decision reset');
    if(document.getElementById('dr-title').textContent==='Cross-officer analysis') showAnalysis();
    else if(action==='remove') closePanel();
    else openSuggestion(key);
  }catch(e){ toast('server unreachable — open via http://localhost:9766/pyramid'); setDrawerBusy(false); }
}

// ── Click a red dotted gap line / + handle → drawer with Approve / Dismiss ──
async function loadGapState(){ try{ const r=await(await fetch('/api/gaps')).json(); if(r&&r.decisions) GAP_STATE=r.decisions; }catch(e){} }
function openGap(key){
  const g=GAPS.find(x=>gapKey(x)===key); if(!g) return;
  const dec=GAP_STATE[key];
  document.getElementById('drawer').classList.add('open');
  document.getElementById('dr-title').textContent = g.type==='new-vp'?g.label:('@'+g.id);
  const k=document.getElementById('dr-kind'); k.textContent=dec?dec.toUpperCase():'GAP'; k.className='dr-kind';
  k.style.cssText=dec==='approved'?'background:#0f2a1c;color:#86efac;border:1px solid #16a34a':'background:#2a1414;color:#fca5a5;border:1px solid #b91c1c';
  const run=document.getElementById('dr-run'), open=document.getElementById('dr-open');
  run.disabled=false; open.disabled=false; run._prevTxt=null;
  run.style.display=''; open.style.display='';
  run.className='dr-btn run'; run.textContent=dec==='approved'?'✓ Approved':(g.type==='new-vp'?'✓ Stand up officer':'✓ Adopt orphan');
  open.className='dr-btn'; open.style.cssText='flex:1;display:flex;align-items:center;justify-content:center;gap:7px;padding:10px;border-radius:9px;font-size:13px;font-weight:700;cursor:pointer;background:#2a1414;color:#fca5a5;border:1px solid #b91c1c';
  open.textContent=dec==='removed'?'✕ Dismissed':'✕ Dismiss';
  run.onclick=()=>decideGap(key,'approve'); open.onclick=()=>decideGap(key,'remove');
  let body;
  if(g.type==='new-vp'){
    body=`<div class="rel-flow"><span class="rel-end">Steve</span><span class="rel-mid">stand up new officer →</span><span class="rel-end rel-to">${g.label}</span></div>`+
      `<div class="dr-note" style="margin-top:8px">${g.reason}</div>`+
      `<div class="dr-note" style="margin-top:10px"><b style="color:#fca5a5">${g.directors.length} directors</b><br>${g.directors.map(d=>(d.kind==='skill'?'/':'@')+d.name+(d.note?' <span style="color:#6b7280">— '+d.note+'</span>':'')).join('<br>')}</div>`+
      `<div class="dr-note" style="margin-top:12px;font-size:11px">Approve inserts a full <code>- vp: ${g.id}</code> block (domain, triggers, ${g.directors.length} directors) into cabinet.yaml before <code>architecture_principles</code> — idempotent, git-reversible. The Mermaid view at <code>/</code> then shows it as a full officer.</div>`;
  } else {
    body=`<div class="rel-flow"><span class="rel-end">orphan</span><span class="rel-mid">@${g.id} → adopt under</span><span class="rel-end rel-to">${offName(g.to)}</span></div>`+
      `<div class="dr-note" style="margin-top:8px">${g.reason}</div>`+
      `<div class="dr-note" style="margin-top:12px;font-size:11px">Approve appends <code>- ${g.kind}: ${g.id}</code> under <b>${g.to}</b>'s directors in cabinet.yaml — idempotent, git-reversible.</div>`;
  }
  if(dec) body+=`<div style="margin-top:10px;font-size:12px;color:${dec==='approved'?'#86efac':'#fca5a5'}">Status: <b>${dec}</b>${dec==='approved'?' — written to cabinet.yaml':''} <button class="mini" onclick="decideGap('${key}','reset')">↺ Reset</button></div>`;
  document.getElementById('dr-body').innerHTML=body;
}
async function decideGap(key,action){
  setDrawerBusy(true, action==='approve'?'Approving…':action==='remove'?'Dismissing…':'Resetting…');
  try{
    const r=await(await fetch('/api/gap?action='+action+'&key='+encodeURIComponent(key))).json();
    if(!r.ok){ toast('failed: '+(r.error||'?')); setDrawerBusy(false); return; }
    GAP_STATE=r.decisions||GAP_STATE; drawGaps();
    toast(action==='approve'?(r.cabinet&&r.cabinet.already?'already in cabinet.yaml':('✓ approved — committed '+(r.commit||'(no hash)'))):action==='remove'?'✕ gap dismissed':'↺ decision reset');
    if(action==='remove') closePanel(); else openGap(key);  // re-render re-enables the buttons
  }catch(e){ toast('server unreachable — open via http://localhost:9766/pyramid'); setDrawerBusy(false); }
}

// ───────────────────── IDEAS MODALS ─────────────────────
function openModal(html){ document.getElementById('modal-content').innerHTML=html; document.getElementById('modal').classList.add('open'); }
function closeModal(){ document.getElementById('modal').classList.remove('open'); }
const IDEAS=[
  {icon:'🔎', title:'Task Router — “Who handles this?”', live:true, render:'router', pitch:'Type any task; route it to the right officer → agent → skill, with one-click Run.'},
  {icon:'🧬', title:'Consolidation Advisor', live:true, render:'consol', pitch:'Surface skills shared across officers and recommend folds — per the cabinet’s own architecture rules.'},
  {icon:'📤', title:'Org Snapshot Export', live:true, render:'export', pitch:'Download the whole cabinet as JSON + Markdown for docs, diffs and sharing.'},
  {icon:'🩺', title:'Live Health Overlay', live:true, render:'health', pitch:'Tint each officer node by its domain’s current uptime / canary verdict — see what’s on fire at a glance.'},
  {icon:'💸', title:'Cost / Usage Heatmap', live:true, render:'cost', pitch:'Tint nodes & rank apps by $ from the cost-ledger so the hot (and dead) spenders pop.'},
];
function openIdeasGallery(){
  const cards=IDEAS.map((id,i)=>`<div class="idea-card" onclick="openIdea(${i})"><div class="ic">${id.icon}</div><div class="t">${id.title}</div><div class="p">${id.pitch}</div><span class="tag ${id.live?'live':'prop'}">${id.live?'● LIVE':'◆ PROPOSAL'}</span></div>`).join('');
  openModal(`<h2 class="m-h">💡 Ideas for the Cabinet viewer</h2><div class="m-sub">Five upgrades. The LIVE ones work right now; PROPOSALS can be queued to your CNCP parking-lot.</div><div class="idea-grid">${cards}</div>`);
}
function ideaFooter(i){ return `<div class="m-actions"><button class="m-btn ghost" onclick="openIdeasGallery()">← Ideas</button><button class="m-btn queue" onclick="queueIdea(${i})">➕ Queue to CNCP</button><button class="m-btn ghost" style="margin-left:auto" onclick="closeModal()">Close</button></div>`; }
function openIdea(i){
  const id=IDEAS[i]; let body='';
  if(id.render==='router') body=routerHTML();
  else if(id.render==='consol') body=consolHTML();
  else if(id.render==='export') body=exportHTML();
  else body='<div id="idea-dyn" class="m-sub">Loading live data…</div>';
  openModal(`<h2 class="m-h">${id.icon} ${id.title}</h2><div class="m-sub">${id.pitch}</div>${body}${ideaFooter(i)}`);
  if(id.render==='router'){ const inp=document.getElementById('route-q'); if(inp){ inp.focus(); inp.addEventListener('keydown',e=>{ if(e.key==='Enter') routeTask(); }); } }
  else if(id.render==='health') loadHealth();
  else if(id.render==='cost') loadCost();
}
function queueIdea(i){ const id=IDEAS[i];
  fetch('/api/queue-idea?title='+encodeURIComponent('[Cabinet] '+id.title)+'&summary='+encodeURIComponent(id.pitch)).then(r=>r.json())
    .then(r=>toast(r.ok?'➕ queued to CNCP parking-lot':'queue failed ('+(r.error||r.status)+')')).catch(()=>toast('CNCP unreachable (:3333)')); }
function routerHTML(){ return `<div class="ri"><input id="route-q" placeholder="e.g. scrape a new vendor · fix a slow SQL query · post a reel · check DNS"><button class="m-btn primary" onclick="routeTask()">Route</button></div><div id="route-res" class="m-sub">Type a task and hit Route — I’ll point you to the officer + the exact skills/agents, each runnable.</div>`; }
function routeTask(){
  const qv=(document.getElementById('route-q').value||'').toLowerCase();
  const toks=qv.split(/[^a-z0-9]+/).filter(t=>t.length>2);
  const res=document.getElementById('route-res');
  if(!toks.length){ res.innerHTML='<div class="m-sub">Enter a few words.</div>'; return; }
  const scored=OFF.map(o=>{
    const hay=(o.dm+' '+o.agents.join(' ')+' '+o.skills.join(' ')+' '+(o.scrapers||[]).join(' ')).toLowerCase();
    let score=0; toks.forEach(t=>{ if(hay.includes(t)) score++; });
    const nodes=[]; [...o.agents.map(a=>({k:'subagent',n:a})),...o.skills.map(s=>({k:'skill',n:s})),...(o.scrapers||[]).map(s=>({k:'skill',n:s}))].forEach(x=>{ if(toks.some(t=>x.n.toLowerCase().includes(t))) nodes.push(x); });
    return {o,score:score+nodes.length,nodes:nodes.slice(0,8)};
  }).filter(x=>x.score>0).sort((a,b)=>b.score-a.score).slice(0,3);
  if(!scored.length){ res.innerHTML='<div class="m-sub">No strong match — try words like <b>shopify, dns, video, research, security, compliance, instagram</b>.</div>'; return; }
  res.innerHTML=scored.map(x=>`<div class="route-off" style="--rc:${x.o.col}"><div class="ro-h">${x.o.nm}</div>${x.nodes.length?x.nodes.map(n=>`<span class="route-chip" onclick="runNode('${n.k}','${n.n}')">${n.k==='skill'?'/':'@'}${n.n} ▶</span>`).join(''):'<div style="font-size:11px;color:#8a94a6;margin-top:4px">domain match — open this officer for its toolset</div>'}</div>`).join('');
}
function consolHTML(){
  const rows=SHARED.map(s=>`<div class="row"><span style="color:hsl(${hashHue(s.id)} 82% 70%)">${s.id}</span><span style="color:#8a94a6;font-size:11px">${s.officers.map(offName).join(' · ')} <b style="color:#e7ecf3">(${s.officers.length})</b></span></div>`).join('');
  return `<div class="m-sub"><b>${SHARED.length}</b> skills/agents are wired to 2+ officers. Per the cabinet’s rules, repeated cross-use is the signal to fold a Director into a VP or share a single owner.</div><div class="m-quote">“When a Director is invoked twice for the same VP, consider folding the Director’s logic into the VP. Skills are leaves — fewer agents, broader triggers, cleaner routing.” — architecture_principles</div><div class="m-list">${rows}</div>`;
}
function buildExport(){
  const obj={president:'Steve', officers:OFF.map(o=>({id:o.id,name:o.nm,domain:o.dm,agents:o.agents,skills:o.skills.concat(o.scrapers||[])})), shared:SHARED.map(s=>({id:s.id,officers:s.officers})), suggestions:SUGGEST};
  let md='# The Cabinet\n\n';
  OFF.forEach(o=>{ md+=`## ${o.nm}\n_${o.dm}_\n\n- **Agents:** ${o.agents.join(', ')}\n- **Skills:** ${o.skills.concat(o.scrapers||[]).join(', ')}\n\n`; });
  md+='## Cross-officer shared\n'+SHARED.map(s=>`- \`${s.id}\` — ${s.officers.map(offName).join(', ')}`).join('\n')+'\n';
  return {json:JSON.stringify(obj,null,2), md};
}
function exportHTML(){ return `<div class="m-sub">Download the live cabinet structure (7 officers, all agents/skills, the ${SHARED.length} cross-officer shares + ${SUGGEST.length} suggestions).</div><div class="m-actions" style="border:none;padding:0;margin-top:6px"><button class="m-btn primary" onclick="dl('json')">⬇ cabinet.json</button><button class="m-btn primary" onclick="dl('md')">⬇ cabinet.md</button></div>`; }
function dl(kind){ const e=buildExport(); const blob=new Blob([kind==='json'?e.json:e.md],{type:'text/plain'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='cabinet.'+(kind==='json'?'json':'md'); a.click(); toast('⬇ cabinet.'+kind+' downloaded'); }

// Idea 4 — Live Health Overlay (real DW canary verdicts)
let HEALTH=null, COST=null, _overlay=null;
const HCOL={up:'#34d399',warn:'#fbbf24',down:'#ef4444',na:'#4b5563'};
async function loadHealth(){
  const box=document.getElementById('idea-dyn'); if(!box) return;
  let d; try{ d=await(await fetch('/api/health')).json(); }catch(e){ box.innerHTML='<div class="dr-note">server unreachable — open via :9766/pyramid</div>'; return; }
  HEALTH=d;
  let h='<div style="margin:2px 0 12px">';
  OFF.forEach(o=>{ const oi=d.officers[o.id]; const st=oi?oi.status:'na';
    h+=`<div class="hstat"><span class="d" style="background:${HCOL[st]}"></span><b style="color:${o.col}">${o.nm.replace('VP ','')}</b> — <b>${st.toUpperCase()}</b>${oi?'<div style="color:#8a94a6;font-size:10.5px;margin:1px 0 4px 16px">'+oi.detail+'</div>':' <span style="color:#6b7280;font-size:10.5px">(no monitor)</span>'}</div>`; });
  h+='</div><div class="m-actions" style="border:none;padding:0;margin:0"><button class="m-btn primary" onclick="applyHealth()">🩺 Apply to pyramid</button><button class="m-btn ghost" onclick="clearOverlays()">Clear</button></div>';
  box.innerHTML=h;
}
function applyHealth(){
  if(!HEALTH) return; clearOverlays(true);
  OFF.forEach(o=>{ if(!o._node) return; const oi=HEALTH.officers[o.id]; const st=oi?oi.status:'na';
    o._node.classList.add('h-'+st);
    const dot=document.createElement('span'); dot.className='health-dot '+st; dot.title=oi?oi.detail:'no monitor'; o._node.appendChild(dot); });
  _overlay='health'; document.getElementById('clear-overlay').classList.add('show'); closeModal(); toast('🩺 health overlay applied');
}
// Idea 5 — Cost / Usage Heatmap (real cost-ledger spend, app→officer mapped)
function appOfficer(app){ for(const o of OFF){ if(o.agents.includes(app)||o.skills.includes(app)||(o.scrapers||[]).includes(app)) return o.id; } return null; }
async function loadCost(){
  const box=document.getElementById('idea-dyn'); if(!box) return;
  let d; try{ d=await(await fetch('/api/cost')).json(); }catch(e){ box.innerHTML='<div class="dr-note">server unreachable</div>'; return; }
  if(!d.ok){ box.innerHTML='<div class="dr-note">'+(d.error||'no data')+'</div>'; return; }
  COST=d;
  const max=Math.max(...d.by_app.slice(0,10).map(a=>a.usd),0.0001);
  let h=`<div class="m-sub" style="margin:2px 0 10px">Total <b style="color:#fde68a">$${d.total_usd}</b> over ${d.entries.toLocaleString()} calls · ${(d.first||'').slice(0,10)} → ${(d.last||'').slice(0,10)}</div><div style="font-size:11px;color:#8a94a6;margin-bottom:5px">Top spenders by app (▸ mapped to officer where it’s a cabinet skill)</div>`;
  d.by_app.slice(0,10).forEach(a=>{ const off=appOfficer(a.name);
    h+=`<div class="bar-row"><span class="nm" title="${a.name}">${a.name}${off?' <span style="color:#8a94a6">▸ '+offName(off)+'</span>':''}</span><span class="bar" style="width:${Math.max(4,a.usd/max*220)}px"></span><span class="v">$${a.usd.toFixed(2)}</span></div>`; });
  h+='<div class="m-actions" style="border:none;padding:0;margin-top:12px"><button class="m-btn primary" onclick="applyCost()">💸 Apply heatmap</button><button class="m-btn ghost" onclick="clearOverlays()">Clear</button></div>';
  box.innerHTML=h;
}
function applyCost(){
  if(!COST) return; clearOverlays(true);
  const byOff={}; COST.by_app.forEach(a=>{ const off=appOfficer(a.name); if(off) byOff[off]=(byOff[off]||0)+a.usd; });
  const max=Math.max(...Object.values(byOff),0.0001);
  OFF.forEach(o=>{ if(!o._node) return; const usd=byOff[o.id]||0;
    const badge=document.createElement('span'); badge.className='cost-badge'; badge.textContent='$'+usd.toFixed(2); o._node.appendChild(badge);
    const bar=document.createElement('span'); bar.className='cost-bar'; bar.style.width=(usd>0?Math.max(8,usd/max*100):0)+'%'; o._node.appendChild(bar); });
  _overlay='cost'; document.getElementById('clear-overlay').classList.add('show'); closeModal(); toast('💸 cost heatmap applied (officer-mapped spend)');
}
function clearOverlays(silent){
  document.querySelectorAll('.officer-node').forEach(n=>{ n.classList.remove('h-up','h-warn','h-down'); n.querySelectorAll('.health-dot,.cost-badge,.cost-bar').forEach(e=>e.remove()); });
  document.getElementById('clear-overlay').classList.remove('show'); _overlay=null;
  if(!silent) toast('overlay cleared');
}

// Make every clickable div behave like a button for keyboard + AT users (P0):
// focusable, announced as a button, activatable with Enter/Space. Real <button>
// handles (.gap-h/.sug-h) and the toolbar are already accessible — skip them.
function makeAccessible(){
  document.querySelectorAll('[data-kind],[data-gap]').forEach(el=>{
    if(el.tagName==='BUTTON') return;
    el.tabIndex=0; el.setAttribute('role','button');
    if(!el.getAttribute('aria-label')){
      const lbl=(el.dataset.label||el.dataset.id||el.textContent||'').trim();
      if(lbl) el.setAttribute('aria-label', el.dataset.gap?('Org gap: '+lbl):lbl);
    }
  });
}
legend(); build(); timeline(); buildToolbar(); makeAccessible();
document.getElementById('pyramid').addEventListener('click', onNodeClick);
// Enter / Space activates a focused node (mirrors the click handlers).
document.addEventListener('keydown', e=>{
  if(e.key!=='Enter' && e.key!==' ' && e.key!=='Spacebar') return;
  const el=e.target.closest('[data-kind],[data-gap]'); if(!el || el.tagName==='BUTTON') return;
  e.preventDefault();
  if(el.dataset.gap){ openGap(el.dataset.gap); return; }
  const kind=el.dataset.kind;
  if(kind==='scrapers'){ openScrapers(JSON.parse(el.dataset.scrapers||'[]')); return; }
  openPanel(kind, el.dataset.id, el.dataset.label);
});
// Central drawer focus management: focus the primary action when the drawer
// opens, restore focus to whatever opened it when it closes, trap Tab inside.
(function(){
  const dr=document.getElementById('drawer');
  new MutationObserver(()=>{
    const open=dr.classList.contains('open');
    if(open && !_drawerWasOpen){
      _drawerReturn=document.activeElement;
      // Prefer the primary action (Approve / Run); fall back to close (✕).
      const btn=[...dr.querySelectorAll('.dr-actions .dr-btn')].find(b=>b.offsetParent!==null)
              || dr.querySelector('.dr-x');
      if(btn) requestAnimationFrame(()=>btn.focus());
    } else if(!open && _drawerWasOpen){
      if(_drawerReturn && typeof _drawerReturn.focus==='function') _drawerReturn.focus();
      _drawerReturn=null;
    }
    _drawerWasOpen=open;
  }).observe(dr,{attributes:true,attributeFilter:['class']});
  dr.addEventListener('keydown', e=>{
    if(e.key!=='Tab') return;
    const f=[...dr.querySelectorAll('button,[href],input,select,textarea,[tabindex]:not([tabindex="-1"])')].filter(el=>!el.disabled && el.offsetParent!==null);
    if(!f.length) return;
    const first=f[0], last=f[f.length-1];
    if(e.shiftKey && document.activeElement===first){ e.preventDefault(); last.focus(); }
    else if(!e.shiftKey && document.activeElement===last){ e.preventDefault(); first.focus(); }
  });
})();
document.getElementById('xlines').addEventListener('click', e=>{ const t=e.target.closest('.sug-hit'); if(t) openSuggestion(t.dataset.key); });
document.getElementById('sug-handles').addEventListener('click', e=>{ const t=e.target.closest('.sug-h'); if(t) openSuggestion(t.dataset.key); });
document.getElementById('glines').addEventListener('click', e=>{ const t=e.target.closest('.gap-hit'); if(t) openGap(t.dataset.key); });
document.getElementById('gap-handles').addEventListener('click', e=>{ const t=e.target.closest('.gap-h'); if(t) openGap(t.dataset.key); });
document.getElementById('officers').addEventListener('click', e=>{ const t=e.target.closest('[data-gap]'); if(t){ e.stopPropagation(); openGap(t.dataset.gap); } });
document.getElementById('modal').addEventListener('click', e=>{ if(e.target.id==='modal') closeModal(); });
document.addEventListener('keydown', e=>{ if(e.key==='Escape'){ closePanel(); closeModal(); } });
function redraw(){ drawLines(); drawCross(); drawGaps(); }
Promise.all([loadSugState(), loadGapState()]).then(()=>requestAnimationFrame(()=>requestAnimationFrame(redraw)));
window.addEventListener('resize', ()=>{ requestAnimationFrame(redraw); });
</script>
</body>
</html>