CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/intent-integrity-kit

Closing the intent-to-code chasm - specification-driven development with BDD verification chain

Overall
score

96%

Does it follow best practices?

Validation for skill structure

Overview
Skills
Evals
Files

template.jsskills/iikit-07-analyze/scripts/dashboard/

module.exports = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>IIKit Dashboard</title>\n  <style>\n    /* ====== CSS Custom Properties ====== */\n    :root {\n      --color-bg: #0f1117;\n      --color-surface: #1a1d27;\n      --color-surface-elevated: #222536;\n      --color-surface-hover: #2a2d40;\n      --color-border: #2e3148;\n      --color-border-subtle: #252839;\n      --color-text: #e8eaed;\n      --color-text-secondary: #9aa0b4;\n      --color-text-muted: #6b7189;\n      --color-accent: #3B82F6;\n      --color-accent-hover: #60A5FA;\n      --color-todo: #4a90d9;\n      --color-inprogress: #f5a623;\n      --color-done: #27c93f;\n      --color-p1: #ff4757;\n      --color-p2: #ffa502;\n      --color-p3: #3498db;\n      --color-verified: #27c93f;\n      --color-tampered: #ff4757;\n      --color-missing: #6b7189;\n      --radius-sm: 6px;\n      --radius-md: 10px;\n      --radius-lg: 14px;\n      --shadow-card: 0 2px 8px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2);\n      --shadow-card-hover: 0 8px 24px rgba(0,0,0,0.4), 0 2px 8px rgba(0,0,0,0.3);\n      --shadow-column: 0 1px 4px rgba(0,0,0,0.2);\n      --transition-fast: 0.15s ease;\n      --transition-normal: 0.25s ease;\n      --transition-slow: 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n      --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, Oxygen, sans-serif;\n      --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;\n    }\n\n    /* ====== Light Theme ====== */\n    [data-theme=\"light\"] {\n      --color-bg: #f5f6f8;\n      --color-surface: #ffffff;\n      --color-surface-elevated: #f0f1f4;\n      --color-surface-hover: #e8e9ee;\n      --color-border: #d8dae0;\n      --color-border-subtle: #e4e6eb;\n      --color-text: #1a1d27;\n      --color-text-secondary: #5a5f72;\n      --color-text-muted: #8b90a0;\n      --color-accent: #2563EB;\n      --color-accent-hover: #3B82F6;\n      --shadow-card: 0 1px 4px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);\n      --shadow-card-hover: 0 4px 12px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.06);\n      --shadow-column: 0 1px 3px rgba(0,0,0,0.06);\n    }\n\n    /* ====== Reset & Base ====== */\n    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }\n\n    body {\n      font-family: var(--font-sans);\n      background: var(--color-bg);\n      color: var(--color-text);\n      min-height: 100vh;\n      -webkit-font-smoothing: antialiased;\n      -moz-osx-font-smoothing: grayscale;\n    }\n\n    /* ====== Header ====== */\n    .header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      padding: 16px 28px;\n      background: var(--color-surface);\n      border-bottom: 1px solid var(--color-border);\n      position: sticky;\n      top: 0;\n      z-index: 100;\n      backdrop-filter: blur(12px);\n      gap: 16px;\n    }\n\n    .header-left {\n      display: flex;\n      align-items: center;\n      gap: 16px;\n      min-width: 0;\n      flex: 1 1 auto;\n    }\n\n    .project-label {\n      font-size: 12px;\n      color: var(--color-text-muted);\n      max-width: 160px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      border-left: 1px solid var(--color-border);\n      padding-left: 12px;\n    }\n\n    .feature-selector {\n      position: relative;\n      min-width: 100px;\n      flex: 0 1 300px;\n    }\n\n    .logo {\n      display: flex;\n      align-items: center;\n      gap: 10px;\n      font-weight: 700;\n      font-size: 16px;\n      letter-spacing: -0.3px;\n      color: var(--color-text);\n    }\n\n    .logo-icon {\n      width: 28px;\n      height: 28px;\n      background: linear-gradient(135deg, var(--color-accent), #1D4ED8);\n      border-radius: var(--radius-sm);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 14px;\n      color: white;\n      font-weight: 800;\n    }\n\n    .header-right {\n      display: flex;\n      align-items: center;\n      gap: 14px;\n      flex-shrink: 0;\n    }\n\n    /* ====== Feature Selector ====== */\n\n    .feature-selector select {\n      appearance: none;\n      background: var(--color-surface-elevated);\n      color: var(--color-text);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-sm);\n      padding: 8px 36px 8px 12px;\n      font-size: 13px;\n      font-family: var(--font-sans);\n      font-weight: 500;\n      cursor: pointer;\n      transition: border-color var(--transition-fast), box-shadow var(--transition-fast);\n      width: 100%;\n    }\n\n    .feature-selector select:hover {\n      border-color: var(--color-accent);\n    }\n\n    .feature-selector select:focus {\n      outline: none;\n      border-color: var(--color-accent);\n      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);\n    }\n\n    .feature-selector::after {\n      content: '';\n      position: absolute;\n      right: 12px;\n      top: 50%;\n      transform: translateY(-50%);\n      width: 0;\n      height: 0;\n      border-left: 4px solid transparent;\n      border-right: 4px solid transparent;\n      border-top: 5px solid var(--color-text-secondary);\n      pointer-events: none;\n    }\n\n    /* ====== Integrity Badge ====== */\n    .integrity-badge {\n      display: inline-flex;\n      align-items: center;\n      gap: 6px;\n      padding: 6px 12px;\n      border-radius: 20px;\n      font-size: 12px;\n      font-weight: 600;\n      letter-spacing: 0.3px;\n      text-transform: uppercase;\n      transition: all var(--transition-fast);\n    }\n\n    .integrity-badge.valid {\n      background: rgba(39, 201, 63, 0.12);\n      color: var(--color-verified);\n      border: 1px solid rgba(39, 201, 63, 0.25);\n    }\n\n    .integrity-badge.tampered {\n      background: rgba(255, 71, 87, 0.12);\n      color: var(--color-tampered);\n      border: 1px solid rgba(255, 71, 87, 0.25);\n      animation: pulse-warning 2s ease-in-out infinite;\n    }\n\n    .integrity-badge.missing {\n      background: rgba(107, 113, 137, 0.12);\n      color: var(--color-missing);\n      border: 1px solid rgba(107, 113, 137, 0.25);\n    }\n\n    @keyframes pulse-warning {\n      0%, 100% { opacity: 1; }\n      50% { opacity: 0.7; }\n    }\n\n    .integrity-dot {\n      width: 7px;\n      height: 7px;\n      border-radius: 50%;\n      display: inline-block;\n    }\n\n    .integrity-badge.valid .integrity-dot { background: var(--color-verified); }\n    .integrity-badge.tampered .integrity-dot { background: var(--color-tampered); }\n    .integrity-badge.missing .integrity-dot { background: var(--color-missing); }\n\n    /* ====== Connection Status (reserved) ====== */\n\n    @keyframes pulse-glow {\n      0%, 100% { box-shadow: 0 0 4px rgba(39, 201, 63, 0.4); }\n      50% { box-shadow: 0 0 12px rgba(39, 201, 63, 0.8); }\n    }\n\n    /* ====== Board Layout ====== */\n    .board-container {\n      padding: 24px 28px;\n      overflow-x: auto;\n    }\n\n    .board {\n      display: grid;\n      grid-template-columns: repeat(3, 1fr);\n      gap: 20px;\n      min-width: 900px;\n    }\n\n    /* ====== Columns ====== */\n    .column {\n      background: var(--color-surface);\n      border-radius: var(--radius-lg);\n      border: 1px solid var(--color-border-subtle);\n      box-shadow: var(--shadow-column);\n      display: flex;\n      flex-direction: column;\n      min-height: 200px;\n    }\n\n    .column-header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      padding: 16px 18px 12px;\n      border-bottom: 1px solid var(--color-border-subtle);\n    }\n\n    .column-title {\n      display: flex;\n      align-items: center;\n      gap: 10px;\n      font-size: 13px;\n      font-weight: 600;\n      text-transform: uppercase;\n      letter-spacing: 0.8px;\n      color: var(--color-text-secondary);\n    }\n\n    .column-dot {\n      width: 9px;\n      height: 9px;\n      border-radius: 50%;\n    }\n\n    .column.todo .column-dot { background: var(--color-todo); }\n    .column.in-progress .column-dot { background: var(--color-inprogress); }\n    .column.done .column-dot { background: var(--color-done); }\n\n    .column-count {\n      background: var(--color-surface-elevated);\n      color: var(--color-text-muted);\n      font-size: 11px;\n      font-weight: 700;\n      padding: 2px 8px;\n      border-radius: 10px;\n      min-width: 22px;\n      text-align: center;\n    }\n\n    .column-body {\n      padding: 12px;\n      display: flex;\n      flex-direction: column;\n      gap: 10px;\n      flex: 1;\n    }\n\n    /* ====== Cards ====== */\n    .card {\n      background: var(--color-surface-elevated);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-md);\n      padding: 16px;\n      box-shadow: var(--shadow-card);\n      transition: transform var(--transition-normal), box-shadow var(--transition-normal), opacity var(--transition-slow);\n      cursor: default;\n    }\n\n    .card:hover {\n      transform: translateY(-2px);\n      box-shadow: var(--shadow-card-hover);\n      border-color: var(--color-accent);\n    }\n\n    /* Card slide animation classes */\n    .card.entering {\n      animation: cardEnter 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;\n    }\n\n    .card.exiting {\n      animation: cardExit 0.3s ease-in forwards;\n    }\n\n    @keyframes cardEnter {\n      from { opacity: 0; transform: translateY(-10px) scale(0.97); }\n      to { opacity: 1; transform: translateY(0) scale(1); }\n    }\n\n    @keyframes cardExit {\n      from { opacity: 1; transform: translateY(0) scale(1); }\n      to { opacity: 0; transform: translateY(10px) scale(0.97); }\n    }\n\n    .card-header {\n      display: flex;\n      align-items: flex-start;\n      justify-content: space-between;\n      gap: 10px;\n      margin-bottom: 12px;\n    }\n\n    .card-title {\n      font-size: 14px;\n      font-weight: 600;\n      line-height: 1.4;\n      color: var(--color-text);\n      overflow: hidden;\n      text-overflow: ellipsis;\n      display: -webkit-box;\n      -webkit-line-clamp: 2;\n      -webkit-box-orient: vertical;\n    }\n\n    .card-title:hover {\n      -webkit-line-clamp: unset;\n    }\n\n    .card-id {\n      font-size: 11px;\n      font-family: var(--font-mono);\n      color: var(--color-text-muted);\n      margin-bottom: 4px;\n    }\n\n    /* ====== Priority Badges ====== */\n    .priority-badge {\n      display: inline-flex;\n      align-items: center;\n      padding: 3px 8px;\n      border-radius: 4px;\n      font-size: 11px;\n      font-weight: 700;\n      letter-spacing: 0.5px;\n      flex-shrink: 0;\n    }\n\n    .priority-badge.p1 {\n      background: rgba(255, 71, 87, 0.15);\n      color: var(--color-p1);\n    }\n\n    .priority-badge.p2 {\n      background: rgba(255, 165, 2, 0.15);\n      color: var(--color-p2);\n    }\n\n    .priority-badge.p3 {\n      background: rgba(52, 152, 219, 0.15);\n      color: var(--color-p3);\n    }\n\n    /* ====== Progress Bar ====== */\n    .progress-container {\n      margin: 10px 0;\n    }\n\n    .progress-info {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      margin-bottom: 6px;\n    }\n\n    .progress-label {\n      font-size: 11px;\n      color: var(--color-text-muted);\n      font-weight: 500;\n    }\n\n    .progress-value {\n      font-size: 11px;\n      font-family: var(--font-mono);\n      color: var(--color-text-secondary);\n      font-weight: 600;\n    }\n\n    .progress-bar {\n      width: 100%;\n      height: 4px;\n      background: var(--color-border);\n      border-radius: 2px;\n      overflow: hidden;\n    }\n\n    .progress-fill {\n      height: 100%;\n      border-radius: 2px;\n      transition: width var(--transition-slow);\n      min-width: 0;\n    }\n\n    .column.todo .progress-fill { background: var(--color-todo); }\n    .column.in-progress .progress-fill { background: var(--color-inprogress); }\n    .column.done .progress-fill { background: var(--color-done); }\n\n    /* ====== Task List ====== */\n    .task-list {\n      list-style: none;\n      display: flex;\n      flex-direction: column;\n      gap: 4px;\n      margin-top: 8px;\n    }\n\n    .task-item {\n      display: flex;\n      align-items: flex-start;\n      gap: 8px;\n      padding: 5px 6px;\n      border-radius: var(--radius-sm);\n      font-size: 12px;\n      line-height: 1.5;\n      color: var(--color-text-secondary);\n      transition: background var(--transition-fast);\n    }\n\n    .task-item:hover {\n      background: var(--color-surface-hover);\n    }\n\n    .task-checkbox {\n      flex-shrink: 0;\n      width: 15px;\n      height: 15px;\n      border-radius: 3px;\n      border: 1.5px solid var(--color-border);\n      margin-top: 2px;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      transition: all var(--transition-fast);\n    }\n\n    .task-checkbox.checked {\n      background: var(--color-done);\n      border-color: var(--color-done);\n    }\n\n    .task-checkbox.checked::after {\n      content: '';\n      display: block;\n      width: 4px;\n      height: 7px;\n      border: solid white;\n      border-width: 0 1.5px 1.5px 0;\n      transform: rotate(45deg) translate(-0.5px, -0.5px);\n    }\n\n    .task-item.checked .task-description {\n      text-decoration: line-through;\n      color: var(--color-text-muted);\n    }\n\n    .task-id {\n      font-family: var(--font-mono);\n      font-size: 10px;\n      color: var(--color-text-muted);\n      flex-shrink: 0;\n      margin-top: 1px;\n    }\n\n    .task-description {\n      flex: 1;\n      transition: color var(--transition-fast);\n    }\n\n    /* ====== Empty State ====== */\n    .empty-state {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      padding: 60px 20px;\n      text-align: center;\n      grid-column: 1 / -1;\n    }\n\n    .empty-state-icon {\n      width: 64px;\n      height: 64px;\n      background: var(--color-surface-elevated);\n      border-radius: var(--radius-lg);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 28px;\n      margin-bottom: 16px;\n    }\n\n    .empty-state-title {\n      font-size: 16px;\n      font-weight: 600;\n      color: var(--color-text);\n      margin-bottom: 6px;\n    }\n\n    .empty-state-text {\n      font-size: 13px;\n      color: var(--color-text-muted);\n      max-width: 300px;\n    }\n\n    .column-empty {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 24px 16px;\n      color: var(--color-text-muted);\n      font-size: 12px;\n      font-style: italic;\n      flex: 1;\n    }\n\n    /* ====== Completion Celebration ====== */\n    .card.just-completed {\n      animation: celebrateComplete 0.6s ease-out;\n    }\n\n    @keyframes celebrateComplete {\n      0% { transform: scale(1); }\n      30% { transform: scale(1.03); box-shadow: 0 0 20px rgba(39, 201, 63, 0.3); }\n      100% { transform: scale(1); }\n    }\n\n    /* ====== Loading ====== */\n    .loading {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      padding: 60px;\n      grid-column: 1 / -1;\n    }\n\n    .loading-spinner {\n      width: 32px;\n      height: 32px;\n      border: 3px solid var(--color-border);\n      border-top-color: var(--color-accent);\n      border-radius: 50%;\n      animation: spin 0.8s linear infinite;\n    }\n\n    @keyframes spin {\n      to { transform: rotate(360deg); }\n    }\n\n    /* ====== Responsive ====== */\n    @media (max-width: 1024px) {\n      .board {\n        min-width: unset;\n        grid-template-columns: 1fr;\n      }\n      .column { min-height: 100px; }\n    }\n\n    /* ====== Theme Toggle ====== */\n    .theme-toggle {\n      background: var(--color-surface-elevated);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-sm);\n      padding: 6px 10px;\n      cursor: pointer;\n      font-size: 16px;\n      line-height: 1;\n      transition: border-color var(--transition-fast), background var(--transition-fast);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n    }\n\n    .theme-toggle:hover {\n      border-color: var(--color-accent);\n      background: var(--color-surface-hover);\n    }\n\n    .theme-toggle:focus-visible {\n      outline: 2px solid var(--color-accent);\n      outline-offset: 2px;\n    }\n\n    /* ====== Collapsible Task List ====== */\n    .task-toggle {\n      display: flex;\n      align-items: center;\n      gap: 6px;\n      margin-top: 10px;\n      padding: 4px 6px;\n      border: none;\n      background: none;\n      color: var(--color-text-muted);\n      font-size: 11px;\n      font-family: var(--font-sans);\n      font-weight: 500;\n      cursor: pointer;\n      border-radius: var(--radius-sm);\n      transition: color var(--transition-fast), background var(--transition-fast);\n      width: 100%;\n      text-align: left;\n    }\n\n    .task-toggle:hover {\n      color: var(--color-text-secondary);\n      background: var(--color-surface-hover);\n    }\n\n    .task-toggle-icon {\n      transition: transform var(--transition-fast);\n      font-size: 9px;\n    }\n\n    .task-toggle-icon.expanded {\n      transform: rotate(90deg);\n    }\n\n    .task-list {\n      overflow: hidden;\n      transition: max-height var(--transition-normal), opacity var(--transition-fast);\n    }\n\n    .task-list.collapsed {\n      max-height: 0;\n      opacity: 0;\n      margin-top: 0;\n    }\n\n    .task-list.expanded {\n      max-height: 2000px;\n      opacity: 1;\n    }\n\n    /* ====== Pipeline Bar ====== */\n    .pipeline-bar {\n      display: flex;\n      align-items: center;\n      gap: 0;\n      padding: 14px 28px;\n      background: var(--color-surface);\n      border-bottom: 1px solid var(--color-border);\n      position: sticky;\n      top: 58px;\n      z-index: 90;\n    }\n\n    .pipeline-node {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      gap: 6px;\n      padding: 8px 12px;\n      border-radius: var(--radius-md);\n      cursor: pointer;\n      transition: background var(--transition-fast), transform var(--transition-fast);\n      flex: 1 1 0;\n      min-width: 72px;\n      max-width: 140px;\n      position: relative;\n      border: 1px solid var(--color-border-subtle);\n      background: transparent;\n      overflow: hidden;\n    }\n\n    .pipeline-node:hover {\n      background: var(--color-surface-hover);\n      transform: translateY(-1px);\n    }\n\n    .pipeline-node:focus-visible {\n      outline: 2px solid var(--color-accent);\n      outline-offset: 2px;\n    }\n\n    .pipeline-node.active {\n      border: 2px solid var(--color-accent);\n      background: var(--color-surface-elevated);\n    }\n\n    .pipeline-dot {\n      width: 28px;\n      height: 28px;\n      border-radius: 50%;\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 12px;\n      font-weight: 700;\n      transition: all var(--transition-normal);\n    }\n\n    .pipeline-dot.not_started {\n      background: var(--color-surface-elevated);\n      border: 2px solid var(--color-border);\n      color: var(--color-text-muted);\n    }\n\n    .pipeline-dot.in_progress {\n      background: rgba(245, 166, 35, 0.15);\n      border: 2px solid var(--color-inprogress);\n      color: var(--color-inprogress);\n    }\n\n    .pipeline-dot.complete {\n      background: rgba(39, 201, 63, 0.15);\n      border: 2px solid var(--color-done);\n      color: var(--color-done);\n    }\n\n    .pipeline-dot.skipped {\n      background: var(--color-surface-elevated);\n      border: 2px dashed var(--color-text-muted);\n      color: var(--color-text-muted);\n    }\n\n    .pipeline-dot.available {\n      background: rgba(59, 130, 246, 0.12);\n      border: 2px solid var(--color-accent);\n      color: var(--color-accent);\n    }\n\n    .pipeline-label {\n      font-size: 10px;\n      font-weight: 600;\n      color: var(--color-text-secondary);\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      white-space: nowrap;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      max-width: 100%;\n    }\n    .pipeline-label-multiline {\n      white-space: nowrap;\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      line-height: 1.3;\n    }\n    .pipeline-label-multiline .pipeline-label-line {\n      display: block;\n      max-width: 100%;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .pipeline-progress {\n      font-size: 9px;\n      font-family: var(--font-mono);\n      color: var(--color-text-muted);\n      min-height: 13px;\n    }\n\n    .pipeline-connector {\n      flex: 0 1 24px;\n      min-width: 4px;\n      height: 2px;\n      background: var(--color-border);\n      margin-top: -16px;\n    }\n\n    .pipeline-connector.complete {\n      background: var(--color-done);\n    }\n\n    .pipeline-redirect {\n      font-size: 8px;\n      opacity: 0.5;\n    }\n\n    .pipeline-node.optional .pipeline-label {\n      opacity: 0.8;\n    }\n\n    /* ====== Bugs Tab (standalone, no connector) ====== */\n    .bugs-tab-gap {\n      width: 16px;\n      flex-shrink: 0;\n    }\n\n    .bugs-tab {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      gap: 4px;\n      padding: 6px 12px;\n      cursor: pointer;\n      border-radius: 8px;\n      border: 1px solid var(--color-border);\n      background: var(--color-surface);\n      transition: all 0.15s ease;\n      position: relative;\n      font-family: inherit;\n      color: inherit;\n    }\n\n    .bugs-tab:hover {\n      border-color: var(--color-text-secondary);\n      background: rgba(255, 255, 255, 0.06);\n    }\n\n    .bugs-tab.active {\n      border-color: var(--color-accent);\n      background: rgba(99, 102, 241, 0.08);\n    }\n\n    .bugs-tab.muted {\n      opacity: 0.45;\n    }\n\n    .bugs-tab-icon {\n      font-size: 14px;\n      line-height: 1;\n    }\n\n    .bugs-tab-label {\n      font-size: 10px;\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      color: var(--color-text-secondary);\n    }\n\n    .bugs-tab-badge {\n      position: absolute;\n      top: -6px;\n      right: -6px;\n      min-width: 18px;\n      height: 18px;\n      padding: 0 5px;\n      border-radius: 9px;\n      font-size: 11px;\n      font-weight: 600;\n      line-height: 18px;\n      text-align: center;\n      color: #fff;\n    }\n\n    .bugs-tab-badge.muted {\n      opacity: 0.4;\n      background: var(--color-text-secondary);\n    }\n\n    .bugs-tab-badge.severity-critical { background: #ff4757; }\n    .bugs-tab-badge.severity-high { background: #ffa502; }\n    .bugs-tab-badge.severity-medium { background: #f1c40f; color: #1a1a2e; }\n    .bugs-tab-badge.severity-low { background: #6b7189; }\n\n    /* ====== Bug Severity Colors ====== */\n    .severity-critical { color: #ff4757; }\n    .severity-high { color: #ffa502; }\n    .severity-medium { color: #f1c40f; }\n    .severity-low { color: #6b7189; }\n\n    .severity-badge {\n      display: inline-block;\n      padding: 2px 8px;\n      border-radius: 4px;\n      font-size: 11px;\n      font-weight: 600;\n      text-transform: uppercase;\n      letter-spacing: 0.3px;\n    }\n\n    .severity-badge.severity-critical { background: rgba(255, 71, 87, 0.15); color: #ff4757; }\n    .severity-badge.severity-high { background: rgba(255, 165, 2, 0.15); color: #ffa502; }\n    .severity-badge.severity-medium { background: rgba(241, 196, 15, 0.15); color: #f1c40f; }\n    .severity-badge.severity-low { background: rgba(107, 113, 137, 0.15); color: #6b7189; }\n\n    /* ====== Bugs View ====== */\n    .bugs-view {\n      padding: 24px;\n      max-width: 1200px;\n      margin: 0 auto;\n    }\n\n    .bugs-view-header {\n      margin-bottom: 20px;\n    }\n\n    .bugs-view-title {\n      font-size: 20px;\n      font-weight: 600;\n      color: var(--color-text-primary);\n    }\n\n    .bugs-view-subtitle {\n      font-size: 13px;\n      color: var(--color-text-secondary);\n      margin-top: 4px;\n    }\n\n    .bugs-table {\n      width: 100%;\n      border-collapse: collapse;\n      font-size: 13px;\n    }\n\n    .bugs-table th {\n      text-align: left;\n      padding: 10px 12px;\n      font-weight: 600;\n      color: var(--color-text-secondary);\n      border-bottom: 1px solid var(--color-border);\n      font-size: 11px;\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n    }\n\n    .bugs-table td {\n      padding: 10px 12px;\n      border-bottom: 1px solid rgba(255, 255, 255, 0.04);\n      vertical-align: middle;\n    }\n\n    .bugs-table tr:hover td {\n      background: rgba(255, 255, 255, 0.02);\n    }\n\n    .bugs-table tr.highlighted td {\n      background: rgba(99, 102, 241, 0.12);\n      transition: background 0.3s ease;\n    }\n\n    .bugs-table .bug-id {\n      font-family: 'SF Mono', 'Fira Code', monospace;\n      font-weight: 600;\n      color: var(--color-text-primary);\n    }\n\n    .bugs-table .bug-status {\n      text-transform: capitalize;\n    }\n\n    .bugs-table .bug-status.fixed {\n      color: var(--color-done);\n    }\n\n    .bugs-table .bug-progress {\n      font-family: 'SF Mono', 'Fira Code', monospace;\n      font-size: 12px;\n    }\n\n    .bugs-table .bug-description {\n      max-width: 300px;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .bugs-table .github-link {\n      color: var(--color-accent);\n      text-decoration: none;\n    }\n\n    .bugs-table .github-link:hover {\n      text-decoration: underline;\n    }\n\n    .bugs-empty {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      padding: 80px 20px;\n      text-align: center;\n    }\n\n    .bugs-empty-icon {\n      font-size: 48px;\n      margin-bottom: 16px;\n      opacity: 0.4;\n    }\n\n    .bugs-empty-title {\n      font-size: 18px;\n      font-weight: 600;\n      color: var(--color-text-primary);\n      margin-bottom: 8px;\n    }\n\n    .bugs-empty-text {\n      font-size: 14px;\n      color: var(--color-text-secondary);\n    }\n\n    .orphaned-section {\n      margin-top: 24px;\n      padding: 16px;\n      border: 1px dashed var(--color-border);\n      border-radius: 8px;\n      opacity: 0.7;\n    }\n\n    .orphaned-title {\n      font-size: 13px;\n      font-weight: 600;\n      color: var(--color-text-secondary);\n      margin-bottom: 8px;\n    }\n\n    .orphaned-task {\n      font-size: 12px;\n      color: var(--color-text-secondary);\n      padding: 4px 0;\n    }\n\n    .bug-icon-inline {\n      display: inline-block;\n      width: 14px;\n      height: 14px;\n      margin-right: 4px;\n      vertical-align: middle;\n      opacity: 0.7;\n    }\n\n    /* Pipeline bugs gap & node (spec aliases) */\n    .pipeline-bugs-gap {\n      width: 20px;\n      flex-shrink: 0;\n    }\n    .pipeline-node-bugs.muted {\n      opacity: 0.5;\n    }\n    .bugs-dot {\n      background: var(--color-surface-elevated) !important;\n      border: 2px solid var(--color-border) !important;\n      color: var(--color-text-muted) !important;\n    }\n    .bug-count-badge {\n      display: inline-flex;\n      align-items: center;\n      justify-content: center;\n      min-width: 18px;\n      height: 18px;\n      padding: 0 5px;\n      border-radius: 9px;\n      font-size: 11px;\n      font-weight: 700;\n      margin-left: 4px;\n      vertical-align: middle;\n    }\n    .bug-count-badge.muted {\n      background: var(--color-surface-elevated);\n      color: var(--color-text-muted);\n    }\n\n    /* Bug view additional styles */\n    .bugs-view-container {\n      padding: 24px;\n      max-width: 1200px;\n    }\n    .bugs-view-container h2 {\n      font-size: 18px;\n      font-weight: 600;\n      margin-bottom: 16px;\n      color: var(--color-text);\n    }\n    .bug-row:hover {\n      background: var(--color-surface-hover);\n    }\n    .fix-progress {\n      font-variant-numeric: tabular-nums;\n      font-weight: 500;\n    }\n    .bug-status {\n      text-transform: capitalize;\n    }\n    .bug-status.fixed {\n      color: var(--color-success, #2ecc71);\n    }\n    .bug-github-link {\n      color: var(--color-accent);\n      text-decoration: none;\n    }\n    .bug-github-link:hover {\n      text-decoration: underline;\n    }\n    .bug-empty-state {\n      text-align: center;\n      padding: 48px 24px;\n      color: var(--color-text-muted);\n      font-size: 14px;\n    }\n    .bug-empty-state svg {\n      display: block;\n      margin: 0 auto 12px;\n      opacity: 0.4;\n    }\n    .orphaned-tasks-section {\n      margin-top: 32px;\n      padding: 16px;\n      background: var(--color-surface-elevated);\n      border-radius: var(--radius-md);\n      border: 1px solid var(--color-warning, #f5a623);\n      opacity: 0.7;\n    }\n    .orphaned-tasks-section h3 {\n      font-size: 13px;\n      font-weight: 600;\n      color: var(--color-warning, #f5a623);\n      margin-bottom: 8px;\n    }\n    .orphaned-tasks-section .task-item {\n      font-size: 12px;\n      color: var(--color-text-muted);\n      padding: 4px 0;\n    }\n\n    /* ====== Content Area ====== */\n    .content-area {\n      flex: 1;\n      min-height: 0;\n    }\n\n    .placeholder-view {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      padding: 80px 20px;\n      text-align: center;\n    }\n\n    .placeholder-view-icon {\n      width: 56px;\n      height: 56px;\n      background: var(--color-surface-elevated);\n      border-radius: var(--radius-lg);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 24px;\n      margin-bottom: 16px;\n    }\n\n    .placeholder-view-title {\n      font-size: 16px;\n      font-weight: 600;\n      color: var(--color-text);\n      margin-bottom: 6px;\n    }\n\n    .placeholder-view-text {\n      font-size: 13px;\n      color: var(--color-text-muted);\n      max-width: 360px;\n      margin: 0 auto;\n      text-align: center;\n    }\n\n    /* ====== Checklist Quality Gates Tab ====== */\n    .checklist-view {\n      padding: 24px 28px;\n      max-width: 900px;\n      margin: 0 auto;\n    }\n\n    .gate-indicator {\n      display: flex;\n      align-items: center;\n      gap: 12px;\n      padding: 16px 20px;\n      border-radius: var(--radius-lg);\n      background: var(--color-surface-elevated);\n      border: 1px solid var(--color-border);\n      margin-bottom: 28px;\n    }\n\n    .gate-dot {\n      width: 18px;\n      height: 18px;\n      border-radius: 50%;\n      flex-shrink: 0;\n      transition: background-color 0.4s ease;\n    }\n\n    .gate-dot.green { background-color: var(--color-green, #22c55e); }\n    .gate-dot.yellow { background-color: var(--color-yellow, #eab308); }\n    .gate-dot.red { background-color: var(--color-red, #ef4444); }\n\n    .gate-label {\n      font-size: 15px;\n      font-weight: 700;\n      letter-spacing: 0.5px;\n    }\n\n    .gate-label.open { color: var(--color-green, #22c55e); }\n    .gate-label.blocked-yellow { color: var(--color-yellow, #eab308); }\n    .gate-label.blocked-red { color: var(--color-red, #ef4444); }\n\n    .checklist-rings {\n      display: flex;\n      flex-wrap: wrap;\n      gap: 24px;\n      justify-content: center;\n      margin-bottom: 24px;\n    }\n\n    .checklist-ring-wrapper {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      cursor: pointer;\n      padding: 12px;\n      border-radius: var(--radius-lg);\n      transition: background-color 0.15s ease;\n      border: 2px solid transparent;\n    }\n\n    .checklist-ring-wrapper:hover {\n      background: var(--color-surface-elevated);\n    }\n\n    .checklist-ring-wrapper:focus-visible {\n      outline: 2px solid var(--color-primary);\n      outline-offset: 2px;\n    }\n\n    .checklist-ring-wrapper.expanded {\n      border-color: var(--color-primary);\n      background: var(--color-surface-elevated);\n    }\n\n    .checklist-ring-svg {\n      width: 100px;\n      height: 100px;\n    }\n\n    .checklist-ring-track {\n      fill: none;\n      stroke: var(--color-border);\n      stroke-width: 8;\n    }\n\n    .checklist-ring-fill {\n      fill: none;\n      stroke-width: 8;\n      stroke-linecap: round;\n      transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.4s ease;\n      transform: rotate(-90deg);\n      transform-origin: 50% 50%;\n    }\n\n    .checklist-ring-fill.red { stroke: var(--color-red, #ef4444); }\n    .checklist-ring-fill.yellow { stroke: var(--color-yellow, #eab308); }\n    .checklist-ring-fill.green { stroke: var(--color-green, #22c55e); }\n\n    .checklist-ring-pct {\n      font-size: 18px;\n      font-weight: 700;\n      fill: var(--color-text);\n      text-anchor: middle;\n      dominant-baseline: central;\n    }\n\n    .checklist-ring-name {\n      margin-top: 8px;\n      font-size: 13px;\n      font-weight: 600;\n      color: var(--color-text);\n      text-align: center;\n    }\n\n    .checklist-ring-fraction {\n      font-size: 12px;\n      color: var(--color-text-muted);\n    }\n\n    .checklist-detail {\n      max-height: 0;\n      overflow: hidden;\n      transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.15s ease, opacity 0.15s ease;\n      border-radius: var(--radius-lg);\n      background: var(--color-surface-elevated);\n      border: 1px solid transparent;\n      margin-bottom: 0;\n      opacity: 0;\n    }\n\n    .checklist-detail.open {\n      max-height: 2000px;\n      border-color: var(--color-border);\n      margin-bottom: 16px;\n      opacity: 1;\n    }\n\n    .checklist-detail-inner {\n      padding: 16px 20px;\n    }\n\n    .checklist-category {\n      font-size: 12px;\n      font-weight: 700;\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      color: var(--color-text-muted);\n      margin: 16px 0 8px;\n      padding-bottom: 4px;\n      border-bottom: 1px solid var(--color-border);\n    }\n\n    .checklist-category:first-child {\n      margin-top: 0;\n    }\n\n    .checklist-item {\n      display: flex;\n      align-items: flex-start;\n      gap: 8px;\n      padding: 6px 0;\n      font-size: 13px;\n      color: var(--color-text);\n      line-height: 1.4;\n    }\n\n    .checklist-item-icon {\n      flex-shrink: 0;\n      width: 18px;\n      font-size: 14px;\n    }\n\n    .checklist-item-icon.checked { color: var(--color-green, #22c55e); }\n    .checklist-item-icon.unchecked { color: var(--color-text-muted); }\n\n    .checklist-item-id {\n      font-family: var(--font-mono, monospace);\n      font-size: 11px;\n      color: var(--color-text-muted);\n      background: var(--color-surface);\n      padding: 1px 5px;\n      border-radius: var(--radius-sm, 4px);\n      flex-shrink: 0;\n    }\n\n    .checklist-item-tag {\n      font-size: 10px;\n      font-weight: 600;\n      padding: 1px 6px;\n      border-radius: 9999px;\n      background: var(--color-primary-muted, rgba(99, 102, 241, 0.15));\n      color: var(--color-primary, #6366f1);\n      flex-shrink: 0;\n    }\n\n    .checklist-empty {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      padding: 80px 20px;\n      text-align: center;\n    }\n\n    .checklist-empty-icon {\n      width: 56px;\n      height: 56px;\n      background: var(--color-surface-elevated);\n      border-radius: var(--radius-lg);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 24px;\n      margin-bottom: 16px;\n    }\n\n    .checklist-empty-title {\n      font-size: 16px;\n      font-weight: 600;\n      color: var(--color-text);\n      margin-bottom: 6px;\n    }\n\n    .checklist-empty-text {\n      font-size: 13px;\n      color: var(--color-text-muted);\n      max-width: 360px;\n    }\n\n    /* ====== Testify Traceability Tab ====== */\n    .testify-view {\n      padding: 24px 28px;\n      max-width: 1200px;\n      margin: 0 auto;\n    }\n\n    .testify-seal {\n      display: flex;\n      align-items: center;\n      gap: 10px;\n      padding: 12px 18px;\n      border-radius: var(--radius-lg);\n      background: var(--color-surface-elevated);\n      border: 1px solid var(--color-border);\n      margin-bottom: 20px;\n      font-size: 13px;\n      font-weight: 600;\n    }\n\n    .testify-seal-dot {\n      width: 10px;\n      height: 10px;\n      border-radius: 50%;\n      flex-shrink: 0;\n    }\n\n    .testify-seal-dot.valid { background: var(--color-verified); }\n    .testify-seal-dot.tampered { background: var(--color-tampered); animation: pulse-warning 2s ease-in-out infinite; }\n    .testify-seal-dot.missing { background: var(--color-text-muted); }\n\n    .testify-seal-label.valid { color: var(--color-verified); }\n    .testify-seal-label.tampered { color: var(--color-tampered); }\n    .testify-seal-label.missing { color: var(--color-text-muted); }\n\n    .testify-seal-hash {\n      font-family: var(--font-mono);\n      font-size: 11px;\n      color: var(--color-text-muted);\n      margin-left: auto;\n    }\n\n    .testify-sankey-container {\n      background: var(--color-surface);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-lg);\n      padding: 20px;\n      overflow-x: auto;\n    }\n\n    .testify-sankey-svg {\n      display: block;\n      width: 100%;\n    }\n\n    .testify-sankey-svg .sankey-node {\n      cursor: pointer;\n      transition: opacity var(--transition-fast);\n    }\n\n    .testify-sankey-svg .sankey-node:focus {\n      outline: 2px solid var(--color-accent);\n      outline-offset: 2px;\n    }\n\n    .testify-sankey-svg .sankey-node-rect {\n      rx: 4;\n      ry: 4;\n      stroke-width: 1.5;\n      transition: stroke var(--transition-fast), fill var(--transition-fast);\n    }\n\n    .testify-sankey-svg .sankey-node-label {\n      font-family: var(--font-mono);\n      font-size: 10px;\n      font-weight: 600;\n      fill: var(--color-text);\n      pointer-events: none;\n    }\n\n    .testify-sankey-svg .sankey-node-desc {\n      font-family: var(--font-sans);\n      font-size: 9px;\n      fill: var(--color-text-secondary);\n      pointer-events: none;\n    }\n\n    .testify-sankey-svg .sankey-flow {\n      fill-opacity: 0.25;\n      stroke-opacity: 0.4;\n      stroke-width: 1;\n      transition: fill-opacity var(--transition-fast), stroke-opacity var(--transition-fast), opacity var(--transition-fast);\n    }\n\n    .testify-sankey-svg .sankey-flow:hover {\n      fill-opacity: 0.45;\n      stroke-opacity: 0.7;\n    }\n\n    .testify-sankey-svg .sankey-group-bg {\n      rx: 6;\n      ry: 6;\n    }\n\n    .testify-sankey-svg .sankey-group-label {\n      font-family: var(--font-sans);\n      font-size: 10px;\n      font-weight: 600;\n      fill: var(--color-text-secondary);\n    }\n\n    .testify-sankey-svg .sankey-col-label {\n      font-family: var(--font-sans);\n      font-size: 11px;\n      font-weight: 700;\n      fill: var(--color-text-muted);\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n    }\n\n    .testify-sankey-svg .dimmed {\n      opacity: 0.12;\n    }\n\n    .testify-sankey-svg .highlighted .sankey-flow {\n      fill-opacity: 0.5;\n      stroke-opacity: 0.8;\n    }\n\n    .testify-sankey-svg .gap-node .sankey-node-rect {\n      stroke: var(--color-tampered);\n      stroke-width: 2;\n      stroke-dasharray: 4 2;\n    }\n\n    .testify-empty {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      padding: 80px 20px;\n      text-align: center;\n    }\n\n    .testify-empty-icon {\n      width: 56px;\n      height: 56px;\n      background: var(--color-surface-elevated);\n      border-radius: var(--radius-lg);\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      font-size: 24px;\n      margin-bottom: 16px;\n    }\n\n    .testify-empty-title {\n      font-size: 16px;\n      font-weight: 600;\n      color: var(--color-text);\n      margin-bottom: 6px;\n    }\n\n    .testify-empty-text {\n      font-size: 13px;\n      color: var(--color-text-muted);\n      max-width: 400px;\n    }\n\n    .testify-empty-text code {\n      background: var(--color-surface-elevated);\n      padding: 2px 6px;\n      border-radius: 4px;\n      font-family: var(--font-mono);\n      font-size: 12px;\n    }\n\n    @keyframes testify-fade-in {\n      from { opacity: 0; transform: translateY(6px); }\n      to { opacity: 1; transform: translateY(0); }\n    }\n\n    .testify-sankey-svg .sankey-col-left .sankey-node { animation: testify-fade-in 0.4s ease both; }\n    .testify-sankey-svg .sankey-col-mid .sankey-node { animation: testify-fade-in 0.4s ease 0.15s both; }\n    .testify-sankey-svg .sankey-col-right .sankey-node { animation: testify-fade-in 0.4s ease 0.3s both; }\n    .testify-sankey-svg .sankey-flow { animation: testify-fade-in 0.4s ease 0.45s both; }\n\n    /* ====== Spec Story Map Tab ====== */\n    .storymap-view {\n      padding: 24px 28px;\n      display: flex;\n      transition: padding-right var(--transition-normal);\n    }\n\n    .storymap-main {\n      flex: 1;\n      min-width: 0;\n    }\n\n    .storymap-section-title {\n      font-size: 13px;\n      font-weight: 600;\n      text-transform: uppercase;\n      letter-spacing: 0.8px;\n      color: var(--color-text-secondary);\n      margin-bottom: 16px;\n    }\n\n    /* Story Map Swim Lanes */\n    .swim-lanes {\n      margin-bottom: 32px;\n    }\n\n    .swim-lane {\n      display: flex;\n      align-items: flex-start;\n      gap: 12px;\n      margin-bottom: 12px;\n      min-height: 80px;\n    }\n\n    .swim-lane-label {\n      width: 40px;\n      flex-shrink: 0;\n      font-size: 12px;\n      font-weight: 700;\n      letter-spacing: 0.5px;\n      padding: 8px 0;\n      text-align: center;\n      border-radius: var(--radius-sm);\n      color: white;\n    }\n\n    .swim-lane-label.p1 { background: var(--color-p1); }\n    .swim-lane-label.p2 { background: var(--color-p2); }\n    .swim-lane-label.p3 { background: var(--color-p3); }\n\n    .swim-lane-cards {\n      display: flex;\n      gap: 12px;\n      flex-wrap: wrap;\n      flex: 1;\n      min-height: 40px;\n      padding: 4px;\n      border-radius: var(--radius-sm);\n      border: 1px dashed var(--color-border-subtle);\n    }\n\n    .swim-lane-cards:empty::after {\n      content: 'No stories';\n      color: var(--color-text-muted);\n      font-size: 12px;\n      font-style: italic;\n      padding: 8px;\n    }\n\n    /* Story Cards */\n    .story-card {\n      background: var(--color-surface);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-md);\n      padding: 12px 14px;\n      min-width: 200px;\n      max-width: 280px;\n      cursor: pointer;\n      transition: border-color var(--transition-fast), box-shadow var(--transition-fast);\n    }\n\n    .story-card:hover {\n      border-color: var(--color-accent);\n      box-shadow: var(--shadow-card-hover);\n    }\n\n    .story-card-header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      margin-bottom: 6px;\n    }\n\n    .story-card-id {\n      font-size: 11px;\n      font-weight: 700;\n      font-family: var(--font-mono);\n      color: var(--color-accent);\n    }\n\n    .story-card-priority {\n      font-size: 10px;\n      font-weight: 700;\n      padding: 2px 6px;\n      border-radius: 10px;\n      color: white;\n    }\n\n    .story-card-priority.p1 { background: var(--color-p1); }\n    .story-card-priority.p2 { background: var(--color-p2); }\n    .story-card-priority.p3 { background: var(--color-p3); }\n\n    .story-card-title {\n      font-size: 13px;\n      font-weight: 500;\n      color: var(--color-text);\n      margin-bottom: 8px;\n      line-height: 1.3;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      display: -webkit-box;\n      -webkit-line-clamp: 2;\n      -webkit-box-orient: vertical;\n    }\n\n    .story-card-meta {\n      display: flex;\n      flex-wrap: wrap;\n      gap: 6px;\n      align-items: center;\n    }\n\n    .story-card-badge {\n      font-size: 10px;\n      font-weight: 600;\n      padding: 2px 6px;\n      border-radius: 8px;\n      background: var(--color-surface-elevated);\n      color: var(--color-text-secondary);\n      font-family: var(--font-mono);\n    }\n\n    .story-card-badge.scenarios {\n      background: rgba(59, 130, 246, 0.12);\n      color: var(--color-accent);\n    }\n\n    .story-card-badge.clarifications {\n      background: rgba(245, 166, 35, 0.12);\n      color: var(--color-inprogress);\n      cursor: pointer;\n    }\n\n    .story-card-badge.clarifications:hover {\n      background: rgba(245, 166, 35, 0.25);\n    }\n\n    /* Requirements Graph */\n    .graph-container {\n      background: var(--color-surface);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-lg);\n      position: relative;\n      overflow: hidden;\n      margin-bottom: 24px;\n    }\n\n    .graph-svg {\n      width: 100%;\n      cursor: grab;\n    }\n\n    .graph-svg:active { cursor: grabbing; }\n\n    .graph-edge {\n      stroke: var(--color-border);\n      stroke-width: 1.5;\n      fill: none;\n      transition: opacity var(--transition-fast), stroke var(--transition-fast);\n    }\n\n    .graph-edge.highlighted {\n      stroke: var(--color-text);\n      stroke-width: 2.5;\n    }\n\n    .graph-edge.dimmed { opacity: 0.15; }\n\n    .graph-node { cursor: pointer; }\n    .graph-node.dimmed { opacity: 0.2; }\n\n    .graph-node circle {\n      transition: stroke var(--transition-fast), r var(--transition-fast);\n    }\n\n    .graph-node.highlighted circle {\n      stroke: var(--color-text);\n      stroke-width: 3;\n      filter: drop-shadow(0 0 4px var(--color-text));\n    }\n\n    .graph-node-us circle { fill: var(--color-accent); }\n    .graph-node-fr circle { fill: var(--color-done); }\n    .graph-node-sc circle { fill: var(--color-inprogress); }\n\n    .graph-node text {\n      font-size: 10px;\n      font-weight: 600;\n      font-family: var(--font-mono);\n      fill: var(--color-text);\n      text-anchor: middle;\n      pointer-events: none;\n    }\n\n    .graph-tooltip {\n      position: absolute;\n      background: var(--color-surface-elevated);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-sm);\n      padding: 8px 12px;\n      font-size: 12px;\n      color: var(--color-text);\n      max-width: 300px;\n      pointer-events: none;\n      z-index: 50;\n      box-shadow: var(--shadow-card);\n      display: none;\n    }\n\n    .graph-empty {\n      display: flex;\n      align-items: center;\n      justify-content: center;\n      height: 200px;\n      color: var(--color-text-muted);\n      font-size: 13px;\n    }\n\n    .graph-legend {\n      display: flex;\n      gap: 16px;\n      padding: 8px 14px;\n      border-top: 1px solid var(--color-border-subtle);\n      font-size: 11px;\n      color: var(--color-text-secondary);\n    }\n\n    .graph-legend-item {\n      display: flex;\n      align-items: center;\n      gap: 5px;\n    }\n\n    .graph-legend-dot {\n      width: 8px;\n      height: 8px;\n      border-radius: 50%;\n    }\n\n    .graph-legend-dot.us { background: var(--color-accent); }\n    .graph-legend-dot.fr { background: var(--color-done); }\n    .graph-legend-dot.sc { background: var(--color-inprogress); }\n\n    /* Clarify View */\n    .clarify-view {\n      padding: 24px 32px;\n      max-width: 800px;\n      margin: 0 auto;\n    }\n\n    .clarify-empty {\n      text-align: center;\n      padding: 60px 20px;\n      color: var(--color-text-muted);\n      max-width: 480px;\n      margin: 0 auto;\n      word-break: normal;\n      overflow-wrap: break-word;\n    }\n\n    .clarify-header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      margin-bottom: 24px;\n      padding-bottom: 16px;\n      border-bottom: 1px solid var(--color-border);\n    }\n\n    .clarify-title {\n      font-size: 16px;\n      font-weight: 600;\n      color: var(--color-text);\n    }\n\n    .clarify-count {\n      font-size: 12px;\n      color: var(--color-text-muted);\n      background: var(--color-surface-hover);\n      padding: 4px 10px;\n      border-radius: 12px;\n    }\n\n    .clarify-session {\n      margin-bottom: 24px;\n    }\n\n    .clarify-session-label {\n      font-size: 11px;\n      font-weight: 600;\n      color: var(--color-text-muted);\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      margin-bottom: 12px;\n    }\n\n    .clarify-entries {\n      display: flex;\n      flex-direction: column;\n      gap: 12px;\n    }\n\n    .clarify-entry {\n      background: var(--color-surface);\n      border: 1px solid var(--color-border-subtle);\n      border-radius: var(--radius-md);\n      padding: 14px 16px;\n    }\n\n    .clarify-question {\n      font-size: 13px;\n      font-weight: 600;\n      color: var(--color-text);\n      margin-bottom: 8px;\n      line-height: 1.5;\n    }\n\n    .clarify-answer {\n      font-size: 13px;\n      color: var(--color-text-secondary);\n      line-height: 1.5;\n    }\n\n    .clarify-q-label, .clarify-a-label {\n      display: inline-block;\n      width: 20px;\n      height: 20px;\n      line-height: 20px;\n      text-align: center;\n      border-radius: 4px;\n      font-size: 11px;\n      font-weight: 700;\n      margin-right: 8px;\n      flex-shrink: 0;\n    }\n\n    .clarify-q-label {\n      background: var(--color-accent);\n      color: white;\n    }\n\n    .clarify-a-label {\n      background: var(--color-done);\n      color: white;\n    }\n\n    .clarify-refs {\n      display: flex;\n      flex-wrap: wrap;\n      gap: 6px;\n      margin-top: 10px;\n      padding-top: 8px;\n      border-top: 1px solid var(--color-border-subtle);\n    }\n\n    .clarify-ref {\n      font-size: 11px;\n      font-weight: 600;\n      padding: 2px 8px;\n      border-radius: 10px;\n      background: var(--color-surface-hover);\n      color: var(--color-accent);\n      letter-spacing: 0.3px;\n      text-decoration: none;\n      cursor: pointer;\n      transition: background var(--transition-fast), color var(--transition-fast);\n    }\n\n    .clarify-ref:hover {\n      background: var(--color-accent);\n      color: white;\n    }\n\n    .story-card.highlighted {\n      outline: 2px solid var(--color-accent);\n      outline-offset: 2px;\n      animation: cardPulse 0.6s ease-out;\n    }\n\n    @keyframes cardPulse {\n      0% { outline-color: transparent; }\n      50% { outline-color: var(--color-accent); }\n      100% { outline-color: var(--color-accent); }\n    }\n\n    /* ====== Cross-Link Navigation ====== */\n    .cross-link {\n      cursor: pointer;\n      text-decoration: underline;\n      text-decoration-style: dotted;\n      text-decoration-color: var(--color-accent);\n      text-underline-offset: 2px;\n      transition: text-decoration-color var(--transition-fast);\n    }\n    .cross-link:hover {\n      text-decoration-style: solid;\n      text-decoration-color: var(--color-accent-hover);\n    }\n\n    .card.highlighted {\n      outline: 2px solid var(--color-accent);\n      outline-offset: 2px;\n      animation: highlight-pulse 1.5s ease-out;\n    }\n\n    .task-item.highlighted {\n      background: color-mix(in srgb, var(--color-accent) 15%, transparent);\n      border-radius: var(--radius-sm);\n      animation: highlight-pulse 1.5s ease-out;\n    }\n\n    .checklist-item.highlighted {\n      background: color-mix(in srgb, var(--color-accent) 15%, transparent);\n      border-radius: var(--radius-sm);\n      animation: highlight-pulse 1.5s ease-out;\n    }\n\n    .clarify-entry.highlighted {\n      outline: 2px solid var(--color-accent);\n      outline-offset: 2px;\n      border-radius: var(--radius-sm);\n      animation: highlight-pulse 1.5s ease-out;\n    }\n\n    .sankey-node.task-checked .sankey-node-rect {\n      stroke: var(--color-done);\n      stroke-width: 2;\n    }\n\n    .cross-link-tip {\n      font-size: 11px;\n      font-style: italic;\n      color: var(--color-text-muted);\n      padding: 6px 12px;\n      user-select: none;\n    }\n\n    @keyframes highlight-pulse {\n      0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-accent) 40%, transparent); }\n      70% { box-shadow: 0 0 0 8px transparent; }\n      100% { box-shadow: 0 0 0 0 transparent; }\n    }\n\n    /* Detail Panel — floating left sidebar */\n    .detail-panel {\n      position: fixed;\n      left: 0;\n      top: 57px;\n      width: 360px;\n      height: calc(100vh - 57px);\n      overflow-y: auto;\n      background: var(--color-surface);\n      border-right: 1px solid var(--color-border);\n      padding: 20px 24px;\n      box-shadow: 4px 0 16px rgba(0,0,0,0.15);\n      z-index: 90;\n      animation: slideIn 0.2s ease-out;\n    }\n\n    @keyframes slideIn {\n      from { opacity: 0; transform: translateX(-12px); }\n      to { opacity: 1; transform: translateX(0); }\n    }\n\n    .detail-panel-header {\n      display: flex;\n      align-items: center;\n      justify-content: space-between;\n      margin-bottom: 12px;\n    }\n\n    .detail-panel-id {\n      font-size: 12px;\n      font-weight: 700;\n      font-family: var(--font-mono);\n      padding: 3px 8px;\n      border-radius: 8px;\n      color: white;\n    }\n\n    .detail-panel-id.us { background: var(--color-accent); }\n    .detail-panel-id.fr { background: var(--color-done); }\n    .detail-panel-id.sc { background: var(--color-inprogress); }\n\n    .detail-panel-close {\n      background: none;\n      border: none;\n      color: var(--color-text-secondary);\n      cursor: pointer;\n      font-size: 18px;\n      padding: 4px 8px;\n      border-radius: var(--radius-sm);\n      transition: background var(--transition-fast);\n    }\n\n    .detail-panel-close:hover {\n      background: var(--color-surface-hover);\n    }\n\n    .detail-panel-title {\n      font-size: 16px;\n      font-weight: 600;\n      color: var(--color-text);\n      margin-bottom: 12px;\n    }\n\n    .detail-panel-body {\n      font-size: 13px;\n      color: var(--color-text-secondary);\n      line-height: 1.7;\n      white-space: pre-wrap;\n    }\n\n    .detail-panel-body strong {\n      color: var(--color-text);\n    }\n\n    /* Storymap empty state */\n    .storymap-empty {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      justify-content: center;\n      padding: 60px 20px;\n      text-align: center;\n      color: var(--color-text-muted);\n      font-size: 13px;\n    }\n\n    /* ====== Constitution Tab ====== */\n    .constitution-view {\n      padding: 24px 28px;\n    }\n\n    .premise-section {\n      margin-bottom: 24px;\n      padding-bottom: 20px;\n      border-bottom: 1px solid var(--border);\n    }\n    .premise-title {\n      font-size: 1.1rem;\n      font-weight: 700;\n      margin: 0 0 12px;\n      color: var(--text);\n    }\n    .premise-heading {\n      font-size: 0.85rem;\n      font-weight: 600;\n      text-transform: uppercase;\n      letter-spacing: 0.04em;\n      color: var(--text-secondary);\n      margin: 14px 0 6px;\n    }\n    .premise-text {\n      font-size: 0.9rem;\n      line-height: 1.55;\n      color: var(--text);\n      margin: 0 0 6px;\n    }\n    .premise-list {\n      margin: 4px 0 8px 18px;\n      padding: 0;\n      font-size: 0.9rem;\n      line-height: 1.55;\n      color: var(--text);\n    }\n    .premise-list li {\n      margin-bottom: 3px;\n    }\n\n    .constitution-layout {\n      display: flex;\n      gap: 32px;\n      align-items: flex-start;\n    }\n\n    @media (max-width: 900px) {\n      .constitution-layout { flex-direction: column; }\n    }\n\n    .constitution-left {\n      flex: 1 1 50%;\n      min-width: 0;\n      display: flex;\n      align-items: flex-start;\n      justify-content: center;\n    }\n\n    .constitution-right {\n      flex: 1 1 40%;\n      min-width: 0;\n    }\n\n    .constitution-summary {\n      display: flex;\n      flex-direction: column;\n      gap: 2px;\n      margin-bottom: 20px;\n    }\n\n    .constitution-summary-item {\n      display: flex;\n      align-items: center;\n      gap: 10px;\n      padding: 6px 8px;\n      font-size: 13px;\n      font-weight: 500;\n      color: var(--color-text);\n      border-bottom: 1px solid var(--color-border-subtle);\n      cursor: pointer;\n      border-radius: var(--radius-sm);\n      transition: background var(--transition-fast);\n    }\n\n    .constitution-summary-item:last-child { border-bottom: none; }\n    .constitution-summary-item:hover { background: var(--color-surface-hover); }\n    .constitution-summary-item.selected { background: var(--color-surface-elevated); border-left: 3px solid var(--color-accent); }\n\n    .constitution-summary-item .principle-num {\n      font-family: var(--font-mono);\n      font-size: 11px;\n      color: var(--color-text-muted);\n      flex-shrink: 0;\n      width: 24px;\n    }\n\n    .constitution-summary-item .level-badge {\n      font-size: 9px;\n      font-weight: 700;\n      padding: 2px 5px;\n      border-radius: 3px;\n      text-transform: uppercase;\n      margin-left: auto;\n      flex-shrink: 0;\n      letter-spacing: 0.3px;\n    }\n\n    .level-badge.must { background: rgba(59, 130, 246, 0.15); color: var(--color-accent); }\n    .level-badge.should { background: rgba(245, 166, 35, 0.15); color: var(--color-inprogress); }\n    .level-badge.may { background: rgba(107, 113, 137, 0.15); color: var(--color-text-muted); }\n\n    .constitution-body {\n      display: flex;\n      gap: 24px;\n      align-items: flex-start;\n    }\n\n    @media (max-width: 768px) {\n      .constitution-body { flex-direction: column; }\n    }\n\n    .radar-container {\n      width: 100%;\n      max-width: 520px;\n    }\n\n    .radar-container svg {\n      width: 100%;\n      height: auto;\n      overflow: visible;\n    }\n\n    .radar-axis {\n      cursor: pointer;\n      transition: opacity var(--transition-fast);\n    }\n\n    .radar-axis:hover { opacity: 0.8; }\n    .radar-axis:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n\n    .detail-card {\n      background: var(--color-surface-elevated);\n      border: 1px solid var(--color-border);\n      border-radius: var(--radius-lg);\n      padding: 20px;\n      margin-top: 12px;\n      animation: cardEnter 0.25s ease;\n    }\n\n    .detail-card h3 {\n      font-size: 16px;\n      font-weight: 700;\n      margin-bottom: 4px;\n      color: var(--color-text);\n    }\n\n    .detail-card .detail-level {\n      margin-bottom: 12px;\n    }\n\n    .detail-card .detail-text {\n      font-size: 13px;\n      line-height: 1.6;\n      color: var(--color-text-secondary);\n      margin-bottom: 16px;\n    }\n\n    .detail-card .detail-rationale {\n      font-size: 12px;\n      line-height: 1.5;\n      color: var(--color-text-muted);\n      padding-top: 12px;\n      border-top: 1px solid var(--color-border-subtle);\n    }\n\n    .detail-card .detail-rationale strong {\n      color: var(--color-text-secondary);\n    }\n\n    .amendment-timeline {\n      margin-top: 24px;\n      padding: 16px 0;\n      border-top: 1px solid var(--color-border-subtle);\n    }\n\n    .amendment-timeline-label {\n      font-size: 11px;\n      font-weight: 600;\n      text-transform: uppercase;\n      letter-spacing: 0.5px;\n      color: var(--color-text-muted);\n      margin-bottom: 8px;\n    }\n\n    .amendment-timeline-content {\n      display: flex;\n      align-items: center;\n      gap: 12px;\n      font-size: 12px;\n      color: var(--color-text-secondary);\n    }\n\n    .amendment-timeline-dot {\n      width: 8px;\n      height: 8px;\n      border-radius: 50%;\n      background: var(--color-accent);\n      flex-shrink: 0;\n    }\n\n    .amendment-timeline-line {\n      flex: 1;\n      height: 2px;\n      background: var(--color-border);\n    }\n\n    /* ====== Plan View ====== */\n    .planview-view { padding: 24px 28px; }\n    .planview-section {\n      margin-bottom: 28px; background: var(--color-surface); border: 1px solid var(--color-border);\n      border-radius: var(--radius-lg); padding: 20px 24px; box-shadow: var(--shadow-column);\n    }\n    .planview-section-title {\n      font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px;\n      color: var(--color-text-secondary); margin-bottom: 16px;\n    }\n    /* Badge Wall */\n    .badge-wall { display: flex; flex-wrap: wrap; gap: 10px; }\n    .tech-badge {\n      display: inline-flex; flex-direction: column; padding: 10px 16px;\n      background: var(--color-surface-elevated); border: 1px solid var(--color-border-subtle);\n      border-radius: var(--radius-md); transition: all var(--transition-fast);\n      position: relative; cursor: default; box-shadow: 0 1px 3px rgba(0,0,0,0.15);\n      border-left: 3px solid var(--color-accent);\n    }\n    .tech-badge:hover { background: var(--color-surface-hover); border-color: var(--color-accent); box-shadow: var(--shadow-card); transform: translateY(-1px); }\n    .tech-badge-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-text-muted); margin-bottom: 3px; }\n    .tech-badge-value { font-size: 13px; font-weight: 600; color: var(--color-text); }\n    .tech-badge-tooltip {\n      display: none; position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);\n      background: var(--color-surface-elevated); border: 1px solid var(--color-border);\n      border-radius: var(--radius-sm); padding: 10px 14px; font-size: 12px; color: var(--color-text-secondary);\n      max-width: 300px; white-space: normal; z-index: 50; box-shadow: var(--shadow-card-hover);\n      pointer-events: none; line-height: 1.5;\n    }\n    .tech-badge:hover .tech-badge-tooltip { display: block; }\n    /* Tessl Tiles Panel */\n    .tessl-tiles { display: flex; flex-wrap: wrap; gap: 12px; }\n    .tessl-tile-card {\n      display: flex; flex-direction: column; padding: 14px 18px;\n      background: var(--color-surface-elevated); border: 1px solid var(--color-border-subtle);\n      border-radius: var(--radius-md); min-width: 200px;\n      border-left: 3px solid var(--color-done); box-shadow: 0 1px 3px rgba(0,0,0,0.15);\n      transition: all var(--transition-fast);\n    }\n    .tessl-tile-card:hover { box-shadow: var(--shadow-card); transform: translateY(-1px); }\n    .tessl-tile-name { font-size: 13px; font-weight: 600; color: var(--color-text); font-family: var(--font-mono); }\n    .tessl-tile-version { font-size: 11px; color: var(--color-text-muted); margin-top: 4px; }\n    .tessl-tile-eval { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border-subtle); }\n    .tessl-eval-score { font-size: 28px; font-weight: 700; color: var(--color-done); }\n    .tessl-eval-bar { height: 4px; background: var(--color-border); border-radius: 2px; margin-top: 6px; overflow: hidden; }\n    .tessl-eval-bar-fill { height: 100%; background: var(--color-done); border-radius: 2px; }\n    .tessl-eval-multiplier {\n      display: inline-block; margin-top: 6px; padding: 2px 10px; border-radius: 12px;\n      background: rgba(39, 201, 63, 0.15); color: var(--color-done); font-size: 11px; font-weight: 600;\n    }\n    /* File Structure Tree — VS Code style */\n    .file-tree { font-family: var(--font-mono); font-size: 13px; }\n    .file-tree-entry {\n      display: flex; align-items: center; height: 26px; padding: 0 8px;\n      border-radius: var(--radius-sm); transition: background var(--transition-fast);\n      cursor: default; gap: 0; white-space: nowrap;\n    }\n    .file-tree-entry:hover { background: var(--color-surface-hover); }\n    .file-tree-indent { display: inline-flex; flex-shrink: 0; }\n    .file-tree-guide {\n      width: 18px; height: 26px; position: relative; flex-shrink: 0;\n    }\n    .file-tree-guide::before {\n      content: ''; position: absolute; left: 8px; top: 0; bottom: 0;\n      width: 1px; background: var(--color-border-subtle);\n    }\n    .file-tree-chevron {\n      width: 18px; height: 26px; display: inline-flex; align-items: center; justify-content: center;\n      flex-shrink: 0; cursor: pointer; color: var(--color-text-muted);\n      transition: color var(--transition-fast); font-size: 11px; user-select: none;\n    }\n    .file-tree-chevron:hover { color: var(--color-accent); }\n    .file-tree-chevron-spacer { width: 18px; flex-shrink: 0; }\n    .file-tree-file-icon {\n      width: 18px; height: 26px; display: inline-flex; align-items: center; justify-content: center;\n      flex-shrink: 0; font-size: 14px;\n    }\n    .file-tree-file-icon.dir { color: var(--color-accent); }\n    .file-tree-file-icon.file { color: var(--color-text-muted); }\n    .file-tree-file-icon.file-js { color: #f0db4f; }\n    .file-tree-file-icon.file-json { color: #f0db4f; }\n    .file-tree-file-icon.file-md { color: var(--color-accent); }\n    .file-tree-file-icon.file-html { color: #e44d26; }\n    .file-tree-file-icon.file-css { color: #264de4; }\n    .file-tree-label {\n      display: flex; align-items: center; gap: 6px; flex-shrink: 0;\n    }\n    .file-tree-name { color: var(--color-text); white-space: nowrap; flex-shrink: 0; }\n    .file-tree-name.planned { color: var(--color-text-muted); }\n    .file-tree-status {\n      flex-shrink: 0; font-size: 10px; padding: 1px 6px; border-radius: 3px; font-weight: 600;\n    }\n    .file-tree-status.existing { color: var(--color-done); background: rgba(39,201,63,0.1); }\n    .file-tree-status.planned-tag { color: var(--color-text-muted); background: var(--color-surface-elevated); }\n    .file-tree-comment {\n      color: var(--color-text-muted); margin-left: auto; padding-left: 16px;\n      font-size: 12px; opacity: 0.5; overflow: hidden; text-overflow: ellipsis;\n      white-space: nowrap; min-width: 0; flex: 1 1 0;\n    }\n    .file-tree-comment.truncated { cursor: help !important; }\n    .file-tree-children { overflow: hidden; }\n    .file-tree-children.collapsed { display: none; }\n    /* Architecture Diagram */\n    .diagram-container { position: relative; }\n    .diagram-svg {\n      width: 100%; background: var(--color-bg); border: 1px solid var(--color-border);\n      border-radius: var(--radius-md); padding: 8px;\n    }\n    .diagram-node { cursor: pointer; transition: all var(--transition-fast); }\n    .diagram-node:hover { filter: brightness(1.2); }\n    .diagram-node-rect { rx: 10; ry: 10; stroke-width: 2; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); }\n    .diagram-node-label { font-family: var(--font-sans); font-size: 13px; font-weight: 600; fill: var(--color-text); }\n    .diagram-edge-line { stroke: var(--color-border); stroke-width: 2; fill: none; stroke-dasharray: 6 3; }\n    .diagram-edge-label {\n      font-family: var(--font-mono); font-size: 10px; fill: var(--color-text-muted);\n      paint-order: stroke; stroke: var(--color-bg); stroke-width: 4px;\n    }\n    .diagram-raw { font-family: var(--font-mono); font-size: 12px; white-space: pre; overflow-x: auto; padding: 16px; background: var(--color-bg); border: 1px solid var(--color-border); border-radius: var(--radius-md); color: var(--color-text-secondary); line-height: 1.4; }\n    /* Diagram legend */\n    .diagram-legend { display: flex; gap: 16px; margin-top: 12px; justify-content: center; }\n    .diagram-legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--color-text-muted); }\n    .diagram-legend-dot { width: 10px; height: 10px; border-radius: 3px; }\n    /* Plan empty states */\n    .planview-empty { text-align: center; padding: 48px 20px; color: var(--color-text-muted); }\n    .planview-empty-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--color-text-secondary); }\n    .planview-empty-text { font-size: 14px; line-height: 1.6; }\n\n    /* ====== Scrollbar ====== */\n    ::-webkit-scrollbar { width: 6px; height: 6px; }\n    ::-webkit-scrollbar-track { background: transparent; }\n    ::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 3px; }\n    ::-webkit-scrollbar-thumb:hover { background: var(--color-text-muted); }\n\n    /* ====== Analyze View ====== */\n    .analyze-view { padding: 24px; display: flex; flex-direction: column; gap: 24px; }\n\n    .analyze-empty {\n      display: flex; flex-direction: column; align-items: center; justify-content: center;\n      padding: 80px 40px; text-align: center;\n    }\n    .analyze-empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.4; }\n    .analyze-empty-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }\n    .analyze-empty-text { color: var(--color-text-secondary); max-width: 420px; }\n    .analyze-empty-text code { background: var(--color-surface-elevated); padding: 2px 8px; border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 13px; }\n\n    .analyze-section {\n      background: var(--color-surface); border: 1px solid var(--color-border);\n      border-radius: var(--radius-lg); overflow: hidden;\n    }\n    .analyze-section-header {\n      padding: 16px 20px; font-size: 14px; font-weight: 600; text-transform: uppercase;\n      letter-spacing: 0.05em; color: var(--color-text-secondary);\n      border-bottom: 1px solid var(--color-border);\n    }\n\n    /* Health Gauge */\n    .gauge-container {\n      display: flex; align-items: center; justify-content: center;\n      padding: 32px 20px; gap: 32px; flex-wrap: wrap;\n    }\n    .gauge-svg { width: 200px; height: 120px; }\n    .gauge-arc { fill: none; stroke-width: 18; stroke-linecap: round; }\n    .gauge-zone-red { stroke: #ff4757; }\n    .gauge-zone-yellow { stroke: #ffa502; }\n    .gauge-zone-green { stroke: #27c93f; }\n    .gauge-needle {\n      fill: var(--color-text); transition: transform 1s ease-out;\n    }\n    @media (prefers-reduced-motion: reduce) {\n      .gauge-needle { transition: none; }\n    }\n    .gauge-score {\n      font-size: 36px; font-weight: 700; text-anchor: middle;\n      fill: var(--color-text);\n    }\n    .gauge-label {\n      font-size: 12px; text-anchor: middle; fill: var(--color-text-secondary);\n    }\n    .gauge-breakdown {\n      display: flex; flex-direction: column; gap: 8px; min-width: 200px;\n    }\n    .gauge-factor {\n      display: flex; justify-content: space-between; align-items: center;\n      font-size: 13px; padding: 6px 12px;\n      background: var(--color-surface-elevated); border-radius: var(--radius-sm);\n    }\n    .gauge-factor-label { color: var(--color-text-secondary); display: flex; flex-direction: column; gap: 2px; }\n    .gauge-factor-value { font-weight: 600; font-family: var(--font-mono); white-space: nowrap; }\n    .gauge-factor-context { font-size: 11px; color: var(--color-text-muted); font-weight: 400; }\n    .gauge-factor-green { border-left: 3px solid #27c93f; }\n    .gauge-factor-yellow { border-left: 3px solid #ffa502; }\n    .gauge-factor-red { border-left: 3px solid #ff4757; }\n    .gauge-trend {\n      display: inline-flex; align-items: center; gap: 4px;\n      font-size: 14px; font-weight: 600; margin-left: 8px;\n    }\n    .gauge-trend-up { color: #27c93f; }\n    .gauge-trend-down { color: #ff4757; }\n    .gauge-na { font-size: 24px; color: var(--color-text-muted); text-align: center; padding: 40px; }\n\n    /* Heatmap */\n    .heatmap-wrapper { overflow-x: auto; padding: 16px 20px; }\n    .heatmap-table {\n      width: 100%; border-collapse: collapse; font-size: 13px;\n    }\n    .heatmap-table th {\n      padding: 10px 16px; font-weight: 600; text-transform: uppercase;\n      letter-spacing: 0.05em; font-size: 11px; color: var(--color-text-secondary);\n      border-bottom: 2px solid var(--color-border); text-align: center;\n    }\n    .heatmap-table th[scope=\"row\"] {\n      text-align: left; font-size: 13px; text-transform: none; letter-spacing: normal;\n      color: var(--color-text); max-width: 300px; white-space: nowrap;\n      overflow: hidden; text-overflow: ellipsis;\n    }\n    .heatmap-table td {\n      padding: 8px 16px; text-align: center; border-bottom: 1px solid var(--color-border-subtle);\n    }\n    .heatmap-cell {\n      display: inline-block; width: 28px; height: 28px; border-radius: 6px;\n      cursor: pointer; transition: transform var(--transition-fast);\n    }\n    .heatmap-cell:hover { transform: scale(1.15); }\n    .heatmap-cell:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }\n    .coverage-covered { background: #27c93f; }\n    .coverage-partial { background: #ffa502; }\n    .coverage-missing { background: #ff4757; }\n    .coverage-na { background: var(--color-text-muted); opacity: 0.4; }\n    .heatmap-refs {\n      display: none; font-size: 11px; color: var(--color-text-secondary);\n      padding: 4px 0; font-family: var(--font-mono);\n    }\n    .heatmap-refs.expanded { display: block; }\n    .heatmap-row { animation: fadeIn 0.3s ease forwards; opacity: 0; }\n    @keyframes fadeIn { to { opacity: 1; } }\n    @media (prefers-reduced-motion: reduce) {\n      .heatmap-row { animation: none; opacity: 1; }\n    }\n    .heatmap-empty {\n      padding: 40px; text-align: center; color: var(--color-text-muted); font-size: 14px;\n    }\n    .heatmap-desc {\n      font-size: 12px; color: var(--color-text-secondary); max-width: 400px;\n      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n    }\n    .heatmap-refs-inline { display: none; }\n    .heatmap-scroll { max-height: 500px; overflow-y: auto; }\n    .coverage-summary {\n      display: flex; gap: 16px; margin-bottom: 20px;\n    }\n    .cov-stat { flex: 1; }\n    .cov-stat-header {\n      display: flex; justify-content: space-between; align-items: baseline;\n      margin-bottom: 6px; font-size: 13px;\n    }\n    .cov-stat-label { font-weight: 600; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em; font-size: 11px; }\n    .cov-stat-value { font-weight: 700; font-family: var(--font-mono); font-size: 14px; }\n    .cov-bar {\n      height: 6px; background: var(--color-surface-elevated); border-radius: 3px; overflow: hidden;\n    }\n    .cov-bar-fill { height: 100%; border-radius: 3px; transition: width 0.8s ease-out; }\n    .cov-bar-green { background: #27c93f; }\n    .cov-bar-yellow { background: #ffa502; }\n    .cov-bar-red { background: #ff4757; }\n    .coverage-success {\n      text-align: center; padding: 24px; color: #27c93f; font-weight: 500; font-size: 14px;\n    }\n    .coverage-gaps-label {\n      font-size: 13px; font-weight: 600; color: var(--color-text-secondary);\n      margin-bottom: 12px;\n    }\n\n    /* Severity Table */\n    .severity-wrapper { padding: 0; }\n    .severity-controls {\n      display: flex; gap: 12px; padding: 12px 20px; align-items: center;\n      border-bottom: 1px solid var(--color-border-subtle);\n    }\n    .severity-filter {\n      background: var(--color-surface-elevated); color: var(--color-text);\n      border: 1px solid var(--color-border); border-radius: var(--radius-sm);\n      padding: 6px 12px; font-size: 13px; font-family: var(--font-sans);\n      cursor: pointer;\n    }\n    .severity-table {\n      width: 100%; border-collapse: collapse; font-size: 13px;\n    }\n    .severity-table th {\n      padding: 10px 16px; font-weight: 600; text-align: left;\n      border-bottom: 2px solid var(--color-border); cursor: pointer;\n      user-select: none; font-size: 12px; text-transform: uppercase;\n      letter-spacing: 0.05em; color: var(--color-text-secondary);\n    }\n    .severity-table th:hover { color: var(--color-text); }\n    .severity-table th:focus { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n    .severity-table td {\n      padding: 10px 16px; border-bottom: 1px solid var(--color-border-subtle);\n      vertical-align: top;\n    }\n    .severity-table tr[data-severity] { cursor: pointer; transition: background var(--transition-fast); }\n    .severity-table tr[data-severity]:hover { background: var(--color-surface-hover); }\n    .severity-table tr[data-severity]:focus { outline: 2px solid var(--color-accent); outline-offset: -2px; }\n    .severity-badge {\n      display: inline-block; padding: 2px 10px; border-radius: 12px;\n      font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;\n    }\n    .severity-critical { background: rgba(255,71,87,0.15); color: #ff4757; }\n    .severity-high { background: rgba(255,165,2,0.15); color: #ffa502; }\n    .severity-medium { background: rgba(255,193,7,0.15); color: #e0a800; }\n    .severity-low { background: rgba(52,152,219,0.15); color: #3498db; }\n    .severity-recommendation {\n      display: none; padding: 12px 16px; background: var(--color-surface-elevated);\n      font-size: 12px; color: var(--color-text-secondary); border-bottom: 1px solid var(--color-border-subtle);\n    }\n    .severity-recommendation.expanded { display: table-row; }\n    .severity-resolved td { text-decoration: line-through; opacity: 0.5; }\n    .severity-empty {\n      padding: 40px; text-align: center; color: #27c93f; font-size: 14px; font-weight: 500;\n    }\n    .severity-table-scroll { max-height: 500px; overflow-y: auto; }\n\n    /* Animations */\n    .analyze-section.slide-in {\n      animation: slideInUp 0.4s var(--transition-slow) forwards; opacity: 0;\n      transform: translateY(16px);\n    }\n    @keyframes slideInUp {\n      to { opacity: 1; transform: translateY(0); }\n    }\n    @media (prefers-reduced-motion: reduce) {\n      .analyze-section.slide-in { animation: none; opacity: 1; transform: none; }\n    }\n\n    /* Toast notification */\n    .toast {\n      position: fixed;\n      bottom: 24px;\n      left: 50%;\n      transform: translateX(-50%) translateY(20px);\n      background: var(--color-surface);\n      color: var(--color-text);\n      border: 1px solid var(--color-border);\n      border-radius: 8px;\n      padding: 10px 20px;\n      font-size: 13px;\n      box-shadow: var(--shadow-card-hover);\n      z-index: 200;\n      opacity: 0;\n      pointer-events: none;\n      transition: opacity 0.25s ease, transform 0.25s ease;\n    }\n    .toast.visible {\n      opacity: 1;\n      transform: translateX(-50%) translateY(0);\n      pointer-events: auto;\n    }\n\n  </style>\n</head>\n<body>\n  <div id=\"toast\" class=\"toast\"></div>\n  <!-- Header -->\n  <header class=\"header\" role=\"banner\">\n    <div class=\"header-left\">\n      <div class=\"logo\">\n        <div class=\"logo-icon\" aria-hidden=\"true\">D</div>\n        <span>IIKit Dashboard</span>\n      </div>\n      <div class=\"project-label\" id=\"projectLabel\" title=\"\"></div>\n      <div class=\"feature-selector\" role=\"navigation\" aria-label=\"Feature selector\">\n        <select id=\"featureSelect\" aria-label=\"Select feature to display\" tabindex=\"0\">\n          <option value=\"\">Loading features...</option>\n        </select>\n      </div>\n    </div>\n    <div class=\"header-right\">\n      <div id=\"integrityBadge\" class=\"integrity-badge missing\" role=\"status\" aria-label=\"Test integrity status\">\n        <span class=\"integrity-dot\" aria-hidden=\"true\"></span>\n        <span class=\"integrity-text\">Checking...</span>\n      </div>\n      <button onclick=\"location.reload()\" class=\"theme-toggle\" aria-label=\"Refresh dashboard\" title=\"Refresh (re-read data)\" tabindex=\"0\" style=\"margin-right:4px;\">\n        <span>&#8635;</span>\n      </button>\n      <button id=\"themeToggle\" class=\"theme-toggle\" aria-label=\"Toggle light/dark theme\" title=\"Toggle theme\" tabindex=\"0\">\n        <span id=\"themeIcon\">&#9790;</span>\n      </button>\n      <div id=\"activityIndicator\" role=\"status\" aria-label=\"Agent activity: idle\" title=\"Agent idle — no recent file changes\" style=\"display:none\"></div>\n    </div>\n  </header>\n\n  <!-- Pipeline Bar -->\n  <nav id=\"pipelineBar\" class=\"pipeline-bar\" role=\"navigation\" aria-label=\"IIKit workflow pipeline\">\n  </nav>\n\n  <!-- Content Area -->\n  <main class=\"content-area\" role=\"main\">\n    <div id=\"contentArea\">\n      <div class=\"board-container\">\n        <div id=\"board\" class=\"board\" role=\"region\" aria-label=\"Dashboard board\">\n          <div class=\"loading\" id=\"loadingState\">\n            <div class=\"loading-spinner\" aria-label=\"Loading board data\"></div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </main>\n\n  <script>\n    (function() {\n      'use strict';\n\n      // ====== State ======\n      let currentFeature = null;\n      let currentBoard = null;\n      let currentPipeline = null;\n      let currentChecklist = null;\n      let expandedChecklist = null;\n      let currentTestify = null;\n      let currentAnalyze = null;\n      let currentBugs = null;\n      let activeTab = localStorage.getItem('iikit-active-tab') || null;\n      let externalSankeyHighlight = null;\n      // DASHBOARD_DATA mode: when data is inlined by the generator\n      const staticMode = typeof window.DASHBOARD_DATA === 'object' && window.DASHBOARD_DATA !== null;\n      let previousCardColumns = {}; // Track card positions for animations\n\n      // ====== DOM References ======\n      const boardEl = document.getElementById('board');\n      const featureSelect = document.getElementById('featureSelect');\n      const integrityBadge = document.getElementById('integrityBadge');\n      const activityIndicator = document.getElementById('activityIndicator');\n      const loadingState = document.getElementById('loadingState');\n      const pipelineBar = document.getElementById('pipelineBar');\n      const contentArea = document.getElementById('contentArea');\n\n      // ====== Pipeline ======\n      const PHASE_ICONS = {\n        not_started: '',\n        in_progress: '&#9654;',\n        complete: '&#10003;',\n        skipped: '&#8212;',\n        available: '&#9679;'\n      };\n\n      const BUG_ICON_SMALL = '<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M8 2l1.88 1.88M14.12 3.88L16 2M9 7.13v-1a3 3 0 0 1 6 0v1M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6zM3.5 11H2M5.06 6.5l-1.5-1M22 11h-1.5M18.94 6.5l1.5-1M12 20v2\"/></svg>';\n\n      function getBugsBadgeHtml(bugsData) {\n        if (!bugsData || !bugsData.exists) return '';\n        const count = bugsData.summary.open;\n        const sev = bugsData.summary.highestOpenSeverity;\n\n        const colors = { critical: '#ff4757', high: '#ffa502', medium: '#f1c40f', low: '#6b7189' };\n        const textColors = { critical: '#fff', high: '#fff', medium: '#1a1a2e', low: '#fff' };\n\n        if (count === 0) {\n          return ' <span class=\"bug-count-badge muted\">0</span>';\n        }\n\n        const bg = colors[sev] || '#6b7189';\n        const fg = textColors[sev] || '#fff';\n        return ` <span class=\"bug-count-badge\" style=\"background:${bg};color:${fg}\">${count}</span>`;\n      }\n\n      function renderPipeline(pipeline) {\n        if (!pipeline || !pipeline.phases) return;\n        currentPipeline = pipeline;\n\n        pipelineBar.innerHTML = '';\n\n        pipeline.phases.forEach((phase, i) => {\n          // Add connector before each node (except the first)\n          if (i > 0) {\n            const connector = document.createElement('div');\n            connector.className = 'pipeline-connector';\n            // Color connector green if previous phase is complete\n            const prevPhase = pipeline.phases[i - 1];\n            if (prevPhase.status === 'complete') {\n              connector.classList.add('complete');\n            }\n            pipelineBar.appendChild(connector);\n          }\n\n          const node = document.createElement('button');\n          node.className = 'pipeline-node' + (phase.optional ? ' optional' : '');\n          if (activeTab === phase.id) node.classList.add('active');\n          node.setAttribute('tabindex', '0');\n          if (activeTab === phase.id) node.setAttribute('aria-current', 'true');\n\n          node.innerHTML = `\n            <div class=\"pipeline-dot ${phase.status}\">${PHASE_ICONS[phase.status] || ''}</div>\n            <span class=\"pipeline-label${phase.name.includes('\\n') ? ' pipeline-label-multiline' : ''}\">${phase.name.includes('\\n') ? phase.name.split('\\n').map(l => `<span class=\"pipeline-label-line\">${escapeHtml(l)}</span>`).join('') : escapeHtml(phase.name)}${phase.id === 'tasks' ? ' <span class=\"pipeline-redirect\" title=\"Opens Implement board\">&#x2197;</span>' : ''}</span>\n            <span class=\"pipeline-progress\">${phase.progress || ''}</span>\n          `;\n\n          node.addEventListener('click', () => switchTab(phase.id));\n          pipelineBar.appendChild(node);\n        });\n\n        // Bugs tab — standalone, no connector\n        const bugsGap = document.createElement('div');\n        bugsGap.className = 'pipeline-bugs-gap';\n        pipelineBar.appendChild(bugsGap);\n\n        const bugsTab = document.createElement('button');\n        bugsTab.className = 'bugs-tab' + (activeTab === 'bugs' ? ' active' : '');\n        bugsTab.setAttribute('data-view', 'bugs');\n        bugsTab.setAttribute('tabindex', '0');\n        if (activeTab === 'bugs') bugsTab.setAttribute('aria-current', 'true');\n\n        const badgeHtml = currentBugs ? getBugsBadgeHtml(currentBugs) : '';\n        bugsTab.innerHTML = `\n          <div class=\"pipeline-dot bugs-dot\">${BUG_ICON_SMALL}</div>\n          <span class=\"pipeline-label\">Bugs${badgeHtml}</span>\n        `;\n        if (currentBugs && !currentBugs.exists) {\n          bugsTab.classList.add('muted');\n        }\n        bugsTab.addEventListener('click', () => switchTab('bugs'));\n        pipelineBar.appendChild(bugsTab);\n      }\n\n      function switchTab(phaseId) {\n        if (phaseId === 'tasks') {\n          // Tasks tab redirects to Implement (kanban board) since tasks are shown there\n          activeTab = 'implement';\n          if (currentPipeline) renderPipeline(currentPipeline);\n          renderBoardView();\n          showToast('Tasks are managed on the Implement board');\n          return;\n        }\n\n        activeTab = phaseId;\n        localStorage.setItem('iikit-active-tab', phaseId);\n        // Re-render pipeline to update active state\n        if (currentPipeline) renderPipeline(currentPipeline);\n\n        if (phaseId === 'implement') {\n          renderBoardView();\n        } else if (phaseId === 'constitution') {\n          renderConstitutionView();\n        } else if (phaseId === 'spec') {\n          renderStoryMapView();\n        } else if (phaseId === 'plan') {\n          renderPlanView();\n        } else if (phaseId === 'clarify') {\n          renderClarifyView();\n        } else if (phaseId === 'checklist') {\n          renderChecklistView();\n        } else if (phaseId === 'testify') {\n          renderTestifyView();\n        } else if (phaseId === 'analyze') {\n          renderAnalyzeView();\n        } else if (phaseId === 'bugs') {\n          renderBugsView();\n        } else {\n          renderPlaceholderView(phaseId);\n        }\n      }\n\n      // ====== Cross-Panel Navigation ======\n      const CROSS_LINK_MOD_KEY = /Mac|iPhone|iPad/.test(navigator.platform || '') ? '\\u2318' : 'Ctrl';\n\n      async function navigateToPanel(panelId, entityId) {\n        switchTab(panelId);\n\n        // Sentinel elements per panel — poll until the panel has rendered\n        const sentinels = {\n          spec: '.graph-node', testify: '.sankey-node', implement: '.card',\n          checklist: '.checklist-item', clarify: '.clarify-entry', analyze: '.heatmap-row',\n          bugs: '.bug-row'\n        };\n        const sel = sentinels[panelId];\n        if (sel) {\n          for (let i = 0; i < 30; i++) {\n            if (contentArea.querySelector(sel)) break;\n            await new Promise(r => setTimeout(r, 50));\n          }\n        }\n\n        // Dispatch to panel-specific highlight\n        const highlighters = {\n          spec: highlightSpecEntity,\n          testify: highlightTestifyEntity,\n          implement: highlightBoardEntity,\n          checklist: highlightChecklistEntity,\n          clarify: highlightClarifyEntity,\n          bugs: highlightBugsEntity\n        };\n        const fn = highlighters[panelId];\n        if (fn) fn(entityId);\n      }\n\n      // Delegated Cmd/Ctrl+click handler for all cross-links\n      contentArea.addEventListener('click', (e) => {\n        if (!(e.metaKey || e.ctrlKey)) return;\n        const link = e.target.closest('[data-cross-target]');\n        if (link) {\n          e.preventDefault();\n          e.stopPropagation();\n          navigateToPanel(link.dataset.crossTarget, link.dataset.crossId);\n        }\n      });\n\n      // ====== Panel-Specific Highlight Functions ======\n\n      function highlightSpecEntity(entityId) {\n        // Only normalize US refs (US-2 → US2); FR/SC keep their hyphens (SC-007 stays SC-007)\n        const nodeId = entityId.replace(/^US-/i, 'US');\n        highlightGraphNode(nodeId);\n\n        // Scroll to story card (for US refs) or graph node (for FR/SC)\n        const card = contentArea.querySelector(`.story-card[data-story-id=\"${nodeId}\"]`);\n        if (card) {\n          card.scrollIntoView({ behavior: 'smooth', block: 'center' });\n          card.classList.add('highlighted');\n          setTimeout(() => card.classList.remove('highlighted'), 2000);\n          const story = currentStoryMap?.stories?.find(s => s.id === nodeId);\n          if (story) showDetailPanel(nodeId, 'us', story.title, story.body || '');\n          return;\n        }\n        const nodeEl = contentArea.querySelector(`.graph-node[data-id=\"${nodeId}\"]`);\n        if (nodeEl) {\n          nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n          const type = nodeId.startsWith('FR') ? 'fr' : 'sc';\n          showDetailPanel(nodeId, type, nodeId, nodeEl.dataset.desc || '');\n        }\n      }\n\n      function highlightTestifyEntity(entityId) {\n        if (externalSankeyHighlight) {\n          externalSankeyHighlight(entityId);\n        }\n        const nodeEl = contentArea.querySelector(`.sankey-node[data-node-id=\"${entityId}\"]`);\n        if (nodeEl) {\n          nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n        }\n      }\n\n      function highlightBoardEntity(entityId) {\n        // Task IDs (T prefix): find .task-id span by text, expand list, highlight\n        if (/^T\\d+$/i.test(entityId)) {\n          const taskIds = contentArea.querySelectorAll('.task-id');\n          for (const span of taskIds) {\n            if (span.textContent.trim() === entityId) {\n              const taskItem = span.closest('.task-item');\n              const taskList = span.closest('.task-list');\n              // Expand collapsed task list\n              if (taskList && taskList.classList.contains('collapsed')) {\n                const btn = taskList.previousElementSibling;\n                if (btn && btn.classList.contains('task-toggle')) {\n                  btn.click();\n                }\n              }\n              if (taskItem) {\n                taskItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n                taskItem.classList.add('highlighted');\n                setTimeout(() => taskItem.classList.remove('highlighted'), 2000);\n              }\n              return;\n            }\n          }\n        }\n\n        // Story IDs (US prefix): find .card[data-card-id]\n        const card = contentArea.querySelector(`.card[data-card-id=\"${entityId}\"]`);\n        if (card) {\n          card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n          card.classList.add('highlighted');\n          setTimeout(() => card.classList.remove('highlighted'), 2000);\n        }\n      }\n\n      function highlightChecklistEntity(entityId) {\n        // Search tags and inline cross-link spans for the entity ID\n        const matches = contentArea.querySelectorAll(`.checklist-item-tag, .checklist-item .cross-link[data-cross-id=\"${entityId}\"]`);\n        for (const el of matches) {\n          if (el.dataset.crossId === entityId || el.textContent.trim() === entityId) {\n            const item = el.closest('.checklist-item');\n            // Expand parent section if collapsed\n            const detail = el.closest('.checklist-detail');\n            if (detail && !detail.classList.contains('open')) {\n              const filename = detail.id?.replace('checklist-detail-', '');\n              if (filename && currentChecklist) {\n                toggleChecklistExpand(filename, currentChecklist);\n                // Re-query after re-render\n                setTimeout(() => highlightChecklistEntity(entityId), 100);\n                return;\n              }\n            }\n            if (item) {\n              item.scrollIntoView({ behavior: 'smooth', block: 'center' });\n              item.classList.add('highlighted');\n              setTimeout(() => item.classList.remove('highlighted'), 2000);\n            }\n            return;\n          }\n        }\n      }\n\n      function highlightClarifyEntity(entityId) {\n        const ref = contentArea.querySelector(`.clarify-ref[data-ref-id=\"${entityId}\"]`);\n        if (ref) {\n          const entry = ref.closest('.clarify-entry');\n          if (entry) {\n            entry.scrollIntoView({ behavior: 'smooth', block: 'center' });\n            entry.classList.add('highlighted');\n            setTimeout(() => entry.classList.remove('highlighted'), 2000);\n          }\n        }\n      }\n\n      function renderBoardView() {\n        contentArea.innerHTML = `\n          <div class=\"board-container\">\n            <div class=\"cross-link-tip\">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>\n            <div id=\"board\" class=\"board\" role=\"region\" aria-label=\"Dashboard board\"></div>\n          </div>`;\n        // Re-assign boardEl reference\n        const newBoardEl = document.getElementById('board');\n        if (currentBoard) {\n          renderBoardInto(newBoardEl, currentBoard);\n        }\n      }\n\n      // ====== Bugs View (T014, T015, T016) ======\n\n      async function renderBugsView() {\n        if (!currentFeature) return;\n\n        contentArea.innerHTML = '<div class=\"loading\">Loading bug data...</div>';\n        try {\n          let data;\n          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {\n            data = window.DASHBOARD_DATA.featureData[currentFeature].bugs;\n          } else {\n            const res = await fetch(`/api/bugs/${currentFeature}`);\n            if (!res.ok) throw new Error(`HTTP ${res.status}`);\n            data = await res.json();\n          }\n          currentBugs = data;\n          if (currentPipeline) renderPipeline(currentPipeline);\n          renderBugsContent(data);\n        } catch (err) {\n          contentArea.innerHTML = `<div class=\"error-message\">Error loading bugs: ${escapeHtml(err.message)}</div>`;\n        }\n      }\n\n      function renderBugsContent(data) {\n        if (!data || !data.exists || data.bugs.length === 0) {\n          // T015: Empty state\n          const msg = !data || !data.exists\n            ? 'No bugs have been reported for this feature.'\n            : 'All clear — no bug entries found.';\n          contentArea.innerHTML = `\n            <div class=\"bugs-empty\">\n              <div class=\"bugs-empty-icon\">&#10003;</div>\n              <div class=\"bugs-empty-title\">No Bugs</div>\n              <div class=\"bugs-empty-text\">${msg}</div>\n            </div>`;\n          return;\n        }\n\n        const { bugs, orphanedTasks, summary, repoUrl } = data;\n\n        let tableRows = '';\n        for (const bug of bugs) {\n          // Fix task progress\n          const progressText = bug.fixTasks.total > 0\n            ? `${bug.fixTasks.checked}/${bug.fixTasks.total}`\n            : '\\u2014';\n\n          // GitHub link (T016)\n          let githubHtml = '\\u2014';\n          if (bug.githubIssue) {\n            const resolvedUrl = resolveGitHubUrl(bug.githubIssue, repoUrl);\n            if (resolvedUrl) {\n              githubHtml = `<a href=\"${escapeHtml(resolvedUrl)}\" class=\"github-link bug-github-link\" target=\"_blank\" rel=\"noopener\">${escapeHtml(bug.githubIssue)} <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" style=\"vertical-align:middle\"><path d=\"M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3\"/></svg></a>`;\n            } else {\n              githubHtml = escapeHtml(bug.githubIssue);\n            }\n          }\n\n          // Fix task list for cross-links\n          let taskLinks = '';\n          if (bug.fixTasks.total > 0) {\n            taskLinks = bug.fixTasks.tasks.map(t =>\n              `<span class=\"task-id cross-link\" data-cross-target=\"implement\" data-cross-id=\"${t.id}\" style=\"cursor:pointer;color:var(--color-accent);font-family:monospace;font-size:11px;\">${t.id}</span>`\n            ).join(', ');\n          }\n\n          tableRows += `\n            <tr class=\"bug-row\" data-bug-id=\"${bug.id}\">\n              <td class=\"bug-id\">${bug.id}</td>\n              <td><span class=\"severity-badge severity-${bug.severity}\">${bug.severity}</span></td>\n              <td class=\"bug-status ${bug.status === 'fixed' ? 'fixed' : ''}\">${bug.status}</td>\n              <td class=\"bug-progress\">${progressText}${taskLinks ? '<br>' + taskLinks : ''}</td>\n              <td class=\"bug-description\" title=\"${escapeHtml(bug.description || '')}\">${escapeHtml(bug.description || '\\u2014')}</td>\n              <td>${githubHtml}</td>\n            </tr>`;\n        }\n\n        // Orphaned tasks section (TS-020)\n        let orphanedHtml = '';\n        if (orphanedTasks && orphanedTasks.length > 0) {\n          orphanedHtml = `\n            <div class=\"orphaned-section\">\n              <div class=\"orphaned-title\">&#9888; Orphaned Fix Tasks (no matching bug in bugs.md)</div>\n              ${orphanedTasks.map(t => `\n                <div class=\"orphaned-task\">\n                  <span class=\"task-id cross-link\" data-cross-target=\"implement\" data-cross-id=\"${t.id}\" style=\"cursor:pointer;color:var(--color-accent);\">${t.id}</span>\n                  [${t.bugTag}] ${escapeHtml(t.description)}\n                </div>\n              `).join('')}\n            </div>`;\n        }\n\n        contentArea.innerHTML = `\n          <div class=\"bugs-view\">\n            <div class=\"bugs-view-header\">\n              <div class=\"bugs-view-title\">Bug Tracking</div>\n              <div class=\"bugs-view-subtitle\">${summary.total} total &middot; ${summary.open} open &middot; ${summary.fixed} fixed</div>\n            </div>\n            <div class=\"cross-link-tip\">Tip: ${CROSS_LINK_MOD_KEY}+click task IDs to navigate to the Implement board</div>\n            <table class=\"bugs-table\" role=\"table\" aria-label=\"Bug status table\">\n              <thead>\n                <tr>\n                  <th>Bug ID</th>\n                  <th aria-sort=\"descending\">Severity</th>\n                  <th>Status</th>\n                  <th>Fix Tasks</th>\n                  <th>Description</th>\n                  <th>GitHub</th>\n                </tr>\n              </thead>\n              <tbody>${tableRows}</tbody>\n            </table>\n            ${orphanedHtml}\n          </div>`;\n      }\n\n      // T016: Client-side GitHub URL resolution\n      function resolveGitHubUrl(issueRef, repoUrl) {\n        if (!issueRef || !repoUrl) return null;\n        const match = issueRef.match(/#(\\d+)/);\n        if (!match) return null;\n        let base = repoUrl.replace(/\\.git$/, '');\n        const ssh = base.match(/^git@([^:]+):(.+)$/);\n        if (ssh) base = 'https://' + ssh[1] + '/' + ssh[2];\n        return base + '/issues/' + match[1];\n      }\n\n      // T021: Highlight a bug row in the Bugs tab\n      function highlightBugsEntity(entityId) {\n        const row = contentArea.querySelector(`tr[data-bug-id=\"${entityId}\"]`);\n        if (row) {\n          row.scrollIntoView({ behavior: 'smooth', block: 'center' });\n          row.classList.add('highlighted');\n          setTimeout(() => row.classList.remove('highlighted'), 2000);\n        }\n      }\n\n      function renderPlaceholderView(phaseId) {\n        const phaseNames = {\n          constitution: 'Constitution', spec: 'Specification', clarify: 'Clarification',\n          plan: 'Plan', checklist: 'Checklist', testify: 'Test Specs',\n          tasks: 'Tasks', analyze: 'Analysis', implement: 'Implementation'\n        };\n        const name = phaseNames[phaseId] || phaseId;\n        contentArea.innerHTML = `\n          <div class=\"placeholder-view\">\n            <div class=\"placeholder-view-title\">${name} View</div>\n            <div class=\"placeholder-view-text\">This phase visualization is coming soon. It will be available in a future update.</div>\n          </div>`;\n      }\n\n      // ====== Checklist Quality Gates View ======\n\n      async function renderChecklistView() {\n        if (!currentFeature) return;\n\n        try {\n          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {\n            currentChecklist = window.DASHBOARD_DATA.featureData[currentFeature].checklist;\n          } else {\n            const res = await fetch(`/api/checklist/${currentFeature}`);\n            if (!res.ok) throw new Error('Failed to load');\n            currentChecklist = await res.json();\n          }\n          expandedChecklist = null;\n          renderChecklistContent(currentChecklist);\n        } catch {\n          contentArea.innerHTML = '<div class=\"checklist-empty\"><div class=\"checklist-empty-title\">Failed to load checklist data.</div></div>';\n        }\n      }\n\n      function renderChecklistContent(data) {\n        if (!data) return;\n\n        // Empty state\n        if (!data.files || data.files.length === 0) {\n          contentArea.innerHTML = `\n            <div class=\"checklist-empty\">\n              <div class=\"checklist-empty-icon\">&#9744;</div>\n              <div class=\"checklist-empty-title\">No checklists generated for this feature</div>\n              <div class=\"checklist-empty-text\">Run <code>/iikit-04-checklist</code> to generate domain-specific quality checklists for requirements validation.</div>\n            </div>`;\n          return;\n        }\n\n        const CIRCUMFERENCE = 2 * Math.PI * 42; // radius=42 for viewBox 100x100\n\n        let html = '<div class=\"checklist-view\">';\n\n        // Gate indicator\n        html += renderGateIndicator(data.gate);\n\n        // Progress rings\n        html += '<div class=\"checklist-rings\">';\n        data.files.forEach((file) => {\n          const isExpanded = expandedChecklist === file.filename;\n          const offset = CIRCUMFERENCE - (file.percentage / 100) * CIRCUMFERENCE;\n\n          html += `<div class=\"checklist-ring-wrapper${isExpanded ? ' expanded' : ''}\"\n            tabindex=\"0\" role=\"button\"\n            aria-expanded=\"${isExpanded}\"\n            aria-controls=\"checklist-detail-${escapeHtml(file.filename)}\"\n            aria-label=\"${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%\"\n            data-filename=\"${escapeHtml(file.filename)}\">\n            <svg class=\"checklist-ring-svg\" viewBox=\"0 0 100 100\" role=\"img\"\n              aria-label=\"${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%\">\n              <circle class=\"checklist-ring-track\" cx=\"50\" cy=\"50\" r=\"42\" />\n              <circle class=\"checklist-ring-fill ${file.color}\" cx=\"50\" cy=\"50\" r=\"42\"\n                stroke-dasharray=\"${CIRCUMFERENCE}\"\n                stroke-dashoffset=\"${offset}\" />\n              <text class=\"checklist-ring-pct\" x=\"50\" y=\"50\">${file.percentage}%</text>\n            </svg>\n            <div class=\"checklist-ring-name\">${escapeHtml(file.name)}</div>\n            <div class=\"checklist-ring-fraction\">${file.checked}/${file.total}</div>\n          </div>`;\n        });\n        html += '</div>';\n\n        html += `<div class=\"cross-link-tip\">Tip: ${CROSS_LINK_MOD_KEY}+click any FR/SC tag to navigate to its linked panel</div>`;\n\n        // Accordion detail panels\n        data.files.forEach((file) => {\n          const isExpanded = expandedChecklist === file.filename;\n          html += `<div id=\"checklist-detail-${escapeHtml(file.filename)}\" class=\"checklist-detail${isExpanded ? ' open' : ''}\">`;\n          html += '<div class=\"checklist-detail-inner\">';\n          html += renderChecklistDetail(file);\n          html += '</div></div>';\n        });\n\n        html += '</div>';\n        contentArea.innerHTML = html;\n\n        // Attach click and keyboard handlers to rings\n        document.querySelectorAll('.checklist-ring-wrapper').forEach(wrapper => {\n          const filename = wrapper.getAttribute('data-filename');\n          wrapper.addEventListener('click', () => toggleChecklistExpand(filename, data));\n          wrapper.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') {\n              e.preventDefault();\n              toggleChecklistExpand(filename, data);\n            }\n          });\n        });\n      }\n\n      function renderGateIndicator(gate) {\n        if (!gate) return '';\n        const levelClass = gate.level === 'yellow' ? 'blocked-yellow' : (gate.level === 'red' ? 'blocked-red' : 'open');\n        return `<div class=\"gate-indicator\" role=\"status\" aria-live=\"polite\">\n          <div class=\"gate-dot ${gate.level}\"></div>\n          <span class=\"gate-label ${levelClass}\">${escapeHtml(gate.label)}</span>\n        </div>`;\n      }\n\n      function renderChecklistDetail(file) {\n        if (!file || !file.items || file.items.length === 0) {\n          return '<div style=\"color:var(--color-text-muted);font-size:13px;\">No items detected in this checklist.</div>';\n        }\n\n        let html = '';\n        let currentCategory = null;\n\n        file.items.forEach(item => {\n          if (item.category !== currentCategory) {\n            currentCategory = item.category;\n            if (currentCategory) {\n              html += `<h4 class=\"checklist-category\">${escapeHtml(currentCategory)}</h4>`;\n            }\n          }\n\n          const icon = item.checked\n            ? '<span class=\"checklist-item-icon checked\">&#10003;</span>'\n            : '<span class=\"checklist-item-icon unchecked\">&#9744;</span>';\n\n          const chkId = item.chkId\n            ? `<span class=\"checklist-item-id\">${escapeHtml(item.chkId)}</span>`\n            : '';\n\n          const tags = (item.tags || []).map(t => {\n            const isCrossLink = /^(FR|SC)-?\\d+$/i.test(t);\n            if (isCrossLink) {\n              return `<span class=\"checklist-item-tag cross-link\" data-cross-target=\"spec\" data-cross-id=\"${escapeHtml(t)}\">${escapeHtml(t)}</span>`;\n            }\n            return `<span class=\"checklist-item-tag\">${escapeHtml(t)}</span>`;\n          }).join('');\n\n          const linkedText = escapeHtml(item.text).replace(\n            /\\b((?:FR|SC)-\\d+)\\b/g,\n            '<span class=\"cross-link\" data-cross-target=\"spec\" data-cross-id=\"$1\">$1</span>'\n          );\n\n          html += `<div class=\"checklist-item\">\n            ${icon}${chkId}\n            <span style=\"flex:1\">${linkedText}</span>\n            ${tags}\n          </div>`;\n        });\n\n        return html;\n      }\n\n      function toggleChecklistExpand(filename, data) {\n        if (expandedChecklist === filename) {\n          expandedChecklist = null;\n        } else {\n          expandedChecklist = filename;\n        }\n        renderChecklistContent(data);\n      }\n\n      // ====== Testify Traceability View ======\n\n      async function renderTestifyView() {\n        if (!currentFeature) return;\n\n        try {\n          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {\n            currentTestify = window.DASHBOARD_DATA.featureData[currentFeature].testify;\n            renderTestifyContent(currentTestify);\n            return;\n          }\n          const res = await fetch(`/api/testify/${currentFeature}`);\n          if (!res.ok) throw new Error('Failed to load');\n          currentTestify = await res.json();\n          renderTestifyContent(currentTestify);\n        } catch {\n          contentArea.innerHTML = '<div class=\"testify-empty\"><div class=\"testify-empty-title\">Failed to load testify data.</div></div>';\n        }\n      }\n\n      function renderTestifyContent(data) {\n        if (!data) return;\n\n        // Empty state\n        if (!data.exists) {\n          contentArea.innerHTML = `\n            <div class=\"testify-empty\">\n              <div class=\"testify-empty-icon\">&#128269;</div>\n              <div class=\"testify-empty-title\">No test specifications generated for this feature</div>\n              <div class=\"testify-empty-text\">Run <code>/iikit-05-testify</code> to generate test specifications with traceability links to requirements and tasks.</div>\n            </div>`;\n          return;\n        }\n\n        let html = '<div class=\"testify-view\">';\n\n        // Integrity seal\n        html += renderIntegritySeal(data.integrity);\n\n        html += `<div class=\"cross-link-tip\">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>`;\n\n        // Sankey diagram\n        html += renderSankeyDiagram(data);\n\n        html += '</div>';\n        contentArea.innerHTML = html;\n\n        // Attach hover and keyboard handlers after DOM insert\n        attachSankeyInteractions(data);\n      }\n\n      function renderIntegritySeal(integrity) {\n        const labels = { valid: 'Verified', tampered: 'Tampered', missing: 'Missing' };\n        const label = labels[integrity.status] || 'Unknown';\n        const hashDisplay = integrity.currentHash ? integrity.currentHash.substring(0, 12) + '...' : '';\n\n        return `<div class=\"testify-seal\" role=\"status\" aria-live=\"polite\" aria-label=\"Assertion integrity: ${label}\">\n          <div class=\"testify-seal-dot ${escapeHtml(integrity.status)}\"></div>\n          <span class=\"testify-seal-label ${escapeHtml(integrity.status)}\">${escapeHtml(label)}</span>\n          <span style=\"color: var(--color-text-muted); font-weight: 400;\">Assertion Integrity</span>\n          ${hashDisplay ? `<span class=\"testify-seal-hash\">${escapeHtml(hashDisplay)}</span>` : ''}\n        </div>`;\n      }\n\n      function renderSankeyDiagram(data) {\n        const { requirements, testSpecs, tasks, edges, gaps, pyramid } = data;\n\n        // Layout constants\n        const PAD = 20;\n        const COL_LEFT = 40;\n        const COL_MID = 440;\n        const COL_RIGHT = 840;\n        const NODE_W = 180;\n        const NODE_H = 32;\n        const NODE_GAP = 6;\n        const GROUP_PAD = 8;\n        const GROUP_GAP = 16;\n        const LABEL_Y = 20;\n        const COL_LABEL_Y = LABEL_Y;\n\n        // Compute positions for each column\n        const reqNodes = requirements.map((r, i) => ({\n          ...r, x: COL_LEFT, y: PAD + 30 + i * (NODE_H + NODE_GAP),\n          col: 'left', isGap: gaps.untestedRequirements.includes(r.id)\n        }));\n\n        // Middle column: group by pyramid type\n        const typeOrder = ['acceptance', 'contract', 'validation'];\n        const typeLabels = { acceptance: 'Acceptance', contract: 'Contract', validation: 'Validation' };\n        const typeColors = { acceptance: '#3B82F6', contract: '#22C55E', validation: '#8B5CF6' };\n        const midNodes = [];\n        const groupMeta = [];\n        let midY = PAD + 30;\n\n        for (const type of typeOrder) {\n          const ids = pyramid[type].ids;\n          const count = pyramid[type].count;\n          const groupStartY = midY;\n          const specs = ids.map(id => testSpecs.find(ts => ts.id === id)).filter(Boolean);\n\n          // Cluster width scales with count — narrower at top (acceptance), wider at bottom (validation)\n          const widthScale = type === 'acceptance' ? 0.7 : type === 'contract' ? 0.85 : 1.0;\n          const effectiveW = Math.round(NODE_W * widthScale);\n          const xOffset = COL_MID + Math.round((NODE_W - effectiveW) / 2);\n\n          for (let j = 0; j < specs.length; j++) {\n            midNodes.push({\n              ...specs[j], x: xOffset, y: midY + GROUP_PAD + j * (NODE_H + NODE_GAP),\n              w: effectiveW, col: 'mid', type,\n              isGap: gaps.unimplementedTests.includes(specs[j].id)\n            });\n          }\n\n          const groupH = count > 0\n            ? GROUP_PAD * 2 + count * NODE_H + (count - 1) * NODE_GAP\n            : GROUP_PAD * 2 + NODE_H; // min height for empty groups\n\n          groupMeta.push({\n            type, label: `${typeLabels[type]}: ${count}`,\n            x: xOffset - GROUP_PAD, y: groupStartY,\n            w: effectiveW + GROUP_PAD * 2, h: groupH,\n            color: typeColors[type]\n          });\n\n          midY += groupH + GROUP_GAP;\n        }\n\n        // Right column: tasks — mark completed tasks from board data\n        const checkedTaskIds = new Set();\n        if (currentBoard) {\n          for (const cards of [currentBoard.todo, currentBoard.in_progress, currentBoard.done]) {\n            for (const card of (cards || [])) {\n              for (const t of (card.tasks || [])) {\n                if (t.checked) checkedTaskIds.add(t.id);\n              }\n            }\n          }\n        }\n        const taskNodes = tasks.map((t, i) => ({\n          ...t, x: COL_RIGHT, y: PAD + 30 + i * (NODE_H + NODE_GAP),\n          col: 'right', isGap: false, isChecked: checkedTaskIds.has(t.id)\n        }));\n\n        // SVG height\n        const maxLeft = reqNodes.length > 0 ? reqNodes[reqNodes.length - 1].y + NODE_H + PAD : 100;\n        const maxMid = midY + PAD;\n        const maxRight = taskNodes.length > 0 ? taskNodes[taskNodes.length - 1].y + NODE_H + PAD : 100;\n        const svgH = Math.max(maxLeft, maxMid, maxRight, 200);\n        const svgW = COL_RIGHT + NODE_W + PAD + 40;\n\n        // Build node lookup for edge drawing\n        const nodeMap = {};\n        for (const n of reqNodes) nodeMap[n.id] = n;\n        for (const n of midNodes) nodeMap[n.id] = n;\n        for (const n of taskNodes) nodeMap[n.id] = n;\n\n        // Build directed edge indices for chain computation\n        // reqToTest: requirement ID -> [test spec IDs]\n        // testToReq: test spec ID -> [requirement IDs]\n        // testToTask: test spec ID -> [task IDs]\n        // taskToTest: task ID -> [test spec IDs]\n        const reqToTest = {};\n        const testToReq = {};\n        const testToTask = {};\n        const taskToTest = {};\n        for (const e of edges) {\n          if (e.type === 'requirement-to-test') {\n            if (!reqToTest[e.from]) reqToTest[e.from] = [];\n            reqToTest[e.from].push(e.to);\n            if (!testToReq[e.to]) testToReq[e.to] = [];\n            testToReq[e.to].push(e.from);\n          } else if (e.type === 'test-to-task') {\n            if (!testToTask[e.from]) testToTask[e.from] = [];\n            testToTask[e.from].push(e.to);\n            if (!taskToTest[e.to]) taskToTest[e.to] = [];\n            taskToTest[e.to].push(e.from);\n          }\n        }\n\n        const reqIdSet = new Set(requirements.map(r => r.id));\n        const tsIdSet = new Set(testSpecs.map(t => t.id));\n\n        // Compute directed chain: follows flow direction, not transitive BFS\n        function getDirectedChain(startId) {\n          const chain = new Set([startId]);\n\n          if (reqIdSet.has(startId)) {\n            // Requirement: go right → test specs → tasks\n            const linkedTests = reqToTest[startId] || [];\n            for (const tsId of linkedTests) {\n              chain.add(tsId);\n              for (const taskId of (testToTask[tsId] || [])) chain.add(taskId);\n            }\n          } else if (tsIdSet.has(startId)) {\n            // Test spec: go left → requirements, go right → tasks\n            for (const reqId of (testToReq[startId] || [])) chain.add(reqId);\n            for (const taskId of (testToTask[startId] || [])) chain.add(taskId);\n          } else {\n            // Task: go left → test specs → requirements\n            const linkedTests = taskToTest[startId] || [];\n            for (const tsId of linkedTests) {\n              chain.add(tsId);\n              for (const reqId of (testToReq[tsId] || [])) chain.add(reqId);\n            }\n          }\n\n          return chain;\n        }\n\n        let svg = `<svg class=\"testify-sankey-svg\" viewBox=\"0 0 ${svgW} ${svgH}\" role=\"img\"\n          aria-label=\"Traceability Sankey diagram: ${requirements.length} requirements, ${testSpecs.length} test specifications, ${tasks.length} tasks\">`;\n\n        // Column labels\n        svg += `<text class=\"sankey-col-label\" x=\"${COL_LEFT + NODE_W / 2}\" y=\"${COL_LABEL_Y}\" text-anchor=\"middle\">Requirements</text>`;\n        svg += `<text class=\"sankey-col-label\" x=\"${COL_MID + NODE_W / 2}\" y=\"${COL_LABEL_Y}\" text-anchor=\"middle\">Test Specs</text>`;\n        svg += `<text class=\"sankey-col-label\" x=\"${COL_RIGHT + NODE_W / 2}\" y=\"${COL_LABEL_Y}\" text-anchor=\"middle\">Tasks</text>`;\n\n        // Pyramid group backgrounds\n        for (const g of groupMeta) {\n          svg += `<rect class=\"sankey-group-bg\" x=\"${g.x}\" y=\"${g.y}\" width=\"${g.w}\" height=\"${g.h}\" fill=\"${g.color}\" fill-opacity=\"0.06\" stroke=\"${g.color}\" stroke-opacity=\"0.15\" stroke-width=\"1\"/>`;\n          svg += `<text class=\"sankey-group-label\" x=\"${g.x + g.w / 2}\" y=\"${g.y - 4}\" text-anchor=\"middle\" fill=\"${g.color}\">${escapeHtml(g.label)}</text>`;\n        }\n\n        // Flow bands (edges)\n        svg += '<g class=\"sankey-flows\">';\n        for (const e of edges) {\n          const src = nodeMap[e.from];\n          const tgt = nodeMap[e.to];\n          if (!src || !tgt) continue;\n\n          const srcW = src.w || NODE_W;\n          const tgtW = tgt.w || NODE_W;\n          const x1 = src.x + srcW;\n          const y1 = src.y + NODE_H / 2;\n          const x2 = tgt.x;\n          const y2 = tgt.y + NODE_H / 2;\n          const cx = (x1 + x2) / 2;\n\n          // Color by test type\n          let color = '#3B82F6';\n          if (e.type === 'requirement-to-test') {\n            const ts = testSpecs.find(t => t.id === e.to);\n            if (ts) color = typeColors[ts.type] || color;\n          } else if (e.type === 'test-to-task') {\n            const ts = testSpecs.find(t => t.id === e.from);\n            if (ts) color = typeColors[ts.type] || color;\n          }\n\n          const chainIds = [e.from, e.to].join(' ');\n          svg += `<path class=\"sankey-flow\" d=\"M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}\"\n            fill=\"none\" stroke=\"${color}\" data-chain-ids=\"${escapeHtml(chainIds)}\" data-from=\"${escapeHtml(e.from)}\" data-to=\"${escapeHtml(e.to)}\"/>`;\n        }\n        svg += '</g>';\n\n        // Render nodes\n        function renderNodeGroup(nodes, colClass) {\n          let s = `<g class=\"${colClass}\">`;\n          for (const n of nodes) {\n            const w = n.w || NODE_W;\n            const chain = Array.from(getDirectedChain(n.id)).join(' ');\n            const gapClass = n.isGap ? ' gap-node' : (n.isChecked ? ' task-checked' : '');\n            const isUntestedReq = gaps.untestedRequirements.includes(n.id);\n            const isUnimplTest = gaps.unimplementedTests.includes(n.id);\n            let ariaDesc = n.id;\n            if (n.traceability && n.traceability.length > 0) {\n              ariaDesc += ', linked from ' + n.traceability.join(', ');\n            }\n            // Find connections\n            const outgoing = edges.filter(e => e.from === n.id).map(e => e.to);\n            const incoming = edges.filter(e => e.to === n.id).map(e => e.from);\n            if (incoming.length > 0) ariaDesc += ', linked from ' + incoming.join(', ');\n            if (outgoing.length > 0) ariaDesc += ', linked to ' + outgoing.join(', ');\n            if (isUntestedReq) ariaDesc += ' (untested)';\n            if (isUnimplTest) ariaDesc += ' (unimplemented)';\n\n            const fillColor = n.isGap ? 'var(--color-surface-elevated)' : 'var(--color-surface-elevated)';\n            const strokeColor = n.isGap ? 'var(--color-tampered)' : 'var(--color-border)';\n            const label = n.id;\n            const desc = n.text || n.title || n.description || '';\n            const truncDesc = desc.length > 22 ? desc.substring(0, 22) + '...' : desc;\n\n            // Cross-link: requirements → spec, tests → spec (trace to requirement), tasks → implement\n            let crossAttr = '';\n            if (n.col === 'left') {\n              crossAttr = ` data-cross-target=\"spec\" data-cross-id=\"${escapeHtml(n.id)}\"`;\n            } else if (n.col === 'mid') {\n              // Test specs link to spec view (trace back to the requirement they verify)\n              const traceReq = Array.isArray(n.traceability) && n.traceability.length > 0 ? n.traceability[0] : n.id;\n              crossAttr = ` data-cross-target=\"spec\" data-cross-id=\"${escapeHtml(traceReq)}\"`;\n            } else if (n.col === 'right') {\n              crossAttr = ` data-cross-target=\"implement\" data-cross-id=\"${escapeHtml(n.id)}\"`;\n            }\n            s += `<g class=\"sankey-node${gapClass}\" tabindex=\"0\" role=\"button\"\n              data-node-id=\"${escapeHtml(n.id)}\" data-chain=\"${escapeHtml(chain)}\"${crossAttr}\n              aria-describedby=\"desc-${escapeHtml(n.id)}\">\n              <title>${escapeHtml(n.id)}: ${escapeHtml(desc)}${isUntestedReq ? ' (untested)' : ''}${isUnimplTest ? ' (unimplemented)' : ''}</title>\n              <desc id=\"desc-${escapeHtml(n.id)}\">${escapeHtml(ariaDesc)}</desc>\n              <rect class=\"sankey-node-rect\" x=\"${n.x}\" y=\"${n.y}\" width=\"${w}\" height=\"${NODE_H}\"\n                fill=\"${fillColor}\" stroke=\"${strokeColor}\"/>\n              <text class=\"sankey-node-label\" x=\"${n.x + 8}\" y=\"${n.y + 13}\">${escapeHtml(label)}</text>\n              <text class=\"sankey-node-desc\" x=\"${n.x + 8}\" y=\"${n.y + 25}\">${escapeHtml(truncDesc)}</text>\n            </g>`;\n          }\n          s += '</g>';\n          return s;\n        }\n\n        svg += renderNodeGroup(reqNodes, 'sankey-col-left');\n        svg += renderNodeGroup(midNodes, 'sankey-col-mid');\n        svg += renderNodeGroup(taskNodes, 'sankey-col-right');\n\n        // Empty column messages\n        if (requirements.length === 0) {\n          svg += `<text x=\"${COL_LEFT + NODE_W / 2}\" y=\"${PAD + 60}\" text-anchor=\"middle\" fill=\"var(--color-text-muted)\" font-size=\"11\">No requirements found in spec.md</text>`;\n        }\n        if (tasks.length === 0) {\n          svg += `<text x=\"${COL_RIGHT + NODE_W / 2}\" y=\"${PAD + 60}\" text-anchor=\"middle\" fill=\"var(--color-text-muted)\" font-size=\"11\">No tasks generated \\u2014 run /iikit-06-tasks</text>`;\n        }\n\n        svg += '</svg>';\n\n        return `<div class=\"testify-sankey-container\">${svg}</div>`;\n      }\n\n      function attachSankeyInteractions(data) {\n        const svgEl = contentArea.querySelector('.testify-sankey-svg');\n        if (!svgEl) return;\n\n        // Clear fade-in animations after they complete so .dimmed opacity can take effect\n        // (CSS animation fill-mode: both overrides normal opacity rules)\n        setTimeout(() => {\n          svgEl.querySelectorAll('.sankey-node, .sankey-flow').forEach(el => {\n            el.style.animation = 'none';\n          });\n        }, 1000);\n\n        const allNodes = svgEl.querySelectorAll('.sankey-node');\n        const allFlows = svgEl.querySelectorAll('.sankey-flow');\n        const allElements = [...allNodes, ...allFlows];\n        let lockedChain = null; // Click locks the highlight\n\n        // Expose highlight to cross-panel navigation\n        externalSankeyHighlight = (nodeId) => { lockedChain = nodeId; highlightChain(nodeId); };\n\n        function highlightChain(nodeId) {\n          // Get full chain from data-chain attribute\n          const nodeEl = svgEl.querySelector(`[data-node-id=\"${nodeId}\"]`);\n          if (!nodeEl) return;\n          const chainStr = nodeEl.getAttribute('data-chain') || '';\n          const chainIds = new Set(chainStr.split(' ').filter(Boolean));\n\n          // Dim everything, highlight chain members\n          for (const el of allNodes) {\n            const elId = el.getAttribute('data-node-id');\n            if (chainIds.has(elId)) {\n              el.classList.add('highlighted');\n              el.classList.remove('dimmed');\n            } else {\n              el.classList.add('dimmed');\n              el.classList.remove('highlighted');\n            }\n          }\n          for (const flow of allFlows) {\n            const from = flow.getAttribute('data-from');\n            const to = flow.getAttribute('data-to');\n            if (chainIds.has(from) && chainIds.has(to)) {\n              flow.classList.add('highlighted');\n              flow.classList.remove('dimmed');\n            } else {\n              flow.classList.add('dimmed');\n              flow.classList.remove('highlighted');\n            }\n          }\n        }\n\n        function clearHighlight() {\n          if (lockedChain) return; // Don't clear if a chain is click-locked\n          for (const el of allElements) {\n            el.classList.remove('dimmed', 'highlighted');\n          }\n        }\n\n        // Hover handlers on nodes\n        for (const node of allNodes) {\n          const nodeId = node.getAttribute('data-node-id');\n          node.addEventListener('mouseenter', () => {\n            if (!lockedChain) highlightChain(nodeId);\n          });\n          node.addEventListener('mouseleave', clearHighlight);\n          // Click: lock/unlock chain highlight (skip if Cmd/Ctrl for cross-navigation)\n          node.addEventListener('click', (e) => {\n            if (e.metaKey || e.ctrlKey) return;\n            if (lockedChain === nodeId) {\n              lockedChain = null;\n              clearHighlight();\n            } else {\n              lockedChain = nodeId;\n              highlightChain(nodeId);\n            }\n          });\n          // Keyboard: Enter/Space triggers chain highlight\n          node.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') {\n              e.preventDefault();\n              if (lockedChain === nodeId) {\n                lockedChain = null;\n                clearHighlight();\n              } else {\n                lockedChain = nodeId;\n                highlightChain(nodeId);\n              }\n            }\n          });\n        }\n\n        // Hover handlers on flows\n        for (const flow of allFlows) {\n          const from = flow.getAttribute('data-from');\n          flow.addEventListener('mouseenter', () => {\n            if (!lockedChain) highlightChain(from);\n          });\n          flow.addEventListener('mouseleave', clearHighlight);\n        }\n\n        // Click on empty SVG area clears locked chain\n        svgEl.addEventListener('click', (e) => {\n          if (e.target === svgEl || e.target.tagName === 'svg') {\n            lockedChain = null;\n            for (const el of allElements) {\n              el.classList.remove('dimmed', 'highlighted');\n            }\n          }\n        });\n      }\n\n      // ====== Spec Story Map View ======\n      let currentStoryMap = null;\n\n      async function renderStoryMapView() {\n        if (!currentFeature) return;\n\n        try {\n          if (!currentStoryMap) {\n            if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {\n              currentStoryMap = window.DASHBOARD_DATA.featureData[currentFeature].storyMap;\n            } else {\n              const res = await fetch(`/api/storymap/${currentFeature}`);\n              if (!res.ok) throw new Error('Failed to load');\n              currentStoryMap = await res.json();\n            }\n          }\n          renderStoryMapContent(currentStoryMap);\n        } catch {\n          contentArea.innerHTML = '<div class=\"storymap-empty\">Failed to load story map data.</div>';\n        }\n      }\n\n      function renderStoryMapContent(data) {\n        if (!data.stories.length && !data.requirements.length) {\n          contentArea.innerHTML = `\n            <div class=\"storymap-empty\">\n              <div class=\"placeholder-view-title\">No Specification Data</div>\n              <div>This feature's spec.md has no user stories or requirements yet.</div>\n            </div>`;\n          return;\n        }\n\n        contentArea.innerHTML = `\n          <div class=\"storymap-view\" role=\"region\" aria-label=\"Spec Story Map\">\n            <div class=\"storymap-main\">\n              <div class=\"cross-link-tip\">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>\n              <div class=\"storymap-section-title\">Story Map</div>\n              <div class=\"swim-lanes\" role=\"list\" aria-label=\"User stories by priority\"></div>\n              <div class=\"storymap-section-title\">Requirements Graph</div>\n              <div class=\"graph-container\">\n                <svg class=\"graph-svg\" aria-label=\"Requirements relationship graph\"></svg>\n                <div class=\"graph-tooltip\"></div>\n                <div class=\"graph-legend\">\n                  <div class=\"graph-legend-item\"><span class=\"graph-legend-dot us\"></span> User Story</div>\n                  <div class=\"graph-legend-item\"><span class=\"graph-legend-dot fr\"></span> Requirement</div>\n                  <div class=\"graph-legend-item\"><span class=\"graph-legend-dot sc\"></span> Success Criterion</div>\n                </div>\n              </div>\n            </div>\n          </div>\n          <div class=\"detail-panel-slot\"></div>`;\n\n        renderSwimLanes(data);\n        renderRequirementsGraph(data);\n      }\n\n      function renderSwimLanes(data) {\n        const container = contentArea.querySelector('.swim-lanes');\n        if (!container) return;\n\n        const lanes = { P1: [], P2: [], P3: [] };\n        for (const story of data.stories) {\n          const p = story.priority || 'P3';\n          if (!lanes[p]) lanes[p] = [];\n          lanes[p].push(story);\n        }\n\n        let html = '';\n        for (const [priority, stories] of Object.entries(lanes)) {\n          html += `<div class=\"swim-lane\" role=\"listitem\">\n            <div class=\"swim-lane-label ${priority.toLowerCase()}\">${priority}</div>\n            <div class=\"swim-lane-cards\">`;\n          for (const story of stories) {\n            const refs = (story.requirementRefs || []).map(r => `<span class=\"story-card-badge\">${escapeHtml(r)}</span>`).join('');\n            html += `<div class=\"story-card\" data-story-id=\"${escapeHtml(story.id)}\" data-cross-target=\"implement\" data-cross-id=\"${escapeHtml(story.id)}\" tabindex=\"0\" role=\"button\" aria-label=\"${escapeHtml(story.title)}\">\n              <div class=\"story-card-header\">\n                <span class=\"story-card-id\">${escapeHtml(story.id)}</span>\n                <span class=\"story-card-priority ${story.priority.toLowerCase()}\">${escapeHtml(story.priority)}</span>\n              </div>\n              <div class=\"story-card-title\">${escapeHtml(story.title)}</div>\n              <div class=\"story-card-meta\">\n                <span class=\"story-card-badge scenarios\">${story.scenarioCount || 0} scenario${(story.scenarioCount || 0) !== 1 ? 's' : ''}</span>\n                ${refs}\n              </div>\n            </div>`;\n          }\n          html += '</div></div>';\n        }\n        container.innerHTML = html;\n\n        // Story card click → highlight graph node + show detail (FR-016)\n        container.querySelectorAll('.story-card').forEach(card => {\n          card.addEventListener('click', (e) => {\n            if (e.metaKey || e.ctrlKey) return; // Let cross-link handler handle Cmd/Ctrl+click\n            const storyId = card.dataset.storyId;\n            highlightGraphNode(storyId);\n            const story = data.stories.find(s => s.id === storyId);\n            if (story) showDetailPanel(storyId, 'us', story.title, story.body || '');\n            const nodeEl = contentArea.querySelector(`.graph-node[data-id=\"${storyId}\"]`);\n            if (nodeEl) nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });\n          });\n          card.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.click(); }\n          });\n        });\n      }\n\n      function renderRequirementsGraph(data) {\n        const svg = contentArea.querySelector('.graph-svg');\n        if (!svg) return;\n\n        if (!data.requirements.length && !data.successCriteria.length) {\n          const container = svg.parentElement;\n          container.innerHTML = '<div class=\"graph-empty\">No requirements or success criteria defined yet.</div>' +\n            container.querySelector('.graph-legend')?.outerHTML || '';\n          return;\n        }\n\n        // Build nodes\n        const nodes = [];\n        const nodeMap = {};\n\n        for (const s of data.stories) {\n          const n = { id: s.id, type: 'us', label: s.id, desc: s.title, x: 0, y: 0, vx: 0, vy: 0 };\n          nodes.push(n);\n          nodeMap[s.id] = n;\n        }\n        for (const r of data.requirements) {\n          const n = { id: r.id, type: 'fr', label: r.id, desc: r.text, x: 0, y: 0, vx: 0, vy: 0 };\n          nodes.push(n);\n          nodeMap[r.id] = n;\n        }\n        for (const sc of data.successCriteria) {\n          const n = { id: sc.id, type: 'sc', label: sc.id, desc: sc.text, x: 0, y: 0, vx: 0, vy: 0 };\n          nodes.push(n);\n          nodeMap[sc.id] = n;\n        }\n\n        // Size SVG based on node count\n        const width = svg.clientWidth || 800;\n        const height = Math.min(width, Math.max(300, nodes.length * 30 + 100));\n        svg.style.height = height + 'px';\n        svg.setAttribute('viewBox', `0 0 ${width} ${height}`);\n\n        // Initial positions: spread nodes randomly within bounds\n        const pad = 50;\n        for (const n of nodes) {\n          n.x = pad + Math.random() * (width - 2 * pad);\n          n.y = pad + Math.random() * (height - 2 * pad);\n        }\n\n        // Build edges\n        const edges = data.edges.filter(e => nodeMap[e.from] && nodeMap[e.to]);\n\n        // Simple force-directed layout\n        for (let iter = 0; iter < 120; iter++) {\n          // Repulsion between all pairs\n          for (let i = 0; i < nodes.length; i++) {\n            for (let j = i + 1; j < nodes.length; j++) {\n              let dx = nodes[j].x - nodes[i].x;\n              let dy = nodes[j].y - nodes[i].y;\n              let dist = Math.sqrt(dx * dx + dy * dy) || 1;\n              let force = 3000 / (dist * dist);\n              let fx = (dx / dist) * force;\n              let fy = (dy / dist) * force;\n              nodes[i].vx -= fx;\n              nodes[i].vy -= fy;\n              nodes[j].vx += fx;\n              nodes[j].vy += fy;\n            }\n          }\n          // Attraction along edges\n          for (const e of edges) {\n            const a = nodeMap[e.from];\n            const b = nodeMap[e.to];\n            let dx = b.x - a.x;\n            let dy = b.y - a.y;\n            let dist = Math.sqrt(dx * dx + dy * dy) || 1;\n            let force = (dist - 100) * 0.05;\n            let fx = (dx / dist) * force;\n            let fy = (dy / dist) * force;\n            a.vx += fx;\n            a.vy += fy;\n            b.vx -= fx;\n            b.vy -= fy;\n          }\n          // Apply velocities with damping\n          for (const n of nodes) {\n            n.vx *= 0.7;\n            n.vy *= 0.7;\n            n.x += n.vx;\n            n.y += n.vy;\n            n.x = Math.max(pad, Math.min(width - pad, n.x));\n            n.y = Math.max(pad, Math.min(height - pad, n.y));\n          }\n        }\n\n        // Render edges\n        let svgContent = '';\n        for (const e of edges) {\n          const a = nodeMap[e.from];\n          const b = nodeMap[e.to];\n          svgContent += `<line class=\"graph-edge\" data-from=\"${e.from}\" data-to=\"${e.to}\" x1=\"${a.x}\" y1=\"${a.y}\" x2=\"${b.x}\" y2=\"${b.y}\"/>`;\n        }\n\n        // Render nodes\n        const radius = { us: 18, fr: 14, sc: 12 };\n        for (const n of nodes) {\n          const r = radius[n.type] || 14;\n          const crossTarget = n.type === 'us' ? 'implement' : 'testify';\n          svgContent += `<g class=\"graph-node graph-node-${n.type}\" data-id=\"${n.id}\" data-desc=\"${escapeHtml(n.desc)}\" data-cross-target=\"${crossTarget}\" data-cross-id=\"${n.id}\" tabindex=\"0\" role=\"button\" aria-label=\"${n.label}: ${escapeHtml(n.desc)}\">\n            <circle cx=\"${n.x}\" cy=\"${n.y}\" r=\"${r}\" stroke=\"var(--color-bg)\" stroke-width=\"2\"/>\n            <text x=\"${n.x}\" y=\"${n.y + r + 14}\">${n.label}</text>\n          </g>`;\n        }\n\n        svg.innerHTML = svgContent;\n\n        // Click-to-highlight + detail panel (FR-006)\n        svg.addEventListener('click', (e) => {\n          if (e.metaKey || e.ctrlKey) return; // Let cross-link handler handle Cmd/Ctrl+click\n          const nodeEl = e.target.closest('.graph-node');\n          if (nodeEl) {\n            const id = nodeEl.dataset.id;\n            highlightGraphNode(id);\n            // Show detail panel\n            const story = data.stories.find(s => s.id === id);\n            const req = data.requirements.find(r => r.id === id);\n            const sc = data.successCriteria.find(s => s.id === id);\n            if (story) showDetailPanel(id, 'us', story.title, story.body || '');\n            else if (req) showDetailPanel(id, 'fr', id, req.text);\n            else if (sc) showDetailPanel(id, 'sc', id, sc.text);\n          } else {\n            clearGraphHighlight();\n            closeDetailPanel();\n          }\n        });\n\n        // Tooltip on hover (FR-014)\n        const tooltip = contentArea.querySelector('.graph-tooltip');\n        svg.addEventListener('mouseover', (e) => {\n          const nodeEl = e.target.closest('.graph-node');\n          if (nodeEl && tooltip) {\n            tooltip.textContent = nodeEl.dataset.desc;\n            tooltip.style.display = 'block';\n          }\n        });\n        svg.addEventListener('mousemove', (e) => {\n          if (tooltip && tooltip.style.display === 'block') {\n            const rect = svg.parentElement.getBoundingClientRect();\n            tooltip.style.left = (e.clientX - rect.left + 12) + 'px';\n            tooltip.style.top = (e.clientY - rect.top - 8) + 'px';\n          }\n        });\n        svg.addEventListener('mouseout', (e) => {\n          if (!e.target.closest('.graph-node') && tooltip) {\n            tooltip.style.display = 'none';\n          }\n        });\n\n        // Drag nodes (FR-007)\n        let dragNode = null;\n        let dragOffset = { x: 0, y: 0 };\n\n        svg.addEventListener('mousedown', (e) => {\n          if (e.metaKey || e.ctrlKey) return; // Don't drag on Cmd/Ctrl+click\n          const nodeEl = e.target.closest('.graph-node');\n          if (!nodeEl) return;\n          e.preventDefault();\n          dragNode = nodeEl;\n          const circle = nodeEl.querySelector('circle');\n          const svgRect = svg.getBoundingClientRect();\n          const viewBox = svg.viewBox.baseVal;\n          const scaleX = viewBox.width / svgRect.width;\n          const scaleY = viewBox.height / svgRect.height;\n          dragOffset.x = parseFloat(circle.getAttribute('cx')) - (e.clientX - svgRect.left) * scaleX;\n          dragOffset.y = parseFloat(circle.getAttribute('cy')) - (e.clientY - svgRect.top) * scaleY;\n          svg.style.cursor = 'grabbing';\n        });\n\n        document.addEventListener('mousemove', (e) => {\n          if (!dragNode) return;\n          const svgRect = svg.getBoundingClientRect();\n          const viewBox = svg.viewBox.baseVal;\n          const scaleX = viewBox.width / svgRect.width;\n          const scaleY = viewBox.height / svgRect.height;\n          const nx = (e.clientX - svgRect.left) * scaleX + dragOffset.x;\n          const ny = (e.clientY - svgRect.top) * scaleY + dragOffset.y;\n          const circle = dragNode.querySelector('circle');\n          const text = dragNode.querySelector('text');\n          circle.setAttribute('cx', nx);\n          circle.setAttribute('cy', ny);\n          text.setAttribute('x', nx);\n          text.setAttribute('y', ny + parseFloat(circle.getAttribute('r')) + 14);\n          // Update connected edges\n          const nodeId = dragNode.dataset.id;\n          svg.querySelectorAll('.graph-edge').forEach(edge => {\n            if (edge.dataset.from === nodeId) { edge.setAttribute('x1', nx); edge.setAttribute('y1', ny); }\n            if (edge.dataset.to === nodeId) { edge.setAttribute('x2', nx); edge.setAttribute('y2', ny); }\n          });\n        });\n\n        document.addEventListener('mouseup', () => {\n          if (dragNode) {\n            dragNode = null;\n            svg.style.cursor = 'grab';\n          }\n        });\n\n        // Zoom/pan via wheel (FR-008)\n        svg.addEventListener('wheel', (e) => {\n          e.preventDefault();\n          const viewBox = svg.viewBox.baseVal;\n          const scale = e.deltaY > 0 ? 1.1 : 0.9;\n          const svgRect = svg.getBoundingClientRect();\n          const mx = ((e.clientX - svgRect.left) / svgRect.width) * viewBox.width + viewBox.x;\n          const my = ((e.clientY - svgRect.top) / svgRect.height) * viewBox.height + viewBox.y;\n          const nw = viewBox.width * scale;\n          const nh = viewBox.height * scale;\n          viewBox.x = mx - (mx - viewBox.x) * scale;\n          viewBox.y = my - (my - viewBox.y) * scale;\n          viewBox.width = nw;\n          viewBox.height = nh;\n        }, { passive: false });\n      }\n\n      function highlightGraphNode(nodeId) {\n        const svg = contentArea.querySelector('.graph-svg');\n        if (!svg) return;\n\n        // Build connected set\n        const connected = new Set([nodeId]);\n        svg.querySelectorAll('.graph-edge').forEach(edge => {\n          if (edge.dataset.from === nodeId) connected.add(edge.dataset.to);\n          if (edge.dataset.to === nodeId) connected.add(edge.dataset.from);\n        });\n\n        svg.querySelectorAll('.graph-node').forEach(n => {\n          const id = n.dataset.id;\n          n.classList.toggle('highlighted', id === nodeId);\n          n.classList.toggle('dimmed', !connected.has(id));\n        });\n        svg.querySelectorAll('.graph-edge').forEach(e => {\n          const isConnected = e.dataset.from === nodeId || e.dataset.to === nodeId;\n          e.classList.toggle('highlighted', isConnected);\n          e.classList.toggle('dimmed', !isConnected);\n        });\n      }\n\n      function clearGraphHighlight() {\n        const svg = contentArea.querySelector('.graph-svg');\n        if (!svg) return;\n        svg.querySelectorAll('.graph-node').forEach(n => { n.classList.remove('highlighted', 'dimmed'); });\n        svg.querySelectorAll('.graph-edge').forEach(e => { e.classList.remove('highlighted', 'dimmed'); });\n      }\n\n      function showDetailPanel(id, type, title, body) {\n        const slot = contentArea.querySelector('.detail-panel-slot');\n        if (!slot) return;\n\n        // Simple markdown-like rendering: bold, italic, numbered lists\n        const rendered = body\n          .replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>')\n          .replace(/\\*(.+?)\\*/g, '<em>$1</em>')\n          .replace(/^(\\d+)\\.\\s+/gm, '<br>$1. ');\n\n        // Check for related clarifications\n        let clarifyLink = '';\n        if (currentStoryMap && currentStoryMap.clarifications) {\n          const hasClarify = currentStoryMap.clarifications.some(c =>\n            c.refs && c.refs.some(r => r === id || r.replace(/^(US|FR|SC)-/i, (_, p) => p.toUpperCase()) === id)\n          );\n          if (hasClarify) {\n            clarifyLink = `<div style=\"margin-top:12px;padding-top:8px;border-top:1px solid var(--color-border-subtle)\">\n              <a href=\"#\" class=\"clarify-nav-link\" data-entity-id=\"${escapeHtml(id)}\" style=\"font-size:12px;color:var(--color-accent);cursor:pointer;text-decoration:underline\">View Clarifications &rarr;</a>\n            </div>`;\n          }\n        }\n\n        slot.innerHTML = `\n          <div class=\"detail-panel\" role=\"region\" aria-label=\"Detail view for ${escapeHtml(id)}\">\n            <div class=\"detail-panel-header\">\n              <span class=\"detail-panel-id ${type}\">${escapeHtml(id)}</span>\n              <button class=\"detail-panel-close\" aria-label=\"Close detail panel\">&times;</button>\n            </div>\n            <div class=\"detail-panel-title\">${escapeHtml(title)}</div>\n            <div class=\"detail-panel-body\">${rendered}</div>\n            ${clarifyLink}\n          </div>`;\n\n        slot.querySelector('.detail-panel-close').addEventListener('click', closeDetailPanel);\n        const clarifyNav = slot.querySelector('.clarify-nav-link');\n        if (clarifyNav) {\n          clarifyNav.addEventListener('click', (e) => {\n            e.preventDefault();\n            navigateToPanel('clarify', clarifyNav.dataset.entityId);\n          });\n        }\n        slot.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n      }\n\n      function closeDetailPanel() {\n        const slot = contentArea.querySelector('.detail-panel-slot');\n        if (slot) slot.innerHTML = '';\n      }\n\n      // ====== Clarify View ======\n      async function renderClarifyView() {\n        if (!currentFeature) {\n          contentArea.innerHTML = '<div class=\"placeholder-view\"><div class=\"placeholder-view-title\">Select a feature to view clarifications</div></div>';\n          return;\n        }\n\n        // Check if spec phase has been completed\n        const specPhase = currentPipeline?.phases?.find(p => p.id === 'spec');\n        if (specPhase && specPhase.status === 'not_started') {\n          contentArea.innerHTML = `\n            <div class=\"clarify-view\" role=\"region\" aria-label=\"Clarifications\">\n              <div class=\"clarify-empty\">\n                <div class=\"placeholder-view-title\">Specification Not Yet Created</div>\n                <div class=\"placeholder-view-text\">The Clarify phase refines an existing specification. Run <code>/iikit-01-specify</code> first to create the feature spec, then run <code>/iikit-02-clarify</code> to resolve ambiguities.</div>\n              </div>\n            </div>`;\n          return;\n        }\n\n        try {\n          if (!currentStoryMap) {\n            if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {\n              currentStoryMap = window.DASHBOARD_DATA.featureData[currentFeature].storyMap;\n            } else {\n              const res = await fetch(`/api/storymap/${currentFeature}`);\n              if (!res.ok) throw new Error('Failed to load');\n              currentStoryMap = await res.json();\n            }\n          }\n          renderClarifyContent(currentStoryMap.clarifications || []);\n        } catch {\n          contentArea.innerHTML = '<div class=\"placeholder-view\"><div class=\"placeholder-view-title\">Failed to load clarification data</div></div>';\n        }\n      }\n\n      function renderClarifyContent(clarifications) {\n        if (!clarifications.length) {\n          // TS-019/TS-020: Distinguish \"clarify was run, no issues\" from \"never run\"\n          const clarifyPhase = currentPipeline?.phases?.find(p => p.id === 'clarify');\n          const clarifyWasRun = clarifyPhase && (clarifyPhase.status === 'complete' || clarifyPhase.status === 'skipped');\n\n          const title = clarifyWasRun\n            ? 'Clarification Complete'\n            : 'Clarify Not Yet Run';\n          const text = clarifyWasRun\n            ? 'The clarification phase was completed with no ambiguities found. The specification is clear as written.'\n            : 'This is an optional phase. Run <code>/iikit-02-clarify</code> to identify and resolve ambiguities in the spec.';\n\n          contentArea.innerHTML = `\n            <div class=\"clarify-view\" role=\"region\" aria-label=\"Clarifications\">\n              <div class=\"clarify-empty\">\n                <div class=\"placeholder-view-title\">${title}</div>\n                <div class=\"placeholder-view-text\">${text}</div>\n              </div>\n            </div>`;\n          return;\n        }\n\n        // Group by session\n        const sessions = {};\n        for (const c of clarifications) {\n          if (!sessions[c.session]) sessions[c.session] = [];\n          sessions[c.session].push(c);\n        }\n\n        let html = `<div class=\"clarify-view\" role=\"region\" aria-label=\"Clarifications\">\n          <div class=\"clarify-header\">\n            <span class=\"clarify-title\">Clarification Trail</span>\n            <span class=\"clarify-count\">${clarifications.length} Q&amp;A${clarifications.length !== 1 ? 's' : ''}</span>\n          </div>\n          <div class=\"cross-link-tip\">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>\n          <div class=\"clarify-sessions\">`;\n\n        for (const [session, entries] of Object.entries(sessions)) {\n          html += `<div class=\"clarify-session\">\n            <div class=\"clarify-session-label\">Session ${escapeHtml(session)}</div>\n            <div class=\"clarify-entries\">`;\n          for (const c of entries) {\n            const refsHtml = (c.refs && c.refs.length)\n              ? `<div class=\"clarify-refs\">${c.refs.map(r => `<a class=\"clarify-ref\" href=\"#\" data-ref-id=\"${escapeHtml(r)}\">${escapeHtml(r)}</a>`).join('')}</div>`\n              : '';\n            html += `<div class=\"clarify-entry\">\n              <div class=\"clarify-question\"><span class=\"clarify-q-label\">Q</span> ${escapeHtml(c.question)}</div>\n              <div class=\"clarify-answer\"><span class=\"clarify-a-label\">A</span> ${escapeHtml(c.answer)}</div>\n              ${refsHtml}\n            </div>`;\n          }\n          html += '</div></div>';\n        }\n\n        html += '</div></div>';\n        contentArea.innerHTML = html;\n\n        // Wire up ref links to navigate to Spec tab and highlight the item\n        contentArea.querySelectorAll('.clarify-ref').forEach(link => {\n          link.addEventListener('click', (e) => {\n            e.preventDefault();\n            const refId = link.dataset.refId;\n            navigateToSpecItem(refId);\n          });\n        });\n      }\n\n      async function navigateToSpecItem(refId) {\n        // Thin wrapper — delegates to generic cross-panel navigation\n        navigateToPanel('spec', refId);\n      }\n\n      // ====== Constitution View ======\n      let currentConstitution = null;\n      let currentPremise = null;\n      let selectedPrinciple = null;\n\n      async function renderConstitutionView() {\n        try {\n          if (staticMode) {\n            if (!currentConstitution) currentConstitution = window.DASHBOARD_DATA.constitution;\n            if (!currentPremise) currentPremise = window.DASHBOARD_DATA.premise;\n          } else {\n            const fetches = [];\n            if (!currentConstitution) {\n              fetches.push(fetch('/api/constitution').then(r => r.json()).then(d => { currentConstitution = d; }));\n            }\n            if (!currentPremise) {\n              fetches.push(fetch('/api/premise').then(r => r.json()).then(d => { currentPremise = d; }));\n            }\n            await Promise.all(fetches);\n          }\n          renderConstitutionContent(currentConstitution);\n        } catch (err) {\n          console.error('Failed to load constitution:', err);\n          contentArea.innerHTML = '<div class=\"placeholder-view\"><div class=\"placeholder-view-title\">Failed to load constitution</div></div>';\n        }\n      }\n\n      function renderConstitutionContent(data) {\n        if (!data || !data.exists) {\n          contentArea.innerHTML = `\n            <div class=\"placeholder-view\">\n              <div class=\"placeholder-view-title\">No Constitution Found</div>\n              <div class=\"placeholder-view-text\">Run /iikit-00-constitution to define your project's governance principles.</div>\n            </div>`;\n          return;\n        }\n\n        const principles = data.principles;\n        if (principles.length === 0) {\n          contentArea.innerHTML = `\n            <div class=\"placeholder-view\">\n              <div class=\"placeholder-view-title\">No Principles Found</div>\n              <div class=\"placeholder-view-text\">Your CONSTITUTION.md exists but has no parseable principles.</div>\n            </div>`;\n          return;\n        }\n\n        // Summary list\n        const summaryHtml = principles.map((p, i) => {\n          const isSelected = selectedPrinciple && selectedPrinciple.number === p.number;\n          return `<div class=\"constitution-summary-item${isSelected ? ' selected' : ''}\" id=\"principle-item-${i}\"><span class=\"principle-num\">${escapeHtml(p.number)}.</span> ${escapeHtml(p.name)} <span class=\"level-badge ${p.level.toLowerCase()}\">${p.level}</span></div>`;\n        }).join('');\n\n        // Radar chart SVG\n        const radarSvg = generateRadarSVG(principles);\n\n        // Detail card — only shown when a principle is selected\n        const detailHtml = selectedPrinciple\n          ? `<div class=\"detail-card\">${renderDetailCard(selectedPrinciple)}</div>`\n          : '';\n\n        // Timeline\n        const timelineHtml = data.version ? renderTimeline(data.version) : '';\n\n        // Premise section\n        const premiseHtml = currentPremise && currentPremise.exists\n          ? `<div class=\"premise-section\">${renderPremiseContent(currentPremise.content)}</div>`\n          : '';\n\n        contentArea.innerHTML = `\n          <div class=\"constitution-view\">\n            ${premiseHtml}\n            <div class=\"constitution-layout\">\n              <div class=\"constitution-left\">\n                <div class=\"radar-container\">${radarSvg}</div>\n              </div>\n              <div class=\"constitution-right\">\n                <div class=\"constitution-summary\">${summaryHtml}</div>\n                ${detailHtml}\n              </div>\n            </div>\n            ${timelineHtml}\n          </div>`;\n\n        // Attach click handlers to radar axes AND list items\n        function selectPrinciple(p) {\n          selectedPrinciple = p;\n          renderConstitutionContent(data);\n        }\n\n        principles.forEach((p, i) => {\n          const axisEl = document.getElementById(`radar-axis-${i}`);\n          if (axisEl) {\n            axisEl.addEventListener('click', () => selectPrinciple(p));\n            axisEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectPrinciple(p); } });\n          }\n          const itemEl = document.getElementById(`principle-item-${i}`);\n          if (itemEl) {\n            itemEl.addEventListener('click', () => selectPrinciple(p));\n          }\n        });\n      }\n\n      function renderPremiseContent(markdown) {\n        // Simple markdown to HTML: headings, paragraphs, bold, lists\n        const lines = markdown.split('\\n');\n        let html = '';\n        let inList = false;\n        for (const line of lines) {\n          const trimmed = line.trim();\n          if (!trimmed) {\n            if (inList) { html += '</ul>'; inList = false; }\n            continue;\n          }\n          if (/^# /.test(trimmed)) {\n            if (inList) { html += '</ul>'; inList = false; }\n            html += `<h3 class=\"premise-title\">${escapeHtml(trimmed.slice(2))}</h3>`;\n          } else if (/^## /.test(trimmed)) {\n            if (inList) { html += '</ul>'; inList = false; }\n            html += `<h4 class=\"premise-heading\">${escapeHtml(trimmed.slice(3))}</h4>`;\n          } else if (/^- /.test(trimmed)) {\n            if (!inList) { html += '<ul class=\"premise-list\">'; inList = true; }\n            const content = trimmed.slice(2).replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n            html += `<li>${content}</li>`;\n          } else {\n            if (inList) { html += '</ul>'; inList = false; }\n            const content = trimmed.replace(/\\*\\*(.+?)\\*\\*/g, '<strong>$1</strong>');\n            html += `<p class=\"premise-text\">${content}</p>`;\n          }\n        }\n        if (inList) html += '</ul>';\n        return html;\n      }\n\n      function generateRadarSVG(principles) {\n        const size = 360;\n        const cx = size / 2;\n        const cy = size / 2;\n        const maxR = size / 2 - 60;\n        const levels = { 'MUST': 1, 'SHOULD': 0.66, 'MAY': 0.33 };\n        const n = principles.length;\n        const angleStep = (2 * Math.PI) / n;\n        const startAngle = -Math.PI / 2; // Start from top\n\n        // Ring lines (33%, 66%, 100%)\n        let rings = '';\n        [0.33, 0.66, 1].forEach(pct => {\n          const r = maxR * pct;\n          const points = [];\n          for (let i = 0; i < n; i++) {\n            const angle = startAngle + i * angleStep;\n            points.push(`${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`);\n          }\n          rings += `<polygon points=\"${points.join(' ')}\" fill=\"none\" stroke=\"var(--color-border)\" stroke-width=\"1\" opacity=\"0.5\"/>`;\n        });\n\n        // Axis lines from center to outer edge\n        let axes = '';\n        for (let i = 0; i < n; i++) {\n          const angle = startAngle + i * angleStep;\n          const x2 = cx + maxR * Math.cos(angle);\n          const y2 = cy + maxR * Math.sin(angle);\n          axes += `<line x1=\"${cx}\" y1=\"${cy}\" x2=\"${x2}\" y2=\"${y2}\" stroke=\"var(--color-border)\" stroke-width=\"1\" opacity=\"0.3\"/>`;\n        }\n\n        // Filled polygon connecting principle values\n        const valuePoints = principles.map((p, i) => {\n          const angle = startAngle + i * angleStep;\n          const r = maxR * (levels[p.level] || 0.66);\n          return `${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`;\n        });\n        const polygon = `<polygon points=\"${valuePoints.join(' ')}\" fill=\"var(--color-accent)\" fill-opacity=\"0.2\" stroke=\"var(--color-accent)\" stroke-width=\"2\"/>`;\n\n        // Value dots and clickable areas\n        let dots = '';\n        principles.forEach((p, i) => {\n          const angle = startAngle + i * angleStep;\n          const r = maxR * (levels[p.level] || 0.66);\n          const x = cx + r * Math.cos(angle);\n          const y = cy + r * Math.sin(angle);\n\n          // Label position (pushed further out)\n          const labelR = maxR + 24;\n          const lx = cx + labelR * Math.cos(angle);\n          const ly = cy + labelR * Math.sin(angle);\n          const anchor = Math.abs(Math.cos(angle)) < 0.1 ? 'middle' : Math.cos(angle) > 0 ? 'start' : 'end';\n\n          const isSelected = selectedPrinciple && selectedPrinciple.number === p.number;\n\n          dots += `\n            <g id=\"radar-axis-${i}\" class=\"radar-axis\" tabindex=\"0\" role=\"button\"\n               aria-label=\"${escapeHtml(p.name)} (${p.level})\">\n              <circle cx=\"${x}\" cy=\"${y}\" r=\"${isSelected ? 7 : 5}\" fill=\"var(--color-accent)\" stroke=\"${isSelected ? 'var(--color-text)' : 'none'}\" stroke-width=\"2\"/>\n              <circle cx=\"${x}\" cy=\"${y}\" r=\"16\" fill=\"transparent\"/>\n              <text x=\"${lx}\" y=\"${ly}\" text-anchor=\"${anchor}\" dominant-baseline=\"middle\"\n                    font-size=\"11\" font-weight=\"600\" fill=\"var(--color-text-secondary)\"\n                    style=\"letter-spacing: 0.2px;\">${escapeHtml(p.name)}</text>\n            </g>`;\n        });\n\n        const ariaLabel = `Radar chart showing ${n} constitution principles: ${principles.map(p => p.name + ' (' + p.level + ')').join(', ')}`;\n\n        return `<svg viewBox=\"0 0 ${size} ${size}\" role=\"img\" aria-label=\"${escapeHtml(ariaLabel)}\">${rings}${axes}${polygon}${dots}</svg>`;\n      }\n\n      function renderDetailCard(principle) {\n        // Strip rationale from main text to avoid duplication\n        let mainText = principle.text;\n        if (principle.rationale) {\n          const ratIdx = mainText.indexOf('**Rationale**');\n          if (ratIdx > -1) mainText = mainText.substring(0, ratIdx);\n        }\n        mainText = mainText.trim();\n\n        return `\n          <h3>${escapeHtml(principle.number)}. ${escapeHtml(principle.name)}</h3>\n          <div class=\"detail-level\"><span class=\"level-badge ${principle.level.toLowerCase()}\">${principle.level}</span></div>\n          <div class=\"detail-text\">${escapeHtml(mainText).replace(/\\n\\n/g, '<br><br>').replace(/\\n/g, ' ')}</div>\n          ${principle.rationale ? `<div class=\"detail-rationale\"><strong>Rationale:</strong> ${escapeHtml(principle.rationale)}</div>` : ''}`;\n      }\n\n      function renderTimeline(version) {\n        return `\n          <div class=\"amendment-timeline\">\n            <div class=\"amendment-timeline-label\">Amendment History</div>\n            <div class=\"amendment-timeline-content\">\n              <div class=\"amendment-timeline-dot\"></div>\n              <span>v${escapeHtml(version.version)} &mdash; Ratified ${escapeHtml(version.ratified)}</span>\n              ${version.ratified !== version.lastAmended ? `\n                <div class=\"amendment-timeline-line\"></div>\n                <div class=\"amendment-timeline-dot\"></div>\n                <span>Amended ${escapeHtml(version.lastAmended)}</span>\n              ` : ''}\n            </div>\n          </div>`;\n      }\n\n      // ====== Analyze Consistency View ======\n\n      async function renderAnalyzeView() {\n        if (!currentFeature) return;\n        try {\n          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {\n            currentAnalyze = window.DASHBOARD_DATA.featureData[currentFeature].analyze;\n          } else {\n            const res = await fetch(`/api/analyze/${currentFeature}`);\n            if (!res.ok) throw new Error('Failed to load');\n            currentAnalyze = await res.json();\n          }\n          renderAnalyzeContent(currentAnalyze);\n        } catch {\n          contentArea.innerHTML = '<div class=\"analyze-empty\"><div class=\"analyze-empty-title\">Failed to load analyze data.</div></div>';\n        }\n      }\n\n      function renderAnalyzeContent(data) {\n        if (!data) return;\n\n        if (!data.exists) {\n          contentArea.innerHTML = `\n            <div class=\"analyze-empty\">\n              <div class=\"analyze-empty-icon\">&#128270;</div>\n              <div class=\"analyze-empty-title\">No analysis data for this feature</div>\n              <div class=\"analyze-empty-text\">Run <code>/iikit-07-analyze</code> to generate a cross-artifact consistency analysis report.</div>\n            </div>`;\n          return;\n        }\n\n        let html = '<div class=\"analyze-view\">';\n\n        // Health Gauge Section\n        html += '<div class=\"analyze-section\">';\n        html += '<div class=\"analyze-section-header\">Health Score</div>';\n        html += renderHealthGauge(data.healthScore, data);\n        html += '</div>';\n\n        // Coverage Heatmap Section\n        html += '<div class=\"analyze-section\" style=\"animation-delay: 0.1s\">';\n        html += `<div class=\"cross-link-tip\">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>`;\n        html += '<div class=\"analyze-section-header\">Coverage Heatmap</div>';\n        html += renderHeatmap(data.heatmap);\n        html += '</div>';\n\n        // Severity Table Section\n        html += '<div class=\"analyze-section slide-in\" style=\"animation-delay: 0.2s\">';\n        html += '<div class=\"analyze-section-header\">Issues</div>';\n        html += renderSeverityTable(data.issues);\n        html += '</div>';\n\n        html += '</div>';\n        contentArea.innerHTML = html;\n\n        // Attach event listeners after rendering\n        attachHeatmapListeners();\n        attachSeverityListeners();\n\n        // Trigger gauge animation after a frame\n        requestAnimationFrame(() => {\n          const needle = contentArea.querySelector('.gauge-needle');\n          if (needle && data.healthScore) {\n            const angle = (data.healthScore.score / 100) * 180;\n            needle.style.transform = `rotate(${angle}deg)`;\n          }\n        });\n      }\n\n      function renderHealthGauge(healthScore, analyzeData) {\n        if (!healthScore) {\n          return '<div class=\"gauge-na\">N/A</div>';\n        }\n\n        const { score, zone, factors, trend } = healthScore;\n\n        // SVG semicircular gauge\n        // Arc center at (100, 90), radius 70\n        const cx = 100, cy = 90, r = 70;\n        const startAngle = Math.PI;\n        const endAngle = 0;\n\n        function arcPath(startPct, endPct) {\n          const a1 = Math.PI - (startPct / 100) * Math.PI;\n          const a2 = Math.PI - (endPct / 100) * Math.PI;\n          const x1 = cx + r * Math.cos(a1);\n          const y1 = cy - r * Math.sin(a1);\n          const x2 = cx + r * Math.cos(a2);\n          const y2 = cy - r * Math.sin(a2);\n          const large = (endPct - startPct) > 50 ? 1 : 0;\n          return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;\n        }\n\n        let trendHtml = '';\n        if (trend === 'improving') {\n          trendHtml = '<span class=\"gauge-trend gauge-trend-up\">&#9650; improving</span>';\n        } else if (trend === 'declining') {\n          trendHtml = '<span class=\"gauge-trend gauge-trend-down\">&#9660; declining</span>';\n        }\n\n        let gaugeHtml = '<div class=\"gauge-container\">';\n        gaugeHtml += `<svg class=\"gauge-svg\" viewBox=\"0 0 200 110\" role=\"meter\" aria-valuenow=\"${score}\" aria-valuemin=\"0\" aria-valuemax=\"100\" aria-label=\"Health score: ${score} out of 100\">`;\n\n        // Zone arcs\n        gaugeHtml += `<path class=\"gauge-arc gauge-zone-red\" d=\"${arcPath(0, 40)}\" />`;\n        gaugeHtml += `<path class=\"gauge-arc gauge-zone-yellow\" d=\"${arcPath(40, 70)}\" />`;\n        gaugeHtml += `<path class=\"gauge-arc gauge-zone-green\" d=\"${arcPath(70, 100)}\" />`;\n\n        // Needle (starts at 0, animated via CSS transform)\n        gaugeHtml += `<line class=\"gauge-needle\" x1=\"${cx}\" y1=\"${cy}\" x2=\"${cx - r + 10}\" y2=\"${cy}\" stroke-width=\"3\" stroke=\"var(--color-text)\" style=\"transform-origin: ${cx}px ${cy}px; transform: rotate(0deg);\" />`;\n\n        // Score text\n        gaugeHtml += `<text class=\"gauge-score\" x=\"${cx}\" y=\"${cy - 15}\">${score}</text>`;\n        gaugeHtml += `<text class=\"gauge-label\" x=\"${cx}\" y=\"${cy + 5}\">/ 100</text>`;\n\n        gaugeHtml += '</svg>';\n\n        // Breakdown with context\n        gaugeHtml += '<div class=\"gauge-breakdown\">';\n        if (factors) {\n          const metrics = analyzeData?.metrics;\n          const issues = analyzeData?.issues || [];\n          const alignment = analyzeData?.constitutionAlignment || [];\n\n          for (const [key, factor] of Object.entries(factors)) {\n            const isOk = factor.value === 100;\n            const colorClass = factor.value >= 71 ? 'gauge-factor-green' : factor.value >= 41 ? 'gauge-factor-yellow' : 'gauge-factor-red';\n            let context = '';\n\n            if (key === 'requirementsCoverage' && metrics) {\n              context = metrics.requirementCoverage || '';\n            } else if (key === 'constitutionCompliance') {\n              const violations = alignment.filter(a => a.status === 'VIOLATION');\n              context = violations.length ? violations.map(v => v.principle).join(', ') : 'All aligned';\n            } else if (key === 'phaseSeparation') {\n              const resolved = issues.filter(i => i.category && /phase/i.test(i.category) && i.resolved);\n              const unresolved = issues.filter(i => i.category && /phase/i.test(i.category) && !i.resolved);\n              if (unresolved.length) context = `${unresolved.length} violation${unresolved.length > 1 ? 's' : ''} open`;\n              else if (resolved.length) context = `${resolved.length} resolved`;\n              else if (factor.value < 100) context = 'See issues below';\n              else context = 'No violations';\n            } else if (key === 'testCoverage' && metrics) {\n              context = metrics.totalTestSpecs ? `${metrics.totalTestSpecs} test specs` : '';\n            }\n\n            gaugeHtml += `<div class=\"gauge-factor ${colorClass}\">`;\n            gaugeHtml += `<span class=\"gauge-factor-label\">${escapeHtml(factor.label)}`;\n            if (context) gaugeHtml += `<span class=\"gauge-factor-context\">${escapeHtml(context)}</span>`;\n            gaugeHtml += `</span>`;\n            gaugeHtml += `<span class=\"gauge-factor-value\">${factor.value}%</span>`;\n            gaugeHtml += `</div>`;\n          }\n        }\n        if (trendHtml) gaugeHtml += `<div style=\"text-align: center; margin-top: 4px;\">${trendHtml}</div>`;\n        gaugeHtml += '</div>';\n        gaugeHtml += '</div>';\n\n        return gaugeHtml;\n      }\n\n      function renderHeatmap(heatmap) {\n        if (!heatmap || !heatmap.rows || heatmap.rows.length === 0) {\n          return '<div class=\"heatmap-empty\">No coverage data</div>';\n        }\n\n        // Only look at columns with real data (skip 'plan' if ALL plan cells are N/A)\n        const activeCols = (heatmap.columns || []).filter(c => {\n          if (c !== 'plan') return true;\n          return heatmap.rows.some(r => r.cells.plan?.status !== 'na');\n        });\n        const total = heatmap.rows.length;\n\n        // Find rows with gaps (missing or partial in any active column)\n        const gaps = heatmap.rows.filter(row =>\n          activeCols.some(col => {\n            const cell = row.cells[col];\n            return cell && (cell.status === 'missing' || cell.status === 'partial');\n          })\n        );\n\n        // Coverage stats per column\n        const colStats = {};\n        for (const col of activeCols) {\n          const covered = heatmap.rows.filter(r => r.cells[col]?.status === 'covered').length;\n          colStats[col] = { covered, total, pct: total > 0 ? Math.round((covered / total) * 100) : 0 };\n        }\n\n        let html = '<div style=\"padding: 20px;\">';\n\n        // Summary bar\n        html += '<div class=\"coverage-summary\">';\n        for (const col of activeCols) {\n          const s = colStats[col];\n          const colorClass = s.pct === 100 ? 'cov-bar-green' : s.pct >= 70 ? 'cov-bar-yellow' : 'cov-bar-red';\n          html += `<div class=\"cov-stat\">`;\n          html += `<div class=\"cov-stat-header\"><span class=\"cov-stat-label\">${escapeHtml(col.charAt(0).toUpperCase() + col.slice(1))}</span><span class=\"cov-stat-value\">${s.covered}/${s.total}</span></div>`;\n          html += `<div class=\"cov-bar\"><div class=\"cov-bar-fill ${colorClass}\" style=\"width: ${s.pct}%\"></div></div>`;\n          html += `</div>`;\n        }\n        html += '</div>';\n\n        if (gaps.length === 0) {\n          // All covered — compact success\n          html += `<div class=\"coverage-success\">All ${total} requirements are covered by tasks and tests</div>`;\n        } else {\n          // Show only the gaps\n          html += `<div class=\"coverage-gaps-label\">${gaps.length} requirement${gaps.length > 1 ? 's' : ''} with gaps:</div>`;\n          html += '<table class=\"heatmap-table\" role=\"grid\">';\n          html += '<thead><tr><th scope=\"col\">Requirement</th><th scope=\"col\">Description</th>';\n          for (const col of activeCols) {\n            html += `<th scope=\"col\">${escapeHtml(col.charAt(0).toUpperCase() + col.slice(1))}</th>`;\n          }\n          html += '</tr></thead><tbody>';\n\n          gaps.forEach((row, i) => {\n            html += `<tr class=\"heatmap-row\" style=\"animation-delay: ${i * 0.04}s\">`;\n            html += `<th scope=\"row\"><code>${escapeHtml(row.id)}</code></th>`;\n            html += `<td class=\"heatmap-desc\">${escapeHtml(row.text)}</td>`;\n\n            for (const col of activeCols) {\n              const cell = row.cells[col] || { status: 'na', refs: [] };\n              const statusClass = 'coverage-' + cell.status;\n              const statusLabel = cell.status === 'covered' ? 'Covered' : cell.status === 'partial' ? 'Partial' : cell.status === 'missing' ? 'Missing' : 'N/A';\n              const refs = (cell.refs && cell.refs.length > 0) ? cell.refs.join(', ') : '';\n              html += `<td>`;\n              html += `<span class=\"heatmap-cell ${statusClass}\" tabindex=\"0\" role=\"button\" aria-label=\"${escapeHtml(row.id)} ${col}: ${statusLabel}\" data-req=\"${escapeHtml(row.id)}\" data-col=\"${escapeHtml(col)}\"></span>`;\n              if (refs) {\n                html += `<span class=\"heatmap-refs-inline\">${refs}</span>`;\n                html += `<div class=\"heatmap-refs\" data-req=\"${escapeHtml(row.id)}\" data-col=\"${escapeHtml(col)}\">${refs}</div>`;\n              }\n              html += `</td>`;\n            }\n            html += '</tr>';\n          });\n\n          html += '</tbody></table>';\n        }\n\n        html += '</div>';\n        return html;\n      }\n\n      function renderSeverityTable(issues) {\n        if (!issues || issues.length === 0) {\n          return '<div class=\"severity-empty\">No issues found &mdash; all artifacts are consistent</div>';\n        }\n\n        // Collect unique categories and severities\n        const categories = [...new Set(issues.map(i => i.category))].sort();\n        const severities = ['critical', 'high', 'medium', 'low'];\n\n        let html = '<div class=\"severity-wrapper\">';\n\n        // Filters\n        html += '<div class=\"severity-controls\">';\n        html += '<select class=\"severity-filter\" id=\"analyzeCategoryFilter\"><option value=\"\">All Categories</option>';\n        for (const cat of categories) {\n          html += `<option value=\"${escapeHtml(cat)}\">${escapeHtml(cat)}</option>`;\n        }\n        html += '</select>';\n        html += '<select class=\"severity-filter\" id=\"analyzeSeverityFilter\"><option value=\"\">All Severities</option>';\n        for (const sev of severities) {\n          html += `<option value=\"${sev}\">${sev.charAt(0).toUpperCase() + sev.slice(1)}</option>`;\n        }\n        html += '</select>';\n        html += '</div>';\n\n        // Table\n        html += '<div class=\"severity-table-scroll\"><table class=\"severity-table\">';\n        html += '<thead><tr>';\n        html += '<th tabindex=\"0\" data-sort=\"id\">ID</th>';\n        html += '<th tabindex=\"0\" data-sort=\"category\">Category</th>';\n        html += '<th tabindex=\"0\" data-sort=\"severity\">Severity</th>';\n        html += '<th tabindex=\"0\" data-sort=\"location\">Location</th>';\n        html += '<th tabindex=\"0\" data-sort=\"summary\">Summary</th>';\n        html += '</tr></thead><tbody>';\n\n        // Sort by severity by default\n        const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };\n        const sorted = [...issues].sort((a, b) => (severityOrder[a.severity] || 4) - (severityOrder[b.severity] || 4));\n\n        for (const issue of sorted) {\n          const resolvedClass = issue.resolved ? ' severity-resolved' : '';\n          html += `<tr data-severity=\"${escapeHtml(issue.severity)}\" data-category=\"${escapeHtml(issue.category)}\" tabindex=\"0\" class=\"${resolvedClass}\">`;\n          html += `<td>${escapeHtml(issue.id)}</td>`;\n          html += `<td>${escapeHtml(issue.category)}</td>`;\n          html += `<td><span class=\"severity-badge severity-${escapeHtml(issue.severity)}\">${escapeHtml(issue.severity)}</span></td>`;\n          html += `<td><code>${escapeHtml(issue.location)}</code></td>`;\n          html += `<td>${escapeHtml(issue.summary)}</td>`;\n          html += '</tr>';\n          html += `<tr class=\"severity-recommendation\" data-expand-for=\"${escapeHtml(issue.id)}\"><td colspan=\"5\">${escapeHtml(issue.recommendation)}</td></tr>`;\n        }\n\n        html += '</tbody></table></div></div>';\n        return html;\n      }\n\n      function attachHeatmapListeners() {\n        const cells = contentArea.querySelectorAll('.heatmap-cell');\n        cells.forEach(cell => {\n          const handler = () => {\n            const req = cell.dataset.req;\n            const col = cell.dataset.col;\n            const ref = contentArea.querySelector(`.heatmap-refs[data-req=\"${req}\"][data-col=\"${col}\"]`);\n            if (ref) ref.classList.toggle('expanded');\n          };\n          cell.addEventListener('click', handler);\n          cell.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }\n          });\n        });\n      }\n\n      function attachSeverityListeners() {\n        // Row expansion\n        const rows = contentArea.querySelectorAll('.severity-table tr[data-severity]');\n        rows.forEach(row => {\n          const handler = () => {\n            const nextRow = row.nextElementSibling;\n            if (nextRow && nextRow.classList.contains('severity-recommendation')) {\n              nextRow.classList.toggle('expanded');\n            }\n          };\n          row.addEventListener('click', handler);\n          row.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }\n          });\n        });\n\n        // Column sorting\n        const headers = contentArea.querySelectorAll('.severity-table th[data-sort]');\n        headers.forEach(th => {\n          const handler = () => {\n            const sortKey = th.dataset.sort;\n            const tbody = contentArea.querySelector('.severity-table tbody');\n            if (!tbody) return;\n\n            const dataRows = Array.from(tbody.querySelectorAll('tr[data-severity]'));\n            const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };\n\n            dataRows.sort((a, b) => {\n              let aVal, bVal;\n              if (sortKey === 'severity') {\n                aVal = severityOrder[a.dataset.severity] || 4;\n                bVal = severityOrder[b.dataset.severity] || 4;\n                return aVal - bVal;\n              }\n              const aIdx = Array.from(th.parentElement.children).indexOf(th);\n              aVal = a.children[aIdx]?.textContent || '';\n              bVal = b.children[aIdx]?.textContent || '';\n              return aVal.localeCompare(bVal);\n            });\n\n            // Rebuild tbody preserving recommendation rows\n            const fragment = document.createDocumentFragment();\n            for (const row of dataRows) {\n              fragment.appendChild(row);\n              const expandId = row.querySelector('td')?.textContent;\n              const recRow = tbody.querySelector(`tr[data-expand-for=\"${expandId}\"]`);\n              if (recRow) fragment.appendChild(recRow);\n            }\n            tbody.innerHTML = '';\n            tbody.appendChild(fragment);\n          };\n          th.addEventListener('click', handler);\n          th.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }\n          });\n        });\n\n        // Filters\n        const catFilter = document.getElementById('analyzeCategoryFilter');\n        const sevFilter = document.getElementById('analyzeSeverityFilter');\n\n        function applyFilters() {\n          const catVal = catFilter?.value || '';\n          const sevVal = sevFilter?.value || '';\n          const allRows = contentArea.querySelectorAll('.severity-table tbody tr');\n\n          allRows.forEach(row => {\n            if (row.classList.contains('severity-recommendation')) {\n              // Hide recommendations when parent is hidden\n              const expandFor = row.dataset.expandFor;\n              const parent = contentArea.querySelector(`.severity-table tr[data-severity] td:first-child`);\n              row.style.display = row.classList.contains('expanded') ? '' : 'none';\n              return;\n            }\n            if (!row.dataset.severity) return;\n\n            const matchCat = !catVal || row.dataset.category === catVal;\n            const matchSev = !sevVal || row.dataset.severity === sevVal;\n            row.style.display = (matchCat && matchSev) ? '' : 'none';\n\n            // Also hide its recommendation row\n            const nextRow = row.nextElementSibling;\n            if (nextRow && nextRow.classList.contains('severity-recommendation')) {\n              nextRow.style.display = (matchCat && matchSev) ? (nextRow.classList.contains('expanded') ? '' : 'none') : 'none';\n            }\n          });\n        }\n\n        if (catFilter) catFilter.addEventListener('change', applyFilters);\n        if (sevFilter) sevFilter.addEventListener('change', applyFilters);\n      }\n\n      function selectDefaultTab(pipeline) {\n        if (!pipeline || !pipeline.phases) return 'implement';\n\n        const impl = pipeline.phases.find(p => p.id === 'implement');\n        if (impl && (impl.status === 'in_progress' || impl.status === 'complete')) return 'implement';\n\n        // Walk backward through all phases to find last completed\n        const allPhases = ['constitution', 'spec', 'clarify', 'plan', 'checklist', 'testify', 'tasks', 'analyze', 'implement'];\n        for (let i = allPhases.length - 1; i >= 0; i--) {\n          const phase = pipeline.phases.find(p => p.id === allPhases[i]);\n          if (phase && phase.status === 'complete') {\n            return allPhases[i];\n          }\n        }\n\n        return 'implement';\n      }\n\n      async function loadPipeline(featureId) {\n        try {\n          let pipeline;\n          if (staticMode && window.DASHBOARD_DATA.featureData[featureId]) {\n            pipeline = window.DASHBOARD_DATA.featureData[featureId].pipeline;\n          } else {\n            const res = await fetch(`/api/pipeline/${featureId}`);\n            if (!res.ok) return;\n            pipeline = await res.json();\n          }\n          currentPipeline = pipeline;\n\n          if (!activeTab) {\n            activeTab = selectDefaultTab(pipeline);\n          }\n\n          renderPipeline(pipeline);\n          switchTab(activeTab);\n        } catch (err) {\n          console.error('Failed to load pipeline:', err);\n        }\n      }\n\n      // ====== Feature Loading ======\n      async function loadFeatures() {\n        try {\n          let features;\n          if (staticMode) {\n            features = window.DASHBOARD_DATA.features;\n          } else {\n            const res = await fetch('/api/features');\n            features = await res.json();\n          }\n          updateFeatureSelector(features);\n\n          if (features.length > 0 && !currentFeature) {\n            currentFeature = features[0].id;\n            featureSelect.value = currentFeature;\n            loadPipeline(currentFeature);\n            loadBoard(currentFeature);\n            loadBugs(currentFeature);\n          } else if (features.length === 0) {\n            showEmptyState();\n          }\n        } catch (err) {\n          console.error('Failed to load features:', err);\n        }\n      }\n\n      function updateFeatureSelector(features) {\n        featureSelect.innerHTML = '';\n        if (features.length === 0) {\n          featureSelect.innerHTML = '<option value=\"\">No features found</option>';\n          return;\n        }\n        for (const f of features) {\n          const opt = document.createElement('option');\n          opt.value = f.id;\n          opt.textContent = `${f.id} — ${f.name} (${f.progress})`;\n          featureSelect.appendChild(opt);\n        }\n      }\n\n      featureSelect.addEventListener('change', () => {\n        const val = featureSelect.value;\n        if (val && val !== currentFeature) {\n          currentFeature = val;\n          previousCardColumns = {};\n          activeTab = null; // Reset tab selection for new feature\n          currentStoryMap = null; // Reset story map cache\n          currentPlanView = null; // Reset plan view cache\n          currentTestify = null; // Reset testify cache\n          currentAnalyze = null; // Reset analyze cache\n          currentBugs = null; // Reset bugs cache\n          loadPipeline(val);\n          loadBoard(val);\n          loadBugs(val);\n        }\n      });\n\n      featureSelect.addEventListener('keydown', (e) => {\n        if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {\n          // Default browser behavior handles this\n          return;\n        }\n      });\n\n      // ====== Board Loading ======\n      async function loadBoard(featureId) {\n        try {\n          showLoading();\n          let board;\n          if (staticMode && window.DASHBOARD_DATA.featureData[featureId]) {\n            board = window.DASHBOARD_DATA.featureData[featureId].board;\n          } else {\n            const res = await fetch(`/api/board/${featureId}`);\n            if (!res.ok) throw new Error(`HTTP ${res.status}`);\n            board = await res.json();\n          }\n          currentBoard = board;\n          renderBoard(board);\n          updateIntegrity(board.integrity);\n        } catch (err) {\n          console.error('Failed to load board:', err);\n          showEmptyState('Failed to load board data');\n        }\n      }\n\n      async function loadBugs(featureId) {\n        try {\n          let bugs;\n          if (staticMode && window.DASHBOARD_DATA.featureData[featureId]) {\n            bugs = window.DASHBOARD_DATA.featureData[featureId].bugs;\n          } else {\n            const res = await fetch(`/api/bugs/${featureId}`);\n            if (!res.ok) return;\n            bugs = await res.json();\n          }\n          currentBugs = bugs;\n          if (currentPipeline) renderPipeline(currentPipeline);\n          if (activeTab === 'bugs') renderBugsContent(currentBugs);\n        } catch {\n          // Silently fail — bugs are optional\n        }\n      }\n\n      function showLoading() {\n        boardEl.innerHTML = '<div class=\"loading\"><div class=\"loading-spinner\"></div></div>';\n      }\n\n      function showEmptyState(msg) {\n        boardEl.innerHTML = `\n          <div class=\"empty-state\">\n            <div class=\"empty-state-icon\">&#9776;</div>\n            <div class=\"empty-state-title\">${msg || 'No features found'}</div>\n            <div class=\"empty-state-text\">Create a feature with spec.md and tasks.md in your specs/ directory to get started.</div>\n          </div>`;\n      }\n\n      // ====== Board Rendering ======\n      function renderBoardInto(targetEl, board) {\n        if (!board || !targetEl) return;\n\n        const columns = [\n          { key: 'todo', label: 'Todo', cards: board.todo || [] },\n          { key: 'in_progress', label: 'In Progress', cards: board.in_progress || [] },\n          { key: 'done', label: 'Done', cards: board.done || [] }\n        ];\n\n        // Build new card positions\n        const newCardColumns = {};\n        for (const col of columns) {\n          for (const card of col.cards) {\n            newCardColumns[card.id] = col.key;\n          }\n        }\n\n        // Detect moved cards\n        const movedCards = {};\n        for (const [cardId, newCol] of Object.entries(newCardColumns)) {\n          const oldCol = previousCardColumns[cardId];\n          if (oldCol && oldCol !== newCol) {\n            movedCards[cardId] = { from: oldCol, to: newCol };\n          }\n        }\n\n        targetEl.innerHTML = '';\n\n        for (const col of columns) {\n          const colEl = document.createElement('div');\n          colEl.className = `column ${col.key === 'in_progress' ? 'in-progress' : col.key}`;\n          colEl.setAttribute('role', 'region');\n          colEl.setAttribute('aria-label', `${col.label} column with ${col.cards.length} stories`);\n\n          colEl.innerHTML = `\n            <div class=\"column-header\">\n              <div class=\"column-title\">\n                <span class=\"column-dot\" aria-hidden=\"true\"></span>\n                ${col.label}\n              </div>\n              <span class=\"column-count\">${col.cards.length}</span>\n            </div>\n            <div class=\"column-body\" id=\"col-${col.key}\"></div>`;\n\n          targetEl.appendChild(colEl);\n\n          const bodyEl = colEl.querySelector('.column-body');\n\n          if (col.cards.length === 0) {\n            bodyEl.innerHTML = '<div class=\"column-empty\">No stories</div>';\n          } else {\n            for (const card of col.cards) {\n              const cardEl = createCardElement(card, col.key);\n\n              // Add animation class if card just moved here\n              if (movedCards[card.id]) {\n                cardEl.classList.add('entering');\n                if (movedCards[card.id].to === 'done') {\n                  cardEl.classList.add('just-completed');\n                }\n                // Remove animation class after it completes\n                cardEl.addEventListener('animationend', () => {\n                  cardEl.classList.remove('entering', 'just-completed');\n                }, { once: true });\n              }\n\n              bodyEl.appendChild(cardEl);\n            }\n          }\n        }\n\n        previousCardColumns = newCardColumns;\n      }\n\n      function renderBoard(board) {\n        const targetEl = document.getElementById('board');\n        if (targetEl) renderBoardInto(targetEl, board);\n      }\n\n      const BUG_SVG_ICON = '<svg class=\"bug-icon-inline\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M8 2l1.88 1.88M14.12 3.88L16 2M9 7.13v-1a3.003 3.003 0 116 0v1M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 014-4h4a4 4 0 014 4v3c0 3.3-2.7 6-6 6zM2 11h3M19 11h3M2 16h3M19 16h3\"/></svg>';\n\n      function createCardElement(card, columnKey) {\n        const el = document.createElement('div');\n        el.className = 'card' + (card.isBugCard ? ' bug-card' : '');\n        el.setAttribute('data-card-id', card.id);\n        el.setAttribute('role', 'article');\n        el.setAttribute('aria-label', `${card.title} - ${card.priority} - ${card.progress} tasks complete`);\n\n        const progressParts = card.progress.split('/');\n        const checked = parseInt(progressParts[0], 10);\n        const total = parseInt(progressParts[1], 10);\n        const pct = total > 0 ? Math.round((checked / total) * 100) : 0;\n\n        const priorityClass = card.priority ? card.priority.toLowerCase() : 'p3';\n\n        const cardIdHtml = card.isBugCard\n          ? `<div class=\"card-id cross-link\" data-cross-target=\"bugs\" data-cross-id=\"${card.id}\">${BUG_SVG_ICON}${card.id}</div>`\n          : `<div class=\"card-id cross-link\" data-cross-target=\"spec\" data-cross-id=\"${card.id}\">${card.id}</div>`;\n\n        el.innerHTML = `\n          ${cardIdHtml}\n          <div class=\"card-header\">\n            <div class=\"card-title\" title=\"${escapeHtml(card.title)}\">${card.isBugCard ? BUG_SVG_ICON : ''}${escapeHtml(card.title)}</div>\n            <span class=\"priority-badge ${priorityClass}\" aria-label=\"Priority ${card.priority}\">${card.priority}</span>\n          </div>\n          <div class=\"progress-container\">\n            <div class=\"progress-info\">\n              <span class=\"progress-label\">Progress</span>\n              <span class=\"progress-value\">${card.progress} (${pct}%)</span>\n            </div>\n            <div class=\"progress-bar\" role=\"progressbar\" aria-valuenow=\"${pct}\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n              <div class=\"progress-fill\" style=\"width: ${pct}%\"></div>\n            </div>\n          </div>\n          <button class=\"task-toggle\" onclick=\"toggleTasks(this)\" aria-expanded=\"false\" aria-controls=\"tasks-${card.id}\">\n            <span class=\"task-toggle-icon\" aria-hidden=\"true\">&#9654;</span>\n            ${(card.tasks || []).length} tasks\n          </button>\n          <ul class=\"task-list collapsed\" id=\"tasks-${card.id}\" aria-label=\"Tasks for ${card.id}\">\n            ${(card.tasks || []).map(t => {\n              const isBugTask = t.isBugFix || (t.id && t.id.startsWith('T-B'));\n              const taskIcon = isBugTask ? BUG_SVG_ICON : '';\n              const bugTagLink = t.bugTag\n                ? ` <span class=\"cross-link\" data-cross-target=\"bugs\" data-cross-id=\"${t.bugTag}\" style=\"cursor:pointer;color:var(--color-accent);font-size:11px;\">[${t.bugTag}]</span>`\n                : '';\n              return `\n              <li class=\"task-item ${t.checked ? 'checked' : ''}\" data-task-id=\"${t.id}\">\n                <span class=\"task-checkbox ${t.checked ? 'checked' : ''}\" aria-hidden=\"true\"></span>\n                ${taskIcon}<span class=\"task-id cross-link\" data-cross-target=\"testify\" data-cross-id=\"${t.id}\">${t.id}</span>${bugTagLink}\n                <span class=\"task-description\">${escapeHtml(t.description)}</span>\n              </li>`;\n            }).join('')}\n          </ul>`;\n\n        return el;\n      }\n\n      function escapeHtml(str) {\n        if (!str) return '';\n        return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;');\n      }\n\n      // ====== Toast ======\n      function showToast(message, duration = 2500) {\n        const el = document.getElementById('toast');\n        el.textContent = message;\n        el.classList.add('visible');\n        setTimeout(() => el.classList.remove('visible'), duration);\n      }\n\n      // ====== Integrity Badge ======\n      function updateIntegrity(integrity) {\n        if (!integrity) return;\n        const badge = integrityBadge;\n        const textEl = badge.querySelector('.integrity-text');\n\n        badge.className = `integrity-badge ${integrity.status}`;\n\n        switch (integrity.status) {\n          case 'valid':\n            textEl.textContent = 'Verified';\n            badge.setAttribute('aria-label', 'Test integrity: verified');\n            badge.title = 'Assertion hash matches stored hash';\n            break;\n          case 'tampered':\n            textEl.textContent = 'Tampered';\n            badge.setAttribute('aria-label', 'Test integrity: tampered - assertions may have been modified');\n            badge.title = 'Assertion hash does not match stored hash!';\n            break;\n          case 'missing':\n            textEl.textContent = 'Missing';\n            badge.setAttribute('aria-label', 'Test integrity: no hash data available');\n            badge.title = 'No test-specs.md or context.json found';\n            break;\n        }\n      }\n\n      // ====== Plan View ======\n      let currentPlanView = null;\n\n      async function renderPlanView() {\n        if (!currentFeature) return;\n        try {\n          if (!currentPlanView) {\n            if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {\n              currentPlanView = window.DASHBOARD_DATA.featureData[currentFeature].planView;\n            } else {\n              const res = await fetch(`/api/planview/${currentFeature}`);\n              currentPlanView = await res.json();\n            }\n          }\n          renderPlanViewContent(currentPlanView);\n        } catch (err) {\n          contentArea.innerHTML = `<div class=\"planview-empty\"><div class=\"planview-empty-title\">Error loading plan</div><div class=\"planview-empty-text\">${escapeHtml(err.message)}</div></div>`;\n        }\n      }\n\n      function renderPlanViewContent(data) {\n        if (!data || !data.exists) {\n          contentArea.innerHTML = `<div class=\"planview-empty\"><div class=\"planview-empty-title\">No plan created yet</div><div class=\"planview-empty-text\">Run /iikit-03-plan to create a technical implementation plan for this feature.</div></div>`;\n          return;\n        }\n\n        let html = '<div class=\"planview-view\">';\n\n        // Badge Wall\n        html += '<div class=\"planview-section\">';\n        html += '<div class=\"planview-section-title\">Tech Stack</div>';\n        if (data.techContext && data.techContext.length > 0) {\n          html += '<div class=\"badge-wall\">';\n          for (const entry of data.techContext) {\n            const tooltip = findResearchTooltip(entry.value, data.researchDecisions);\n            html += `<div class=\"tech-badge\" role=\"listitem\">`;\n            html += `<span class=\"tech-badge-label\">${escapeHtml(entry.label)}</span>`;\n            html += `<span class=\"tech-badge-value\">${escapeHtml(entry.value)}</span>`;\n            if (tooltip) {\n              html += `<div class=\"tech-badge-tooltip\" role=\"tooltip\">${escapeHtml(tooltip)}</div>`;\n            }\n            html += `</div>`;\n          }\n          html += '</div>';\n        } else {\n          html += '<div class=\"planview-empty\"><div class=\"planview-empty-text\">No tech stack defined in plan</div></div>';\n        }\n        html += '</div>';\n\n        // Tessl Tiles Panel\n        if (data.tesslTiles && data.tesslTiles.length > 0) {\n          html += '<div class=\"planview-section\">';\n          html += '<div class=\"planview-section-title\">Tessl Tiles</div>';\n          html += '<div class=\"tessl-tiles\">';\n          for (const tile of data.tesslTiles) {\n            html += `<div class=\"tessl-tile-card\">`;\n            html += `<span class=\"tessl-tile-name\">${escapeHtml(tile.name)}</span>`;\n            html += `<span class=\"tessl-tile-version\">v${escapeHtml(tile.version)}</span>`;\n            if (tile.eval) {\n              const chartTotal = tile.eval.chartData.pass + tile.eval.chartData.fail;\n              const barPct = chartTotal > 0 ? Math.round(tile.eval.chartData.pass / chartTotal * 100) : tile.eval.score;\n              html += `<div class=\"tessl-tile-eval\">`;\n              html += `<span class=\"tessl-eval-score\">${tile.eval.score}%</span>`;\n              html += `<div class=\"tessl-eval-bar\" title=\"${tile.eval.chartData.pass} pass, ${tile.eval.chartData.fail} fail\"><div class=\"tessl-eval-bar-fill\" style=\"width:${barPct}%\"></div></div>`;\n              if (tile.eval.multiplier) {\n                html += `<span class=\"tessl-eval-multiplier\">\\u2191 ${tile.eval.multiplier}x</span>`;\n              }\n              html += `</div>`;\n            }\n            html += `</div>`;\n          }\n          html += '</div></div>';\n        }\n\n        // File Structure Tree\n        if (data.fileStructure && data.fileStructure.entries && data.fileStructure.entries.length > 0) {\n          html += '<div class=\"planview-section\">';\n          html += '<div class=\"planview-section-title\">Project Structure</div>';\n          html += '<div class=\"file-tree\" role=\"tree\" aria-label=\"Project file structure\">';\n          html += renderFileTree(data.fileStructure.entries, 0);\n          html += '</div></div>';\n        }\n\n        // Architecture Diagram\n        if (data.diagram && data.diagram.nodes && data.diagram.nodes.length > 0) {\n          html += '<div class=\"planview-section\">';\n          html += '<div class=\"planview-section-title\">Architecture</div>';\n          html += '<div class=\"diagram-container\">';\n          html += renderDiagramSVG(data.diagram);\n          html += '</div>';\n          html += renderDiagramLegend(data.diagram);\n          html += '<div id=\"diagram-detail\" class=\"detail-panel-slot\"></div>';\n          html += '</div>';\n        } else if (data.diagram && data.diagram.raw) {\n          // Fallback: raw ASCII\n          html += '<div class=\"planview-section\">';\n          html += '<div class=\"planview-section-title\">Architecture</div>';\n          html += `<pre class=\"diagram-raw\">${escapeHtml(data.diagram.raw)}</pre>`;\n          html += '</div>';\n        }\n\n        html += '</div>';\n        contentArea.innerHTML = html;\n\n        // Attach event handlers\n        attachTreeHandlers();\n        attachDiagramHandlers(data.diagram);\n      }\n\n      function findResearchTooltip(badgeValue, decisions) {\n        if (!decisions || decisions.length === 0) return null;\n        const valueLower = badgeValue.toLowerCase();\n        for (const d of decisions) {\n          if (d.title && valueLower.includes(d.title.toLowerCase().split(' ')[0])) {\n            return d.rationale || d.decision;\n          }\n          // Also check if decision title words appear in badge value\n          if (d.title) {\n            const words = d.title.toLowerCase().split(/\\s+/);\n            for (const word of words) {\n              if (word.length > 3 && valueLower.includes(word)) {\n                return d.rationale || d.decision;\n              }\n            }\n          }\n        }\n        return null;\n      }\n\n      function getFileIconInfo(name, isDir) {\n        if (isDir) return { cls: 'dir', ch: '\\uD83D\\uDCC2' };\n        const ext = name.split('.').pop();\n        if (['js', 'mjs'].includes(ext)) return { cls: 'file-js', ch: 'JS' };\n        if (ext === 'json') return { cls: 'file-json', ch: '{}' };\n        if (ext === 'md') return { cls: 'file-md', ch: 'M\\u2193' };\n        if (ext === 'html') return { cls: 'file-html', ch: '</>' };\n        return { cls: 'file', ch: '\\u25CB' };\n      }\n\n      function renderFileTree(entries) {\n        let html = '';\n        let i = 0;\n        while (i < entries.length) {\n          const entry = entries[i];\n          const isDir = entry.type === 'directory';\n          const expanded = entry.depth < 2;\n\n          // Indent guides\n          let indent = '';\n          for (let d = 0; d < entry.depth; d++) {\n            indent += '<span class=\"file-tree-guide\"></span>';\n          }\n\n          const { cls, ch } = getFileIconInfo(entry.name, isDir);\n\n          if (isDir) {\n            const children = [];\n            let j = i + 1;\n            while (j < entries.length && entries[j].depth > entry.depth) {\n              children.push(entries[j]);\n              j++;\n            }\n            const childId = `tree-${entry.depth}-${entry.name}`.replace(/[^a-zA-Z0-9-]/g, '_');\n\n            html += `<div class=\"file-tree-entry\">`;\n            html += `<span class=\"file-tree-indent\">${indent}</span>`;\n            html += `<span class=\"file-tree-chevron\" data-target=\"${childId}\">${expanded ? '\\u25BE' : '\\u25B8'}</span>`;\n            html += `<span class=\"file-tree-file-icon ${cls}\">${ch}</span>`;\n            html += `<span class=\"file-tree-label\"><span class=\"file-tree-name\">${escapeHtml(entry.name)}</span></span>`;\n            if (entry.comment) html += `<span class=\"file-tree-comment\" data-full=\"${escapeHtml(entry.comment)}\">${escapeHtml(entry.comment)}</span>`;\n            html += `</div>`;\n            html += `<div id=\"${childId}\" class=\"file-tree-children${expanded ? '' : ' collapsed'}\">`;\n            html += renderFileTree(children);\n            html += `</div>`;\n            i = j;\n          } else {\n            const isPlanned = entry.exists === false;\n            const nameClass = isPlanned ? ' planned' : '';\n\n            html += `<div class=\"file-tree-entry\">`;\n            html += `<span class=\"file-tree-indent\">${indent}</span>`;\n            html += `<span class=\"file-tree-chevron-spacer\"></span>`;\n            html += `<span class=\"file-tree-file-icon ${cls}\">${ch}</span>`;\n            html += `<span class=\"file-tree-label\">`;\n            html += `<span class=\"file-tree-name${nameClass}\">${escapeHtml(entry.name)}</span>`;\n            if (isPlanned) html += `<span class=\"file-tree-status planned-tag\">planned</span>`;\n            html += `</span>`;\n            if (entry.comment) html += `<span class=\"file-tree-comment\" data-full=\"${escapeHtml(entry.comment)}\">${escapeHtml(entry.comment)}</span>`;\n            html += `</div>`;\n            i++;\n          }\n        }\n        return html;\n      }\n\n      function renderDiagramLegend(diagram) {\n        if (!diagram || !diagram.nodes) return '';\n        const types = new Set(diagram.nodes.map(n => n.type).filter(t => t !== 'default'));\n        if (types.size === 0) return '';\n        const colors = { client: 'var(--color-accent)', server: 'var(--color-p2)', storage: 'var(--color-done)', external: 'var(--color-p1)' };\n        let html = '<div class=\"diagram-legend\">';\n        for (const type of types) {\n          html += `<div class=\"diagram-legend-item\"><span class=\"diagram-legend-dot\" style=\"background:${colors[type] || 'var(--color-text-muted)'}\"></span>${type}</div>`;\n        }\n        html += '</div>';\n        return html;\n      }\n\n      function attachTreeHandlers() {\n        document.querySelectorAll('.file-tree-chevron').forEach(chevron => {\n          chevron.addEventListener('click', () => {\n            const targetId = chevron.dataset.target;\n            const children = document.getElementById(targetId);\n            if (!children) return;\n            const isCollapsed = children.classList.contains('collapsed');\n            children.classList.toggle('collapsed', !isCollapsed);\n            chevron.textContent = isCollapsed ? '\\u25BE' : '\\u25B8';\n          });\n        });\n        // Add title tooltip only on comments that are actually truncated\n        document.querySelectorAll('.file-tree-comment').forEach(comment => {\n          if (comment.scrollWidth > comment.clientWidth) {\n            comment.classList.add('truncated');\n            comment.title = comment.dataset.full || comment.textContent;\n          }\n        });\n      }\n\n      function renderDiagramSVG(diagram) {\n        if (!diagram || !diagram.nodes || diagram.nodes.length === 0) return '';\n\n        // Calculate SVG dimensions from node positions\n        let maxX = 0, maxY = 0;\n        for (const n of diagram.nodes) {\n          const right = n.x + n.width;\n          const bottom = n.y + n.height;\n          if (right > maxX) maxX = right;\n          if (bottom > maxY) maxY = bottom;\n        }\n\n        // Scale to SVG viewport\n        const padding = 40;\n        const svgWidth = 800;\n        const scaleX = (svgWidth - padding * 2) / (maxX || 1);\n        const scaleY = scaleX; // maintain aspect ratio\n        const svgHeight = maxY * scaleY + padding * 2;\n\n        const nodeTypeColors = {\n          client: 'var(--color-accent)',\n          server: 'var(--color-p2)',\n          storage: 'var(--color-done)',\n          external: 'var(--color-p1)',\n          default: 'var(--color-text-muted)'\n        };\n\n        let svg = `<svg class=\"diagram-svg\" viewBox=\"0 0 ${svgWidth} ${svgHeight}\" role=\"figure\" aria-label=\"Architecture diagram\">`;\n\n        // Draw edges first (behind nodes)\n        for (const edge of diagram.edges) {\n          const fromNode = diagram.nodes.find(n => n.id === edge.from);\n          const toNode = diagram.nodes.find(n => n.id === edge.to);\n          if (!fromNode || !toNode) continue;\n\n          const x1 = (fromNode.x + fromNode.width / 2) * scaleX + padding;\n          const y1 = (fromNode.y + fromNode.height) * scaleY + padding;\n          const x2 = (toNode.x + toNode.width / 2) * scaleX + padding;\n          const y2 = toNode.y * scaleY + padding;\n\n          svg += `<line class=\"diagram-edge-line\" x1=\"${x1}\" y1=\"${y1}\" x2=\"${x2}\" y2=\"${y2}\" aria-hidden=\"true\"/>`;\n          if (edge.label) {\n            const midX = (x1 + x2) / 2 + 8;\n            const midY = (y1 + y2) / 2;\n            svg += `<text class=\"diagram-edge-label\" x=\"${midX}\" y=\"${midY}\">${escapeHtml(edge.label)}</text>`;\n          }\n        }\n\n        // Draw nodes\n        for (const node of diagram.nodes) {\n          const x = node.x * scaleX + padding;\n          const y = node.y * scaleY + padding;\n          const w = node.width * scaleX;\n          const h = node.height * scaleY;\n          const color = nodeTypeColors[node.type] || nodeTypeColors.default;\n\n          svg += `<g class=\"diagram-node\" data-node-id=\"${node.id}\" tabindex=\"0\" role=\"img\" aria-label=\"${escapeHtml(node.label)} (${node.type})\">`;\n          svg += `<rect class=\"diagram-node-rect\" x=\"${x}\" y=\"${y}\" width=\"${w}\" height=\"${h}\" fill=\"var(--color-surface)\" stroke=\"${color}\"/>`;\n          svg += `<text class=\"diagram-node-label\" x=\"${x + w/2}\" y=\"${y + h/2 + 5}\" text-anchor=\"middle\">${escapeHtml(node.label)}</text>`;\n          svg += `</g>`;\n        }\n\n        svg += '</svg>';\n        return svg;\n      }\n\n      function attachDiagramHandlers(diagram) {\n        if (!diagram) return;\n        document.querySelectorAll('.diagram-node').forEach(nodeEl => {\n          const handler = () => {\n            const nodeId = nodeEl.dataset.nodeId;\n            const node = diagram.nodes.find(n => n.id === nodeId);\n            if (!node) return;\n            showPlanDetailPanel(node);\n          };\n          nodeEl.addEventListener('click', handler);\n          nodeEl.addEventListener('keydown', (e) => {\n            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }\n          });\n        });\n      }\n\n      function showPlanDetailPanel(node) {\n        const slot = document.getElementById('diagram-detail');\n        if (!slot) return;\n        const typeLabel = node.type !== 'default' ? ` (${node.type})` : '';\n        slot.innerHTML = `\n          <div class=\"detail-panel\">\n            <div class=\"detail-panel-header\">\n              <span class=\"detail-panel-id\">${escapeHtml(node.label)}${typeLabel}</span>\n              <button class=\"detail-panel-close\" aria-label=\"Close detail panel\">\\u00d7</button>\n            </div>\n            <div class=\"detail-panel-body\"><pre style=\"white-space:pre-wrap;font-family:var(--font-mono);font-size:13px;color:var(--color-text-secondary)\">${escapeHtml(node.content)}</pre></div>\n          </div>`;\n        slot.querySelector('.detail-panel-close').addEventListener('click', () => { slot.innerHTML = ''; });\n      }\n\n      // ====== WebSocket (removed — replaced by meta-refresh in static mode) ======\n\n      // ====== Theme Toggle ======\n      const themeToggle = document.getElementById('themeToggle');\n      const themeIcon = document.getElementById('themeIcon');\n      const html = document.documentElement;\n\n      // Three-state cycle: system -> light -> dark -> system\n      let themeMode = localStorage.getItem('iikit-theme') || 'system';\n\n      function getSystemTheme() {\n        return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';\n      }\n\n      function applyTheme(mode) {\n        themeMode = mode;\n        if (mode === 'system') {\n          localStorage.removeItem('iikit-theme');\n          const resolved = getSystemTheme();\n          if (resolved === 'light') {\n            html.setAttribute('data-theme', 'light');\n          } else {\n            html.removeAttribute('data-theme');\n          }\n          themeIcon.textContent = '\\uD83D\\uDDA5'; // monitor\n          themeToggle.setAttribute('aria-label', 'Theme: System (click for Light)');\n          themeToggle.title = 'Theme: System';\n        } else if (mode === 'light') {\n          localStorage.setItem('iikit-theme', 'light');\n          html.setAttribute('data-theme', 'light');\n          themeIcon.textContent = '\\u2600'; // sun\n          themeToggle.setAttribute('aria-label', 'Theme: Light (click for Dark)');\n          themeToggle.title = 'Theme: Light';\n        } else {\n          localStorage.setItem('iikit-theme', 'dark');\n          html.removeAttribute('data-theme');\n          themeIcon.textContent = '\\u263E'; // moon\n          themeToggle.setAttribute('aria-label', 'Theme: Dark (click for System)');\n          themeToggle.title = 'Theme: Dark';\n        }\n      }\n\n      themeToggle.addEventListener('click', () => {\n        const next = themeMode === 'system' ? 'light' : themeMode === 'light' ? 'dark' : 'system';\n        applyTheme(next);\n      });\n\n      // Listen for OS theme changes — only react in system mode\n      window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {\n        if (themeMode === 'system') applyTheme('system');\n      });\n\n      // Apply on load\n      applyTheme(themeMode);\n\n      // ====== Init ======\n      if (staticMode) {\n        // Bootstrap from inlined DASHBOARD_DATA (generated HTML)\n        const D = window.DASHBOARD_DATA;\n        const label = document.getElementById('projectLabel');\n        const dirName = D.meta.projectPath.split('/').pop();\n        label.textContent = dirName;\n        label.title = D.meta.projectPath;\n        currentConstitution = D.constitution;\n        currentPremise = D.premise;\n        updateFeatureSelector(D.features);\n        if (D.features.length > 0) {\n          currentFeature = D.features[0].id;\n          featureSelect.value = currentFeature;\n          const fd = D.featureData[currentFeature];\n          if (fd) {\n            currentPipeline = fd.pipeline;\n            currentBoard = fd.board;\n            currentBugs = fd.bugs;\n            if (!activeTab) activeTab = selectDefaultTab(fd.pipeline);\n            renderPipeline(fd.pipeline);\n            renderBoard(fd.board);\n            updateIntegrity(fd.board.integrity);\n            if (fd.bugs && currentPipeline) renderPipeline(currentPipeline);\n            switchTab(activeTab);\n          }\n        } else {\n          // No features — show constitution directly (it's project-level, not per-feature)\n          renderConstitutionView();\n        }\n      } else {\n        // Fetch mode (server-based)\n        fetch('/api/meta').then(r => r.json()).then(meta => {\n          const label = document.getElementById('projectLabel');\n          const dirName = meta.projectPath.split('/').pop();\n          label.textContent = dirName;\n          label.title = meta.projectPath;\n        }).catch(() => {});\n        loadFeatures();\n      }\n    })();\n\n    // ====== Task List Toggle (global, called from onclick) ======\n    function toggleTasks(btn) {\n      const list = btn.nextElementSibling;\n      const icon = btn.querySelector('.task-toggle-icon');\n      const isCollapsed = list.classList.contains('collapsed');\n\n      if (isCollapsed) {\n        list.classList.remove('collapsed');\n        list.classList.add('expanded');\n        icon.classList.add('expanded');\n        btn.setAttribute('aria-expanded', 'true');\n      } else {\n        list.classList.remove('expanded');\n        list.classList.add('collapsed');\n        icon.classList.remove('expanded');\n        btn.setAttribute('aria-expanded', 'false');\n      }\n    }\n  </script>\n</body>\n</html>\n";

Install with Tessl CLI

npx tessl i tessl-labs/intent-integrity-kit@2.3.5

skills

README.md

tile.json