<!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 ==================== */
  #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);}
  .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);}

  /* 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;}
  }
</style>
</head>
<body>

<!-- ===== 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>
    <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 292" 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="180" rx="5" fill="#f4f7f9" stroke="#33414f" stroke-width="1"/>
      <text class="tt" x="40" y="55" data-key="direction.mission_label"></text>
      <text class="ts" x="40" y="70" data-key="direction.mission_l1"></text>
      <text class="ts" x="40" y="82" data-key="direction.mission_l2"></text>
      <text class="tt" x="40" y="105" data-key="direction.vision_label"></text>
      <text class="ts" x="40" y="120" data-key="direction.vision_l1"></text>
      <text class="ts" x="40" y="132" data-key="direction.vision_l2"></text>
      <line x1="40" y1="146" x2="780" y2="146" stroke="#33414f" stroke-width="0.5" opacity="0.4"/>
      <text class="tt" x="40" y="164" fill="#33414f">STRATEGY<tspan fill="#9aa6b2"> — click the box 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>
      <text class="ts" x="40" y="203" fill="#9c5a22" data-key="direction.strategy_l3"></text>
    </g>
    <a href="file:///Users/feiyun/Desktop/beta-network/butterOPC-venture-page.md" target="_blank" rel="noopener"><use href="#lo-md" x="781" y="38" width="14" height="14"/></a>

    <!-- OKR bar -->
    <line x1="403" y1="216" x2="403" y2="238" stroke="#2f5d8a" stroke-width="1.5" marker-end="url(#ar)"/>
    <line x1="417" y1="238" x2="417" y2="216" stroke="#9c5a22" stroke-width="1.5" marker-end="url(#ar)"/>
    <text class="ts" x="395" y="231" text-anchor="end" fill="#2f5d8a">drives</text>
    <text class="ts" x="425" y="231" fill="#9c5a22">learning</text>

    <g class="hit" data-card="c-okr">
      <rect class="hitbox" x="20" y="242" width="780" height="38" rx="3" fill="#e7edf3" stroke="#2f5d8a" stroke-width="1.1"/>
      <text class="th" x="40" y="266" fill="#2f5d8a" font-size="15">OKR</text>
      <text class="ts" x="88" y="266" data-key="okr.bar.tagline"></text>
    </g>
    <a href="file:///Users/feiyun/Desktop/beta-network/butterOPC-venture-page.md" target="_blank" rel="noopener"><use href="#lo-md" x="781" y="248" width="14" height="14"/></a>


    </svg>

    <div class="ledger">
      <div class="ledger-cap">OPERATING MODEL · THESIS ONE, ASIA CLIENTS</div>
      <div class="ledger-head"><span>Demand, who pays</span><span></span><span>Supply, the talent</span></div>
      <div class="grp"><div class="pair svc-pair"><div class="cust"><p class="cust-cn" data-key="cust.ent.cn"></p><p class="cust-en" data-key="cust.ent.en"></p><p class="own">@<span data-key="cust.ent.owner"></span></p><p class="cust-note" data-key="cust.ent.note"></p></div><span class="arw">&#8594;</span><div class="svc"><span class="shared-tag" data-key="svc.ent.label"></span><div class="svc-grid"><div class="lob lob-h1"><div class="lob-head"><span class="hchip hchip-h1">H1</span><span class="dots"><i class="dot on"></i><i class="dot on"></i></span><span class="tag" data-key="lob.hacks.label"></span><span class="lob-name" data-key="lob.hacks.name"></span><span class="own">@<span data-key="lob.hacks.owner"></span></span></div><div class="lob-body"><p class="sup"><span class="sup-name" data-key="lob.hacks.supply"></span><span class="sup-gloss" data-key="lob.hacks.gloss"></span></p><p class="dom" data-key="lob.hacks.domain"></p><p class="tal"><span class="tl">talent</span><span data-key="lob.hacks.talent"></span></p></div><details class="fold"><summary>create, capture, scale</summary><div class="vs"><div class="vr"><span class="vl">create</span><span class="vt" data-key="lob.hacks.create"></span></div><div class="vr"><span class="vl">capture</span><span class="vt" data-key="lob.hacks.capture"></span></div><div class="vr"><span class="vl">scale</span><span class="vt" data-key="lob.hacks.scale"></span></div></div></details></div><div class="lob lob-h2"><div class="lob-head"><span class="hchip hchip-h2">H2</span><span class="dots"><i class="dot on"></i><i class="dot"></i></span><span class="tag" data-key="lob.brix.label"></span><span class="lob-name" data-key="lob.brix.name.v3"></span><span class="own">@<span data-key="lob.brix.owner"></span></span></div><div class="lob-body"><p class="sup"><span class="sup-name" data-key="lob.brix.supply"></span><span class="sup-gloss" data-key="lob.brix.gloss"></span></p><p class="dom" data-key="lob.brix.domain.v2"></p><p class="tal"><span class="tl">talent</span><span data-key="lob.brix.talent"></span></p></div><details class="fold"><summary>create, capture, scale</summary><div class="vs"><div class="vr"><span class="vl">create</span><span class="vt" data-key="lob.brix.create"></span></div><div class="vr"><span class="vl">capture</span><span class="vt" data-key="lob.brix.capture"></span></div><div class="vr"><span class="vl">scale</span><span class="vt" data-key="lob.brix.scale"></span></div></div></details></div></div></div></div></div>
      <div class="grp"><div class="pair"><div class="cust"><p class="cust-cn" data-key="cust.inst.cn"></p><p class="cust-en" data-key="cust.inst.en"></p><p class="own">@<span data-key="cust.inst.owner"></span></p><p class="cust-note" data-key="cust.inst.note"></p></div><span class="arw">&#8594;</span><div class="lob lob-h1"><div class="lob-head"><span class="hchip hchip-h1">H1</span><span class="dots"><i class="dot on"></i><i class="dot on"></i></span><span class="tag" data-key="lob.veta.label"></span><span class="lob-name" data-key="lob.veta.name"></span><span class="own">@<span data-key="lob.veta.owner"></span></span></div><div class="lob-body"><p class="sup"><span class="sup-name" data-key="lob.veta.supply"></span><span class="sup-gloss" data-key="lob.veta.gloss"></span></p><p class="dom" data-key="lob.veta.domain"></p><p class="tal"><span class="tl">talent</span><span data-key="lob.veta.talent"></span></p></div><details class="fold"><summary>create, capture, scale</summary><div class="vs"><div class="vr"><span class="vl">create</span><span class="vt" data-key="lob.veta.create"></span></div><div class="vr"><span class="vl">capture</span><span class="vt" data-key="lob.veta.capture"></span></div><div class="vr"><span class="vl">scale</span><span class="vt" data-key="lob.veta.scale"></span></div></div></details></div></div></div>
      <div class="grp"><div class="pair"><div class="cust"><p class="cust-cn" data-key="cust.cap.cn"></p><p class="cust-en" data-key="cust.cap.en"></p><p class="own">@<span data-key="cust.cap.owner"></span></p><p class="cust-note" data-key="cust.cap.note"></p></div><span class="arw">&#8594;</span><div class="lob lob-h1"><div class="lob-head"><span class="hchip hchip-h1">H1</span><span class="dots"><i class="dot on"></i><i class="dot on"></i></span><span class="tag" data-key="lob.betafund.label"></span><span class="lob-name" data-key="lob.betafund.name"></span><span class="own">@<span data-key="lob.betafund.owner"></span></span></div><div class="lob-body"><p class="sup"><span class="sup-name" data-key="lob.betafund.supply"></span><span class="sup-gloss" data-key="lob.betafund.gloss"></span></p><p class="dom" data-key="lob.betafund.domain"></p><p class="tal"><span class="tl">talent</span><span data-key="lob.betafund.talent"></span></p><p class="fin"><span class="tl">spv</span><span data-key="lob.betafund.spv"></span></p><div class="sides"><p class="side"><span class="sl">side</span><span data-key="lob.betafund.side1"></span></p><p class="side"><span class="sl">side</span><span data-key="lob.betafund.side2"></span></p></div></div><details class="fold"><summary>create, capture, scale</summary><div class="vs"><div class="vr"><span class="vl">create</span><span class="vt" data-key="lob.betafund.create"></span></div><div class="vr"><span class="vl">capture</span><span class="vt" data-key="lob.betafund.capture"></span></div><div class="vr"><span class="vl">scale</span><span class="vt" data-key="lob.betafund.scale"></span></div></div></details></div></div></div>
      <div class="ledger-key"><span class="hchip hchip-h1">H1</span><span class="dots"><i class="dot on"></i><i class="dot on"></i></span> paying and repeatable<span class="sep"></span><span class="hchip hchip-h2">H2</span><span class="dots"><i class="dot on"></i><i class="dot"></i></span> paying, not yet repeatable<span class="sep"></span><span class="hchip hchip-h3">H3</span><span class="dots"><i class="dot"></i><i class="dot"></i></span> neither yet<span class="sep"></span>a bracket means both customers buy the same thing</div>
    </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 tiers</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>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 · Q2 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 on</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-md"></use></svg>venture.md</span><span class="ar">· each OKR's priorities &rarr;</span><span class="lo"><svg viewBox="0 0 24 24"><use href="#lo-pages"></use></svg>butterPages</span></div>
        <p data-key="dossier.okr.intro"></p>
        <h4>Customer-value OKRs · think → build → invest</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>
          <li data-key="okr.7"></li>
          <li data-key="okr.8"></li>
        </ul>
        <h4>Open questions — become OKRs once answered</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>
          <li class="q" data-key="dossier.okr.open.4"></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 — live state</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 — what persists</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 · week of 2026-05-18 — 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>

<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:'');};

  function showForm(which){['form-login','form-signup','form-verify'].forEach(f=>$(f).className=(f===which?'':'hide'));}
  function showLogin(){showForm('form-login');msg('');}
  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){
    // 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,'');
        el.textContent=text;
      }else{
        el.innerHTML=row.content;
      }
    });
  }

  function paintAll(){
    blocksByKey.forEach((row,key)=>paintKey(key,row));
    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();openInlineEdit(key);});
      }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 doesn't support contenteditable cleanly — open a tiny inline editor prompt
  function openInlineEdit(key){
    if(!canEdit)return;
    const row=blocksByKey.get(key);
    const current=row?row.content:'';
    const next=prompt('Edit "'+key+'"\n\nNote: SVG text is plain (no HTML tags rendered).',current.replace(/<[^>]+>/g,''));
    if(next===null)return;
    if(next===current)return;
    // 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'));
    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){
        toast('Save failed: '+(data.error||r.status),true);
        document.querySelectorAll('text[data-key="'+CSS.escape(key)+'"]').forEach(el=>{el.classList.remove('saving');el.classList.add('err');});
        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(()=>toast('Network error',true));
  }

  // ===== 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);
    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();
      msg('Session expired, sign in again.','err');showLogin();
      $('app').className='hide';$('gate-wrap').className='';
      return false;
    }
    if(r.status===403){
      clearTokens();
      msg('That account is not authorized. Sign in with a @betauniversity.org account.','err');showLogin();
      $('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');
      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;
        // 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 prev=blocksByKey.get(row.block_key);
          if(!prev||prev.updated_at!==row.updated_at){
            blocksByKey.set(row.block_key,row);
            const t=lastSavedAt.get(row.block_key);
            if(t && (Date.now()-t)<2500)return;
            const focused=document.activeElement;
            if(focused && focused.dataset && focused.dataset.key===row.block_key)return;
            paintKey(row.block_key,row);
          }
        });
      }catch(e){}
    },4000);
  }

  async function bootApp(){
    if(!await loadBlocks())return;
    $('gate-wrap').className='hide';
    $('app').className='';
    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();}
  })();
</script>
</body>
</html>
