UNPKG

@phren/cli

Version:

Knowledge layer for AI agents - CLI, MCP server, and data layer

9 lines • 124 kB
export const WEB_UI_STYLES = "    /*\n     * Typography scale — modular 1.25 ratio, base = 14px\n     * --text-xs   (caption):  ~11px  (base / 1.25^2, rounded)\n     * --text-sm   (small):     12px  (base / 1.25, rounded)\n     * --text-base (body):      14px\n     * --text-md   (h3):        18px  (base * 1.25, rounded)\n     * --text-lg   (h2):        22px  (base * 1.25^2, rounded)\n     * --text-xl   (h1):        28px  (base * 1.25^3, rounded)\n     */\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n    :root {\n      --text-xs: 11px;\n      --text-sm: 12px;\n      --text-base: 14px;\n      --text-md: 18px;\n      --text-lg: 22px;\n      --text-xl: 28px;\n      --bg: #f9f8f6;\n      --surface: #ffffff;\n      --surface-raised: #fafbfc;\n      --surface-sunken: #f1f3f6;\n      --ink: #1a1a18;\n      --ink-secondary: #3d3d3a;\n      --muted: #7a7872;\n      --accent: #b8860b;\n      --accent-hover: #9a7209;\n      --accent-dim: rgba(184,134,11,.08);\n      --accent-glow: rgba(184,134,11,.15);\n      --cyan: #2b5f8e;\n      --cyan-dim: rgba(43,95,142,.08);\n      --border: #e5e7eb;\n      --border-light: #f3f4f6;\n      --danger: #c45a4a;\n      --danger-dim: rgba(239,68,68,.08);\n      --warning: #d4892e;\n      --success: #10b981;\n      --success-dim: rgba(16,185,129,.08);\n      --purple: #b8860b;\n      --purple-dim: rgba(184,134,11,.08);\n      --blue: #3b82f6;\n      --red: #c45a4a;\n      --green: #10b981;\n      --radius: 10px;\n      --radius-sm: 6px;\n      --shadow-sm: 0 1px 2px rgba(0,0,0,.04), 0 1px 1px rgba(0,0,0,.02);\n      --shadow: 0 2px 8px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);\n      --shadow-lg: 0 8px 24px rgba(0,0,0,.08), 0 2px 8px rgba(0,0,0,.04);\n      --font: \"Inter\", system-ui, -apple-system, sans-serif;\n      --mono: \"JetBrains Mono\", \"Fira Code\", \"Cascadia Code\", monospace;\n    }\n\n    [data-theme=\"dark\"] {\n      --bg: #0d0e0c;\n      --surface: #15160f;\n      --surface-raised: #1c1d16;\n      --surface-sunken: #0a0b08;\n      --ink: #e8e4d9;\n      --ink-secondary: #b5b0a3;\n      --muted: #6b6860;\n      --accent: #d4892e;\n      --accent-hover: #e09a3a;\n      --accent-dim: rgba(212,137,46,.10);\n      --accent-glow: rgba(212,137,46,.18);\n      --cyan: #3a7bae;\n      --cyan-dim: rgba(58,123,174,.12);\n      --border: #2a2820;\n      --border-light: #1e1d17;\n      --danger-dim: rgba(239,68,68,.1);\n      --success-dim: rgba(16,185,129,.1);\n      --purple-dim: rgba(212,137,46,.12);\n      --shadow-sm: 0 1px 3px rgba(0,0,0,.4);\n      --shadow: 0 2px 8px rgba(0,0,0,.5), 0 0 1px rgba(0,0,0,.3);\n      --shadow-lg: 0 8px 32px rgba(0,0,0,.6), 0 0 1px rgba(0,0,0,.4);\n    }\n    [data-theme=\"dark\"] .split-item.selected { background: rgba(212,137,46,.08); border-left-color: var(--accent); }\n    [data-theme=\"dark\"] .split-item:hover { background: var(--surface-raised); }\n    [data-theme=\"dark\"] .hook-item.selected { background: rgba(212,137,46,.08); border-left-color: var(--accent); }\n    [data-theme=\"dark\"] .hook-item:hover { background: var(--surface-raised); }\n    [data-theme=\"dark\"] .projects-search { background: var(--surface); color: var(--ink); }\n    [data-theme=\"dark\"] .review-filters select { background: var(--surface); color: var(--ink); }\n    [data-theme=\"dark\"] .review-edit-textarea { background: var(--surface-sunken); color: var(--ink); border-color: var(--border); }\n    [data-theme=\"dark\"] .reader-content textarea { background: var(--surface-sunken); color: var(--ink); }\n    [data-theme=\"dark\"] .graph-container { background: #0a0b08; }\n    [data-theme=\"dark\"] .reader-toolbar { background: var(--surface-sunken); }\n    [data-theme=\"dark\"] .card { border-color: var(--border); }\n    [data-theme=\"dark\"] .badge-project { background: rgba(212,137,46,.15); color: #d4892e; }\n    [data-theme=\"dark\"] .badge { background: var(--surface-sunken); }\n    [data-theme=\"dark\"] .review-card { border-color: var(--border); background: var(--surface-raised); }\n    [data-theme=\"dark\"] .review-card:hover { border-color: var(--accent); }\n\n    body {\n      font-family: var(--font);\n      background: var(--bg);\n      color: var(--ink);\n      line-height: 1.5;\n      min-height: 100vh;\n    }\n\n    /* ── Header ─────────────────────────────────────────────── */\n    .header {\n      background: rgba(255,255,255,.82);\n      backdrop-filter: blur(16px) saturate(1.8);\n      -webkit-backdrop-filter: blur(16px) saturate(1.8);\n      border-bottom: 1px solid var(--border);\n      padding: 0 24px;\n      display: flex;\n      align-items: center;\n      gap: 32px;\n      height: 56px;\n      position: sticky;\n      top: 0;\n      z-index: 100;\n    }\n    [data-theme=\"dark\"] .header {\n      background: rgba(13,14,12,.82);\n      border-bottom-color: var(--border);\n      box-shadow: 0 1px 0 rgba(212,137,46,.08);\n    }\n    .header-brand {\n      font-size: var(--text-md);\n      font-weight: 700;\n      color: var(--accent);\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      letter-spacing: -0.03em;\n    }\n    .header-brand svg {\n      width: 28px;\n      height: 28px;\n      opacity: .9;\n      flex-shrink: 0;\n      filter: drop-shadow(0 0 4px rgba(155,139,196,.5));\n      cursor: pointer;\n      animation: phren-breathe 3.6s ease-in-out infinite;\n      transform-origin: center;\n      transition: filter .2s;\n    }\n    .header-brand svg:hover {\n      animation: phren-wobble .5s ease-in-out;\n      filter: drop-shadow(0 0 8px rgba(155,139,196,.7));\n    }\n    .header-brand svg.popped {\n      animation: phren-pop .45s ease-out forwards;\n    }\n    @keyframes phren-breathe {\n      0%, 100% { transform: scale(1); filter: drop-shadow(0 0 4px rgba(155,139,196,.5)); }\n      50% { transform: scale(1.06); filter: drop-shadow(0 0 6px rgba(155,139,196,.65)); }\n    }\n    @keyframes phren-wobble {\n      0%   { transform: rotate(0deg) scale(1.1); }\n      20%  { transform: rotate(-12deg) scale(1.1); }\n      40%  { transform: rotate(10deg) scale(1.1); }\n      60%  { transform: rotate(-6deg) scale(1.1); }\n      80%  { transform: rotate(4deg) scale(1.1); }\n      100% { transform: rotate(0deg) scale(1.1); }\n    }\n    @keyframes phren-pop {\n      0%   { transform: scale(1); }\n      30%  { transform: scale(1.35); filter: drop-shadow(0 0 8px rgba(155,139,196,.8)); }\n      60%  { transform: scale(0.92); }\n      100% { transform: scale(1); filter: drop-shadow(0 0 4px rgba(155,139,196,.5)); }\n    }\n    .nav { display: flex; gap: 0; height: 100%; }\n    .nav-item {\n      padding: 0 16px;\n      font-size: var(--text-base);\n      font-weight: 500;\n      color: var(--muted);\n      cursor: pointer;\n      border: none;\n      background: none;\n      height: 100%;\n      display: flex;\n      align-items: center;\n      border-bottom: 2px solid transparent;\n      transition: color .15s, border-color .15s;\n      font-family: var(--font);\n      letter-spacing: -0.01em;\n    }\n    .nav-item:hover { color: var(--ink); }\n    .nav-item.active {\n      color: var(--accent);\n      border-bottom-color: var(--accent);\n      font-weight: 600;\n    }\n    .nav-item .count {\n      background: var(--accent-dim);\n      color: var(--accent);\n      font-size: var(--text-xs);\n      padding: 0 8px;\n      border-radius: 10px;\n      margin-left: 8px;\n      font-weight: 700;\n      letter-spacing: .02em;\n    }\n\n    /* ── Main ────────────────────────────────────────────────── */\n    .main { padding: 24px; max-width: 1400px; margin: 0 auto; }\n    .tab-content { display: none; }\n    .tab-content.active { display: block; }\n\n    /* ── Cards ───────────────────────────────────────────────── */\n    .card {\n      background: var(--surface-raised);\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      box-shadow: var(--shadow-sm);\n      overflow: hidden;\n    }\n    .card-header {\n      padding: 12px 20px;\n      border-bottom: 1px solid var(--border-light);\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      background: var(--surface-raised);\n    }\n    .card-header h2 {\n      font-size: var(--text-sm);\n      font-weight: 650;\n      text-transform: uppercase;\n      letter-spacing: .05em;\n      color: var(--muted);\n    }\n    .card-body { padding: 20px; }\n\n    /* ── Projects Tab ────────────────────────────────────────── */\n    .projects-grid {\n      display: grid;\n      grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));\n      gap: 16px;\n    }\n    .project-card {\n      background: var(--surface-raised);\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      padding: 20px;\n      cursor: pointer;\n      transition: box-shadow .2s, border-color .2s, transform .15s;\n      position: relative;\n    }\n    .project-card:hover {\n      box-shadow: var(--shadow-lg);\n      border-color: color-mix(in srgb, var(--accent) 40%, var(--border));\n      transform: translateY(-1px);\n    }\n    .project-card.selected {\n      border-color: var(--accent);\n      box-shadow: 0 0 0 1px var(--accent), var(--shadow);\n    }\n    .project-card-name {\n      font-size: var(--text-md);\n      font-weight: 600;\n      margin-bottom: 8px;\n      display: flex;\n      align-items: center;\n      gap: 8px;\n    }\n    .project-card-summary {\n      font-size: var(--text-base);\n      color: var(--muted);\n      line-height: 1.5;\n      margin-bottom: 12px;\n      display: -webkit-box;\n      -webkit-line-clamp: 2;\n      -webkit-box-orient: vertical;\n      overflow: hidden;\n    }\n    .project-card-stats {\n      display: flex;\n      gap: 16px;\n      font-size: var(--text-sm);\n      color: var(--muted);\n    }\n    .project-card-stat {\n      display: flex;\n      align-items: center;\n      gap: 4px;\n    }\n    .project-card-stat strong { color: var(--ink); font-weight: 600; }\n\n    /* Project detail panel */\n    .project-detail {\n      margin-top: 20px;\n    }\n    .project-detail-header {\n      display: flex;\n      align-items: center;\n      gap: 16px;\n      margin-bottom: 16px;\n    }\n    .project-detail-header h2 { font-size: var(--text-lg); font-weight: 700; }\n    .project-detail-header .btn { font-size: var(--text-sm); }\n    .project-detail-tabs {\n      display: flex;\n      gap: 0;\n      border-bottom: 1px solid var(--border);\n      margin-bottom: 0;\n    }\n    .project-detail-tab {\n      padding: 12px 20px;\n      font-size: var(--text-base);\n      font-weight: 500;\n      color: var(--muted);\n      cursor: pointer;\n      border: none;\n      background: none;\n      border-bottom: 2px solid transparent;\n      font-family: var(--font);\n      transition: color .15s;\n    }\n    .project-detail-tab:hover { color: var(--ink); }\n    .project-detail-tab.active { color: var(--accent); border-bottom-color: var(--accent); }\n    .project-detail-content {\n      background: var(--surface);\n      border: 1px solid var(--border);\n      border-top: none;\n      border-radius: 0 0 var(--radius) var(--radius);\n      min-height: 400px;\n    }\n    .project-detail-content pre {\n      margin: 0;\n      padding: 20px;\n      font-family: var(--mono);\n      font-size: var(--text-sm);\n      line-height: 1.7;\n      white-space: pre-wrap;\n      word-break: break-word;\n      overflow: auto;\n      max-height: 600px;\n    }\n    .project-detail-empty {\n      padding: 60px 20px;\n      text-align: center;\n      color: var(--muted);\n      font-size: var(--text-base);\n    }\n\n    /* ── Finding detail cards ─────────────────────────────────── */\n    .finding-cards { display: flex; flex-direction: column; gap: 10px; padding: 14px; }\n    .finding-detail-card {\n      border: 1px solid var(--border);\n      border-radius: var(--radius-sm);\n      background: var(--surface-raised);\n      overflow: hidden;\n    }\n    .finding-detail-card summary {\n      list-style: none;\n      cursor: pointer;\n      padding: 12px 14px;\n      color: var(--ink-secondary);\n      font-size: var(--text-sm);\n      line-height: 1.5;\n      font-weight: 500;\n    }\n    .finding-detail-card summary::-webkit-details-marker { display: none; }\n    .finding-detail-card[open] summary {\n      border-bottom: 1px solid var(--border-light);\n      background: color-mix(in srgb, var(--accent) 4%, var(--surface-raised));\n    }\n    .finding-detail-meta {\n      padding: 10px 14px 12px;\n      display: flex;\n      gap: 12px;\n      flex-wrap: wrap;\n      align-items: center;\n      font-size: var(--text-xs);\n      color: var(--muted);\n    }\n    .finding-score-indicator {\n      width: 8px;\n      height: 8px;\n      border-radius: 50%;\n      display: inline-block;\n      margin-right: 6px;\n      vertical-align: middle;\n    }\n    .finding-score-indicator.healthy { background: #10b981; }\n    .finding-score-indicator.decaying { background: #f59e0b; }\n    .finding-score-indicator.stale { background: #ef4444; }\n\n\n    /* ── Review Tab ──────────────────────────────────────────── */\n    .review-filters {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      margin-bottom: 16px;\n      flex-wrap: wrap;\n    }\n    .review-filters select {\n      padding: 8px 28px 8px 12px;\n      border: 1px solid var(--border);\n      border-radius: var(--radius-sm);\n      font-size: var(--text-sm);\n      font-family: var(--font);\n      font-weight: 500;\n      background: var(--surface);\n      color: var(--ink-secondary);\n      outline: none;\n      cursor: pointer;\n      transition: border-color .15s;\n      appearance: none;\n      -webkit-appearance: none;\n      background-image: url(\"data:image/svg+xml,%3Csvg width='10' height='6' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%236b7280' fill='none' stroke-width='1.5'/%3E%3C/svg%3E\");\n      background-repeat: no-repeat;\n      background-position: right 8px center;\n    }\n    .review-filters select:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }\n    .review-cards { display: flex; flex-direction: column; gap: 12px; }\n    .review-card {\n      background: var(--surface-raised);\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      padding: 0;\n      box-shadow: var(--shadow-sm);\n      transition: border-color .2s, box-shadow .2s;\n      overflow: hidden;\n    }\n    .review-card:hover {\n      border-color: color-mix(in srgb, var(--accent) 30%, var(--border));\n      box-shadow: var(--shadow), 0 0 0 1px var(--accent-dim);\n    }\n    .review-card-inner {\n      padding: 16px 18px;\n    }\n    .review-card-header {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      margin-bottom: 10px;\n      flex-wrap: wrap;\n    }\n    .review-card-text {\n      font-size: var(--text-base);\n      line-height: 1.65;\n      margin-bottom: 12px;\n      color: var(--ink);\n    }\n    .review-card-text code {\n      background: var(--surface-sunken);\n      border: 1px solid var(--border);\n      padding: 0 4px;\n      border-radius: 4px;\n      font-size: var(--text-sm);\n      font-family: var(--mono);\n    }\n    .review-card-text p { margin: 0 0 8px; }\n    .review-card-text p:last-child { margin-bottom: 0; }\n    .review-card-actions {\n      display: flex;\n      gap: 8px;\n      align-items: center;\n      padding-top: 10px;\n      border-top: 1px solid var(--border-light);\n    }\n    .review-card-edit {\n      margin-top: 12px;\n      padding-top: 12px;\n      border-top: 1px solid var(--border-light);\n    }\n    .review-edit-textarea {\n      width: 100%;\n      min-height: 80px;\n      padding: 12px 16px;\n      border: 1px solid var(--border);\n      border-radius: var(--radius-sm);\n      font-size: var(--text-base);\n      font-family: var(--font);\n      line-height: 1.6;\n      resize: vertical;\n      background: var(--surface-sunken);\n      color: var(--ink);\n      transition: border-color .15s;\n    }\n    .review-edit-textarea:focus {\n      outline: none;\n      border-color: var(--accent);\n      box-shadow: 0 0 0 3px var(--accent-dim);\n    }\n    .review-help {\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      background: var(--surface);\n      overflow: hidden;\n    }\n    .review-help summary {\n      cursor: pointer;\n      font-size: var(--text-sm);\n      font-weight: 600;\n      color: var(--muted);\n      padding: 12px 16px;\n      letter-spacing: .02em;\n      text-transform: uppercase;\n    }\n    .review-help dl { margin: 0; padding: 0 16px 16px; font-size: var(--text-base); }\n    .review-help dt { font-weight: 600; margin-top: 12px; color: var(--ink-secondary); font-size: var(--text-base); }\n    .review-help dd { margin: 4px 0 0 0; color: var(--muted); line-height: 1.55; font-size: var(--text-base); }\n    .review-help dd code {\n      background: var(--surface-sunken);\n      border: 1px solid var(--border);\n      padding: 0 4px;\n      border-radius: 4px;\n      font-size: var(--text-sm);\n      font-family: var(--mono);\n    }\n    .panes { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 16px; }\n    .card ul { margin: 0; padding-left: 0; list-style: none; max-height: 220px; overflow: auto; font-size: var(--text-sm); }\n    .card li { padding: 8px 0; color: var(--muted); border-bottom: 1px solid var(--border-light); line-height: 1.5; font-size: var(--text-sm); }\n    .card li:last-child { border-bottom: none; }\n\n    /* ── Star button ─────────────────────────────────────────── */\n    .star-btn {\n      position: absolute;\n      top: 12px;\n      right: 12px;\n      background: none;\n      border: none;\n      font-size: var(--text-md);\n      cursor: pointer;\n      color: var(--border);\n      transition: color .15s;\n      padding: 4px;\n      line-height: 1;\n    }\n    .star-btn:hover { color: var(--warning); }\n    .star-btn.starred { color: var(--warning); }\n\n    /* ── Project search ──────────────────────────────────────── */\n    .projects-search {\n      width: 100%;\n      padding: 12px 16px;\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      font-size: var(--text-base);\n      font-family: var(--font);\n      margin-bottom: 16px;\n      background: var(--surface);\n      outline: none;\n      transition: border-color .15s;\n    }\n    .projects-search:focus { border-color: var(--accent); }\n\n    /* ── GitHub link ──────────────────────────────────────────── */\n    .github-link {\n      font-size: var(--text-sm);\n      color: var(--muted);\n      text-decoration: none;\n      display: inline-flex;\n      align-items: center;\n      gap: 4px;\n    }\n    .github-link:hover { color: var(--ink); }\n\n    /* ── Graph Tab ───────────────────────────────────────────── */\n    .graph-container {\n      background: #0d0e0c;\n      border-radius: var(--radius);\n      border: 1px solid var(--border);\n      overflow: hidden;\n      position: relative;\n    }\n    #graph-canvas {\n      width: 100%;\n      height: calc(100vh - 160px);\n      min-height: 800px;\n      display: block;\n      cursor: grab;\n    }\n    #graph-canvas:active { cursor: grabbing; }\n    .graph-controls {\n      position: absolute;\n      top: 12px;\n      right: 12px;\n      display: flex;\n      flex-direction: column;\n      gap: 6px;\n      z-index: 2;\n    }\n    .graph-controls button {\n      width: 36px;\n      height: 36px;\n      background: rgba(255,255,255,.1);\n      border: 1px solid rgba(255,255,255,.15);\n      border-radius: 6px;\n      color: #e2e8f0;\n      font-size: var(--text-md);\n      cursor: pointer;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      backdrop-filter: blur(8px);\n      transition: background .15s;\n    }\n    .graph-controls button:hover { background: rgba(255,255,255,.2); }\n    .graph-legend {\n      display: flex;\n      gap: 24px;\n      padding: 14px 20px;\n      background: rgba(10,11,8,.85);\n      border-top: 1px solid rgba(255,255,255,.1);\n      backdrop-filter: blur(8px);\n    }\n    .graph-legend-item {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      color: #94a3b8;\n      font-size: var(--text-sm);\n      font-weight: 500;\n    }\n    .graph-legend-dot {\n      width: 12px;\n      height: 12px;\n      border-radius: 50%;\n      display: inline-block;\n    }\n    .graph-tooltip {\n      position: absolute;\n      background: rgba(10,11,8,.95);\n      color: #e2e8f0;\n      padding: 8px 12px;\n      border-radius: 6px;\n      font-size: var(--text-sm);\n      max-width: 300px;\n      pointer-events: none;\n      opacity: 0;\n      transition: opacity .15s;\n      border: 1px solid rgba(255,255,255,.1);\n      line-height: 1.4;\n      z-index: 10;\n    }\n    .graph-tooltip.visible { opacity: 1; }\n    .graph-filters {\n      position: absolute;\n      top: 12px;\n      left: 12px;\n      right: 52px;\n      display: flex;\n      flex-direction: column;\n      gap: 10px;\n      padding: 10px 14px;\n      background: var(--surface);\n      border: 1px solid var(--border);\n      border-radius: 8px;\n      backdrop-filter: blur(8px);\n    }\n    .graph-filter {\n      display: flex;\n      gap: 8px;\n      flex-wrap: wrap;\n      align-items: center;\n    }\n    .graph-filter-btn {\n      padding: 4px 12px;\n      background: rgba(255,255,255,.08);\n      border: 1px solid rgba(255,255,255,.12);\n      border-radius: 20px;\n      color: #94a3b8;\n      font-size: var(--text-xs);\n      font-weight: 500;\n      cursor: pointer;\n      transition: all .15s;\n      font-family: var(--font);\n    }\n    .graph-filter-btn:hover, .graph-filter-btn.active {\n      background: rgba(255,255,255,.15);\n      color: #e2e8f0;\n    }\n\n    /* ── Two-pane (Skills/Hooks) ─────────────────────────────── */\n    .split-view {\n      display: grid;\n      grid-template-columns: 280px 1fr;\n      gap: 0;\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      background: var(--surface);\n      height: calc(100vh - 140px);\n      min-height: 520px;\n      overflow: hidden;\n    }\n    .split-sidebar {\n      border-right: 1px solid var(--border);\n      overflow-y: auto;\n      background: var(--surface-sunken);\n    }\n    .split-group-label {\n      padding: 8px 16px;\n      font-size: var(--text-xs);\n      text-transform: uppercase;\n      letter-spacing: .06em;\n      color: var(--muted);\n      font-weight: 600;\n      background: var(--surface-sunken);\n      border-bottom: 1px solid var(--border);\n      position: sticky;\n      top: 0;\n    }\n    .split-item {\n      padding: 12px 16px;\n      cursor: pointer;\n      border-bottom: 1px solid var(--border-light);\n      font-size: var(--text-base);\n      transition: background .1s;\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n    }\n    .split-item:hover { background: var(--surface-raised); }\n    .split-item.selected { background: var(--accent-dim); border-left: 3px solid var(--accent); padding-left: 12px; }\n    .split-reader {\n      display: flex;\n      flex-direction: column;\n    }\n    .reader-toolbar {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      padding: 12px 16px;\n      border-bottom: 1px solid var(--border);\n      background: var(--surface-sunken);\n    }\n    .reader-title { font-weight: 650; font-size: var(--text-base); flex-shrink: 0; }\n    .reader-path {\n      font-size: var(--text-xs);\n      color: var(--muted);\n      flex: 1;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      font-family: var(--mono);\n    }\n    .reader-content {\n      flex: 1;\n      overflow: auto;\n      display: flex;\n      flex-direction: column;\n      min-height: 0;\n    }\n    .reader-content pre {\n      margin: 0;\n      padding: 20px;\n      font-size: var(--text-sm);\n      line-height: 1.7;\n      font-family: var(--mono);\n      white-space: pre-wrap;\n      word-break: break-word;\n    }\n    .reader-content textarea {\n      flex: 1;\n      width: 100%;\n      min-height: 300px;\n      padding: 20px;\n      font-size: var(--text-sm);\n      line-height: 1.7;\n      font-family: var(--mono);\n      border: none;\n      outline: none;\n      resize: none;\n    }\n    .reader-empty {\n      padding: 60px 20px;\n      text-align: center;\n      color: var(--muted);\n      font-size: var(--text-base);\n    }\n\n    /* Hook items */\n    .hook-item {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      padding: 12px 16px;\n      border-bottom: 1px solid var(--border-light);\n      cursor: pointer;\n      transition: background .1s;\n    }\n    .hook-item:hover { background: var(--surface-raised); }\n    .hook-item.selected { background: var(--accent-dim); border-left: 3px solid var(--accent); padding-left: 12px; }\n    .hook-name { flex: 1; font-size: var(--text-base); font-weight: 500; }\n    .hook-custom-event { font-size: var(--text-sm); font-weight: 600; color: var(--ink); }\n    .hook-custom-cmd { font-size: var(--text-xs); color: var(--muted); word-break: break-all; margin-top: 4px; }\n\n    /* ── Badges & Buttons ────────────────────────────────────── */\n    .badge {\n      display: inline-flex;\n      align-items: center;\n      padding: 2px 8px;\n      border-radius: 6px;\n      font-size: var(--text-xs);\n      font-weight: 600;\n      background: var(--surface-sunken);\n      color: var(--muted);\n      letter-spacing: .02em;\n      text-transform: uppercase;\n    }\n    .badge-project { background: var(--purple-dim); color: var(--purple); }\n    .badge-on { background: var(--success-dim); color: var(--success); }\n    .badge-off { background: var(--danger-dim); color: var(--danger); }\n    .badge-count { background: var(--accent); color: white; min-width: 20px; text-align: center; border-radius: 10px; }\n    .badge-machine { background: var(--accent-dim); color: var(--accent); }\n    .badge-model { background: rgba(245,158,11,.1); color: #d97706; }\n\n    .btn {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      gap: 8px;\n      padding: 8px 16px;\n      border-radius: var(--radius-sm);\n      font-size: var(--text-sm);\n      font-weight: 550;\n      font-family: var(--font);\n      cursor: pointer;\n      transition: all .15s ease;\n      border: 1px solid var(--border);\n      background: var(--surface);\n      color: var(--ink-secondary);\n      letter-spacing: -0.01em;\n    }\n    .btn:hover { background: var(--surface-sunken); color: var(--ink); }\n    .btn:active { transform: scale(.97); }\n    .btn-primary { background: var(--accent); color: white; border-color: transparent; }\n    .btn-primary:hover { background: var(--accent-hover); }\n    .btn-approve {\n      background: var(--success-dim);\n      color: var(--success);\n      border-color: transparent;\n      font-weight: 600;\n    }\n    .btn-approve:hover { background: var(--success); color: white; }\n    .btn-reject {\n      background: transparent;\n      color: var(--muted);\n      border-color: var(--border);\n    }\n    .btn-reject:hover { background: var(--danger-dim); color: var(--danger); border-color: var(--danger); }\n    .btn-sm { padding: 4px 12px; font-size: var(--text-xs); }\n\n    kbd {\n      background: var(--surface-sunken);\n      border: 1px solid var(--border);\n      border-radius: 4px;\n      padding: 0 8px;\n      font-size: var(--text-xs);\n      font-family: var(--mono);\n      font-weight: 550;\n      color: var(--muted);\n      line-height: 1;\n    }\n\n    .text-muted { color: var(--muted); }\n    .status-msg { font-size: var(--text-sm); padding: 4px 8px; border-radius: var(--radius-sm); }\n    .status-msg.ok { background: var(--success-dim); color: var(--green); }\n    .status-msg.err { background: var(--danger-dim); color: var(--red); }\n\n    /* ── Status LED ──────────────────────────────────────────────── */\n    .status-led {\n      width: 8px;\n      height: 8px;\n      border-radius: 50%;\n      display: inline-block;\n      vertical-align: middle;\n      margin-right: 8px;\n      flex-shrink: 0;\n    }\n    .status-led-ok {\n      background: #22c55e;\n      color: #22c55e;\n      animation: ledPulse 2.5s infinite;\n    }\n    .status-led-warn {\n      background: #f59e0b;\n      color: #f59e0b;\n      animation: ledPulse 1.2s infinite;\n    }\n    .status-led-err {\n      background: #ef4444;\n      color: #ef4444;\n      animation: ledPulse 0.6s infinite;\n    }\n    @keyframes ledPulse {\n      0% { opacity: 1; box-shadow: 0 0 0 0 currentColor; }\n      70% { opacity: 0.8; box-shadow: 0 0 0 5px transparent; }\n      100% { opacity: 1; }\n    }\n\n    @media (max-width: 900px) {\n      .projects-grid { grid-template-columns: 1fr; }\n      .split-view { grid-template-columns: 1fr; }\n      .panes { grid-template-columns: 1fr; }\n      .header { padding: 0 12px; gap: 12px; }\n      .main { padding: 16px; }\n    }\n\n    ::view-transition-old(root), ::view-transition-new(root) {\n      animation-duration: 0.18s;\n    }\n\n    @keyframes cardIn {\n      from { opacity: 0; transform: translateY(8px); }\n      to { opacity: 1; transform: translateY(0); }\n    }\n    .review-card {\n      animation: cardIn 0.3s cubic-bezier(.21,1.02,.73,1) backwards;\n    }\n    .review-card.removing {\n      animation: cardOut 0.25s ease forwards;\n      pointer-events: none;\n    }\n    @keyframes cardOut {\n      to { opacity: 0; transform: translateY(-6px) scale(.98); margin-top: -4px; padding: 0; }\n    }\n\n    /* ── Batch actions ───────────────────────────────────────────── */\n    .review-card-check {\n      position: absolute;\n      top: 16px;\n      right: 16px;\n      width: 18px;\n      height: 18px;\n      border: 2px solid var(--border);\n      border-radius: 4px;\n      cursor: pointer;\n      background: var(--surface);\n      transition: all .15s;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      z-index: 2;\n    }\n    .review-card-check:hover { border-color: var(--accent); }\n    .review-card-check.checked {\n      background: var(--accent);\n      border-color: var(--accent);\n    }\n    .review-card-check.checked::after {\n      content: '';\n      width: 6px;\n      height: 10px;\n      border: solid white;\n      border-width: 0 2px 2px 0;\n      transform: rotate(45deg);\n      margin-top: -2px;\n    }\n    .review-card { position: relative; }\n    .batch-bar {\n      position: fixed;\n      bottom: 24px;\n      left: 50%;\n      transform: translateX(-50%) translateY(80px);\n      background: var(--surface-raised, #1a2233);\n      border: 1px solid var(--border);\n      border-radius: 12px;\n      padding: 12px 16px;\n      display: flex;\n      align-items: center;\n      gap: 12px;\n      box-shadow: 0 8px 32px rgba(0,0,0,.2);\n      z-index: 500;\n      transition: transform .3s cubic-bezier(.21,1.02,.73,1);\n      pointer-events: all;\n    }\n    .batch-bar.visible {\n      transform: translateX(-50%) translateY(0);\n    }\n    .batch-bar-count {\n      font-size: var(--text-base);\n      font-weight: 600;\n      color: var(--ink);\n      min-width: 80px;\n    }\n\n    .review-banner-pill {\n      display: inline-flex;\n      align-items: center;\n      gap: 4px;\n      padding: 2px 10px;\n      border-radius: 99px;\n      font-size: var(--text-xs);\n      font-weight: 600;\n      background: var(--surface-raised);\n      border: 1px solid var(--border);\n      color: var(--ink);\n    }\n    .review-banner-pill.rp-danger {\n      background: rgba(239,68,68,.12);\n      border-color: rgba(239,68,68,.3);\n      color: #f87171;\n    }\n    .review-banner-pill.rp-warn {\n      background: rgba(251,191,36,.10);\n      border-color: rgba(251,191,36,.3);\n      color: #fbbf24;\n    }\n\n    /* ── Diff view ───────────────────────────────────────────────── */\n    .review-diff {\n      display: grid;\n      grid-template-columns: 1fr 1fr;\n      gap: 1px;\n      background: var(--border);\n      border: 1px solid var(--border);\n      border-radius: var(--radius-sm);\n      overflow: hidden;\n      margin-bottom: 8px;\n      font-size: var(--text-sm);\n      font-family: var(--mono);\n      line-height: 1.6;\n    }\n    .review-diff-pane {\n      padding: 12px;\n      background: var(--surface-sunken);\n      white-space: pre-wrap;\n      word-break: break-word;\n      min-height: 60px;\n    }\n    .review-diff-pane-label {\n      font-size: var(--text-xs);\n      font-weight: 600;\n      text-transform: uppercase;\n      letter-spacing: .05em;\n      color: var(--muted);\n      margin-bottom: 8px;\n      font-family: var(--font);\n    }\n    .diff-del { background: var(--danger-dim); color: var(--danger); text-decoration: line-through; }\n    .diff-ins { background: var(--success-dim); color: var(--success); }\n\n    /* ── Drag reorder ────────────────────────────────────────────── */\n    .review-card.dragging {\n      opacity: .5;\n      transform: scale(.98);\n      z-index: 10;\n    }\n    .review-card.drag-over {\n      border-top: 2px solid var(--accent);\n      margin-top: -1px;\n    }\n    .review-card-drag-handle {\n      width: 16px;\n      height: 16px;\n      cursor: grab;\n      color: var(--border);\n      transition: color .15s;\n      flex-shrink: 0;\n      margin-right: 4px;\n    }\n    .review-card-drag-handle:hover { color: var(--muted); }\n    .review-card-drag-handle:active { cursor: grabbing; }\n\n\n    /* ── Similarity groups ────────────────────────────────────────── */\n    .review-group {\n      border: 1px solid var(--border);\n      border-radius: var(--radius);\n      padding: 8px;\n      margin-bottom: 12px;\n      background: color-mix(in srgb, var(--surface) 95%, var(--accent) 5%);\n    }\n    .review-group .review-card { margin-bottom: 8px; }\n    .review-group .review-card:last-child { margin-bottom: 0; }\n    .review-group-header {\n      font-size: var(--text-xs);\n      color: var(--muted);\n      margin-bottom: 8px;\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      padding: 0 4px;\n    }\n    .review-group-toggle {\n      background: none;\n      border: none;\n      color: var(--muted);\n      cursor: pointer;\n      font-size: var(--text-xs);\n      padding: 0 4px;\n    }\n    .review-group-toggle:hover { color: var(--ink-secondary); }\n    .review-group.collapsed .review-card:not(:first-child) { display: none; }\n\n    /* ── Select all ──────────────────────────────────────────────── */\n    .review-select-all {\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      font-size: var(--text-sm);\n      color: var(--muted);\n      cursor: pointer;\n      user-select: none;\n      margin-bottom: 8px;\n    }\n    .review-select-all input[type=\"checkbox\"] {\n      width: 14px;\n      height: 14px;\n      cursor: pointer;\n      accent-color: var(--accent);\n    }\n    /* ── Toast ───────────────────────────────────────────────────── */\n    .toast-container {\n      position: fixed;\n      bottom: 24px;\n      left: 50%;\n      transform: translateX(-50%);\n      z-index: 999;\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      gap: 8px;\n      pointer-events: none;\n    }\n    .toast {\n      display: flex;\n      align-items: center;\n      gap: 12px;\n      padding: 12px 16px;\n      background: var(--surface-raised, #1a2233);\n      color: var(--ink, #e8ecf4);\n      border: 1px solid var(--border, #1e2a3e);\n      border-radius: var(--radius);\n      font-size: var(--text-base);\n      font-weight: 500;\n      box-shadow: 0 8px 32px rgba(0,0,0,.2);\n      pointer-events: all;\n      animation: toastIn 0.25s cubic-bezier(.21,1.02,.73,1);\n      max-width: 420px;\n      backdrop-filter: blur(12px);\n    }\n    .toast.ok { border-color: var(--success); background: var(--success-dim); color: var(--success); }\n    .toast.err { border-color: var(--danger); background: var(--danger-dim); color: var(--danger); }\n    @keyframes toastIn {\n      from { opacity: 0; transform: translateY(12px) scale(.96); }\n      to { opacity: 1; transform: translateY(0) scale(1); }\n    }\n    .toast-undo {\n      background: none;\n      border: 1px solid currentColor;\n      color: inherit;\n      border-radius: var(--radius-sm);\n      padding: 4px 12px;\n      font-size: var(--text-xs);\n      font-weight: 600;\n      cursor: pointer;\n      font-family: var(--font);\n      opacity: .8;\n      transition: opacity .15s;\n    }\n    .toast-undo:hover { opacity: 1; }\n\n    /* ── Command Palette ─────────────────────────────────────────── */\n    .cmdpal-overlay {\n      position: fixed;\n      inset: 0;\n      background: rgba(0,0,0,.4);\n      backdrop-filter: blur(4px);\n      z-index: 900;\n      display: none;\n      align-items: flex-start;\n      justify-content: center;\n      padding-top: 14vh;\n    }\n    .cmdpal-overlay.open { display: flex; }\n    .cmdpal-box {\n      background: var(--surface-raised);\n      border: 1px solid var(--border);\n      border-radius: 12px;\n      width: 100%;\n      max-width: 520px;\n      box-shadow: 0 24px 80px rgba(0,0,0,.25);\n      overflow: hidden;\n    }\n    .cmdpal-input {\n      width: 100%;\n      padding: 16px 20px;\n      font-size: var(--text-md);\n      font-family: var(--font);\n      border: none;\n      outline: none;\n      background: transparent;\n      color: var(--ink);\n      border-bottom: 1px solid var(--border);\n      font-weight: 450;\n    }\n    .cmdpal-input::placeholder { color: var(--muted); }\n    .cmdpal-results { max-height: 340px; overflow-y: auto; }\n    .cmdpal-item {\n      padding: 12px 20px;\n      cursor: pointer;\n      font-size: var(--text-base);\n      display: flex;\n      align-items: center;\n      gap: 8px;\n      transition: background .1s;\n    }\n    .cmdpal-item:hover, .cmdpal-item.selected { background: var(--accent-dim); }\n    .cmdpal-item-name { font-weight: 550; color: var(--ink); }\n    .cmdpal-item-meta { font-size: var(--text-xs); color: var(--muted); margin-left: auto; font-weight: 500; }\n    .cmdpal-empty { padding: 32px 20px; text-align: center; color: var(--muted); font-size: var(--text-base); }\n    @keyframes countPop {\n      0% { transform: scale(1); }\n      50% { transform: scale(1.3); }\n      100% { transform: scale(1); }\n    }\n    @keyframes countFlip {\n      0% { transform: scale(1) rotateX(0); }\n      40% { transform: scale(1.3) rotateX(90deg); }\n      60% { transform: scale(1.3) rotateX(-10deg); }\n      100% { transform: scale(1) rotateX(0); }\n    }\n    .count-animating {\n      animation: countPop 0.3s ease-out;\n    }\n    .count-flipping {\n      animation: countFlip 0.3s ease-out;\n    }";
const WEB_UI_SCRIPT_TEMPLATE = "(function() {\n  // ── State ────────────────────────────────────────────────────\n  var _authToken = '__PHREN_AUTH_TOKEN_1d7b7c99__';\n  var _skillsLoaded = false, _hooksLoaded = false, _graphLoaded = false;\n  var _currentSkillPath = null, _currentHookPath = null;\n  var _currentSkillEnabled = true, _currentSkillSource = '', _currentSkillName = '';\n  var _editingSkill = false, _editingHook = false;\n  var _selectedProject = null;\n  var _scoresPromise = null;\n\n  var _activeFetches = 0;\n  function setLed(state) {\n    var led = document.getElementById('sync-led');\n    if (!led) return;\n    led.className = 'status-led status-led-' + state;\n    led.title = state === 'ok' ? 'Synced' : state === 'warn' ? 'Working...' : 'Error';\n  }\n  function ledFetch(url, opts) {\n    _activeFetches++;\n    setLed('warn');\n    return fetch(url, opts).then(function(r) {\n      _activeFetches--;\n      if (_activeFetches <= 0) { _activeFetches = 0; setLed('ok'); }\n      return r;\n    }).catch(function(err) {\n      _activeFetches--;\n      if (_activeFetches <= 0) { _activeFetches = 0; setLed('err'); }\n      setTimeout(function() { if (_activeFetches <= 0) setLed('ok'); }, 3000);\n      throw err;\n    });\n  }\n\n  // ── Tab switching ────────────────────────────────────────────\n  window.switchTab = function(tab) {\n    function doSwitch() {\n      document.querySelectorAll('.tab-content').forEach(function(el) { el.classList.remove('active'); });\n      document.querySelectorAll('.nav-item').forEach(function(el) { el.classList.remove('active'); });\n      var tabEl = document.getElementById('tab-' + tab);\n      if (tabEl) tabEl.classList.add('active');\n      var navBtn = document.querySelector('.nav-item[data-tab=\"' + tab + '\"]');\n      if (navBtn) navBtn.classList.add('active');\n      if (tab === 'projects' && !document.querySelector('.project-card')) loadProjects();\n      if (tab === 'review') { loadReviewCards(); loadReviewActivity(); }\n      if (tab === 'skills' && !_skillsLoaded) loadSkills();\n      if (tab === 'hooks' && !_hooksLoaded) loadHooks();\n      if (tab === 'graph' && !_graphLoaded) loadGraph();\n    }\n    if (document.startViewTransition) {\n      document.startViewTransition(doSwitch);\n    } else {\n      doSwitch();\n    }\n  };\n\n  // ── Projects ─────────────────────────────────────────────────\n  function getStarredProjects() {\n    try { return JSON.parse(localStorage.getItem('phren-starred-projects') || '[]'); } catch { return []; }\n  }\n  function setStarredProjects(arr) {\n    localStorage.setItem('phren-starred-projects', JSON.stringify(arr));\n  }\n\n  function renderProjectCards(data) {\n    var grid = document.getElementById('projects-grid');\n    if (!data.length) {\n      grid.innerHTML = '<div style=\"padding:60px;color:var(--muted);grid-column:1/-1;text-align:center\"><svg viewBox=\"0 0 80 80\" width=\"80\" height=\"80\" fill=\"none\" style=\"margin:0 auto 12px;display:block\"><path d=\"M18 30h44v26a4 4 0 01-4 4H22a4 4 0 01-4-4V30z\" stroke=\"var(--accent)\" stroke-width=\"2\" fill=\"var(--accent-dim)\"/><path d=\"M18 30l6-10h16l6 10\" stroke=\"var(--accent)\" stroke-width=\"2\" fill=\"none\" stroke-linejoin=\"round\"/></svg><div style=\"font-weight:500;margin-bottom:4px\">No projects yet</div><div style=\"font-size:var(--text-sm)\">Run <code>phren init</code> to create one.</div></div>';\n      return;\n    }\n    var starred = getStarredProjects();\n    // Sort: starred first, then by activity\n    var sorted = data.slice().sort(function(a, b) {\n      var aStarred = starred.indexOf(a.name) !== -1 ? 1 : 0;\n      var bStarred = starred.indexOf(b.name) !== -1 ? 1 : 0;\n      if (aStarred !== bStarred) return bStarred - aStarred;\n      return 0; // preserve server sort order\n    });\n    grid.innerHTML = sorted.map(function(p) {\n      var isStarred = starred.indexOf(p.name) !== -1;\n      var githubHtml = p.githubUrl ? '<a class=\"github-link\" href=\"'+esc(p.githubUrl)+'\" target=\"_blank\" rel=\"noopener\" data-ui-action=\"stopProp\">GitHub</a>' : '';\n      var sparkHtml = '';\n      if (p.sparkline && p.sparkline.some(function(v) { return v > 0; })) {\n        var sp = p.sparkline;\n        var max = Math.max.apply(null, sp) || 1;\n        var w = 80, h = 20;\n        var pts = sp.map(function(v, i) { return (i * w / (sp.length - 1)).toFixed(1) + ',' + (h - (v / max) * (h - 2)).toFixed(1); }).join(' ');\n        sparkHtml = '<svg class=\"project-sparkline\" width=\"' + w + '\" height=\"' + h + '\" viewBox=\"0 0 ' + w + ' ' + h + '\" style=\"margin-top:8px;display:block\"><polyline points=\"' + pts + '\" fill=\"none\" stroke=\"var(--accent)\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" opacity=\".7\" /><polyline points=\"0,' + h + ' ' + pts + ' ' + w + ',' + h + '\" fill=\"var(--accent-dim)\" stroke=\"none\" /></svg>';\n      }\n      return '<div class=\"project-card\" data-ui-action=\"selectProject\" data-project=\"'+esc(p.name)+'\" data-summary=\"'+esc(p.summaryText || '')+'\">' +\n        '<button class=\"star-btn'+(isStarred ? ' starred' : '')+'\" data-ui-action=\"toggleStar\" title=\"Star project\">&#9733;</button>' +\n        '<div class=\"project-card-name\">' + esc(p.name) + '</div>' +\n        (p.summaryText ? '<div class=\"project-card-summary\">' + esc(p.summaryText) + '</div>' : '<div class=\"project-card-summary\" style=\"font-style:italic\">No summary</div>') +\n        '<div class=\"project-card-stats\">' +\n          '<span class=\"project-card-stat\"><strong>' + p.findingCount + '</strong> findings</span>' +\n          '<span class=\"project-card-stat\"><strong>' + p.taskCount + '</strong> task</span>' +\n          (p.hasClaudeMd ? '<span class=\"project-card-stat\">AGENTS.md</span>' : '') +\n          (p.hasReference ? '<span class=\"project-card-stat\">reference/</span>' : '') +\n          githubHtml +\n        '</div>' +\n        sparkHtml +\n      '</div>';\n    }).join('');\n  }\n\n  var _projectData = [];\n\n  function loadProjects() {\n    fetch(authUrl('/api/projects')).then(function(r) { return r.json(); }).then(function(data) {\n      _projectData = data;\n      renderProjectCards(data);\n      if (_selectedProject) {\n        var cards = document.querySelectorAll('.project-card');\n        for (var ci = 0; ci < cards.length; ci++) {\n          if (cards[ci].getAttribute('data-project') === _selectedProject) { cards[ci].classList.add('selected'); break; }\n        }\n      }\n    });\n  }\n\n  // ── Review queue (live) ───────────────────────────────────────\n  var _reviewData = [];\n  var _reviewCardKeys = new Set(); // tracks project+line keys currently in DOM\n\n  function cardKey(item) { return item.project + '\\\\x00' + item.line; }\n\n  function renderReviewCard(item, delayMs) {\n    var key = cardKey(item);\n    var projectBadge = '<span class=\"badge badge-project\">' + esc(item.project) + '</span>';\n    var sectionBadge = '<span class=\"badge\">' + esc(item.section) + '</span>';\n    var machineBadge = item.machine ? '<span class=\"badge badge-machine\" title=\"Machine: ' + esc(item.machine) + '\">' + esc(item.machine) + '</span>' : '';\n    var modelBadge = (item.model && item.model !== 'unknown') ? '<span class=\"badge badge-model\" title=\"Model: ' + esc(item.model) + '\">' + esc(item.model) + '</span>' : '';\n    var dateSpan = '<span class=\"text-muted\" style=\"font-size:var(--text-sm);margin-left:auto\">' + esc(item.date) + '</span>';\n\n    var cardText = esc(item.text);\n\n    var div = document.createElement('div');\n    div.className = 'review-card';\n    div.setAttribute('data-key', key);\n    div.setAttribute('data-project', item.project);\n    div.setAttribute('data-machine', item.machine || '');\n    div.setAttribute('data-model', item.model || '');\n    div.style.animationDelay = delayMs + 'ms';\n    div.innerHTML =\n      '<div class=\"review-card-inner\">' +\n        '<div class=\"review-card-header\">' +\n          projectBadge + sectionBadge + dateSpan +\n        '</div>' +\n        '<div class=\"review-card-text\">' + cardText + '</div>' +\n        (machineBadge || modelBadge ? '<div style=\"display:flex;gap:6px;align-items:center;margin-bottom:8px\">' + machineBadge + modelBadge + '</div>' : '') +\n        '<div class=\"review-card-actions\">' +\n          '<button type=\"button\" class=\"btn btn-sm btn-approve\" data-ui-action=\"reviewAction\" data-review-type=\"approve\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\"><path d=\"M3 8.5l3.5 3.5 6.5-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg> Approve</button>' +\n          '<button type=\"button\" class=\"btn btn-sm btn-reject\" data-ui-action=\"reviewAction\" data-review-type=\"reject\"><svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\"><path d=\"M4 4l8 8M12 4l-8 8\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"/></svg> Dismiss</button>' +\n          '<button type=\"button\" class=\"btn btn-sm\" data-ui-action=\"toggleReviewEdit\" style=\"margin-left:auto\">Edit</button>' +\n        '</div>' +\n        '<div class=\"review-card-edit\" style=\"display:none\">' +\n          '<form data-ui-action=\"reviewEditSubmit\">' +\n            '<textarea name=\"new_text\" class=\"review-edit-textarea\">' + cardText + '</textarea>' +\n            '<div style=\"display:flex;gap:8px;margin-top:8px\">' +\n              '<button type=\"submit\" class=\"btn btn-sm btn-primary\">Save</button>' +\n              '<button type=\"button\" class=\"btn btn-sm\" data-ui-action=\"toggleReviewEdit\">Cancel</button>' +\n            '</div>' +\n          '</form>' +\n        '</div>' +\n      '</div>';\n\n    var approveBtn = div.querySelector('.btn-approve');\n    if (approveBtn) {\n      approveBtn.setAttribute('data-project', item.project);\n      approveBtn.setAttribute('data-line', item.line);\n    }\n    var rejectBtn = div.querySelector('.btn-reject');\n    if (rejectBtn) {\n      rejectBtn.setAttribute('data-project', item.project);\n      rejectBtn.setAttribute('data-line', item.line);\n    }\n    var editForm = div.querySelector('.review-card-edit form');\n    if (editForm) {\n      editForm.setAttribute('data-project', item.project);\n      editForm.setAttribute('data-line', item.line);\n    }\n\n    // Render queue text as escaped plain text; review items are not trusted markdown/HTML.\n    var textEl = div.querySelector('.review-card-text');\n    if (textEl) {\n      textEl.innerHTML = esc(item.text).replace(/\\n/g, '<br>');\n    }\n\n    // Add batch checkbox\n    var checkbox = document.createElement('div');\n    checkbox.className = 'review-card-check';\n    checkbox.onclick = function(e) {\n      e.stopPropagation();\n      this.classList.toggle('checked');\n      updateBatchBar();\n    };\n    div.querySelector('.review-card-inner').appendChild(checkbox);\n\n    // Drag reorder\n    div.setAttribute('draggable', 'true');\n    var handle = div.querySelector('.review-card-drag-handle');\n    if (handle) {\n      handle.addEventListener('mousedown', function() { div.setAttribute('draggable', 'true'); });\n    }\n    div.addEventListener('dragstart', function(e) {\n      div.classList.add('dragging');\n      e.dataTransfer.effectAllowed = 'move';\n      e.dataTransfer.setData('text/plain', key);\n    });\n    div.addEventListener('dragend', function() {\n      div.classList.remove('dragging');\n      document.querySelectorAll('.review-card.drag-over').forEach(function(c) { c.classList.remove('drag-over'); });\n    });\n    div.addEventListener('dragover', function(e) {\n      e.preventDefault();\n      e.dataTransfer.dropEffect = 'move';\n      var dragging = document.querySelector('.review-card.dragging');\n      if (dragging && dragging !== div) {\n        div.classList.add('drag-over');\n      }\n    });\n    div.addEventListener('dragleave', function() {\n      div.classList.remove('drag-over');\n    });\n    div.addEventListener('drop', function(e) {\n      e.preventDefault();\n      div.classList.remove('drag-over');\n      var dragging = document.querySelector('.review-card.dragging');\n      if (dragging && dragging !== div) {\n        var list = div.parentNode;\n        var cards = Array.from(list.querySelectorAll('.review-card'));\n        var dragIdx = cards.indexOf(dragging);\n        var dropIdx = cards.indexOf(div);\n        if (dragIdx < dropIdx) {\n          list.insertBefore(dragging, div.nextSibling);\n        } else {\n          list.insertBefore(dragging, div);\n        }\n      }\n    });\n\n    return div;\n  }\n\n  function loadReviewCards() {\n    var url = '/api/review-queue' + (_authToken ? '?_auth=' + encodeURIComponent(_authToken) : '');\n    fetch(url).then(function(r) { return r.json(); }).then(function(data) {\n      _reviewData = data;\n      var list = document.getElementById('review-cards-list');\n      if (!list) return;\n\n      // Build set of incoming keys\n      var incomingKeys = new Set(data.map(cardKey));\n\n      // Remove cards no longer in queue (animate out)\n      var existing = list.querySelectorAll('.review-card[data-key]');\n      existing.forEach(function(card) {\n        var key = card.getAttribute('data-key');\n        if (!incomingKeys.has(key)) {\n          var nextSibling = card.nextSibling; var parentNode = card.parentNode; card.remove(); updateReviewCount(-1); if (parentNode && parentNode.classList && parentNode.classList.contains('review-group') && !parentNode.querySelector('.review-card')) { parentNode.remove(); }\n          setTimeout(function() { if (card.parentNode) card.parentNode.removeChild(card); }, 300);\n          _reviewCardKeys.delete(key);\n        }\n      });\n\n      // Add new cards with similarity grouping (animate in with stagger)\n      var newItems = data.filter(function(item) { return !_reviewCardKeys.has(cardKey(item)); });\n      if (newItems.length > 0) {\n        // Remove existing groups if full re-render\n        list.querySelectorAll('.review-group').forEach(function(g) { g.remove(); });\n\n        var groups = clusterCards(newItems);\n        var delayBase = 0;\n        groups.forEach(function(groupItems) {\n          if (groupItems.length > 1) {\n            // Multi-item group: wrap in a group container\n            var groupDiv = document.createElement('div');\n            groupDiv.className = 'review-group';\n            var header = document.createElement('div');\n            header.className = 'review-group-header';\n            header.innerHTML = '<span>Similar (' + groupItems.length + ' items)</span><button class=\"review-group-toggle\" data-ui-action=\"toggleGroup\">\\u25BC</button>';\n            groupDiv.appendChild(header);\n            groupItems.forEach(function(item) {\n              var key = cardKey(item);\n              var card = renderReviewCard(item, delayBase);\n              groupDiv.appendChild(card);\n              _reviewCardKeys.add(key);\n              delayBase += 40;\n            });\n            list.appendChild(groupDiv);\n          } else {\n            // Singleton: render normally\n            var item = groupItems[0];\n            var key = cardKey(item);\n            var card = renderReviewCard(item, delayBase);\n            list.appendChild(card);\n            _reviewCardKeys.add(key);\n            delayBase += 40;\n          }\n        });\n      }\n\n      // Show/hide select-all checkbox\n      var selectAllEl = document.getElementById('review-select-all');\n      if (selectAllEl) selectAllEl.style.display = data.length > 0 ? 'flex' : 'none';\n\n      // Empty state\n      var totalVisible = list.querySelectorAll('.review-card:not(.removing)').length;\n      var emptyEl = list.querySelector('.review-cards-empty');\n      if (totalVisible === 0 && newItems.length === 0) {\n        if (!emptyEl) {\n          var empty = document.createElement('div');\n          empty.className = 'review-cards-empty';\n          empty.style.cssText = 'text-align:center;padding:40px;color:var(--muted)';\n          empty.innerHTML = '<svg viewBox=\"0 0 80 80\" width=\"80\" height=\"80\" fill=\"none\" style=\"margin:0 auto 12px;display:block\"><rect x=\"15\" y=\"20\" width=\"50\" height=\"40\" rx=\"4\" stroke=\"var(--accent)\" stroke-width=\"2\" fill=\"var(--accent-dim)\"/><polyline points=\"28,38 36,46 52,32\" stroke=\"var(--accent)\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/></svg><div style=\"font-weight:500;margin-bottom:4px\">All caught up</div><div style=\"font-size:var(--text-sm);color:var(--muted)\">No memories waiting for review.</div>';\n          list.appendChild(empty);\n        }\n      } else if (emptyEl) {\n        emptyEl.remove();\n      }\n\n      // Remove loading spinner\n      var loading = list.querySelector('.review-cards-loading');\n      if (loading) loading.remove();\n\n      // Update nav badge\n      var navBtn = document.querySelector('.nav-item[data-tab=\"review\"] .count');\n      if (data.length > 0) {\n        if (!navBtn) {\n          var btn = document.querySelector('.nav-item[data-tab=\"review\"]');\n          if (btn) {\n            var countSpan = document.createElement('span');\n            countSpan.className = 'count count-animating';\n            countSpan.textContent = String(data.length);\n            btn.appendChild(countSpan);\n          }\n        } else {\n          var oldCount = parseInt(navBtn.textContent, 10) || 0;\n          if (oldCount !== data.length) {\n            animateCount(navBtn, oldCount, data.length);\n            navBtn.classList.remove('count-animating');\n            void navBtn.offsetWidth;\n            navBtn.classList.add('count-animating');\n          }\n        }\n      } else {\n        if (navBtn) navBtn.remove();\n      }\n\n      // Update filters\n      var machines = new Set(), models = new Set(), projects = new Set();\n      data.forEach(function(item) {\n        projects.add(item.project);\n        if (item.machine) machines.add(item.machine);\n        if (item.model && item.model !== 'unknown') models.add(item.model);\n      });\n\n      var filterContainer = document.getElementById('review-filters');\n      if (filterContainer) {\n        filterContainer.style.display = data.length > 0 ? 'flex' : 'none';\n      }\n\n      function updateSelect(id, values, allLabel) {\n        var sel = document.getElementById(id);\n        if (!sel) return;\n        var current = sel.value;\n        sel.innerHTML = '<option value=\"\">' + allLabel + '</option>';\n        Array.from(values).sort().forEach(function(v) {\n          var opt = document.createElement('option');\n          opt.value = v;\n          opt.textContent = v;\n          if (v === current) opt.selected = true;\n          sel.appendChild(opt);\n        });\n      }\n      updateSelect('review-filter-project', projects, 'All projects');\n      updateSelect('review-filter-machine', machines, 'All machines');\n      updateSelect('review-filter-model', models, 'All models');\n\n      // Show/hide keyboard hints\n      var hints = document.getElementById('review-kbd-hints');\n      if (hints) hints.style.display = data.length > 0 ? 'flex' : 'none';\n\n      // Re-apply current filter\n      filterReviewCards();\n    }).catch(function() {\n      var list = document.getElementById('review-cards-list');\n      if (list) {\n        var loading = list.querySelector('.review-cards-loading');\n        if (loading) loading.textContent = 'Failed to load review queue.';\n      }\n    });\n  }\n\n  function loadReviewActivity() {\n    var url = '/api/review-activity' + (_authToken ? '?_auth=' + encodeURIComponent(_authToken) : '');\n    fetch(url).then(function(r) { return r.json(); }).then(function(data) {\n      var acceptedEl = document.getElementById('accepted-list');\n      var usageEl = document.getElementById('usage-list');\n      if (acceptedEl) acceptedEl.innerHTML = data.accepted && data.accepted.length ? data.accepted.map(function(l) { return '<li>' + esc(l) + '</li>'; }).join('') : '<li style=\"color:var(--muted)\">None yet.</li>';\n      if (usageEl) usageEl.innerHTML = data.usage && data.usage.length ? data.usage.map(function(l) { return '<li>' + esc(l) + '</li>'; }).join('') : '<li style=\"color:var(--muted)\">No usage events yet.</li>';\n    }).catch(function() {});\n  }\n\n  function refreshLiveState() {\n    loadProjects();\n    fetch(authUrl('/api/runtime-health')).then(function(r) { return r.json(); }).then(function(data) {\n      var summary = document.getElementById('sync-state-summary');\n      if (!summary) return;\n      function _phrenTimeAgo(iso) {\n        if (!iso) return '';\n        try {\n          var d = new Date(iso), diff = Math.max(0, Date.now() - d.getTime()), sec = Math.floor(diff / 1000);\n          if (sec < 60) return 'just now';\n          var min = Math.floor(sec / 60);\n          if (min < 60) return min + ' min ago';\n          var hr = Math.floor(min / 60);\n          if (hr < 24) return hr + 'h ago';\n          return Math.floor(hr / 24) + 'd ago';\n        } catch(e) { return iso; }\n      }\n      function _phrenSyncBadge(status, detail) {\n        if (!status || status === 'n/a') return 'n/a';\n        if (status === 'ok') return '<span style=\"color:var(--success)\">ok</span>';\n        return '<span style=\"color:var(--danger)\">' + esc(detail || status) + '</span>';\n      }\n      var _pt = _phrenTimeAgo(data.lastPullAt), _pst = _phrenTimeAgo(data.lastPushAt);\n      var _hasErr = (data.lastPushStatus && data.lastPushStatus !== 'ok' && data.lastPushStatus !== 'n/a') || (data.lastPullStatus && data.lastPullStatus !== 'ok' && data.lastPullStatus !== 'n/a');\n      var _retryBtn = _hasErr ? '<div style=\"grid-column:1/-1;margin-top:4px\"><button class=\"btn btn-sm\" data-ui-action=\"retrySync\">Retry sync</button></div>' : '';\n      summary.innerHTML =\n        '<div><strong>Auto-save</strong><div class=\"text-muted\">' + esc(data.autoSaveStatus || 'n/a') + '</div></div>' +\n        '<div><strong>Last pull</strong><div class=\"text-muted\">' + _phrenSyncBadge(data.lastPullStatus, data.lastPullDetail) + (_pt ? ' <span style=\"opacity:.6\">' + esc(_pt) + '</span>' : '') + '</div></div>' +\n        '<div><strong>Last push</strong><div class=\"text-muted\">' + _phrenSyncBadge(data.lastPushStatus, data.lastPushDetail) + (_pst ? ' <span style=\"opacity:.6\">' + esc(_pst) + '</span>' : '') + '</div></div>' +\n        '<div><strong>Unsynced commits</strong><div class=\"text-muted\">' + esc(String(data.unsyncedCommits || 0)) + '</div></div>' + _retryBtn;\n      // Update sync LED\n      var led = document.getElementById('sync-led');\n      if (led) {\n        var pushOk = !data.lastPushStatus || data.lastPushStatus === 'ok' || data.lastPushStatus === 'n/a';\n        var pullOk = !data.lastPullStatus || data.lastPullStatus === 'ok' || data.lastPullStatus === 'n/a';\n        var hasUnsynced = (data.unsyncedCommits || 0) > 0;\n        if (!pushOk || !pullOk) {\n          led.className = 'status-led status-led-err';\n          led.title = 'Sync error';\n        } else if (hasUnsynced) {\n          led.className = 'status-led status-led-warn';\n          led.title = hasUnsynced + ' unsynced commit' + (hasUnsynced > 1 ? 's' : '');\n        } else {\n          led.className = 'status-led status-led-ok';\n          led.title = 'phren is synced';\n        }\n      }\n    });\n    if (_selectedProject) {\n      var activeTab = document.querySelector('.project-detail-tab.active');\n      var activeFile = activeTab ? activeTab.textContent : 'Findings';\n      var fileMap = { 'Findings': 'FINDINGS.md', 'Tasks': 'tasks.md', 'AGENTS.md': 'AGENTS.md', 'Summary': 'summary.md', 'Reference': 'reference:browser' };\n      loadProjectFile(fileMap[activeFile] || 'FINDINGS.md', activeTab);\n    }\n    if (_skillsLoaded) loadSkills();\n    if (_hooksLoaded) loadHooks();\n    if (_graphLoaded && !_graphRunning) loadGraph();\n    loadReviewCards();\n    loadReviewActivity();\n  }\n\n  function pollLiveUpdates() {\n    fetch(authUrl('/api/change-token'))\n      .then(function(r) { return r.json(); })\n      .then(function(data) {\n        if (!data || !data.token) return;\n        if (!_lastChangeToken) {\n          _lastChangeToken = data.token;\n          return;\n        }\n        if (data.token === _lastChangeToken) return;\n        _lastChangeToken = data.token;\n        refreshLiveState();\n      })\n      .catch(function() {});\n  }\n\n  window.toggleStar = function(name) {\n    var starred = getStarredProjects();\n    var idx = starred.indexOf(name);\n    if (idx !== -1) starred.splice(idx, 1); else starred.push(name);\n    setStarredProjects(starred);\n    renderProjectCards(_projectData);\n  };\n\n  window.filterProjects = function(query) {\n    var cards = document.querySelectorAll('.project-card');\n    var q = query.toLowerCase();\n    cards.forEach(function(card) {\n      var name = (card.getAttribute('data-project') || '').toLowerCase();\n      var summary = (card.getAttribute('data-summary') || '').toLowerCase();\n      card.style.display = (!q || name.indexOf(q) !== -1 || summary.indexOf(q) !== -1) ? '' : 'none';\n    });\n  };\n\n  window.toggleReviewEdit = function(btn) {\n    var card = btn.closest('.review-card');\n    if (!card) return;\n    var editSection = card.querySelector('.review-card-edit');\n    if (!editSection) return;\n    var isVisible = editSection.style.display !== 'none';\n    editSection.style.display = isVisible ? 'none' : 'block';\n\n    if (!isVisible) {\n      // Create diff view if not present\n      var existing = editSection.querySelector('.review-diff');\n      if (existing) existing.remove();\n\n      var ta = editSection.querySelector('textarea');\n      var originalText = card.querySelector('.review-card-text') ? card.querySelector('.review-card-text').textContent.trim() : '';\n\n      var diffContainer = document.createElement('div');\n      diffContainer.className = 'review-diff';\n      diffContainer.innerHTML =\n        '<div class=\"review-diff-pane\"><div class=\"review-diff-pane-label\">Original</div><div class=\"diff-original\"></div></div>' +\n        '<div class=\"review-diff-pane\"><div class=\"review-diff-pane-label\">Edited</div><div class=\"diff-edited\"></div></div>';\n\n      // Insert before the textarea\n      ta.parentNode.insertBefore(diffContainer, ta);\n\n      function updateDiff() {\n        var original = originalText;\n        var edited = ta.value;\n        var origEl = diffContainer.querySelector('.diff-original');\n        var editEl = diffContainer.querySelector('.diff-edited');\n\n        if (original === edited) {\n          origEl.textContent = original;\n          editEl.innerHTML = '<span style=\"color:var(--muted);font-style:italic\">No changes</span>';\n          return;\n        }\n\n        // Simple word-level diff\n        var origWords = original.split(/(\\\\s+)/);\n        var editWords = edited.split(/(\\\\s+)/);\n        var origHtml = '', editHtml = '';\n\n        // Simple sequential comparison\n        var i = 0, j = 0;\n        while (i < origWords.length || j < editWords.length) {\n          if (i < origWords.length && j < editWords.length && origWords[i] === editWords[j]) {\n            origHtml += esc(origWords[i]);\n            editHtml += esc(editWords[j]);\n            i++; j++;\n          } else if (i < origWords.length && editWords.indexOf(origWords[i], j) === -1) {\n            origHtml += '<span class=\"diff-del\">' + esc(origWords[i]) + '</span>';\n            i++;\n          } else if (j < editWords.length && origWords.indexOf(editWords[j], i) === -1) {\n            editHtml += '<span class=\"diff-ins\">' + esc(editWords[j]) + '</span>';\n            j++;\n          } else {\n            // advance both\n            if (i < origWords.length) { origHtml += '<span class=\"diff-del\">' + esc(origWords[i]) + '</span>'; i++; }\n            if (j < editWords.length) { editHtml += '<span class=\"diff-ins\">' + esc(editWords[j]) + '</span>'; j++; }\n          }\n        }\n        origEl.innerHTML = origHtml;\n        editEl.innerHTML = editHtml;\n      }\n\n      updateDiff();\n      ta.addEventListener('input', updateDiff);\n    }\n  };\n\n  window.filterReviewCards = function() {\n    var fp = document.getElementById('review-filter-project');\n    var fm = document.getElementById('review-filter-machine');\n    var fmod = document.getElementById('review-filter-model');\n    if (!fp) return;\n    var project = fp.value;\n    var machine = fm ? fm.value : '';\n    var model = fmod ? fmod.value : '';\n    var cards = document.querySelectorAll('.review-card');\n    var shown = 0;\n    var total = cards.length;\n    cards.forEach(function(card) {\n      var cp = card.getAttribute('data-project') || '';\n      var cm = card.getAttribute('data-machine') || '';\n      var cmod = card.getAttribute('data-model') || '';\n      var show = (!project || cp === project) && (!machine || cm === machine) && (!model || cmod === model);\n      card.style.display = show ? '' : 'none';\n      if (show) shown++;\n    });\n    var countEl = document.getElementById('review-filter-count');\n    if (countEl) countEl.textContent = shown < total ? (shown + ' of ' + total) : '';\n  };\n\n  window.selectProject = function(name, el) {\n    _selectedProject = name;\n    document.querySelectorAll('.project-card').forEach(function(c) { c.classList.remove('selected'); });\n    if (el) el.classList.add('selected');\n    var area = document.getElementById('project-detail-area');\n    area.innerHTML =\n      '<div class=\"project-detail\">' +\n        '<div class=\"project-detail-header\"><h2>' + esc(name) + '</h2></div>' +\n        '<div class=\"project-detail-tabs\">' +\n          '<button class=\"project-detail-tab active\" data-ui-action=\"loadProjectFile\" data-file=\"FINDINGS.md\">Findings</button>' +\n          '<button class=\"project-detail-tab\" data-ui-action=\"loadProjectFile\" data-file=\"tasks.md\">Tasks</button>' +\n          '<button class=\"project-detail-tab\" data-ui-action=\"loadProjectFile\" data-file=\"AGENTS.md\">AGENTS.md</button>' +\n          '<button class=\"project-detail-tab\" data-ui-action=\"loadProjectFile\" data-file=\"summary.md\">Summary</button>' +\n          '<button class=\"project-detail-tab\" data-ui-action=\"loadProjectFile\" data-file=\"reference:browser\">Reference</button>' +\n        '</div>' +\n        '<div class=\"project-detail-content\" id=\"project-content\"><div class=\"project-detail-empty\">Loading...</div></div>' +\n      '</div>';\n    loadProjectFile('FINDINGS.md', area.querySelector('.project-detail-tab'));\n    area.scrollIntoView({ behavior: 'smooth', block: 'start' });\n  };\n\n\n\n  function normalizeScoresPayload(payload) {\n    if (!payload || typeof payload !== 'object') return {};\n    if (payload.entries && typeof payload.entries === 'object') return payload.entries;\n    return payload;\n  }\n\n  function loadScores() {\n    if (_scoresPromise) return _scoresPromise;\n    _scoresPromise = fetch(authUrl('/api/scores'))\n      .then(function(r) { return r.json(); })\n      .then(function(payload) { return normalizeScoresPayload(payload); })\n      .catch(function() { return {}; });\n    return _scoresPromise;\n  }\n\n  function scoreDigest(text) {\n    if (!window.crypto || !window.crypto.subtle || typeof TextEncoder === 'undefined') return Promise.resolve('');\n    var data = new TextEncoder().encode(text);\n    return window.crypto.subtle.digest('SHA-1', data).then(function(buf) {\n      var bytes = Array.from(new Uint8Array(buf));\n      return bytes.map(function(b) { return b.toString(16).padStart(2, '0'); }).join('').slice(0, 12);\n    }).catch(function() { return ''; });\n  }\n\n  function computeQualityFromEntry(entry) {\n    if (!entry) return null;\n    var now = Date.now();\n    var lastUsed = entry.lastUsedAt ? new Date(entry.lastUsedAt).getTime() : 0;\n    var daysSince = lastUsed ? (now - lastUsed) / 86400000 : 999;\n\n    var recencyBoost = 0;\n    if (daysSince <= 7) recencyBoost = 0.15;\n    else if (daysSince <= 30) recencyBoost = 0;\n    else recencyBoost = -0.1 * Math.min(3, (daysSince - 30) / 30);\n\n    var impressions = entry.impressions || 0;\n    var frequencyBoost = impressions > 0 ? Math.min(0.2, Math.log2(impressions + 1) * 0.05) : 0;\n    var helpful = entry.helpful || 0;\n    var reprompt = entry.repromptPenalty || 0;\n    var regression = entry.regressionPenalty || 0;\n    var penalties = reprompt + regression * 2;\n    var feedbackScore = helpful * 0.15 - penalties * 0.2;\n    var raw = 1 + feedbackScore + recencyBoost + frequencyBoost;\n    return Math.max(0.2, Math.min(1.5, raw));\n  }\n\n  function decayStateFromQuality(multiplier) {\n    if (typeof multiplier !== 'number' || Number.isNaN(multiplier)) return 'unknown';\n    if (multiplier > 0.7) return 'healthy';\n    if (multiplier >= 0.3) return 'decaying';\n    return 'stale';\n  }\n\n  function parseFindings(content) {\n    var lines = String(content || '').split(String.fromCharCode(10));\n    var findings = [];\n    var currentDate = '';\n    var inDetails = false;\n    lines.forEach(function(line) {\n      var trimmed = line.trim();\n      if (trimmed.indexOf('<details') === 0) { inDetails = true; return; }\n      if (inDetails) {\n        if (trimmed.indexOf('</details>') === 0) inDetails = false;\n        return;\n      }\n      if (trimmed.indexOf('## ') === 0) {\n        currentDate = trimmed.slice(3).trim();\n        return;\n      }\n      if (trimmed.indexOf('- ') !== 0) return;\n      var text = trimmed.slice(2).trim();\n      if (!text) return;\n      findings.push({ text: text, date: currentDate });\n    });\n    return findings;\n  }\n\n  function renderFindingsContent(container, project, content) {\n    var findings = parseFindings(content);\n    if (!findings.length) {\n      container.innerHTML = '<pre>' + esc(content) + '</pre>';\n      return;\n    }\n    loadScores().then(function(scores) {\n      return Promise.all(findings.map(function(finding) {\n        var short = finding.text.trim().replace(/  +/g, ' ').slice(0, 200);\n        return scoreDigest(project + ':FINDINGS.md:' + short).then(function(digest) {\n          var key = digest ? (project + '/FINDINGS.md:' + digest) : '';\n          var entry = key ? scores[key] : null;\n          var multiplier = computeQualityFromEntry(entry);\n          var state = decayStateFromQuality(multiplier);\n          return {\n            text: finding.text,\n            date: finding.date,\n            key: key,\n            multiplier: multiplier,\n            state: state,\n          };\n        });\n      }));\n    }).then(function(items) {\n      var html = '<div style=\"display:flex;gap:8px;padding:14px 14px 0 14px\"><input id=\"finding-add-input\" type=\"text\" placeholder=\"Add a finding\u2026\" style=\"flex:1;border:1px solid var(--border);border-radius:var(--radius-sm);padding:6px 10px;background:var(--surface);color:var(--ink);font-size:var(--text-sm);font-family:var(--font)\" onkeydown=\"if(event.key===\\'Enter\\')window.phrenAddFinding(\\'' + esc(project).replace(/'/g, \"\\\\'\") + '\\')\"><button onclick=\"window.phrenAddFinding(\\'' + esc(project).replace(/'/g, \"\\\\'\") + '\\')\" style=\"border:1px solid var(--accent);border-radius:var(--radius-sm);padding:4px 12px;background:var(--accent);color:#fff;font-size:var(--text-sm);cursor:pointer;font-family:var(--font)\">Add</button></div>';\n      html += '<div class=\"finding-cards\">';\n      items.forEach(function(item) {\n        var scoreText = typeof item.multiplier === 'number' ? item.multiplier.toFixed(2) : 'n/a';\n        var stateText = item.state === 'unknown' ? 'unknown' : item.state;\n        var stateClass = item.state === 'unknown' ? 'decaying' : item.state;\n        html +=\n          '<details class=\"finding-detail-card\">' +\n            '<summary>' + esc(item.text) + '</summary>' +\n            '<div class=\"finding-detail-meta\">' +\n              (item.date ? '<span>Date: ' + esc(item.date) + '</span>' : '') +\n              '<span>Quality multiplier: ' + esc(scoreText) + '</span>' +\n              '<span><span class=\"finding-score-indicator ' + esc(stateClass) + '\"></span>Decay state: ' + esc(stateText) + '</span>' +\n              '<button onclick=\"window.phrenRemoveFinding(\\'' + esc(project).replace(/'/g, \"\\\\'\") + '\\', this)\" data-finding-text=\"' + esc(item.text) + '\" style=\"margin-left:auto;background:none;border:1px solid var(--border);border-radius:4px;padding:2px 8px;cursor:pointer;color:var(--muted);font-size:11px\">Remove</button>' +\n            '</div>' +\n          '</details>';\n      });\n      html += '</div>';\n      container.innerHTML = html;\n    }).catch(function() {\n      container.innerHTML = '<pre>' + esc(content) + '</pre>';\n    });\n  }\n\n  window.loadProjectFile = function(file, btn) {\n    if (!_selectedProject) return;\n    document.querySelectorAll('.project-detail-tab').forEach(function(b) { b.classList.remove('active'); });\n    if (btn) btn.classList.add('active');\n    var container = document.getElementById('project-content');\n    container.innerHTML = '<div class=\"project-detail-empty\">Loading...</div>';\n    if (file === 'reference:browser' && typeof window.phrenLoadProjectReference === 'function') {\n      window.phrenLoadProjectReference();\n      return;\n    }\n    fetch(authUrl('/api/project-content?project=' + encodeURIComponent(_selectedProject) + '&file=' + encodeURIComponent(file)))\n      .then(function(r) { return r.json(); })\n      .then(function(data) {\n        if (!data.ok) {\n          container.innerHTML = '<div class=\"project-detail-empty\">' + esc(data.error || 'File not found') + '</div>';\n          return;\n        }\n        if (file === 'FINDINGS.md') {\n          renderFindingsContent(container, _selectedProject, data.content);\n          return;\n        }\n        container.innerHTML = '<pre>' + esc(data.content) + '</pre>';\n      });\n  };\n\n  loadProjects();\n  loadReviewCards();\n  loadReviewActivity();\n  var _lastChangeToken = '';\n  pollLiveUpdates();\n  window.setInterval(pollLiveUpdates, 2000);\n\n  // ── Auth helpers ─────────────────────────────────────────────\n  function authUrl(base) {\n    return base + (base.indexOf('?') === -1 ? '?' : '&') + '_auth=' + encodeURIComponent(_authToken);\n  }\n  function authBody(body) {\n    return body + (_authToken ? '&_auth=' + encodeURIComponent(_authToken) : '');\n  }\n\n  // ── Finding management ───────────────────────────────────────\n  window.phrenAddFinding = function(project) {\n    var input = document.getElementById('finding-add-input');\n    if (!input || !input.value.trim()) return;\n    var text = input.value.trim();\n    fetch(authUrl('/api/csrf-token')).then(function(r) { return r.json(); }).then(function(csrfData) {\n      var body = new URLSearchParams({ text: text });\n      if (csrfData.token) body.set('_csrf', csrfData.token);\n      return fetch(authUrl('/api/findings/' + encodeURIComponent(project)), { method: 'POST', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });\n    }).then(function(r) { return r.json(); }).then(function(data) {\n      if (data.ok) { input.value = ''; window.selectProjectFile('FINDINGS.md'); } else { alert(data.error || 'Failed to add finding'); }\n    }).catch(function(err) { alert('Error: ' + String(err)); });\n  };\n  window.phrenRemoveFinding = function(project, btn) {\n    var text = btn.getAttribute('data-finding-text');\n    if (!text || !confirm('Remove this finding?')) return;\n    fetch(authUrl('/api/csrf-token')).then(function(r) { return r.json(); }).then(function(csrfData) {\n      var body = new URLSearchParams({ text: text });\n      if (csrfData.token) body.set('_csrf', csrfData.token);\n      return fetch(authUrl('/api/findings/' + encodeURIComponent(project)), { method: 'DELETE', body: body, headers: { 'content-type': 'application/x-www-form-urlencoded' } });\n    }).then(function(r) { return r.json(); }).then(function(data) {\n      if (data.ok) { window.selectProjectFile('FINDINGS.md'); } else { alert(data.error || 'Failed to remove finding'); }\n    }).catch(function(err) { alert('Error: ' + String(err)); });\n  };\n\n  // ── Skills ───────────────────────────────────────────────────\n  function loadSkills() {\n    fetch(authUrl('/api/skills')).then(function(r) { return r.json(); }).then(function(data) {\n      _skillsLoaded = true;\n      var list = document.getElementById('skills-list');\n      if (!data.length) { list.innerHTML = '<div style=\"padding:40px 20px;color:var(--muted);text-align:center\"><svg viewBox=\"0 0 80 80\" width=\"80\" height=\"80\" fill=\"none\" style=\"margin:0 auto 8px;display:block\"><path d=\"M40 16l4 12h-8l4-12z\" stroke=\"var(--accent)\" stroke-width=\"2\" fill=\"var(--accent-dim)\" stroke-linejoin=\"round\"/><path d=\"M40 28v20\" stroke=\"var(--accent)\" stroke-width=\"2.5\" stroke-linecap=\"round\"/><path d=\"M34 38h12\" stroke=\"var(--accent)\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M36 48l4 12 4-12\" stroke=\"var(--accent)\" stroke-width=\"2\" fill=\"none\" stroke-linejoin=\"round\"/></svg><div style=\"font-weight:500;font-size:var(--text-base)\">No skills installed</div></div>'; return; }\n      var bySource = {};\n      data.forEach(function(s) { (bySource[s.source] = bySource[s.source] || []).push(s); });\n      var html = '';\n      Object.keys(bySource).sort().forEach(function(src) {\n        html += '<div class=\"split-group-label\">' + esc(src) + '</div>';\n        bySource[src].forEach(function(s) {\n          html += '<div class=\"split-item\" data-path=\"' + esc(s.path) + '\" data-name=\"' + esc(s.name) + '\" data-enabled=\"' + (s.enabled ? 'true' : 'false') + '\" data-source=\"' + esc(s.source) + '\" data-ui-action=\"selectSkillFromEl\">' +\n            '<span>' + esc(s.name) + '</span>' +\n            '<span class=\"badge ' + (s.enabled ? 'badge-on' : 'badge-off') + '\">' + (s.enabled ? 'active' : 'inactive') + '</span>' +\n          '</div>';\n        });\n      });\n      list.innerHTML = html;\n    });\n  }\n\n  window.selectSkillFromEl = function(el) {\n    if (!el) return;\n    selectSkill(el.getAttribute('data-path') || '', el, el.getAttribute('data-name') || '', el.getAttribute('data-enabled') === 'true', el.getAttribute('data-source') || '');\n  };\n\n  window.selectSkill = function(filePath, el, name, enabled, source) {\n    if (_editingSkill && !confirm('Discard unsaved changes?')) return;\n    _editingSkill = false;\n    _currentSkillPath = filePath;\n    _currentSkillEnabled = typeof enabled === 'boolean' ? enabled : true;\n    _currentSkillSource = source || '';\n    _currentSkillName = name || '';\n    document.querySelectorAll('#skills-list .split-item').forEach(function(i) { i.classList.remove('selected'); });\n    if (el) el.classList.add('selected');\n    var reader = document.getElementById('skills-reader');\n    reader.innerHTML = '<div class=\"reader-empty\">Loading...</div>';\n    fetch(authUrl('/api/skill-content?path=' + encodeURIComponent(filePath)))\n      .then(function(r) { return r.json(); })\n      .then(function(data) {\n        if (!data.ok) { reader.innerHTML = '<div class=\"reader-empty\">' + esc(data.error || 'Error loading file') + '</div>'; return; }\n        var statusBadge = '<span class=\"badge ' + (_currentSkillEnabled ? 'badge-on' : 'badge-off') + '\" id=\"skill-enabled-badge\">' + (_currentSkillEnabled ? 'active' : 'inactive') + '</span>';\n        var toggleLabel = _currentSkillEnabled ? 'Disable' : 'Enable';\n        reader.innerHTML =\n          '<div class=\"reader-toolbar\">' +\n            '<span class=\"reader-title\">' + esc(name) + '</span>' +\n            '<span class=\"reader-path\">' + esc(filePath) + '</span>' +\n            statusBadge +\n            '<span id=\"skill-status\"></span>' +\n            '<button class=\"btn btn-sm\" data-ui-action=\"toggleSkill\">' + toggleLabel + '</button>' +\n            '<button class=\"btn btn-sm\" data-ui-action=\"editSkill\">Edit</button>' +\n          '</div>' +\n          '<div class=\"reader-content\"><pre id=\"skill-pre\">' + esc(data.content) + '</pre></div>';\n      });\n  };\n\n  window.editSkill = function() {\n    var pre = document.getElementById('skill-pre');\n    if (!pre) return;\n    _editingSkill = true;\n    var content = pre.textContent;\n    var toolbar = document.querySelector('#skills-reader .reader-toolbar');\n    var btns = toolbar.querySelectorAll('.btn');\n    btns.forEach(function(b) { b.remove(); });\n    var saveBtn = document.createElement('button');\n    saveBtn.className = 'btn btn-sm btn-primary';\n    saveBtn.textContent = 'Save';\n    saveBtn.onclick = saveSkill;\n    var cancelBtn = document.createElement('button');\n    cancelBtn.className = 'btn btn-sm';\n    cancelBtn.textContent = 'Cancel';\n    cancelBtn.onclick = cancelSkillEdit;\n    toolbar.appendChild(saveBtn);\n    toolbar.appendChild(cancelBtn);\n    var ta = document.createElement('textarea');\n    ta.id = 'skill-textarea';\n    ta.value = content;\n    pre.replaceWith(ta);\n    ta.focus();\n  };\n\n  window.cancelSkillEdit = function() {\n    _editingSkill = false;\n    if (_currentSkillPath) {\n      var items = document.querySelectorAll('#skills-list .split-item.selected');\n      if (items.length) items[0].click();\n    }\n  };\n\n  window.saveSkill = function() {\n    var ta = document.getElementById('skill-textarea');\n    if (!ta || !_currentSkillPath) return;\n    fetchCsrfToken(function(csrfToken) {\n    var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';\n    fetch('/api/skill-save', {\n      method: 'POST',\n      headers: { 'content-type': 'application/x-www-form-urlencoded' },\n      body: authBody('path=' + encodeURIComponent(_currentSkillPath) + '&content=' + encodeURIComponent(ta.value)) + csrfPart\n    }).then(function(r) { return r.json(); }).then(function(data) {\n      if (data.ok) {\n        _editingSkill = false;\n        setStatus('skill-status', 'Saved', 'ok');\n        var pre = document.createElement('pre');\n        pre.id = 'skill-pre';\n        pre.textContent = ta.value;\n        ta.replaceWith(pre);\n        var toolbar = document.querySelector('#skills-reader .reader-toolbar');\n        var btns = toolbar.querySelectorAll('.btn');\n        btns.forEach(function(b) { b.remove(); });\n        var toggleBtn = document.createElement('button');\n        toggleBtn.className = 'btn btn-sm';\n        toggleBtn.textContent = _currentSkillEnabled ? 'Disable' : 'Enable';\n        toggleBtn.setAttribute('data-ui-action', 'toggleSkill');\n        toolbar.appendChild(toggleBtn);\n        var editBtn = document.createElement('button');\n        editBtn.className = 'btn btn-sm';\n        editBtn.textContent = 'Edit';\n        editBtn.onclick = window.editSkill;\n        toolbar.appendChild(editBtn);\n      } else {\n        setStatus('skill-status', data.error || 'Save failed', 'err');\n      }\n    });\n    });\n  };\n\n  window.toggleSkill = function() {\n    if (!_currentSkillPath || !_currentSkillName) return;\n    fetchCsrfToken(function(csrfToken) {\n      var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';\n      var nextEnabled = !_currentSkillEnabled;\n      fetch('/api/skill-toggle', {\n        method: 'POST',\n        headers: { 'content-type': 'application/x-www-form-urlencoded' },\n        body: authBody('project=' + encodeURIComponent(_currentSkillSource) + '&name=' + encodeURIComponent(_currentSkillName) + '&enabled=' + encodeURIComponent(String(nextEnabled))) + csrfPart\n      }).then(function(r) { return r.json(); }).then(function(data) {\n        if (!data.ok) return;\n        _currentSkillEnabled = nextEnabled;\n        loadSkills();\n        window.selectSkill(_currentSkillPath, null, _currentSkillName, _currentSkillEnabled, _currentSkillSource);\n      });\n    });\n  };\n\n  // ── Hooks ────────────────────────────────────────────────────\n  function loadHooks() {\n    fetch(authUrl('/api/hooks')).then(function(r) { return r.json(); }).then(function(data) {\n      _hooksLoaded = true;\n      var list = document.getElementById('hooks-list');\n      var html = '<div class=\"split-group-label\">Lifecycle Hooks</div>';\n      data.tools.forEach(function(t) {\n        html += '<div class=\"hook-item\" data-config-path=\"' + esc(t.configPath) + '\" data-tool=\"' + esc(t.tool) + '\" data-exists=\"' + (t.exists ? 'true' : 'false') + '\" data-ui-action=\"selectHookFromEl\">' +\n          '<span class=\"hook-name\">' + esc(t.tool) + '</span>' +\n          '<span class=\"badge ' + (t.enabled ? 'badge-on' : 'badge-off') + '\">' + (t.enabled ? 'on' : 'off') + '</span>' +\n        '</div>';\n      });\n      if (data.customHooks && data.customHooks.length) {\n        html += '<div class=\"split-group-label\">Custom Hooks</div>';\n        data.customHooks.forEach(function(ch) {\n          html += '<div class=\"split-item\" style=\"cursor:default;flex-direction:column;align-items:flex-start\">' +\n            '<div class=\"hook-custom-event\">' + esc(ch.event) + '</div>' +\n            '<div class=\"hook-custom-cmd\">' + esc(ch.command) + '</div>' +\n          '</div>';\n        });\n      }\n      // Show illustrated empty state if no hook configs exist on disk and no custom hooks\n      var anyExists = data.tools.some(function(t) { return t.exists; });\n      if (!anyExists && (!data.customHooks || !data.customHooks.length)) {\n        html += '<div style=\"padding:40px 20px;color:var(--muted);text-align:center\"><svg viewBox=\"0 0 80 80\" width=\"80\" height=\"80\" fill=\"none\" style=\"margin:0 auto 8px;display:block\"><rect x=\"20\" y=\"28\" width=\"16\" height=\"24\" rx=\"3\" stroke=\"var(--accent)\" stroke-width=\"2\" fill=\"var(--accent-dim)\"/><rect x=\"44\" y=\"28\" width=\"16\" height=\"24\" rx=\"3\" stroke=\"var(--accent)\" stroke-width=\"2\" fill=\"var(--accent-dim)\"/><path d=\"M36 36c4-6 4-6 8 0\" stroke=\"var(--accent)\" stroke-width=\"2\" stroke-linecap=\"round\"/><path d=\"M36 44c4-6 4-6 8 0\" stroke=\"var(--accent)\" stroke-width=\"2\" stroke-linecap=\"round\"/></svg><div style=\"font-weight:500;font-size:var(--text-base)\">No hooks configured</div></div>';\n      }\n      html += '<div class=\"split-group-label\">Per-Project Overrides</div>';\n      if (data.projectOverrides && data.projectOverrides.length) {\n        data.projectOverrides.forEach(function(po) {\n          var baseLabel = po.baseEnabled === true ? 'enabled' : po.baseEnabled === false ? 'disabled' : 'default';\n          html += '<div class=\"split-item\" style=\"cursor:default;flex-direction:column;align-items:flex-start;gap:4px\">';\n          html += '<div style=\"display:flex;align-items:center;gap:8px;width:100%\"><span class=\"hook-name\">' + esc(po.project) + '</span><span class=\"badge ' + (po.baseEnabled === false ? 'badge-off' : 'badge-on') + '\">' + esc(baseLabel) + '</span></div>';\n          po.events.forEach(function(ev) {\n            if (ev.configured === null) return;\n            html += '<div style=\"display:flex;align-items:center;gap:8px;padding-left:12px;font-size:var(--text-xs)\"><span style=\"color:var(--muted)\">' + esc(ev.event) + '</span><span class=\"badge ' + (ev.enabled ? 'badge-on' : 'badge-off') + '\" style=\"font-size:10px\">' + (ev.enabled ? 'on' : 'off') + '</span></div>';\n          });\n          html += '</div>';\n        });\n      } else {\n        html += '<div class=\"split-item\" style=\"cursor:default;color:var(--muted);font-size:var(--text-sm)\">No per-project overrides configured. Use <code>phren hooks enable/disable</code> with <code>--project</code> or configure in Settings.</div>';\n      }\n      list.innerHTML = html;\n    });\n  }\n\n  window.selectHookFromEl = function(el) {\n    if (!el) return;\n    selectHook(\n      el.getAttribute('data-config-path') || '',\n      el,\n      el.getAttribute('data-tool') || '',\n      el.getAttribute('data-exists') === 'true'\n    );\n  };\n\n  window.selectHook = function(filePath, el, toolName, exists) {\n    if (_editingHook && !confirm('Discard unsaved changes?')) return;\n    _editingHook = false;\n    _currentHookPath = filePath;\n    document.querySelectorAll('#hooks-list .hook-item').forEach(function(i) { i.classList.remove('selected'); });\n    if (el) el.classList.add('selected');\n    var reader = document.getElementById('hooks-reader');\n    if (!exists) {\n      reader.innerHTML =\n        '<div class=\"reader-toolbar\"><span class=\"reader-title\">' + esc(toolName) + '</span><span class=\"reader-path\">' + esc(filePath) + '</span></div>' +\n        '<div class=\"reader-empty\">Config file not found. This tool may not be installed or configured.</div>';\n      return;\n    }\n    reader.innerHTML =\n      '<div class=\"reader-toolbar\">' +\n        '<span class=\"reader-title\">' + esc(toolName) + '</span>' +\n        '<span class=\"reader-path\">' + esc(filePath) + '</span>' +\n        '<span id=\"hook-status\"></span>' +\n        '<button class=\"btn btn-sm\" data-ui-action=\"editHook\">Edit</button>' +\n        '<button class=\"btn btn-sm btn-primary\" data-tool=\"' + esc(toolName) + '\" data-ui-action=\"toggleHookToolFromEl\">Toggle</button>' +\n      '</div>' +\n      '<div class=\"reader-content\"><div class=\"reader-empty\">Loading...</div></div>';\n    fetch(authUrl('/api/skill-content?path=' + encodeURIComponent(filePath)))\n      .then(function(r) { return r.json(); })\n      .then(function(data) {\n        var content = reader.querySelector('.reader-content');\n        if (!content) return;\n        if (!data.ok) { content.innerHTML = '<div class=\"reader-empty\">' + esc(data.error || 'Error loading file') + '</div>'; return; }\n        content.innerHTML = '<pre id=\"hook-pre\">' + esc(data.content) + '</pre>';\n      });\n  };\n\n  window.toggleHookToolFromEl = function(el) {\n    if (!el) return;\n    toggleHookTool(el.getAttribute('data-tool') || '');\n  };\n\n  window.editHook = function() {\n    var pre = document.getElementById('hook-pre');\n    if (!pre) return;\n    _editingHook = true;\n    var toolbar = document.querySelector('#hooks-reader .reader-toolbar');\n    var btns = toolbar.querySelectorAll('.btn');\n    btns.forEach(function(b) { b.remove(); });\n    var saveBtn = document.createElement('button');\n    saveBtn.className = 'btn btn-sm btn-primary';\n    saveBtn.textContent = 'Save';\n    saveBtn.onclick = window.saveHook;\n    var cancelBtn = document.createElement('button');\n    cancelBtn.className = 'btn btn-sm';\n    cancelBtn.textContent = 'Cancel';\n    cancelBtn.onclick = window.cancelHookEdit;\n    toolbar.appendChild(saveBtn);\n    toolbar.appendChild(cancelBtn);\n    var ta = document.createElement('textarea');\n    ta.id = 'hook-textarea';\n    ta.value = pre.textContent;\n    pre.replaceWith(ta);\n    ta.focus();\n  };\n\n  window.cancelHookEdit = function() {\n    _editingHook = false;\n    _hooksLoaded = false;\n    loadHooks();\n    document.getElementById('hooks-reader').innerHTML = '<div class=\"reader-empty\">Select a hook config to view its contents.</div>';\n  };\n\n  window.saveHook = function() {\n    var ta = document.getElementById('hook-textarea');\n    if (!ta || !_currentHookPath) return;\n    fetchCsrfToken(function(csrfToken) {\n    var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';\n    fetch('/api/skill-save', {\n      method: 'POST',\n      headers: { 'content-type': 'application/x-www-form-urlencoded' },\n      body: authBody('path=' + encodeURIComponent(_currentHookPath) + '&content=' + encodeURIComponent(ta.value)) + csrfPart\n    }).then(function(r) { return r.json(); }).then(function(data) {\n      if (data.ok) {\n        _editingHook = false;\n        setStatus('hook-status', 'Saved', 'ok');\n        window.cancelHookEdit();\n      } else {\n        setStatus('hook-status', data.error || 'Save failed', 'err');\n      }\n    });\n    });\n  };\n\n  window.toggleHookTool = function(toolName) {\n    fetchCsrfToken(function(csrfToken) {\n    var csrfPart = csrfToken ? '&_csrf=' + encodeURIComponent(csrfToken) : '';\n    fetch('/api/hook-toggle', {\n      method: 'POST',\n      headers: { 'content-type': 'application/x-www-form-urlencoded' },\n      body: authBody('tool=' + encodeURIComponent(toolName)) + csrfPart\n    }).then(function(r) { return r.json(); }).then(function(data) {\n      if (data.ok) { _hooksLoaded = false; loadHooks(); }\n    });\n    });\n  };\n\n  // ── Graph (Force-directed) ───────────────────────────────────\n  var _graphData = null;\n  var _graphNodes = [];\n  var _graphZoom = 1;\n  var _graphPanX = 0, _graphPanY = 0;\n  var _graphDrag = null;\n  var _graphRunning = false;\n  var _graphAlpha = 1;\n  var _graphFilter = 'all';\n  var _graphProjectFilter = 'all';\n  var _graphSourceFilter = 'all';\n  var _graphNodeLimit = 5000;\n  var _graphTotalNodes = 0;\n  var _graphListenersAttached = false;\n  var _graphSelectedNodeId = '';\n\n  var COLORS = { project: '#d4892e', decision: '#3b82f6', pitfall: '#ef4444', pattern: '#10b981', tradeoff: '#f59e0b', architecture: '#8b4a12', bug: '#dc2626', 'task-active': '#10b981', 'task-queue': '#b8860b', entity: '#3a7bae', reference: '#6b8e7a', other: '#f4a261' };\n  var RADII = { project: 18, decision: 8, pitfall: 8, pattern: 8, tradeoff: 8, architecture: 8, bug: 8, 'task-active': 7, 'task-queue': 7, entity: 10, reference: 6, other: 8 };\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  function loadGraph() {\n    var url = '/api/graph' + (_authToken ? '?_auth=' + encodeURIComponent(_authToken) : '');\n    fetch(url).then(function(r) {\n      if (!r.ok) throw new Error('HTTP ' + r.status);\n      return r.json();\n    }).then(function(data) {\n      _graphLoaded = true;\n      _graphTotalNodes = data.total || data.nodes.length;\n      if (data.nodes.length > _graphNodeLimit) {\n        var projectNodes = data.nodes.filter(function(n) { return n.group === 'project'; });\n        var findingNodes = data.nodes.filter(function(n) { return n.group !== 'project'; });\n        var allowed = Math.max(0, _graphNodeLimit - projectNodes.length);\n        var kept = findingNodes.slice(findingNodes.length - allowed);\n        var keptSet = {};\n        projectNodes.concat(kept).forEach(function(n) { keptSet[n.id] = true; });\n        data = { nodes: projectNodes.concat(kept), links: data.links.filter(function(l) { return keptSet[l.source] && keptSet[l.target]; }), total: data.total };\n      }\n      _graphData = data;\n      \n      if (window.phrenGraph && window.phrenGraph.mount) { window.phrenGraph.mount(data); } else { console.error('[phren] phrenGraph.mount not available'); }\n    }).catch(function(err) {\n      _graphLoaded = false;\n      var canvas = document.getElementById('graph-canvas');\n      if (canvas) {\n        canvas.innerHTML = '<div style=\\'padding:40px;text-align:center;color:#ef4444;font:14px system-ui\\'>Graph failed to load: ' + err.message + '</div>';\n      }\n    });\n  }\n\n  /* initGraph removed — handled by phrenGraph.mount in memory-ui-graph.ts */\n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  \n\n  // ── Utils ────────────────────────────────────────────────────\n  function setStatus(id, msg, type) {\n    var el = document.getElementById(id);\n    if (!el) return;\n    el.className = 'status-msg' + (type ? ' ' + type : '');\n    el.textContent = msg;\n    if (msg) setTimeout(function() { el.textContent = ''; el.className = ''; }, 3000);\n  }\n\n  function esc(s) {\n    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;');\n  }\n\n  // ── Dark mode ────────────────────────────────────────────────\n  (function initTheme() {\n    var saved = localStorage.getItem('phren-theme');\n    var prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\n    var theme = saved || (prefersDark ? 'dark' : 'light');\n    document.documentElement.setAttribute('data-theme', theme);\n    var btn = document.getElementById('theme-toggle');\n    if (btn) btn.textContent = theme === 'dark' ? '🌙' : '☀️';\n  })();\n\n  window.toggleTheme = function() {\n    var current = document.documentElement.getAttribute('data-theme') || 'light';\n    var next = current === 'dark' ? 'light' : 'dark';\n    document.documentElement.setAttribute('data-theme', next);\n    localStorage.setItem('phren-theme', next);\n    var btn = document.getElementById('theme-toggle');\n    if (btn) btn.textContent = next === 'dark' ? '🌙' : '☀️';\n  };\n\n  // ── Toast system ─────────────────────────────────────────────\n  function showToast(msg, type) {\n    var container = document.getElementById('toast-container');\n    if (!container) return;\n    var toast = document.createElement('div');\n    toast.className = 'toast' + (type ? ' ' + type : '');\n    toast.textContent = msg;\n    container.appendChild(toast);\n    setTimeout(function() { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 3000);\n  }\n\n  var _pendingUndo = null;\n\n  function showUndoToast(action, onUndo, onCommit) {\n    var container = document.getElementById('toast-container');\n    if (!container) return;\n    var toast = document.createElement('div');\n    toast.className = 'toast';\n    var label = document.createElement('span');\n    label.textContent = action.charAt(0).toUpperCase() + action.slice(1) + 'd memory';\n    var undoBtn = document.createElement('button');\n    undoBtn.className = 'toast-undo';\n    undoBtn.textContent = 'Undo';\n    toast.appendChild(label);\n    toast.appendChild(undoBtn);\n    container.appendChild(toast);\n\n    var committed = false;\n    var timer = setTimeout(function() {\n      committed = true;\n      if (toast.parentNode) toast.parentNode.removeChild(toast);\n      onCommit();\n    }, 2000);\n\n    undoBtn.onclick = function() {\n      if (committed) return;\n      clearTimeout(timer);\n      committed = true;\n      if (toast.parentNode) toast.parentNode.removeChild(toast);\n      onUndo();\n    };\n\n    _pendingUndo = {\n      execute: function() {\n        if (committed) return;\n        clearTimeout(timer);\n        committed = true;\n        if (toast.parentNode) toast.parentNode.removeChild(toast);\n        onCommit();\n      }\n    };\n  }\n\n  // ── Review fetch actions ──────────────────────────────────────\n  function fetchCsrfToken(cb) {\n    var url = '/api/csrf-token' + (_authToken ? '?_auth=' + encodeURIComponent(_authToken) : '');\n    fetch(url).then(function(r) { return r.json(); }).then(function(d) { cb(d.token || null); }).catch(function() { cb(null); });\n  }\n\n  // ── Batch actions ─────────────────────────────────────────────\n  function getCheckedCards() {\n    return Array.from(document.querySelectorAll('.review-card-check.checked')).map(function(cb) {\n      return cb.closest('.review-card');\n    }).filter(Boolean);\n  }\n\n  function updateBatchBar() {\n    var checked = getCheckedCards();\n    var bar = document.getElementById('batch-bar');\n    var count = document.getElementById('batch-count');\n    if (!bar || !count) return;\n    count.textContent = checked.length + ' selected';\n    bar.classList.toggle('visible', checked.length > 0);\n  }\n\n  window.toggleSelectAll = function(checked) {\n    document.querySelectorAll('.review-card').forEach(function(card) {\n      if (card.style.display === 'none') return;\n      var cb = card.querySelector('.review-card-check');\n      if (!cb) return;\n      if (checked) cb.classList.add('checked');\n      else cb.classList.remove('checked');\n    });\n    updateBatchBar();\n  };\n\n  window.clearBatchSelection = function() {\n    document.querySelectorAll('.review-card-check.checked').forEach(function(cb) {\n      cb.classList.remove('checked');\n    });\n    var selectAllCb = document.querySelector('#review-select-all input[type=\"checkbox\"]');\n    if (selectAllCb) selectAllCb.checked = false;\n    updateBatchBar();\n  };\n\n\n  // -- Similarity grouping --\n  function wordSet(text) {\n    return new Set(text.toLowerCase().replace(/[^a-z0-9\\s]/g, '').split(/\\s+/).filter(function(w) { return w.length > 2; }));\n  }\n\n  function jaccardSimilarity(a, b) {\n    var intersection = 0;\n    a.forEach(function(w) { if (b.has(w)) intersection++; });\n    var union = new Set(Array.from(a).concat(Array.from(b))).size;\n    return union === 0 ? 0 : intersection / union;\n  }\n\n  function clusterCards(items) {\n    if (items.length <= 1) return [items];\n    var THRESHOLD = 0.25;\n    var wordSets = items.map(function(item) { return wordSet(item.text || ''); });\n    var assigned = new Array(items.length).fill(-1);\n    var groups = [];\n\n    for (var i = 0; i < items.length; i++) {\n      if (assigned[i] !== -1) continue;\n      var group = [i];\n      assigned[i] = groups.length;\n      for (var j = i + 1; j < items.length; j++) {\n        if (assigned[j] !== -1) continue;\n        if (jaccardSimilarity(wordSets[i], wordSets[j]) > THRESHOLD) {\n          group.push(j);\n          assigned[j] = groups.length;\n        }\n      }\n      groups.push(group);\n    }\n    return groups.map(function(indices) {\n      return indices.map(function(idx) { return items[idx]; });\n    });\n  }\n\n  window.toggleGroup = function(btn) {\n    var group = btn.closest('.review-group');\n    if (group) group.classList.toggle('collapsed');\n    btn.textContent = group && group.classList.contains('collapsed') ? '\\u25B6' : '\\u25BC';\n  };\n\n  window.batchAction = function(action) {\n    var cards = getCheckedCards();\n    if (!cards.length) return;\n    var remaining = cards.length;\n    cards.forEach(function(card) {\n      var project = card.getAttribute('data-project');\n      var key = card.getAttribute('data-key');\n      var line = key ? key.split('\\\\x00')[1] : '';\n      if (!project || !line) return;\n      card.classList.add('removing');\n      fetchCsrfToken(function(csrfToken) {\n        var body = 'project=' + encodeURIComponent(project) + '&line=' + encodeURIComponent(line);\n        if (_authToken) body += '&_auth=' + encodeURIComponent(_authToken);\n        if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);\n        ledFetch('/api/' + action, {\n          method: 'POST',\n          headers: { 'content-type': 'application/x-www-form-urlencoded' },\n          body: body\n        }).then(function(r) { return r.json(); }).then(function(d) {\n          remaining--;\n          if (d.ok) {\n            var cardParent = card.parentNode; if (cardParent) card.remove(); if (cardParent && cardParent.classList && cardParent.classList.contains('review-group') && !cardParent.querySelector('.review-card')) { cardParent.remove(); }\n          } else {\n            card.classList.remove('removing');\n          }\n          if (remaining <= 0) {\n            updateBatchBar();\n            loadReviewCards();\n          }\n        }).catch(function() {\n          remaining--;\n          card.classList.remove('removing');\n          if (remaining <= 0) updateBatchBar();\n        });\n      });\n    });\n    showToast(action.charAt(0).toUpperCase() + action.slice(1) + 'd ' + cards.length + ' memories', 'ok');\n  };\n\n  function removeCard(card, action, project, line, text) {\n    var nextSibling = card.nextSibling; var parentNode = card.parentNode; card.remove(); updateReviewCount(-1);\n    // Clear any pending undo for a different card\n    if (_pendingUndo) {\n      _pendingUndo.execute();\n      _pendingUndo = null;\n    }\n    showUndoToast(action, function undo() {\n      // Cancel: re-insert the card\n      if (parentNode) { parentNode.insertBefore(card, nextSibling); updateReviewCount(1); }\n      _pendingUndo = null;\n    }, function commit() {\n      // Execute: actually call the server\n      _pendingUndo = null;\n      fetchCsrfToken(function(csrfToken) {\n        var body = 'project=' + encodeURIComponent(project) + '&line=' + encodeURIComponent(line);\n        if (_authToken) body += '&_auth=' + encodeURIComponent(_authToken);\n        if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);\n        ledFetch('/api/' + action, {\n          method: 'POST',\n          headers: { 'content-type': 'application/x-www-form-urlencoded' },\n          body: body\n        }).then(function(r) { return r.json(); }).then(function(d) {\n          if (d.ok) {\n            loadReviewCards();\n          } else {\n            if (parentNode) { parentNode.insertBefore(card, nextSibling); updateReviewCount(1); }\n            showToast('Error: ' + (d.error || 'Unknown error'), 'err');\n          }\n        }).catch(function() {\n          card.classList.remove('removing');\n          showToast('Network error', 'err');\n        });\n      });\n    });\n  }\n\n  window.reviewActionFromEl = function(btn, action) {\n    if (!btn) return;\n    window.reviewAction(btn, action, btn.getAttribute('data-project') || '', btn.getAttribute('data-line') || '');\n  };\n\n  window.reviewAction = function(btn, action, project, line) {\n    var card = btn.closest('.review-card');\n    if (!card) return;\n    var text = card.querySelector('.review-card-text') ? card.querySelector('.review-card-text').textContent : '';\n    removeCard(card, action, project, line, text);\n  };\n\n  window.reviewEditSubmitFromEl = function(e, form) {\n    if (!form) return;\n    window.reviewEditSubmit(e, form.getAttribute('data-project') || '', form.getAttribute('data-line') || '');\n  };\n\n  window.reviewEditSubmit = function(e, project, line) {\n    e.preventDefault();\n    var form = e.target;\n    var ta = form.querySelector('textarea[name=\"new_text\"]');\n    if (!ta) return;\n    var newText = ta.value;\n    var card = form.closest('.review-card');\n    fetchCsrfToken(function(csrfToken) {\n      var body = 'project=' + encodeURIComponent(project) + '&line=' + encodeURIComponent(line) + '&new_text=' + encodeURIComponent(newText);\n      if (_authToken) body += '&_auth=' + encodeURIComponent(_authToken);\n      if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);\n      ledFetch('/api/edit', {\n        method: 'POST',\n        headers: { 'content-type': 'application/x-www-form-urlencoded' },\n        body: body\n      }).then(function(r) { return r.json(); }).then(function(d) {\n        if (d.ok) {\n          var textEl = card ? card.querySelector('.review-card-text') : null;\n          if (textEl) textEl.innerHTML = esc(newText).replace(/\\n/g, '<br>');\n          var editSection = card ? card.querySelector('.review-card-edit') : null;\n          if (editSection) editSection.style.display = 'none';\n          showToast('Saved', 'ok');\n        } else {\n          showToast('Error: ' + (d.error || 'Unknown error'), 'err');\n        }\n      }).catch(function() { showToast('Network error', 'err'); });\n    });\n  };\n\n  function animateCount(el, from, to) {\n    if (from === to) return;\n    var diff = Math.abs(from - to);\n    if (diff <= 3) {\n      // Cycle through intermediate values at 80ms intervals\n      var step = from < to ? 1 : -1;\n      var current = from;\n      var interval = setInterval(function() {\n        current += step;\n        el.textContent = String(current);\n        if (current === to) clearInterval(interval);\n      }, 80);\n    } else {\n      // Quick scale-and-flip CSS animation\n      el.classList.remove('count-flipping');\n      void el.offsetWidth;\n      el.classList.add('count-flipping');\n      setTimeout(function() { el.textContent = String(to); }, 120);\n      setTimeout(function() { el.classList.remove('count-flipping'); }, 350);\n    }\n  }\n\n  function updateReviewCount(delta) {\n    var navBtn = document.querySelector('.nav-item[data-tab=\"review\"] .count');\n    if (!navBtn) return;\n    var current = parseInt(navBtn.textContent, 10) || 0;\n    var next = current + delta;\n    if (next <= 0) {\n      navBtn.classList.add('count-flipping');\n      setTimeout(function() { navBtn.remove(); }, 300);\n    } else {\n      animateCount(navBtn, current, next);\n    }\n  }\n\n  // ── Keyboard shortcuts ────────────────────────────────────────\n  var _focusedCard = null;\n\n  function getVisibleCards() {\n    return Array.from(document.querySelectorAll('.review-card')).filter(function(c) { return c.style.display !== 'none'; });\n  }\n\n  function focusCard(card) {\n    if (_focusedCard) { _focusedCard.style.outline = ''; _focusedCard.style.outlineOffset = ''; }\n    _focusedCard = card;\n    if (card) {\n      card.style.outline = '2px solid var(--accent)';\n      card.style.outlineOffset = '2px';\n      card.style.borderRadius = 'var(--radius)';\n      card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n    }\n  }\n\n  document.addEventListener('keydown', function(e) {\n    // Ignore if typing in an input/textarea\n    var tag = document.activeElement && document.activeElement.tagName;\n    if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return;\n\n    var activeTab = document.querySelector('.tab-content.active');\n    var isReviewTab = activeTab && activeTab.id === 'tab-review';\n    var isProjectsTab = activeTab && activeTab.id === 'tab-projects';\n\n    // / → focus project search\n    if (e.key === '/' && isProjectsTab) {\n      e.preventDefault();\n      var search = document.getElementById('projects-search');\n      if (search) search.focus();\n      return;\n    }\n\n    if (!isReviewTab) return;\n\n    var cards = getVisibleCards();\n    if (!cards.length) return;\n\n    if (e.key === 'j' || e.key === 'ArrowDown') {\n      e.preventDefault();\n      var idx = _focusedCard ? cards.indexOf(_focusedCard) : -1;\n      focusCard(cards[Math.min(idx + 1, cards.length - 1)]);\n    } else if (e.key === 'k' || e.key === 'ArrowUp') {\n      e.preventDefault();\n      var idx2 = _focusedCard ? cards.indexOf(_focusedCard) : cards.length;\n      focusCard(cards[Math.max(idx2 - 1, 0)]);\n    } else if (e.key === 'a' && _focusedCard) {\n      var approveBtn = _focusedCard.querySelector('.btn-approve');\n      if (approveBtn) approveBtn.click();\n    } else if (e.key === 'r' && _focusedCard) {\n      var rejectBtn = _focusedCard.querySelector('.btn-reject');\n      if (rejectBtn) rejectBtn.click();\n    } else if (e.key === 'e' && _focusedCard) {\n      var editBtn = _focusedCard.querySelector('.review-card-actions .btn:not(.btn-approve):not(.btn-reject)');\n      if (editBtn) editBtn.click();\n    } else if (e.key === 'Escape') {\n      if (_focusedCard) {\n        var editSection = _focusedCard.querySelector('.review-card-edit');\n        if (editSection && editSection.style.display !== 'none') {\n          toggleReviewEdit(_focusedCard.querySelector('.review-card-actions .btn:not(.btn-approve):not(.btn-reject)'));\n        }\n      }\n    }\n  });\n\n  // ── Command palette ───────────────────────────────────────────\n  var _cmdpalSelected = -1;\n\n  window.openCmdPal = function() {\n    var overlay = document.getElementById('cmdpal');\n    if (!overlay) return;\n    overlay.classList.add('open');\n    var input = document.getElementById('cmdpal-input');\n    if (input) { input.value = ''; input.focus(); }\n    cmdpalSearch('');\n  };\n\n  window.closeCmdPal = function(e) {\n    if (e && e.target !== document.getElementById('cmdpal')) return;\n    var overlay = document.getElementById('cmdpal');\n    if (overlay) overlay.classList.remove('open');\n  };\n\n  window.cmdpalSearch = function(query) {\n    _cmdpalSelected = -1;\n    var results = document.getElementById('cmdpal-results');\n    if (!results) return;\n    var q = query.toLowerCase();\n    var matches = _projectData.filter(function(p) {\n      return !q || p.name.toLowerCase().indexOf(q) !== -1 || (p.summaryText || '').toLowerCase().indexOf(q) !== -1;\n    }).slice(0, 8);\n    if (!matches.length) {\n      results.innerHTML = '<div class=\"cmdpal-empty\">No projects found</div>';\n      return;\n    }\n    results.innerHTML = matches.map(function(p, i) {\n      return '<div class=\"cmdpal-item\" data-name=\"' + esc(p.name) + '\" data-ui-action=\"cmdpalSelect\">' +\n        '<span class=\"cmdpal-item-name\">' + esc(p.name) + '</span>' +\n        '<span class=\"cmdpal-item-meta\">' + p.findingCount + ' findings</span>' +\n      '</div>';\n    }).join('');\n  };\n\n  window.cmdpalKey = function(e) {\n    var items = document.querySelectorAll('.cmdpal-item');\n    if (e.key === 'ArrowDown') {\n      e.preventDefault();\n      _cmdpalSelected = Math.min(_cmdpalSelected + 1, items.length - 1);\n      items.forEach(function(el, i) { el.classList.toggle('selected', i === _cmdpalSelected); });\n    } else if (e.key === 'ArrowUp') {\n      e.preventDefault();\n      _cmdpalSelected = Math.max(_cmdpalSelected - 1, 0);\n      items.forEach(function(el, i) { el.classList.toggle('selected', i === _cmdpalSelected); });\n    } else if (e.key === 'Enter') {\n      if (_cmdpalSelected >= 0 && items[_cmdpalSelected]) {\n        var name = items[_cmdpalSelected].getAttribute('data-name');\n        if (name) cmdpalSelect(name);\n      }\n    } else if (e.key === 'Escape') {\n      document.getElementById('cmdpal').classList.remove('open');\n    }\n  };\n\n  window.cmdpalSelect = function(name) {\n    document.getElementById('cmdpal').classList.remove('open');\n    switchTab('projects');\n    setTimeout(function() {\n      var card = document.querySelector('.project-card[data-project=\"' + name + '\"]');\n      if (card) card.click();\n      else {\n        var search = document.getElementById('projects-search');\n        if (search) { search.value = name; filterProjects(name); }\n      }\n    }, 200);\n  };\n\n  // Cmd+K / Ctrl+K to open\n  document.addEventListener('keydown', function(e) {\n    if ((e.metaKey || e.ctrlKey) && e.key === 'k') {\n      e.preventDefault();\n      var overlay = document.getElementById('cmdpal');\n      if (overlay && overlay.classList.contains('open')) {\n        overlay.classList.remove('open');\n      } else {\n        openCmdPal();\n      }\n    }\n  });\n// Event delegation for dynamically generated UI elements\n  document.addEventListener('click', function(e) {\n    var target = e.target;\n    if (!target || typeof target.closest !== 'function') return;\n    var actionEl = target.closest('[data-ui-action]');\n    if (!actionEl) return;\n    var action = actionEl.getAttribute('data-ui-action');\n    if (action === 'selectProject') {\n      selectProject(actionEl.closest('.project-card').getAttribute('data-project'), actionEl.closest('.project-card'));\n    } else if (action === 'toggleStar') {\n      e.stopPropagation();\n      var card = actionEl.closest('.project-card');\n      toggleStar(card ? card.getAttribute('data-project') : '');\n    } else if (action === 'stopProp') {\n      e.stopPropagation();\n    } else if (action === 'reviewAction') {\n      reviewActionFromEl(actionEl, actionEl.getAttribute('data-review-type'));\n    } else if (action === 'toggleReviewEdit') {\n      toggleReviewEdit(actionEl);\n    } else if (action === 'toggleGroup') {\n      toggleGroup(actionEl);\n    } else if (action === 'loadProjectFile') {\n      loadProjectFile(actionEl.getAttribute('data-file'), actionEl);\n    } else if (action === 'selectSkillFromEl') {\n      selectSkillFromEl(actionEl.closest('.split-item'));\n    } else if (action === 'editSkill') {\n      editSkill();\n    } else if (action === 'toggleSkill') {\n      toggleSkill();\n    } else if (action === 'selectHookFromEl') {\n      selectHookFromEl(actionEl.closest('.split-item'));\n    } else if (action === 'editHook') {\n      editHook();\n    } else if (action === 'toggleHookToolFromEl') {\n      toggleHookToolFromEl(actionEl);\n    } else if (action === 'cmdpalSelect') {\n      cmdpalSelect(actionEl.closest('.cmdpal-item').getAttribute('data-name'));\n    } else if (action === 'retrySync') {\n      actionEl.disabled = true;\n      actionEl.textContent = 'Syncing...';\n      window._phrenFetchCsrfToken(function(csrfToken) {\n        var body = window._phrenAuthBody('message=retry+sync');\n        if (csrfToken) body += '&_csrf=' + encodeURIComponent(csrfToken);\n        fetch('/api/sync', { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: body }).then(function(r) { return r.json(); }).then(function(data) {\n          if (typeof refreshLiveState === 'function') refreshLiveState();\n          if (typeof showToast === 'function') showToast(data.ok ? 'Sync complete' : (data.error || 'Sync failed'), data.ok ? 'success' : 'error');\n        }).catch(function() {\n          if (typeof showToast === 'function') showToast('Sync failed', 'error');\n        });\n      });\n    }\n  });\n  document.addEventListener('submit', function(e) {\n    var form = e.target;\n    if (!form || typeof form.getAttribute !== 'function') return;\n    if (form.getAttribute('data-ui-action') === 'reviewEditSubmit') {\n      e.preventDefault();\n      reviewEditSubmitFromEl(e, form);\n    }\n  });\n})();\n";
export function renderWebUiScript(authToken) {
    const safeToken = JSON.stringify(authToken).slice(1, -1); // escape for JS string literal
    const script = WEB_UI_SCRIPT_TEMPLATE.replace("__PHREN_AUTH_TOKEN_1d7b7c99__", safeToken);
    return script;
}
// Static review UI payloads live here so the page wrapper only owns HTML scaffolding and sync-state interpolation.