← Git Explorer results

Git Explorer

Deepseek v4.1 Flash · full

Initial view of Git Explorer

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 73,031 bytes · SHA-256 82d4377ee09d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>GitContext · Branching Topology + Contextual Events</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
*{box-sizing:border-box}
:root{
  --bg:#080c12; --panel:#0e141d; --panel2:#121b26; --line:#1d2836; --line2:#26374a;
  --text:#d9e3ef; --dim:#7b8ea3; --accent:#58a6ff; --accent2:#7ee787;
  --freeze:#f0883e; --milestone:#3fb950; --incident:#f85149; --audit:#bc8cff; --release:#58a6ff; --note:#8b949e;
  --mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
}
html,body{height:100%;margin:0}
body{
  background:
    radial-gradient(1100px 620px at 78% -8%, #14304d55, transparent 60%),
    radial-gradient(900px 500px at 8% 105%, #1d3b2c55, transparent 60%),
    var(--bg);
  color:var(--text);
  font:13px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
  overflow:hidden;
}
button,input,select,textarea{font:inherit;color:inherit}
.hidden{display:none!important}
::-webkit-scrollbar{width:9px;height:9px}
::-webkit-scrollbar-thumb{background:#22303f;border-radius:8px}
::-webkit-scrollbar-thumb:hover{background:#2e4358}
::-webkit-scrollbar-track{background:transparent}

/* ---------- shell ---------- */
.app{height:100%;display:grid;grid-template-rows:50px 1fr}
.topbar{display:flex;align-items:center;gap:10px;padding:0 14px;border-bottom:1px solid var(--line);
  background:linear-gradient(180deg,#0f1721,#0b1119)}
.brand{display:flex;align-items:baseline;gap:9px;font-weight:600;letter-spacing:.3px}
.brand b{font-size:15px}
.brand .glyph{color:var(--accent);font-size:17px}
.brand small{color:var(--dim);font-weight:400;font-size:11px;letter-spacing:.6px;text-transform:uppercase}
.spacer{flex:1}
.btn{background:#152131;border:1px solid var(--line2);color:#c7d5e4;padding:5px 11px;border-radius:7px;
  cursor:pointer;transition:.14s;font-size:12px;white-space:nowrap}
.btn:hover{background:#1c2c40;border-color:#39536f;color:#fff;transform:translateY(-1px)}
.btn.primary{background:linear-gradient(180deg,#2f7fd6,#1e5fa8);border-color:#4a90d9;color:#fff;font-weight:600}
.btn.primary:hover{background:linear-gradient(180deg,#3a8ee8,#2469b8)}
.btn.ghost{background:transparent;border-color:#2a3a4d}
.btn.danger:hover{background:#3a1b1b;border-color:#7a3232;color:#ffb4b4}
.btn.tiny{padding:2px 7px;font-size:11px;border-radius:6px}
.sep{width:1px;height:22px;background:var(--line2)}

.body{display:grid;grid-template-columns:326px 1fr;min-height:0}

/* ---------- sidebar ---------- */
.sidebar{display:flex;flex-direction:column;min-height:0;border-right:1px solid var(--line);background:#0b1119d9}
.tabs{display:flex;gap:2px;padding:8px 8px 0;border-bottom:1px solid var(--line)}
.tab{flex:1;background:transparent;border:0;border-bottom:2px solid transparent;color:var(--dim);
  padding:7px 4px;cursor:pointer;font-size:11.5px;letter-spacing:.4px;text-transform:uppercase;font-weight:600}
.tab:hover{color:#b6c8da}
.tab.active{color:var(--accent);border-bottom-color:var(--accent)}
.panels{flex:1;overflow-y:auto;padding:12px}
.panel{display:none;flex-direction:column;gap:10px}
.panel.active{display:flex}
.card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:11px;display:flex;flex-direction:column;gap:8px}
.card h3{margin:0;font-size:11px;letter-spacing:.9px;text-transform:uppercase;color:var(--dim);font-weight:700}
.frow{display:flex;flex-direction:column;gap:4px}
.frow2{display:grid;grid-template-columns:1fr 1fr;gap:8px}
label{font-size:10.5px;letter-spacing:.5px;text-transform:uppercase;color:var(--dim);font-weight:600}
input[type=text],input[type=date],select,textarea{
  background:#0a121b;border:1px solid var(--line2);border-radius:7px;padding:6px 8px;width:100%;outline:none;transition:.14s}
textarea{resize:vertical;min-height:52px;font-size:12px}
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px #58a6ff22}
.picklist{max-height:132px;overflow-y:auto;border:1px solid var(--line2);border-radius:8px;background:#0a121b;padding:5px}
.pick{display:flex;gap:7px;align-items:center;padding:4px 5px;border-radius:6px;cursor:pointer;font-size:11.5px}
.pick:hover{background:#16222f}
.pick input{accent-color:var(--accent)}
.pick .pid{font-family:var(--mono);color:var(--accent2);font-size:11px}
.pick .pmsg{color:#9fb1c4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rowbtns{display:flex;gap:6px}
.hint{color:var(--dim);font-size:11px;line-height:1.4}

.list{display:flex;flex-direction:column;gap:5px;max-height:290px;overflow-y:auto;padding-right:2px}
.item{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--line);
  border-radius:8px;padding:6px 8px;cursor:pointer;transition:.13s}
.item:hover{border-color:#38506c;background:#16222f}
.item.sel{border-color:var(--accent);box-shadow:0 0 0 2px #58a6ff25}
.item .dot{width:9px;height:9px;border-radius:50%;flex:none}
.item .mid{flex:1;min-width:0}
.item .t{font-weight:600;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.item .s{font-size:10.5px;color:var(--dim);font-family:var(--mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.badge{font-size:9.5px;padding:1px 6px;border-radius:20px;border:1px solid;letter-spacing:.4px;text-transform:uppercase;font-weight:700;flex:none}
.actions{display:flex;gap:3px;flex:none}

/* ---------- stage ---------- */
.stage{display:grid;grid-template-rows:minmax(0,1fr) 214px;min-width:0;min-height:0}
.pane{position:relative;min-height:0;display:flex;flex-direction:column;min-width:0}
.pane-head{display:flex;align-items:center;gap:10px;padding:7px 13px;border-bottom:1px solid var(--line);
  background:#0c131c;font-size:11px;letter-spacing:.9px;text-transform:uppercase;color:#9db0c4;font-weight:700}
.pane-head .hint{text-transform:none;letter-spacing:0;font-weight:400}
.graph-pane{flex:1;position:relative;min-height:0;overflow:hidden}
#graph{display:block;width:100%;height:100%;cursor:grab}
#graph.grabbing{cursor:grabbing}
.flow-pane{border-top:1px solid var(--line);background:#0a1017}
.flow-scroll{flex:1;overflow-x:auto;overflow-y:hidden;min-height:0}
.flow-inner{display:flex;gap:14px;padding:20px 16px 12px;position:relative;width:max-content;min-width:100%}
.flow-inner::before{content:'';position:absolute;left:0;right:0;top:31px;height:2px;
  background:linear-gradient(90deg,transparent,#22303f 4%,#22303f 96%,transparent)}
.flow-empty{color:var(--dim);padding:26px 18px;font-size:12px}
.flow-item{position:relative;width:196px;padding-top:44px;cursor:pointer}
.flow-item .fdot{position:absolute;top:32px;left:50%;transform:translate(-50%,-50%);width:11px;height:11px;
  border-radius:50%;background:var(--c);box-shadow:0 0 0 3px #0a1017,0 0 12px var(--c);transition:.15s}
.flow-item .fstem{position:absolute;top:37px;left:50%;width:2px;height:14px;background:var(--c);opacity:.45}
.flow-item .fcard{background:var(--panel);border:1px solid var(--line);border-left:3px solid var(--c);
  border-radius:9px;padding:8px 10px;transition:.15s;height:100%}
.flow-item:hover .fcard{border-color:#3d5a7a;transform:translateY(-2px);box-shadow:0 8px 20px #0008}
.flow-item:hover .fdot{transform:translate(-50%,-50%) scale(1.35)}
.flow-item.sel .fcard{border-color:var(--accent);box-shadow:0 0 0 2px #58a6ff33,0 8px 22px #0009}
.flow-item.sel .fdot{box-shadow:0 0 0 4px #58a6ff55,0 0 16px var(--c)}
.fcard .ftop{display:flex;justify-content:space-between;gap:6px;align-items:center}
.fcard .ftype{font-size:9.5px;letter-spacing:.6px;text-transform:uppercase;font-weight:800;color:var(--c)}
.fcard .fdate{font-size:10px;color:var(--dim);font-family:var(--mono)}
.fcard .ftitle{font-weight:600;font-size:12.5px;margin:3px 0 6px;line-height:1.25}
.fcard .flink{font-family:var(--mono);font-size:10.5px;color:#8fa6bd;display:flex;gap:5px;align-items:center;
  border-top:1px dashed #1f2b3a;padding-top:5px;overflow:hidden}
.fcard .flink b{color:var(--accent2);font-weight:600}
.gnode .msg,.gnode .pill,.gnode .tagtxt{paint-order:stroke;stroke:#0a0f16;stroke-width:3.4px;stroke-linejoin:round}
svg text{user-select:none}

/* ---------- modal ---------- */
.overlay{position:fixed;inset:0;background:#04070bcc;backdrop-filter:blur(3px);z-index:100;display:flex;
  align-items:center;justify-content:center;animation:fade .14s ease}
@keyframes fade{from{opacity:0}to{opacity:1}}
.modal{width:min(660px,92vw);max-height:86vh;display:flex;flex-direction:column;background:#0d141d;
  border:1px solid var(--line2);border-radius:13px;box-shadow:0 24px 70px #000c;overflow:hidden}
.modal h3{margin:0;padding:12px 15px;font-size:12px;letter-spacing:.9px;text-transform:uppercase;color:#a9bccf;
  border-bottom:1px solid var(--line);background:#0b1119}
.modal .mbody{padding:14px 15px;overflow:auto;display:flex;flex-direction:column;gap:10px}
.modal .mact{display:flex;gap:8px;justify-content:flex-end;padding:11px 15px;border-top:1px solid var(--line);background:#0b1119}
.modal textarea.mtext{width:100%;min-height:230px;font-family:var(--mono);font-size:11.5px;line-height:1.5;
  background:#080d14;border:1px solid var(--line2);border-radius:9px;padding:10px;resize:vertical}
.modal .mnote{color:var(--dim);font-size:11.5px;line-height:1.5}
.modal .mnote code{font-family:var(--mono);color:#7ee787;background:#12202c;padding:1px 5px;border-radius:5px}
input[type=file]{font-size:11.5px;color:var(--dim)}

/* ---------- responsive ---------- */
.menu-btn{display:none}
@media (max-width: 980px){
  .body{grid-template-columns:1fr}
  .menu-btn{display:inline-block}
  .topbar{overflow-x:auto}
  .topbar::-webkit-scrollbar{height:0}
  .sidebar{position:fixed;top:51px;bottom:0;left:0;width:302px;z-index:60;transform:translateX(-104%);
    transition:transform .22s ease;box-shadow:18px 0 44px #000b;background:#0b1119f7}
  .sidebar.open{transform:none}
  .stage{grid-template-rows:minmax(0,1fr) 178px}
  .inspector{width:260px}
  .legend{max-width:150px}
}

/* ---------- legend / inspector / tooltip ---------- */
.legend{position:absolute;top:12px;right:12px;background:#0b1119e6;border:1px solid var(--line);border-radius:9px;
  padding:8px 10px;font-size:10.5px;display:flex;flex-direction:column;gap:4px;backdrop-filter:blur(6px);pointer-events:none;
  max-width:190px}
.legend .lg-title{color:var(--dim);letter-spacing:.8px;font-weight:700;font-size:9.5px;text-transform:uppercase;margin-bottom:1px}
.legend .row{display:flex;align-items:center;gap:6px;color:#a9bccf}
.legend .sw{width:9px;height:9px;border-radius:50%}
.legend .sw.sq{border-radius:3px}

.inspector{position:absolute;left:14px;bottom:12px;width:322px;background:#0b1119f0;border:1px solid var(--line2);
  border-radius:11px;padding:11px 12px;backdrop-filter:blur(8px);box-shadow:0 14px 40px #000a;display:none}
.inspector.on{display:block}
.inspector .ihead{display:flex;align-items:center;gap:8px}
.inspector .ihash{font-family:var(--mono);color:var(--accent2);font-size:12.5px;font-weight:700}
.inspector .iclose{margin-left:auto;background:none;border:0;color:var(--dim);cursor:pointer;font-size:15px;line-height:1;padding:0 3px}
.inspector .iclose:hover{color:#fff}
.inspector h4{margin:7px 0 3px;font-size:13px;font-weight:600}
.inspector .meta{font-size:11px;color:var(--dim);font-family:var(--mono)}
.inspector .chip{display:inline-flex;align-items:center;gap:5px;font-size:10.5px;padding:2px 8px;border-radius:20px;
  border:1px solid var(--line2);margin:6px 5px 0 0;background:#121b26}
.inspector .evlist{margin-top:8px;border-top:1px dashed #22303f;padding-top:7px;display:flex;flex-direction:column;gap:5px}
.inspector .ev{display:flex;gap:7px;align-items:flex-start;font-size:11.5px;padding:5px 7px;border-radius:7px;
  background:#121b26;border-left:3px solid var(--c)}
.inspector .ev .et{font-weight:600}
.inspector .ev .es{color:var(--dim);font-size:10.5px}

.tooltip{position:absolute;pointer-events:none;z-index:40;background:#0d151ff2;border:1px solid #35506e;
  border-radius:9px;padding:9px 11px;max-width:300px;box-shadow:0 12px 34px #000b;opacity:0;transition:opacity .12s;
  backdrop-filter:blur(8px)}
.tooltip.on{opacity:1}
.tooltip .tt-type{font-size:9.5px;letter-spacing:.7px;text-transform:uppercase;font-weight:800;margin-bottom:3px}
.tooltip .tt-title{font-weight:700;font-size:13px;margin-bottom:4px}
.tooltip .tt-desc{color:#a4b7ca;font-size:11.5px;margin-bottom:6px}
.tooltip .tt-sep{border-top:1px dashed #26374a;margin:6px 0;padding-top:6px}
.tooltip .tt-line{font-family:var(--mono);font-size:11px;color:#9db0c4;display:flex;gap:6px}
.tooltip .tt-line b{color:var(--accent2)}
.tooltip .k{color:var(--dim);font-size:10px;letter-spacing:.5px;text-transform:uppercase;font-weight:700}

/* ---------- svg bits ---------- */
.edge{fill:none;stroke-linecap:round;transition:stroke .18s,opacity .18s}
.gnode{cursor:pointer}
.gnode .halo{fill:none;stroke:var(--accent);stroke-width:1.6;stroke-dasharray:4 4;opacity:0;transition:opacity .2s}
.gnode.sel .halo{opacity:1;animation:spin 9s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.gnode .core{transition:.18s}
.gnode:hover .core{filter:url(#glow)}
.gnode .msg{font-size:12.5px;fill:#bcd0e4;pointer-events:none}
.gnode.sel .msg{fill:#fff}
.gnode .shas{font-family:var(--mono);font-size:10px;fill:#5f7488;pointer-events:none}
.gnode.dim{opacity:.42}
.evmark{cursor:pointer;transition:.16s}
.evmark .box{stroke-width:1;transition:.16s}
.evmark.sel{filter:url(#glow)}
.evmark.dim{opacity:.35}
.evmark:hover .box{stroke-width:1.6}
.pill{font-size:9.5px;font-weight:700;letter-spacing:.4px}
.pathlink{fill:none;stroke-dasharray:3 3}
</style>
</head>
<body>
<div class="app">
  <header class="topbar">
    <button class="btn menu-btn" id="btnMenu" title="Panels">☰</button>
    <div class="brand"><span class="glyph">◈</span><b>GitContext</b><small>topology × events</small></div>
    <div class="sep"></div>
    <button class="btn" id="btnImport">⇩ Import</button>
    <button class="btn" id="btnExport">⇧ Export</button>
    <button class="btn" id="btnSample">⟲ Sample repo</button>
    <button class="btn danger" id="btnClear">✕ Clear</button>
    <div class="spacer"></div>
    <div class="sep"></div>
    <button class="btn" id="btnFit">⛶ Fit view</button>
    <button class="btn" id="btnDagre">⇅ Re-flow</button>
  </header>

  <div class="body">
    <aside class="sidebar">
      <div class="tabs">
        <button class="tab active" data-tab="commits">Commits</button>
        <button class="tab" data-tab="branches">Branches</button>
        <button class="tab" data-tab="tags">Tags</button>
        <button class="tab" data-tab="events">Events</button>
      </div>
      <div class="panels">

        <section class="panel active" data-panel="commits">
          <div class="card">
            <h3 id="cFormTitle">New commit</h3>
            <div class="frow"><label>Message</label><input type="text" id="cMsg" placeholder="e.g. Add OAuth token refresh"></div>
            <div class="frow2">
              <div class="frow"><label>Author</label><input type="text" id="cAuthor" value="dev"></div>
              <div class="frow"><label>Date</label><input type="date" id="cDate"></div>
            </div>
            <div class="frow"><label>Branch label</label><input type="text" id="cBranch" list="branchDL" placeholder="main"><datalist id="branchDL"></datalist></div>
            <div class="frow"><label>Parent commits (DAG parents)</label><div class="picklist" id="cParents"></div></div>
            <div class="rowbtns">
              <button class="btn primary" id="cSave">Add commit</button>
              <button class="btn ghost hidden" id="cCancel">Cancel</button>
            </div>
            <div class="hint">⌥ Tip: tick two parents to create a merge commit.</div>
          </div>
          <div class="card">
            <h3>History (newest first)</h3>
            <div class="list" id="commitList"></div>
          </div>
        </section>

        <section class="panel" data-panel="branches">
          <div class="card">
            <h3 id="bFormTitle">New branch</h3>
            <div class="frow"><label>Branch name</label><input type="text" id="bName" placeholder="feature/payments"></div>
            <div class="frow"><label>Points at commit</label><select id="bTarget"></select></div>
            <div class="rowbtns">
              <button class="btn primary" id="bSave">Add branch</button>
              <button class="btn ghost hidden" id="bCancel">Cancel</button>
            </div>
          </div>
          <div class="card"><h3>Branches</h3><div class="list" id="branchList"></div></div>
        </section>

        <section class="panel" data-panel="tags">
          <div class="card">
            <h3 id="tFormTitle">New tag</h3>
            <div class="frow"><label>Tag name</label><input type="text" id="tName" placeholder="v1.0.0"></div>
            <div class="frow"><label>Tagged commit</label><select id="tTarget"></select></div>
            <div class="rowbtns">
              <button class="btn primary" id="tSave">Add tag</button>
              <button class="btn ghost hidden" id="tCancel">Cancel</button>
            </div>
          </div>
          <div class="card"><h3>Tags</h3><div class="list" id="tagList"></div></div>
        </section>

        <section class="panel" data-panel="events">
          <div class="card">
            <h3 id="eFormTitle">New contextual event</h3>
            <div class="frow"><label>Title</label><input type="text" id="eTitle" placeholder="Code Freeze"></div>
            <div class="frow2">
              <div class="frow"><label>Type</label><select id="eType"></select></div>
              <div class="frow"><label>Date</label><input type="date" id="eDate"></div>
            </div>
            <div class="frow"><label>Description</label><textarea id="eDesc" placeholder="What happened, and why it matters…"></textarea></div>
            <div class="frow"><label>Anchor</label>
              <select id="eTargetGroup"></select>
            </div>
            <div class="hint" id="eAnchorPreview"></div>
            <div class="rowbtns">
              <button class="btn primary" id="eSave">Add event</button>
              <button class="btn ghost hidden" id="eCancel">Cancel</button>
            </div>
          </div>
          <div class="card"><h3>Events (chronological)</h3><div class="list" id="eventList"></div></div>
        </section>

      </div>
    </aside>

    <main class="stage">
      <div class="pane graph-pane">
        <div class="pane-head">Git graph <span class="hint">· drag to pan · wheel to zoom · click a commit to reveal its anchored events</span></div>
        <div style="position:relative;flex:1;min-height:0" id="graphHolder">
          <svg id="graph"></svg>
          <div class="legend" id="legend"></div>
          <div class="inspector" id="inspector"></div>
          <div class="tooltip" id="tooltip"></div>
        </div>
      </div>
      <div class="pane flow-pane">
        <div class="pane-head">Contextual flow <span class="hint">· every external event, ordered along the history it belongs to</span></div>
        <div class="flow-scroll" id="flowScroll"><div class="flow-inner" id="flowInner"></div></div>
      </div>
    </main>
  </div>
</div>
<script>
'use strict';
/* ============================================================
   GitContext — Git branching topology × external contextual events
   ============================================================ */

const STORE_KEY = 'gitcontext.state.v1';
const PALETTE = ['#58a6ff','#3fb950','#f0883e','#bc8cff','#f778ba','#e3b341','#4dd0e1','#ff7b72','#7ee787','#d2a8ff'];
const TYPES = {
  freeze:    {name:'Code Freeze', hex:'#f0883e', glyph:'❄'},
  milestone: {name:'Milestone',   hex:'#3fb950', glyph:'◆'},
  incident:  {name:'Incident',    hex:'#f85149', glyph:'▲'},
  audit:     {name:'Audit',       hex:'#bc8cff', glyph:'⚖'},
  release:   {name:'Release',     hex:'#58a6ff', glyph:'★'},
  note:      {name:'Note',        hex:'#8b949e', glyph:'✎'}
};
let ROW_GAP = 62, LANE_GAP = 132, GUTTER_X = -190, MARK_W = 158, MARK_H = 28;

const $  = (s, r = document) => r.querySelector(s);
const $$ = (s, r = document) => Array.from(r.querySelectorAll(s));
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, m => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));
const trunc = (s, n) => { s = String(s == null ? '' : s); return s.length > n ? s.slice(0, n - 1) + '…' : s; };
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
function rnd(n){ let s=''; const h='0123456789abcdef'; for(let i=0;i<n;i++) s+=h[Math.floor(Math.random()*16)]; return s; }
function uid(p){ return p + rnd(6); }
const tOf = x => TYPES[x] ? TYPES[x] : TYPES.note;
const dstr = ts => { const d = new Date(ts||Date.now()); return d.toISOString().slice(0,10); };
const tstr = s => { const d = new Date((s||'') + 'T12:00:00Z'); return isNaN(d) ? Date.now() : d.getTime(); };
const fmtDate = s => { const d = new Date((s||'') + 'T12:00:00Z'); return isNaN(d) ? '—' : d.toISOString().slice(0,10); };

/* ---------------- state ---------------- */
let state = freshState();
let layout = null;
let selectedId = null;         // selected commit id
let hoverEv = null;
let editing = { commit:null, branch:null, tag:null, event:null };
let lastCommitCount = -1;

function freshState(){ return { commits:[], branches:[], tags:[], events:[], head:'main', meta:{name:'repo'} }; }

function sampleState(){
  const base = Date.UTC(2024, 2, 4, 9, 0, 0), D = 86400000;
  const c = (id, message, parents, branch, day, author) =>
    ({ id, message, parents, branch, ts: base + day*D, author });
  const commits = [
    c('a1c0e77','Initial commit: project scaffolding',[],'main',0,'ana'),
    c('b7d21f4','Add CI pipeline (lint + unit tests)',['a1c0e77'],'main',2,'ana'),
    c('c93aa10','Introduce auth module skeleton',['b7d21f4'],'feature/auth',4,'ravi'),
    c('d2f8b65','Implement JWT signing & rotation',['c93aa10'],'feature/auth',6,'ravi'),
    c('e5b1043','Add refresh-token endpoint',['d2f8b65'],'feature/auth',8,'ravi'),
    c('f4a9c02','Refresh README badges',['b7d21f4'],'main',5,'ana'),
    c('07be318','Bump eslint to v9',['f4a9c02'],'main',7,'sam'),
    c('1de6a90','Merge branch feature/auth',['e5b1043','07be318'],'main',10,'ana'),
    c('2ab77c5','Rate-limit the token endpoint',['1de6a90'],'feature/hardening',12,'sam'),
    c('3c0d5ef','Add structured audit logging',['2ab77c5'],'feature/hardening',13,'sam'),
    c('4e91b23','Document deployment runbook',['1de6a90'],'main',12,'ana'),
    c('5fa0c76','Merge branch feature/hardening',['3c0d5ef','4e91b23'],'main',15,'ana'),
    c('6b2e144','Release v1.0.0 polish',['5fa0c76'],'main',18,'ana')
  ];
  const ev = (title, type, commitId, date, description, branch) =>
    ({ id: uid('ev_'), title, type, commitId, date, description, branch: branch || null });
  const events = [
    ev('Project kickoff review','note','a1c0e77','2024-03-04','Scope agreed with stakeholders; the trunk is now under protected-branch rules.',null),
    ev('Sprint 1 code freeze','freeze','07be318','2024-03-11','Only release-blocking fixes allowed into main until the v0.9 cut.',null),
    ev('Auth feature complete','milestone','e5b1043','2024-03-12','Login, refresh and rotation paths all pass acceptance. Ready for review.',null),
    ev('Third-party security audit','audit','d2f8b65','2024-03-10','External reviewers checked token storage and signing boundaries.',null),
    ev('Sev-2: token revocation gap','incident','2ab77c5','2024-03-16','Revoked tokens stayed valid for 15 minutes. Mitigated with a deny-list.',null),
    ev('Docs freeze for launch','freeze','4e91b23','2024-03-16','Runbook and API reference frozen ahead of the release candidate.',null),
    ev('Hardening sign-off','audit','3c0d5ef','2024-03-17','Security team signed off on the hardening branch before it merged.',null),
    ev('v1.0.0 released','release','6b2e144','2024-03-22','Trunk tagged and published; the milestone is now closed.',null)
  ];
  return {
    commits, events,
    branches:[
      {name:'main', head:'6b2e144'},
      {name:'feature/auth', head:'e5b1043'},
      {name:'feature/hardening', head:'3c0d5ef'}
    ],
    tags:[{name:'v1.0.0', commit:'5fa0c76'},{name:'v0.9.0-beta', commit:'1de6a90'}],
    head:'main', meta:{name:'demo-service'}
  };
}

/* ---------------- persistence ---------------- */
function save(){
  try{ localStorage.setItem(STORE_KEY, JSON.stringify(state)); }
  catch(e){ console.warn('save failed', e); }
}
function load(){
  try{
    const raw = localStorage.getItem(STORE_KEY);
    if(!raw) return null;
    const s = JSON.parse(raw);
    return normalize(s);
  }catch(e){ console.warn('load failed', e); return null; }
}
function normalize(s){
  const out = freshState();
  if(!s || typeof s !== 'object') return out;
  out.meta = s.meta || {name:'repo'};
  out.commits = (Array.isArray(s.commits) ? s.commits : []).map(c => ({
    id: String(c.id),
    message: String(c.message == null ? '' : c.message),
    parents: (Array.isArray(c.parents) ? c.parents : []).filter(p => p && p !== c.id).map(String),
    branch: c.branch ? String(c.branch) : 'main',
    ts: typeof c.ts === 'number' ? c.ts : tstr(c.date),
    author: c.author ? String(c.author) : 'dev'
  }));
  const ids = new Set(out.commits.map(c => c.id));
  out.commits.forEach(c => { c.parents = c.parents.filter(p => ids.has(p)); });
  out.branches = (Array.isArray(s.branches) ? s.branches : []).map(b => ({
    name: String(b.name), head: ids.has(b.head) ? b.head : (out.commits[0] ? out.commits[0].id : null)
  })).filter(b => b.name);
  out.tags = (Array.isArray(s.tags) ? s.tags : []).map(t => ({ name:String(t.name), commit:String(t.commit) }))
    .filter(t => t.name && ids.has(t.commit));
  out.events = (Array.isArray(s.events) ? s.events : []).map(e => ({
    id: e.id ? String(e.id) : uid('ev_'),
    title: String(e.title == null ? '' : e.title),
    type: TYPES[e.type] ? e.type : 'note',
    description: e.description ? String(e.description) : '',
    commitId: String(e.commitId || e.commit || ''),
    date: e.date ? String(e.date).slice(0,10) : dstr(e.ts),
    branch: e.branch ? String(e.branch) : null
  })).filter(e => ids.has(e.commitId));
  out.head = s.head ? String(s.head) : (out.branches[0] ? out.branches[0].name : 'main');
  if(!out.branches.some(b => b.name === out.head) && out.branches[0]) out.head = out.branches[0].name;
  return out;
}

/* ---------------- colours & lookups ---------------- */
function branchColorMap(){
  const m = new Map();
  const names = state.branches.map(b => b.name);
  state.commits.forEach(c => { if(c.branch && !names.includes(c.branch)) names.push(c.branch); });
  names.forEach((n,i) => m.set(n, PALETTE[i % PALETTE.length]));
  return m;
}
let COLOR = new Map();
const colorOf = name => COLOR.get(name) || '#5b6b7d';
const commitById = id => state.commits.find(c => c.id === id);
const branchByName = n => state.branches.find(b => b.name === n);
const headCommit = () => { const b = branchByName(state.head); return b ? b.head : null; };
function headOfBranch(cid){
  const kids = childrenOf(cid);
  if(!kids.length) return true;
  return false;
}
function childrenOf(id){
  return state.commits.filter(c => (c.parents||[]).includes(id));
}
function eventsFor(cid){ return state.events.filter(e => e.commitId === cid); }
function branchLabelOf(c){ return c.branch || 'main'; }
function branchTips(){
  const m = new Map();
  state.branches.forEach(b => { if(!b.head) return; if(!m.has(b.head)) m.set(b.head, []); m.get(b.head).push(b.name); });
  return m;
}
function tagsByCommit(){
  const m = new Map();
  state.tags.forEach(t => { if(!m.has(t.commit)) m.set(t.commit, []); m.get(t.commit).push(t.name); });
  return m;
}
function descendants(id){
  const out = new Set(); const stack = [id];
  while(stack.length){
    const cur = stack.pop();
    childrenOf(cur).forEach(k => { if(!out.has(k.id)){ out.add(k.id); stack.push(k.id); } });
  }
  return out;
}

/* ---------------- DAG layout ---------------- */
function computeLayout(){
  const commits = state.commits, byId = new Map(commits.map(c => [c.id, c]));
  const remaining = new Map(commits.map(c => [c.id, 0]));
  for(const c of commits) for(const p of (c.parents||[])) if(byId.has(p)) remaining.set(p, remaining.get(p)+1);

  const pool = commits.filter(c => remaining.get(c.id) === 0).slice();
  const sortPool = () => pool.sort((a,b) => (b.ts||0)-(a.ts||0) || String(a.id).localeCompare(String(b.id)));
  sortPool();
  const order = [], placed = new Set();
  while(pool.length){
    const c = pool.shift();
    if(placed.has(c.id)) continue;
    placed.add(c.id); order.push(c);
    for(const p of (c.parents||[])){
      if(!byId.has(p)) continue;
      remaining.set(p, remaining.get(p)-1);
      if(remaining.get(p) <= 0 && !placed.has(p)){ pool.push(byId.get(p)); sortPool(); }
    }
  }
  commits.forEach(c => { if(!placed.has(c.id)){ placed.add(c.id); order.push(c); } });

  const lanes = [], pos = new Map();
  order.forEach((c, row) => {
    let lane = lanes.indexOf(c.id);
    if(lane === -1){ lane = lanes.indexOf(null); if(lane === -1){ lanes.push(null); lane = lanes.length-1; } }
    for(let k = 0; k < lanes.length; k++) if(k !== lane && lanes[k] === c.id) lanes[k] = null;
    pos.set(c.id, { lane, row });
    const ps = (c.parents||[]).filter(p => byId.has(p));
    if(ps.length){
      lanes[lane] = ps[0];
      for(let i = 1; i < ps.length; i++){
        let pl = lanes.indexOf(ps[i]);
        if(pl === -1){ pl = lanes.indexOf(null); if(pl === -1){ lanes.push(null); pl = lanes.length-1; } lanes[pl] = ps[i]; }
      }
    } else lanes[lane] = null;
  });
  const edges = [];
  for(const c of order) for(const p of (c.parents||[])) if(byId.has(p)) edges.push({ child:c, parent:byId.get(p) });

  const laneCount = Math.max(1, lanes.length);
  const rows = Math.max(1, order.length);
  const p = id => { const q = pos.get(id) || {lane:0,row:0}; return { x:q.lane*LANE_GAP, y:q.row*ROW_GAP, lane:q.lane, row:q.row }; };
  const bounds = {
    minX: GUTTER_X - 10, minY: -56,
    maxX: (laneCount-1)*LANE_GAP + 360, maxY: (rows-1)*ROW_GAP + 70
  };
  return { order, edges, pos, laneCount, rows, point:p, bounds };
}

/* ============================================================
   Rendering
   ============================================================ */
let svg, view, zoom;

function initGraph(){
  svg = d3.select('#graph');
  const defs = svg.append('defs');
  const f = defs.append('filter').attr('id','glow').attr('x','-90%').attr('y','-90%').attr('width','280%').attr('height','280%');
  f.append('feGaussianBlur').attr('stdDeviation','3.2').attr('result','b');
  const fm = f.append('feMerge');
  fm.append('feMergeNode').attr('in','b');
  fm.append('feMergeNode').attr('in','SourceGraphic');
  view = svg.append('g').attr('class','view');
  zoom = d3.zoom().scaleExtent([0.18, 2.8]).on('zoom', ev => view.attr('transform', ev.transform));
  svg.call(zoom).on('dblclick.zoom', null);
  svg.on('click', ev => { if(ev.target.closest('svg') === ev.target) selectCommit(null); });
  svg.on('mousedown', () => svg.classed('grabbing', true));
  window.addEventListener('mouseup', () => svg.classed('grabbing', false));
}

function neighborsOf(id){
  const s = new Set([id]);
  const c = commitById(id);
  if(c) (c.parents||[]).forEach(p => s.add(p));
  childrenOf(id).forEach(k => s.add(k.id));
  state.events.filter(e => e.commitId === id).forEach(() => {});
  return s;
}

/* ---- graph ---- */
function renderGraph(){
  const prevCount = lastCommitCount;
  COLOR = branchColorMap();
  layout = computeLayout();
  lastCommitCount = state.commits.length;
  const { order, edges, point } = layout;
  view.selectAll('*').remove();
  const root = view.append('g');

  const dPath = (a,b) => { const dy = Math.max(26, Math.abs(b.y-a.y)*0.44); return `M${a.x},${a.y} C${a.x},${a.y+dy} ${b.x},${b.y-dy} ${b.x},${b.y}`; };
  const near = selectedId ? neighborsOf(selectedId) : null;
  const eOp = e => !selectedId ? 0.8 : (e.child.id === selectedId || e.parent.id === selectedId) ? 1 : 0.22;

  const eg = root.append('g').attr('class','edges');
  const cg = root.append('g').attr('class','connectors');
  eg.selectAll('path.u').data(edges).join('path')
    .attr('d', e => dPath(point(e.child.id), point(e.parent.id)))
    .attr('fill','none').attr('stroke','#05090e').attr('stroke-width',7.5).attr('stroke-linecap','round').attr('opacity',.85);
  eg.selectAll('path.c').data(edges).join('path')
    .attr('class','edge')
    .attr('d', e => dPath(point(e.child.id), point(e.parent.id)))
    .attr('stroke', e => colorOf(e.child.branch))
    .attr('stroke-width', e => (selectedId && (e.child.id === selectedId || e.parent.id === selectedId)) ? 3.6 : 2.6)
    .attr('opacity', eOp);

  const tips = branchTips(), tagMap = tagsByCommit(), hc = headCommit();

  /* labels layer (behind nodes) */
  const lg = root.append('g').attr('class','labels');
  order.forEach(c => {
    const q = point(c.id);
    const g = lg.append('g').attr('class','lab').attr('transform',`translate(${q.x},${q.y})`);
    const dim = selectedId && selectedId !== c.id && !near.has(c.id);
    if(dim) g.attr('opacity', .34);
    const label = trunc(c.message, 30);
    const tw = Math.min(268, label.length * 6.6 + 20);
    g.append('rect').attr('x',13).attr('y',-10).attr('width',tw).attr('height',21).attr('rx',6)
      .attr('fill', c.id === selectedId ? '#0d1826' : '#080d14')
      .attr('fill-opacity', c.id === selectedId ? .92 : .74)
      .attr('stroke', c.id === selectedId ? '#35597f' : 'none').attr('stroke-width',1);
    g.append('text').attr('class','msg').attr('x',18).attr('y',1).attr('dominant-baseline','central').text(label);
    if(c.id === selectedId) g.append('text').attr('class','shas').attr('x',18).attr('y',22).attr('dominant-baseline','central').text(c.id);
    let px = 18;
    (tips.get(c.id) || []).slice(0,3).forEach(n => {
      const t = state.branches.find(b => b.name === n) || {};
      const isHead = state.head === n;
      px += svgPill(g, px, -19, (isHead ? '⌂ ' : '') + n, colorOf(n), { fill: isHead ? colorOf(n) : null, textFill: isHead ? '#08111b' : colorOf(n), fs:9.5 });
      px += 5;
    });
    (tagMap.get(c.id) || []).slice(0,3).forEach(n => { px += svgPill(g, px, -19, '⌗ ' + n, '#e3b341', {fs:9.5, fill:'#3a2f10', textFill:'#f2cd66'}) + 5; });
  });

  /* nodes layer */
  const ng = root.append('g').attr('class','nodes');
  const gn = ng.selectAll('g').data(order, d => d.id).join('g')
    .attr('class', d => 'gnode' + (d.id === selectedId ? ' sel' : ''))
    .attr('transform', d => { const q = point(d.id); return `translate(${q.x},${q.y})`; })
    .attr('data-id', d => d.id)
    .attr('opacity', d => (!selectedId || near.has(d.id)) ? 1 : .4);
  gn.append('circle').attr('class','halo').attr('r',19);
  gn.filter(d => (d.parents||[]).length > 1).append('path')
    .attr('class','core').attr('d','M0,-13 L13,0 L0,13 L-13,0 Z')
    .attr('fill', d => colorOf(d.branch)).attr('stroke','#050a10').attr('stroke-width',2.5);
  gn.filter(d => (d.parents||[]).length <= 1).append('circle')
    .attr('class','core').attr('r',11)
    .attr('fill', d => colorOf(d.branch)).attr('stroke','#050a10').attr('stroke-width',2.5);
  gn.filter(d => d.id === hc).append('circle')
    .attr('r',16.5).attr('fill','none').attr('stroke','#e6edf6').attr('stroke-width',2).attr('stroke-dasharray','3 4')
    .attr('opacity',.95);
  gn.filter(d => !(d.parents||[]).length).append('circle')
    .attr('r',11).attr('fill','none').attr('stroke', d => colorOf(d.branch)).attr('stroke-width',5).attr('opacity',.28);

  /* commit -> commit link lines are done; now the event anchors */
  const evByCommit = new Map();
  state.events.forEach(e => { if(!layout.pos.has(e.commitId)) return; if(!evByCommit.has(e.commitId)) evByCommit.set(e.commitId, []); evByCommit.get(e.commitId).push(e); });

  const mg = root.append('g').attr('class','markers');
  evByCommit.forEach((list, cid) => {
    const q = point(cid);
    list.forEach((e, i) => {
      const y = q.y + (i - (list.length-1)/2) * 36;
      const t = tOf(e.type);
      const isSel = selectedId === cid;
      const otherSel = selectedId && !isSel;
      cg.append('path')
        .attr('class','pathlink')
        .attr('d', `M${GUTTER_X + MARK_W},${y} C${GUTTER_X + MARK_W + 26},${y} ${q.x - 40},${q.y} ${q.x - 12},${q.y}`)
        .attr('stroke', t.hex).attr('stroke-opacity', otherSel ? .12 : .38).attr('stroke-width', 1);
      const g = mg.append('g')
        .attr('class','evmark' + (isSel ? ' sel' : ''))
        .attr('transform',`translate(${GUTTER_X},${y})`)
        .attr('opacity', otherSel ? .35 : 1)
        .attr('data-ev', e.id).attr('data-commit', cid);
      g.append('rect').attr('class','box').attr('x',0).attr('y',-MARK_H/2)
        .attr('width',MARK_W).attr('height',MARK_H).attr('rx',8)
        .attr('fill', t.hex).attr('fill-opacity', isSel ? .28 : .13)
        .attr('stroke', t.hex).attr('stroke-opacity', .75);
      g.append('rect').attr('x',0).attr('y',-MARK_H/2).attr('width',3.5).attr('height',MARK_H).attr('rx',1.8).attr('fill', t.hex);
      g.append('text').attr('x',12).attr('y',0).attr('dominant-baseline','central')
        .attr('font-size',11).attr('fill', t.hex).text(t.glyph);
      g.append('text').attr('class','pill').attr('x',28).attr('y',0).attr('dominant-baseline','central')
        .attr('font-size',11).attr('fill','#cfdcea').text(trunc(e.title, 20));
    });
  });

  /* zone frame: contextual gutter ⟷ commit DAG */
  const fg = root.append('g').attr('class','frame');
  const bottom = ((layout.rows - 1) * ROW_GAP) + 46;
  fg.append('line').attr('x1',-18).attr('x2',-18).attr('y1',-44).attr('y2',bottom)
    .attr('stroke','#1e2a38').attr('stroke-width',1).attr('stroke-dasharray','5 7');
  fg.append('text').attr('x',GUTTER_X + 2).attr('y',-34).attr('fill','#63768a')
    .attr('font-size',10).attr('font-weight',700).attr('letter-spacing','1.6').text('CONTEXTUAL EVENTS');
  fg.append('text').attr('x',4).attr('y',-34).attr('fill','#63768a')
    .attr('font-size',10).attr('font-weight',700).attr('letter-spacing','1.6').text('COMMIT DAG · newest first');

  /* branch / head legend inside graph: none, kept in the legend box */
  eg.lower(); cg.raise(); lg.raise(); mg.raise(); ng.raise();

  if(prevCount !== state.commits.length || prevCount === -1){
    requestAnimationFrame(() => fitView(prevCount === -1 ? 0 : 320));
  }
}

function svgPill(g, x, y, text, hex, opts){
  opts = opts || {};
  const fs = opts.fs || 9.5;
  const w = Math.max(24, text.length * fs * 0.62 + 14), h = fs + 9;
  gg(g, x, y, text, hex, opts, w, h);
  return w;
}
function gg(g, x, y, text, hex, opts, w, h){
  const fs = opts.fs || 9.5;
  const grp = g.append('g');
  grp.append('rect').attr('x',x).attr('y',y-h/2).attr('width',w).attr('height',h).attr('rx',h/2)
    .attr('fill', opts.fill || hex).attr('fill-opacity', opts.fill ? 1 : .16)
    .attr('stroke', hex).attr('stroke-opacity', opts.fill ? .9 : .8).attr('stroke-width',1);
  grp.append('text').attr('class','pill').attr('x', x + w/2).attr('y', y).attr('text-anchor','middle')
    .attr('dominant-baseline','central').attr('font-size', fs).attr('fill', opts.textFill || hex).text(text);
}

function fitView(dur){
  const holder = $('#graphHolder');
  const w = holder.clientWidth || 900, h = holder.clientHeight || 500;
  if(!layout) return;
  const b = layout.bounds;
  const cw = Math.max(240, b.maxX - b.minX), ch = Math.max(160, b.maxY - b.minY);
  const k = clamp(Math.min(w/cw, h/ch) * 0.96, 0.7, 1.15);
  const scH = ch*k;
  const tx = (w - cw*k)/2 - b.minX*k;
  const ty = (scH > h - 8) ? (14 - b.minY*k) : ((h - scH)/2 - b.minY*k);
  const target = d3.zoomIdentity.translate(tx, ty).scale(k);
  if(dur) svg.transition().duration(dur).call(zoom.transform, target);
  else svg.call(zoom.transform, target);
}
function ensureVisible(id, dur){
  if(!layout || !id) return;
  const holder = $('#graphHolder');
  const w = holder.clientWidth, h = holder.clientHeight;
  const t = d3.zoomTransform(svg.node());
  const p = layout.point(id);
  const sx = t.applyX(p.x), sy = t.applyY(p.y);
  if(sx > 120 && sx < w - 60 && sy > 30 && sy < h - 30) return;
  const tx = w * 0.46 - p.x * t.k, ty = h * 0.5 - p.y * t.k;
  const target = d3.zoomIdentity.translate(tx, ty).scale(t.k);
  svg.transition().duration(dur || 420).call(zoom.transform, target);
}

/* ---- contextual flow ---- */
function sortedEvents(){
  const rowOf = e => { const p = layout && layout.pos.get(e.commitId); return p ? p.row : 9999; };
  return state.events.slice().sort((a,b) => rowOf(a) - rowOf(b) || String(a.date).localeCompare(String(b.date)) || String(a.title).localeCompare(String(b.title)));
}
function renderFlow(){
  const inner = $('#flowInner');
  const evs = sortedEvents();
  if(!evs.length){
    inner.innerHTML = '<div class="flow-empty">No contextual events yet — create one in the <b>Events</b> tab and anchor it to a commit.</div>';
    return;
  }
  inner.innerHTML = evs.map(e => {
    const t = tOf(e.type), c = commitById(e.commitId);
    const sel = selectedId && selectedId === e.commitId;
    return `<div class="flow-item${sel ? ' sel' : ''}" data-ev="${esc(e.id)}" data-commit="${esc(e.commitId)}" style="--c:${t.hex}">
      <div class="fstem"></div><div class="fdot"></div>
      <div class="fcard">
        <div class="ftop"><span class="ftype">${t.glyph} ${esc(t.name)}</span><span class="fdate">${esc(e.date || '')}</span></div>
        <div class="ftitle">${esc(e.title)}</div>
        <div class="flink">⛓ <b>${esc(e.commitId)}</b><span>${esc(c ? trunc(c.message, 20) : '')}</span></div>
      </div>
    </div>`;
  }).join('');
}

/* ---- sidebar lists ---- */
function renderLists(){
  const byTs = state.commits.slice().sort((a,b) => (b.ts||0)-(a.ts||0));
  $('#commitList').innerHTML = byTs.length ? byTs.map(c => {
    const evn = eventsFor(c.id).length;
    return `<div class="item${c.id === selectedId ? ' sel' : ''}" data-commit="${esc(c.id)}">
      <span class="dot" style="background:${colorOf(c.branch)}"></span>
      <div class="mid">
        <div class="t">${esc(c.message)}</div>
        <div class="s">${esc(c.id)} · ${esc(c.branch)} · ${esc(dstr(c.ts))}${evn ? ' · ⚑' + evn : ''}</div>
      </div>
      <div class="actions">
        <button class="btn tiny" data-act="edit-commit" data-id="${esc(c.id)}" title="Edit">✎</button>
        <button class="btn tiny danger" data-act="del-commit" data-id="${esc(c.id)}" title="Delete">✕</button>
      </div>
    </div>`;
  }).join('') : '<div class="hint">No commits yet. Add one above to start the history.</div>';

  $('#branchList').innerHTML = state.branches.length ? state.branches.map(b => {
    const c = commitById(b.head);
    const isHead = b.name === state.head;
    return `<div class="item" data-branch="${esc(b.name)}">
      <span class="dot" style="background:${colorOf(b.name)}"></span>
      <div class="mid">
        <div class="t">${esc(b.name)} ${isHead ? '<span class="badge" style="color:#7ee787;border-color:#2c5a34">HEAD</span>' : ''}</div>
        <div class="s">→ ${esc(b.head || '∅')} ${esc(c ? trunc(c.message, 18) : '')}</div>
      </div>
      <div class="actions">
        ${isHead ? '' : `<button class="btn tiny" data-act="head-branch" data-name="${esc(b.name)}" title="Checkout (set HEAD)">⌂</button>`}
        <button class="btn tiny" data-act="edit-branch" data-name="${esc(b.name)}" title="Edit">✎</button>
        <button class="btn tiny danger" data-act="del-branch" data-name="${esc(b.name)}" title="Delete">✕</button>
      </div>
    </div>`;
  }).join('') : '<div class="hint">No branches.</div>';

  $('#tagList').innerHTML = state.tags.length ? state.tags.map(t => {
    const c = commitById(t.commit);
    return `<div class="item" data-tag="${esc(t.name)}">
      <span class="dot" style="background:#e3b341;border-radius:3px"></span>
      <div class="mid"><div class="t">${esc(t.name)}</div><div class="s">@ ${esc(t.commit)} ${esc(c ? trunc(c.message, 18) : '')}</div></div>
      <div class="actions">
        <button class="btn tiny" data-act="edit-tag" data-name="${esc(t.name)}" title="Edit">✎</button>
        <button class="btn tiny danger" data-act="del-tag" data-name="${esc(t.name)}" title="Delete">✕</button>
      </div>
    </div>`;
  }).join('') : '<div class="hint">No tags.</div>';

  $('#eventList').innerHTML = sortedEvents().length ? sortedEvents().map(e => {
    const t = tOf(e.type), c = commitById(e.commitId);
    return `<div class="item${selectedId === e.commitId ? ' sel' : ''}" data-ev="${esc(e.id)}" data-commit="${esc(e.commitId)}">
      <span class="dot" style="background:${t.hex};border-radius:3px"></span>
      <div class="mid">
        <div class="t">${t.glyph} ${esc(e.title)}</div>
        <div class="s">${esc(t.name)} · ${esc(e.date || '')} · ⛓ ${esc(e.commitId)} ${esc(c ? trunc(c.message, 16) : '')}</div>
      </div>
      <div class="actions">
        <button class="btn tiny" data-act="edit-event" data-id="${esc(e.id)}" title="Edit">✎</button>
        <button class="btn tiny danger" data-act="del-event" data-id="${esc(e.id)}" title="Delete">✕</button>
      </div>
    </div>`;
  }).join('') : '<div class="hint">No events yet.</div>';
}

/* ---- legend / inspector / tooltip ---- */
function renderLegend(){
  const swatch = Object.keys(TYPES).map(k =>
    `<div class="row"><span class="sw sq" style="background:${TYPES[k].hex}"></span>${esc(TYPES[k].name)}</div>`).join('');
  const brs = state.branches.slice(0,6).map(b =>
    `<div class="row"><span class="sw" style="background:${colorOf(b.name)}"></span>${esc(b.name)}</div>`).join('');
  $('#legend').innerHTML =
    `<div class="lg-title">Branches</div>${brs || '<div class="row">—</div>'}
     <div class="lg-title" style="margin-top:6px">Event types</div>${swatch}`;
}

function renderInspector(){
  const box = $('#inspector');
  const c = selectedId ? commitById(selectedId) : null;
  if(!c){ box.classList.remove('on'); box.innerHTML = ''; return; }
  const evs = eventsFor(c.id);
  const tags = state.tags.filter(t => t.commit === c.id);
  const brs = state.branches.filter(b => b.head === c.id);
  box.classList.add('on');
  box.innerHTML = `
    <div class="ihead">
      <span class="dot" style="width:10px;height:10px;border-radius:50%;background:${colorOf(c.branch)}"></span>
      <span class="ihash">${esc(c.id)}</span>
      <span class="badge" style="color:${colorOf(c.branch)};border-color:${colorOf(c.branch)}">${esc(c.branch)}</span>
      <button class="iclose" data-act="close-inspector">✕</button>
    </div>
    <h4>${esc(c.message)}</h4>
    <div class="meta">${esc(c.author || 'dev')} · ${esc(dstr(c.ts))} · parents: ${(c.parents||[]).length ? (c.parents||[]).map(esc).join(', ') : '∅ (root)'}</div>
    <div>${brs.map(b => `<span class="chip">⌂ ${esc(b.name)}${b.name === state.head ? ' · HEAD' : ''}</span>`).join('')}
      ${tags.map(t => `<span class="chip" style="color:#f2cd66;border-color:#4a3c14">⌗ ${esc(t.name)}</span>`).join('')}
      <span class="chip" style="color:${evs.length ? '#bc8cff' : '#5d6f80'}">⚑ ${evs.length} event${evs.length === 1 ? '' : 's'}</span></div>
    ${evs.length ? `<div class="evlist">${evs.map(e => { const t = tOf(e.type);
      return `<div class="ev" style="--c:${t.hex}"><div><div class="et">${t.glyph} ${esc(e.title)}</div>
        <div class="es">${esc(t.name)} · ${esc(e.date || '')}${e.description ? ' — ' + esc(trunc(e.description, 70)) : ''}</div></div></div>`;
    }).join('')}</div>` : `<div class="hint" style="margin-top:8px">No contextual events anchored here.</div>`}
  `;
}

const tip = () => $('#tooltip');
function showTip(html, cx, cy){
  const el = tip(), holder = $('#graphHolder').getBoundingClientRect();
  el.innerHTML = html;
  el.classList.add('on');
  const w = el.offsetWidth, h = el.offsetHeight;
  let x = cx - holder.left + 16, y = cy - holder.top + 14;
  x = clamp(x, 8, holder.width - w - 8);
  y = clamp(y, 8, Math.max(8, holder.height - h - 8));
  el.style.left = x + 'px'; el.style.top = y + 'px';
}
function hideTip(){ tip().classList.remove('on'); }
function commitTip(c){
  return `<div class="k">Commit</div>
    <div class="tt-title">${esc(c.message)}</div>
    <div class="tt-line"><b>${esc(c.id)}</b><span>${esc(c.branch)}</span></div>
    <div class="tt-line"><span class="k" style="margin-right:4px">by</span>${esc(c.author || 'dev')} · ${esc(dstr(c.ts))}</div>
    <div class="tt-line"><span class="k" style="margin-right:4px">parents</span>${(c.parents||[]).length ? (c.parents||[]).map(esc).join(', ') : 'root'}</div>
    ${eventsFor(c.id).length ? `<div class="tt-sep"><div class="k">Anchored events</div>${eventsFor(c.id).map(e => { const t = tOf(e.type); return `<div class="tt-line" style="color:${t.hex}">${t.glyph} ${esc(e.title)}</div>`; }).join('')}</div>` : ''}
    <div class="tt-sep"><div class="k">click to reveal its contextual markers</div></div>`;
}
function eventTip(e){
  const t = tOf(e.type), c = commitById(e.commitId);
  return `<div class="tt-type" style="color:${t.hex}">${t.glyph} ${esc(t.name)}${e.branch ? ' · branch ' + esc(e.branch) : ''}</div>
    <div class="tt-title">${esc(e.title)}</div>
    ${e.description ? `<div class="tt-desc">${esc(e.description)}</div>` : ''}
    <div class="tt-sep">
      <div class="k">Anchored to commit</div>
      <div class="tt-line"><b>${esc(e.commitId)}</b><span>${esc(c ? trunc(c.message, 26) : 'missing')}</span></div>
      <div class="tt-line" style="margin-top:2px"><span class="k">branch</span><span>${esc(c ? c.branch : '—')}</span><span class="k">date</span><span>${esc(e.date || '—')}</span></div>
    </div>`;
}

/* ---- master render ---- */
function renderAll(){
  sortState();
  renderGraph();
  renderFlow();
  renderLists();
  renderLegend();
  renderInspector();
  refreshSelects();
  renderAnchorPreview();
}
function sortState(){
  state.branches = state.branches.filter(b => b && b.name);
  state.tags = state.tags.filter(t => t && t.name && commitById(t.commit));
  state.events = state.events.filter(e => commitById(e.commitId));
}
function selectCommit(id){
  selectedId = (selectedId === id) ? null : id;
  renderGraph(); renderFlow(); renderLists(); renderInspector();
}

/* ============================================================
   Forms, CRUD & wiring
   ============================================================ */
function flash(el){ if(!el) return; el.focus(); el.style.borderColor = '#f85149'; el.style.boxShadow = '0 0 0 3px #f8514933';
  setTimeout(() => { el.style.borderColor = ''; el.style.boxShadow = ''; }, 1100); }
function byTsDesc(a,b){ return (b.ts||0)-(a.ts||0); }

function fillSelect(el, items, keep){
  const prev = (keep !== undefined && keep !== null) ? keep : el.value;
  el.innerHTML = '';
  items.forEach(it => el.add(new Option(it.label, it.v)));
  if(prev && Array.from(el.options).some(o => o.value === prev)) el.value = prev;
}

function refreshSelects(){
  const commits = state.commits.slice().sort(byTsDesc);
  $('#branchDL').innerHTML = state.branches.map(b => `<option value="${esc(b.name)}">`).join('');
  fillSelect($('#bTarget'), commits.map(c => ({v:c.id, label:`${c.id} · ${trunc(c.message,30)}`})),
    editing.branch && branchByName(editing.branch) ? branchByName(editing.branch).head : undefined);
  fillSelect($('#tTarget'), commits.map(c => ({v:c.id, label:`${c.id} · ${trunc(c.message,30)}`})),
    editing.tag ? (state.tags.find(t => t.name === editing.tag) || {}).commit : undefined);

  const gsel = $('#eTargetGroup'), prev = gsel.value;
  gsel.innerHTML = '';
  const og1 = document.createElement('optgroup'); og1.label = 'Anchored to commit';
  commits.forEach(c => og1.appendChild(new Option(`${c.id} · ${trunc(c.message,34)}`, 'commit:' + c.id)));
  gsel.appendChild(og1);
  if(state.branches.length){
    const og2 = document.createElement('optgroup'); og2.label = 'Anchored to branch (follows its tip)';
    state.branches.forEach(b => og2.appendChild(new Option(`${b.name} → ${b.head || '∅'}`, 'branch:' + b.name)));
    gsel.appendChild(og2);
  }
  const evNow = editing.event ? state.events.find(e => e.id === editing.event) : null;
  const want = evNow ? 'commit:' + evNow.commitId : prev;
  if(want && Array.from(gsel.options).some(o => o.value === want)) gsel.value = want;
  const tsel = $('#eType');
  if(!tsel.options.length){
    Object.keys(TYPES).forEach(k => tsel.add(new Option(TYPES[k].glyph + '  ' + TYPES[k].name, k)));
    tsel.value = 'milestone';
  }
}

function renderParentPicker(){
  const editingId = editing.commit;
  const banned = editingId ? new Set([editingId, ...descendants(editingId)]) : new Set();
  const cur = editingId ? (commitById(editingId) || {parents:[]}).parents : [];
  const checked = new Set(cur);
  const list = state.commits.slice().sort(byTsDesc).filter(c => !banned.has(c.id));
  $('#cParents').innerHTML = list.length ? list.map(c => `
    <label class="pick"><input type="checkbox" value="${esc(c.id)}" ${checked.has(c.id) ? 'checked' : ''}>
      <span class="pid">${esc(c.id)}</span>
      <span class="dot" style="width:8px;height:8px;border-radius:50%;background:${colorOf(c.branch)}"></span>
      <span class="pmsg">${esc(trunc(c.message, 34))}</span></label>`).join('')
    : '<div class="hint" style="padding:5px">No other commits available.</div>';
}

function renderAnchorPreview(){
  const el = $('#eAnchorPreview'); if(!el) return;
  const v = $('#eTargetGroup').value || '';
  let c = null, via = '';
  if(v.startsWith('commit:')) c = commitById(v.slice(7));
  else if(v.startsWith('branch:')){ const b = branchByName(v.slice(7)); c = b ? commitById(b.head) : null; via = ' via branch ' + v.slice(7); }
  el.innerHTML = c
    ? `🔗 Anchored to commit <b style="color:#7ee787">${esc(c.id)}</b>${esc(via)} — “${esc(trunc(c.message, 44))}” on <b style="color:${colorOf(c.branch)}">${esc(c.branch)}</b>`
    : 'Select a commit or branch to anchor this event.';
}

/* ---- commit form ---- */
function resetCommitForm(){
  editing.commit = null;
  $('#cFormTitle').textContent = 'New commit';
  $('#cSave').textContent = 'Add commit';
  $('#cCancel').classList.add('hidden');
  $('#cMsg').value = '';
  $('#cDate').value = dstr(Date.now());
  $('#cParents').querySelectorAll('input').forEach(i => i.checked = false);
  renderParentPicker();
}
function startEditCommit(id){
  const c = commitById(id); if(!c) return;
  editing.commit = id;
  $('#cFormTitle').textContent = 'Edit commit ' + id;
  $('#cSave').textContent = 'Save changes';
  $('#cCancel').classList.remove('hidden');
  $('#cMsg').value = c.message;
  $('#cAuthor').value = c.author || 'dev';
  $('#cDate').value = dstr(c.ts);
  $('#cBranch').value = c.branch || 'main';
  renderParentPicker();
  switchTab('commits');
  $('#cMsg').focus();
}
function saveCommit(){
  const msg = $('#cMsg').value.trim();
  if(!msg){ flash($('#cMsg')); return; }
  const parents = $$('#cParents input:checked').map(i => i.value);
  const branch = $('#cBranch').value.trim() || 'main';
  const author = $('#cAuthor').value.trim() || 'dev';
  const ts = tstr($('#cDate').value) || Date.now();
  if(editing.commit){
    const c = commitById(editing.commit);
    if(!c){ flash($('#cMsg')); return; }
    Object.assign(c, { message:msg, parents, branch, author, ts });
    const b = branchByName(branch);
    if(!b) state.branches.push({ name:branch, head:c.id });
  } else {
    let id; do { id = rnd(7); } while(commitById(id));
    state.commits.push({ id, message:msg, parents, branch, author, ts });
    if(!state.branches.some(b => b.name === branch)) state.branches.push({ name:branch, head:id });
    else { const b = branchByName(branch); if(b) b.head = id; }
    selectedId = id;
  }
  resetCommitForm(); save(); renderAll();
}
function deleteCommit(id){
  const c = commitById(id); if(!c) return;
  const kids = childrenOf(id), evs = eventsFor(id).length;
  const reparent = (c.parents || []).join(', ') || 'root';
  if(!confirm(`Delete commit ${id}?\n\n• ${kids.length} child commit(s) re-parented onto [${reparent}]\n• ${evs} anchored event(s) removed\n• branch pointers and tags on this commit are relinked`)) return;
  state.commits = state.commits.filter(x => x.id !== id);
  state.commits.forEach(x => {
    const i = x.parents.indexOf(id);
    if(i >= 0){
      x.parents.splice(i, 1);
      (c.parents || []).forEach(p => { if(p !== x.id && !x.parents.includes(p)) x.parents.push(p); });
    }
  });
  state.events = state.events.filter(e => e.commitId !== id);
  state.tags = state.tags.filter(t => t.commit !== id);
  const fallback = (c.parents || [])[0] || (state.commits[0] ? state.commits[0].id : null);
  state.branches.forEach(b => { if(b.head === id) b.head = fallback; });
  state.branches = state.branches.filter(b => b.head);
  if(selectedId === id) selectedId = null;
  save(); renderAll();
}

/* ---- branch form ---- */
function resetBranchForm(){
  editing.branch = null;
  $('#bFormTitle').textContent = 'New branch';
  $('#bSave').textContent = 'Add branch';
  $('#bCancel').classList.add('hidden');
  $('#bName').value = '';
}
function startEditBranch(name){
  const b = branchByName(name); if(!b) return;
  editing.branch = name;
  $('#bFormTitle').textContent = 'Edit branch ' + name;
  $('#bSave').textContent = 'Save changes';
  $('#bCancel').classList.remove('hidden');
  $('#bName').value = name;
  refreshSelects();
  if(b.head) $('#bTarget').value = b.head;
  switchTab('branches');
  $('#bName').focus();
}
function saveBranch(){
  const name = $('#bName').value.trim();
  if(!name){ flash($('#bName')); return; }
  const head = $('#bTarget').value || (state.commits[0] ? state.commits[0].id : null);
  if(editing.branch){
    const b = branchByName(editing.branch);
    if(!b){ editing.branch = null; return; }
    if(name !== editing.branch){
      if(branchByName(name)){ alert('A branch named “' + name + '” already exists.'); return; }
      const old = editing.branch;
      b.name = name;
      state.commits.forEach(c => { if(c.branch === old) c.branch = name; });
      state.events.forEach(e => { if(e.branch === old) e.branch = name; });
      if(state.head === old) state.head = name;
    }
    b.head = head;
  } else {
    if(branchByName(name)){ alert('A branch named “' + name + '” already exists.'); return; }
    state.branches.push({ name, head });
    if(head) selectedId = head;
  }
  resetBranchForm(); save(); renderAll();
}
function deleteBranch(name){
  if(!confirm('Delete branch “' + name + '”?\nThe commits themselves are kept.')) return;
  state.branches = state.branches.filter(b => b.name !== name);
  state.events.forEach(e => { if(e.branch === name) e.branch = null; });
  if(state.head === name) state.head = state.branches[0] ? state.branches[0].name : 'main';
  save(); renderAll();
}

/* ---- tag form ---- */
function resetTagForm(){
  editing.tag = null;
  $('#tFormTitle').textContent = 'New tag';
  $('#tSave').textContent = 'Add tag';
  $('#tCancel').classList.add('hidden');
  $('#tName').value = '';
}
function startEditTag(name){
  const t = state.tags.find(x => x.name === name); if(!t) return;
  editing.tag = name;
  $('#tFormTitle').textContent = 'Edit tag ' + name;
  $('#tSave').textContent = 'Save changes';
  $('#tCancel').classList.remove('hidden');
  $('#tName').value = name;
  refreshSelects();
  $('#tTarget').value = t.commit;
  switchTab('tags');
  $('#tName').focus();
}
function saveTag(){
  const name = $('#tName').value.trim();
  if(!name){ flash($('#tName')); return; }
  const commit = $('#tTarget').value;
  if(!commit){ alert('Add a commit first.'); return; }
  if(editing.tag){
    const t = state.tags.find(x => x.name === editing.tag);
    if(t){ t.name = name; t.commit = commit; }
  } else {
    if(state.tags.some(t => t.name === name)){ alert('Tag “' + name + '” already exists.'); return; }
    state.tags.push({ name, commit });
  }
  resetTagForm(); save(); renderAll();
}
function deleteTag(name){
  if(!confirm('Delete tag “' + name + '”?')) return;
  state.tags = state.tags.filter(t => t.name !== name);
  save(); renderAll();
}

/* ---- event form ---- */
function resetEventForm(){
  editing.event = null;
  $('#eFormTitle').textContent = 'New contextual event';
  $('#eSave').textContent = 'Add event';
  $('#eCancel').classList.add('hidden');
  $('#eTitle').value = '';
  $('#eDesc').value = '';
  $('#eDate').value = dstr(Date.now());
  renderAnchorPreview();
}
function startEditEvent(id){
  const e = state.events.find(x => x.id === id); if(!e) return;
  editing.event = id;
  $('#eFormTitle').textContent = 'Edit event';
  $('#eSave').textContent = 'Save changes';
  $('#eCancel').classList.remove('hidden');
  $('#eTitle').value = e.title;
  $('#eType').value = e.type;
  $('#eDesc').value = e.description || '';
  $('#eDate').value = e.date || dstr(Date.now());
  refreshSelects();
  $('#eTargetGroup').value = 'commit:' + e.commitId;
  renderAnchorPreview();
  switchTab('events');
  $('#eTitle').focus();
}
function saveEvent(){
  const title = $('#eTitle').value.trim();
  if(!title){ flash($('#eTitle')); return; }
  const v = $('#eTargetGroup').value || '';
  let commitId = null, branchName = null;
  if(v.startsWith('commit:')) commitId = v.slice(7);
  else if(v.startsWith('branch:')){ branchName = v.slice(7); const b = branchByName(branchName); commitId = b ? b.head : null; }
  if(!commitId || !commitById(commitId)){ alert('Choose the commit (or branch) this event is anchored to.'); return; }
  const payload = {
    title, type: $('#eType').value, date: $('#eDate').value || dstr(Date.now()),
    description: $('#eDesc').value.trim(), commitId, branch: branchName
  };
  if(editing.event){
    const e = state.events.find(x => x.id === editing.event);
    if(e) Object.assign(e, payload);
  } else {
    state.events.push(Object.assign({ id: uid('ev_') }, payload));
  }
  selectedId = commitId;
  resetEventForm(); save(); renderAll();
}
function deleteEvent(id){
  const e = state.events.find(x => x.id === id); if(!e) return;
  if(!confirm('Delete event “' + e.title + '”?')) return;
  state.events = state.events.filter(x => x.id !== id);
  save(); renderAll();
}

/* ---- tabs ---- */
function switchTab(name){
  $$('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
  $$('.panel').forEach(p => p.classList.toggle('active', p.dataset.panel === name));
}

/* ---- modal ---- */
function closeModal(){ const o = $('#overlay'); if(o) o.remove(); }
function modal(title, bodyHTML, buttons){
  closeModal();
  const ov = document.createElement('div');
  ov.className = 'overlay'; ov.id = 'overlay';
  ov.innerHTML = `<div class="modal"><h3>${title}</h3><div class="mbody">${bodyHTML}</div><div class="mact"></div></div>`;
  document.body.appendChild(ov);
  const act = ov.querySelector('.mact');
  buttons.forEach(b => {
    const el = document.createElement('button');
    el.className = 'btn ' + (b.cls || '');
    el.textContent = b.label;
    el.onclick = () => b.onClick(ov);
    act.appendChild(el);
  });
  ov.addEventListener('mousedown', e => { if(e.target === ov) closeModal(); });
  return ov;
}
function download(name, text){
  const blob = new Blob([text], { type:'application/json' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob); a.download = name;
  document.body.appendChild(a); a.click();
  setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 400);
}
function importState(text){
  let parsed;
  try { parsed = JSON.parse(text); }
  catch(err){ alert('That is not valid JSON.\n\n' + err.message); return false; }
  if(Array.isArray(parsed)) parsed = { commits: parsed, events: parsed.events || [] };
  const next = normalize(parsed);
  if(!next.commits.length && !next.events.length){ alert('No commits or events found in that data.'); return false; }
  if(!next.branches.length && next.commits.length){
    const newest = next.commits.slice().sort(byTsDesc)[0];
    next.branches.push({ name:'main', head:newest.id });
  }
  next.branches.forEach(b => { if(!b.head) b.head = next.commits[0].id; });
  state = next;
  selectedId = null;
  lastCommitCount = -1;
  save(); renderAll(); closeModal();
  return true;
}
function exportModal(){
  modal('Export state — JSON', `
    <div class="mnote">The complete unified state (commits, DAG parents, branches, tags and anchored events). Paste it back through <b>Import</b> to restore this exact view.</div>
    <textarea class="mtext" id="jsonOut" spellcheck="false"></textarea>`,
    [
      { label:'Copy', onClick: () => { const t = $('#jsonOut'); t.select(); try{ document.execCommand('copy'); }catch(e){} } },
      { label:'Download .json', cls:'primary', onClick: () => download('gitcontext-' + new Date().toISOString().slice(0,10) + '.json', $('#jsonOut').value) },
      { label:'Close', cls:'ghost', onClick: closeModal }
    ]);
  $('#jsonOut').value = JSON.stringify(state, null, 2);
}
function importModal(){
  modal('Import commits & contextual events', `
    <div class="mnote">Paste JSON exported from here (or any object shaped like
      <code>{ commits:[{ id, message, parents[], branch, ts }], branches:[…], tags:[…], events:[{ id, title, type, commitId, description, date }] }</code>).</div>
    <input type="file" id="jsonFile" accept=".json,application/json">
    <textarea class="mtext" id="jsonIn" spellcheck="false" placeholder='{"commits":[ … ],"events":[ … ]}'></textarea>`,
    [
      { label:'Cancel', cls:'ghost', onClick: closeModal },
      { label:'Load & replace state', cls:'primary', onClick: () => { const t = $('#jsonIn').value.trim(); if(!t){ alert('Paste some JSON first.'); return; } importState(t); } }
    ]);
  $('#jsonFile').addEventListener('change', ev => {
    const f = ev.target.files && ev.target.files[0]; if(!f) return;
    const r = new FileReader();
    r.onload = () => { $('#jsonIn').value = r.result; };
    r.readAsText(f);
  });
}

/* ---- interaction wiring ---- */
const DENSITY = [ {row:62, lane:132}, {row:84, lane:158}, {row:46, lane:112} ];
let densityIdx = 0;

function wireUI(){
  /* tabs */
  $$('.tab').forEach(t => t.addEventListener('click', () => {
    switchTab(t.dataset.tab);
    document.querySelector('.sidebar').classList.remove('open');
  }));
  $('#btnMenu').addEventListener('click', () => document.querySelector('.sidebar').classList.toggle('open'));

  /* buttons: commit */
  $('#cSave').addEventListener('click', saveCommit);
  $('#cCancel').addEventListener('click', () => { resetCommitForm(); });
  $('#cMsg').addEventListener('keydown', e => { if(e.key === 'Enter') saveCommit(); });
  /* branch */
  $('#bSave').addEventListener('click', saveBranch);
  $('#bCancel').addEventListener('click', resetBranchForm);
  $('#bName').addEventListener('keydown', e => { if(e.key === 'Enter') saveBranch(); });
  /* tag */
  $('#tSave').addEventListener('click', saveTag);
  $('#tCancel').addEventListener('click', resetTagForm);
  $('#tName').addEventListener('keydown', e => { if(e.key === 'Enter') saveTag(); });
  /* event */
  $('#eSave').addEventListener('click', saveEvent);
  $('#eCancel').addEventListener('click', resetEventForm);
  $('#eTargetGroup').addEventListener('change', renderAnchorPreview);

  /* list delegation */
  $('.panels').addEventListener('click', ev => {
    const btn = ev.target.closest('button[data-act]');
    if(btn){
      ev.stopPropagation();
      const a = btn.dataset.act;
      if(a === 'edit-commit') startEditCommit(btn.dataset.id);
      else if(a === 'del-commit') deleteCommit(btn.dataset.id);
      else if(a === 'edit-branch') startEditBranch(btn.dataset.name);
      else if(a === 'del-branch') deleteBranch(btn.dataset.name);
      else if(a === 'head-branch'){ state.head = btn.dataset.name; save(); renderAll(); }
      else if(a === 'edit-tag') startEditTag(btn.dataset.name);
      else if(a === 'del-tag') deleteTag(btn.dataset.name);
      else if(a === 'edit-event') startEditEvent(btn.dataset.id);
      else if(a === 'del-event') deleteEvent(btn.dataset.id);
      return;
    }
    const item = ev.target.closest('.item');
    if(item){
      if(item.dataset.commit) setSelection(item.dataset.commit);
      else if(item.dataset.ev){ const e = state.events.find(x => x.id === item.dataset.ev); if(e) setSelection(e.commitId); }
      else if(item.dataset.branch){ const b = branchByName(item.dataset.branch); if(b && b.head) setSelection(b.head); }
    }
  });

  /* graph interaction */
  let downAt = null;
  $('#graph').addEventListener('mousedown', e => { downAt = { x:e.clientX, y:e.clientY }; });
  $('#graph').addEventListener('click', e => {
    if(downAt && (Math.abs(e.clientX - downAt.x) + Math.abs(e.clientY - downAt.y)) > 4) return;
    const m = e.target.closest('.evmark');
    if(m){
      const ev = state.events.find(x => x.id === m.dataset.ev);
      setSelection(m.dataset.commit);
      if(ev) showTip(eventTip(ev), e.clientX, e.clientY);
      return;
    }
    const n = e.target.closest('.gnode');
    if(n){ selectCommit(n.dataset.id); return; }
    setSelection(null);
  });
  $('#graph').addEventListener('mousemove', e => {
    const m = e.target.closest('.evmark');
    if(m){ const ev = state.events.find(x => x.id === m.dataset.ev); if(ev){ showTip(eventTip(ev), e.clientX, e.clientY); return; } }
    const n = e.target.closest('.gnode');
    if(n){ const c = commitById(n.dataset.id); if(c){ showTip(commitTip(c), e.clientX, e.clientY); return; } }
    hideTip();
  });
  $('#graph').addEventListener('mouseleave', hideTip);

  /* flow interaction */
  $('#flowInner').addEventListener('click', e => {
    const it = e.target.closest('.flow-item'); if(!it) return;
    setSelection(it.dataset.commit);
  });
  $('#flowInner').addEventListener('mouseover', e => {
    const it = e.target.closest('.flow-item'); if(!it) return;
    const ev = state.events.find(x => x.id === it.dataset.ev);
    if(ev){ const r = it.getBoundingClientRect(); showTip(eventTip(ev), r.left + r.width/2, r.top); }
  });
  $('#flowInner').addEventListener('mouseout', e => { if(e.target.closest('.flow-item')) hideTip(); });

  /* inspector */
  $('#inspector').addEventListener('click', e => {
    if(e.target.closest('[data-act=close-inspector]')) setSelection(null);
  });

  /* toolbar */
  $('#btnExport').addEventListener('click', exportModal);
  $('#btnImport').addEventListener('click', importModal);
  $('#btnFit').addEventListener('click', () => fitView(320));
  $('#btnDagre').addEventListener('click', () => {
    densityIdx = (densityIdx + 1) % DENSITY.length;
    ROW_GAP = DENSITY[densityIdx].row; LANE_GAP = DENSITY[densityIdx].lane;
    renderAll(); fitView(340);
  });
  $('#btnSample').addEventListener('click', () => {
    if(state.commits.length && !confirm('Replace the current state with the sample repository?')) return;
    state = sampleState(); selectedId = null; lastCommitCount = -1; save(); renderAll();
  });
  $('#btnClear').addEventListener('click', () => {
    if(!confirm('Delete all commits, branches, tags and events?')) return;
    state = freshState(); selectedId = null; lastCommitCount = -1; save(); renderAll();
  });

  document.addEventListener('keydown', e => {
    if(e.key === 'Escape'){ if($('#overlay')) closeModal(); else setSelection(null); }
  });
}

function setSelection(id){
  selectedId = id;
  renderGraph(); renderFlow(); renderLists(); renderInspector(); renderLegend();
  if(id) ensureVisible(id, 380);
}

/* ---- boot ---- */
(function init(){
  initGraph();
  const saved = load();
  state = saved || sampleState();
  if(!saved) save();
  if(!state.branches.length && state.commits.length){
    state.branches.push({ name:'main', head: state.commits.slice().sort(byTsDesc)[0].id });
  }
  if(!state.head || !branchByName(state.head)) state.head = state.branches[0] ? state.branches[0].name : 'main';
  resetCommitForm(); resetBranchForm(); resetTagForm(); resetEventForm();
  wireUI();
  renderAll();
  window.addEventListener('beforeunload', save);
  window.GitContext = {
    get state(){ return state; },
    setSelection, selectCommit, renderAll, importState, fitView,
    loadSample(){ state = sampleState(); selectedId = null; lastCommitCount = -1; save(); renderAll(); },
    clearAll(){ state = freshState(); selectedId = null; lastCommitCount = -1; save(); renderAll(); },
    layout: () => layout, save
  };
})();



</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"94434600-d56c-4a4b-a53d-3f1307a952fb","tokensIn":2754092,"tokensOut":62092,"tokensTotal":2816184,"cost":0.05899620000000001,"turns":32,"toolCalls":42,"failedToolCalls":0,"timestamp":"2026-09-10T23:40:43.429Z"} -->