← Git Explorer results

Git Explorer

muse spark 1.2 contributor · full

Initial view of Git Explorer

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 56,515 bytes · SHA-256 e4a1f2ebb76e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Git & Structure Visualizer — DAG + Context</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
  --bg:#0a0e14;
  --panel:#111827;
  --panel2:#1a2234;
  --panel3:#1e2a44;
  --border:#243044;
  --border2:#2d3d56;
  --text:#e2e8f0;
  --muted:#94a3b8;
  --dim:#64748b;
  --accent:#38bdf8;
  --accent2:#818cf8;
  --green:#22c55e;
  --yellow:#eab308;
  --orange:#f97316;
  --pink:#ec4899;
  --red:#ef4444;
  --violet:#8b5cf6;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);overflow:hidden;height:100vh;display:flex;flex-direction:column}
::-webkit-scrollbar{width:6px;height:6px}
::-webkit-scrollbar-thumb{background:#2d3d56;border-radius:99px}
::-webkit-scrollbar-track{background:transparent}

/* Header */
header{
  height:56px;background:rgba(17,24,39,0.9);backdrop-filter:blur(12px);border-bottom:1px solid var(--border);
  display:flex;align-items:center;justify-content:space-between;padding:0 16px;gap:12px;flex-shrink:0;z-index:10
}
.brand{display:flex;align-items:center;gap:12px}
.logo{width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#38bdf8,#818cf8);display:grid;place-items:center;font-size:18px}
.brand h1{font-size:15px;font-weight:800;letter-spacing:-0.02em;line-height:1}
.brand p{font-size:11px;color:var(--muted);font-family:'JetBrains Mono',monospace}
.header-actions{display:flex;align-items:center;gap:8px}
.btn{font-family:'Inter',sans-serif;font-size:12.5px;font-weight:600;padding:7px 12px;border-radius:8px;border:1px solid var(--border);background:var(--panel2);color:var(--text);cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:.15s}
.btn:hover{background:var(--panel3);border-color:var(--border2);transform:translateY(-1px)}
.btn.primary{background:linear-gradient(135deg,#0ea5e9,#6366f1);border-color:transparent;color:white}
.btn.primary:hover{filter:brightness(1.08)}
.btn.ghost{background:transparent}
.btn.small{padding:5px 9px;font-size:11.5px}
.btn.icon{width:32px;height:32px;padding:0;justify-content:center}
.badge{font-size:10px;font-weight:700;padding:2px 6px;border-radius:99px;background:var(--panel3);border:1px solid var(--border);color:var(--muted);font-family:'JetBrains Mono',monospace}

/* Layout */
.app{flex:1;display:grid;grid-template-columns:300px 1fr 320px;overflow:hidden}
@media(max-width:1100px){.app{grid-template-columns:280px 1fr} .right{display:none}}
@media(max-width:720px){.app{grid-template-columns:1fr} .left{display:none} header{height:auto;padding:10px;flex-wrap:wrap}}

/* Panels */
.left,.right{background:var(--panel);border-right:1px solid var(--border);display:flex;flex-direction:column;overflow:hidden}
.right{border-right:none;border-left:1px solid var(--border)}
.panel-head{padding:14px 14px 10px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);flex-shrink:0}
.panel-head h2{font-size:11px;font-weight:800;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted)}
.panel-body{flex:1;overflow:auto;padding:12px;display:flex;flex-direction:column;gap:14px}

/* Cards */
.card{background:var(--panel2);border:1px solid var(--border);border-radius:12px;overflow:hidden}
.card-head{padding:10px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);background:rgba(255,255,255,0.02)}
.card-head h3{font-size:12px;font-weight:700}
.card-body{padding:12px}

/* Branch list */
.branch-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;border:1px solid transparent;cursor:pointer;transition:.15s}
.branch-item:hover{background:var(--panel3);border-color:var(--border)}
.branch-item.active{background:rgba(56,189,248,0.08);border-color:rgba(56,189,248,0.3)}
.branch-dot{width:10px;height:10px;border-radius:50%;flex-shrink:0;box-shadow:0 0 0 3px rgba(255,255,255,0.06)}
.branch-meta{flex:1;min-width:0}
.branch-name{font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.branch-head{font-size:11px;color:var(--muted);font-family:'JetBrains Mono',monospace}
.branch-actions{display:flex;gap:4px}

/* Forms */
.form-grid{display:grid;gap:8px}
label{font-size:11px;font-weight:600;color:var(--muted);letter-spacing:0.04em;text-transform:uppercase}
input,select,textarea{width:100%;background:#0f172a;border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 10px;font-size:13px;font-family:'Inter',sans-serif;outline:none;transition:.15s}
input:focus,select:focus,textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(56,189,248,0.15)}
textarea{resize:vertical;min-height:60px}
.row{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.hint{font-size:11px;color:var(--dim);line-height:1.4}

/* Center */
.center{display:flex;flex-direction:column;overflow:hidden;background:radial-gradient(1200px 600px at 50% -10%, rgba(56,189,248,0.06), transparent 60%), radial-gradient(800px 400px at 90% 90%, rgba(129,140,248,0.07), transparent 60%), var(--bg)}
.graph-toolbar{height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;border-bottom:1px solid var(--border);background:rgba(17,24,39,0.6);backdrop-filter:blur(8px);flex-shrink:0;gap:8px}
.toolbar-left,.toolbar-right{display:flex;align-items:center;gap:8px}
.legend{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--muted)}
.legend i{width:10px;height:10px;border-radius:50%;display:inline-block}
.graph-wrap{flex:1;position:relative;overflow:auto;background:var(--bg);scrollbar-width:thin;min-height:0;padding:12px 12px 120px 4px;box-sizing:border-box}
#gitGraph{display:block;overflow:visible}
.graph-overlay{position:absolute;top:10px;left:10px;display:flex;gap:6px;z-index:2;pointer-events:none}
.mini-stat{background:rgba(15,23,42,0.85);backdrop-filter:blur(8px);border:1px solid var(--border);border-radius:8px;padding:6px 10px;font-size:11px;font-family:'JetBrains Mono',monospace;color:var(--muted)}
.mini-stat b{color:var(--text)}

/* Tooltip */
.tooltip{position:fixed;pointer-events:none;z-index:50;max-width:320px;background:#0f172a;border:1px solid var(--border2);border-radius:12px;padding:12px;box-shadow:0 20px 40px rgba(0,0,0,0.5);opacity:0;transform:translateY(4px);transition:.15s}
.tooltip.show{opacity:1;transform:translateY(0)}
.tooltip h4{font-size:13px;font-weight:700;margin-bottom:4px}
.tooltip p{font-size:12px;color:var(--muted);line-height:1.5}
.tooltip .meta{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--dim);margin-top:6px}

/* Contextual Flow */
.flow{height:170px;border-top:1px solid var(--border);background:rgba(17,24,39,0.7);backdrop-filter:blur(8px);flex-shrink:0;display:flex;flex-direction:column}
.flow-head{height:36px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;border-bottom:1px solid var(--border);flex-shrink:0}
.flow-head h3{font-size:11px;font-weight:800;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted)}
.flow-track{flex:1;overflow:auto;padding:10px 12px;display:flex;gap:10px;align-items:stretch}
.event-chip{flex-shrink:0;width:220px;background:var(--panel2);border:1px solid var(--border);border-radius:12px;padding:10px;display:flex;flex-direction:column;gap:8px;cursor:pointer;transition:.15s;position:relative}
.event-chip:hover{border-color:var(--border2);transform:translateY(-2px);box-shadow:0 8px 20px rgba(0,0,0,0.3)}
.event-chip.active{border-color:var(--accent);box-shadow:0 0 0 3px rgba(56,189,248,0.15), 0 8px 20px rgba(0,0,0,0.3)}
.event-chip.anchored::before{content:'';position:absolute;top:-1px;left:12px;right:12px;height:3px;border-radius:0 0 6px 6px}
.event-top{display:flex;align-items:center;gap:8px}
.event-icon{width:28px;height:28px;border-radius:8px;display:grid;place-items:center;font-size:14px;flex-shrink:0}
.event-title{font-size:12.5px;font-weight:700;line-height:1.2}
.event-commit{font-family:'JetBrains Mono',monospace;font-size:10.5px;color:var(--muted);display:flex;align-items:center;gap:4px}
.event-desc{font-size:11.5px;color:var(--muted);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.event-tag{font-size:10px;font-weight:700;padding:2px 6px;border-radius:99px;border:1px solid var(--border);align-self:flex-start}

/* Right details */
.detail-empty{flex:1;display:grid;place-items:center;padding:20px;text-align:center;color:var(--muted)}
.detail-empty div{max-width:240px}
.commit-detail{padding:14px;display:flex;flex-direction:column;gap:12px}
.commit-hash{font-family:'JetBrains Mono',monospace;font-size:12px;background:#0f172a;border:1px solid var(--border);padding:6px 8px;border-radius:8px;display:flex;align-items:center;justify-content:space-between}
.commit-msg{font-size:14px;font-weight:700;line-height:1.4}
.commit-meta{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.meta-box{background:var(--panel2);border:1px solid var(--border);border-radius:8px;padding:8px}
.meta-box span{font-size:10px;color:var(--muted);text-transform:uppercase;letter-spacing:0.06em;font-weight:700}
.meta-box p{font-size:12px;font-weight:600;margin-top:2px;font-family:'JetBrains Mono',monospace}
.anchored-list{display:flex;flex-direction:column;gap:8px}
.anchored-item{background:var(--panel2);border:1px solid var(--border);border-radius:10px;padding:10px;display:flex;gap:10px;align-items:flex-start}
.anchored-item .icon{width:32px;height:32px;border-radius:8px;display:grid;place-items:center;flex-shrink:0}

/* D3 styles */
.link{fill:none;stroke-width:2.5;opacity:0.9}
.link.merge{stroke-dasharray:0}
.commit-node{cursor:pointer;transition:.15s}
.commit-node:hover{filter:brightness(1.2)}
.commit-node.selected circle{stroke:white;stroke-width:2.5;filter:drop-shadow(0 0 8px rgba(255,255,255,0.5))}
.commit-label{font-family:'JetBrains Mono',monospace;font-size:10px;fill:var(--muted);pointer-events:none}
.commit-label-bg{fill:rgba(10,14,20,0.85);stroke:var(--border);stroke-width:0.5;rx:4}
.event-anchor{cursor:pointer}
.event-anchor:hover{filter:brightness(1.15)}
.event-connector{stroke-width:1.5;stroke-dasharray:4 4;opacity:0.6}

/* Modal */
.modal-backdrop{position:fixed;inset:0;background:rgba(2,6,23,0.7);backdrop-filter:blur(6px);display:none;place-items:center;z-index:40;padding:16px}
.modal-backdrop.open{display:grid}
.modal{background:var(--panel);border:1px solid var(--border2);border-radius:16px;width:min(520px,100%);max-height:90vh;overflow:auto;box-shadow:0 24px 60px rgba(0,0,0,0.6)}
.modal-head{padding:16px 16px 0;display:flex;align-items:center;justify-content:space-between}
.modal-head h3{font-size:15px;font-weight:800}
.modal-body{padding:16px;display:grid;gap:12px}
.modal-foot{padding:12px 16px 16px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);margin-top:4px}

/* Empty */
.empty{padding:16px;text-align:center;color:var(--muted);font-size:12px}

/* Tag pill */
.tag-pill{font-size:10px;font-weight:700;padding:2px 6px;border-radius:99px;background:rgba(234,179,8,0.15);border:1px solid rgba(234,179,8,0.3);color:#facc15;font-family:'JetBrains Mono',monospace}
</style>
</head>
<body>
<header>
  <div class="brand">
    <div class="logo">◈</div>
    <div>
      <h1>Git Structure Visualizer</h1>
      <p>DAG + Contextual Anchors • D3.js</p>
    </div>
    <span class="badge" id="statsBadge">0 commits • 0 events</span>
  </div>
  <div class="header-actions">
    <button class="btn ghost small" onclick="importData()">⤒ Import</button>
    <button class="btn ghost small" onclick="exportData()">⤓ Export</button>
    <button class="btn ghost small" onclick="resetData()">↺ Reset</button>
    <button class="btn primary small" onclick="openModal('commit')">+ New Commit</button>
    <input type="file" id="fileInput" accept=".json" style="display:none">
  </div>
</header>

<div class="app">
  <!-- LEFT -->
  <aside class="left">
    <div class="panel-head">
      <h2>Workspace</h2>
      <button class="btn small ghost" onclick="openModal('branch')" title="New branch">+ Branch</button>
    </div>
    <div class="panel-body">
      <!-- Branches -->
      <div class="card">
        <div class="card-head">
          <h3>⎇ Branches</h3>
          <span class="badge" id="branchCount">0</span>
        </div>
        <div class="card-body" style="padding:8px">
          <div id="branchList"></div>
        </div>
      </div>

      <!-- Quick commit -->
      <div class="card">
        <div class="card-head"><h3>◉ Create Commit</h3></div>
        <div class="card-body">
          <div class="form-grid">
            <label>Message</label>
            <input id="quickMsg" placeholder="feat: add authentication">
            <div class="row">
              <div><label>Branch</label><select id="quickBranch"></select></div>
              <div><label>Parent(s)</label><select id="quickParent"></select></div>
            </div>
            <button class="btn primary" onclick="quickCommit()">Commit to branch</button>
            <p class="hint">Creates a new commit on the selected branch. For merges, use “New Commit” and pick two parents.</p>
          </div>
        </div>
      </div>

      <!-- Events -->
      <div class="card">
        <div class="card-head">
          <h3>◆ Contextual Events</h3>
          <button class="btn small" onclick="openModal('event')">+ Add</button>
        </div>
        <div class="card-body" style="padding:0">
          <div id="eventList" style="max-height:260px;overflow:auto"></div>
        </div>
      </div>

      <!-- Tags -->
      <div class="card">
        <div class="card-head"><h3>🏷 Tags</h3><button class="btn small ghost" onclick="openModal('tag')">+ Tag</button></div>
        <div class="card-body" id="tagList" style="padding:8px"></div>
      </div>
    </div>
  </aside>

  <!-- CENTER -->
  <main class="center">
    <div class="graph-toolbar">
      <div class="toolbar-left">
        <div class="legend">
          <span><i style="background:#38bdf8"></i> main</span>
          <span><i style="background:#22c55e"></i> feature</span>
          <span><i style="background:#f97316"></i> hotfix</span>
          <span><i style="background:#a78bfa"></i> release</span>
          <span style="margin-left:8px;color:var(--dim)">— parent &nbsp; ⇢ merge</span>
        </div>
      </div>
      <div class="toolbar-right">
        <button class="btn small ghost" onclick="zoomFit()">⊡ Fit</button>
        <button class="btn small ghost" onclick="toggleLabels()" id="labelBtn">Labels: On</button>
        <button class="btn small" onclick="openModal('event')">◆ Anchor Event</button>
      </div>
    </div>

    <div class="graph-wrap" id="graphWrap">
      <div class="graph-overlay">
        <div class="mini-stat"><b id="commitCount">0</b> commits</div>
        <div class="mini-stat"><b id="eventCount">0</b> anchored</div>
        <div class="mini-stat" id="selectedInfo" style="display:none"></div>
      </div>
      <svg id="gitGraph"></svg>
    </div>

    <div class="flow">
      <div class="flow-head">
        <h3>Contextual Flow — Events anchored to commits</h3>
        <span class="badge" id="flowBadge">drag to scroll • click to focus</span>
      </div>
      <div class="flow-track" id="flowTrack"></div>
    </div>
  </main>

  <!-- RIGHT -->
  <aside class="right">
    <div class="panel-head">
      <h2>Inspector</h2>
      <span class="badge" id="inspectorBadge">select a node</span>
    </div>
    <div id="inspector" style="flex:1;overflow:auto;display:flex;flex-direction:column">
      <div class="detail-empty" id="emptyState">
        <div>
          <div style="font-size:32px;margin-bottom:8px">◈</div>
          <p style="font-weight:700;color:var(--text)">No selection</p>
          <p style="font-size:12px;margin-top:6px;line-height:1.5">Click any commit node on the graph. Its anchored contextual markers will highlight in the flow below and details appear here.</p>
          <div style="margin-top:12px;display:flex;gap:6px;justify-content:center;flex-wrap:wrap">
            <span class="badge">hover anchor → tooltip</span>
            <span class="badge">click chip → focus</span>
          </div>
        </div>
      </div>
      <div id="detailView" style="display:none"></div>
    </div>
  </aside>
</div>

<!-- Tooltip -->
<div class="tooltip" id="tooltip"></div>

<!-- Modals -->
<div class="modal-backdrop" id="modalCommit">
  <div class="modal">
    <div class="modal-head"><h3>New Commit</h3><button class="btn icon ghost" onclick="closeModals()">✕</button></div>
    <div class="modal-body">
      <label>Commit message</label><input id="m_msg" placeholder="feat(auth): add OAuth flow">
      <div class="row">
        <div><label>Author</label><input id="m_author" placeholder="Alex Chen"></div>
        <div><label>Branch</label><select id="m_branch"></select></div>
      </div>
      <div class="row">
        <div><label>Parent 1 (required)</label><select id="m_parent1"></select></div>
        <div><label>Parent 2 (merge, optional)</label><select id="m_parent2"><option value="">— none —</option></select></div>
      </div>
      <label>Anchor event (optional, auto-creates)</label>
      <div class="row">
        <div><input id="m_eventTitle" placeholder="e.g., Code Freeze"></div>
        <div><select id="m_eventType"><option value="milestone">Milestone</option><option value="review">Review</option><option value="release">Release</option><option value="incident">Incident</option><option value="decision">Decision</option></select></div>
      </div>
      <p class="hint">Tip: select two parents to create a merge commit. The graph will draw a merging path.</p>
    </div>
    <div class="modal-foot"><button class="btn ghost" onclick="closeModals()">Cancel</button><button class="btn primary" onclick="createCommitModal()">Create Commit</button></div>
  </div>
</div>

<div class="modal-backdrop" id="modalBranch">
  <div class="modal">
    <div class="modal-head"><h3>New Branch</h3><button class="btn icon ghost" onclick="closeModals()">✕</button></div>
    <div class="modal-body">
      <label>Branch name</label><input id="b_name" placeholder="feature/payments">
      <label>Start from commit</label><select id="b_from"></select>
      <label>Color</label><select id="b_color"><option value="#38bdf8">Ocean (main)</option><option value="#22c55e">Green (feature)</option><option value="#f97316">Orange (hotfix)</option><option value="#a78bfa">Violet (release)</option><option value="#ec4899">Pink</option><option value="#eab308">Amber</option></select>
    </div>
    <div class="modal-foot"><button class="btn ghost" onclick="closeModals()">Cancel</button><button class="btn primary" onclick="createBranch()">Create Branch</button></div>
  </div>
</div>

<div class="modal-backdrop" id="modalEvent">
  <div class="modal">
    <div class="modal-head"><h3>Anchor Contextual Event</h3><button class="btn icon ghost" onclick="closeModals()">✕</button></div>
    <div class="modal-body">
      <label>Title *</label><input id="e_title" placeholder="Code Freeze — v2.0">
      <div class="row">
        <div><label>Type</label><select id="e_type"><option value="milestone">Milestone</option><option value="review">Review</option><option value="release">Release</option><option value="incident">Incident</option><option value="decision">Decision</option><option value="deploy">Deploy</option></select></div>
        <div><label>Association — Commit *</label><select id="e_commit"></select></div>
      </div>
      <label>Description</label><textarea id="e_desc" placeholder="Context about this marker and why it matters at this point in history..."></textarea>
      <label>Spatial reference (optional)</label><input id="e_ref" placeholder="e.g., Sprint 12, Q3, Gate 3">
    </div>
    <div class="modal-foot"><button class="btn ghost" onclick="closeModals()">Cancel</button><button class="btn primary" onclick="createEvent()">Anchor Event</button></div>
  </div>
</div>

<div class="modal-backdrop" id="modalTag">
  <div class="modal">
    <div class="modal-head"><h3>New Tag</h3><button class="btn icon ghost" onclick="closeModals()">✕</button></div>
    <div class="modal-body">
      <label>Tag name</label><input id="t_name" placeholder="v1.2.0">
      <label>Commit</label><select id="t_commit"></select>
    </div>
    <div class="modal-foot"><button class="btn ghost" onclick="closeModals()">Cancel</button><button class="btn primary" onclick="createTag()">Create Tag</button></div>
  </div>
</div>

<div class="modal-backdrop" id="modalEditCommit">
  <div class="modal">
    <div class="modal-head"><h3>Edit Commit</h3><button class="btn icon ghost" onclick="closeModals()">✕</button></div>
    <div class="modal-body">
      <label>Message</label><input id="ec_msg">
      <div class="row">
        <div><label>Author</label><input id="ec_author"></div>
        <div><label>Branch</label><select id="ec_branch"></select></div>
      </div>
      <input type="hidden" id="ec_id">
    </div>
    <div class="modal-foot"><button class="btn ghost" onclick="closeModals()">Cancel</button><button class="btn primary" onclick="saveEditCommit()">Save</button></div>
  </div>
</div>

<script>
const STORAGE_KEY='gitVisualizer_v2';
const BRANCH_COLORS={main:'#38bdf8','feature/login':'#22c55e','feature/api':'#22c55e','feature/payments':'#22c55e',hotfix:'#f97316',release:'#a78bfa'};

let state={
  commits:[],
  branches:[],
  events:[],
  tags:[]
};
let selectedId=null;
let showLabels=true;

const EVENT_STYLES={
  milestone:{icon:'◆',bg:'#0ea5e9',color:'#e0f2fe',label:'Milestone'},
  review:{icon:'◎',bg:'#8b5cf6',color:'#ede9fe',label:'Review'},
  release:{icon:'⬢',bg:'#22c55e',color:'#dcfce7',label:'Release'},
  incident:{icon:'⚠',bg:'#ef4444',color:'#fee2e2',label:'Incident'},
  decision:{icon:'⬡',bg:'#eab308',color:'#fef9c3',label:'Decision'},
  deploy:{icon:'🚀',bg:'#06b6d4',color:'#cffafe',label:'Deploy'}
};

function uid(prefix='c'){return prefix+Math.random().toString(36).slice(2,7)+Date.now().toString(36).slice(-3)}
function short(id){return id.slice(0,7)}

// Initial data
function seed(){
  const commits=[
    {id:'c_a1b2c3d',message:'Initial commit — project scaffold',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*14,parents:[],branch:'main'},
    {id:'c_b2c3d4e',message:'feat: add routing & layout',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*13,parents:['c_a1b2c3d'],branch:'main'},
    {id:'c_c3d4e5f',message:'feat: user model & DB schema',author:'Sam Rivera',timestamp:Date.now()-1000*60*60*24*12,parents:['c_b2c3d4e'],branch:'main'},
    {id:'c_d4e5f6g',message:'feat(login): OAuth form UI',author:'Sam Rivera',timestamp:Date.now()-1000*60*60*24*11,parents:['c_c3d4e5f'],branch:'feature/login'},
    {id:'c_e5f6g7h',message:'feat(login): JWT middleware',author:'Sam Rivera',timestamp:Date.now()-1000*60*60*24*10,parents:['c_d4e5f6g'],branch:'feature/login'},
    {id:'c_f6g7h8i',message:'feat(api): REST endpoints',author:'Jordan Kim',timestamp:Date.now()-1000*60*60*24*11,parents:['c_c3d4e5f'],branch:'feature/api'},
    {id:'c_g7h8i9j',message:'feat(api): pagination & caching',author:'Jordan Kim',timestamp:Date.now()-1000*60*60*24*9,parents:['c_f6g7h8i'],branch:'feature/api'},
    {id:'c_h8i9j0k',message:'fix: sanitize inputs',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*9,parents:['c_c3d4e5f'],branch:'hotfix'},
    {id:'c_i9j0k1l',message:'Merge feature/login → main',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*8,parents:['c_b2c3d4e','c_e5f6g7h'],branch:'main'},
    {id:'c_j0k1l2m',message:'Merge feature/api → main',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*7,parents:['c_i9j0k1l','c_g7h8i9j'],branch:'main'},
    {id:'c_k1l2m3n',message:'Merge hotfix → main',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*6,parents:['c_j0k1l2m','c_h8i9j0k'],branch:'main'},
    {id:'c_l2m3n4o',message:'chore: bump to v1.2.0-rc',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*5,parents:['c_k1l2m3n'],branch:'release'},
    {id:'c_m3n4o5p',message:'docs: release notes',author:'Taylor Wu',timestamp:Date.now()-1000*60*60*24*4,parents:['c_l2m3n4o'],branch:'release'},
    {id:'c_n4o5p6q',message:'Merge release → main (v1.2.0)',author:'Alex Chen',timestamp:Date.now()-1000*60*60*24*2,parents:['c_k1l2m3n','c_m3n4o5p'],branch:'main'},
  ];
  const branches=[
    {name:'main',head:'c_n4o5p6q',color:'#38bdf8'},
    {name:'feature/login',head:'c_e5f6g7h',color:'#22c55e'},
    {name:'feature/api',head:'c_g7h8i9j',color:'#22c55e'},
    {name:'hotfix',head:'c_h8i9j0k',color:'#f97316'},
    {name:'release',head:'c_m3n4o5p',color:'#a78bfa'},
  ];
  const events=[
    {id:'e1',title:'Sprint Kickoff — Auth & API',type:'milestone',commitId:'c_c3d4e5f',ref:'Sprint 11',description:'Team splits into two streams: login UI and REST API. Branching strategy agreed.'},
    {id:'e2',title:'Security Review',type:'review',commitId:'c_h8i9j0k',ref:'Gate 2',description:'Hotfix for input sanitization flagged in pen-test. Must merge before RC.'},
    {id:'e3',title:'Feature Completion — Login',type:'milestone',commitId:'c_e5f6g7h',ref:'Sprint 11',description:'OAuth + JWT complete, ready for integration.'},
    {id:'e4',title:'API Freeze',type:'decision',commitId:'c_g7h8i9j',ref:'Sprint 12',description:'Pagination & caching finalized. No further API changes until release.'},
    {id:'e5',title:'Code Freeze — v1.2.0',type:'milestone',commitId:'c_k1l2m3n',ref:'Q3 Release',description:'All features merged to main. Main is now frozen for RC.'},
    {id:'e6',title:'Release Candidate',type:'release',commitId:'c_l2m3n4o',ref:'v1.2.0-rc',description:'RC cut on release branch. QA sign-off pending.'},
    {id:'e7',title:'Production Deploy',type:'deploy',commitId:'c_n4o5p6q',ref:'Prod',description:'v1.2.0 merged and deployed. Tag created, release notes published.'},
    {id:'e8',title:'Post-mortem Incident #42',type:'incident',commitId:'c_h8i9j0k',ref:'INC-42',description:'XSS vector discovered in legacy form. Hotfix branch created same day.'},
  ];
  const tags=[
    {name:'v1.1.0',commitId:'c_b2c3d4e'},
    {name:'v1.2.0-rc',commitId:'c_l2m3n4o'},
    {name:'v1.2.0',commitId:'c_n4o5p6q'},
  ];
  return {commits,branches,events,tags};
}

function load(){
  try{
    const raw=localStorage.getItem(STORAGE_KEY);
    if(raw){ state=JSON.parse(raw); }
    else { state=seed(); save(); }
  }catch(e){ state=seed(); }
}
function save(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); updateBadges(); }

function updateBadges(){
  document.getElementById('statsBadge').textContent=`${state.commits.length} commits • ${state.events.length} events`;
  document.getElementById('commitCount').textContent=state.commits.length;
  document.getElementById('eventCount').textContent=state.events.length;
  document.getElementById('branchCount').textContent=state.branches.length;
}

// Layout computation
function computeLayout(){
  // order commits topologically by timestamp
  const sorted=[...state.commits].sort((a,b)=>a.timestamp-b.timestamp);
  const idToIndex=new Map(sorted.map((c,i)=>[c.id,i]));
  // lanes: assign each branch a lane index
  const branchNames=[...new Set(state.branches.map(b=>b.name))];
  // ensure all commit branches are represented
  state.commits.forEach(c=>{ if(!branchNames.includes(c.branch)) branchNames.push(c.branch); });
  const laneMap=new Map(branchNames.map((n,i)=>[n,i]));
  const laneCount=Math.max(branchNames.length,1);
  const xFor=(commit)=>{
    const lane=laneMap.get(commit.branch) ?? 0;
    return 70 + lane * 130;
  };
  const yFor=(commit)=>{
    const idx=idToIndex.get(commit.id);
    return 52 + idx * 46;
  };
  // also need to handle commits whose parents are on different lanes -> x is already lane-based
  return {sorted, xFor, yFor, laneMap, laneCount, idToIndex};
}

// Color helper
function branchColor(name){
  const b=state.branches.find(x=>x.name===name);
  if(b) return b.color;
  return BRANCH_COLORS[name] || '#64748b';
}

// Rendering
let svg, g, zoomBehavior;
function initGraph(){
  svg=d3.select('#gitGraph');
  const wrap=document.getElementById('graphWrap');
  g=svg.append('g').attr('class','graph-g');
  zoomBehavior=d3.zoom().scaleExtent([0.4,2]).on('zoom',(e)=>{ g.attr('transform',e.transform); });
  svg.call(zoomBehavior);
  window.addEventListener('resize',()=> renderGraph());
}

function renderAll(){
  renderBranches();
  renderEventList();
  renderTagList();
  renderQuickSelects();
  renderGraph();
  renderFlow();
  updateBadges();
}

function renderBranches(){
  const el=document.getElementById('branchList');
  el.innerHTML=state.branches.map(b=>{
    const isActive=b.name==='main';
    return `<div class="branch-item ${selectedId && state.commits.find(c=>c.id===selectedId)?.branch===b.name?'active':''}" onclick="focusBranch('${b.name}')">
      <span class="branch-dot" style="background:${b.color}"></span>
      <div class="branch-meta"><div class="branch-name">${b.name}</div><div class="branch-head">→ ${short(b.head)}</div></div>
      <div class="branch-actions">
        <button class="btn small ghost" onclick="event.stopPropagation(); deleteBranch('${b.name}')" title="Delete">✕</button>
      </div>
    </div>`;
  }).join('') || `<div class="empty">No branches</div>`;
}

function renderEventList(){
  const el=document.getElementById('eventList');
  if(!state.events.length){ el.innerHTML=`<div class="empty">No events anchored yet</div>`; return; }
  // sort by commit order
  const order=new Map(state.commits.map((c,i)=>[c.id,i]));
  const sorted=[...state.events].sort((a,b)=>(order.get(a.commitId)??999)-(order.get(b.commitId)??999));
  el.innerHTML=sorted.map(ev=>{
    const s=EVENT_STYLES[ev.type]||EVENT_STYLES.milestone;
    const commit=state.commits.find(c=>c.id===ev.commitId);
    const isActive=selectedId===ev.commitId;
    return `<div class="branch-item" style="align-items:flex-start;cursor:pointer;${isActive?'background:rgba(56,189,248,0.08);border-color:rgba(56,189,248,0.3)':''}" onclick="selectCommit('${ev.commitId}')">
      <span class="event-icon" style="background:${s.bg};color:white;font-size:11px">${s.icon}</span>
      <div style="flex:1;min-width:0">
        <div style="font-size:12px;font-weight:700;line-height:1.2">${ev.title}</div>
        <div style="font-size:11px;color:var(--muted)">${s.label} • <span style="font-family:'JetBrains Mono',monospace">${commit?short(commit.id):'—'}</span> ${ev.ref?`• ${ev.ref}`:''}</div>
        <div style="font-size:11px;color:var(--dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${ev.description||''}</div>
      </div>
      <button class="btn small ghost" onclick="event.stopPropagation(); deleteEvent('${ev.id}')">✕</button>
    </div>`;
  }).join('');
}

function renderTagList(){
  const el=document.getElementById('tagList');
  if(!state.tags.length){ el.innerHTML=`<div class="empty" style="padding:8px">No tags</div>`; return; }
  el.innerHTML=state.tags.map(t=>{
    const c=state.commits.find(x=>x.id===t.commitId);
    return `<div style="display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--panel);border:1px solid var(--border);border-radius:8px;margin-bottom:6px">
      <span class="tag-pill">${t.name}</span>
      <span style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--muted)">${c?short(c.id):'—'} ${c?c.message.slice(0,24):''}</span>
      <button class="btn small ghost" style="margin-left:auto" onclick="deleteTag('${t.name}')">✕</button>
    </div>`;
  }).join('');
}

function renderQuickSelects(){
  const branches=state.branches;
  const commits=state.commits;
  const optsB=branches.map(b=>`<option value="${b.name}">${b.name}</option>`).join('');
  const optsC=commits.map(c=>`<option value="${c.id}">${short(c.id)} — ${c.message.slice(0,30)}</option>`).join('');
  const qB=document.getElementById('quickBranch'); if(qB) qB.innerHTML=optsB;
  const qP=document.getElementById('quickParent'); if(qP) qP.innerHTML=optsC;
  const mB=document.getElementById('m_branch'); if(mB) mB.innerHTML=optsB;
  const mP1=document.getElementById('m_parent1'); if(mP1) mP1.innerHTML=optsC;
  const mP2=document.getElementById('m_parent2'); if(mP2) mP2.innerHTML='<option value="">— none —</option>'+optsC;
  const bFrom=document.getElementById('b_from'); if(bFrom) bFrom.innerHTML=optsC;
  const eCommit=document.getElementById('e_commit'); if(eCommit) eCommit.innerHTML=optsC;
  const tCommit=document.getElementById('t_commit'); if(tCommit) tCommit.innerHTML=optsC;
  const ecBranch=document.getElementById('ec_branch'); if(ecBranch) ecBranch.innerHTML=optsB;
}

function renderFlow(){
  const track=document.getElementById('flowTrack');
  if(!state.events.length){ track.innerHTML=`<div class="empty" style="align-self:center">No contextual events — anchor one to a commit</div>`; return; }
  const order=new Map(state.commits.map((c,i)=>[c.id,i]));
  const sorted=[...state.events].sort((a,b)=>(order.get(a.commitId)??999)-(order.get(b.commitId)??999));
  track.innerHTML=sorted.map(ev=>{
    const s=EVENT_STYLES[ev.type]||EVENT_STYLES.milestone;
    const isActive=selectedId===ev.commitId;
    return `<div class="event-chip ${isActive?'active':''} anchored" style="border-top-color:${s.bg}" onclick="selectCommit('${ev.commitId}')" onmouseenter="hoverEvent('${ev.id}',true)" onmouseleave="hoverEvent('${ev.id}',false)">
      <div class="event-top">
        <span class="event-icon" style="background:${s.bg};color:white">${s.icon}</span>
        <div style="flex:1;min-width:0">
          <div class="event-title">${ev.title}</div>
          <div class="event-commit"><span style="width:8px;height:8px;border-radius:50%;background:${branchColor(state.commits.find(c=>c.id===ev.commitId)?.branch)};display:inline-block"></span> ${short(ev.commitId)} ${ev.ref?`• ${ev.ref}`:''}</div>
        </div>
      </div>
      <div class="event-desc">${ev.description||'No description'}</div>
      <span class="event-tag" style="background:${s.bg}18;border-color:${s.bg}40;color:${s.bg}">${s.label}</span>
    </div>`;
  }).join('');
  document.getElementById('flowBadge').textContent=`${sorted.length} events • ${selectedId?'linked to '+short(selectedId):'click to focus'}`;
}

function renderGraph(){
  if(!svg) return;
  const {sorted, xFor, yFor, laneMap} = computeLayout();
  const neededW = laneMap.size*130+580;
  const height=Math.max(1080, sorted.length*46+520);
  svg.attr('viewBox',null).attr('width', neededW).attr('height', height);

  g.selectAll('*').remove();

  // defs for gradients
  const defs=g.append('defs');
  // draw links
  const links=[];
  sorted.forEach(c=>{
    c.parents.forEach(pid=>{
      const parent=state.commits.find(x=>x.id===pid);
      if(parent) links.push({source:parent,target:c});
    });
  });

  // links group
  const linkG=g.append('g').attr('class','links');
  links.forEach(l=>{
    const x1=xFor(l.source), y1=yFor(l.source);
    const x2=xFor(l.target), y2=yFor(l.target);
    const isMerge=l.target.parents.length>1;
    // bezier: vertical then horizontal
    const midY=(y1+y2)/2;
    let path;
    if(x1===x2){
      path=`M ${x1} ${y1+14} L ${x2} ${y2-14}`;
    } else {
      // curved merge
      path=`M ${x1} ${y1+14} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2-14}`;
    }
    linkG.append('path')
      .attr('d',path)
      .attr('class','link'+(isMerge?' merge':''))
      .attr('stroke', isMerge ? '#a78bfa' : branchColor(l.target.branch))
      .attr('stroke-opacity', isMerge?0.9:0.55)
      .attr('stroke-width', isMerge?2.5:2);
    // arrow for merge?
    if(isMerge){
      linkG.append('circle').attr('cx',x2).attr('cy',y2-14).attr('r',2).attr('fill','#a78bfa');
    }
  });

  // branch lane labels
  const laneG=g.append('g');
  [...laneMap.entries()].forEach(([name,idx])=>{
    const x=70+idx*130;
    laneG.append('rect').attr('x',x-36).attr('y',8).attr('width',72).attr('height',16).attr('rx',8).attr('fill',branchColor(name)).attr('opacity',0.14).attr('stroke',branchColor(name)).attr('stroke-opacity',0.25);
    laneG.append('text').attr('x',x).attr('y',19).attr('text-anchor','middle').attr('font-size','9px').attr('font-family','JetBrains Mono,monospace').attr('fill',branchColor(name)).attr('font-weight','800').text(name);
    laneG.append('line').attr('x1',x).attr('x2',x).attr('y1',28).attr('y2',height-10).attr('stroke',branchColor(name)).attr('stroke-opacity',0.1).attr('stroke-width',1.5).attr('stroke-dasharray','8 8');
  });

  // event connectors & anchors
  const eventByCommit=new Map();
  state.events.forEach(ev=>{
    if(!eventByCommit.has(ev.commitId)) eventByCommit.set(ev.commitId,[]);
    eventByCommit.get(ev.commitId).push(ev);
  });

  // nodes group
  const nodeG=g.append('g').attr('class','nodes');
  sorted.forEach(c=>{
    const x=xFor(c), y=yFor(c);
    const isSelected=selectedId===c.id;
    const hasEvents=eventByCommit.has(c.id);
    const color=branchColor(c.branch);
    const events=eventByCommit.get(c.id)||[];

    const ng=nodeG.append('g')
      .attr('class','commit-node'+(isSelected?' selected':''))
      .attr('transform',`translate(${x},${y})`)
      .on('click',()=>selectCommit(c.id))
      .on('mouseenter',(e)=>showCommitTooltip(e,c))
      .on('mouseleave',hideTooltip)
      .on('mousemove',moveTooltip);

    // halo for selected
    if(isSelected){
      ng.append('circle').attr('r',18).attr('fill',color).attr('opacity',0.15);
    }
    // commit circle
    ng.append('circle').attr('r',hasEvents?11:9).attr('fill',color).attr('stroke','#0a0e14').attr('stroke-width',2);
    // inner dot for merge
    if(c.parents.length>1){
      ng.append('circle').attr('r',3).attr('fill','white').attr('opacity',0.95);
    } else {
      ng.append('circle').attr('r',2.5).attr('fill','white').attr('opacity',0.9);
    }
    // tag indicator
    const tag=state.tags.find(t=>t.commitId===c.id);
    if(tag){
      ng.append('g').attr('transform','translate(14,-14)').call(g=>{
        g.append('rect').attr('x',-2).attr('y',-8).attr('width', tag.name.length*6+10).attr('height',14).attr('rx',6).attr('fill','#eab308').attr('stroke','#0a0e14').attr('stroke-width',1);
        g.append('text').attr('x',3).attr('y',2).attr('font-size','8px').attr('font-family','JetBrains Mono,monospace').attr('font-weight','700').attr('fill','#0a0e14').text(tag.name);
      });
    }

    // labels — placed below-right to avoid anchor collision
    if(showLabels){
      const maxLen = 30;
      const label=c.message.length>maxLen?c.message.slice(0,maxLen-1)+'…':c.message;
      const tw=label.length*5.6+20;
      const hashW=52;
      const bg=ng.append('g').attr('transform','translate(16,14)');
      bg.append('rect').attr('x',0).attr('y',-9).attr('width',tw+hashW).attr('height',18).attr('rx',6).attr('fill', isSelected?'#1e293b':'#0f172a').attr('stroke', isSelected?'#334155':'#1e293b').attr('stroke-width',1).attr('opacity',1);
      bg.append('text').attr('x',8).attr('y',3).text(label).attr('fill', isSelected?'#f1f5f9':'#e2e8f0').attr('font-size','11px').attr('font-family','Inter,sans-serif').attr('font-weight', isSelected?'700':'500');
      bg.append('rect').attr('x',tw+4).attr('y',-6).attr('width',hashW-6).attr('height',12).attr('rx',4).attr('fill','#1e293b').attr('stroke','#334155').attr('stroke-width',0.5);
      bg.append('text').attr('x',tw+8).attr('y',3).attr('font-size','8px').attr('font-family','JetBrains Mono,monospace').attr('fill','#94a3b8').attr('font-weight','600').text(short(c.id));
    }

    // event anchors — above the node to avoid label overlap
    events.forEach((ev,idx)=>{
      const s=EVENT_STYLES[ev.type]||EVENT_STYLES.milestone;
      const ax= 28 + idx*34;
      const ay= -24;
      // connector — short diagonal from node to anchor
      g.append('line')
        .attr('class','event-connector')
        .attr('x1',x+10).attr('y1',y-10)
        .attr('x2',x+ax).attr('y2',y+ay+10)
        .attr('stroke',s.bg)
        .attr('opacity', isSelected?0.85:0.45);
      const ag=g.append('g')
        .attr('class','event-anchor')
        .attr('transform',`translate(${x+ax},${y+ay})`)
        .on('click',(e)=>{e.stopPropagation(); selectCommit(c.id);})
        .on('mouseenter',(e)=>showEventTooltip(e,ev,c))
        .on('mouseleave',hideTooltip)
        .on('mousemove',moveTooltip);
      // diamond/pill
      ag.append('rect').attr('x',-14).attr('y',-10).attr('width',28).attr('height',20).attr('rx',6).attr('fill',s.bg).attr('stroke','white').attr('stroke-width', isSelected?1.5:0.8).attr('opacity', isSelected?1:0.95);
      ag.append('text').attr('text-anchor','middle').attr('y',4).attr('font-size','11px').attr('fill','white').text(s.icon);
      // small label below
      if(events.length===1){
        ag.append('text').attr('text-anchor','middle').attr('y',22).attr('font-size','8px').attr('font-family','JetBrains Mono,monospace').attr('fill',s.bg).attr('font-weight','700').text(ev.type.toUpperCase());
      }
    });
  });

  // update inspector highlight
  document.getElementById('selectedInfo').style.display=selectedId?'block':'none';
  if(selectedId){
    const c=state.commits.find(x=>x.id===selectedId);
    document.getElementById('selectedInfo').innerHTML=`● <b style="color:${branchColor(c.branch)}">${c.branch}</b> ${short(c.id)} • ${c.message.slice(0,24)}`;
  }
}

// Tooltips
const tooltip=document.getElementById('tooltip');
function showCommitTooltip(e,c){
  const tag=state.tags.find(t=>t.commitId===c.id);
  const evs=state.events.filter(ev=>ev.commitId===c.id);
  tooltip.innerHTML=`
    <h4 style="font-family:'JetBrains Mono',monospace">${short(c.id)} <span style="color:${branchColor(c.branch)}">⎇ ${c.branch}</span></h4>
    <p style="color:var(--text);font-weight:600">${c.message}</p>
    <p>by ${c.author} • ${new Date(c.timestamp).toLocaleDateString()} • parents: ${c.parents.length?c.parents.map(short).join(', '):'— root'}</p>
    ${tag?`<div class="meta"><span class="tag-pill">${tag.name}</span> tagged</div>`:''}
    ${evs.length?`<div class="meta" style="color:#38bdf8">◆ ${evs.length} anchored event(s): ${evs.map(x=>x.title).join(', ')}</div>`:''}
    <div class="meta">Click to inspect • Hover anchor for event details</div>
  `;
  tooltip.classList.add('show');
  moveTooltip(e);
}
function showEventTooltip(e,ev,c){
  const s=EVENT_STYLES[ev.type]||EVENT_STYLES.milestone;
  tooltip.innerHTML=`
    <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px"><span style="width:28px;height:28px;border-radius:8px;background:${s.bg};display:grid;place-items:center;color:white">${s.icon}</span><h4>${ev.title}</h4></div>
    <p>${ev.description||'No description'}</p>
    <div class="meta">Type: ${s.label} • Ref: ${ev.ref||'—'}</div>
    <div class="meta" style="color:${branchColor(c.branch)}">Anchored to commit <b>${short(c.id)}</b> on <b>${c.branch}</b> — "${c.message}"</div>
    <div class="meta">Spatial link confirmed ✓</div>
  `;
  tooltip.classList.add('show');
  moveTooltip(e);
}
function moveTooltip(e){
  const x=e.clientX+14, y=e.clientY+14;
  const r=tooltip.getBoundingClientRect();
  let nx=x, ny=y;
  if(nx+r.width>window.innerWidth-10) nx=e.clientX - r.width -14;
  if(ny+r.height>window.innerHeight-10) ny=e.clientY - r.height -14;
  tooltip.style.left=nx+'px';
  tooltip.style.top=ny+'px';
}
function hideTooltip(){ tooltip.classList.remove('show'); }
function hoverEvent(id,enter){
  if(!enter) return hideTooltip();
  const ev=state.events.find(x=>x.id===id);
  if(!ev) return;
  const c=state.commits.find(x=>x.id===ev.commitId);
  // fake event for tooltip
  const fake={clientX: window.innerWidth/2, clientY: 200};
  showEventTooltip(fake,ev,c);
}

// Selection
function selectCommit(id){
  selectedId=id;
  renderGraph();
  renderFlow();
  renderBranches();
  renderEventList();
  showInspector(id);
  save();
}
function focusBranch(name){
  const head=state.branches.find(b=>b.name===name)?.head;
  if(head) selectCommit(head);
}
function showInspector(id){
  const c=state.commits.find(x=>x.id===id);
  if(!c){ document.getElementById('emptyState').style.display='grid'; document.getElementById('detailView').style.display='none'; return; }
  document.getElementById('emptyState').style.display='none';
  const dv=document.getElementById('detailView');
  dv.style.display='block';
  const evs=state.events.filter(e=>e.commitId===id);
  const tag=state.tags.find(t=>t.commitId===id);
  const children=state.commits.filter(x=>x.parents.includes(id));
  dv.innerHTML=`
    <div class="commit-detail">
      <div class="commit-hash"><span>${short(c.id)} • ${c.id}</span><button class="btn small ghost" onclick="copyHash('${c.id}')">copy</button></div>
      <div class="commit-msg">${c.message}</div>
      ${tag?`<div><span class="tag-pill">${tag.name}</span></div>`:''}
      <div class="commit-meta">
        <div class="meta-box"><span>Branch</span><p style="color:${branchColor(c.branch)}">${c.branch}</p></div>
        <div class="meta-box"><span>Author</span><p>${c.author}</p></div>
        <div class="meta-box"><span>Date</span><p style="font-size:11px">${new Date(c.timestamp).toLocaleString()}</p></div>
        <div class="meta-box"><span>Parents</span><p>${c.parents.length?c.parents.map(short).join(', '):'— root'}</p></div>
      </div>
      ${children.length?`<div class="meta-box"><span>Children</span><p>${children.map(x=>short(x.id)).join(', ')}</p></div>`:''}
      <div style="display:flex;gap:6px">
        <button class="btn small" onclick="openEditCommit('${c.id}')">✎ Edit</button>
        <button class="btn small ghost" onclick="deleteCommit('${c.id}')" style="color:var(--red);border-color:rgba(239,68,68,0.3)">Delete</button>
        <button class="btn small ghost" onclick="openModal('event','${c.id}')">◆ Anchor Event</button>
      </div>
      <div>
        <h4 style="font-size:11px;letter-spacing:0.08em;text-transform:uppercase;color:var(--muted);margin-bottom:8px">Anchored Contextual Events (${evs.length})</h4>
        <div class="anchored-list">
          ${evs.length? evs.map(ev=>{
            const s=EVENT_STYLES[ev.type];
            return `<div class="anchored-item" style="border-color:${s.bg}40">
              <span class="icon" style="background:${s.bg};color:white">${s.icon}</span>
              <div style="flex:1;min-width:0">
                <div style="font-size:12px;font-weight:700">${ev.title}</div>
                <div style="font-size:11px;color:var(--muted)">${s.label} • ${ev.ref||'no ref'}</div>
                <div style="font-size:11px;color:var(--dim);margin-top:4px">${ev.description||''}</div>
              </div>
              <button class="btn small ghost" onclick="deleteEvent('${ev.id}')">✕</button>
            </div>`;
          }).join('') : `<div class="empty" style="border:1px dashed var(--border);border-radius:8px">No events anchored here. <a href="#" onclick="openModal('event','${c.id}');return false" style="color:var(--accent)">Anchor one →</a></div>`}
        </div>
      </div>
    </div>
  `;
  document.getElementById('inspectorBadge').textContent=short(c.id);
}

// CRUD
function quickCommit(){
  const msg=document.getElementById('quickMsg').value.trim();
  const branch=document.getElementById('quickBranch').value;
  const parent=document.getElementById('quickParent').value;
  if(!msg) return alert('Message required');
  if(!branch) return alert('Branch required');
  const id=uid('c_');
  const commit={id, message:msg, author:'You', timestamp:Date.now(), parents: parent?[parent]:[], branch};
  // update branch head
  const b=state.branches.find(x=>x.name===branch);
  if(b) b.head=id; else state.branches.push({name:branch,head:id,color:'#22c55e'});
  state.commits.push(commit);
  document.getElementById('quickMsg').value='';
  save(); renderAll(); selectCommit(id);
}
function createCommitModal(){
  const msg=document.getElementById('m_msg').value.trim();
  const author=document.getElementById('m_author').value.trim()||'You';
  const branch=document.getElementById('m_branch').value;
  const p1=document.getElementById('m_parent1').value;
  const p2=document.getElementById('m_parent2').value;
  const evTitle=document.getElementById('m_eventTitle').value.trim();
  const evType=document.getElementById('m_eventType').value;
  if(!msg) return alert('Message required');
  if(!p1) return alert('Parent 1 required');
  const id=uid('c_');
  const parents=[p1]; if(p2) parents.push(p2);
  const commit={id,message:msg,author,timestamp:Date.now(),parents,branch};
  state.commits.push(commit);
  const b=state.branches.find(x=>x.name===branch);
  if(b) b.head=id; else state.branches.push({name:branch,head:id,color:branchColor(branch)});
  if(evTitle){
    state.events.push({id:uid('e'),title:evTitle,type:evType,commitId:id,ref:'',description:`Auto-anchored at commit ${short(id)}`});
  }
  closeModals(); save(); renderAll(); selectCommit(id);
  document.getElementById('m_msg').value=''; document.getElementById('m_eventTitle').value='';
}
function createBranch(){
  const name=document.getElementById('b_name').value.trim();
  const from=document.getElementById('b_from').value;
  const color=document.getElementById('b_color').value;
  if(!name) return alert('Name required');
  if(state.branches.find(b=>b.name===name)) return alert('Branch exists');
  if(!from) return alert('Pick a commit');
  state.branches.push({name,head:from,color});
  closeModals(); save(); renderAll();
  document.getElementById('b_name').value='';
}
function createEvent(){
  const title=document.getElementById('e_title').value.trim();
  const type=document.getElementById('e_type').value;
  const commitId=document.getElementById('e_commit').value;
  const desc=document.getElementById('e_desc').value.trim();
  const ref=document.getElementById('e_ref').value.trim();
  if(!title) return alert('Title required');
  if(!commitId) return alert('Commit association required');
  state.events.push({id:uid('e'),title,type,commitId,description:desc,ref});
  closeModals(); save(); renderAll(); selectCommit(commitId);
  document.getElementById('e_title').value=''; document.getElementById('e_desc').value=''; document.getElementById('e_ref').value='';
}
function createTag(){
  const name=document.getElementById('t_name').value.trim();
  const commitId=document.getElementById('t_commit').value;
  if(!name) return alert('Name required');
  state.tags.push({name,commitId});
  closeModals(); save(); renderAll();
  document.getElementById('t_name').value='';
}
function deleteBranch(name){
  if(name==='main') return alert('Cannot delete main');
  if(!confirm(`Delete branch "${name}"? Commits remain.`)) return;
  state.branches=state.branches.filter(b=>b.name!==name);
  save(); renderAll();
}
function deleteEvent(id){
  state.events=state.events.filter(e=>e.id!==id);
  save(); renderAll(); if(selectedId) showInspector(selectedId);
}
function deleteTag(name){
  state.tags=state.tags.filter(t=>t.name!==name);
  save(); renderAll();
}
function deleteCommit(id){
  if(!confirm('Delete this commit? Children that reference it will become roots. Anchored events will be removed.')) return;
  state.commits=state.commits.filter(c=>c.id!==id);
  // remove parent refs
  state.commits.forEach(c=>{ c.parents=c.parents.filter(p=>p!==id); });
  state.events=state.events.filter(e=>e.commitId!==id);
  state.tags=state.tags.filter(t=>t.commitId!==id);
  // fix branch heads
  state.branches.forEach(b=>{ if(b.head===id){ const remaining=state.commits.filter(c=>c.branch===b.name); b.head=remaining.length?remaining[remaining.length-1].id: (state.commits[0]?.id||'') }});
  selectedId=null;
  document.getElementById('emptyState').style.display='grid';
  document.getElementById('detailView').style.display='none';
  save(); renderAll();
}
function openEditCommit(id){
  const c=state.commits.find(x=>x.id===id);
  if(!c) return;
  document.getElementById('ec_id').value=c.id;
  document.getElementById('ec_msg').value=c.message;
  document.getElementById('ec_author').value=c.author;
  document.getElementById('ec_branch').value=c.branch;
  openModal('editCommit');
}
function saveEditCommit(){
  const id=document.getElementById('ec_id').value;
  const c=state.commits.find(x=>x.id===id);
  if(!c) return;
  c.message=document.getElementById('ec_msg').value.trim()||c.message;
  c.author=document.getElementById('ec_author').value.trim()||c.author;
  const newBranch=document.getElementById('ec_branch').value;
  if(newBranch!==c.branch){
    c.branch=newBranch;
    const b=state.branches.find(x=>x.name===newBranch);
    if(!b) state.branches.push({name:newBranch,head:id,color:'#22c55e'});
  }
  closeModals(); save(); renderAll(); selectCommit(id);
}
function copyHash(id){ navigator.clipboard.writeText(id); }

// Modals
function openModal(kind, presetCommit){
  closeModals();
  if(kind==='commit') document.getElementById('modalCommit').classList.add('open');
  if(kind==='branch') document.getElementById('modalBranch').classList.add('open');
  if(kind==='event'){ document.getElementById('modalEvent').classList.add('open'); if(presetCommit) document.getElementById('e_commit').value=presetCommit; }
  if(kind==='tag') document.getElementById('modalTag').classList.add('open');
  if(kind==='editCommit') document.getElementById('modalEditCommit').classList.add('open');
}
function closeModals(){ document.querySelectorAll('.modal-backdrop').forEach(m=>m.classList.remove('open')); }
document.querySelectorAll('.modal-backdrop').forEach(m=> m.addEventListener('click',e=>{ if(e.target===m) closeModals(); }));

// Import / Export / Reset
function exportData(){
  const blob=new Blob([JSON.stringify(state,null,2)],{type:'application/json'});
  const url=URL.createObjectURL(blob);
  const a=document.createElement('a'); a.href=url; a.download='git-visualizer-state.json'; a.click(); URL.revokeObjectURL(url);
}
function importData(){ document.getElementById('fileInput').click(); }
document.getElementById('fileInput').addEventListener('change',e=>{
  const file=e.target.files[0]; if(!file) return;
  const reader=new FileReader();
  reader.onload=()=>{
    try{
      const data=JSON.parse(reader.result);
      if(data.commits && data.branches){ state=data; if(!state.tags) state.tags=[]; if(!state.events) state.events=[]; save(); renderAll(); alert('Imported successfully'); }
      else alert('Invalid file: missing commits/branches');
    }catch(err){ alert('Invalid JSON'); }
  };
  reader.readAsText(file);
  e.target.value='';
});
function resetData(){
  if(!confirm('Reset to seeded demo data? LocalStorage will be overwritten.')) return;
  state=seed(); save(); selectedId=null; renderAll();
  document.getElementById('emptyState').style.display='grid'; document.getElementById('detailView').style.display='none';
}

// Helpers
function zoomFit(){
  svg.transition().duration(350).call(zoomBehavior.transform, d3.zoomIdentity.translate(10,10).scale(1));
  // also scroll container to top-left
  document.getElementById('graphWrap').scrollTo({top:0, left:0, behavior:'smooth'});
}
function toggleLabels(){ showLabels=!showLabels; document.getElementById('labelBtn').textContent='Labels: '+(showLabels?'On':'Off'); renderGraph(); }

// Init
load();
initGraph();
renderAll();
// auto-select a commit with events to demonstrate anchoring
setTimeout(()=>selectCommit('c_k1l2m3n'), 300);

// drag scroll for flow
const track=document.getElementById('flowTrack');
let isDown=false, startX, scrollLeft;
track.addEventListener('mousedown',e=>{isDown=true; track.style.cursor='grabbing'; startX=e.pageX-track.offsetLeft; scrollLeft=track.scrollLeft});
track.addEventListener('mouseleave',()=>{isDown=false; track.style.cursor='grab'});
track.addEventListener('mouseup',()=>{isDown=false; track.style.cursor='grab'});
track.addEventListener('mousemove',e=>{if(!isDown) return; e.preventDefault(); const x=e.pageX-track.offsetLeft; const walk=(x-startX)*1.2; track.scrollLeft=scrollLeft-walk});
track.style.cursor='grab';
</script>
</body>
</html>