<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>BETA NETWORK OPC</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Martian+Mono:wght@400;500;600;700&family=Spectral:ital,wght@0,400;0,500;0,600;1,400;1,500&display=swap" rel="stylesheet">
<style>
  :root{
    --paper:#e7eaed; --panel:#eef1f3; --panel-2:#f5f7f8; --panel-3:#fafbfb;
    --ink:#1b2531; --ink-2:#586573; --ink-3:#929cab;
    --grid:#d4dbe1; --grid-2:#c3cdd5; --rule:#abb5c0; --frame:#33414f;
    --blue:#2f5d8a; --build:#9c5a22; --invest:#2f6e54; --red:#bf3a2b;
    --mono:'Martian Mono',ui-monospace,'SF Mono',Menlo,monospace;
    --serif:'Spectral',Georgia,'Times New Roman',serif;
  }
  *{box-sizing:border-box;margin:0;padding:0;}
  html,body{min-height:100%;}
  body{background:#e7eaed;color:#1b2531;
    font-family:var(--serif);font-size:15px;line-height:1.66;
    -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
    background-color:var(--paper);
    background-image:
      linear-gradient(var(--grid) .9px,transparent .9px),
      linear-gradient(90deg,var(--grid) .9px,transparent .9px),
      linear-gradient(var(--grid-2) .9px,transparent .9px),
      linear-gradient(90deg,var(--grid-2) .9px,transparent .9px);
    background-size:27px 27px,27px 27px,135px 135px,135px 135px;
    background-position:-1px -1px;}

  /* ==================== AUTH GATE ==================== */
  #resume{display:none;}
  html.resuming #gate-wrap{display:none;}
  html.resuming #resume{display:flex;min-height:100vh;align-items:center;justify-content:center;
    font-family:var(--mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:#586573;}
  #gate-wrap{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:24px;}
  .gate{background:#f5f7f8;border:1.4px solid #33414f;max-width:392px;width:100%;
    padding:30px 30px 26px;position:relative;font-family:var(--mono);
    box-shadow:0 1px 0 rgba(27,37,49,.06),0 30px 64px -42px rgba(27,37,49,.6);}
  .gate::before{content:"";position:absolute;inset:6px;border:1px solid #33414f;pointer-events:none;}
  .mk{width:34px;height:34px;background:#2f5d8a;margin-bottom:18px;}
  h1.gate-h{font-size:17px;font-weight:700;letter-spacing:-.01em;text-transform:uppercase;margin-bottom:5px;}
  .sub{font-size:10px;color:#586573;line-height:1.55;font-family:var(--mono);}
  .sub.gap{margin-bottom:20px;}
  label{display:block;font-size:8px;font-weight:600;letter-spacing:.15em;
    text-transform:uppercase;color:#929cab;margin:15px 0 5px;font-family:var(--mono);}
  input{width:100%;font-family:var(--mono);font-size:13px;padding:9px 11px;
    border:1.1px solid #abb5c0;background:#fff;color:#1b2531;}
  input:focus{outline:none;border-color:#2f5d8a;}
  button.gbtn,button.gbutton,.gate button{font-family:var(--mono);font-size:10.5px;font-weight:600;letter-spacing:.07em;
    text-transform:uppercase;cursor:pointer;border:1.1px solid #33414f;
    background:#1b2531;color:#f5f7f8;transition:.13s ease;}
  .gate button{width:100%;padding:11px;margin-top:18px;}
  .gate button:hover{background:#2f5d8a;border-color:#2f5d8a;}
  .gate button.alt{background:transparent;color:#586573;border-color:#abb5c0;margin-top:8px;}
  .gate button.alt:hover{background:#fff;color:#1b2531;}
  .gbtn{display:flex;align-items:center;justify-content:center;gap:9px;
    background:#fff!important;color:#1b2531!important;border:1.1px solid #33414f;}
  .gate .gbtn{margin-top:2px;}
  .gbtn:hover{background:#eef1f3!important;color:#1b2531!important;border-color:#2f5d8a;}
  .gbtn svg{width:15px;height:15px;}
  .orline{display:flex;align-items:center;gap:10px;margin:16px 0 0;
    font-size:8px;font-weight:600;letter-spacing:.15em;text-transform:uppercase;color:#929cab;}
  .orline::before,.orline::after{content:"";flex:1;height:1px;background:#cdd5dc;}
  .msg{font-size:10px;margin-top:14px;line-height:1.5;min-height:13px;color:#586573;font-family:var(--mono);}
  .msg.err{color:#bf3a2b;} .msg.ok{color:#2f6e54;}
  .hide{display:none!important;}

  /* ==================== APP SHELL ==================== */
  #app{padding:0;}
  #bar{position:sticky;top:0;z-index:60;display:flex;align-items:center;gap:13px;
    background:#1b2531;color:#f5f7f8;padding:7px 14px;font-size:10px;font-family:var(--mono);}
  #bar .t{font-weight:600;letter-spacing:.14em;text-transform:uppercase;}
  #bar .ro{font-size:8.5px;letter-spacing:.07em;text-transform:uppercase;color:#9aa6b2;}
  #bar .ro b{color:#7fa8cc;font-weight:600;}
  #bar .sp{flex:1;}
  #bar button{padding:6px 12px;font-size:9px;font-family:var(--mono);font-weight:600;
    letter-spacing:.07em;text-transform:uppercase;cursor:pointer;
    border:1.1px solid #33414f;background:transparent;color:#9aa6b2;transition:.13s ease;}
  #bar button:hover{background:#33414f;color:#f5f7f8;}
  #bar .live{display:inline-flex;align-items:center;gap:5px;color:#7fa8cc;}
  #bar .live .dot{width:7px;height:7px;border-radius:50%;background:#2f6e54;box-shadow:0 0 8px #2f6e54;}
  #bar .live.off .dot{background:#9c5a22;box-shadow:none;}
  #bar .live.err .dot{background:#bf3a2b;}
  #savetoast{position:fixed;bottom:18px;right:18px;background:rgba(27,37,49,.92);color:#f5f7f8;
    font-family:var(--mono);font-size:11px;font-weight:500;padding:9px 14px;z-index:70;
    border-left:3px solid #2f6e54;opacity:0;transition:opacity .14s ease;pointer-events:none;}
  #savetoast.show{opacity:1;}
  #savetoast.err{border-left-color:#bf3a2b;}

  body::before{content:"";position:fixed;inset:0;z-index:1;pointer-events:none;
    opacity:.42;mix-blend-mode:multiply;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='150' height='150'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");}
  ::selection{background:rgba(47,93,138,.22);color:var(--ink);}

  /* ---- the drawing sheet ---- */
  .sheet{position:relative;max-width:1180px;margin:0 auto;background:var(--paper);
    border:1.4px solid var(--frame);padding:30px 34px 24px;z-index:2;
    box-shadow:0 1px 0 rgba(27,37,49,.06),0 30px 66px -44px rgba(27,37,49,.55);
    margin-top:36px;margin-bottom:90px;}
  .sheet::before{content:"";position:absolute;inset:7px;border:1px solid var(--frame);
    pointer-events:none;}
  .reg{position:absolute;width:23px;height:23px;color:var(--blue);z-index:4;}
  .reg-tl{top:-11.5px;left:-11.5px;} .reg-tr{top:-11.5px;right:-11.5px;}
  .reg-bl{bottom:-11.5px;left:-11.5px;} .reg-br{bottom:-11.5px;right:-11.5px;}

  /* ---- title block ---- */
  .titleblock{display:grid;grid-template-columns:1fr 286px;
    border:1.2px solid var(--frame);margin-bottom:16px;background:var(--panel-3);}
  .tb-main{padding:17px 22px 21px;border-right:1.2px solid var(--frame);}
  .tb-project{font-family:var(--mono);font-size:9px;font-weight:600;
    letter-spacing:.16em;text-transform:uppercase;color:var(--ink-2);
    display:flex;align-items:center;gap:9px;margin-bottom:14px;}
  .tb-project b{background:var(--blue);color:var(--panel-3);font-weight:600;
    padding:3px 7px;letter-spacing:.14em;}
  h1.tb-title{font-family:var(--mono);font-weight:700;font-size:50px;line-height:.96;
    letter-spacing:-.022em;text-transform:uppercase;color:var(--ink);margin:0 0 12px;}
  .tb-sub{font-family:var(--serif);font-style:italic;font-size:15.5px;
    color:var(--ink-2);max-width:46ch;}
  .tb-sub b{font-style:normal;font-weight:600;color:var(--ink);}
  .tb-side{display:grid;grid-template-columns:1fr 1fr;align-content:start;}
  .tb-cell{border-bottom:1px solid var(--frame);border-left:1px solid var(--frame);
    padding:9px 12px;}
  .tb-cell.tb-wide{grid-column:1 / -1;}
  .tb-cell.tb-last{border-bottom:none;}
  .tb-k{display:block;font-family:var(--mono);font-size:7.5px;font-weight:600;
    letter-spacing:.17em;text-transform:uppercase;color:var(--ink-3);margin-bottom:4px;}
  .tb-v{font-family:var(--mono);font-size:11.5px;font-weight:500;color:var(--ink);}
  .tb-v.accent{color:var(--blue);}

  /* ---- revision note ---- */
  .revnote{display:flex;gap:13px;align-items:flex-start;
    border:1.1px solid var(--red);border-left-width:4px;
    background:rgba(191,58,43,.055);padding:11px 15px;margin-bottom:26px;
    font-family:var(--serif);font-size:13.5px;color:var(--ink);}
  .rv-tag{font-family:var(--mono);font-size:9px;font-weight:600;letter-spacing:.1em;
    color:#fff;background:var(--red);padding:4px 8px;flex-shrink:0;
    white-space:nowrap;margin-top:1px;}

  /* ---- drawing frame for the schematic ---- */
  .diagram{position:relative;background:var(--panel-2);
    border:1.2px solid var(--frame);margin-bottom:8px;cursor:crosshair;}
  .dwg-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
    font-family:var(--mono);font-size:9px;font-weight:500;letter-spacing:.07em;
    text-transform:uppercase;color:var(--ink-2);
    padding:8px 14px;border-bottom:1.1px solid var(--frame);background:var(--panel-3);}
  .dwg-head b{background:var(--ink);color:var(--panel-3);font-weight:600;
    padding:3px 7px;letter-spacing:.1em;}
  .dwg-head .nts{margin-left:auto;color:var(--ink-3);}
  .diagram svg{width:100%;height:auto;display:block;padding:16px 16px 10px;}

  /* ===== Ledger: demand pays for supply (thesis one) ===== */
  .ledger{padding:4px 16px 18px;font-family:var(--mono);}
  .opm{padding:4px 16px 18px;}
  .opm svg{width:100%;height:auto;display:block;padding:0;}
  .opm text{font-family:var(--mono);}
  .ledger-cap{font-size:9.5px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;
    color:var(--ink-2);padding-bottom:8px;}
  .ledger-head{display:grid;grid-template-columns:minmax(150px,.85fr) 30px 1fr;gap:12px;
    font-size:8.5px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);
    padding-bottom:6px;border-bottom:1px solid var(--rule);}
  .grp{padding:14px 0;border-bottom:1px solid var(--grid);}
  .grp:last-of-type{border-bottom:none;}
  .grp-shared{display:grid;grid-template-columns:1fr 22px minmax(210px,.62fr);gap:10px;align-items:center;}
  .grp-main{display:grid;gap:12px;}
  .pair{display:grid;grid-template-columns:minmax(150px,.85fr) 30px 1fr;gap:12px;align-items:start;}
  .cust{display:grid;gap:2px;align-content:start;padding-top:2px;}
  .cust-cn{margin:0;font-size:13px;font-weight:600;color:var(--ink);}
  .cust-en{margin:0;font-size:9px;color:var(--ink-3);}
  .cust-note{margin:0;font-size:8.5px;color:var(--ink-3);}
  .own{margin:0;font-size:9.5px;font-weight:600;color:var(--invest);}
  .arw{display:flex;justify-content:center;padding-top:10px;font-size:12px;color:var(--invest);}

  .lob{border:1px solid var(--rule);border-radius:3px;background:var(--panel-3);
    padding:10px 12px;display:grid;gap:7px;}
  .lob-h1{border-color:var(--invest);background:#ecf3ef;}
  .lob-h2{border-color:var(--blue);background:var(--panel-3);}
  .lob-h3{border-style:dashed;border-color:var(--ink-3);background:var(--panel-3);}
  .lob-head{display:flex;align-items:center;gap:7px;flex-wrap:wrap;}
  .lob-head .own{margin-left:auto;}
  .lob-name{font-size:11.5px;font-weight:700;color:var(--ink);}
  .tag{font-size:8px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);}
  .lob-body{display:grid;gap:3px;}
  .lob-body p{margin:0;}
  .sup-name{font-size:11px;font-weight:600;color:var(--ink);}
  .sup-gloss{font-size:8.5px;color:var(--ink-3);margin-left:5px;}
  .dom{font-size:9px;color:var(--blue);}
  .tal{font-size:9px;color:var(--ink-2);}
  .tl{font-size:8px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;
    color:var(--ink-3);margin-right:5px;}
  .fin{font-size:9px;color:var(--ink-2);}
  .sides{margin-top:4px;padding-top:5px;border-top:1px dashed var(--grid-2);display:grid;gap:3px;}
  .side{font-size:8.5px;color:var(--ink-3);line-height:1.45;}
  .sl{font-size:7.5px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;
    color:var(--ink-3);margin-right:5px;opacity:.8;}

  .hchip{display:inline-flex;align-items:center;justify-content:center;min-width:26px;height:15px;
    border-radius:2px;font-size:8px;font-weight:700;letter-spacing:.06em;}
  .hchip-h1{background:var(--invest);border:1px solid var(--invest);color:#fff;}
  .hchip-h2{background:#e7edf3;border:1px solid var(--blue);color:var(--blue);}
  .hchip-h3{background:transparent;border:1px dashed var(--ink-3);color:var(--ink-3);}
  .dots{display:inline-flex;gap:3px;}
  .dot{width:4px;height:4px;border-radius:50%;border:1px solid var(--ink-3);background:transparent;}
  .dot.on{border-color:var(--invest);background:var(--invest);}

  .fold{border-top:1px solid var(--grid);padding-top:6px;}
  .fold>summary{list-style:none;cursor:pointer;display:flex;align-items:center;gap:5px;
    font-size:8px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-3);}
  .fold>summary::-webkit-details-marker{display:none;}
  .fold>summary::before{content:"";width:0;height:0;border-left:4px solid currentColor;
    border-top:3px solid transparent;border-bottom:3px solid transparent;transition:transform .12s ease;}
  .fold[open]>summary::before{transform:rotate(90deg);}
  .fold[open]>summary,.fold>summary:hover{color:var(--blue);}
  .vs{display:grid;gap:4px;margin-top:6px;}
  .vr{display:grid;grid-template-columns:52px 1fr;gap:8px;align-items:baseline;}
  .vl{font-size:8px;font-weight:600;letter-spacing:.13em;text-transform:uppercase;color:var(--blue);}
  .vt{font-size:9px;line-height:1.5;color:var(--ink-2);}

  .brace{align-self:stretch;min-height:52px;margin:6px 0;border-right:1px solid var(--invest);
    border-top:1px solid var(--invest);border-bottom:1px solid var(--invest);border-radius:0 3px 3px 0;
    position:relative;}
  .brace::after{content:"";position:absolute;top:50%;right:-10px;width:10px;
    border-top:1px solid var(--invest);}
  .svc{display:grid;gap:7px;}
  .svc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(228px,1fr));gap:10px;align-items:start;}
  .pair.svc-pair{grid-template-columns:minmax(140px,190px) 30px 1fr;}
  .grp-shared-col{display:grid;gap:5px;}
  .shared-tag{justify-self:start;font-size:8px;font-weight:600;letter-spacing:.13em;
    text-transform:uppercase;color:var(--invest);background:#e3efe9;border-radius:2px;padding:2px 6px;}
  .lob-shared{border-color:var(--invest);}

  .ledger-key{display:flex;align-items:center;gap:6px;flex-wrap:wrap;padding-top:12px;
    border-top:1px solid var(--rule);font-size:8.5px;color:var(--ink-3);}
  .ledger-key .sep{width:14px;}

  @media (max-width:720px){
    .pair,.ledger-head{grid-template-columns:1fr;}
    .arw{display:none;}
    .grp-shared{grid-template-columns:1fr;}
    .brace{display:none;}
  }
  svg{display:block;}

  /* SVG schematic */
  .th{fill:var(--ink);font-family:var(--mono);font-weight:600;font-size:11.5px;}
  .ts{fill:var(--ink-2);font-family:var(--mono);font-weight:400;font-size:9px;}
  .tt{fill:var(--blue);font-family:var(--mono);font-weight:600;font-size:8.5px;
    letter-spacing:.13em;}
  .cap{fill:var(--ink-2);font-family:var(--mono);font-weight:600;font-size:9.5px;
    letter-spacing:.13em;}
  .pri{fill:var(--ink-2);font-family:var(--mono);font-weight:400;font-size:8px;}
  .hit{cursor:pointer;}
  .hitbox{transition:stroke-width .14s ease;}
  .hit:hover .hitbox{stroke-width:2.2;}
  .hit:focus-visible{outline:none;}
  .hit:focus-visible .hitbox{stroke-width:2.4;}
  .diagram a{cursor:pointer;}

  /* dossier sources (hidden — content for modal) */
  details{background:var(--panel);border:1.1px solid var(--rule);
    margin-bottom:9px;overflow:hidden;}
  summary{list-style:none;padding:13px 16px;display:flex;align-items:baseline;gap:11px;}
  summary::-webkit-details-marker{display:none;}
  .s-title{font-family:var(--mono);font-weight:600;font-size:14.5px;
    letter-spacing:-.01em;color:var(--ink);}
  .s-meta{margin-left:auto;font-family:var(--mono);font-size:9.5px;
    letter-spacing:.04em;color:var(--ink-3);text-transform:uppercase;}
  .body{padding:3px 18px 18px 18px;}
  .body p{color:var(--ink-2);font-size:14px;margin:9px 0;}
  .body h4{font-family:var(--mono);font-size:9.5px;letter-spacing:.15em;
    text-transform:uppercase;color:var(--blue);margin:17px 0 7px;font-weight:600;}
  .body ul{list-style:none;}
  .body li{font-family:var(--serif);font-size:14px;color:var(--ink-2);
    padding:4px 0 4px 17px;position:relative;}
  .body li::before{content:"\203A";position:absolute;left:0;color:var(--blue);
    font-family:var(--mono);}
  .body li.no::before{content:"\2715";color:var(--red);}
  .body li.q::before{content:"\25A1";color:var(--ink-3);}
  .body li.done::before{content:"\2713";color:var(--invest);}
  .body i{font-style:italic;}
  .body b{color:var(--ink);}

  .cell{border:1px solid var(--rule);padding:12px 14px;margin:10px 0;
    background:var(--panel-3);}
  .cell-tag{font-family:var(--mono);font-size:9px;letter-spacing:.13em;
    text-transform:uppercase;color:var(--ink-3);font-weight:600;margin-bottom:6px;
    padding-bottom:6px;border-bottom:1px solid var(--rule);}
  .cell .okr{font-family:var(--mono);font-size:12.5px;font-weight:600;
    color:var(--ink);margin-top:7px;}
  .cell .kr{font-family:var(--mono);font-size:11px;color:var(--ink-2);margin:3px 0 0;}
  .cell .kr .x{font-weight:700;color:var(--blue);}
  .cell.empty{background:var(--panel);border-style:dashed;}
  .cell.empty .note{font-family:var(--serif);font-size:12.5px;font-style:italic;
    color:var(--ink-3);}
  .cell .price{font-family:var(--mono);font-size:10px;color:var(--invest);
    font-weight:500;margin:6px 0 2px;}
  .src{font-family:var(--mono);font-size:10.5px;margin-top:9px;padding-top:8px;
    border-top:1px dashed var(--rule);}
  .src .lbl{color:var(--ink-3);}
  .src a{color:var(--blue);text-decoration:none;border-bottom:1px solid rgba(47,93,138,.35);}
  .src a:hover{border-bottom-color:var(--blue);}
  .src .gid{color:var(--ink-3);font-size:9.5px;}
  .live-pill{font-family:var(--mono);font-size:11px;margin-top:8px;padding:8px 10px;
    background:rgba(47,93,138,.06);border:1px solid rgba(47,93,138,.26);
    color:var(--ink-2);}
  .live-pill .hd{background:var(--blue);color:var(--panel-3);font-weight:600;
    font-size:8.5px;letter-spacing:.12em;padding:2px 6px;margin-right:6px;}
  .live-pill b{color:var(--ink);font-weight:700;}
  .live-pill .brk{color:var(--ink-3);}

  .rhythm{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;}
  .beat{flex:1;min-width:132px;background:var(--panel-3);border:1px solid var(--rule);
    padding:10px 12px;}
  .beat.now{border-color:var(--red);border-width:1.4px;}
  .beat .b1{font-family:var(--mono);font-size:11.5px;font-weight:600;color:var(--ink);}
  .beat.now .b1{color:var(--red);}
  .beat .b2{font-family:var(--serif);font-size:11.5px;color:var(--ink-2);margin-top:4px;}

  .foot{font-family:var(--mono);margin-top:18px;padding-top:13px;
    border-top:1.2px solid var(--frame);font-size:9px;color:var(--ink-3);
    letter-spacing:.08em;text-transform:uppercase;display:flex;
    justify-content:space-between;flex-wrap:wrap;gap:10px;}

  /* "lives on" app badges */
  .lo{display:inline-flex;align-items:center;gap:4px;font-family:var(--mono);
    font-size:9px;font-weight:600;letter-spacing:.02em;text-transform:uppercase;
    padding:2px 6px 2px 4px;border:1px solid var(--rule);background:var(--panel-3);
    color:var(--ink-2);white-space:nowrap;vertical-align:middle;line-height:1;}
  .lo svg{width:12px;height:12px;display:block;flex-shrink:0;}
  .lo.gap{border-color:var(--red);color:var(--red);background:rgba(191,58,43,.07);}
  a.lo{text-decoration:none;cursor:pointer;transition:border-color .13s ease,color .13s ease;}
  a.lo:hover{border-color:var(--blue);color:var(--blue);}
  .lo-ico{width:12px;height:12px;display:inline-block;vertical-align:-2px;
    margin-right:6px;flex-shrink:0;}
  .lo-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:9px 0 5px;}
  .lo-row .ar{color:var(--ink-3);font-family:var(--mono);font-size:8.5px;
    letter-spacing:.08em;text-transform:uppercase;}
  .lo-row.tight{margin:8px 0 0;}
  .body ul.pri-list,.body ul.tool-list{margin-top:4px;}
  .body ul.pri-list li,.body ul.tool-list li{padding-left:0;font-family:var(--serif);}
  .body ul.pri-list li::before,.body ul.tool-list li::before{content:none;}

  /* ==================== MODAL (with editable fields) ==================== */
  .modal-bg{display:none;position:fixed;inset:0;z-index:90;
    background:rgba(40,50,62,.5);backdrop-filter:blur(2px);
    padding:52px 20px;overflow-y:auto;}
  .modal-bg.show{display:flex;align-items:flex-start;justify-content:center;}
  .modal{position:relative;width:100%;max-width:680px;background:var(--panel-2);
    border:1.4px solid var(--frame);padding:8px;
    box-shadow:0 34px 80px -30px rgba(27,37,49,.7);animation:pop .18s ease;}
  .modal::before{content:"";position:absolute;inset:4px;border:1px solid var(--frame);
    pointer-events:none;}
  @keyframes pop{from{opacity:0;transform:translateY(-12px);}to{opacity:1;transform:none;}}
  .modal-inner{padding:22px 26px 26px;border-left:4px solid var(--frame);}
  .modal.n1 .modal-inner{border-left-color:var(--build);}
  .modal.n2 .modal-inner{border-left-color:var(--blue);}
  .modal.n3 .modal-inner{border-left-color:var(--invest);}
  .modal.dir .modal-inner{border-left-color:var(--frame);}
  .modal-x{position:absolute;top:14px;right:14px;width:30px;height:30px;
    background:var(--panel-3);border:1px solid var(--rule);color:var(--ink-2);
    font-family:var(--mono);font-size:12px;cursor:pointer;line-height:1;
    transition:all .13s ease;z-index:2;}
  .modal-x:hover{background:var(--ink);color:var(--panel-3);border-color:var(--ink);}
  .modal-ttl{font-family:var(--mono);font-size:18px;font-weight:600;
    letter-spacing:-.01em;margin:0 44px 14px 0;color:var(--ink);}
  .modal .body{padding:0;}
  .modal .body>*:first-child{margin-top:0;}

  /* editable text affordance */
  [data-key].editable{position:relative;outline:none;border-radius:2px;transition:background .14s ease, box-shadow .14s ease;}
  [data-key].editable:hover{background:rgba(47,93,138,.07);box-shadow:inset 0 0 0 1px rgba(47,93,138,.18);}
  [data-key].editable:focus{background:rgba(47,93,138,.12);box-shadow:inset 0 0 0 1.5px rgba(47,93,138,.55);}
  [data-key].saving{box-shadow:inset 0 0 0 1.5px rgba(156,90,34,.6);}
  [data-key].saved{box-shadow:inset 0 0 0 1.5px rgba(47,110,84,.55);}
  [data-key].err{box-shadow:inset 0 0 0 1.5px rgba(191,58,43,.65);}
  /* SVG text: outline using filter not box-shadow */
  text[data-key]{cursor:pointer;}
  text[data-key]:hover{fill:var(--blue);}
  text[data-key].saving{fill:var(--build);}
  text[data-key].saved{fill:var(--invest);}
  text[data-key].err{fill:var(--red);}
  text[data-key].ph{fill:var(--ink-3);opacity:.4;}
  text[data-key].ph:hover{fill:var(--blue);opacity:1;}

  /* operating-model priority lists (.ed = editable, edit mode only) */
  .opm-bt{transition:fill .14s ease;}
  .opm-pri.ed .opm-b{cursor:text;}
  .opm-pri.ed text{-webkit-user-select:none;user-select:none;}
  .opm-pri.ed .opm-b:hover .opm-bt{fill:var(--blue);}
  .opm-x,.opm-add{opacity:0;pointer-events:none;cursor:pointer;transition:opacity .14s ease;}
  .opm-x circle,.opm-add circle{fill:#fff;stroke:var(--rule);stroke-width:1;transition:fill .12s ease,stroke .12s ease;}
  .opm-x path,.opm-add path{fill:none;stroke:var(--ink-2);stroke-width:1.3;stroke-linecap:round;}
  .opm-x:hover circle{fill:var(--red);stroke:var(--red);}
  .opm-add:hover circle{fill:var(--blue);stroke:var(--blue);}
  .opm-x:hover path,.opm-add:hover path{stroke:#fff;}
  .opm-addtxt{fill:var(--ink-3);opacity:.75;cursor:pointer;transition:fill .14s ease,opacity .14s ease;}
  .opm-pri:hover .opm-addtxt{fill:var(--blue);opacity:1;}
  @media (hover:hover){.opm-b:hover .opm-x,.opm-pri:hover .opm-add{opacity:1;pointer-events:all;}}
  @media (hover:none){.opm-x{display:none;}.opm-add{opacity:.6;pointer-events:all;}}
  @media (prefers-reduced-motion:reduce){
    .opm-bt,.opm-x,.opm-add,.opm-x circle,.opm-add circle,.opm-addtxt,#undobar{transition:none;}
  }

  /* overlay editors: list textarea, one-line input */
  #opm-ed{position:absolute;z-index:80;}
  #opm-ed textarea{display:block;width:100%;resize:none;overflow:hidden;box-sizing:border-box;
    font-family:var(--mono);line-height:1.55;color:var(--ink);background:#fff;
    padding:7px 10px;border:1.5px solid var(--blue);outline:none;transition:none;
    box-shadow:0 12px 28px -14px rgba(27,37,49,.6);}
  #opm-ed .hint{margin-top:5px;padding:3px 6px;font-family:var(--mono);font-size:10px;line-height:1.4;
    color:var(--ink-2);background:rgba(245,247,248,.96);pointer-events:none;}
  input.ed-in{position:absolute;z-index:80;width:auto;box-sizing:border-box;padding:0 8px;
    font-family:var(--mono);color:var(--ink);background:#fff;border:1.5px solid var(--blue);transition:none;
    box-shadow:0 12px 28px -14px rgba(27,37,49,.6);}

  /* undo bar (bottom-left, mirrors #savetoast) */
  #undobar{position:fixed;bottom:18px;left:18px;z-index:70;display:flex;align-items:center;gap:12px;
    max-width:calc(100vw - 36px);padding:7px 7px 7px 14px;background:rgba(27,37,49,.94);color:#f5f7f8;
    font-family:var(--mono);font-size:11px;font-weight:500;border-left:3px solid var(--build);
    opacity:0;transform:translateY(6px);pointer-events:none;
    transition:opacity .14s ease,transform .14s ease;}
  #undobar.show{opacity:1;transform:none;pointer-events:auto;}
  #undobar button{padding:5px 11px;font-family:var(--mono);font-size:9px;font-weight:600;
    letter-spacing:.07em;text-transform:uppercase;cursor:pointer;border:1.1px solid #5d6b7a;
    background:transparent;color:#f5f7f8;transition:.13s ease;}
  #undobar button:hover{background:#f5f7f8;color:#1b2531;border-color:#f5f7f8;}

  /* page-load motion */
  @keyframes rise{from{opacity:0;transform:translateY(11px);}to{opacity:1;transform:none;}}
  @keyframes regin{from{opacity:0;transform:scale(.3) rotate(-30deg);}to{opacity:1;transform:none;}}
  @keyframes sheetin{from{opacity:0;}to{opacity:1;}}
  .sheet{animation:sheetin .5s ease backwards;}
  .reg{animation:regin .5s cubic-bezier(.34,1.4,.5,1) backwards;}
  .reg-tl{animation-delay:.16s;} .reg-tr{animation-delay:.24s;}
  .reg-br{animation-delay:.32s;} .reg-bl{animation-delay:.40s;}
  .titleblock,.revnote,.diagram{animation:rise .6s cubic-bezier(.22,.61,.25,1) backwards;}
  .titleblock{animation-delay:.12s;}
  .revnote{animation-delay:.22s;}
  .diagram{animation-delay:.32s;}

  @media (prefers-reduced-motion:reduce){
    *,*::before{animation-duration:.001s!important;animation-delay:0s!important;
      transition-duration:.001s!important;}
  }
  @media (max-width:720px){
    .titleblock{grid-template-columns:1fr;}
    .tb-main{border-right:none;border-bottom:1.2px solid var(--frame);}
    h1.tb-title{font-size:38px;}
  }
/* ===== DWG 02: Q4 OKRs → priorities ===== */
  html{scroll-behavior:smooth;}
  @media (prefers-reduced-motion:reduce){html{scroll-behavior:auto;}}
  .q4{cursor:auto;margin-top:18px;}
  .q4 [id]{scroll-margin-top:44px;}
  .q4-mini{font-family:var(--mono);font-size:8px;font-weight:500;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-3);margin-right:2px;}
  .q4-top{display:flex;flex-wrap:wrap;gap:8px 28px;align-items:center;padding:9px 14px;border-bottom:1px solid var(--rule);background:var(--panel-2);}
  .q4-ns,.q4-sheets{display:flex;flex-wrap:wrap;align-items:center;gap:6px 8px;}
  .q4-ns-t{font-family:var(--serif);font-size:14px;font-weight:600;color:var(--ink);}
  .q4-ns-l{font-family:var(--mono);font-size:9px;color:var(--ink-2);}
  .q4-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1px;background:var(--rule);}
  .q4-card{background:var(--panel-3);padding:11px 13px 9px;min-width:0;scroll-margin-top:44px;}
  .q4-card.q4-money{background:var(--panel);}
  .q4-ch{display:flex;align-items:baseline;gap:8px;padding-bottom:7px;border-bottom:1px solid var(--rule);margin-bottom:8px;}
  .q4-code{font-family:var(--mono);font-size:9px;font-weight:600;letter-spacing:.08em;background:var(--ink);color:var(--panel-3);padding:2px 6px;flex:none;}
  .q4-name{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ink);line-height:1.35;}
  .q4-kr{display:grid;grid-template-columns:auto minmax(0,1fr);column-gap:10px;row-gap:3px;align-items:start;padding:5px 0;}
  .q4-kr+.q4-kr{border-top:1px dotted var(--rule);}
  .q4-big{font-family:var(--mono);font-size:21px;font-weight:600;line-height:1.1;color:var(--blue);font-variant-numeric:tabular-nums;min-width:2.6ch;}
  .q4-krt{display:flex;flex-direction:column;min-width:0;}
  .q4-lab{font-family:var(--serif);font-size:12.5px;line-height:1.3;color:var(--ink);}
  .q4-sub{font-family:var(--mono);font-size:8.5px;line-height:1.4;color:var(--ink-2);}
  .q4-kr .q4-chip{grid-column:2;justify-self:start;}
  .q4-pl{list-style:none;margin:6px 0 0;padding:0;counter-reset:q4p;}
  .q4-p{counter-increment:q4p;position:relative;padding:7px 0 7px 20px;border-top:1px solid var(--grid);}
  .q4-p::before{content:counter(q4p);position:absolute;left:0;top:9px;font-family:var(--mono);font-size:9px;font-weight:600;color:var(--ink-3);}
  .q4-pt{display:block;font-family:var(--serif);font-size:13px;line-height:1.4;color:var(--ink);}
  .q4-meta{display:flex;flex-wrap:wrap;align-items:center;gap:4px 5px;margin-top:4px;}
  .q4-when{font-family:var(--mono);font-size:8.5px;color:var(--ink-2);margin-right:2px;}
  .q4-k{font-family:var(--mono);font-size:8px;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-3);border:1px dashed var(--rule);padding:0 4px;line-height:1.5;margin-right:2px;}
  .q4-chip{font-family:var(--mono);font-size:9px;line-height:1.5;padding:1px 6px;border:1px solid var(--rule);text-decoration:none;white-space:nowrap;display:inline-block;border-radius:0;}
  a.q4-chip{cursor:pointer;}
  .q4-drv{background:var(--ink);border-color:var(--ink);color:var(--panel-3);font-weight:700;}
  .q4-con{background:transparent;border-color:var(--ink-2);color:var(--ink-2);font-weight:500;}
  .q4-own{background:var(--panel-2);border-color:var(--blue);color:var(--blue);font-weight:600;}
  .q4-plain{border-color:transparent;color:var(--ink-3);padding-left:2px;padding-right:2px;}
  a.q4-chip:hover{border-color:var(--blue);background:var(--blue);color:var(--panel-3);text-decoration:underline;}
  a.q4-chip:focus-visible{outline:2px solid var(--blue);outline-offset:2px;}
  @media (max-width:900px){.q4-grid{grid-template-columns:repeat(2,minmax(0,1fr));}}
  @media (max-width:720px){.q4-grid{grid-template-columns:1fr;}.q4-top{padding:9px 12px;}}
  
</style>
<script>
  // Returning visit with a saved session: hide the sign-in card until the session is tried,
  // so a silent refresh never looks like a logout.
  try{if(localStorage.getItem('boc_tok')||localStorage.getItem('boc_rtok')||/[?&]access_token=/.test(location.search))document.documentElement.classList.add('resuming');}catch(e){}
</script>
</head>
<body>

<div id="resume">Loading…</div>

<!-- ===== GATE ===== -->
<div id="gate-wrap">
  <div class="gate" id="gate">
    <div class="mk"></div>
    <h1 class="gate-h">BETA NETWORK OPC</h1>
    <div class="sub gap">Operating framework: editable. Sign in with a @betauniversity.org account.</div>
    <div id="form-login">
      <button type="button" class="gbtn" onclick="doGoogle()">
        <svg viewBox="0 0 48 48" aria-hidden="true"><path fill="#4285F4" d="M45.12 24.5c0-1.56-.14-3.06-.4-4.5H24v8.51h11.84c-.51 2.75-2.06 5.08-4.39 6.64v5.52h7.11c4.16-3.83 6.56-9.47 6.56-16.17z"/><path fill="#34A853" d="M24 46c5.94 0 10.92-1.97 14.56-5.33l-7.11-5.52c-1.97 1.32-4.49 2.1-7.45 2.1-5.73 0-10.58-3.87-12.31-9.07H4.34v5.7C7.96 41.07 15.4 46 24 46z"/><path fill="#FBBC05" d="M11.69 28.18C11.25 26.86 11 25.45 11 24s.25-2.86.69-4.18v-5.7H4.34C2.85 17.09 2 20.45 2 24s.85 6.91 2.34 9.88l7.35-5.7z"/><path fill="#EA4335" d="M24 10.75c3.23 0 6.13 1.11 8.41 3.29l6.31-6.31C34.91 4.18 29.93 2 24 2 15.4 2 7.96 6.93 4.34 14.12l7.35 5.7c1.73-5.2 6.58-9.07 12.31-9.07z"/></svg>
        Continue with Google
      </button>
      <div class="orline">or email</div>
      <label>Email</label><input id="li-email" type="email" autocomplete="email" placeholder="fei@betauniversity.org">
      <label>Password</label><input id="li-pw" type="password" autocomplete="current-password">
      <button type="button" onclick="doLogin()">Sign in</button>
      <button type="button" class="alt" onclick="showSignup()">First time here? Create account</button>
    </div>
    <div id="form-signup" class="hide">
      <label>Email</label><input id="su-email" type="email" autocomplete="email" placeholder="fei@betauniversity.org">
      <label>Password</label><input id="su-pw" type="password" autocomplete="new-password">
      <div class="sub" style="margin-top:7px">8+ characters: upper, lower, number, and a symbol.</div>
      <button type="button" onclick="doSignup()">Create account</button>
      <button type="button" class="alt" onclick="showLogin()">Back to sign in</button>
    </div>
    <div id="form-verify" class="hide">
      <label>6-digit code: check your email</label><input id="vf-code" type="text" inputmode="numeric" autocomplete="one-time-code">
      <button type="button" onclick="doVerify()">Verify &amp; sign in</button>
    </div>
    <div class="msg" id="msg"></div>
  </div>
</div>

<!-- ===== APP ===== -->
<div id="app" class="hide">
  <div id="bar">
    <span class="t">BETA NETWORK OPC</span>
    <span class="ro" id="bar-mode">loading…</span>
    <span class="sp"></span>
    <span class="live off" id="bar-live"><span class="dot"></span><span id="bar-live-t">connecting</span></span>
    <a href="/workstands.html" style="padding:6px 12px;font-size:9px;font-family:var(--mono);font-weight:600;letter-spacing:.07em;text-transform:uppercase;text-decoration:none;border:1.1px solid #33414f;color:#9aa6b2;">Where work stands</a>
    <button type="button" onclick="signOut()">Sign out</button>
  </div>

  <div class="sheet">
    <svg class="reg reg-tl"><use href="#reg"></use></svg>
    <svg class="reg reg-tr"><use href="#reg"></use></svg>
    <svg class="reg reg-bl"><use href="#reg"></use></svg>
    <svg class="reg reg-br"><use href="#reg"></use></svg>

    <!-- ====================== TITLE BLOCK ====================== -->
    <header class="titleblock">
      <div class="tb-main">
        <div class="tb-project" data-key="title.project"></div>
        <h1 class="tb-title" data-key="title.heading"></h1>
      </div>
      <div class="tb-side">
        <div class="tb-cell tb-wide"><span class="tb-k">Drawing No.</span><span class="tb-v accent" data-key="title.drawing_no"></span></div>
        <div class="tb-cell"><span class="tb-k">Revision</span><span class="tb-v" data-key="title.revision"></span></div>
        <div class="tb-cell"><span class="tb-k">Issued</span><span class="tb-v" data-key="title.issued"></span></div>
        <div class="tb-cell"><span class="tb-k">Scale</span><span class="tb-v" data-key="title.scale"></span></div>
        <div class="tb-cell"><span class="tb-k">Sheet</span><span class="tb-v" data-key="title.sheet"></span></div>
        <div class="tb-cell tb-wide tb-last"><span class="tb-k">Drawn by</span><span class="tb-v" data-key="title.drawn_by"></span></div>
      </div>
    </header>

    <div class="revnote">
      <span class="rv-tag">&#9650; REV NOTE</span>
      <div data-key="title.revnote"></div>
    </div>

    <!-- app-logo symbols -->
    <svg width="0" height="0" style="position:absolute" aria-hidden="true">
    <defs>
      <symbol id="lo-md" viewBox="0 0 24 24">
        <rect x="2" y="2" width="20" height="20" rx="5" fill="#3a4250"/>
        <path d="M5.6 16.5V9l3.6 4.3L12.8 9v7.5" fill="none" stroke="#fff" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
        <path d="M16.4 9.4v6m0 0l-2.3-2.5m2.3 2.5l2.3-2.5" fill="none" stroke="#fff" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/>
      </symbol>
      <symbol id="lo-pages" viewBox="0 0 24 24">
        <rect x="2" y="2" width="20" height="20" rx="5" fill="#f4ebd0"/>
        <text x="12" y="17.4" font-family="Georgia,'Times New Roman',serif" font-size="15" font-weight="700" fill="#2b2b2b" text-anchor="middle">P</text>
      </symbol>
      <symbol id="lo-gcal" viewBox="0 0 24 24">
        <rect x="3" y="4" width="18" height="17" rx="3" fill="#fff" stroke="#dadce0" stroke-width="1"/>
        <rect x="6.8" y="2.4" width="2.4" height="4.6" rx="1.2" fill="#5f6368"/>
        <rect x="14.8" y="2.4" width="2.4" height="4.6" rx="1.2" fill="#5f6368"/>
        <text x="12" y="17.6" font-family="Arial,Helvetica,sans-serif" font-size="11.5" font-weight="700" fill="#4285f4" text-anchor="middle">31</text>
      </symbol>
      <symbol id="lo-luma" viewBox="0 0 24 24">
        <rect x="2" y="2" width="20" height="20" rx="5" fill="#f4604d"/>
        <path d="M12 5c.6 4 2.4 5.8 6.4 6.4-4 .6-5.8 2.4-6.4 6.4-.6-4-2.4-5.8-6.4-6.4 4-.6 5.8-2.4 6.4-6.4z" fill="#fff"/>
      </symbol>
      <symbol id="lo-granola" viewBox="0 0 24 24">
        <rect x="2" y="2" width="20" height="20" rx="5" fill="#e08a4a"/>
        <g stroke="#fff" stroke-width="2.1" stroke-linecap="round">
          <line x1="6.5" y1="10.4" x2="6.5" y2="13.6"/>
          <line x1="9.5" y1="7.4" x2="9.5" y2="16.6"/>
          <line x1="12" y1="9.6" x2="12" y2="14.4"/>
          <line x1="14.5" y1="6.6" x2="14.5" y2="17.4"/>
          <line x1="17.5" y1="10" x2="17.5" y2="14"/>
        </g>
      </symbol>
      <symbol id="lo-google" viewBox="0 0 24 24">
        <rect x="2" y="2" width="20" height="20" rx="5" fill="#fff" stroke="#dadce0" stroke-width="1"/>
        <path d="M12 10.9v2.7h3.8c-.17 1.05-1.3 3.05-3.8 3.05a4.15 4.15 0 1 1 0-8.3c1.25 0 2.1.55 2.6 1l1.9-1.85C17.2 6.35 14.8 5.3 12 5.3a6.7 6.7 0 1 0 0 13.4c3.85 0 6.4-2.7 6.4-6.5 0-.45-.05-.78-.11-1.1z" fill="#4285f4"/>
      </symbol>
      <symbol id="reg" viewBox="0 0 24 24">
        <circle cx="12" cy="12" r="7.4" fill="none" stroke="currentColor" stroke-width="1.1"/>
        <path d="M12 1.4v8.2M12 14.4v8.2M1.4 12h8.2M14.4 12h8.2" stroke="currentColor" stroke-width="1.1"/>
      </symbol>
    </defs>
    </svg>

    <!-- ====================== SVG SCHEMATIC ====================== -->
    <div class="diagram">
    <div class="dwg-head"><b>DWG&nbsp;01</b> BETA NETWORK OPC <span class="nts">click any text to edit · icons jump to source</span></div>
    <svg viewBox="0 0 820 285" role="img" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <marker id="ar" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
        <path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
      </marker>
    </defs>

    <!-- DIRECTION -->
    <text class="cap" x="20" y="22">DIRECTION · WHY AND WHAT</text>
    <g class="hit" data-card="c-direction">
      <rect class="hitbox" x="20" y="32" width="780" height="241" rx="5" fill="#f4f7f9" stroke="#33414f" stroke-width="1"/>
      <text class="tt" x="40" y="52" data-key="direction.mission_label"></text>
      <text class="ts" x="40" y="67" data-key="direction.mission_l1"></text>
      <text class="ts" x="40" y="79" data-key="direction.mission_l2"></text>
      <text class="tt" x="40" y="99" data-key="direction.vision_label"></text>
      <text class="ts" x="40" y="114" data-key="direction.vision_l1"></text>
      <text class="ts" x="40" y="126" data-key="direction.vision_l2"></text>
      <text class="ts" x="40" y="138" data-key="direction.vision_l3"></text>
      <line x1="40" y1="148" x2="780" y2="148" stroke="#33414f" stroke-width="0.5" opacity="0.4"/>
      <text class="tt" x="40" y="164" fill="#33414f">STRATEGY<tspan fill="#9aa6b2"> · click for the full strategy doc</tspan></text>
      <text class="ts" x="40" y="179" data-key="direction.strategy_l1"></text>
      <text class="ts" x="40" y="191" data-key="direction.strategy_l2"></text>
    </g>

    <!-- OKR bar, inside the Direction box under Strategy -->
    <line x1="403" y1="203" x2="403" y2="223" stroke="#2f5d8a" stroke-width="1.5" marker-end="url(#ar)"/>
    <line x1="417" y1="223" x2="417" y2="203" stroke="#9c5a22" stroke-width="1.5" marker-end="url(#ar)"/>
    <text class="ts" x="395" y="216" text-anchor="end" fill="#2f5d8a">drives</text>
    <text class="ts" x="425" y="216" fill="#9c5a22">learning</text>

    <g class="hit" data-card="c-okr">
      <rect class="hitbox" x="32" y="227" width="756" height="34" rx="3" fill="#e7edf3" stroke="#2f5d8a" stroke-width="1.1"/>
      <text class="th" x="44" y="249" fill="#2f5d8a" font-size="14">OKR</text>
      <text class="ts" x="84" y="249" data-key="okr.bar.tagline"></text>
    </g>


    </svg>

<div class="opm"><div class="ledger-cap">OPERATING MODEL · Q4 2026</div><svg viewBox="0 0 820 560" role="img" xmlns="http://www.w3.org/2000/svg">
<defs><marker id="opm-ar" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#586573"/></marker><marker id="opm-loop" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" fill="#2f5d8a"/></marker></defs>
<text data-key="opm.talent-label" x="20" y="20" font-size="10" font-weight="600" fill="#586573">U.S. AI TALENT</text>
<path d="M677 44V28H143V42" fill="none" stroke="#2f5d8a" stroke-width="2" marker-end="url(#opm-loop)"/>
<text data-key="opm.loop-label" x="410" y="20" font-size="10" font-weight="600" fill="#1b2531" text-anchor="middle">founder loop: our founders bring talent back</text>
<g fill="none" stroke="#586573" stroke-width="1"><path d="M266 84H285" marker-end="url(#opm-ar)"/><path d="M533 84H552" marker-end="url(#opm-ar)"/></g>
<rect class="opm-outer" data-row="top" data-code="o1" x="20" y="44" width="246" height="200" rx="0" fill="none" stroke="#33414f" stroke-width="1"/>
<text data-key="opm.relate-name" x="30" y="61" font-size="11.5" font-weight="600" fill="#1b2531">Community arm · BETA</text>
<text data-key="opm.relate-site" x="30" y="73" font-size="8" font-weight="400" fill="#586573">betauniversity.org</text>
<g class="opm-pri" data-row="top" data-code="o1" data-kind="str" data-list="opm.o1.strategy" data-x="30" data-right="256" data-default="大撬中，中撬小: big names pull in the next tier"></g>
<rect class="opm-tint" data-row="top" data-code="o1" data-list="opm.o1.pri" x="24" y="100" width="238" height="100" fill="#e7edf3"/>
<text data-key="opm.o1-name" x="30" y="118" font-size="10" font-weight="600" fill="#1b2531">O1 · Talent keeps coming back</text>
<text class="opm-ownl" data-code="o1" x="30" y="132" font-size="7.5" font-weight="700" letter-spacing=".04em" fill="#2f5d8a">Owner</text>
<text data-key="opm.o1-owner" x="30" y="132" font-size="9" font-weight="600" fill="#2f5d8a">@Kathryn</text>
<g class="opm-pri" data-row="top" data-code="o1" data-kind="kr" data-list="opm.o1.kr" data-x="30" data-right="256" data-default="Top talents ~50 → 100&#10;Came back ~14% → 50%"></g>
<g class="opm-pri" data-row="top" data-code="o1" data-kind="pri" data-list="opm.o1.pri" data-x="30" data-right="256" data-default="Researcher dinners: monthly&#10;Fireside chats: 2 in Q4&#10;Second touch for all · Dec 15"></g>
<rect class="opm-outer" data-row="top" data-code="o2" x="287" y="44" width="246" height="200" rx="0" fill="none" stroke="#33414f" stroke-width="1"/>
<text data-key="opm.preserve-name" x="297" y="61" font-size="11.5" font-weight="600" fill="#1b2531">Expert consulting arm</text>
<text data-key="opm.preserve-site" x="297" y="73" font-size="8" font-weight="400" fill="#586573">vetainsights.com</text>
<g class="opm-pri" data-row="top" data-code="o2" data-kind="str" data-list="opm.o2.strategy" data-x="297" data-right="523" data-default="100% 数字化记录，复用: record every call, reuse it"></g>
<rect class="opm-tint" data-row="top" data-code="o2" data-list="opm.o2.pri" x="291" y="100" width="238" height="100" fill="#e7edf3"/>
<text data-key="opm.o2-name" x="297" y="118" font-size="10" font-weight="600" fill="#1b2531">O2 · Knowledge gets reused</text>
<text class="opm-ownl" data-code="o2" x="297" y="132" font-size="7.5" font-weight="700" letter-spacing=".04em" fill="#2f5d8a">Owner</text>
<text data-key="opm.o2-owner" x="297" y="132" font-size="9" font-weight="600" fill="#2f5d8a">@Janet</text>
<g class="opm-pri" data-row="top" data-code="o2" data-kind="kr" data-list="opm.o2.kr" data-x="297" data-right="523" data-default="Digitized in 7 days → 100%&#10;Used in insights posts → 80%"></g>
<g class="opm-pri" data-row="top" data-code="o2" data-kind="pri" data-list="opm.o2.pri" data-x="297" data-right="523" data-default="Beta Archive live · Nov 1&#10;Gap check: weekly&#10;Insights posts: 2-3 a week&#10;Founder stories: ongoing"></g>
<rect class="opm-outer" data-row="top" data-code="o3" x="554" y="44" width="246" height="200" rx="0" fill="none" stroke="#33414f" stroke-width="1"/>
<text data-key="opm.invest-name" x="564" y="61" font-size="11.5" font-weight="600" fill="#1b2531">Investment arm</text>
<text data-key="opm.invest-site" x="564" y="73" font-size="8" font-weight="400" fill="#586573">llamabeta.com → betafund.ai</text>
<g class="opm-pri" data-row="top" data-code="o3" data-kind="str" data-list="opm.o3.strategy" data-x="564" data-right="790" data-default="跟投: follow the best lead investors"></g>
<rect class="opm-tint" data-row="top" data-code="o3" data-list="opm.o3.pri" x="558" y="100" width="238" height="100" fill="#e7edf3"/>
<text data-key="opm.o3-name" x="564" y="118" font-size="10" font-weight="600" fill="#1b2531">O3 · Only the best founders</text>
<text class="opm-ownl" data-code="o3" x="564" y="132" font-size="7.5" font-weight="700" letter-spacing=".04em" fill="#2f5d8a">Owner</text>
<text data-key="opm.o3-owner" x="564" y="132" font-size="9" font-weight="600" fill="#2f5d8a">@David</text>
<g class="opm-pri" data-row="top" data-code="o3" data-kind="kr" data-list="opm.o3.kr" data-x="564" data-right="790" data-default="New portcos 3 → 10&#10;Active co-investors → 25"></g>
<g class="opm-pri" data-row="top" data-code="o3" data-kind="pri" data-list="opm.o3.pri" data-x="564" data-right="790" data-default="Co-invest: live deals weekly&#10;Decide direct deals · Nov 30&#10;Founder scouting · Nov 15&#10;Talent check-ins: monthly"></g>
<g id="opm-low" transform="translate(0,0)">
<g fill="none" stroke="#586573" stroke-width="1"><path d="M143 232V248" marker-end="url(#opm-ar)"/><path d="M410 232V248" marker-end="url(#opm-ar)"/><path d="M677 232V248" marker-end="url(#opm-ar)"/><path d="M143 352V308" marker-end="url(#opm-ar)"/><path d="M410 352V308" marker-end="url(#opm-ar)"/><path d="M677 352V308" marker-end="url(#opm-ar)"/></g>
<rect x="20" y="252" width="780" height="28" fill="#f4f7f9" stroke="#33414f" stroke-width="1"/>
<text data-key="opm.asset-name" x="410" y="271" font-size="11.5" font-weight="600" fill="#1b2531" text-anchor="middle">Our relationships with world-class talent</text>
<text data-key="opm.pays-1" x="151" y="332" font-size="10" font-weight="400" fill="#586573">pays</text>
<text data-key="opm.pays-2" x="418" y="332" font-size="10" font-weight="400" fill="#586573">pays</text>
<text data-key="opm.pays-3" x="685" y="332" font-size="10" font-weight="400" fill="#586573">invests</text>
<rect class="opm-outer" data-row="bot" data-code="o1d" x="20" y="44" width="246" height="200" rx="0" fill="none" stroke="#33414f" stroke-width="1"/>
<text data-key="opm.ent-name" x="30" y="66" font-size="11.5" font-weight="600" fill="#1b2531">Asia companies</text>
<g class="opm-pri" data-row="bot" data-code="o1d" data-kind="str" data-list="opm.o1d.strategy" data-x="30" data-right="256" data-default="Companies pay to reach our talent"></g>
<rect class="opm-tint" data-row="bot" data-code="o1d" data-list="opm.o1d.pri" x="24" y="100" width="238" height="100" fill="#e7edf3"/>
<text data-key="opm.o1d-name" x="30" y="118" font-size="10" font-weight="600" fill="#1b2531">O1$ · Companies pay</text>
<text class="opm-ownl" data-code="o1d" x="30" y="132" font-size="7.5" font-weight="700" letter-spacing=".04em" fill="#2f5d8a">Owner</text>
<text data-key="opm.o1d-owner" x="30" y="132" font-size="9" font-weight="600" fill="#2f5d8a">@Daniel</text>
<g class="opm-pri" data-row="bot" data-code="o1d" data-kind="kr" data-list="opm.o1d.kr" data-x="30" data-right="256" data-default="Event revenue → 2× Q3&#10;Paid enterprise hires → 2"></g>
<g class="opm-pri" data-row="bot" data-code="o1d" data-kind="pri" data-list="opm.o1d.pri" data-x="30" data-right="256" data-default="2 paying sponsors/hackathon&#10;CES sponsors signed · Dec 15&#10;Hiring: 10 offers, 2 paid"></g>
<rect class="opm-outer" data-row="bot" data-code="o2d" x="287" y="44" width="246" height="200" rx="0" fill="none" stroke="#33414f" stroke-width="1"/>
<text data-key="opm.brk-name" x="297" y="66" font-size="11.5" font-weight="600" fill="#1b2531">Brokerages, 研学 buyers</text>
<g class="opm-pri" data-row="bot" data-code="o2d" data-kind="str" data-list="opm.o2d.strategy" data-x="297" data-right="523" data-default="Pay per call, per report, per study trip"></g>
<rect class="opm-tint" data-row="bot" data-code="o2d" data-list="opm.o2d.pri" x="291" y="100" width="238" height="100" fill="#e7edf3"/>
<text data-key="opm.o2d-name" x="297" y="118" font-size="10" font-weight="600" fill="#1b2531">O2$ · Paid expert insight</text>
<text class="opm-ownl" data-code="o2d" x="297" y="132" font-size="7.5" font-weight="700" letter-spacing=".04em" fill="#2f5d8a">Owner</text>
<text data-key="opm.o2d-owner" x="297" y="132" font-size="9" font-weight="600" fill="#2f5d8a">@Jack</text>
<g class="opm-pri" data-row="bot" data-code="o2d" data-kind="kr" data-list="opm.o2d.kr" data-x="297" data-right="523" data-default="Consulting cash → $300K&#10;研学 老板: target set Nov 6"></g>
<g class="opm-pri" data-row="bot" data-code="o2d" data-kind="pri" data-list="opm.o2d.pri" data-x="297" data-right="523" data-default="Collect ¥2.93M, 民生 first&#10;小红书 + 字节 paid work · Dec 31&#10;研学 Q4 roster · Nov 15&#10;证券 Update: monthly"></g>
<rect class="opm-outer" data-row="bot" data-code="o3d" x="554" y="44" width="246" height="200" rx="0" fill="none" stroke="#33414f" stroke-width="1"/>
<text data-key="opm.fo-name" x="564" y="66" font-size="11.5" font-weight="600" fill="#1b2531">Family offices</text>
<g class="opm-pri" data-row="bot" data-code="o3d" data-kind="str" data-list="opm.o3d.strategy" data-x="564" data-right="790" data-default="Fund + 老股 SPVs: fees now, profit share later"></g>
<rect class="opm-tint" data-row="bot" data-code="o3d" data-list="opm.o3d.pri" x="558" y="100" width="238" height="100" fill="#e7edf3"/>
<text data-key="opm.o3d-name" x="564" y="118" font-size="10" font-weight="600" fill="#1b2531">O3$ · Investors fund us</text>
<text class="opm-ownl" data-code="o3d" x="564" y="132" font-size="7.5" font-weight="700" letter-spacing=".04em" fill="#2f5d8a">Owner</text>
<text data-key="opm.o3d-owner" x="564" y="132" font-size="9" font-weight="600" fill="#2f5d8a">@Lilly</text>
<g class="opm-pri" data-row="bot" data-code="o3d" data-kind="kr" data-list="opm.o3d.kr" data-x="564" data-right="790" data-default="Direct (fund): target Nov 6&#10;Indirect (SPV 老股) → 2× Q4"></g>
<g class="opm-pri" data-row="bot" data-code="o3d" data-kind="pri" data-list="opm.o3d.pri" data-x="564" data-right="790" data-default="SPV on each bar-clearing deal&#10;Share each deal within a week&#10;LP insights: monthly"></g>
</g>
<g id="opm-cap" transform="translate(0,0)">
<text data-key="opm.asia-label" x="20" y="534" font-size="10" font-weight="600" fill="#586573">ASIA CLIENTS · pay for access to the talent above</text>
<text data-key="opm.key" x="20" y="554" font-size="9" font-weight="400" fill="#586573">Q4 2026 · KR = now → target</text>
</g>
</svg></div>

    <svg viewBox="0 0 820 180" role="img" xmlns="http://www.w3.org/2000/svg">
    <!-- ENABLING LAYER -->
    <a href="/incentives.html" target="_blank" rel="noopener">
      <rect x="20" y="20" width="254" height="46" rx="3" fill="#eef0f2" stroke="#abb5c0" stroke-width="0.9" style="cursor:pointer"/>
      <text class="tt" x="36" y="39" fill="#586573" data-key="enabling.talent.title"></text>
      <text class="pri" x="36" y="54" data-key="enabling.talent.note"></text>
    </a>
    <rect x="283" y="20" width="254" height="46" rx="3" fill="#eef0f2" stroke="#abb5c0" stroke-width="0.9"/>
    <text class="tt" x="299" y="39" fill="#586573" data-key="enabling.capital.title"></text>
    <text class="pri" x="299" y="54" data-key="enabling.capital.note"></text>
    <rect x="546" y="20" width="254" height="46" rx="3" fill="#eef0f2" stroke="#abb5c0" stroke-width="0.9"/>
    <text class="tt" x="562" y="39" fill="#586573" data-key="enabling.legal.title"></text>
    <text class="pri" x="562" y="54" fill="#9aa6b2" data-key="enabling.legal.note"></text>

    <!-- OPERATING RHYTHM -->
    <rect x="20" y="84" width="780" height="20" fill="#e4e9ed"/>
    <text class="cap" x="36" y="98" data-key="rhythm.banner"></text>
    <a href="https://calendar.google.com" target="_blank" rel="noopener"><use href="#lo-gcal" x="748" y="87" width="14" height="14"/></a>
    <a href="https://app.granola.ai" target="_blank" rel="noopener"><use href="#lo-granola" x="768" y="87" width="14" height="14"/></a>

    <g class="hit" data-card="c-rhythm">
      <rect class="hitbox" x="20" y="104" width="156" height="56" fill="#f4f7f9" stroke="#abb5c0"/>
      <text class="th" x="98" y="124" text-anchor="middle" data-key="rhythm.quarterly.title"></text>
      <text class="ts" x="98" y="138" text-anchor="middle" data-key="rhythm.quarterly.l1"></text>
      <text class="ts" x="98" y="149" text-anchor="middle" data-key="rhythm.quarterly.l2"></text>
    </g>
    <g class="hit" data-card="c-rhythm">
      <rect class="hitbox" x="176" y="104" width="156" height="56" fill="#f4f7f9" stroke="#abb5c0"/>
      <text class="th" x="254" y="124" text-anchor="middle" data-key="rhythm.monthly.title"></text>
      <text class="ts" x="254" y="138" text-anchor="middle" data-key="rhythm.monthly.l1"></text>
      <text class="ts" x="254" y="149" text-anchor="middle" data-key="rhythm.monthly.l2"></text>
    </g>
    <g class="hit" data-card="c-rhythm">
      <rect class="hitbox" x="332" y="104" width="156" height="56" fill="#fbeeec" stroke="#bf3a2b" stroke-width="1.5"/>
      <text class="th" x="410" y="124" text-anchor="middle" fill="#bf3a2b" data-key="rhythm.weekly.title"></text>
      <text class="ts" x="410" y="138" text-anchor="middle" data-key="rhythm.weekly.l1"></text>
      <text class="ts" x="410" y="149" text-anchor="middle" data-key="rhythm.weekly.l2"></text>
    </g>
    <g class="hit" data-card="c-rhythm">
      <rect class="hitbox" x="488" y="104" width="156" height="56" fill="#f4f7f9" stroke="#abb5c0"/>
      <text class="th" x="566" y="124" text-anchor="middle" data-key="rhythm.daily.title"></text>
      <text class="ts" x="566" y="138" text-anchor="middle" data-key="rhythm.daily.l1"></text>
      <text class="ts" x="566" y="149" text-anchor="middle" data-key="rhythm.daily.l2"></text>
    </g>
    <g class="hit" data-card="c-found">
      <rect class="hitbox" x="644" y="104" width="156" height="56" fill="#f4f7f9" stroke="#abb5c0"/>
      <text class="th" x="722" y="124" text-anchor="middle" data-key="rhythm.friday.title"></text>
      <text class="ts" x="722" y="138" text-anchor="middle" data-key="rhythm.friday.l1"></text>
      <text class="ts" x="722" y="149" text-anchor="middle" data-key="rhythm.friday.l2"></text>
    </g>
    <rect x="20" y="84" width="780" height="76" fill="none" stroke="#33414f" stroke-width="1.1"/>
    
    </svg>
    </div>

    <!-- ===== DOSSIER TEMPLATES (hidden source for modal content) ===== -->
    <div class="dossiers" hidden>

    <details class="dir" id="c-direction">
      <summary><span class="s-title">Direction</span><span class="s-meta" data-key="dossier.direction.meta"></span></summary>
      <div class="body">
        <div class="lo-row"><span class="ar">lives on</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span></div>
        <h4>Mission</h4>
        <p data-key="dossier.direction.mission"></p>
        <h4>Vision</h4>
        <p data-key="dossier.direction.vision"></p>
        <h4>Three verbs</h4>
        <ul>
          <li data-key="dossier.direction.tier.think"></li>
          <li data-key="dossier.direction.tier.build"></li>
          <li data-key="dossier.direction.tier.invest"></li>
        </ul>
        <h4>Strategy</h4>
        <p data-key="dossier.direction.strategy"></p>
        <h4 data-key="dossier.direction.plays.h">Three plays, one per verb</h4>
        <ul><li data-key="dossier.direction.play.1"><b>撬 · Relate: 大撬中，中撬小.</b> Big names pull in the next tier, and the next tier pulls in the rising builders around them. Our access to top names goes to the top of the ladder: firesides with names like Fei-Fei Li (大) fill the researcher dinners (中), and those researchers bring the builders who come to hackathons and warm intros (小). <b>Measured by O1</b>: 100 top talents, 50% come back. Companies pay for the rooms and the hires (O1$).</li><li data-key="dossier.direction.play.2"><b>Preserve: 100% 数字化记录，复用.</b> Every call, dinner and interview is recorded, tagged and filed in the Beta Archive within 7 days, then reused in insights posts, client reports, deal memos and LP updates. Unrecorded, a conversation pays once. Recorded, it pays every time it is reused, and it trains the BETA Brain project. <b>Measured by O2</b>: 100% digitized, 80% used. Brokerages and 研学 buyers pay for the insight (O2$).</li><li data-key="dossier.direction.play.3"><b>跟投 · Invest.</b> We follow the best lead investors into rounds instead of leading and pricing them. Co-investors bring the deals, the talent bar decides which ones we take, and every co-invest strengthens the tie that brings the next deal. <b>Measured by O3</b>: 10 bar-clearing portfolio companies, 25 active co-investors. Family offices fund the SPVs and the fund (O3$).</li></ul>
        <h4>Key bets 2026</h4>
        <ul>
          <li data-key="dossier.direction.bet.1"></li>
          <li data-key="dossier.direction.bet.2"></li>
          <li data-key="dossier.direction.bet.3"></li>
          <li data-key="dossier.direction.bet.4"></li>
        </ul>
        <h4>What we are NOT doing</h4>
        <ul>
          <li class="no" data-key="dossier.direction.not.1"></li>
          <li class="no" data-key="dossier.direction.not.2"></li>
          <li class="no" data-key="dossier.direction.not.3"></li>
        </ul>
      </div>
    </details>

    <details class="dir" id="c-okr">
      <summary><span class="s-title">OKRs · Q4 2026</span><span class="s-meta" data-key="dossier.okr.meta"></span></summary>
      <div class="body">
        <div class="lo-row"><span class="ar">objectives + KRs →</span><a class="lo" href="https://claude.ai/artifact/2fL6xd8RvK4J6u2LmBEXHT" target="_blank" rel="noopener">OKR review ↗</a><span class="ar">· priorities →</span><span class="lo">key priorities in each OKR box</span></div>
        <p data-key="dossier.okr.intro"></p>
        <h4>Customer-value OKRs</h4>
        <ul>
          <li data-key="okr.1"></li>
          <li data-key="okr.2"></li>
          <li data-key="okr.3"></li>
          <li data-key="okr.4"></li>
          <li data-key="okr.5"></li>
          <li data-key="okr.6"></li>
        </ul>
        <h4>Open items: settle by Nov 6</h4>
        <ul>
          <li class="done" data-key="dossier.okr.open.1"></li>
          <li class="q" data-key="dossier.okr.open.2"></li>
          <li class="q" data-key="dossier.okr.open.3"></li>
        </ul>
      </div>
    </details>

    <details class="n2" id="c-n2">
      <summary><span class="s-title">Research</span><span class="s-meta" data-key="dossier.think.meta"></span></summary>
      <div class="body">
        <div class="cell">
          <div class="cell-tag">Research · demand</div>
          <div class="okr" data-key="dossier.think.demand.okr"></div>
          <div class="kr"><span class="x">X</span> <span data-key="dossier.think.demand.kr"></span></div>
          <div class="price" data-key="dossier.think.demand.price"></div>
          <div class="lo-row tight"><span class="ar">objective + KR &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span><span class="ar">· priorities &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-pages"></use></svg>butterPages</span></div>
          <ul class="pri-list">
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.think.demand.pri.1"></span></li>
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.think.demand.pri.2"></span></li>
          </ul>
          <div class="src"><span class="lbl">↗ data · seg 2.1 ·</span>
            <a href="https://docs.google.com/spreadsheets/d/1CF7nyTJGfUrS-oUDTvZA3iKdFUbkc_bKvtEm6l98Rew/edit#gid=205812973" target="_blank" rel="noopener">二级证券 / 研学 — Google Sheet</a></div>
          <div class="live-pill"><span class="hd">LIVE</span><span data-key="dossier.think.demand.live"></span></div>
        </div>
        <div class="cell">
          <div class="cell-tag">Research · supply</div>
          <div class="okr" data-key="dossier.think.supply.okr"></div>
          <div class="kr"><span class="x">X</span> <span data-key="dossier.think.supply.kr"></span></div>
          <div class="lo-row tight"><span class="ar">objective + KR &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span><span class="ar">· priorities &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-pages"></use></svg>butterPages</span></div>
          <ul class="pri-list">
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.think.supply.pri.1"></span></li>
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.think.supply.pri.2"></span></li>
          </ul>
          <div class="src"><span class="lbl">↗ data · seg 2.2 ·</span>
            <a href="https://docs.google.com/spreadsheets/d/1QW4nUyve2mxhWJ9JRr_gulrgFLUCnDRBtjQnpwoE7XU/edit" target="_blank" rel="noopener">experts — Google Sheet</a></div>
          <div class="live-pill"><span class="hd">LIVE</span><span data-key="dossier.think.supply.live"></span></div>
        </div>
        <div class="cell empty">
          <div class="cell-tag">Research · catalyst</div>
          <div class="note" data-key="dossier.think.catalyst.note"></div>
        </div>
      </div>
    </details>

    <details class="n1" id="c-n1">
      <summary><span class="s-title">Hackathons</span><span class="s-meta" data-key="dossier.build.meta"></span></summary>
      <div class="body">
        <div class="cell">
          <div class="cell-tag">Hackathons · demand</div>
          <div class="okr" data-key="dossier.build.demand.okr"></div>
          <div class="kr"><span class="x">X</span> <span data-key="dossier.build.demand.kr"></span></div>
          <div class="price" data-key="dossier.build.demand.price"></div>
          <div class="lo-row tight"><span class="ar">objective + KR &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span><span class="ar">· priorities &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-pages"></use></svg>butterPages</span></div>
          <ul class="pri-list">
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.build.demand.pri.1"></span></li>
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.build.demand.pri.2"></span></li>
          </ul>
          <div class="src"><span class="lbl">↗ data · seg 1.1 ·</span>
            <a href="https://docs.google.com/spreadsheets/d/1Xy-6eLqo18voWjDlzkTq5IWD-my5irGDLYegko0tGc0/edit#gid=1085694298" target="_blank" rel="noopener">企业业务部门 — Google Sheet</a></div>
          <div class="live-pill"><span class="hd">LIVE</span><span data-key="dossier.build.demand.live"></span></div>
        </div>
        <div class="cell">
          <div class="cell-tag">Hackathons · supply</div>
          <div class="okr" data-key="dossier.build.supply.okr"></div>
          <div class="kr"><span class="x">X</span> <span data-key="dossier.build.supply.kr"></span></div>
          <div class="lo-row tight"><span class="ar">objective + KR &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span><span class="ar">· priorities &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-pages"></use></svg>butterPages</span></div>
          <ul class="pri-list">
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.build.supply.pri.1"></span></li>
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.build.supply.pri.2"></span></li>
          </ul>
          <div class="src"><span class="lbl">↗ data · seg 1.2 ·</span>
            <a href="https://lu.ma/cal-MvY3wcADGCzQG99" target="_blank" rel="noopener">hackathon builders — Luma calendar</a></div>
          <div class="live-pill"><span class="hd">LIVE</span><span data-key="dossier.build.supply.live"></span></div>
        </div>
        <div class="cell">
          <div class="cell-tag">Hackathons · catalyst — community partners</div>
          <div class="note" style="font-style:italic;color:var(--ink-3)" data-key="dossier.build.catalyst.note"></div>
          <div class="src"><span class="lbl">↗ data · seg 1.3 ·</span>
            <a href="https://docs.google.com/spreadsheets/d/1dp9IcHnv6fyrlGbY9eddWxhe3dRFSEVFs9E_kuI7gfg/edit" target="_blank" rel="noopener">community partners — Google Sheet</a></div>
          <div class="live-pill"><span class="hd">LIVE</span><span data-key="dossier.build.catalyst.live"></span></div>
        </div>
      </div>
    </details>

    <details class="n3" id="c-n3">
      <summary><span class="s-title">Fund — North Star</span><span class="s-meta" data-key="dossier.fund.meta"></span></summary>
      <div class="body">
        <div class="cell">
          <div class="cell-tag">Fund · supply</div>
          <div class="okr" data-key="dossier.fund.supply.okr"></div>
          <div class="kr"><span class="x">X</span> <span data-key="dossier.fund.supply.kr"></span></div>
          <div class="lo-row tight"><span class="ar">objectives + KRs &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span><span class="ar">· priorities &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-pages"></use></svg>butterPages</span></div>
          <ul class="pri-list">
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.fund.supply.pri.1"></span></li>
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.fund.supply.pri.2"></span></li>
          </ul>
          <div class="src"><span class="lbl">↗ data · seg 3.3 ·</span>
            <a href="https://docs.google.com/spreadsheets/d/1EiHX8RD6Ids4WUauqtPrX6f4yZlRm2Oy9g9UAMUZiME/edit" target="_blank" rel="noopener">founders — Google Sheet</a></div>
          <div class="live-pill"><span class="hd">LIVE</span><span data-key="dossier.fund.supply.live"></span></div>
        </div>
        <div class="cell">
          <div class="cell-tag">Fund · demand — LP pipeline + 老股broker</div>
          <div class="okr" data-key="dossier.fund.demand.okr"></div>
          <div class="kr"><span class="x">X</span> <span data-key="dossier.fund.demand.kr"></span></div>
          <div class="lo-row tight"><span class="ar">objectives + KRs &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span><span class="ar">· priorities &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-pages"></use></svg>butterPages</span></div>
          <ul class="pri-list">
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.fund.demand.pri.1"></span></li>
            <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.fund.demand.pri.2"></span></li>
          </ul>
          <div class="src"><span class="lbl">↗ data · seg 3.1 ·</span>
            <a href="https://docs.google.com/spreadsheets/d/1CF7nyTJGfUrS-oUDTvZA3iKdFUbkc_bKvtEm6l98Rew/edit#gid=0" target="_blank" rel="noopener">家办 + 企业CFO/战投 — Google Sheet</a></div>
          <div class="live-pill"><span class="hd">LIVE</span><span data-key="dossier.fund.demand.live"></span></div>
        </div>
        <div class="cell empty">
          <div class="cell-tag">Fund · catalyst — co-investors</div>
          <div class="note" data-key="dossier.fund.catalyst.note"></div>
        </div>
      </div>
    </details>

    <details class="dir" id="c-rhythm">
      <summary><span class="s-title">Operating Rhythm</span><span class="s-meta" data-key="dossier.rhythm.meta"></span></summary>
      <div class="body">
        <div class="lo-row"><span class="ar">every cadence lives on</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-gcal"></use></svg>Google Calendar</span><span class="ar">· fei@betauniversity.org</span></div>
        <p data-key="dossier.rhythm.intro"></p>
        <div class="rhythm">
          <div class="beat"><div class="b1">Quarterly</div><div class="b2" data-key="dossier.rhythm.quarterly"></div></div>
          <div class="beat"><div class="b1">Monthly</div><div class="b2" data-key="dossier.rhythm.monthly"></div></div>
          <div class="beat now"><div class="b1">Weekly ◂ Mon</div><div class="b2" data-key="dossier.rhythm.weekly"></div></div>
          <div class="beat"><div class="b1">Daily</div><div class="b2" data-key="dossier.rhythm.daily"></div></div>
          <div class="beat"><div class="b1">Fri settle</div><div class="b2" data-key="dossier.rhythm.friday"></div></div>
        </div>
        <p data-key="dossier.rhythm.outro"></p>
      </div>
    </details>

    <details class="dir" id="c-found">
      <summary><span class="s-title">Friday settle 沉淀</span><span class="s-meta" data-key="dossier.found.meta"></span></summary>
      <div class="body">
        <h4>Workspace tools</h4>
        <ul class="tool-list">
          <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-granola"></use></svg><span data-key="dossier.found.tool.granola"></span></li>
          <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-google"></use></svg><span data-key="dossier.found.tool.gws"></span></li>
          <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-pages"></use></svg><span data-key="dossier.found.tool.pages"></span></li>
        </ul>
        <h4>Knowledge layer</h4>
        <ul class="tool-list">
          <li><svg class="lo-ico" viewBox="0 0 24 24"><use href="#lo-md"></use></svg><span data-key="dossier.found.know.1"></span></li>
          <li><span data-key="dossier.found.know.2"></span></li>
          <li><span data-key="dossier.found.know.3"></span></li>
        </ul>
        <div class="lo-row"><span class="ar">latest settle · full log in</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span></div>
        <ul>
          <li data-key="dossier.found.settle.1"></li>
          <li data-key="dossier.found.settle.2"></li>
          <li data-key="dossier.found.settle.3"></li>
          <li data-key="dossier.found.settle.4"></li>
        </ul>
      </div>
    </details>

    </div><!-- /.dossiers -->

    <div class="foot">
      <span data-key="foot.left"></span>
      <span data-key="foot.middle"></span>
      <span data-key="foot.right"></span>
    </div>
  </div><!-- /.sheet -->
</div><!-- /#app -->

<div class="modal-bg" id="modalBg" onclick="closeModal(event)">
  <div class="modal dir" id="modalBox" role="dialog" aria-modal="true" aria-labelledby="modalTtl">
    <button class="modal-x" type="button" onclick="closeModal()" aria-label="Close">✕</button>
    <div class="modal-inner">
      <div class="modal-ttl" id="modalTtl"></div>
      <div class="body" id="modalBody"></div>
    </div>
  </div>
</div>

<div id="savetoast"></div>
<div id="undobar" role="status" aria-live="polite"><span id="undo-t"></span><button type="button" id="undo-btn">Undo</button></div>

<script>
  const APP_ID='app_70guwbkz9ccb';
  const AUTH='https://api.butterbase.ai/auth/'+APP_ID;
  const FN='https://api.butterbase.ai/v1/'+APP_ID+'/fn/blocks';
  const WS_URL='wss://api.butterbase.ai/v1/'+APP_ID+'/realtime';
  const TOK_KEY='boc_tok';
  const RTOK_KEY='boc_rtok';

  let tok=localStorage.getItem(TOK_KEY);
  let rtok=localStorage.getItem(RTOK_KEY);
  let refreshInflight=null;      // single-flight guard within this tab
  let refreshTimer=null;         // proactive pre-expiry refresh
  let pendEmail='',pendPw='';
  let canEdit=false;
  let myEmail=null;
  let blocksByKey=new Map();     // block_key -> {id, content, kind, updated_at, updated_by}
  let pendingSaves=new Map();    // block_key -> debounce timer id
  let lastSavedAt=new Map();     // block_key -> ts (for realtime echo dedup)
  let ws=null;
  let pollTimer=null;

  const $=id=>document.getElementById(id);
  const msg=(t,cls)=>{const m=$('msg');m.textContent=t||'';m.className='msg'+(cls?' '+cls:'');if(cls==='err')endResume();};
  function endResume(){document.documentElement.classList.remove('resuming');}

  function showForm(which){['form-login','form-signup','form-verify'].forEach(f=>$(f).className=(f===which?'':'hide'));}
  function showLogin(){showForm('form-login');msg('');endResume();}
  function showSignup(){showForm('form-signup');msg('');}

  function doGoogle(){
    location.href=AUTH+'/oauth/google?redirect_to='+encodeURIComponent(location.origin+location.pathname);
  }

  const sleep=ms=>new Promise(r=>setTimeout(r,ms));
  // Access-token expiry in ms since epoch (0 when unreadable).
  function jwtExpMs(t){
    try{
      const p=JSON.parse(atob(t.split('.')[1].replace(/-/g,'+').replace(/_/g,'/')));
      return (p.exp||0)*1000;
    }catch(e){return 0;}
  }
  function tokenAlive(t,marginMs){return !!t && jwtExpMs(t)-Date.now()>(marginMs||0);}

  function saveTokens(d){
    if(d&&d.access_token){tok=d.access_token;localStorage.setItem(TOK_KEY,tok);}
    if(d&&d.refresh_token){rtok=d.refresh_token;localStorage.setItem(RTOK_KEY,rtok);}
    scheduleRefresh();
  }
  function clearTokens(){
    tok=null;rtok=null;
    clearTimeout(refreshTimer);refreshTimer=null;
    localStorage.removeItem(TOK_KEY);localStorage.removeItem(RTOK_KEY);
  }

  // Refresh the access token BEFORE it dies, so a page left open all day never
  // 401s and never bounces back to the Google button.
  function scheduleRefresh(){
    clearTimeout(refreshTimer);
    if(!tok)return;
    const exp=jwtExpMs(tok);
    if(!exp)return;
    const delay=Math.max(30000,Math.min(exp-Date.now()-5*60*1000,2147483000));
    refreshTimer=setTimeout(()=>{tryRefresh();},delay);
  }

  // Exchange the stored refresh token for a fresh access token. The API ROTATES
  // refresh tokens, so this is the fragile part of the session and it is written
  // defensively:
  //   * single-flight per tab — two concurrent 401s cannot burn the token twice
  //   * re-reads localStorage first — another tab may already have rotated it
  //   * only 401/403 clears the session; 5xx / network blips retry and KEEP the
  //     token (the old code wiped it on any non-ok and forced a Google re-login)
  async function tryRefresh(){
    if(refreshInflight)return refreshInflight;
    refreshInflight=(async()=>{
      tok=localStorage.getItem(TOK_KEY);
      rtok=localStorage.getItem(RTOK_KEY);
      if(tokenAlive(tok,60000)){scheduleRefresh();return true;}   // someone else already did it
      if(!rtok)return false;
      for(let attempt=0;attempt<3;attempt++){
        let r=null;
        try{r=await postJSON(AUTH+'/refresh',{refresh_token:rtok});}
        catch(e){await sleep(800*(attempt+1));continue;}
        if(r.ok&&r.data&&r.data.access_token){saveTokens(r.data);return true;}
        if(r.status===401||r.status===403){
          // Genuinely rejected. Before giving up, check whether another tab won
          // the rotation race and left us a live token.
          const freshR=localStorage.getItem(RTOK_KEY);
          if(freshR&&freshR!==rtok){rtok=freshR;continue;}
          const freshA=localStorage.getItem(TOK_KEY);
          if(tokenAlive(freshA,60000)){tok=freshA;scheduleRefresh();return true;}
          clearTokens();
          return false;
        }
        await sleep(800*(attempt+1));   // 5xx / 429 — keep the token, try again
      }
      return false;                     // transient failure: session left intact
    })().finally(()=>{refreshInflight=null;});
    return refreshInflight;
  }

  // Any authenticated call goes through here: refresh-and-retry once on 401.
  async function authFetch(url,opts,retried){
    if(!tokenAlive(tok,10000)&&rtok)await tryRefresh();
    const o=Object.assign({},opts||{});
    o.headers=Object.assign({},o.headers||{},{Authorization:'Bearer '+tok});
    const r=await fetch(url,o);
    if(r.status===401&&!retried&&await tryRefresh())return authFetch(url,opts,true);
    return r;
  }

  // Another tab signed in / rotated tokens — adopt them instead of fighting.
  window.addEventListener('storage',e=>{
    if(e.key===TOK_KEY){tok=e.newValue;scheduleRefresh();}
    if(e.key===RTOK_KEY){rtok=e.newValue;}
  });
  // Coming back to a backgrounded tab: top up the token before anything 401s.
  document.addEventListener('visibilitychange',()=>{
    if(document.visibilityState==='visible'&&rtok&&!tokenAlive(tok,120000))tryRefresh();
  });

  async function postJSON(url,body){
    const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
    let d=null;try{d=await r.json();}catch(e){}
    return {ok:r.ok,status:r.status,data:d||{}};
  }

  async function doLogin(){
    const email=$('li-email').value.trim(),pw=$('li-pw').value;
    if(!email||!pw){msg('Enter email and password.','err');return;}
    msg('Signing in…');
    try{
      const res=await postJSON(AUTH+'/login',{email,password:pw});
      if(!res.ok){msg(res.data.error||res.data.message||'Sign-in failed.','err');return;}
      saveTokens(res.data);bootApp();
    }catch(e){msg('Network error.','err');}
  }

  async function doSignup(){
    const email=$('su-email').value.trim(),pw=$('su-pw').value;
    if(!email||!pw){msg('Enter email and password.','err');return;}
    msg('Creating account…');
    try{
      const res=await postJSON(AUTH+'/signup',{email,password:pw});
      if(!res.ok){msg(res.data.error||res.data.message||'Sign-up failed.','err');return;}
      pendEmail=email;pendPw=pw;
      const lr=await postJSON(AUTH+'/login',{email,password:pw});
      if(lr.ok&&lr.data.access_token){saveTokens(lr.data);bootApp();return;}
      showForm('form-verify');msg('Account created. Enter the 6-digit code from your email.','ok');
    }catch(e){msg('Network error.','err');}
  }

  async function doVerify(){
    const code=$('vf-code').value.trim();
    if(!code){msg('Enter the code.','err');return;}
    msg('Verifying…');
    try{
      const res=await postJSON(AUTH+'/verify-email',{email:pendEmail,code});
      if(!res.ok){msg(res.data.error||res.data.message||'Verification failed.','err');return;}
      const lr=await postJSON(AUTH+'/login',{email:pendEmail,password:pendPw});
      if(lr.ok&&lr.data.access_token){saveTokens(lr.data);bootApp();return;}
      msg('Verified. Now sign in.','ok');showForm('form-login');
    }catch(e){msg('Network error.','err');}
  }

  function signOut(){
    clearTokens();
    if(ws)try{ws.close();}catch(e){}
    if(pollTimer)clearInterval(pollTimer);
    location.reload();
  }

  // ===== Toast =====
  let toastTimer=null;
  function toast(t,isErr){
    const e=$('savetoast');e.textContent=t;e.className='show'+(isErr?' err':'');
    clearTimeout(toastTimer);toastTimer=setTimeout(()=>{e.classList.remove('show');},1600);
  }

  // ===== Render: paint every [data-key] from blocksByKey =====
  function paintKey(key,row){
    if(PRI_RE.test(key)){if(!(priEd&&priEd.key===key))rePri(key);return;}   // priority lists draw themselves
    // text containing inline HTML for dossier strings; SVG text needs plain text
    const els=document.querySelectorAll('[data-key="'+CSS.escape(key)+'"]');
    els.forEach(el=>{
      const isSvgText=(el.namespaceURI==='http://www.w3.org/2000/svg');
      if(isSvgText){
        // SVG <text> — strip HTML tags
        const text=row.content.replace(/<[^>]+>/g,'');
        const ph=!text&&canEdit;   // emptied label: faint "+" so it stays clickable (edit mode only)
        el.textContent=ph?'+':text;
        el.classList.toggle('ph',ph);
      }else{
        el.innerHTML=row.content;
      }
    });
  }

  function paintAll(){
    blocksByKey.forEach((row,key)=>paintKey(key,row));
    rePriAll();   // also draws the lists whose row is missing (built-in default)
    if(canEdit)wireEditing();
  }

  // ===== Inline editing =====
  function wireEditing(){
    document.querySelectorAll('[data-key]').forEach(el=>{
      if(el.dataset.wired)return;
      el.dataset.wired='1';
      const key=el.dataset.key;
      const isSvgText=(el.namespaceURI==='http://www.w3.org/2000/svg');
      if(isSvgText){
        el.addEventListener('click',(e)=>{e.stopPropagation();if(el.closest('a'))e.preventDefault();openInlineEdit(key,el);});
      }else{
        el.classList.add('editable');
        el.setAttribute('contenteditable','true');
        el.setAttribute('spellcheck','false');
        el.addEventListener('input',()=>scheduleSave(key,el,/*isHTML*/true));
        el.addEventListener('blur',()=>{flushSave(key,el,true);});
        el.addEventListener('keydown',(e)=>{
          if(e.key==='Escape'){e.preventDefault();el.blur();}
        });
      }
    });
  }

  function scheduleSave(key,el,isHTML){
    el.classList.remove('saved','err');
    el.classList.add('saving');
    if(pendingSaves.has(key))clearTimeout(pendingSaves.get(key));
    pendingSaves.set(key,setTimeout(()=>flushSave(key,el,isHTML),600));
  }

  async function flushSave(key,el,isHTML){
    if(pendingSaves.has(key)){clearTimeout(pendingSaves.get(key));pendingSaves.delete(key);}
    const content=isHTML?el.innerHTML:el.textContent;
    const row=blocksByKey.get(key);
    if(row && row.content===content){el.classList.remove('saving');return;}
    try{
      const r=await authFetch(FN+'?key='+encodeURIComponent(key),{
        method:'PATCH',
        headers:{'Content-Type':'application/json'},
        body:JSON.stringify({content})
      });
      const data=await decodeBody(r).catch(()=>({}));
      if(!r.ok){
        el.classList.remove('saving');el.classList.add('err');
        toast('Save failed: '+(data.error||r.status),true);
        return;
      }
      const updated=data;
      blocksByKey.set(key,updated);
      lastSavedAt.set(key,Date.now());
      el.classList.remove('saving');el.classList.add('saved');
      paintKey(key,updated);
      setTimeout(()=>el.classList.remove('saved'),900);
      toast('saved · '+key);
    }catch(e){
      el.classList.remove('saving');el.classList.add('err');
      toast('Network error',true);
    }
  }

  // SVG text can't be contenteditable: edit it in a one-line input laid over the text
  function openInlineEdit(key,el){
    if(!canEdit||document.querySelector('input.ed-in'))return;
    el=el||document.querySelector('text[data-key="'+CSS.escape(key)+'"]');
    if(!el)return;
    const row=blocksByKey.get(key);
    const start=row?row.content.replace(/<[^>]+>/g,''):(el.classList.contains('ph')?'':el.textContent);
    const r=el.getBoundingClientRect(),m=el.getScreenCTM(),vw=document.documentElement.clientWidth;
    const w=Math.min(Math.max(r.width+20,160),vw-16),h=Math.max(r.height+8,26);
    const inp=document.createElement('input');
    inp.className='ed-in';inp.type='text';inp.value=start;inp.spellcheck=false;
    inp.setAttribute('aria-label','Edit '+key);
    inp.style.cssText='left:'+(Math.max(8,Math.min(r.left-6,vw-w-8))+scrollX)+'px;top:'+(r.top+r.height/2-h/2+scrollY)+
      'px;width:'+w+'px;height:'+h+'px;font-size:'+Math.max(12,parseFloat(getComputedStyle(el).fontSize)*(m?m.a:1))+'px';
    document.body.appendChild(inp);
    inp.focus();inp.select();
    let done=false;
    const finish=save=>{
      if(done)return;done=true;
      const next=inp.value;
      inp.remove();
      if(save&&next!==start)saveInline(key,next,start);
    };
    inp.addEventListener('keydown',e=>{
      if(e.isComposing||e.keyCode===229)return;
      if(e.key==='Enter'){e.preventDefault();finish(true);}
      else if(e.key==='Escape'){e.preventDefault();finish(false);}
    });
    inp.addEventListener('blur',()=>finish(true));
  }

  function saveInline(key,next,start){
    const row=blocksByKey.get(key);
    // mirror locally first
    const optimistic={...row,content:next};
    blocksByKey.set(key,optimistic);
    paintKey(key,optimistic);
    document.querySelectorAll('text[data-key="'+CSS.escape(key)+'"]').forEach(el=>el.classList.add('saving'));
    const fail=msg=>{   // revert to the previous row and repaint; the toast stays
      toast(msg,true);
      if(row)blocksByKey.set(key,row);else blocksByKey.delete(key);
      paintKey(key,row||{content:start});
      document.querySelectorAll('text[data-key="'+CSS.escape(key)+'"]').forEach(el=>{
        el.classList.remove('saving');el.classList.add('err');
        setTimeout(()=>el.classList.remove('err'),1600);
      });
    };
    authFetch(FN+'?key='+encodeURIComponent(key),{
      method:'PATCH',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({content:next})
    }).then(async r=>{
      const data=await decodeBody(r).catch(()=>({}));
      if(!r.ok){fail('Save failed: '+(data.error||r.status));return;}
      const updated=data;
      blocksByKey.set(key,updated);
      lastSavedAt.set(key,Date.now());
      paintKey(key,updated);
      document.querySelectorAll('text[data-key="'+CSS.escape(key)+'"]').forEach(el=>{
        el.classList.remove('saving');el.classList.add('saved');
        setTimeout(()=>el.classList.remove('saved'),900);
      });
      toast('saved · '+key);
    }).catch(()=>fail('Network error'));
  }

  // ===== Operating-model lists =====
  // Each box keeps three rows: opm.<c>.strategy (one phrase), opm.<c>.kr and opm.<c>.pri (newline
  // lists). The page draws the "Strategy" / "KR" / "Priority" labels, wraps long lines with a hanging
  // indent, and lines up both rows of boxes (strategy, KR and priority blocks take the row's longest).
  const SVGNS='http://www.w3.org/2000/svg';
  const PRI_RE=/^opm\.o[123]d?\.(?:pri|kr|strategy)$/;
  const PRI_FS=9,PRI_SLACK=3,PRI_MS=10;   // priority font size, wrap tolerance (px), measuring scale
  const KIND={
    str:{lab:'Strategy',col:'#2f5d8a',fs:9.5,fw:700,lh:13,ph:'+ add strategy',aria:'Strategy'},
    kr:{lab:'KR',col:'#2f5d8a',fs:9,fw:400,lh:13,ph:'+ add KR',aria:'Key results, one per line'},
    pri:{lab:'Priority',col:'#9c5a22',fs:PRI_FS,fw:400,lh:14,ph:'+ add priority',aria:'Priorities, one per line'}
  };
  const OPM_ROWS={top:['o1','o2','o3'],bot:['o1d','o2d','o3d']};
  const OPM_T={top:44};
  const CJK='\\u2E80-\\u9FFF\\uAC00-\\uD7AF\\uF900-\\uFAFF\\uFF00-\\uFFEF';
  const CJK_RE=new RegExp('['+CJK+']');
  const TOK_RE=new RegExp('['+CJK+']|[^\\s'+CJK+']+|\\s+','g');
  const priPend=new Map();   // key -> {n,text,base,tail}: saves queued or in flight
  let priEd=null;            // open list editor {key,g,box,ta,orig,done,fit}
  const priMt={};            // hidden <text> probes in the opm svg, one per font setting, used to measure text
  const undoStack=[];        // deleted lines the undo bar can still bring back: {key,i,gone}
  let undoTimer=null;

  const svgEl=(tag,attrs,parent)=>{
    const e=document.createElementNS(SVGNS,tag);
    for(const k in attrs)e.setAttribute(k,attrs[k]);
    if(parent)parent.appendChild(e);
    return e;
  };
  const priG=key=>document.querySelector('.opm-pri[data-list="'+key+'"]');
  const isStr=key=>/\.strategy$/.test(key);

  // stored text -> lines: drop empty lines, strip ONE leading marker (· or •, or - / * followed by a space)
  function priLines(s){
    return String(s==null?'':s).split(/\r?\n/)
      .map(l=>l.replace(/^\s*(?:[·•]\s*|[-*]\s+)/,'').trim()).filter(Boolean);
  }
  // the strategy is one phrase: newlines collapse to spaces
  function listOf(key,s){
    if(isStr(key)){const c=String(s==null?'':s).replace(/\s*\r?\n\s*/g,' ').trim();return c?[c]:[];}
    return priLines(s);
  }
  // text in effect for a list: pending save, else stored row, else the built-in default
  function priText(key){
    const p=priPend.get(key);if(p)return p.text;
    const r=blocksByKey.get(key);if(r&&r.content!=null)return r.content;
    const g=priG(key);return g?g.dataset.default:'';
  }
  const priItems=key=>listOf(key,priText(key));

  // width of s at font size fs / weight fw, measured at PRI_MS x the size so it does not drift with the
  // on-screen scale; estimated (ASCII .7em, CJK 1.05em) while #app is hidden
  function priW(s,fs,fw,ls){
    fs=fs||PRI_FS;fw=fw||400;
    if(!s)return 0;
    const mk=fs+'/'+fw+'/'+(ls||0);
    let mt=priMt[mk];
    if(!mt||!mt.isConnected){   // one probe per font setting: a probe whose weight is changed later keeps its old metrics
      mt=priMt[mk]=svgEl('text',{x:0,y:-40,'font-size':fs*PRI_MS,'font-weight':fw,'letter-spacing':ls?ls*fs*PRI_MS:0,visibility:'hidden','pointer-events':'none'},$('opm-low').ownerSVGElement);
    }
    mt.textContent=s;
    let w=0;try{w=mt.getComputedTextLength();}catch(e){}
    if(w>0)return w/PRI_MS;
    let n=0;for(const c of s)n+=CJK_RE.test(c)?1.05:.7;
    return n*fs;
  }
  // Word wrap to width w. Words that must not be split are glued into units: a date ("Nov 1"), a number
  // with its unit ("2x Q3", "2-3 a week", "7 days"), anything after "→" or "·" ("· Dec 15"), and "→" with the word before it.
  // CJK breaks between characters, a unit wider than w breaks by letter, and a short last line pulls one unit down.
  const MON_RE=/^(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Sept|Oct|Nov|Dec)[a-z]*\.?$/;
  function wrapUnits(s){
    const items=[];let sp=false;
    for(const t of s.match(TOK_RE)||[]){if(/^\s/.test(t)){sp=true;continue;}items.push({t,sp});sp=false;}
    const units=[];
    items.forEach((it,i)=>{
      const p=items[i-1];
      const glue=p&&(/→$/.test(p.t)||p.t==='·'||it.t==='→'||/^(?:a|an)$/i.test(p.t)||
        (MON_RE.test(p.t)&&/^\d/.test(it.t))||(/^[~$¥+]?\d/.test(p.t)&&it.t!=='·'&&it.sp));
      if(glue&&units.length){const u=units[units.length-1];u.t+=(it.sp?' ':'')+it.t;}
      else units.push({t:it.t,sp:it.sp});
    });
    return units;
  }
  function priWrap(s,w,fs,fw){
    const W=t=>priW(t,fs,fw),txt=a=>a.map((u,i)=>(i&&u.sp?' ':'')+u.t).join('');
    const lines=[];let cur=[];
    const flush=()=>{if(cur.length)lines.push(cur);cur=[];};
    for(const u of wrapUnits(s)){
      if(cur.length&&W(txt(cur.concat(u)))>w)flush();
      if(!cur.length&&W(u.t)>w){
        let c='';
        for(const ch of u.t){if(c&&W(c+ch)>w){lines.push([{t:c,sp:false}]);c='';}c+=ch;}
        cur=[{t:c,sp:false}];
      }else cur.push(u);
    }
    flush();
    const n=lines.length;
    if(n>1){
      const last=lines[n-1],prev=lines[n-2];
      if(txt(last).length<=8&&prev.length>1){
        const m=prev[prev.length-1],nl=[{t:m.t,sp:false}].concat(last.map((u,i)=>i?u:{t:u.t,sp:true}));
        if(W(txt(nl))<=w){prev.pop();lines[n-1]=nl;}
      }
    }
    const out=lines.map(txt);
    return out.length?out:[s];
  }
  // width of the label column: widest of the four labels, plus a gap
  function labW(){
    return Math.ceil(Math.max(...['Strategy','Priority','Owner','KR'].map(t=>priW(t,7.5,700,.04)))+5);
  }

  // measure one list: wrapped lines per item, total visual lines
  function measurePri(g,LW){
    const key=g.dataset.list,K=KIND[g.dataset.kind],x=+g.dataset.x+LW,lim=+g.dataset.right-x+PRI_SLACK;
    const items=priItems(key);
    g._items=items;
    g._wrap=items.map(t=>priW(t,K.fs,K.fw)<=lim?[t]:priWrap(t,lim,K.fs,K.fw));
    g.dataset.vl=g._wrap.reduce((n,w)=>n+w.length,0);
  }
  // draw one list at its laid-out position: label + text per line, hit areas and controls in edit mode
  const REC_RE=/\b(?:daily|weekly|bi-?weekly|monthly|quarterly|every (?:day|week|month|quarter))\b|每[天日周月季]/i;
  function drawPri(g,LW){
    const key=g.dataset.list,K=KIND[g.dataset.kind],kind=g.dataset.kind;
    const x=+g.dataset.x,right=+g.dataset.right,top=+g.dataset.top,y0=+g.dataset.y0,items=g._items,wrapped=g._wrap;
    g.textContent='';
    g.classList.toggle('ed',canEdit);
    if(canEdit)svgEl('rect',{class:'opm-hit',x:x-12,y:top,width:right-x+24,height:+g.dataset.h,fill:'none','pointer-events':'all'},g);
    if(kind==='pri'&&items.length)svgEl('path',{d:'M'+x+' '+top+'H'+right,fill:'none',stroke:'#c3cdd8','stroke-width':.8},g);
    let vl=0;
    items.forEach((t,i)=>{
      const b=svgEl('g',{class:'opm-b','data-i':i},g),y=y0+K.lh*vl,k=wrapped[i].length;
      if(canEdit)svgEl('rect',{class:'opm-bh',x:x-12,y:y-11,width:right-x+24,height:K.lh*k,fill:'none','pointer-events':'all'},b);
      svgEl('text',{class:'opm-lab',x:x,y:y,'font-size':7.5,'font-weight':700,'letter-spacing':'.04em',fill:K.col},b).textContent=kind==='pri'?'P'+(i+1):K.lab;
      // recurring priority (cadence word in the text, as typed): ↻ after the P# label; text itself untouched
      const rec=kind==='pri'&&t.match(REC_RE);
      if(rec){const r=svgEl('text',{class:'opm-rec',x:x+16,y:y+.5,'font-size':11,'font-weight':700,fill:'#2f5d8a'},b);r.textContent='↻';svgEl('title',{},r).textContent='Recurring: '+rec[0].toLowerCase();}
      const tx=svgEl('text',{class:'opm-bt','font-size':K.fs,'font-weight':K.fw,fill:'#1b2531'},b);
      wrapped[i].forEach((ln,j)=>{svgEl('tspan',{x:x+LW,y:y+K.lh*j},tx).textContent=ln;});
      if(canEdit&&kind!=='str'){
        const c=svgEl('g',{class:'opm-x',transform:'translate('+(right+4)+' '+(y-3.4)+')',role:'button','aria-label':'Delete line'},b);
        svgEl('circle',{r:5},c);svgEl('path',{d:'M-1.9-1.9L1.9 1.9M1.9-1.9L-1.9 1.9'},c);
      }
      vl+=k;
    });
    if(canEdit){
      if(items.length){
        if(kind!=='str'){
          const a=svgEl('g',{class:'opm-add',transform:'translate('+(right+4)+' '+top+')',role:'button','aria-label':'Add line'},g);
          svgEl('circle',{r:5},a);svgEl('path',{d:'M-2.2 0H2.2M0-2.2V2.2'},a);
        }
      }else svgEl('text',{class:'opm-addtxt',x:x,y:y0,'font-size':K.fs},g).textContent=K.ph;
      wirePri(g);
    }
  }

  // Lay out both rows: strategy / name / owner / KR / divider / priority, each block as tall as the row's longest.
  function layoutOpm(){
    const svg=$('opm-low').ownerSVGElement,LW=labW();
    document.querySelectorAll('.opm-pri').forEach(g=>measurePri(g,LW));
    const L=(c,k)=>document.querySelector('.opm-pri[data-code="'+c+'"][data-kind="'+k+'"]');
    const at=(row,T)=>{
      const cs=OPM_ROWS[row],mx=k=>Math.max(1,...cs.map(c=>+L(c,k).dataset.vl||0));
      const S=mx('str'),Kn=mx('kr'),P=mx('pri');
      const sy=T+42,tintY=sy+(S-1)*KIND.str.lh+12;
      const ky=tintY+52,div=ky+(Kn-1)*KIND.kr.lh+10,py=div+16;
      const tintB=py+(P-1)*KIND.pri.lh+12,outerB=tintB+8;
      cs.forEach(c=>{
        const set=(k,o)=>{const g=L(c,k);for(const n in o)g.dataset[n]=o[n];};
        set('str',{y0:sy,top:T+28,h:S*KIND.str.lh+6});
        set('kr',{y0:ky,top:tintY+38,h:div-(tintY+38)});
        set('pri',{y0:py,top:div,h:tintB-div});
        const q=sel=>document.querySelector(sel);
        const ob=q('.opm-outer[data-code="'+c+'"]');ob.setAttribute('y',T);ob.setAttribute('height',outerB-T);
        const tint=q('.opm-tint[data-code="'+c+'"]');tint.setAttribute('y',tintY);tint.setAttribute('height',tintB-tintY);
        const ix=+L(c,'str').dataset.x;
        const nm=q('text[data-key="opm.'+c+'-name"]');nm.setAttribute('y',tintY+18);
        const ow=q('text[data-key="opm.'+c+'-owner"]'),ol=q('.opm-ownl[data-code="'+c+'"]');
        ow.setAttribute('x',ix+LW);ow.setAttribute('y',tintY+32);ol.setAttribute('y',tintY+32);
        const hd=ob.nextElementSibling;const st=hd.nextElementSibling;const hasSite=!!st&&/-site$/.test(st.getAttribute('data-key')||'');hd.setAttribute('y',T+(hasSite?17:22));if(hasSite)st.setAttribute('y',T+29);
        ['str','kr','pri'].forEach(k=>{const g=L(c,k);if(!(priEd&&priEd.key===g.dataset.list))drawPri(g,LW);});
      });
      return outerB;
    };
    const topB=at('top',44);
    const bandY=topB+16,bandH=26,botT=bandY+bandH+16;
    const low=$('opm-low'),ars=low.querySelectorAll(':scope > g:first-child > path');
    [143,410,677].forEach((x,i)=>{
      ars[i].setAttribute('d','M'+x+' '+topB+'V'+bandY);
      ars[i+3].setAttribute('d','M'+x+' '+botT+'V'+(bandY+bandH));
    });
    const br=low.querySelector(':scope > rect');br.setAttribute('y',bandY);br.setAttribute('height',bandH);
    low.querySelector('text[data-key="opm.asset-name"]').setAttribute('y',bandY+17);
    [1,2,3].forEach(i=>low.querySelector('text[data-key="opm.pays-'+i+'"]').setAttribute('y',botT-4));
    const botB=at('bot',botT);
    const cap=$('opm-cap'),cs=cap.querySelectorAll('text');
    cs[0].setAttribute('y',botB+24);cs[1].setAttribute('y',botB+42);
    svg.setAttribute('viewBox','0 0 820 '+(botB+54));
    if(priEd)posPriEd();
  }
  function rePri(key){layoutOpm();}
  function rePriAll(){layoutOpm();}

  // one delegated click handler per list (edit mode only)
  function wirePri(g){
    if(g.dataset.wired)return;
    g.dataset.wired='1';
    g.addEventListener('click',e=>{
      const key=g.dataset.list,b=e.target.closest('.opm-b');
      if(e.target.closest('.opm-x'))delPri(key,+b.dataset.i);
      else if(b)openPriEdit(key,+b.dataset.i);
      else if(e.target.closest('.opm-add,.opm-addtxt')||!g.querySelector('.opm-b'))openPriEdit(key,-1);
    });
  }

  function delPri(key,i){
    const lines=priItems(key);
    if(i<0||i>=lines.length)return;
    undoStack.push({key,i,gone:lines.splice(i,1)[0]});
    showUndo();
    savePri(key,lines.join('\n'));
  }

  // Optimistic save of one list. Saves for a key run one at a time so a quick run of deletes lands
  // in order, and polls or pushes cannot overwrite the list while its saves are pending.
  function savePri(key,content){
    let st=priPend.get(key);
    if(!st){st={n:0,text:'',base:blocksByKey.get(key),tail:Promise.resolve()};priPend.set(key,st);}
    st.n++;st.text=content;
    rePri(key);
    return st.tail=st.tail.then(()=>patchPri(key,st,content));
  }
  async function patchPri(key,st,content){
    let row=null,err='network error';
    try{
      const r=await authFetch(FN+'?key='+encodeURIComponent(key),{
        method:'PATCH',
        headers:{'Content-Type':'application/json'},
        body:JSON.stringify({content})
      });
      const data=await decodeBody(r).catch(()=>({}));
      if(r.ok)row=data;else err=data.error||r.status;
    }catch(e){}
    st.n--;
    if(row){
      st.base=row;blocksByKey.set(key,row);lastSavedAt.set(key,Date.now());
      toast('saved · '+key);
    }else{
      toast('Save failed: '+err,true);
      if(!st.n){if(st.base)blocksByKey.set(key,st.base);else blocksByKey.delete(key);dropUndo(key);}   // revert
    }
    if(!st.n){priPend.delete(key);rePri(key);}
    return !!row;
  }

  // textarea laid over a list's area; idx = line to put the caret on, -1 = new line at the end
  function openPriEdit(key,idx){
    const g=priG(key);
    if(!canEdit||priEd||!g)return;
    const str=isStr(key),lines=priItems(key),orig=lines.join('\n');
    const box=document.createElement('div'),ta=document.createElement('textarea'),hint=document.createElement('div');
    box.id='opm-ed';ta.rows=1;ta.spellcheck=false;ta.setAttribute('aria-label',KIND[g.dataset.kind].aria);
    ta.value=idx<0&&lines.length&&!str?orig+'\n':orig;
    hint.className='hint';hint.textContent=str?'One short phrase · Enter or click away saves · Esc cancels':'One line each · Enter adds one · ⌘↵ or click away saves · Esc cancels';
    box.append(ta,hint);document.body.appendChild(box);
    const fit=()=>{ta.style.height='auto';ta.style.height=(ta.scrollHeight+ta.offsetHeight-ta.clientHeight)+'px';};
    priEd={key,g,box,ta,orig,done:false,fit};
    posPriEd();
    ta.focus();
    const c=idx<0||str?ta.value.length:lines.slice(0,idx).reduce((s,l)=>s+l.length+1,0);
    ta.setSelectionRange(c,c);
    ta.addEventListener('input',fit);
    ta.addEventListener('keydown',e=>{
      if(e.isComposing||e.keyCode===229)return;
      if(e.key==='Escape'){e.preventDefault();closePriEd(false);}
      else if(e.key==='Enter'&&(e.metaKey||e.ctrlKey||str)){e.preventDefault();closePriEd(true);}
    });
    ta.addEventListener('blur',()=>closePriEd(true));
  }
  function posPriEd(){
    const e=priEd;if(!e)return;
    const g=e.g,svg=g.ownerSVGElement;
    const r=svg.getBoundingClientRect(),sc=r.width/svg.viewBox.baseVal.width;
    const x=+g.dataset.x,right=+g.dataset.right;
    const vw=document.documentElement.clientWidth,w=Math.max((right-x+24)*sc,Math.min(300,vw-16));
    const s=e.box.style;
    s.left=Math.max(8,Math.min(r.left+(x-12)*sc,vw-w-8))+scrollX+'px';
    s.top=r.top+(+g.dataset.top)*sc+scrollY+'px';
    s.width=w+'px';
    e.ta.style.fontSize=Math.max(12,PRI_FS*sc)+'px';
    e.fit();
  }
  function closePriEd(save){
    const e=priEd;if(!e||e.done)return;
    e.done=true;priEd=null;
    const next=listOf(e.key,e.ta.value).join('\n');
    e.box.remove();
    if(save&&next!==e.orig){dropUndo(e.key);savePri(e.key,next);}   // a non-delete save: undo entries of this list are stale
    else rePri(e.key);   // also catches up on repaints skipped while open
  }

  const undoLabel=()=>undoStack.length>1?undoStack.length+' lines deleted':'Line deleted';
  function showUndo(){
    $('undo-t').textContent=undoLabel();
    $('undobar').classList.add('show');
    clearTimeout(undoTimer);undoTimer=setTimeout(hideUndo,6000);
  }
  function hideUndo(){
    clearTimeout(undoTimer);undoStack.length=0;
    $('undobar').classList.remove('show');
  }
  // forget the undo entries of a list that changed some other way (editor save, failed save)
  function dropUndo(key){
    for(let j=undoStack.length-1;j>=0;j--)if(undoStack[j].key===key)undoStack.splice(j,1);
    if(undoStack.length)$('undo-t').textContent=undoLabel();else hideUndo();
  }
  // bring every listed bullet back, newest first, each re-inserted into the CURRENT list
  function undoDel(){
    const items=undoStack.slice().reverse(),by=new Map();
    hideUndo();
    if(priEd&&items.some(u=>u.key===priEd.key))closePriEd(true);   // keep what is being typed in that list
    items.forEach(u=>{
      const lines=by.get(u.key)||priItems(u.key);
      lines.splice(Math.min(u.i,lines.length),0,u.gone);
      by.set(u.key,lines);
    });
    by.forEach((lines,key)=>savePri(key,lines.join('\n')));
  }
  $('undo-btn').addEventListener('mousedown',e=>e.preventDefault());   // keep focus: a textarea blur must not save (and hide the bar) before the click lands
  $('undo-btn').addEventListener('click',undoDel);
  window.addEventListener('resize',()=>{if(priEd)posPriEd();});
  // the web font lands after first paint: re-measure so wrapping uses the real glyph widths
  if(document.fonts){
    const rePriLate=()=>{rePriAll();setTimeout(rePriAll,250);};   // faces can finish a beat after the event
    document.fonts.ready.then(rePriLate);
    document.fonts.addEventListener('loadingdone',rePriLate);
  }

  // ===== Modal =====
  let modalId=null;
  window.openCard=function(id){
    const el=document.getElementById(id);if(!el)return;
    const ttl=el.querySelector('.s-title');
    const body=el.querySelector('.body');
    $('modalTtl').textContent=ttl?ttl.textContent:'';
    $('modalBody').innerHTML=body?body.innerHTML:'';
    const tier=(el.className.match(/\b(n1|n2|n3|dir)\b/)||[0,'dir'])[1];
    $('modalBox').className='modal '+tier;
    $('modalBg').classList.add('show');
    document.body.style.overflow='hidden';
    modalId=id;
    history.replaceState(null,'','#'+id);
    $('modalBody').querySelectorAll('[data-wired]').forEach(n=>n.removeAttribute('data-wired'));   // clones carry the flag, not the listeners
    if(canEdit)wireEditing();   // re-run on modal-cloned [data-key] elements
  };
  window.closeModal=function(e){
    if(e&&e.target&&e.target.id!=='modalBg')return;
    $('modalBg').classList.remove('show');
    document.body.style.overflow='';
    modalId=null;
    history.replaceState(null,'',location.pathname+location.search);
  };
  document.addEventListener('keydown',e=>{if(e.key==='Escape'&&modalId)closeModal();});

  // diagram clicks
  document.addEventListener('click',e=>{
    const g=e.target.closest('g[data-card]');
    if(g && !e.target.closest('[data-key]')){
      openCard(g.dataset.card);
    }
  });

  // ===== Data load =====
  // Function returns gunzipped JSON as application/octet-stream (workaround for proxy
  // re-encoding large responses as zstd without telling the browser).
  async function decodeBody(r){
    const ct=r.headers.get('content-type')||'';
    if(ct.startsWith('application/octet-stream')){
      const buf=await r.arrayBuffer();
      const bytes=new Uint8Array(buf);
      // first 2 bytes = gzip magic 1f 8b → gunzip
      if(bytes.length>=2 && bytes[0]===0x1f && bytes[1]===0x8b){
        const ds=new DecompressionStream('gzip');
        const w=ds.writable.getWriter();w.write(bytes);w.close();
        const text=await new Response(ds.readable).text();
        return JSON.parse(text);
      }
    }
    return await r.json();
  }
  async function loadBlocks(retried){
    const r=await authFetch(FN+'?t='+Date.now());
    if(r.status===401){
      // authFetch already refreshed and retried; a 401 here means the session is
      // genuinely gone, not merely stale.
      if(!retried&&rtok&&await tryRefresh())return loadBlocks(true);
      clearTokens();
      showLogin();msg('Session expired, sign in again.','err');
      $('app').className='hide';$('gate-wrap').className='';
      return false;
    }
    if(r.status===403){
      clearTokens();
      showLogin();msg('That Google account is not allowed here. Sign in as fei@betauniversity.org.','err');
      $('app').className='hide';$('gate-wrap').className='';
      return false;
    }
    if(!r.ok){msg('Could not load (HTTP '+r.status+').','err');return false;}
    const data=await decodeBody(r);
    canEdit=!!data.can_edit;
    myEmail=data.email;
    blocksByKey=new Map();
    (data.rows||[]).forEach(row=>blocksByKey.set(row.block_key,row));
    $('bar-mode').innerHTML='signed in as <b>'+myEmail+'</b> · '+(canEdit?'edit mode':'read-only');
    paintAll();
    return true;
  }

  // ===== Realtime (WebSocket) with poll fallback =====
  function setLive(state,t){
    const el=$('bar-live');
    el.className='live'+(state==='ok'?'':' '+state);
    $('bar-live-t').textContent=t;
  }
  function startRealtime(){
    try{
      ws=new WebSocket(WS_URL+'?token='+encodeURIComponent(tok));
    }catch(e){startPoll();return;}
    ws.onopen=()=>{
      setLive('ok','live');
      if(pollTimer){clearInterval(pollTimer);pollTimer=null;}   // the socket is back: stop polling
      ws.send(JSON.stringify({type:'subscribe',table:'blocks'}));
    };
    ws.onmessage=ev=>{
      let m;try{m=JSON.parse(ev.data);}catch(e){return;}
      if(m.type==='change' && m.table==='blocks' && m.record){
        const row=m.record;
        const prev=blocksByKey.get(row.block_key);
        if(prev&&prev.updated_at&&!(row.updated_at>prev.updated_at))return;   // newer rows only
        // dedup our own echo
        const t=lastSavedAt.get(row.block_key);
        if(t && (Date.now()-t)<2500){return;}
        blocksByKey.set(row.block_key,row);
        // skip painting the element currently focused by user
        const focused=document.activeElement;
        if(focused && focused.dataset && focused.dataset.key===row.block_key)return;
        paintKey(row.block_key,row);
      }
    };
    ws.onclose=()=>{
      setLive('off','reconnecting');
      // Reconnect with a CURRENT token — the socket was opened with whatever was
      // valid at the time, and an expired one just gets rejected forever.
      setTimeout(async()=>{
        if(!tok&&!rtok)return;
        if(!tokenAlive(tok,60000))await tryRefresh();
        if(tok)startRealtime();
      },2500);
    };
    ws.onerror=()=>{setLive('err','realtime error → polling');startPoll();};
  }
  function startPoll(){
    if(pollTimer)return;
    pollTimer=setInterval(async ()=>{
      try{
        const r=await authFetch(FN+'?t='+Date.now());
        if(!r.ok)return;
        const data=await decodeBody(r);
        (data.rows||[]).forEach(row=>{
          const k=row.block_key,prev=blocksByKey.get(k);
          // guards first, so a skipped row is not lost: the next poll sees it again
          if(priPend.has(k))return;                                        // a local save of this list is pending
          if(prev&&prev.updated_at&&!(row.updated_at>prev.updated_at))return;   // newer rows only
          const t=lastSavedAt.get(k);
          if(t && (Date.now()-t)<2500)return;                              // our own echo
          blocksByKey.set(k,row);
          const focused=document.activeElement;
          if(focused && focused.dataset && focused.dataset.key===k)return;
          paintKey(k,row);
        });
      }catch(e){}
    },4000);
  }

  async function bootApp(){
    if(!await loadBlocks())return;
    $('gate-wrap').className='hide';
    $('app').className='';
    endResume();
    rePriAll();   // #app is measurable now: redo the layout with real glyph widths
    startRealtime();
  }

  // OAuth return
  (function(){
    const qp=new URLSearchParams(location.search);
    if(qp.get('error')){msg('Google sign-in failed: '+qp.get('error'),'err');
      history.replaceState({},document.title,location.pathname);}
    else if(qp.get('access_token')){
      saveTokens({access_token:qp.get('access_token'),refresh_token:qp.get('refresh_token')});
      history.replaceState({},document.title,location.pathname);}
  })();

  // Boot: an access token gets used straight away; with only a refresh token we
  // trade it in silently, so a returning visit does not ask for a password.
  (async function(){
    if(tok&&!tokenAlive(tok,60000)&&rtok)await tryRefresh();
    if(tok){scheduleRefresh();bootApp();return;}
    if(rtok&&await tryRefresh()){bootApp();return;}
    endResume();
  })();
  setTimeout(()=>{if($('app').className==='hide')endResume();},10000);
</script>
</body>
</html>
