CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl-labs/intent-integrity-kit

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

93

1.84x
Quality

93%

Does it follow best practices?

Impact

94%

1.84x

Average score across 14 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

index.htmlskills/iikit-05-tasks/scripts/dashboard/src/public/

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>IIKit Dashboard</title>
  <style>
    /* ====== CSS Custom Properties ====== */
    :root {
      --color-bg: #0f1117;
      --color-surface: #1a1d27;
      --color-surface-elevated: #222536;
      --color-surface-hover: #2a2d40;
      --color-border: #2e3148;
      --color-border-subtle: #252839;
      --color-text: #e8eaed;
      --color-text-secondary: #9aa0b4;
      --color-text-muted: #6b7189;
      --color-accent: #3B82F6;
      --color-accent-hover: #60A5FA;
      --color-todo: #4a90d9;
      --color-inprogress: #f5a623;
      --color-done: #27c93f;
      --color-p1: #ff4757;
      --color-p2: #ffa502;
      --color-p3: #3498db;
      --color-verified: #27c93f;
      --color-tampered: #ff4757;
      --color-missing: #6b7189;
      --radius-sm: 6px;
      --radius-md: 10px;
      --radius-lg: 14px;
      --shadow-card: 0 2px 8px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2);
      --shadow-card-hover: 0 8px 24px rgba(0,0,0,0.4), 0 2px 8px rgba(0,0,0,0.3);
      --shadow-column: 0 1px 4px rgba(0,0,0,0.2);
      --transition-fast: 0.15s ease;
      --transition-normal: 0.25s ease;
      --transition-slow: 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
      --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, Oxygen, sans-serif;
      --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
    }

    /* ====== Light Theme ====== */
    [data-theme="light"] {
      --color-bg: #f5f6f8;
      --color-surface: #ffffff;
      --color-surface-elevated: #f0f1f4;
      --color-surface-hover: #e8e9ee;
      --color-border: #d8dae0;
      --color-border-subtle: #e4e6eb;
      --color-text: #1a1d27;
      --color-text-secondary: #5a5f72;
      --color-text-muted: #8b90a0;
      --color-accent: #2563EB;
      --color-accent-hover: #3B82F6;
      --shadow-card: 0 1px 4px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
      --shadow-card-hover: 0 4px 12px rgba(0,0,0,0.12), 0 2px 4px rgba(0,0,0,0.06);
      --shadow-column: 0 1px 3px rgba(0,0,0,0.06);
    }

    /* ====== Reset & Base ====== */
    *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

    body {
      font-family: var(--font-sans);
      background: var(--color-bg);
      color: var(--color-text);
      min-height: 100vh;
      -webkit-font-smoothing: antialiased;
      -moz-osx-font-smoothing: grayscale;
    }

    /* ====== Header ====== */
    .header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 16px 28px;
      background: var(--color-surface);
      border-bottom: 1px solid var(--color-border);
      position: sticky;
      top: 0;
      z-index: 100;
      backdrop-filter: blur(12px);
      gap: 16px;
    }

    .header-left {
      display: flex;
      align-items: center;
      gap: 16px;
      min-width: 0;
      flex: 1 1 auto;
    }

    .project-label {
      font-size: 12px;
      color: var(--color-text-muted);
      max-width: 160px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      border-left: 1px solid var(--color-border);
      padding-left: 12px;
    }

    .feature-selector {
      position: relative;
      min-width: 100px;
      flex: 0 1 300px;
    }

    .logo {
      display: flex;
      align-items: center;
      gap: 10px;
      font-weight: 700;
      font-size: 16px;
      letter-spacing: -0.3px;
      color: var(--color-text);
    }

    .logo-icon {
      width: 28px;
      height: 28px;
      background: linear-gradient(135deg, var(--color-accent), #1D4ED8);
      border-radius: var(--radius-sm);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 14px;
      color: white;
      font-weight: 800;
    }

    .header-right {
      display: flex;
      align-items: center;
      gap: 14px;
      flex-shrink: 0;
    }

    /* ====== Feature Selector ====== */

    .feature-selector select {
      appearance: none;
      background: var(--color-surface-elevated);
      color: var(--color-text);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-sm);
      padding: 8px 36px 8px 12px;
      font-size: 13px;
      font-family: var(--font-sans);
      font-weight: 500;
      cursor: pointer;
      transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
      width: 100%;
    }

    .feature-selector select:hover {
      border-color: var(--color-accent);
    }

    .feature-selector select:focus {
      outline: none;
      border-color: var(--color-accent);
      box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
    }

    .feature-selector::after {
      content: '';
      position: absolute;
      right: 12px;
      top: 50%;
      transform: translateY(-50%);
      width: 0;
      height: 0;
      border-left: 4px solid transparent;
      border-right: 4px solid transparent;
      border-top: 5px solid var(--color-text-secondary);
      pointer-events: none;
    }

    /* ====== Integrity Badge ====== */
    .integrity-badge {
      display: inline-flex;
      align-items: center;
      gap: 6px;
      padding: 6px 12px;
      border-radius: 20px;
      font-size: 12px;
      font-weight: 600;
      letter-spacing: 0.3px;
      text-transform: uppercase;
      transition: all var(--transition-fast);
    }

    .integrity-badge.valid {
      background: rgba(39, 201, 63, 0.12);
      color: var(--color-verified);
      border: 1px solid rgba(39, 201, 63, 0.25);
    }

    .integrity-badge.tampered {
      background: rgba(255, 71, 87, 0.12);
      color: var(--color-tampered);
      border: 1px solid rgba(255, 71, 87, 0.25);
      animation: pulse-warning 2s ease-in-out infinite;
    }

    .integrity-badge.missing {
      background: rgba(107, 113, 137, 0.12);
      color: var(--color-missing);
      border: 1px solid rgba(107, 113, 137, 0.25);
    }

    @keyframes pulse-warning {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.7; }
    }

    .integrity-dot {
      width: 7px;
      height: 7px;
      border-radius: 50%;
      display: inline-block;
    }

    .integrity-badge.valid .integrity-dot { background: var(--color-verified); }
    .integrity-badge.tampered .integrity-dot { background: var(--color-tampered); }
    .integrity-badge.missing .integrity-dot { background: var(--color-missing); }

    /* ====== Connection Status (reserved) ====== */

    @keyframes pulse-glow {
      0%, 100% { box-shadow: 0 0 4px rgba(39, 201, 63, 0.4); }
      50% { box-shadow: 0 0 12px rgba(39, 201, 63, 0.8); }
    }

    /* ====== Board Layout ====== */
    .board-container {
      padding: 24px 28px;
      overflow-x: auto;
    }

    .board {
      display: grid;
      grid-template-columns: repeat(3, 1fr);
      gap: 20px;
      min-width: 900px;
    }

    /* ====== Columns ====== */
    .column {
      background: var(--color-surface);
      border-radius: var(--radius-lg);
      border: 1px solid var(--color-border-subtle);
      box-shadow: var(--shadow-column);
      display: flex;
      flex-direction: column;
      min-height: 200px;
    }

    .column-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 16px 18px 12px;
      border-bottom: 1px solid var(--color-border-subtle);
    }

    .column-title {
      display: flex;
      align-items: center;
      gap: 10px;
      font-size: 13px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.8px;
      color: var(--color-text-secondary);
    }

    .column-dot {
      width: 9px;
      height: 9px;
      border-radius: 50%;
    }

    .column.todo .column-dot { background: var(--color-todo); }
    .column.in-progress .column-dot { background: var(--color-inprogress); }
    .column.done .column-dot { background: var(--color-done); }

    .column-count {
      background: var(--color-surface-elevated);
      color: var(--color-text-muted);
      font-size: 11px;
      font-weight: 700;
      padding: 2px 8px;
      border-radius: 10px;
      min-width: 22px;
      text-align: center;
    }

    .column-body {
      padding: 12px;
      display: flex;
      flex-direction: column;
      gap: 10px;
      flex: 1;
    }

    /* ====== Cards ====== */
    .card {
      background: var(--color-surface-elevated);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      padding: 16px;
      box-shadow: var(--shadow-card);
      transition: transform var(--transition-normal), box-shadow var(--transition-normal), opacity var(--transition-slow);
      cursor: default;
    }

    .card:hover {
      transform: translateY(-2px);
      box-shadow: var(--shadow-card-hover);
      border-color: var(--color-accent);
    }

    /* Card slide animation classes */
    .card.entering {
      animation: cardEnter 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
    }

    .card.exiting {
      animation: cardExit 0.3s ease-in forwards;
    }

    @keyframes cardEnter {
      from { opacity: 0; transform: translateY(-10px) scale(0.97); }
      to { opacity: 1; transform: translateY(0) scale(1); }
    }

    @keyframes cardExit {
      from { opacity: 1; transform: translateY(0) scale(1); }
      to { opacity: 0; transform: translateY(10px) scale(0.97); }
    }

    .card-header {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      gap: 10px;
      margin-bottom: 12px;
    }

    .card-title {
      font-size: 14px;
      font-weight: 600;
      line-height: 1.4;
      color: var(--color-text);
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }

    .card-title:hover {
      -webkit-line-clamp: unset;
    }

    .card-id {
      font-size: 11px;
      font-family: var(--font-mono);
      color: var(--color-text-muted);
      margin-bottom: 4px;
    }

    /* ====== Priority Badges ====== */
    .priority-badge {
      display: inline-flex;
      align-items: center;
      padding: 3px 8px;
      border-radius: 4px;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.5px;
      flex-shrink: 0;
    }

    .priority-badge.p1 {
      background: rgba(255, 71, 87, 0.15);
      color: var(--color-p1);
    }

    .priority-badge.p2 {
      background: rgba(255, 165, 2, 0.15);
      color: var(--color-p2);
    }

    .priority-badge.p3 {
      background: rgba(52, 152, 219, 0.15);
      color: var(--color-p3);
    }

    /* ====== Progress Bar ====== */
    .progress-container {
      margin: 10px 0;
    }

    .progress-info {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 6px;
    }

    .progress-label {
      font-size: 11px;
      color: var(--color-text-muted);
      font-weight: 500;
    }

    .progress-value {
      font-size: 11px;
      font-family: var(--font-mono);
      color: var(--color-text-secondary);
      font-weight: 600;
    }

    .progress-bar {
      width: 100%;
      height: 4px;
      background: var(--color-border);
      border-radius: 2px;
      overflow: hidden;
    }

    .progress-fill {
      height: 100%;
      border-radius: 2px;
      transition: width var(--transition-slow);
      min-width: 0;
    }

    .column.todo .progress-fill { background: var(--color-todo); }
    .column.in-progress .progress-fill { background: var(--color-inprogress); }
    .column.done .progress-fill { background: var(--color-done); }

    /* ====== Task List ====== */
    .task-list {
      list-style: none;
      display: flex;
      flex-direction: column;
      gap: 4px;
      margin-top: 8px;
    }

    .task-item {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      padding: 5px 6px;
      border-radius: var(--radius-sm);
      font-size: 12px;
      line-height: 1.5;
      color: var(--color-text-secondary);
      transition: background var(--transition-fast);
    }

    .task-item:hover {
      background: var(--color-surface-hover);
    }

    .task-checkbox {
      flex-shrink: 0;
      width: 15px;
      height: 15px;
      border-radius: 3px;
      border: 1.5px solid var(--color-border);
      margin-top: 2px;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all var(--transition-fast);
    }

    .task-checkbox.checked {
      background: var(--color-done);
      border-color: var(--color-done);
    }

    .task-checkbox.checked::after {
      content: '';
      display: block;
      width: 4px;
      height: 7px;
      border: solid white;
      border-width: 0 1.5px 1.5px 0;
      transform: rotate(45deg) translate(-0.5px, -0.5px);
    }

    .task-item.checked .task-description {
      text-decoration: line-through;
      color: var(--color-text-muted);
    }

    .task-id {
      font-family: var(--font-mono);
      font-size: 10px;
      color: var(--color-text-muted);
      flex-shrink: 0;
      margin-top: 1px;
    }

    .task-description {
      flex: 1;
      transition: color var(--transition-fast);
    }

    /* ====== Empty State ====== */
    .empty-state {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 60px 20px;
      text-align: center;
      grid-column: 1 / -1;
    }

    .empty-state-icon {
      width: 64px;
      height: 64px;
      background: var(--color-surface-elevated);
      border-radius: var(--radius-lg);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 28px;
      margin-bottom: 16px;
    }

    .empty-state-title {
      font-size: 16px;
      font-weight: 600;
      color: var(--color-text);
      margin-bottom: 6px;
    }

    .empty-state-text {
      font-size: 13px;
      color: var(--color-text-muted);
      max-width: 300px;
    }

    .column-empty {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 24px 16px;
      color: var(--color-text-muted);
      font-size: 12px;
      font-style: italic;
      flex: 1;
    }

    /* ====== Completion Celebration ====== */
    .card.just-completed {
      animation: celebrateComplete 0.6s ease-out;
    }

    @keyframes celebrateComplete {
      0% { transform: scale(1); }
      30% { transform: scale(1.03); box-shadow: 0 0 20px rgba(39, 201, 63, 0.3); }
      100% { transform: scale(1); }
    }

    /* ====== Loading ====== */
    .loading {
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 60px;
      grid-column: 1 / -1;
    }

    .loading-spinner {
      width: 32px;
      height: 32px;
      border: 3px solid var(--color-border);
      border-top-color: var(--color-accent);
      border-radius: 50%;
      animation: spin 0.8s linear infinite;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }

    /* ====== Responsive ====== */
    @media (max-width: 1024px) {
      .board {
        min-width: unset;
        grid-template-columns: 1fr;
      }
      .column { min-height: 100px; }
    }

    /* ====== Theme Toggle ====== */
    .theme-toggle {
      background: var(--color-surface-elevated);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-sm);
      padding: 6px 10px;
      cursor: pointer;
      font-size: 16px;
      line-height: 1;
      transition: border-color var(--transition-fast), background var(--transition-fast);
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .theme-toggle:hover {
      border-color: var(--color-accent);
      background: var(--color-surface-hover);
    }

    .theme-toggle:focus-visible {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
    }

    /* ====== Collapsible Task List ====== */
    .task-toggle {
      display: flex;
      align-items: center;
      gap: 6px;
      margin-top: 10px;
      padding: 4px 6px;
      border: none;
      background: none;
      color: var(--color-text-muted);
      font-size: 11px;
      font-family: var(--font-sans);
      font-weight: 500;
      cursor: pointer;
      border-radius: var(--radius-sm);
      transition: color var(--transition-fast), background var(--transition-fast);
      width: 100%;
      text-align: left;
    }

    .task-toggle:hover {
      color: var(--color-text-secondary);
      background: var(--color-surface-hover);
    }

    .task-toggle-icon {
      transition: transform var(--transition-fast);
      font-size: 9px;
    }

    .task-toggle-icon.expanded {
      transform: rotate(90deg);
    }

    .task-list {
      overflow: hidden;
      transition: max-height var(--transition-normal), opacity var(--transition-fast);
    }

    .task-list.collapsed {
      max-height: 0;
      opacity: 0;
      margin-top: 0;
    }

    .task-list.expanded {
      max-height: 2000px;
      opacity: 1;
    }

    /* ====== Pipeline Bar ====== */
    .pipeline-bar {
      display: flex;
      align-items: center;
      gap: 0;
      padding: 14px 28px;
      background: var(--color-surface);
      border-bottom: 1px solid var(--color-border);
      position: sticky;
      top: 58px;
      z-index: 90;
    }

    .pipeline-node {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 6px;
      padding: 8px 12px;
      border-radius: var(--radius-md);
      cursor: pointer;
      transition: background var(--transition-fast), transform var(--transition-fast);
      flex: 1 1 0;
      min-width: 72px;
      max-width: 140px;
      position: relative;
      border: 1px solid var(--color-border-subtle);
      background: transparent;
      overflow: hidden;
    }

    .pipeline-node:hover {
      background: var(--color-surface-hover);
      transform: translateY(-1px);
    }

    .pipeline-node:focus-visible {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
    }

    .pipeline-node.active {
      border: 2px solid var(--color-accent);
      background: var(--color-surface-elevated);
    }

    .pipeline-clarify-badge {
      position: absolute;
      top: 2px;
      right: 2px;
      font-size: 9px;
      font-weight: 700;
      color: #b8860b;
      background: rgba(245, 166, 35, 0.18);
      border-radius: 8px;
      padding: 1px 5px;
      line-height: 14px;
      pointer-events: none;
    }

    .pipeline-dot {
      width: 28px;
      height: 28px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 12px;
      font-weight: 700;
      transition: all var(--transition-normal);
    }

    .pipeline-dot.not_started {
      background: var(--color-surface-elevated);
      border: 2px solid var(--color-border);
      color: var(--color-text-muted);
    }

    .pipeline-dot.in_progress {
      background: rgba(245, 166, 35, 0.15);
      border: 2px solid var(--color-inprogress);
      color: var(--color-inprogress);
    }

    .pipeline-dot.complete {
      background: rgba(39, 201, 63, 0.15);
      border: 2px solid var(--color-done);
      color: var(--color-done);
    }

    .pipeline-dot.skipped {
      background: var(--color-surface-elevated);
      border: 2px dashed var(--color-text-muted);
      color: var(--color-text-muted);
    }

    .pipeline-dot.available {
      background: rgba(59, 130, 246, 0.12);
      border: 2px solid var(--color-accent);
      color: var(--color-accent);
    }

    .pipeline-label {
      font-size: 10px;
      font-weight: 600;
      color: var(--color-text-secondary);
      text-transform: uppercase;
      letter-spacing: 0.5px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      max-width: 100%;
    }
    .pipeline-label-multiline {
      white-space: nowrap;
      display: flex;
      flex-direction: column;
      align-items: center;
      line-height: 1.3;
    }
    .pipeline-label-multiline .pipeline-label-line {
      display: block;
      max-width: 100%;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }

    .pipeline-progress {
      font-size: 9px;
      font-family: var(--font-mono);
      color: var(--color-text-muted);
      min-height: 13px;
    }

    .pipeline-connector {
      flex: 0 1 24px;
      min-width: 4px;
      height: 2px;
      background: var(--color-border);
      margin-top: -16px;
    }

    .pipeline-connector.complete {
      background: var(--color-done);
    }

    .pipeline-redirect {
      font-size: 8px;
      opacity: 0.5;
    }

    .pipeline-node.optional .pipeline-label {
      opacity: 0.8;
    }

    /* ====== Bugs Tab (standalone, no connector) ====== */
    .bugs-tab-gap {
      width: 16px;
      flex-shrink: 0;
    }

    .bugs-tab {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 4px;
      padding: 6px 12px;
      cursor: pointer;
      border-radius: 8px;
      border: 1px solid var(--color-border);
      background: var(--color-surface);
      transition: all 0.15s ease;
      position: relative;
      font-family: inherit;
      color: inherit;
    }

    .bugs-tab:hover {
      border-color: var(--color-text-secondary);
      background: rgba(255, 255, 255, 0.06);
    }

    .bugs-tab.active {
      border-color: var(--color-accent);
      background: rgba(99, 102, 241, 0.08);
    }

    .bugs-tab.muted {
      opacity: 0.45;
    }

    .bugs-tab-icon {
      font-size: 14px;
      line-height: 1;
    }

    .bugs-tab-label {
      font-size: 10px;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      color: var(--color-text-secondary);
    }

    .bugs-tab-badge {
      position: absolute;
      top: -6px;
      right: -6px;
      min-width: 18px;
      height: 18px;
      padding: 0 5px;
      border-radius: 9px;
      font-size: 11px;
      font-weight: 600;
      line-height: 18px;
      text-align: center;
      color: #fff;
    }

    .bugs-tab-badge.muted {
      opacity: 0.4;
      background: var(--color-text-secondary);
    }

    .bugs-tab-badge.severity-critical { background: #ff4757; }
    .bugs-tab-badge.severity-high { background: #ffa502; }
    .bugs-tab-badge.severity-medium { background: #f1c40f; color: #1a1a2e; }
    .bugs-tab-badge.severity-low { background: #6b7189; }

    /* ====== Bug Severity Colors ====== */
    .severity-critical { color: #ff4757; }
    .severity-high { color: #ffa502; }
    .severity-medium { color: #f1c40f; }
    .severity-low { color: #6b7189; }

    .severity-badge {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 4px;
      font-size: 11px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.3px;
    }

    .severity-badge.severity-critical { background: rgba(255, 71, 87, 0.15); color: #ff4757; }
    .severity-badge.severity-high { background: rgba(255, 165, 2, 0.15); color: #ffa502; }
    .severity-badge.severity-medium { background: rgba(241, 196, 15, 0.15); color: #f1c40f; }
    .severity-badge.severity-low { background: rgba(107, 113, 137, 0.15); color: #6b7189; }

    /* ====== Bugs View ====== */
    .bugs-view {
      padding: 24px;
      max-width: 1200px;
      margin: 0 auto;
    }

    .bugs-view-header {
      margin-bottom: 20px;
    }

    .bugs-view-title {
      font-size: 20px;
      font-weight: 600;
      color: var(--color-text-primary);
    }

    .bugs-view-subtitle {
      font-size: 13px;
      color: var(--color-text-secondary);
      margin-top: 4px;
    }

    .bugs-table {
      width: 100%;
      border-collapse: collapse;
      font-size: 13px;
    }

    .bugs-table th {
      text-align: left;
      padding: 10px 12px;
      font-weight: 600;
      color: var(--color-text-secondary);
      border-bottom: 1px solid var(--color-border);
      font-size: 11px;
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }

    .bugs-table td {
      padding: 10px 12px;
      border-bottom: 1px solid rgba(255, 255, 255, 0.04);
      vertical-align: middle;
    }

    .bugs-table tr:hover td {
      background: rgba(255, 255, 255, 0.02);
    }

    .bugs-table tr.highlighted td {
      background: rgba(99, 102, 241, 0.12);
      transition: background 0.3s ease;
    }

    .bugs-table .bug-id {
      font-family: 'SF Mono', 'Fira Code', monospace;
      font-weight: 600;
      color: var(--color-text-primary);
    }

    .bugs-table .bug-status {
      text-transform: capitalize;
    }

    .bugs-table .bug-status.fixed {
      color: var(--color-done);
    }

    .bugs-table .bug-progress {
      font-family: 'SF Mono', 'Fira Code', monospace;
      font-size: 12px;
    }

    .bugs-table .bug-description {
      max-width: 400px;
      line-height: 1.5;
    }

    .bugs-table .github-link {
      color: var(--color-accent);
      text-decoration: none;
    }

    .bugs-table .github-link:hover {
      text-decoration: underline;
    }

    .bugs-empty {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 80px 20px;
      text-align: center;
    }

    .bugs-empty-icon {
      font-size: 48px;
      margin-bottom: 16px;
      opacity: 0.4;
    }

    .bugs-empty-title {
      font-size: 18px;
      font-weight: 600;
      color: var(--color-text-primary);
      margin-bottom: 8px;
    }

    .bugs-empty-text {
      font-size: 14px;
      color: var(--color-text-secondary);
    }

    .orphaned-section {
      margin-top: 24px;
      padding: 16px;
      border: 1px dashed var(--color-border);
      border-radius: 8px;
      opacity: 0.7;
    }

    .orphaned-title {
      font-size: 13px;
      font-weight: 600;
      color: var(--color-text-secondary);
      margin-bottom: 8px;
    }

    .orphaned-task {
      font-size: 12px;
      color: var(--color-text-secondary);
      padding: 4px 0;
    }

    .bug-icon-inline {
      display: inline-block;
      width: 14px;
      height: 14px;
      margin-right: 4px;
      vertical-align: middle;
      opacity: 0.7;
    }

    /* Pipeline bugs gap & node (spec aliases) */
    .pipeline-bugs-gap {
      width: 20px;
      flex-shrink: 0;
    }
    .pipeline-node-bugs.muted {
      opacity: 0.5;
    }
    .bugs-dot {
      background: var(--color-surface-elevated) !important;
      border: 2px solid var(--color-border) !important;
      color: var(--color-text-muted) !important;
    }
    .bug-count-badge {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      min-width: 18px;
      height: 18px;
      padding: 0 5px;
      border-radius: 9px;
      font-size: 11px;
      font-weight: 700;
      margin-left: 4px;
      vertical-align: middle;
    }
    .bug-count-badge.muted {
      background: var(--color-surface-elevated);
      color: var(--color-text-muted);
    }

    /* Bug view additional styles */
    .bugs-view-container {
      padding: 24px;
      max-width: 1200px;
    }
    .bugs-view-container h2 {
      font-size: 18px;
      font-weight: 600;
      margin-bottom: 16px;
      color: var(--color-text);
    }
    .bug-row:hover {
      background: var(--color-surface-hover);
    }
    .fix-progress {
      font-variant-numeric: tabular-nums;
      font-weight: 500;
    }
    .bug-status {
      text-transform: capitalize;
    }
    .bug-status.fixed {
      color: var(--color-success, #2ecc71);
    }
    .bug-github-link {
      color: var(--color-accent);
      text-decoration: none;
    }
    .bug-github-link:hover {
      text-decoration: underline;
    }
    .bug-empty-state {
      text-align: center;
      padding: 48px 24px;
      color: var(--color-text-muted);
      font-size: 14px;
    }
    .bug-empty-state svg {
      display: block;
      margin: 0 auto 12px;
      opacity: 0.4;
    }
    .orphaned-tasks-section {
      margin-top: 32px;
      padding: 16px;
      background: var(--color-surface-elevated);
      border-radius: var(--radius-md);
      border: 1px solid var(--color-warning, #f5a623);
      opacity: 0.7;
    }
    .orphaned-tasks-section h3 {
      font-size: 13px;
      font-weight: 600;
      color: var(--color-warning, #f5a623);
      margin-bottom: 8px;
    }
    .orphaned-tasks-section .task-item {
      font-size: 12px;
      color: var(--color-text-muted);
      padding: 4px 0;
    }

    /* ====== Content Area ====== */
    .content-area {
      flex: 1;
      min-height: 0;
    }

    .placeholder-view {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 80px 20px;
      text-align: center;
    }

    .placeholder-view-icon {
      width: 56px;
      height: 56px;
      background: var(--color-surface-elevated);
      border-radius: var(--radius-lg);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 24px;
      margin-bottom: 16px;
    }

    .placeholder-view-title {
      font-size: 16px;
      font-weight: 600;
      color: var(--color-text);
      margin-bottom: 6px;
    }

    .placeholder-view-text {
      font-size: 13px;
      color: var(--color-text-muted);
      max-width: 360px;
      margin: 0 auto;
      text-align: center;
    }

    /* ====== Checklist Quality Gates Tab ====== */
    .checklist-view {
      padding: 24px 28px;
      max-width: 900px;
      margin: 0 auto;
    }

    .gate-indicator {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 16px 20px;
      border-radius: var(--radius-lg);
      background: var(--color-surface-elevated);
      border: 1px solid var(--color-border);
      margin-bottom: 28px;
    }

    .gate-dot {
      width: 18px;
      height: 18px;
      border-radius: 50%;
      flex-shrink: 0;
      transition: background-color 0.4s ease;
    }

    .gate-dot.green { background-color: var(--color-green, #22c55e); }
    .gate-dot.yellow { background-color: var(--color-yellow, #eab308); }
    .gate-dot.red { background-color: var(--color-red, #ef4444); }

    .gate-label {
      font-size: 15px;
      font-weight: 700;
      letter-spacing: 0.5px;
    }

    .gate-label.open { color: var(--color-green, #22c55e); }
    .gate-label.blocked-yellow { color: var(--color-yellow, #eab308); }
    .gate-label.blocked-red { color: var(--color-red, #ef4444); }

    .checklist-rings {
      display: flex;
      flex-wrap: wrap;
      gap: 24px;
      justify-content: center;
      margin-bottom: 24px;
    }

    .checklist-ring-wrapper {
      display: flex;
      flex-direction: column;
      align-items: center;
      cursor: pointer;
      padding: 12px;
      border-radius: var(--radius-lg);
      transition: background-color 0.15s ease;
      border: 2px solid transparent;
    }

    .checklist-ring-wrapper:hover {
      background: var(--color-surface-elevated);
    }

    .checklist-ring-wrapper:focus-visible {
      outline: 2px solid var(--color-primary);
      outline-offset: 2px;
    }

    .checklist-ring-wrapper.expanded {
      border-color: var(--color-primary);
      background: var(--color-surface-elevated);
    }

    .checklist-ring-svg {
      width: 100px;
      height: 100px;
    }

    .checklist-ring-track {
      fill: none;
      stroke: var(--color-border);
      stroke-width: 8;
    }

    .checklist-ring-fill {
      fill: none;
      stroke-width: 8;
      stroke-linecap: round;
      transition: stroke-dashoffset 0.8s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.4s ease;
      transform: rotate(-90deg);
      transform-origin: 50% 50%;
    }

    .checklist-ring-fill.red { stroke: var(--color-red, #ef4444); }
    .checklist-ring-fill.yellow { stroke: var(--color-yellow, #eab308); }
    .checklist-ring-fill.green { stroke: var(--color-green, #22c55e); }

    .checklist-ring-pct {
      font-size: 18px;
      font-weight: 700;
      fill: var(--color-text);
      text-anchor: middle;
      dominant-baseline: central;
    }

    .checklist-ring-name {
      margin-top: 8px;
      font-size: 13px;
      font-weight: 600;
      color: var(--color-text);
      text-align: center;
    }

    .checklist-ring-fraction {
      font-size: 12px;
      color: var(--color-text-muted);
    }

    .checklist-detail {
      max-height: 0;
      overflow: hidden;
      transition: max-height 0.35s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.15s ease, opacity 0.15s ease;
      border-radius: var(--radius-lg);
      background: var(--color-surface-elevated);
      border: 1px solid transparent;
      margin-bottom: 0;
      opacity: 0;
    }

    .checklist-detail.open {
      max-height: 2000px;
      border-color: var(--color-border);
      margin-bottom: 16px;
      opacity: 1;
    }

    .checklist-detail-inner {
      padding: 16px 20px;
    }

    .checklist-category {
      font-size: 12px;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      color: var(--color-text-muted);
      margin: 16px 0 8px;
      padding-bottom: 4px;
      border-bottom: 1px solid var(--color-border);
    }

    .checklist-category:first-child {
      margin-top: 0;
    }

    .checklist-item {
      display: flex;
      align-items: flex-start;
      gap: 8px;
      padding: 6px 0;
      font-size: 13px;
      color: var(--color-text);
      line-height: 1.4;
    }

    .checklist-item-icon {
      flex-shrink: 0;
      width: 18px;
      font-size: 14px;
    }

    .checklist-item-icon.checked { color: var(--color-green, #22c55e); }
    .checklist-item-icon.unchecked { color: var(--color-text-muted); }

    .checklist-item-id {
      font-family: var(--font-mono, monospace);
      font-size: 11px;
      color: var(--color-text-muted);
      background: var(--color-surface);
      padding: 1px 5px;
      border-radius: var(--radius-sm, 4px);
      flex-shrink: 0;
    }

    .checklist-item-tag {
      font-size: 10px;
      font-weight: 600;
      padding: 1px 6px;
      border-radius: 9999px;
      background: var(--color-primary-muted, rgba(99, 102, 241, 0.15));
      color: var(--color-primary, #6366f1);
      flex-shrink: 0;
    }

    .checklist-empty {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 80px 20px;
      text-align: center;
    }

    .checklist-empty-icon {
      width: 56px;
      height: 56px;
      background: var(--color-surface-elevated);
      border-radius: var(--radius-lg);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 24px;
      margin-bottom: 16px;
    }

    .checklist-empty-title {
      font-size: 16px;
      font-weight: 600;
      color: var(--color-text);
      margin-bottom: 6px;
    }

    .checklist-empty-text {
      font-size: 13px;
      color: var(--color-text-muted);
      max-width: 360px;
    }

    /* ====== Testify Traceability Tab ====== */
    .testify-view {
      padding: 24px 28px;
      max-width: 1200px;
      margin: 0 auto;
    }

    .testify-seal {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 12px 18px;
      border-radius: var(--radius-lg);
      background: var(--color-surface-elevated);
      border: 1px solid var(--color-border);
      margin-bottom: 20px;
      font-size: 13px;
      font-weight: 600;
    }

    .testify-seal-dot {
      width: 10px;
      height: 10px;
      border-radius: 50%;
      flex-shrink: 0;
    }

    .testify-seal-dot.valid { background: var(--color-verified); }
    .testify-seal-dot.tampered { background: var(--color-tampered); animation: pulse-warning 2s ease-in-out infinite; }
    .testify-seal-dot.missing { background: var(--color-text-muted); }

    .testify-seal-label.valid { color: var(--color-verified); }
    .testify-seal-label.tampered { color: var(--color-tampered); }
    .testify-seal-label.missing { color: var(--color-text-muted); }

    .testify-seal-hash {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--color-text-muted);
      margin-left: auto;
    }

    .testify-sankey-container {
      background: var(--color-surface);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-lg);
      padding: 20px;
      overflow-x: auto;
    }

    .testify-sankey-svg {
      display: block;
      width: 100%;
    }

    .testify-sankey-svg .sankey-node {
      cursor: pointer;
      transition: opacity var(--transition-fast);
    }

    .testify-sankey-svg .sankey-node:focus {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
    }

    .testify-sankey-svg .sankey-node-rect {
      rx: 4;
      ry: 4;
      stroke-width: 1.5;
      transition: stroke var(--transition-fast), fill var(--transition-fast);
    }

    .testify-sankey-svg .sankey-node-label {
      font-family: var(--font-mono);
      font-size: 10px;
      font-weight: 600;
      fill: var(--color-text);
      pointer-events: none;
    }

    .testify-sankey-svg .sankey-node-desc {
      font-family: var(--font-sans);
      font-size: 9px;
      fill: var(--color-text-secondary);
      pointer-events: none;
    }

    .testify-sankey-svg .sankey-flow {
      fill-opacity: 0.25;
      stroke-opacity: 0.4;
      stroke-width: 1;
      transition: fill-opacity var(--transition-fast), stroke-opacity var(--transition-fast), opacity var(--transition-fast);
    }

    .testify-sankey-svg .sankey-flow:hover {
      fill-opacity: 0.45;
      stroke-opacity: 0.7;
    }

    .testify-sankey-svg .sankey-group-bg {
      rx: 6;
      ry: 6;
    }

    .testify-sankey-svg .sankey-group-label {
      font-family: var(--font-sans);
      font-size: 10px;
      font-weight: 600;
      fill: var(--color-text-secondary);
    }

    .testify-sankey-svg .sankey-col-label {
      font-family: var(--font-sans);
      font-size: 11px;
      font-weight: 700;
      fill: var(--color-text-muted);
      text-transform: uppercase;
      letter-spacing: 0.5px;
    }

    .testify-sankey-svg .dimmed {
      opacity: 0.12;
    }

    .testify-sankey-svg .highlighted .sankey-flow {
      fill-opacity: 0.5;
      stroke-opacity: 0.8;
    }

    .testify-sankey-svg .gap-node .sankey-node-rect {
      stroke: var(--color-tampered);
      stroke-width: 2;
      stroke-dasharray: 4 2;
    }

    .testify-empty {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 80px 20px;
      text-align: center;
    }

    .testify-empty-icon {
      width: 56px;
      height: 56px;
      background: var(--color-surface-elevated);
      border-radius: var(--radius-lg);
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 24px;
      margin-bottom: 16px;
    }

    .testify-empty-title {
      font-size: 16px;
      font-weight: 600;
      color: var(--color-text);
      margin-bottom: 6px;
    }

    .testify-empty-text {
      font-size: 13px;
      color: var(--color-text-muted);
      max-width: 400px;
    }

    .testify-empty-text code {
      background: var(--color-surface-elevated);
      padding: 2px 6px;
      border-radius: 4px;
      font-family: var(--font-mono);
      font-size: 12px;
    }

    @keyframes testify-fade-in {
      from { opacity: 0; transform: translateY(6px); }
      to { opacity: 1; transform: translateY(0); }
    }

    .testify-sankey-svg .sankey-col-left .sankey-node { animation: testify-fade-in 0.4s ease both; }
    .testify-sankey-svg .sankey-col-mid .sankey-node { animation: testify-fade-in 0.4s ease 0.15s both; }
    .testify-sankey-svg .sankey-col-right .sankey-node { animation: testify-fade-in 0.4s ease 0.3s both; }
    .testify-sankey-svg .sankey-flow { animation: testify-fade-in 0.4s ease 0.45s both; }

    /* ====== Spec Story Map Tab ====== */
    .storymap-view {
      padding: 24px 28px;
      display: flex;
      transition: padding-right var(--transition-normal);
    }

    .storymap-main {
      flex: 1;
      min-width: 0;
    }

    .storymap-section-title {
      font-size: 13px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.8px;
      color: var(--color-text-secondary);
      margin-bottom: 16px;
    }

    /* Story Map Swim Lanes */
    .swim-lanes {
      margin-bottom: 32px;
    }

    .swim-lane {
      display: flex;
      align-items: flex-start;
      gap: 12px;
      margin-bottom: 12px;
      min-height: 80px;
    }

    .swim-lane-label {
      width: 40px;
      flex-shrink: 0;
      font-size: 12px;
      font-weight: 700;
      letter-spacing: 0.5px;
      padding: 8px 0;
      text-align: center;
      border-radius: var(--radius-sm);
      color: white;
    }

    .swim-lane-label.p1 { background: var(--color-p1); }
    .swim-lane-label.p2 { background: var(--color-p2); }
    .swim-lane-label.p3 { background: var(--color-p3); }

    .swim-lane-cards {
      display: flex;
      gap: 12px;
      flex-wrap: wrap;
      flex: 1;
      min-height: 40px;
      padding: 4px;
      border-radius: var(--radius-sm);
      border: 1px dashed var(--color-border-subtle);
    }

    .swim-lane-cards:empty::after {
      content: 'No stories';
      color: var(--color-text-muted);
      font-size: 12px;
      font-style: italic;
      padding: 8px;
    }

    /* Story Cards */
    .story-card {
      background: var(--color-surface);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      padding: 12px 14px;
      min-width: 200px;
      max-width: 280px;
      cursor: pointer;
      transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
    }

    .story-card:hover {
      border-color: var(--color-accent);
      box-shadow: var(--shadow-card-hover);
    }

    .story-card-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 6px;
    }

    .story-card-id {
      font-size: 11px;
      font-weight: 700;
      font-family: var(--font-mono);
      color: var(--color-accent);
    }

    .story-card-priority {
      font-size: 10px;
      font-weight: 700;
      padding: 2px 6px;
      border-radius: 10px;
      color: white;
    }

    .story-card-priority.p1 { background: var(--color-p1); }
    .story-card-priority.p2 { background: var(--color-p2); }
    .story-card-priority.p3 { background: var(--color-p3); }

    .story-card-title {
      font-size: 13px;
      font-weight: 500;
      color: var(--color-text);
      margin-bottom: 8px;
      line-height: 1.3;
      overflow: hidden;
      text-overflow: ellipsis;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
    }

    .story-card-meta {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      align-items: center;
    }

    .story-card-badge {
      font-size: 10px;
      font-weight: 600;
      padding: 2px 6px;
      border-radius: 8px;
      background: var(--color-surface-elevated);
      color: var(--color-text-secondary);
      font-family: var(--font-mono);
    }

    .story-card-badge.scenarios {
      background: rgba(59, 130, 246, 0.12);
      color: var(--color-accent);
    }

    .story-card-badge.clarifications {
      background: rgba(245, 166, 35, 0.12);
      color: var(--color-inprogress);
      cursor: pointer;
    }

    .story-card-badge.clarifications:hover {
      background: rgba(245, 166, 35, 0.25);
    }

    /* Requirements Graph */
    .graph-container {
      background: var(--color-surface);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-lg);
      position: relative;
      overflow: hidden;
      margin-bottom: 24px;
    }

    .graph-svg {
      width: 100%;
      cursor: grab;
    }

    .graph-svg:active { cursor: grabbing; }

    .graph-edge {
      stroke: var(--color-border);
      stroke-width: 1.5;
      fill: none;
      transition: opacity var(--transition-fast), stroke var(--transition-fast);
    }

    .graph-edge.highlighted {
      stroke: var(--color-text);
      stroke-width: 2.5;
    }

    .graph-edge.dimmed { opacity: 0.15; }

    .graph-node { cursor: pointer; }
    .graph-node.dimmed { opacity: 0.2; }

    .graph-node circle {
      transition: stroke var(--transition-fast), r var(--transition-fast);
    }

    .graph-node.highlighted circle {
      stroke: var(--color-text);
      stroke-width: 3;
      filter: drop-shadow(0 0 4px var(--color-text));
    }

    .graph-node-us circle { fill: var(--color-accent); }
    .graph-node-fr circle { fill: var(--color-done); }
    .graph-node-sc circle { fill: var(--color-inprogress); }

    .graph-node text {
      font-size: 10px;
      font-weight: 600;
      font-family: var(--font-mono);
      fill: var(--color-text);
      text-anchor: middle;
      pointer-events: none;
    }

    .graph-tooltip {
      position: absolute;
      background: var(--color-surface-elevated);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-sm);
      padding: 8px 12px;
      font-size: 12px;
      color: var(--color-text);
      max-width: 300px;
      pointer-events: none;
      z-index: 50;
      box-shadow: var(--shadow-card);
      display: none;
    }

    .graph-empty {
      display: flex;
      align-items: center;
      justify-content: center;
      height: 200px;
      color: var(--color-text-muted);
      font-size: 13px;
    }

    .graph-legend {
      display: flex;
      gap: 16px;
      padding: 8px 14px;
      border-top: 1px solid var(--color-border-subtle);
      font-size: 11px;
      color: var(--color-text-secondary);
    }

    .graph-legend-item {
      display: flex;
      align-items: center;
      gap: 5px;
    }

    .graph-legend-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
    }

    .graph-legend-dot.us { background: var(--color-accent); }
    .graph-legend-dot.fr { background: var(--color-done); }
    .graph-legend-dot.sc { background: var(--color-inprogress); }

    /* Clarify View */
    .clarify-view {
      padding: 24px 32px;
      max-width: 800px;
      margin: 0 auto;
    }

    .clarify-empty {
      text-align: center;
      padding: 60px 20px;
      color: var(--color-text-muted);
      max-width: 480px;
      margin: 0 auto;
      word-break: normal;
      overflow-wrap: break-word;
    }

    .clarify-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 24px;
      padding-bottom: 16px;
      border-bottom: 1px solid var(--color-border);
    }

    .clarify-title {
      font-size: 16px;
      font-weight: 600;
      color: var(--color-text);
    }

    .clarify-count {
      font-size: 12px;
      color: var(--color-text-muted);
      background: var(--color-surface-hover);
      padding: 4px 10px;
      border-radius: 12px;
    }

    .clarify-session {
      margin-bottom: 24px;
    }

    .clarify-session-label {
      font-size: 11px;
      font-weight: 600;
      color: var(--color-text-muted);
      text-transform: uppercase;
      letter-spacing: 0.5px;
      margin-bottom: 12px;
    }

    .clarify-entries {
      display: flex;
      flex-direction: column;
      gap: 12px;
    }

    .clarify-entry {
      background: var(--color-surface);
      border: 1px solid var(--color-border-subtle);
      border-radius: var(--radius-md);
      padding: 14px 16px;
    }

    .clarify-question {
      font-size: 13px;
      font-weight: 600;
      color: var(--color-text);
      margin-bottom: 8px;
      line-height: 1.5;
    }

    .clarify-answer {
      font-size: 13px;
      color: var(--color-text-secondary);
      line-height: 1.5;
    }

    .clarify-q-label, .clarify-a-label {
      display: inline-block;
      width: 20px;
      height: 20px;
      line-height: 20px;
      text-align: center;
      border-radius: 4px;
      font-size: 11px;
      font-weight: 700;
      margin-right: 8px;
      flex-shrink: 0;
    }

    .clarify-q-label {
      background: var(--color-accent);
      color: white;
    }

    .clarify-a-label {
      background: var(--color-done);
      color: white;
    }

    .clarify-refs {
      display: flex;
      flex-wrap: wrap;
      gap: 6px;
      margin-top: 10px;
      padding-top: 8px;
      border-top: 1px solid var(--color-border-subtle);
    }

    .clarify-ref {
      font-size: 11px;
      font-weight: 600;
      padding: 2px 8px;
      border-radius: 10px;
      background: var(--color-surface-hover);
      color: var(--color-accent);
      letter-spacing: 0.3px;
      text-decoration: none;
      cursor: pointer;
      transition: background var(--transition-fast), color var(--transition-fast);
    }

    .clarify-ref:hover {
      background: var(--color-accent);
      color: white;
    }

    .story-card.highlighted {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
      animation: cardPulse 0.6s ease-out;
    }

    @keyframes cardPulse {
      0% { outline-color: transparent; }
      50% { outline-color: var(--color-accent); }
      100% { outline-color: var(--color-accent); }
    }

    /* ====== Cross-Link Navigation ====== */
    .cross-link {
      cursor: pointer;
      text-decoration: underline;
      text-decoration-style: dotted;
      text-decoration-color: var(--color-accent);
      text-underline-offset: 2px;
      transition: text-decoration-color var(--transition-fast);
    }
    .cross-link:hover {
      text-decoration-style: solid;
      text-decoration-color: var(--color-accent-hover);
    }

    .card.highlighted {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
      animation: highlight-pulse 1.5s ease-out;
    }

    .task-item.highlighted {
      background: color-mix(in srgb, var(--color-accent) 15%, transparent);
      border-radius: var(--radius-sm);
      animation: highlight-pulse 1.5s ease-out;
    }

    .checklist-item.highlighted {
      background: color-mix(in srgb, var(--color-accent) 15%, transparent);
      border-radius: var(--radius-sm);
      animation: highlight-pulse 1.5s ease-out;
    }

    .clarify-entry.highlighted {
      outline: 2px solid var(--color-accent);
      outline-offset: 2px;
      border-radius: var(--radius-sm);
      animation: highlight-pulse 1.5s ease-out;
    }

    /* Clarify FAB + Slide-out Panel */
    .clarify-fab {
      position: fixed;
      bottom: 24px;
      right: 24px;
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background: var(--color-inprogress);
      color: #fff;
      border: none;
      font-size: 14px;
      font-weight: 700;
      cursor: pointer;
      box-shadow: 0 4px 12px rgba(0,0,0,0.3);
      z-index: 1000;
      display: none;
      align-items: center;
      justify-content: center;
      transition: transform var(--transition-fast), box-shadow var(--transition-fast);
    }
    .clarify-fab:hover {
      transform: scale(1.1);
      box-shadow: 0 6px 20px rgba(0,0,0,0.4);
    }
    .clarify-fab.visible { display: flex; }
    .clarify-fab.panel-open { background: var(--color-text-muted); }
    .clarify-fab.panel-open:hover { transform: scale(1.1); }

    .clarify-slide-panel {
      position: fixed;
      top: 0;
      right: -420px;
      width: 400px;
      height: 100vh;
      background: var(--color-bg);
      border-left: 1px solid var(--color-border);
      box-shadow: -4px 0 24px rgba(0,0,0,0.3);
      z-index: 999;
      overflow-y: auto;
      transition: right 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
      padding: 20px;
    }
    .clarify-slide-panel.open { right: 0; }
    .clarify-slide-panel .clarify-view { padding: 0; max-width: none; }

    .clarify-tabs {
      display: flex;
      gap: 4px;
      margin-bottom: 16px;
      border-bottom: 1px solid var(--color-border);
      padding-bottom: 8px;
    }
    .clarify-tab {
      background: none;
      border: none;
      color: var(--color-text-muted);
      font-size: 12px;
      font-weight: 600;
      padding: 6px 12px;
      border-radius: var(--radius-sm);
      cursor: pointer;
      transition: background var(--transition-fast), color var(--transition-fast);
    }
    .clarify-tab:hover { background: var(--color-surface-hover); color: var(--color-text); }
    .clarify-tab.active { background: var(--color-accent); color: #fff; }

    .clarify-panel-close {
      background: none;
      border: none;
      color: var(--color-text-muted);
      font-size: 20px;
      cursor: pointer;
      padding: 4px 8px;
      border-radius: var(--radius-sm);
    }
    .clarify-panel-close:hover { color: var(--color-text); background: var(--color-surface-hover); }

    .sankey-node.task-checked .sankey-node-rect {
      stroke: var(--color-done);
      stroke-width: 2;
    }

    .cross-link-tip {
      font-size: 11px;
      font-style: italic;
      color: var(--color-text-muted);
      padding: 6px 12px;
      user-select: none;
    }

    @keyframes highlight-pulse {
      0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--color-accent) 40%, transparent); }
      70% { box-shadow: 0 0 0 8px transparent; }
      100% { box-shadow: 0 0 0 0 transparent; }
    }

    /* Detail Panel — floating left sidebar */
    .detail-panel {
      position: fixed;
      left: 0;
      top: 57px;
      width: 360px;
      height: calc(100vh - 57px);
      overflow-y: auto;
      background: var(--color-surface);
      border-right: 1px solid var(--color-border);
      padding: 20px 24px;
      box-shadow: 4px 0 16px rgba(0,0,0,0.15);
      z-index: 90;
      animation: slideIn 0.2s ease-out;
    }

    @keyframes slideIn {
      from { opacity: 0; transform: translateX(-12px); }
      to { opacity: 1; transform: translateX(0); }
    }

    .detail-panel-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 12px;
    }

    .detail-panel-id {
      font-size: 12px;
      font-weight: 700;
      font-family: var(--font-mono);
      padding: 3px 8px;
      border-radius: 8px;
      color: white;
    }

    .detail-panel-id.us { background: var(--color-accent); }
    .detail-panel-id.fr { background: var(--color-done); }
    .detail-panel-id.sc { background: var(--color-inprogress); }

    .detail-panel-close {
      background: none;
      border: none;
      color: var(--color-text-secondary);
      cursor: pointer;
      font-size: 18px;
      padding: 4px 8px;
      border-radius: var(--radius-sm);
      transition: background var(--transition-fast);
    }

    .detail-panel-close:hover {
      background: var(--color-surface-hover);
    }

    .detail-panel-title {
      font-size: 16px;
      font-weight: 600;
      color: var(--color-text);
      margin-bottom: 12px;
    }

    .detail-panel-body {
      font-size: 13px;
      color: var(--color-text-secondary);
      line-height: 1.7;
      white-space: pre-wrap;
    }

    .detail-panel-body strong {
      color: var(--color-text);
    }

    /* Storymap empty state */
    .storymap-empty {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      padding: 60px 20px;
      text-align: center;
      color: var(--color-text-muted);
      font-size: 13px;
    }

    /* ====== Constitution Tab ====== */
    .constitution-view {
      padding: 24px 28px;
    }

    .premise-section {
      margin-bottom: 28px;
    }
    .premise-section-title {
      font-size: 13px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.8px;
      color: var(--color-text-secondary);
      margin-bottom: 12px;
    }
    .premise-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
      gap: 8px;
    }
    .premise-card {
      background: var(--color-surface);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-md);
      padding: 14px 16px;
      cursor: pointer;
      transition: all var(--transition-fast);
    }
    .premise-card:hover {
      background: var(--color-surface-hover);
      border-color: var(--color-text-muted);
    }
    .premise-card.selected {
      border-color: var(--color-accent);
      background: var(--color-surface-elevated);
    }
    .premise-card-label {
      font-size: 10px;
      font-weight: 700;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      color: var(--color-text-muted);
      margin-bottom: 6px;
    }
    .premise-card.selected .premise-card-label {
      color: var(--color-accent);
    }
    .premise-card-preview {
      font-size: 12px;
      line-height: 1.5;
      color: var(--color-text-secondary);
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      overflow: hidden;
    }
    .premise-card.selected .premise-card-preview {
      display: none;
    }
    .premise-card-body {
      display: none;
      font-size: 13px;
      line-height: 1.6;
      color: var(--color-text-secondary);
    }
    .premise-card.selected .premise-card-body {
      display: block;
      animation: cardEnter 0.25s ease;
    }
    .premise-card-body p {
      margin: 0 0 6px;
    }
    .premise-card-body ul {
      margin: 4px 0 8px 16px;
      padding: 0;
    }
    .premise-card-body li {
      margin-bottom: 3px;
    }
    .premise-card-body strong {
      color: var(--color-text);
    }

    .constitution-layout {
      display: flex;
      gap: 32px;
      align-items: flex-start;
    }

    @media (max-width: 900px) {
      .constitution-layout { flex-direction: column; }
    }

    .constitution-left {
      flex: 1 1 50%;
      min-width: 0;
      display: flex;
      align-items: flex-start;
      justify-content: center;
    }

    .constitution-right {
      flex: 1 1 40%;
      min-width: 0;
    }

    .constitution-summary {
      display: flex;
      flex-direction: column;
      gap: 2px;
      margin-bottom: 20px;
    }

    .constitution-summary-item {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 6px 8px;
      font-size: 13px;
      font-weight: 500;
      color: var(--color-text);
      border-bottom: 1px solid var(--color-border-subtle);
      cursor: pointer;
      border-radius: var(--radius-sm);
      transition: background var(--transition-fast);
    }

    .constitution-summary-item:last-child { border-bottom: none; }
    .constitution-summary-item:hover { background: var(--color-surface-hover); }
    .constitution-summary-item.selected { background: var(--color-surface-elevated); border-left: 3px solid var(--color-accent); }

    .constitution-summary-item .principle-num {
      font-family: var(--font-mono);
      font-size: 11px;
      color: var(--color-text-muted);
      flex-shrink: 0;
      width: 24px;
    }

    .constitution-summary-item .level-badge {
      font-size: 9px;
      font-weight: 700;
      padding: 2px 5px;
      border-radius: 3px;
      text-transform: uppercase;
      margin-left: auto;
      flex-shrink: 0;
      letter-spacing: 0.3px;
    }

    .level-badge.must { background: rgba(59, 130, 246, 0.15); color: var(--color-accent); }
    .level-badge.should { background: rgba(245, 166, 35, 0.15); color: var(--color-inprogress); }
    .level-badge.may { background: rgba(107, 113, 137, 0.15); color: var(--color-text-muted); }

    .constitution-body {
      display: flex;
      gap: 24px;
      align-items: flex-start;
    }

    @media (max-width: 768px) {
      .constitution-body { flex-direction: column; }
    }

    .radar-container {
      width: 100%;
      max-width: 520px;
    }

    .radar-container svg {
      width: 100%;
      height: auto;
      overflow: visible;
    }

    .radar-axis {
      cursor: pointer;
      transition: opacity var(--transition-fast);
    }

    .radar-axis:hover { opacity: 0.8; }
    .radar-axis:focus-visible { outline: 2px solid var(--color-accent); outline-offset: 2px; }

    .detail-card {
      background: var(--color-surface-elevated);
      border: 1px solid var(--color-border);
      border-radius: var(--radius-lg);
      padding: 20px;
      margin-top: 12px;
      animation: cardEnter 0.25s ease;
    }

    .detail-card h3 {
      font-size: 16px;
      font-weight: 700;
      margin-bottom: 4px;
      color: var(--color-text);
    }

    .detail-card .detail-level {
      margin-bottom: 12px;
    }

    .detail-card .detail-text {
      font-size: 13px;
      line-height: 1.6;
      color: var(--color-text-secondary);
      margin-bottom: 16px;
    }

    .detail-card .detail-rationale {
      font-size: 12px;
      line-height: 1.5;
      color: var(--color-text-muted);
      padding-top: 12px;
      border-top: 1px solid var(--color-border-subtle);
    }

    .detail-card .detail-rationale strong {
      color: var(--color-text-secondary);
    }

    .amendment-timeline {
      margin-top: 24px;
      padding: 16px 0;
      border-top: 1px solid var(--color-border-subtle);
    }

    .amendment-timeline-label {
      font-size: 11px;
      font-weight: 600;
      text-transform: uppercase;
      letter-spacing: 0.5px;
      color: var(--color-text-muted);
      margin-bottom: 8px;
    }

    .amendment-timeline-content {
      display: flex;
      align-items: center;
      gap: 12px;
      font-size: 12px;
      color: var(--color-text-secondary);
    }

    .amendment-timeline-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--color-accent);
      flex-shrink: 0;
    }

    .amendment-timeline-line {
      flex: 1;
      height: 2px;
      background: var(--color-border);
    }

    /* ====== Plan View ====== */
    .planview-view { padding: 24px 28px; }
    .planview-section {
      margin-bottom: 28px; background: var(--color-surface); border: 1px solid var(--color-border);
      border-radius: var(--radius-lg); padding: 20px 24px; box-shadow: var(--shadow-column);
    }
    .planview-section-title {
      font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px;
      color: var(--color-text-secondary); margin-bottom: 16px;
    }
    /* Badge Wall */
    .badge-wall { display: flex; flex-wrap: wrap; gap: 10px; }
    .tech-badge {
      display: inline-flex; flex-direction: column; padding: 10px 16px;
      background: var(--color-surface-elevated); border: 1px solid var(--color-border-subtle);
      border-radius: var(--radius-md); transition: all var(--transition-fast);
      position: relative; cursor: default; box-shadow: 0 1px 3px rgba(0,0,0,0.15);
      border-left: 3px solid var(--color-accent);
    }
    .tech-badge:hover { background: var(--color-surface-hover); border-color: var(--color-accent); box-shadow: var(--shadow-card); transform: translateY(-1px); }
    .tech-badge-label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--color-text-muted); margin-bottom: 3px; }
    .tech-badge-value { font-size: 13px; font-weight: 600; color: var(--color-text); }
    .tech-badge-tooltip {
      display: none; position: absolute; bottom: calc(100% + 8px); left: 50%; transform: translateX(-50%);
      background: var(--color-surface-elevated); border: 1px solid var(--color-border);
      border-radius: var(--radius-sm); padding: 10px 14px; font-size: 12px; color: var(--color-text-secondary);
      max-width: 300px; white-space: normal; z-index: 50; box-shadow: var(--shadow-card-hover);
      pointer-events: none; line-height: 1.5;
    }
    .tech-badge:hover .tech-badge-tooltip { display: block; }
    /* Tessl Tiles Panel */
    .tessl-tiles { display: flex; flex-wrap: wrap; gap: 12px; }
    .tessl-tile-card {
      display: flex; flex-direction: column; padding: 14px 18px;
      background: var(--color-surface-elevated); border: 1px solid var(--color-border-subtle);
      border-radius: var(--radius-md); min-width: 200px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.15);
      transition: all var(--transition-fast);
    }
    .tessl-tile-card:hover { box-shadow: var(--shadow-card); transform: translateY(-1px); }
    .tessl-tile-card.score-green { border-left: 3px solid var(--color-done); }
    .tessl-tile-card.score-yellow { border-left: 3px solid var(--color-inprogress); }
    .tessl-tile-card.score-red { border-left: 3px solid var(--color-p1); }
    .tessl-tile-card.score-none { border-left: 3px solid var(--color-border); }
    .tessl-tile-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
    .tessl-tile-name { font-size: 13px; font-weight: 600; color: var(--color-text); font-family: var(--font-mono); }
    .tessl-tile-version { font-size: 11px; color: var(--color-text-muted); margin-top: 4px; }
    .tessl-tile-scores { display: flex; gap: 6px; align-items: center; margin-top: 8px; }
    .tessl-score-badge {
      display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px;
      border-radius: 10px; font-size: 11px; font-weight: 600;
    }
    .tessl-score-badge.green { background: rgba(39, 201, 63, 0.15); color: var(--color-done); }
    .tessl-score-badge.yellow { background: rgba(245, 166, 35, 0.15); color: var(--color-inprogress); }
    .tessl-score-badge.red { background: rgba(255, 71, 87, 0.15); color: var(--color-p1); }
    .tessl-score-badge.muted { background: rgba(107, 113, 137, 0.1); color: var(--color-text-muted); }
    .tessl-score-label { font-size: 9px; text-transform: uppercase; letter-spacing: 0.3px; }
    .tessl-tile-eval { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--color-border-subtle); }
    .tessl-eval-bar { height: 4px; background: var(--color-border); border-radius: 2px; margin-top: 6px; overflow: hidden; }
    .tessl-eval-bar-fill { height: 100%; border-radius: 2px; }
    .tessl-eval-bar-fill.green { background: var(--color-done); }
    .tessl-eval-bar-fill.yellow { background: var(--color-inprogress); }
    .tessl-eval-bar-fill.red { background: var(--color-p1); }
    /* File Structure Tree — VS Code style */
    .file-tree { font-family: var(--font-mono); font-size: 13px; }
    .file-tree-entry {
      display: flex; align-items: center; height: 26px; padding: 0 8px;
      border-radius: var(--radius-sm); transition: background var(--transition-fast);
      cursor: default; gap: 0; white-space: nowrap;
    }
    .file-tree-entry:hover { background: var(--color-surface-hover); }
    .file-tree-indent { display: inline-flex; flex-shrink: 0; }
    .file-tree-guide {
      width: 18px; height: 26px; position: relative; flex-shrink: 0;
    }
    .file-tree-guide::before {
      content: ''; position: absolute; left: 8px; top: 0; bottom: 0;
      width: 1px; background: var(--color-border-subtle);
    }
    .file-tree-chevron {
      width: 18px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
      flex-shrink: 0; cursor: pointer; color: var(--color-text-muted);
      transition: color var(--transition-fast); font-size: 11px; user-select: none;
    }
    .file-tree-chevron:hover { color: var(--color-accent); }
    .file-tree-chevron-spacer { width: 18px; flex-shrink: 0; }
    .file-tree-file-icon {
      width: 18px; height: 26px; display: inline-flex; align-items: center; justify-content: center;
      flex-shrink: 0; font-size: 14px;
    }
    .file-tree-file-icon.dir { color: var(--color-accent); }
    .file-tree-file-icon.file { color: var(--color-text-muted); }
    .file-tree-file-icon.file-js { color: #f0db4f; }
    .file-tree-file-icon.file-json { color: #f0db4f; }
    .file-tree-file-icon.file-md { color: var(--color-accent); }
    .file-tree-file-icon.file-html { color: #e44d26; }
    .file-tree-file-icon.file-css { color: #264de4; }
    .file-tree-label {
      display: flex; align-items: center; gap: 6px; flex-shrink: 0;
    }
    .file-tree-name { color: var(--color-text); white-space: nowrap; flex-shrink: 0; }
    .file-tree-name.planned { color: var(--color-text-muted); }
    .file-tree-status {
      flex-shrink: 0; font-size: 10px; padding: 1px 6px; border-radius: 3px; font-weight: 600;
    }
    .file-tree-status.existing { color: var(--color-done); background: rgba(39,201,63,0.1); }
    .file-tree-status.planned-tag { color: var(--color-text-muted); background: var(--color-surface-elevated); }
    .file-tree-comment {
      color: var(--color-text-muted); margin-left: auto; padding-left: 16px;
      font-size: 12px; opacity: 0.5; overflow: hidden; text-overflow: ellipsis;
      white-space: nowrap; min-width: 0; flex: 1 1 0;
    }
    .file-tree-comment.truncated { cursor: help !important; }
    .file-tree-children { overflow: hidden; }
    .file-tree-children.collapsed { display: none; }
    /* Architecture Diagram */
    .diagram-container { position: relative; }
    .diagram-svg {
      width: 100%; background: var(--color-bg); border: 1px solid var(--color-border);
      border-radius: var(--radius-md); padding: 8px;
    }
    .diagram-node { cursor: pointer; transition: all var(--transition-fast); }
    .diagram-node:hover { filter: brightness(1.2); }
    .diagram-node-rect { rx: 10; ry: 10; stroke-width: 2; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); }
    .diagram-node-label { font-family: var(--font-sans); font-size: 13px; font-weight: 600; fill: var(--color-text); }
    .diagram-edge-line { stroke: var(--color-border); stroke-width: 2; fill: none; stroke-dasharray: 6 3; }
    .diagram-edge-label {
      font-family: var(--font-mono); font-size: 10px; fill: var(--color-text-muted);
      paint-order: stroke; stroke: var(--color-bg); stroke-width: 4px;
    }
    .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; }
    /* Diagram legend */
    .diagram-legend { display: flex; gap: 16px; margin-top: 12px; justify-content: center; }
    .diagram-legend-item { display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--color-text-muted); }
    .diagram-legend-dot { width: 10px; height: 10px; border-radius: 3px; }
    /* Plan empty states */
    .planview-empty { text-align: center; padding: 48px 20px; color: var(--color-text-muted); }
    .planview-empty-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: var(--color-text-secondary); }
    .planview-empty-text { font-size: 14px; line-height: 1.6; }

    /* ====== Scrollbar ====== */
    ::-webkit-scrollbar { width: 6px; height: 6px; }
    ::-webkit-scrollbar-track { background: transparent; }
    ::-webkit-scrollbar-thumb { background: var(--color-border); border-radius: 3px; }
    ::-webkit-scrollbar-thumb:hover { background: var(--color-text-muted); }

    /* ====== Analyze View ====== */
    .analyze-view { padding: 24px; display: flex; flex-direction: column; gap: 24px; }

    .analyze-empty {
      display: flex; flex-direction: column; align-items: center; justify-content: center;
      padding: 80px 40px; text-align: center;
    }
    .analyze-empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.4; }
    .analyze-empty-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; }
    .analyze-empty-text { color: var(--color-text-secondary); max-width: 420px; }
    .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; }

    .analyze-section {
      background: var(--color-surface); border: 1px solid var(--color-border);
      border-radius: var(--radius-lg); overflow: hidden;
    }
    .analyze-section-header {
      padding: 16px 20px; font-size: 14px; font-weight: 600; text-transform: uppercase;
      letter-spacing: 0.05em; color: var(--color-text-secondary);
      border-bottom: 1px solid var(--color-border);
    }

    /* Health Gauge */
    .gauge-container {
      display: flex; align-items: center; justify-content: center;
      padding: 32px 20px; gap: 32px; flex-wrap: wrap;
    }
    .gauge-svg { width: 200px; height: 120px; }
    .gauge-arc { fill: none; stroke-width: 18; stroke-linecap: round; }
    .gauge-zone-red { stroke: #ff4757; }
    .gauge-zone-yellow { stroke: #ffa502; }
    .gauge-zone-green { stroke: #27c93f; }
    .gauge-needle {
      fill: var(--color-text); transition: transform 1s ease-out;
    }
    @media (prefers-reduced-motion: reduce) {
      .gauge-needle { transition: none; }
    }
    .gauge-score {
      font-size: 36px; font-weight: 700; text-anchor: middle;
      fill: var(--color-text);
    }
    .gauge-label {
      font-size: 12px; text-anchor: middle; fill: var(--color-text-secondary);
    }
    .gauge-breakdown {
      display: flex; flex-direction: column; gap: 8px; min-width: 200px;
    }
    .gauge-factor {
      display: flex; justify-content: space-between; align-items: center;
      font-size: 13px; padding: 6px 12px;
      background: var(--color-surface-elevated); border-radius: var(--radius-sm);
    }
    .gauge-factor-label { color: var(--color-text-secondary); display: flex; flex-direction: column; gap: 2px; }
    .gauge-factor-value { font-weight: 600; font-family: var(--font-mono); white-space: nowrap; }
    .gauge-factor-context { font-size: 11px; color: var(--color-text-muted); font-weight: 400; }
    .gauge-factor-green { border-left: 3px solid #27c93f; }
    .gauge-factor-yellow { border-left: 3px solid #ffa502; }
    .gauge-factor-red { border-left: 3px solid #ff4757; }
    .gauge-trend {
      display: inline-flex; align-items: center; gap: 4px;
      font-size: 14px; font-weight: 600; margin-left: 8px;
    }
    .gauge-trend-up { color: #27c93f; }
    .gauge-trend-down { color: #ff4757; }
    .gauge-na { font-size: 24px; color: var(--color-text-muted); text-align: center; padding: 40px; }

    /* Heatmap */
    .heatmap-wrapper { overflow-x: auto; padding: 16px 20px; }
    .heatmap-table {
      width: 100%; border-collapse: collapse; font-size: 13px;
    }
    .heatmap-table th {
      padding: 10px 16px; font-weight: 600; text-transform: uppercase;
      letter-spacing: 0.05em; font-size: 11px; color: var(--color-text-secondary);
      border-bottom: 2px solid var(--color-border); text-align: center;
    }
    .heatmap-table th[scope="row"] {
      text-align: left; font-size: 13px; text-transform: none; letter-spacing: normal;
      color: var(--color-text); max-width: 300px; white-space: nowrap;
      overflow: hidden; text-overflow: ellipsis;
    }
    .heatmap-table td {
      padding: 8px 16px; text-align: center; border-bottom: 1px solid var(--color-border-subtle);
    }
    .heatmap-cell {
      display: inline-block; width: 28px; height: 28px; border-radius: 6px;
      cursor: pointer; transition: transform var(--transition-fast);
    }
    .heatmap-cell:hover { transform: scale(1.15); }
    .heatmap-cell:focus { outline: 2px solid var(--color-accent); outline-offset: 2px; }
    .coverage-covered { background: #27c93f; }
    .coverage-partial { background: #ffa502; }
    .coverage-missing { background: #ff4757; }
    .coverage-na { background: var(--color-text-muted); opacity: 0.4; }
    .heatmap-refs {
      display: none; font-size: 11px; color: var(--color-text-secondary);
      padding: 4px 0; font-family: var(--font-mono);
    }
    .heatmap-refs.expanded { display: block; }
    .heatmap-row { animation: fadeIn 0.3s ease forwards; opacity: 0; }
    @keyframes fadeIn { to { opacity: 1; } }
    @media (prefers-reduced-motion: reduce) {
      .heatmap-row { animation: none; opacity: 1; }
    }
    .heatmap-empty {
      padding: 40px; text-align: center; color: var(--color-text-muted); font-size: 14px;
    }
    .heatmap-desc {
      font-size: 12px; color: var(--color-text-secondary); max-width: 400px;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    .heatmap-refs-inline { display: none; }
    .heatmap-scroll { max-height: 500px; overflow-y: auto; }
    .coverage-summary {
      display: flex; gap: 16px; margin-bottom: 20px;
    }
    .cov-stat { flex: 1; }
    .cov-stat-header {
      display: flex; justify-content: space-between; align-items: baseline;
      margin-bottom: 6px; font-size: 13px;
    }
    .cov-stat-label { font-weight: 600; color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.05em; font-size: 11px; }
    .cov-stat-value { font-weight: 700; font-family: var(--font-mono); font-size: 14px; }
    .cov-bar {
      height: 6px; background: var(--color-surface-elevated); border-radius: 3px; overflow: hidden;
    }
    .cov-bar-fill { height: 100%; border-radius: 3px; transition: width 0.8s ease-out; }
    .cov-bar-green { background: #27c93f; }
    .cov-bar-yellow { background: #ffa502; }
    .cov-bar-red { background: #ff4757; }
    .coverage-success {
      text-align: center; padding: 24px; color: #27c93f; font-weight: 500; font-size: 14px;
    }
    .coverage-gaps-label {
      font-size: 13px; font-weight: 600; color: var(--color-text-secondary);
      margin-bottom: 12px;
    }

    /* Severity Table */
    .severity-wrapper { padding: 0; }
    .severity-controls {
      display: flex; gap: 12px; padding: 12px 20px; align-items: center;
      border-bottom: 1px solid var(--color-border-subtle);
    }
    .severity-filter {
      background: var(--color-surface-elevated); color: var(--color-text);
      border: 1px solid var(--color-border); border-radius: var(--radius-sm);
      padding: 6px 12px; font-size: 13px; font-family: var(--font-sans);
      cursor: pointer;
    }
    .severity-table {
      width: 100%; border-collapse: collapse; font-size: 13px;
    }
    .severity-table th {
      padding: 10px 16px; font-weight: 600; text-align: left;
      border-bottom: 2px solid var(--color-border); cursor: pointer;
      user-select: none; font-size: 12px; text-transform: uppercase;
      letter-spacing: 0.05em; color: var(--color-text-secondary);
    }
    .severity-table th:hover { color: var(--color-text); }
    .severity-table th:focus { outline: 2px solid var(--color-accent); outline-offset: -2px; }
    .severity-table td {
      padding: 10px 16px; border-bottom: 1px solid var(--color-border-subtle);
      vertical-align: top;
    }
    .severity-table tr[data-severity] { cursor: pointer; transition: background var(--transition-fast); }
    .severity-table tr[data-severity]:hover { background: var(--color-surface-hover); }
    .severity-table tr[data-severity]:focus { outline: 2px solid var(--color-accent); outline-offset: -2px; }
    .severity-badge {
      display: inline-block; padding: 2px 10px; border-radius: 12px;
      font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em;
    }
    .severity-critical { background: rgba(255,71,87,0.15); color: #ff4757; }
    .severity-high { background: rgba(255,165,2,0.15); color: #ffa502; }
    .severity-medium { background: rgba(255,193,7,0.15); color: #e0a800; }
    .severity-low { background: rgba(52,152,219,0.15); color: #3498db; }
    .severity-recommendation {
      display: none; padding: 12px 16px; background: var(--color-surface-elevated);
      font-size: 12px; color: var(--color-text-secondary); border-bottom: 1px solid var(--color-border-subtle);
    }
    .severity-recommendation.expanded { display: table-row; }
    .severity-resolved td { text-decoration: line-through; opacity: 0.5; }
    .severity-empty {
      padding: 40px; text-align: center; color: #27c93f; font-size: 14px; font-weight: 500;
    }
    .severity-table-scroll { max-height: 500px; overflow-y: auto; }

    /* Animations */
    .analyze-section.slide-in {
      animation: slideInUp 0.4s var(--transition-slow) forwards; opacity: 0;
      transform: translateY(16px);
    }
    @keyframes slideInUp {
      to { opacity: 1; transform: translateY(0); }
    }
    @media (prefers-reduced-motion: reduce) {
      .analyze-section.slide-in { animation: none; opacity: 1; transform: none; }
    }

    /* Toast notification */
    .toast {
      position: fixed;
      bottom: 24px;
      left: 50%;
      transform: translateX(-50%) translateY(20px);
      background: var(--color-surface);
      color: var(--color-text);
      border: 1px solid var(--color-border);
      border-radius: 8px;
      padding: 10px 20px;
      font-size: 13px;
      box-shadow: var(--shadow-card-hover);
      z-index: 200;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.25s ease, transform 0.25s ease;
    }
    .toast.visible {
      opacity: 1;
      transform: translateX(-50%) translateY(0);
      pointer-events: auto;
    }

  </style>
</head>
<body>
  <div id="toast" class="toast"></div>
  <!-- Header -->
  <header class="header" role="banner">
    <div class="header-left">
      <div class="logo">
        <div class="logo-icon" aria-hidden="true">D</div>
        <span>IIKit Dashboard</span>
      </div>
      <div class="project-label" id="projectLabel" title=""></div>
      <div class="feature-selector" role="navigation" aria-label="Feature selector">
        <select id="featureSelect" aria-label="Select feature to display" tabindex="0">
          <option value="">Loading features...</option>
        </select>
      </div>
    </div>
    <div class="header-right">
      <div id="integrityBadge" class="integrity-badge missing" role="status" aria-label="Test integrity status">
        <span class="integrity-dot" aria-hidden="true"></span>
        <span class="integrity-text">Checking...</span>
      </div>
      <button id="themeToggle" class="theme-toggle" aria-label="Toggle light/dark theme" title="Toggle theme" tabindex="0">
        <span id="themeIcon">&#9790;</span>
      </button>
      <div id="activityIndicator" role="status" aria-label="Agent activity: idle" title="Agent idle — no recent file changes" style="display:none"></div>
    </div>
  </header>

  <!-- Pipeline Bar -->
  <nav id="pipelineBar" class="pipeline-bar" role="navigation" aria-label="IIKit workflow pipeline">
  </nav>

  <!-- Content Area -->
  <main class="content-area" role="main">
    <div id="contentArea">
      <div class="board-container">
        <div id="board" class="board" role="region" aria-label="Dashboard board">
          <div class="loading" id="loadingState">
            <div class="loading-spinner" aria-label="Loading board data"></div>
          </div>
        </div>
      </div>
    </div>
  </main>

  <button class="clarify-fab" id="clarifyFab" title="Clarifications">?0</button>
  <div class="clarify-slide-panel" id="clarifyPanel"></div>

  <script>
    (function() {
      'use strict';
      // Expose switchTab on window for test automation (clarify utility has no pipeline node)
      const _exposeForTests = () => { window._switchTab = (id) => switchTab(id); };
      setTimeout(_exposeForTests, 0);

      // ====== State ======
      let currentFeature = null;
      let currentBoard = null;
      let currentPipeline = null;
      let currentChecklist = null;
      let expandedChecklist = null;
      let currentTestify = null;
      let currentAnalyze = null;
      let currentBugs = null;
      let activeTab = null;
      let externalSankeyHighlight = null;
      // DASHBOARD_DATA mode: when data is inlined by the generator
      const staticMode = typeof window.DASHBOARD_DATA === 'object' && window.DASHBOARD_DATA !== null;
      let previousCardColumns = {}; // Track card positions for animations

      // ====== DOM References ======
      const boardEl = document.getElementById('board');
      const featureSelect = document.getElementById('featureSelect');
      const integrityBadge = document.getElementById('integrityBadge');
      const activityIndicator = document.getElementById('activityIndicator');
      const loadingState = document.getElementById('loadingState');
      const pipelineBar = document.getElementById('pipelineBar');
      const contentArea = document.getElementById('contentArea');

      // ====== Pipeline ======
      const PHASE_ICONS = {
        not_started: '',
        in_progress: '&#9654;',
        complete: '&#10003;',
        skipped: '&#8212;',
        available: '&#9679;'
      };

      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>';

      function getBugsBadgeHtml(bugsData) {
        if (!bugsData || !bugsData.exists) return '';
        const count = bugsData.summary.open;
        const sev = bugsData.summary.highestOpenSeverity;

        const colors = { critical: '#ff4757', high: '#ffa502', medium: '#f1c40f', low: '#6b7189' };
        const textColors = { critical: '#fff', high: '#fff', medium: '#1a1a2e', low: '#fff' };

        if (count === 0) {
          return ' <span class="bug-count-badge muted">0</span>';
        }

        const bg = colors[sev] || '#6b7189';
        const fg = textColors[sev] || '#fff';
        return ` <span class="bug-count-badge" style="background:${bg};color:${fg}">${count}</span>`;
      }

      function renderPipeline(pipeline) {
        if (!pipeline || !pipeline.phases) return;
        currentPipeline = pipeline;

        pipelineBar.innerHTML = '';

        pipeline.phases.forEach((phase, i) => {
          // Add connector before each node (except the first)
          if (i > 0) {
            const connector = document.createElement('div');
            connector.className = 'pipeline-connector';
            // Color connector green if previous phase is complete
            const prevPhase = pipeline.phases[i - 1];
            if (prevPhase.status === 'complete') {
              connector.classList.add('complete');
            }
            pipelineBar.appendChild(connector);
          }

          const node = document.createElement('button');
          node.className = 'pipeline-node' + (phase.optional ? ' optional' : '');
          if (activeTab === phase.id) node.classList.add('active');
          node.setAttribute('tabindex', '0');
          if (activeTab === phase.id) node.setAttribute('aria-current', 'true');

          node.innerHTML = `
            <div class="pipeline-dot ${phase.status}">${PHASE_ICONS[phase.status] || ''}</div>
            <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>
            <span class="pipeline-progress">${phase.progress || ''}</span>
            ${phase.clarifications > 0 ? '<span class="pipeline-clarify-badge" title="' + phase.clarifications + ' Q&amp;A item' + (phase.clarifications > 1 ? 's' : '') + '">Q&amp;A</span>' : ''}
          `;

          node.addEventListener('click', () => switchTab(phase.id));
          // Make clarify badge clickable — opens slide-out panel for this phase
          const badge = node.querySelector('.pipeline-clarify-badge');
          if (badge) {
            badge.style.cursor = 'pointer';
            badge.addEventListener('click', (e) => {
              e.stopPropagation();
              toggleClarifyPanel(phase.id);
            });
          }
          pipelineBar.appendChild(node);
        });

        // Bugs tab — standalone, no connector
        const bugsGap = document.createElement('div');
        bugsGap.className = 'pipeline-bugs-gap';
        pipelineBar.appendChild(bugsGap);

        const bugsTab = document.createElement('button');
        bugsTab.className = 'bugs-tab' + (activeTab === 'bugs' ? ' active' : '');
        bugsTab.setAttribute('data-view', 'bugs');
        bugsTab.setAttribute('tabindex', '0');
        if (activeTab === 'bugs') bugsTab.setAttribute('aria-current', 'true');

        const badgeHtml = currentBugs ? getBugsBadgeHtml(currentBugs) : '';
        bugsTab.innerHTML = `
          <div class="pipeline-dot bugs-dot">${BUG_ICON_SMALL}</div>
          <span class="pipeline-label">Bugs${badgeHtml}</span>
        `;
        if (currentBugs && !currentBugs.exists) {
          bugsTab.classList.add('muted');
        }
        bugsTab.addEventListener('click', () => switchTab('bugs'));
        pipelineBar.appendChild(bugsTab);
      }

      function switchTab(phaseId) {
        if (phaseId === 'tasks') {
          // Tasks tab redirects to Implement (kanban board) since tasks are shown there
          activeTab = 'implement';
          if (currentPipeline) renderPipeline(currentPipeline);
          renderBoardView();
          showToast('Tasks are managed on the Implement board');
          return;
        }

        activeTab = phaseId;
        // Re-render pipeline to update active state
        if (currentPipeline) renderPipeline(currentPipeline);

        if (phaseId === 'implement') {
          renderBoardView();
        } else if (phaseId === 'constitution') {
          renderConstitutionView();
        } else if (phaseId === 'spec') {
          renderStoryMapView();
        } else if (phaseId === 'plan') {
          renderPlanView();
        } else if (phaseId === 'clarify') {
          renderClarifyView();
        } else if (phaseId === 'checklist') {
          renderChecklistView();
        } else if (phaseId === 'testify') {
          renderTestifyView();
        } else if (phaseId === 'analyze') {
          renderAnalyzeView();
        } else if (phaseId === 'bugs') {
          renderBugsView();
        } else {
          renderPlaceholderView(phaseId);
        }
      }

      // ====== Cross-Panel Navigation ======
      const CROSS_LINK_MOD_KEY = /Mac|iPhone|iPad/.test(navigator.platform || '') ? '\u2318' : 'Ctrl';

      async function navigateToPanel(panelId, entityId) {
        switchTab(panelId);

        // Sentinel elements per panel — poll until the panel has rendered
        const sentinels = {
          spec: '.graph-node', testify: '.sankey-node', implement: '.card',
          checklist: '.checklist-item', clarify: '.clarify-entry', analyze: '.heatmap-row',
          bugs: '.bug-row'
        };
        const sel = sentinels[panelId];
        if (sel) {
          for (let i = 0; i < 30; i++) {
            if (contentArea.querySelector(sel)) break;
            await new Promise(r => setTimeout(r, 50));
          }
        }

        // Dispatch to panel-specific highlight
        const highlighters = {
          spec: highlightSpecEntity,
          testify: highlightTestifyEntity,
          implement: highlightBoardEntity,
          checklist: highlightChecklistEntity,
          clarify: highlightClarifyEntity,
          bugs: highlightBugsEntity
        };
        const fn = highlighters[panelId];
        if (fn) fn(entityId);
      }

      // Delegated Cmd/Ctrl+click handler for all cross-links
      contentArea.addEventListener('click', (e) => {
        if (!(e.metaKey || e.ctrlKey)) return;
        const link = e.target.closest('[data-cross-target]');
        if (link) {
          e.preventDefault();
          e.stopPropagation();
          navigateToPanel(link.dataset.crossTarget, link.dataset.crossId);
        }
      });

      // ====== Panel-Specific Highlight Functions ======

      function highlightSpecEntity(entityId) {
        // Only normalize US refs (US-2 → US2); FR/SC keep their hyphens (SC-007 stays SC-007)
        const nodeId = entityId.replace(/^US-/i, 'US');
        highlightGraphNode(nodeId);

        // Scroll to story card (for US refs) or graph node (for FR/SC)
        const card = contentArea.querySelector(`.story-card[data-story-id="${nodeId}"]`);
        if (card) {
          card.scrollIntoView({ behavior: 'smooth', block: 'center' });
          card.classList.add('highlighted');
          setTimeout(() => card.classList.remove('highlighted'), 2000);
          const story = currentStoryMap?.stories?.find(s => s.id === nodeId);
          if (story) showDetailPanel(nodeId, 'us', story.title, story.body || '');
          return;
        }
        const nodeEl = contentArea.querySelector(`.graph-node[data-id="${nodeId}"]`);
        if (nodeEl) {
          nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
          const type = nodeId.startsWith('FR') ? 'fr' : 'sc';
          showDetailPanel(nodeId, type, nodeId, nodeEl.dataset.desc || '');
        }
      }

      function highlightTestifyEntity(entityId) {
        if (externalSankeyHighlight) {
          externalSankeyHighlight(entityId);
        }
        const nodeEl = contentArea.querySelector(`.sankey-node[data-node-id="${entityId}"]`);
        if (nodeEl) {
          nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
        }
      }

      function highlightBoardEntity(entityId) {
        // Task IDs (T prefix): find .task-id span by text, expand list, highlight
        if (/^T\d+$/i.test(entityId)) {
          const taskIds = contentArea.querySelectorAll('.task-id');
          for (const span of taskIds) {
            if (span.textContent.trim() === entityId) {
              const taskItem = span.closest('.task-item');
              const taskList = span.closest('.task-list');
              // Expand collapsed task list
              if (taskList && taskList.classList.contains('collapsed')) {
                const btn = taskList.previousElementSibling;
                if (btn && btn.classList.contains('task-toggle')) {
                  btn.click();
                }
              }
              if (taskItem) {
                taskItem.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                taskItem.classList.add('highlighted');
                setTimeout(() => taskItem.classList.remove('highlighted'), 2000);
              }
              return;
            }
          }
        }

        // Story IDs (US prefix): find .card[data-card-id]
        const card = contentArea.querySelector(`.card[data-card-id="${entityId}"]`);
        if (card) {
          card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
          card.classList.add('highlighted');
          setTimeout(() => card.classList.remove('highlighted'), 2000);
        }
      }

      function highlightChecklistEntity(entityId) {
        // Search tags and inline cross-link spans for the entity ID
        const matches = contentArea.querySelectorAll(`.checklist-item-tag, .checklist-item .cross-link[data-cross-id="${entityId}"]`);
        for (const el of matches) {
          if (el.dataset.crossId === entityId || el.textContent.trim() === entityId) {
            const item = el.closest('.checklist-item');
            // Expand parent section if collapsed
            const detail = el.closest('.checklist-detail');
            if (detail && !detail.classList.contains('open')) {
              const filename = detail.id?.replace('checklist-detail-', '');
              if (filename && currentChecklist) {
                toggleChecklistExpand(filename, currentChecklist);
                // Re-query after re-render
                setTimeout(() => highlightChecklistEntity(entityId), 100);
                return;
              }
            }
            if (item) {
              item.scrollIntoView({ behavior: 'smooth', block: 'center' });
              item.classList.add('highlighted');
              setTimeout(() => item.classList.remove('highlighted'), 2000);
            }
            return;
          }
        }
      }

      function highlightClarifyEntity(entityId) {
        const ref = contentArea.querySelector(`.clarify-ref[data-ref-id="${entityId}"]`);
        if (ref) {
          const entry = ref.closest('.clarify-entry');
          if (entry) {
            entry.scrollIntoView({ behavior: 'smooth', block: 'center' });
            entry.classList.add('highlighted');
            setTimeout(() => entry.classList.remove('highlighted'), 2000);
          }
        }
      }

      function renderBoardView() {
        contentArea.innerHTML = `
          <div class="board-container">
            <div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>
            <div id="board" class="board" role="region" aria-label="Dashboard board"></div>
          </div>`;
        // Re-assign boardEl reference
        const newBoardEl = document.getElementById('board');
        if (currentBoard) {
          renderBoardInto(newBoardEl, currentBoard);
        }
      }

      // ====== Bugs View (T014, T015, T016) ======

      async function renderBugsView() {
        if (!currentFeature) return;

        contentArea.innerHTML = '<div class="loading">Loading bug data...</div>';
        try {
          let data;
          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {
            data = window.DASHBOARD_DATA.featureData[currentFeature].bugs;
          } else {
            const res = await fetch(`/api/bugs/${currentFeature}`);
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            data = await res.json();
          }
          currentBugs = data;
          if (currentPipeline) renderPipeline(currentPipeline);
          renderBugsContent(data);
        } catch (err) {
          contentArea.innerHTML = `<div class="error-message">Error loading bugs: ${escapeHtml(err.message)}</div>`;
        }
      }

      function renderBugsContent(data) {
        if (!data || !data.exists || data.bugs.length === 0) {
          // T015: Empty state
          const msg = !data || !data.exists
            ? 'No bugs have been reported for this feature.'
            : 'All clear — no bug entries found.';
          contentArea.innerHTML = `
            <div class="bugs-empty">
              <div class="bugs-empty-icon">&#10003;</div>
              <div class="bugs-empty-title">No Bugs</div>
              <div class="bugs-empty-text">${msg}</div>
            </div>`;
          return;
        }

        const { bugs, orphanedTasks, summary, repoUrl } = data;

        let tableRows = '';
        for (const bug of bugs) {
          // Fix task progress
          const progressText = bug.fixTasks.total > 0
            ? `${bug.fixTasks.checked}/${bug.fixTasks.total}`
            : '\u2014';

          // GitHub link (T016)
          let githubHtml = '\u2014';
          if (bug.githubIssue) {
            const resolvedUrl = resolveGitHubUrl(bug.githubIssue, repoUrl);
            if (resolvedUrl) {
              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>`;
            } else {
              githubHtml = escapeHtml(bug.githubIssue);
            }
          }

          // Fix task list for cross-links
          let taskLinks = '';
          if (bug.fixTasks.total > 0) {
            taskLinks = bug.fixTasks.tasks.map(t =>
              `<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>`
            ).join(', ');
          }

          // Make FR/SC/TS/T refs in description clickable
          const descHtml = escapeHtml(bug.description || '\u2014')
            .replace(/\b(FR-\d+|SC-\d+)\b/g, '<span class="cross-link" data-cross-target="spec" data-cross-id="$1" style="cursor:pointer;color:var(--color-accent)">$1</span>')
            .replace(/\b(TS-\d+)\b/g, '<span class="cross-link" data-cross-target="testify" data-cross-id="$1" style="cursor:pointer;color:var(--color-accent)">$1</span>')
            .replace(/\b(T\d{3,})\b/g, '<span class="cross-link" data-cross-target="implement" data-cross-id="$1" style="cursor:pointer;color:var(--color-accent)">$1</span>');

          tableRows += `
            <tr class="bug-row" data-bug-id="${bug.id}">
              <td class="bug-id">${bug.id}</td>
              <td><span class="severity-badge severity-${bug.severity}">${bug.severity}</span></td>
              <td class="bug-status ${bug.status === 'fixed' ? 'fixed' : ''}">${bug.status}</td>
              <td class="bug-progress">${progressText}${taskLinks ? '<br>' + taskLinks : ''}</td>
              <td class="bug-description" title="${escapeHtml(bug.description || '')}">${descHtml}</td>
              <td>${githubHtml}</td>
            </tr>`;
        }

        // Orphaned tasks section (TS-020)
        let orphanedHtml = '';
        if (orphanedTasks && orphanedTasks.length > 0) {
          orphanedHtml = `
            <div class="orphaned-section">
              <div class="orphaned-title">&#9888; Orphaned Fix Tasks (no matching bug in bugs.md)</div>
              ${orphanedTasks.map(t => `
                <div class="orphaned-task">
                  <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>
                  [${t.bugTag}] ${escapeHtml(t.description)}
                </div>
              `).join('')}
            </div>`;
        }

        contentArea.innerHTML = `
          <div class="bugs-view">
            <div class="bugs-view-header">
              <div class="bugs-view-title">Bug Tracking</div>
              <div class="bugs-view-subtitle">${summary.total} total &middot; ${summary.open} open &middot; ${summary.fixed} fixed</div>
            </div>
            <div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click task IDs to navigate to the Implement board</div>
            <table class="bugs-table" role="table" aria-label="Bug status table">
              <thead>
                <tr>
                  <th>Bug ID</th>
                  <th aria-sort="descending">Severity</th>
                  <th>Status</th>
                  <th>Fix Tasks</th>
                  <th>Description</th>
                  <th>GitHub</th>
                </tr>
              </thead>
              <tbody>${tableRows}</tbody>
            </table>
            ${orphanedHtml}
          </div>`;
      }

      // T016: Client-side GitHub URL resolution
      function resolveGitHubUrl(issueRef, repoUrl) {
        if (!issueRef || !repoUrl) return null;
        const match = issueRef.match(/#(\d+)/);
        if (!match) return null;
        let base = repoUrl.replace(/\.git$/, '');
        const ssh = base.match(/^git@([^:]+):(.+)$/);
        if (ssh) base = 'https://' + ssh[1] + '/' + ssh[2];
        return base + '/issues/' + match[1];
      }

      // T021: Highlight a bug row in the Bugs tab
      function highlightBugsEntity(entityId) {
        const row = contentArea.querySelector(`tr[data-bug-id="${entityId}"]`);
        if (row) {
          row.scrollIntoView({ behavior: 'smooth', block: 'center' });
          row.classList.add('highlighted');
          setTimeout(() => row.classList.remove('highlighted'), 2000);
        }
      }

      function renderPlaceholderView(phaseId) {
        const phaseNames = {
          constitution: 'Constitution', spec: 'Specification',
          plan: 'Plan', checklist: 'Checklist', testify: 'Test Specs',
          tasks: 'Tasks', analyze: 'Analysis', implement: 'Implementation'
        };
        const name = phaseNames[phaseId] || phaseId;
        contentArea.innerHTML = `
          <div class="placeholder-view">
            <div class="placeholder-view-title">${name} View</div>
            <div class="placeholder-view-text">This phase visualization is coming soon. It will be available in a future update.</div>
          </div>`;
      }

      // ====== Checklist Quality Gates View ======

      async function renderChecklistView() {
        if (!currentFeature) return;

        try {
          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {
            currentChecklist = window.DASHBOARD_DATA.featureData[currentFeature].checklist;
          } else {
            const res = await fetch(`/api/checklist/${currentFeature}`);
            if (!res.ok) throw new Error('Failed to load');
            currentChecklist = await res.json();
          }
          expandedChecklist = null;
          renderChecklistContent(currentChecklist);
        } catch {
          contentArea.innerHTML = '<div class="checklist-empty"><div class="checklist-empty-title">Failed to load checklist data.</div></div>';
        }
      }

      function renderChecklistContent(data) {
        if (!data) return;

        // Empty state
        if (!data.files || data.files.length === 0) {
          contentArea.innerHTML = `
            <div class="checklist-empty">
              <div class="checklist-empty-icon">&#9744;</div>
              <div class="checklist-empty-title">Quality checklists not generated yet</div>
              <div class="checklist-empty-text">Run <code>/iikit-03-checklist</code> to review requirements for completeness, clarity, and consistency.</div>
            </div>`;
          return;
        }

        const CIRCUMFERENCE = 2 * Math.PI * 42; // radius=42 for viewBox 100x100

        let html = '<div class="checklist-view">';

        // Gate indicator
        html += renderGateIndicator(data.gate);

        // Progress rings
        html += '<div class="checklist-rings">';
        data.files.forEach((file) => {
          const isExpanded = expandedChecklist === file.filename;
          const offset = CIRCUMFERENCE - (file.percentage / 100) * CIRCUMFERENCE;

          html += `<div class="checklist-ring-wrapper${isExpanded ? ' expanded' : ''}"
            tabindex="0" role="button"
            aria-expanded="${isExpanded}"
            aria-controls="checklist-detail-${escapeHtml(file.filename)}"
            aria-label="${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%"
            data-filename="${escapeHtml(file.filename)}">
            <svg class="checklist-ring-svg" viewBox="0 0 100 100" role="img"
              aria-label="${escapeHtml(file.name)}: ${file.checked} of ${file.total} items complete, ${file.percentage}%">
              <circle class="checklist-ring-track" cx="50" cy="50" r="42" />
              <circle class="checklist-ring-fill ${file.color}" cx="50" cy="50" r="42"
                stroke-dasharray="${CIRCUMFERENCE}"
                stroke-dashoffset="${offset}" />
              <text class="checklist-ring-pct" x="50" y="50">${file.percentage}%</text>
            </svg>
            <div class="checklist-ring-name">${escapeHtml(file.name)}</div>
            <div class="checklist-ring-fraction">${file.checked}/${file.total}</div>
          </div>`;
        });
        html += '</div>';

        html += `<div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click any FR/SC tag to navigate to its linked panel</div>`;

        // Accordion detail panels
        data.files.forEach((file) => {
          const isExpanded = expandedChecklist === file.filename;
          html += `<div id="checklist-detail-${escapeHtml(file.filename)}" class="checklist-detail${isExpanded ? ' open' : ''}">`;
          html += '<div class="checklist-detail-inner">';
          html += renderChecklistDetail(file);
          html += '</div></div>';
        });

        html += '</div>';
        contentArea.innerHTML = html;

        // Attach click and keyboard handlers to rings
        document.querySelectorAll('.checklist-ring-wrapper').forEach(wrapper => {
          const filename = wrapper.getAttribute('data-filename');
          wrapper.addEventListener('click', () => toggleChecklistExpand(filename, data));
          wrapper.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
              e.preventDefault();
              toggleChecklistExpand(filename, data);
            }
          });
        });
      }

      function renderGateIndicator(gate) {
        if (!gate) return '';
        const levelClass = gate.level === 'yellow' ? 'blocked-yellow' : (gate.level === 'red' ? 'blocked-red' : 'open');
        return `<div class="gate-indicator" role="status" aria-live="polite">
          <div class="gate-dot ${gate.level}"></div>
          <span class="gate-label ${levelClass}">${escapeHtml(gate.label)}</span>
        </div>`;
      }

      function renderChecklistDetail(file) {
        if (!file || !file.items || file.items.length === 0) {
          return '<div style="color:var(--color-text-muted);font-size:13px;">No items detected in this checklist.</div>';
        }

        let html = '';
        let currentCategory = null;

        file.items.forEach(item => {
          if (item.category !== currentCategory) {
            currentCategory = item.category;
            if (currentCategory) {
              html += `<h4 class="checklist-category">${escapeHtml(currentCategory)}</h4>`;
            }
          }

          const icon = item.checked
            ? '<span class="checklist-item-icon checked">&#10003;</span>'
            : '<span class="checklist-item-icon unchecked">&#9744;</span>';

          const chkId = item.chkId
            ? `<span class="checklist-item-id">${escapeHtml(item.chkId)}</span>`
            : '';

          const tags = (item.tags || []).map(t => {
            const isCrossLink = /^(FR|SC)-?\d+$/i.test(t);
            if (isCrossLink) {
              return `<span class="checklist-item-tag cross-link" data-cross-target="spec" data-cross-id="${escapeHtml(t)}">${escapeHtml(t)}</span>`;
            }
            return `<span class="checklist-item-tag">${escapeHtml(t)}</span>`;
          }).join('');

          const linkedText = escapeHtml(item.text).replace(
            /\b((?:FR|SC)-\d+)\b/g,
            '<span class="cross-link" data-cross-target="spec" data-cross-id="$1">$1</span>'
          );

          html += `<div class="checklist-item">
            ${icon}${chkId}
            <span style="flex:1">${linkedText}</span>
            ${tags}
          </div>`;
        });

        return html;
      }

      function toggleChecklistExpand(filename, data) {
        if (expandedChecklist === filename) {
          expandedChecklist = null;
        } else {
          expandedChecklist = filename;
        }
        renderChecklistContent(data);
      }

      // ====== Testify Traceability View ======

      async function renderTestifyView() {
        if (!currentFeature) return;

        try {
          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {
            currentTestify = window.DASHBOARD_DATA.featureData[currentFeature].testify;
            renderTestifyContent(currentTestify);
            return;
          }
          const res = await fetch(`/api/testify/${currentFeature}`);
          if (!res.ok) throw new Error('Failed to load');
          currentTestify = await res.json();
          renderTestifyContent(currentTestify);
        } catch {
          contentArea.innerHTML = '<div class="testify-empty"><div class="testify-empty-title">Failed to load testify data.</div></div>';
        }
      }

      function renderTestifyContent(data) {
        if (!data) return;

        // Empty state
        if (!data.exists) {
          contentArea.innerHTML = `
            <div class="testify-empty">
              <div class="testify-empty-icon">&#128269;</div>
              <div class="testify-empty-title">No test specifications generated for this feature</div>
              <div class="testify-empty-text">Run <code>/iikit-04-testify</code> to generate test specifications with traceability links to requirements and tasks.</div>
            </div>`;
          return;
        }

        let html = '<div class="testify-view">';

        // Integrity seal
        html += renderIntegritySeal(data.integrity);

        html += `<div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>`;

        // Sankey diagram
        html += renderSankeyDiagram(data);

        html += '</div>';
        contentArea.innerHTML = html;

        // Attach hover and keyboard handlers after DOM insert
        attachSankeyInteractions(data);
      }

      function renderIntegritySeal(integrity) {
        const labels = { valid: 'Verified', tampered: 'Tampered', missing: 'Missing' };
        const label = labels[integrity.status] || 'Unknown';
        const hashDisplay = integrity.currentHash ? integrity.currentHash.substring(0, 12) + '...' : '';

        return `<div class="testify-seal" role="status" aria-live="polite" aria-label="Assertion integrity: ${label}">
          <div class="testify-seal-dot ${escapeHtml(integrity.status)}"></div>
          <span class="testify-seal-label ${escapeHtml(integrity.status)}">${escapeHtml(label)}</span>
          <span style="color: var(--color-text-muted); font-weight: 400;">Assertion Integrity</span>
          ${hashDisplay ? `<span class="testify-seal-hash">${escapeHtml(hashDisplay)}</span>` : ''}
        </div>`;
      }

      function renderSankeyDiagram(data) {
        const { requirements, testSpecs, tasks, edges, gaps, pyramid } = data;

        // Layout constants
        const PAD = 20;
        const COL_LEFT = 40;
        const COL_MID = 440;
        const COL_RIGHT = 840;
        const NODE_W = 180;
        const NODE_H = 32;
        const NODE_GAP = 6;
        const GROUP_PAD = 8;
        const GROUP_GAP = 16;
        const LABEL_Y = 20;
        const COL_LABEL_Y = LABEL_Y;

        // Compute positions for each column
        const reqNodes = requirements.map((r, i) => ({
          ...r, x: COL_LEFT, y: PAD + 30 + i * (NODE_H + NODE_GAP),
          col: 'left', isGap: gaps.untestedRequirements.includes(r.id)
        }));

        // Middle column: group by pyramid type
        const typeOrder = ['acceptance', 'contract', 'validation'];
        const typeLabels = { acceptance: 'Acceptance', contract: 'Contract', validation: 'Validation' };
        const typeColors = { acceptance: '#3B82F6', contract: '#22C55E', validation: '#8B5CF6' };
        const midNodes = [];
        const groupMeta = [];
        let midY = PAD + 30;

        for (const type of typeOrder) {
          const ids = pyramid[type].ids;
          const count = pyramid[type].count;
          const groupStartY = midY;
          const specs = ids.map(id => testSpecs.find(ts => ts.id === id)).filter(Boolean);

          // Cluster width scales with count — narrower at top (acceptance), wider at bottom (validation)
          const widthScale = type === 'acceptance' ? 0.7 : type === 'contract' ? 0.85 : 1.0;
          const effectiveW = Math.round(NODE_W * widthScale);
          const xOffset = COL_MID + Math.round((NODE_W - effectiveW) / 2);

          for (let j = 0; j < specs.length; j++) {
            midNodes.push({
              ...specs[j], x: xOffset, y: midY + GROUP_PAD + j * (NODE_H + NODE_GAP),
              w: effectiveW, col: 'mid', type,
              isGap: gaps.unimplementedTests.includes(specs[j].id)
            });
          }

          const groupH = count > 0
            ? GROUP_PAD * 2 + count * NODE_H + (count - 1) * NODE_GAP
            : GROUP_PAD * 2 + NODE_H; // min height for empty groups

          groupMeta.push({
            type, label: `${typeLabels[type]}: ${count}`,
            x: xOffset - GROUP_PAD, y: groupStartY,
            w: effectiveW + GROUP_PAD * 2, h: groupH,
            color: typeColors[type]
          });

          midY += groupH + GROUP_GAP;
        }

        // Right column: tasks — mark completed tasks from board data
        const checkedTaskIds = new Set();
        if (currentBoard) {
          for (const cards of [currentBoard.todo, currentBoard.in_progress, currentBoard.done]) {
            for (const card of (cards || [])) {
              for (const t of (card.tasks || [])) {
                if (t.checked) checkedTaskIds.add(t.id);
              }
            }
          }
        }
        const taskNodes = tasks.map((t, i) => ({
          ...t, x: COL_RIGHT, y: PAD + 30 + i * (NODE_H + NODE_GAP),
          col: 'right', isGap: false, isChecked: checkedTaskIds.has(t.id)
        }));

        // SVG height
        const maxLeft = reqNodes.length > 0 ? reqNodes[reqNodes.length - 1].y + NODE_H + PAD : 100;
        const maxMid = midY + PAD;
        const maxRight = taskNodes.length > 0 ? taskNodes[taskNodes.length - 1].y + NODE_H + PAD : 100;
        const svgH = Math.max(maxLeft, maxMid, maxRight, 200);
        const svgW = COL_RIGHT + NODE_W + PAD + 40;

        // Build node lookup for edge drawing
        const nodeMap = {};
        for (const n of reqNodes) nodeMap[n.id] = n;
        for (const n of midNodes) nodeMap[n.id] = n;
        for (const n of taskNodes) nodeMap[n.id] = n;

        // Build directed edge indices for chain computation
        // reqToTest: requirement ID -> [test spec IDs]
        // testToReq: test spec ID -> [requirement IDs]
        // testToTask: test spec ID -> [task IDs]
        // taskToTest: task ID -> [test spec IDs]
        const reqToTest = {};
        const testToReq = {};
        const testToTask = {};
        const taskToTest = {};
        for (const e of edges) {
          if (e.type === 'requirement-to-test') {
            if (!reqToTest[e.from]) reqToTest[e.from] = [];
            reqToTest[e.from].push(e.to);
            if (!testToReq[e.to]) testToReq[e.to] = [];
            testToReq[e.to].push(e.from);
          } else if (e.type === 'test-to-task') {
            if (!testToTask[e.from]) testToTask[e.from] = [];
            testToTask[e.from].push(e.to);
            if (!taskToTest[e.to]) taskToTest[e.to] = [];
            taskToTest[e.to].push(e.from);
          }
        }

        const reqIdSet = new Set(requirements.map(r => r.id));
        const tsIdSet = new Set(testSpecs.map(t => t.id));

        // Compute directed chain: follows flow direction, not transitive BFS
        function getDirectedChain(startId) {
          const chain = new Set([startId]);

          if (reqIdSet.has(startId)) {
            // Requirement: go right → test specs → tasks
            const linkedTests = reqToTest[startId] || [];
            for (const tsId of linkedTests) {
              chain.add(tsId);
              for (const taskId of (testToTask[tsId] || [])) chain.add(taskId);
            }
          } else if (tsIdSet.has(startId)) {
            // Test spec: go left → requirements, go right → tasks
            for (const reqId of (testToReq[startId] || [])) chain.add(reqId);
            for (const taskId of (testToTask[startId] || [])) chain.add(taskId);
          } else {
            // Task: go left → test specs → requirements
            const linkedTests = taskToTest[startId] || [];
            for (const tsId of linkedTests) {
              chain.add(tsId);
              for (const reqId of (testToReq[tsId] || [])) chain.add(reqId);
            }
          }

          return chain;
        }

        let svg = `<svg class="testify-sankey-svg" viewBox="0 0 ${svgW} ${svgH}" role="img"
          aria-label="Traceability Sankey diagram: ${requirements.length} requirements, ${testSpecs.length} test specifications, ${tasks.length} tasks">`;

        // Column labels
        svg += `<text class="sankey-col-label" x="${COL_LEFT + NODE_W / 2}" y="${COL_LABEL_Y}" text-anchor="middle">Requirements</text>`;
        svg += `<text class="sankey-col-label" x="${COL_MID + NODE_W / 2}" y="${COL_LABEL_Y}" text-anchor="middle">Test Specs</text>`;
        svg += `<text class="sankey-col-label" x="${COL_RIGHT + NODE_W / 2}" y="${COL_LABEL_Y}" text-anchor="middle">Tasks</text>`;

        // Pyramid group backgrounds
        for (const g of groupMeta) {
          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"/>`;
          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>`;
        }

        // Flow bands (edges)
        svg += '<g class="sankey-flows">';
        for (const e of edges) {
          const src = nodeMap[e.from];
          const tgt = nodeMap[e.to];
          if (!src || !tgt) continue;

          const srcW = src.w || NODE_W;
          const tgtW = tgt.w || NODE_W;
          const x1 = src.x + srcW;
          const y1 = src.y + NODE_H / 2;
          const x2 = tgt.x;
          const y2 = tgt.y + NODE_H / 2;
          const cx = (x1 + x2) / 2;

          // Color by test type
          let color = '#3B82F6';
          if (e.type === 'requirement-to-test') {
            const ts = testSpecs.find(t => t.id === e.to);
            if (ts) color = typeColors[ts.type] || color;
          } else if (e.type === 'test-to-task') {
            const ts = testSpecs.find(t => t.id === e.from);
            if (ts) color = typeColors[ts.type] || color;
          }

          const chainIds = [e.from, e.to].join(' ');
          svg += `<path class="sankey-flow" d="M${x1},${y1} C${cx},${y1} ${cx},${y2} ${x2},${y2}"
            fill="none" stroke="${color}" data-chain-ids="${escapeHtml(chainIds)}" data-from="${escapeHtml(e.from)}" data-to="${escapeHtml(e.to)}"/>`;
        }
        svg += '</g>';

        // Render nodes
        function renderNodeGroup(nodes, colClass) {
          let s = `<g class="${colClass}">`;
          for (const n of nodes) {
            const w = n.w || NODE_W;
            const chain = Array.from(getDirectedChain(n.id)).join(' ');
            const gapClass = n.isGap ? ' gap-node' : (n.isChecked ? ' task-checked' : '');
            const isUntestedReq = gaps.untestedRequirements.includes(n.id);
            const isUnimplTest = gaps.unimplementedTests.includes(n.id);
            let ariaDesc = n.id;
            if (n.traceability && n.traceability.length > 0) {
              ariaDesc += ', linked from ' + n.traceability.join(', ');
            }
            // Find connections
            const outgoing = edges.filter(e => e.from === n.id).map(e => e.to);
            const incoming = edges.filter(e => e.to === n.id).map(e => e.from);
            if (incoming.length > 0) ariaDesc += ', linked from ' + incoming.join(', ');
            if (outgoing.length > 0) ariaDesc += ', linked to ' + outgoing.join(', ');
            if (isUntestedReq) ariaDesc += ' (untested)';
            if (isUnimplTest) ariaDesc += ' (unimplemented)';

            const fillColor = n.isGap ? 'var(--color-surface-elevated)' : 'var(--color-surface-elevated)';
            const strokeColor = n.isGap ? 'var(--color-tampered)' : 'var(--color-border)';
            const label = n.id;
            const desc = n.text || n.title || n.description || '';
            const truncDesc = desc.length > 22 ? desc.substring(0, 22) + '...' : desc;

            // Cross-link: requirements → spec, tasks → implement, gap nodes → checklist
            let crossAttr = '';
            if (n.isGap) {
              crossAttr = ` data-cross-target="checklist" data-cross-id="${escapeHtml(n.id)}"`;
            } else if (n.col === 'left') {
              crossAttr = ` data-cross-target="spec" data-cross-id="${escapeHtml(n.id)}"`;
            } else if (n.col === 'right') {
              crossAttr = ` data-cross-target="implement" data-cross-id="${escapeHtml(n.id)}"`;
            }
            s += `<g class="sankey-node${gapClass}" tabindex="0" role="button"
              data-node-id="${escapeHtml(n.id)}" data-chain="${escapeHtml(chain)}"${crossAttr}
              aria-describedby="desc-${escapeHtml(n.id)}">
              <title>${escapeHtml(n.id)}: ${escapeHtml(desc)}${isUntestedReq ? ' (untested)' : ''}${isUnimplTest ? ' (unimplemented)' : ''}</title>
              <desc id="desc-${escapeHtml(n.id)}">${escapeHtml(ariaDesc)}</desc>
              <rect class="sankey-node-rect" x="${n.x}" y="${n.y}" width="${w}" height="${NODE_H}"
                fill="${fillColor}" stroke="${strokeColor}"/>
              <text class="sankey-node-label" x="${n.x + 8}" y="${n.y + 13}">${escapeHtml(label)}</text>
              <text class="sankey-node-desc" x="${n.x + 8}" y="${n.y + 25}">${escapeHtml(truncDesc)}</text>
            </g>`;
          }
          s += '</g>';
          return s;
        }

        svg += renderNodeGroup(reqNodes, 'sankey-col-left');
        svg += renderNodeGroup(midNodes, 'sankey-col-mid');
        svg += renderNodeGroup(taskNodes, 'sankey-col-right');

        // Empty column messages
        if (requirements.length === 0) {
          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>`;
        }
        if (tasks.length === 0) {
          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-05-tasks</text>`;
        }

        svg += '</svg>';

        return `<div class="testify-sankey-container">${svg}</div>`;
      }

      function attachSankeyInteractions(data) {
        const svgEl = contentArea.querySelector('.testify-sankey-svg');
        if (!svgEl) return;

        // Clear fade-in animations after they complete so .dimmed opacity can take effect
        // (CSS animation fill-mode: both overrides normal opacity rules)
        setTimeout(() => {
          svgEl.querySelectorAll('.sankey-node, .sankey-flow').forEach(el => {
            el.style.animation = 'none';
          });
        }, 1000);

        const allNodes = svgEl.querySelectorAll('.sankey-node');
        const allFlows = svgEl.querySelectorAll('.sankey-flow');
        const allElements = [...allNodes, ...allFlows];
        let lockedChain = null; // Click locks the highlight

        // Expose highlight to cross-panel navigation
        externalSankeyHighlight = (nodeId) => { lockedChain = nodeId; highlightChain(nodeId); };

        function highlightChain(nodeId) {
          // Get full chain from data-chain attribute
          const nodeEl = svgEl.querySelector(`[data-node-id="${nodeId}"]`);
          if (!nodeEl) return;
          const chainStr = nodeEl.getAttribute('data-chain') || '';
          const chainIds = new Set(chainStr.split(' ').filter(Boolean));

          // Dim everything, highlight chain members
          for (const el of allNodes) {
            const elId = el.getAttribute('data-node-id');
            if (chainIds.has(elId)) {
              el.classList.add('highlighted');
              el.classList.remove('dimmed');
            } else {
              el.classList.add('dimmed');
              el.classList.remove('highlighted');
            }
          }
          for (const flow of allFlows) {
            const from = flow.getAttribute('data-from');
            const to = flow.getAttribute('data-to');
            if (chainIds.has(from) && chainIds.has(to)) {
              flow.classList.add('highlighted');
              flow.classList.remove('dimmed');
            } else {
              flow.classList.add('dimmed');
              flow.classList.remove('highlighted');
            }
          }
        }

        function clearHighlight() {
          if (lockedChain) return; // Don't clear if a chain is click-locked
          for (const el of allElements) {
            el.classList.remove('dimmed', 'highlighted');
          }
        }

        // Hover handlers on nodes
        for (const node of allNodes) {
          const nodeId = node.getAttribute('data-node-id');
          node.addEventListener('mouseenter', () => {
            if (!lockedChain) highlightChain(nodeId);
          });
          node.addEventListener('mouseleave', clearHighlight);
          // Click: lock/unlock chain highlight (skip if Cmd/Ctrl for cross-navigation)
          node.addEventListener('click', (e) => {
            if (e.metaKey || e.ctrlKey) return;
            if (lockedChain === nodeId) {
              lockedChain = null;
              clearHighlight();
            } else {
              lockedChain = nodeId;
              highlightChain(nodeId);
            }
          });
          // Keyboard: Enter/Space triggers chain highlight
          node.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') {
              e.preventDefault();
              if (lockedChain === nodeId) {
                lockedChain = null;
                clearHighlight();
              } else {
                lockedChain = nodeId;
                highlightChain(nodeId);
              }
            }
          });
        }

        // Hover handlers on flows
        for (const flow of allFlows) {
          const from = flow.getAttribute('data-from');
          flow.addEventListener('mouseenter', () => {
            if (!lockedChain) highlightChain(from);
          });
          flow.addEventListener('mouseleave', clearHighlight);
        }

        // Click on empty SVG area clears locked chain
        svgEl.addEventListener('click', (e) => {
          if (e.target === svgEl || e.target.tagName === 'svg') {
            lockedChain = null;
            for (const el of allElements) {
              el.classList.remove('dimmed', 'highlighted');
            }
          }
        });
      }

      // ====== Spec Story Map View ======
      let currentStoryMap = null;

      async function renderStoryMapView() {
        if (!currentFeature) return;

        try {
          if (!currentStoryMap) {
            if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {
              currentStoryMap = window.DASHBOARD_DATA.featureData[currentFeature].storyMap;
            } else {
              const res = await fetch(`/api/storymap/${currentFeature}`);
              if (!res.ok) throw new Error('Failed to load');
              currentStoryMap = await res.json();
            }
          }
          renderStoryMapContent(currentStoryMap);
        } catch {
          contentArea.innerHTML = '<div class="storymap-empty">Failed to load story map data.</div>';
        }
      }

      function renderStoryMapContent(data) {
        if (!data.stories.length && !data.requirements.length) {
          contentArea.innerHTML = `
            <div class="storymap-empty">
              <div class="placeholder-view-title">No Specification Data</div>
              <div>This feature's spec.md has no user stories or requirements yet.</div>
            </div>`;
          return;
        }

        contentArea.innerHTML = `
          <div class="storymap-view" role="region" aria-label="Spec Story Map">
            <div class="storymap-main">
              <div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>
              <div class="storymap-section-title">Story Map</div>
              <div class="swim-lanes" role="list" aria-label="User stories by priority"></div>
              <div class="storymap-section-title">Requirements Graph</div>
              <div class="graph-container">
                <svg class="graph-svg" aria-label="Requirements relationship graph"></svg>
                <div class="graph-tooltip"></div>
                <div class="graph-legend">
                  <div class="graph-legend-item"><span class="graph-legend-dot us"></span> User Story</div>
                  <div class="graph-legend-item"><span class="graph-legend-dot fr"></span> Requirement</div>
                  <div class="graph-legend-item"><span class="graph-legend-dot sc"></span> Success Criterion</div>
                </div>
              </div>
            </div>
          </div>
          <div class="detail-panel-slot"></div>`;

        renderSwimLanes(data);
        renderRequirementsGraph(data);
      }

      function renderSwimLanes(data) {
        const container = contentArea.querySelector('.swim-lanes');
        if (!container) return;

        const lanes = { P1: [], P2: [], P3: [] };
        for (const story of data.stories) {
          const p = story.priority || 'P3';
          if (!lanes[p]) lanes[p] = [];
          lanes[p].push(story);
        }

        let html = '';
        for (const [priority, stories] of Object.entries(lanes)) {
          html += `<div class="swim-lane" role="listitem">
            <div class="swim-lane-label ${priority.toLowerCase()}">${priority}</div>
            <div class="swim-lane-cards">`;
          for (const story of stories) {
            const refs = (story.requirementRefs || []).map(r => `<span class="story-card-badge">${escapeHtml(r)}</span>`).join('');
            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)}">
              <div class="story-card-header">
                <span class="story-card-id">${escapeHtml(story.id)}</span>
                <span class="story-card-priority ${story.priority.toLowerCase()}">${escapeHtml(story.priority)}</span>
              </div>
              <div class="story-card-title">${escapeHtml(story.title)}</div>
              <div class="story-card-meta">
                <span class="story-card-badge scenarios">${story.scenarioCount || 0} scenario${(story.scenarioCount || 0) !== 1 ? 's' : ''}</span>
                ${refs}
              </div>
            </div>`;
          }
          html += '</div></div>';
        }
        container.innerHTML = html;

        // Story card click → highlight graph node + show detail (FR-016)
        container.querySelectorAll('.story-card').forEach(card => {
          card.addEventListener('click', (e) => {
            if (e.metaKey || e.ctrlKey) return; // Let cross-link handler handle Cmd/Ctrl+click
            const storyId = card.dataset.storyId;
            highlightGraphNode(storyId);
            const story = data.stories.find(s => s.id === storyId);
            if (story) showDetailPanel(storyId, 'us', story.title, story.body || '');
            const nodeEl = contentArea.querySelector(`.graph-node[data-id="${storyId}"]`);
            if (nodeEl) nodeEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
          });
          card.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); card.click(); }
          });
        });
      }

      function renderRequirementsGraph(data) {
        const svg = contentArea.querySelector('.graph-svg');
        if (!svg) return;

        if (!data.requirements.length && !data.successCriteria.length) {
          const container = svg.parentElement;
          container.innerHTML = '<div class="graph-empty">No requirements or success criteria defined yet.</div>' +
            container.querySelector('.graph-legend')?.outerHTML || '';
          return;
        }

        // Build nodes
        const nodes = [];
        const nodeMap = {};

        for (const s of data.stories) {
          const n = { id: s.id, type: 'us', label: s.id, desc: s.title, x: 0, y: 0, vx: 0, vy: 0 };
          nodes.push(n);
          nodeMap[s.id] = n;
        }
        for (const r of data.requirements) {
          const n = { id: r.id, type: 'fr', label: r.id, desc: r.text, x: 0, y: 0, vx: 0, vy: 0 };
          nodes.push(n);
          nodeMap[r.id] = n;
        }
        for (const sc of data.successCriteria) {
          const n = { id: sc.id, type: 'sc', label: sc.id, desc: sc.text, x: 0, y: 0, vx: 0, vy: 0 };
          nodes.push(n);
          nodeMap[sc.id] = n;
        }

        // Size SVG based on node count
        const width = svg.clientWidth || 800;
        const height = Math.min(width, Math.max(300, nodes.length * 30 + 100));
        svg.style.height = height + 'px';
        svg.setAttribute('viewBox', `0 0 ${width} ${height}`);

        // Initial positions: spread nodes randomly within bounds
        const pad = 50;
        for (const n of nodes) {
          n.x = pad + Math.random() * (width - 2 * pad);
          n.y = pad + Math.random() * (height - 2 * pad);
        }

        // Build edges
        const edges = data.edges.filter(e => nodeMap[e.from] && nodeMap[e.to]);

        // Simple force-directed layout
        for (let iter = 0; iter < 120; iter++) {
          // Repulsion between all pairs
          for (let i = 0; i < nodes.length; i++) {
            for (let j = i + 1; j < nodes.length; j++) {
              let dx = nodes[j].x - nodes[i].x;
              let dy = nodes[j].y - nodes[i].y;
              let dist = Math.sqrt(dx * dx + dy * dy) || 1;
              let force = 3000 / (dist * dist);
              let fx = (dx / dist) * force;
              let fy = (dy / dist) * force;
              nodes[i].vx -= fx;
              nodes[i].vy -= fy;
              nodes[j].vx += fx;
              nodes[j].vy += fy;
            }
          }
          // Attraction along edges
          for (const e of edges) {
            const a = nodeMap[e.from];
            const b = nodeMap[e.to];
            let dx = b.x - a.x;
            let dy = b.y - a.y;
            let dist = Math.sqrt(dx * dx + dy * dy) || 1;
            let force = (dist - 100) * 0.05;
            let fx = (dx / dist) * force;
            let fy = (dy / dist) * force;
            a.vx += fx;
            a.vy += fy;
            b.vx -= fx;
            b.vy -= fy;
          }
          // Apply velocities with damping
          for (const n of nodes) {
            n.vx *= 0.7;
            n.vy *= 0.7;
            n.x += n.vx;
            n.y += n.vy;
            n.x = Math.max(pad, Math.min(width - pad, n.x));
            n.y = Math.max(pad, Math.min(height - pad, n.y));
          }
        }

        // Render edges
        let svgContent = '';
        for (const e of edges) {
          const a = nodeMap[e.from];
          const b = nodeMap[e.to];
          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}"/>`;
        }

        // Render nodes
        const radius = { us: 18, fr: 14, sc: 12 };
        for (const n of nodes) {
          const r = radius[n.type] || 14;
          const crossTarget = n.type === 'us' ? 'implement' : 'testify';
          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)}">
            <circle cx="${n.x}" cy="${n.y}" r="${r}" stroke="var(--color-bg)" stroke-width="2"/>
            <text x="${n.x}" y="${n.y + r + 14}">${n.label}</text>
          </g>`;
        }

        svg.innerHTML = svgContent;

        // Click-to-highlight + detail panel (FR-006)
        svg.addEventListener('click', (e) => {
          if (e.metaKey || e.ctrlKey) return; // Let cross-link handler handle Cmd/Ctrl+click
          const nodeEl = e.target.closest('.graph-node');
          if (nodeEl) {
            const id = nodeEl.dataset.id;
            highlightGraphNode(id);
            // Show detail panel
            const story = data.stories.find(s => s.id === id);
            const req = data.requirements.find(r => r.id === id);
            const sc = data.successCriteria.find(s => s.id === id);
            if (story) showDetailPanel(id, 'us', story.title, story.body || '');
            else if (req) showDetailPanel(id, 'fr', id, req.text);
            else if (sc) showDetailPanel(id, 'sc', id, sc.text);
          } else {
            clearGraphHighlight();
            closeDetailPanel();
          }
        });

        // Tooltip on hover (FR-014)
        const tooltip = contentArea.querySelector('.graph-tooltip');
        svg.addEventListener('mouseover', (e) => {
          const nodeEl = e.target.closest('.graph-node');
          if (nodeEl && tooltip) {
            tooltip.textContent = nodeEl.dataset.desc;
            tooltip.style.display = 'block';
          }
        });
        svg.addEventListener('mousemove', (e) => {
          if (tooltip && tooltip.style.display === 'block') {
            const rect = svg.parentElement.getBoundingClientRect();
            tooltip.style.left = (e.clientX - rect.left + 12) + 'px';
            tooltip.style.top = (e.clientY - rect.top - 8) + 'px';
          }
        });
        svg.addEventListener('mouseout', (e) => {
          if (!e.target.closest('.graph-node') && tooltip) {
            tooltip.style.display = 'none';
          }
        });

        // Drag nodes (FR-007)
        let dragNode = null;
        let dragOffset = { x: 0, y: 0 };

        svg.addEventListener('mousedown', (e) => {
          if (e.metaKey || e.ctrlKey) return; // Don't drag on Cmd/Ctrl+click
          const nodeEl = e.target.closest('.graph-node');
          if (!nodeEl) return;
          e.preventDefault();
          dragNode = nodeEl;
          const circle = nodeEl.querySelector('circle');
          const svgRect = svg.getBoundingClientRect();
          const viewBox = svg.viewBox.baseVal;
          const scaleX = viewBox.width / svgRect.width;
          const scaleY = viewBox.height / svgRect.height;
          dragOffset.x = parseFloat(circle.getAttribute('cx')) - (e.clientX - svgRect.left) * scaleX;
          dragOffset.y = parseFloat(circle.getAttribute('cy')) - (e.clientY - svgRect.top) * scaleY;
          svg.style.cursor = 'grabbing';
        });

        document.addEventListener('mousemove', (e) => {
          if (!dragNode) return;
          const svgRect = svg.getBoundingClientRect();
          const viewBox = svg.viewBox.baseVal;
          const scaleX = viewBox.width / svgRect.width;
          const scaleY = viewBox.height / svgRect.height;
          const nx = (e.clientX - svgRect.left) * scaleX + dragOffset.x;
          const ny = (e.clientY - svgRect.top) * scaleY + dragOffset.y;
          const circle = dragNode.querySelector('circle');
          const text = dragNode.querySelector('text');
          circle.setAttribute('cx', nx);
          circle.setAttribute('cy', ny);
          text.setAttribute('x', nx);
          text.setAttribute('y', ny + parseFloat(circle.getAttribute('r')) + 14);
          // Update connected edges
          const nodeId = dragNode.dataset.id;
          svg.querySelectorAll('.graph-edge').forEach(edge => {
            if (edge.dataset.from === nodeId) { edge.setAttribute('x1', nx); edge.setAttribute('y1', ny); }
            if (edge.dataset.to === nodeId) { edge.setAttribute('x2', nx); edge.setAttribute('y2', ny); }
          });
        });

        document.addEventListener('mouseup', () => {
          if (dragNode) {
            dragNode = null;
            svg.style.cursor = 'grab';
          }
        });

        // Zoom/pan via wheel (FR-008)
        svg.addEventListener('wheel', (e) => {
          e.preventDefault();
          const viewBox = svg.viewBox.baseVal;
          const scale = e.deltaY > 0 ? 1.1 : 0.9;
          const svgRect = svg.getBoundingClientRect();
          const mx = ((e.clientX - svgRect.left) / svgRect.width) * viewBox.width + viewBox.x;
          const my = ((e.clientY - svgRect.top) / svgRect.height) * viewBox.height + viewBox.y;
          const nw = viewBox.width * scale;
          const nh = viewBox.height * scale;
          viewBox.x = mx - (mx - viewBox.x) * scale;
          viewBox.y = my - (my - viewBox.y) * scale;
          viewBox.width = nw;
          viewBox.height = nh;
        }, { passive: false });
      }

      function highlightGraphNode(nodeId) {
        const svg = contentArea.querySelector('.graph-svg');
        if (!svg) return;

        // Build connected set
        const connected = new Set([nodeId]);
        svg.querySelectorAll('.graph-edge').forEach(edge => {
          if (edge.dataset.from === nodeId) connected.add(edge.dataset.to);
          if (edge.dataset.to === nodeId) connected.add(edge.dataset.from);
        });

        svg.querySelectorAll('.graph-node').forEach(n => {
          const id = n.dataset.id;
          n.classList.toggle('highlighted', id === nodeId);
          n.classList.toggle('dimmed', !connected.has(id));
        });
        svg.querySelectorAll('.graph-edge').forEach(e => {
          const isConnected = e.dataset.from === nodeId || e.dataset.to === nodeId;
          e.classList.toggle('highlighted', isConnected);
          e.classList.toggle('dimmed', !isConnected);
        });
      }

      function clearGraphHighlight() {
        const svg = contentArea.querySelector('.graph-svg');
        if (!svg) return;
        svg.querySelectorAll('.graph-node').forEach(n => { n.classList.remove('highlighted', 'dimmed'); });
        svg.querySelectorAll('.graph-edge').forEach(e => { e.classList.remove('highlighted', 'dimmed'); });
      }

      function showDetailPanel(id, type, title, body) {
        const slot = contentArea.querySelector('.detail-panel-slot');
        if (!slot) return;

        // Simple markdown-like rendering: bold, italic, numbered lists
        const rendered = body
          .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
          .replace(/\*(.+?)\*/g, '<em>$1</em>')
          .replace(/^(\d+)\.\s+/gm, '<br>$1. ');

        // Check for related clarifications
        let clarifyLink = '';
        if (currentStoryMap && currentStoryMap.clarifications) {
          const hasClarify = currentStoryMap.clarifications.some(c =>
            c.refs && c.refs.some(r => r === id || r.replace(/^(US|FR|SC)-/i, (_, p) => p.toUpperCase()) === id)
          );
          if (hasClarify) {
            clarifyLink = `<div style="margin-top:12px;padding-top:8px;border-top:1px solid var(--color-border-subtle)">
              <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>
            </div>`;
          }
        }

        slot.innerHTML = `
          <div class="detail-panel" role="region" aria-label="Detail view for ${escapeHtml(id)}">
            <div class="detail-panel-header">
              <span class="detail-panel-id ${type}">${escapeHtml(id)}</span>
              <button class="detail-panel-close" aria-label="Close detail panel">&times;</button>
            </div>
            <div class="detail-panel-title">${escapeHtml(title)}</div>
            <div class="detail-panel-body">${rendered}</div>
            ${clarifyLink}
          </div>`;

        slot.querySelector('.detail-panel-close').addEventListener('click', closeDetailPanel);
        const clarifyNav = slot.querySelector('.clarify-nav-link');
        if (clarifyNav) {
          clarifyNav.addEventListener('click', (e) => {
            e.preventDefault();
            navigateToPanel('clarify', clarifyNav.dataset.entityId);
          });
        }
        slot.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
      }

      function closeDetailPanel() {
        const slot = contentArea.querySelector('.detail-panel-slot');
        if (slot) slot.innerHTML = '';
      }

      // ====== Clarify View ======
      async function renderClarifyView() {
        if (!currentFeature) {
          contentArea.innerHTML = '<div class="placeholder-view"><div class="placeholder-view-title">Select a feature to view clarifications</div></div>';
          return;
        }

        // Check if spec phase has been completed
        const specPhase = currentPipeline?.phases?.find(p => p.id === 'spec');
        if (specPhase && specPhase.status === 'not_started') {
          contentArea.innerHTML = `
            <div class="clarify-view" role="region" aria-label="Clarifications">
              <div class="clarify-empty">
                <div class="placeholder-view-title">Specification Not Yet Created</div>
                <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-clarify</code> to resolve ambiguities.</div>
              </div>
            </div>`;
          return;
        }

        try {
          if (!currentStoryMap) {
            if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {
              currentStoryMap = window.DASHBOARD_DATA.featureData[currentFeature].storyMap;
            } else {
              const res = await fetch(`/api/storymap/${currentFeature}`);
              if (!res.ok) throw new Error('Failed to load');
              currentStoryMap = await res.json();
            }
          }
          renderClarifyContent(currentStoryMap.clarifications || []);
        } catch {
          contentArea.innerHTML = '<div class="placeholder-view"><div class="placeholder-view-title">Failed to load clarification data</div></div>';
        }
      }

      function renderClarifyContent(clarifications) {
        if (!clarifications.length) {
          // TS-019/TS-020: Distinguish "clarify was run, no issues" from "never run"
          const clarifyPhase = currentPipeline?.phases?.find(p => p.id === 'clarify');
          const clarifyWasRun = clarifyPhase && (clarifyPhase.status === 'complete' || clarifyPhase.status === 'skipped');

          const title = clarifyWasRun
            ? 'Clarification Complete'
            : 'Clarify Not Yet Run';
          const text = clarifyWasRun
            ? 'The clarification phase was completed with no ambiguities found. The specification is clear as written.'
            : 'This is an optional phase. Run <code>/iikit-clarify</code> to identify and resolve ambiguities in any artifact.';

          contentArea.innerHTML = `
            <div class="clarify-view" role="region" aria-label="Clarifications">
              <div class="clarify-empty">
                <div class="placeholder-view-title">${title}</div>
                <div class="placeholder-view-text">${text}</div>
              </div>
            </div>`;
          return;
        }

        // Group by session
        const sessions = {};
        for (const c of clarifications) {
          if (!sessions[c.session]) sessions[c.session] = [];
          sessions[c.session].push(c);
        }

        let html = `<div class="clarify-view" role="region" aria-label="Clarifications">
          <div class="clarify-header">
            <span class="clarify-title">Clarification Trail</span>
            <span class="clarify-count">${clarifications.length} Q&amp;A${clarifications.length !== 1 ? 's' : ''}</span>
          </div>
          <div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>
          <div class="clarify-sessions">`;

        for (const [session, entries] of Object.entries(sessions)) {
          html += `<div class="clarify-session">
            <div class="clarify-session-label">Session ${escapeHtml(session)}</div>
            <div class="clarify-entries">`;
          for (const c of entries) {
            const refsHtml = (c.refs && c.refs.length)
              ? `<div class="clarify-refs">${c.refs.map(r => `<a class="clarify-ref" href="#" data-ref-id="${escapeHtml(r)}">${escapeHtml(r)}</a>`).join('')}</div>`
              : '';
            html += `<div class="clarify-entry">
              <div class="clarify-question"><span class="clarify-q-label">Q</span> ${escapeHtml(c.question)}</div>
              <div class="clarify-answer"><span class="clarify-a-label">A</span> ${escapeHtml(c.answer)}</div>
              ${refsHtml}
            </div>`;
          }
          html += '</div></div>';
        }

        html += '</div></div>';
        contentArea.innerHTML = html;

        // Wire up ref links to navigate to Spec tab and highlight the item
        contentArea.querySelectorAll('.clarify-ref').forEach(link => {
          link.addEventListener('click', (e) => {
            e.preventDefault();
            const refId = link.dataset.refId;
            navigateToSpecItem(refId);
          });
        });
      }

      async function navigateToSpecItem(refId) {
        // Thin wrapper — delegates to generic cross-panel navigation
        navigateToPanel('spec', refId);
      }

      // ====== Clarify FAB + Slide Panel ======
      const clarifyFab = document.getElementById('clarifyFab');
      const clarifyPanel = document.getElementById('clarifyPanel');
      let clarifyPanelOpen = false;

      let clarifyPanelPhase = null; // Which phase's Q&A to show (null = active tab)

      function updateClarifyFab() {
        const total = (currentPipeline?.phases || []).reduce((sum, p) => sum + (p.clarifications || 0), 0);
        if (total > 0) {
          clarifyFab.textContent = clarifyPanelOpen ? '\u00d7' : 'Q&A';
          clarifyFab.classList.add('visible');
        } else {
          clarifyFab.classList.remove('visible');
        }
        if (clarifyPanelOpen) renderClarifyPanelContent();
      }

      function toggleClarifyPanel(forPhase) {
        if (typeof forPhase === 'string') {
          // Opening for a specific phase
          if (clarifyPanelOpen && clarifyPanelPhase === forPhase) {
            // Already showing this phase — close
            clarifyPanelOpen = false;
          } else {
            clarifyPanelPhase = forPhase;
            clarifyPanelOpen = true;
          }
        } else {
          // Toggle: FAB click or close button
          clarifyPanelOpen = !clarifyPanelOpen;
          if (clarifyPanelOpen) clarifyPanelPhase = activeTab || null;
        }
        clarifyPanel.classList.toggle('open', clarifyPanelOpen);
        clarifyFab.classList.toggle('panel-open', clarifyPanelOpen);
        clarifyFab.textContent = clarifyPanelOpen ? '\u00d7' : 'Q&A';
        if (clarifyPanelOpen) renderClarifyPanelContent();
      }

      function getPhaseEntries(phaseId) {
        if (!currentPipeline?.phases) return [];
        const phase = currentPipeline.phases.find(p => p.id === phaseId);
        return phase?.clarificationEntries || [];
      }

      function renderClarifyPanelContent() {
        const phase = clarifyPanelPhase || activeTab;
        const entries = getPhaseEntries(phase);
        const phaseObj = currentPipeline?.phases?.find(p => p.id === phase);
        const phaseName = phaseObj?.name?.replace('\n', ' ') || phase;

        // Build tab bar for phases that have Q&A
        const phasesWithQA = (currentPipeline?.phases || []).filter(p => p.clarifications > 0);
        let tabsHtml = '';
        if (phasesWithQA.length > 1) {
          tabsHtml = '<div class="clarify-tabs">' + phasesWithQA.map(p => {
            const name = p.name.replace('\n', ' ');
            const active = p.id === phase ? ' active' : '';
            return `<button class="clarify-tab${active}" data-phase="${p.id}">${escapeHtml(name)}</button>`;
          }).join('') + '</div>';
        }

        if (!entries.length) {
          clarifyPanel.innerHTML = `<div class="clarify-view">
            <div class="clarify-header">
              <span class="clarify-title">${escapeHtml(phaseName)} Q&amp;A</span>
              <button class="clarify-panel-close" title="Close">\u00d7</button>
            </div>
            ${tabsHtml}
            <div class="clarify-empty"><div class="placeholder-view-title">No Q&amp;A for this phase</div></div>
          </div>`;
          wireTabsAndClose();
          return;
        }

        const sessions = {};
        for (const c of entries) {
          if (!sessions[c.session]) sessions[c.session] = [];
          sessions[c.session].push(c);
        }
        let html = `<div class="clarify-view">
          <div class="clarify-header">
            <span class="clarify-title">${escapeHtml(phaseName)} Q&amp;A</span>
            <button class="clarify-panel-close" title="Close">\u00d7</button>
          </div>
          ${tabsHtml}`;
        for (const [session, items] of Object.entries(sessions)) {
          html += `<div class="clarify-session">
            <div class="clarify-session-label">Session ${escapeHtml(session)}</div>
            <div class="clarify-entries">`;
          for (const c of items) {
            const refsHtml = (c.refs && c.refs.length)
              ? `<div class="clarify-refs">${c.refs.map(r => `<a class="clarify-ref" href="#">${escapeHtml(r)}</a>`).join('')}</div>`
              : '';
            html += `<div class="clarify-entry">
              <div class="clarify-question"><span class="clarify-q-label">Q</span> ${escapeHtml(c.question)}</div>
              <div class="clarify-answer"><span class="clarify-a-label">A</span> ${escapeHtml(c.answer)}</div>
              ${refsHtml}
            </div>`;
          }
          html += '</div></div>';
        }
        html += '</div>';
        clarifyPanel.innerHTML = html;
        wireTabsAndClose();
      }

      function wireTabsAndClose() {
        const closeBtn = clarifyPanel.querySelector('.clarify-panel-close');
        if (closeBtn) closeBtn.addEventListener('click', () => toggleClarifyPanel());
        clarifyPanel.querySelectorAll('.clarify-tab').forEach(tab => {
          tab.addEventListener('click', () => {
            clarifyPanelPhase = tab.dataset.phase;
            renderClarifyPanelContent();
          });
        });
      }

      clarifyFab.addEventListener('click', () => toggleClarifyPanel());

      // ====== Constitution View ======
      let currentConstitution = null;
      let currentPremise = null;
      let selectedPrinciple = null;

      async function renderConstitutionView() {
        try {
          if (staticMode) {
            if (!currentConstitution) currentConstitution = window.DASHBOARD_DATA.constitution;
            if (!currentPremise) currentPremise = window.DASHBOARD_DATA.premise;
          } else {
            const fetches = [];
            if (!currentConstitution) {
              fetches.push(fetch('/api/constitution').then(r => r.json()).then(d => { currentConstitution = d; }));
            }
            if (!currentPremise) {
              fetches.push(fetch('/api/premise').then(r => r.json()).then(d => { currentPremise = d; }));
            }
            await Promise.all(fetches);
          }
          renderConstitutionContent(currentConstitution);
        } catch (err) {
          console.error('Failed to load constitution:', err);
          contentArea.innerHTML = '<div class="placeholder-view"><div class="placeholder-view-title">Failed to load constitution</div></div>';
        }
      }

      function renderConstitutionContent(data) {
        if (!data || !data.exists) {
          contentArea.innerHTML = `
            <div class="placeholder-view">
              <div class="placeholder-view-title">No Constitution Found</div>
              <div class="placeholder-view-text">Run /iikit-00-constitution to define your project's governance principles.</div>
            </div>`;
          return;
        }

        const principles = data.principles;
        if (principles.length === 0) {
          contentArea.innerHTML = `
            <div class="placeholder-view">
              <div class="placeholder-view-title">No Principles Found</div>
              <div class="placeholder-view-text">Your CONSTITUTION.md exists but has no parseable principles.</div>
            </div>`;
          return;
        }

        // Summary list
        const summaryHtml = principles.map((p, i) => {
          const isSelected = selectedPrinciple && selectedPrinciple.number === p.number;
          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>`;
        }).join('');

        // Radar chart SVG
        const radarSvg = generateRadarSVG(principles);

        // Detail card — only shown when a principle is selected
        const detailHtml = selectedPrinciple
          ? `<div class="detail-card">${renderDetailCard(selectedPrinciple)}</div>`
          : '';

        // Timeline
        const timelineHtml = data.version ? renderTimeline(data.version) : '';

        // Premise section
        let premiseSectionCount = 0;
        let premiseHtml = '';
        if (currentPremise && currentPremise.exists) {
          const premiseResult = renderPremiseContent(currentPremise.content);
          premiseHtml = `<div class="premise-section">
            <div class="premise-section-title">Premise</div>
            <div class="premise-grid">${premiseResult.cardsHtml}</div>
          </div>`;
          premiseSectionCount = premiseResult.sectionCount;
        }

        contentArea.innerHTML = `
          <div class="constitution-view">
            ${premiseHtml}
            <div class="constitution-layout">
              <div class="constitution-left">
                <div class="radar-container">${radarSvg}</div>
              </div>
              <div class="constitution-right">
                <div class="constitution-summary">${summaryHtml}</div>
                ${detailHtml}
              </div>
            </div>
            ${timelineHtml}
          </div>`;

        // Attach click handlers to radar axes AND list items
        function selectPrinciple(p) {
          selectedPrinciple = p;
          renderConstitutionContent(data);
        }

        principles.forEach((p, i) => {
          const axisEl = document.getElementById(`radar-axis-${i}`);
          if (axisEl) {
            axisEl.addEventListener('click', () => selectPrinciple(p));
            axisEl.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); selectPrinciple(p); } });
          }
          const itemEl = document.getElementById(`principle-item-${i}`);
          if (itemEl) {
            itemEl.addEventListener('click', () => selectPrinciple(p));
          }
        });

        // Attach premise collapsible handlers
        if (premiseSectionCount > 0) attachPremiseHandlers(premiseSectionCount);
      }

      let selectedPremiseSection = null;
      let parsedPremiseSections = [];

      function parsePremiseMarkdown(markdown) {
        const lines = markdown.split('\n');
        let title = 'Premise';
        const sections = [];
        let current = null;
        let inList = false;

        function closeList() {
          if (inList && current) { current.body += '</ul>'; inList = false; }
        }

        for (const line of lines) {
          const trimmed = line.trim();
          if (!trimmed) { closeList(); continue; }
          if (/^# /.test(trimmed)) {
            closeList(); title = trimmed.slice(2);
          } else if (/^## /.test(trimmed)) {
            closeList();
            current = { heading: trimmed.slice(3), body: '' };
            sections.push(current);
          } else if (current) {
            if (/^- /.test(trimmed)) {
              if (!inList) { current.body += '<ul>'; inList = true; }
              current.body += `<li>${trimmed.slice(2).replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')}</li>`;
            } else {
              closeList();
              current.body += `<p>${trimmed.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')}</p>`;
            }
          }
        }
        closeList();
        return { title, sections };
      }

      function renderPremiseContent(markdown) {
        const { title, sections } = parsePremiseMarkdown(markdown);
        parsedPremiseSections = sections;

        const cardsHtml = sections.map((s, i) => {
          const isSelected = selectedPremiseSection === i;
          const preview = s.body.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
          const previewText = preview.length > 120 ? preview.slice(0, 117) + '...' : preview;
          return `<div class="premise-card${isSelected ? ' selected' : ''}" id="premise-item-${i}" role="button" tabindex="0">
            <div class="premise-card-label">${escapeHtml(s.heading)}</div>
            <div class="premise-card-preview">${escapeHtml(previewText)}</div>
            <div class="premise-card-body">${s.body}</div>
          </div>`;
        }).join('');

        return { cardsHtml, sectionCount: sections.length };
      }

      function attachPremiseHandlers(sectionCount) {
        for (let i = 0; i < sectionCount; i++) {
          const el = document.getElementById(`premise-item-${i}`);
          if (!el) continue;
          function handler() {
            selectedPremiseSection = selectedPremiseSection === i ? null : i;
            renderConstitutionContent(currentConstitution);
          }
          el.addEventListener('click', handler);
          el.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } });
        }
      }

      function generateRadarSVG(principles) {
        const size = 360;
        const cx = size / 2;
        const cy = size / 2;
        const maxR = size / 2 - 60;
        const levels = { 'MUST': 1, 'SHOULD': 0.66, 'MAY': 0.33 };
        const n = principles.length;
        const angleStep = (2 * Math.PI) / n;
        const startAngle = -Math.PI / 2; // Start from top

        // Ring lines (33%, 66%, 100%)
        let rings = '';
        [0.33, 0.66, 1].forEach(pct => {
          const r = maxR * pct;
          const points = [];
          for (let i = 0; i < n; i++) {
            const angle = startAngle + i * angleStep;
            points.push(`${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`);
          }
          rings += `<polygon points="${points.join(' ')}" fill="none" stroke="var(--color-border)" stroke-width="1" opacity="0.5"/>`;
        });

        // Axis lines from center to outer edge
        let axes = '';
        for (let i = 0; i < n; i++) {
          const angle = startAngle + i * angleStep;
          const x2 = cx + maxR * Math.cos(angle);
          const y2 = cy + maxR * Math.sin(angle);
          axes += `<line x1="${cx}" y1="${cy}" x2="${x2}" y2="${y2}" stroke="var(--color-border)" stroke-width="1" opacity="0.3"/>`;
        }

        // Filled polygon connecting principle values
        const valuePoints = principles.map((p, i) => {
          const angle = startAngle + i * angleStep;
          const r = maxR * (levels[p.level] || 0.66);
          return `${cx + r * Math.cos(angle)},${cy + r * Math.sin(angle)}`;
        });
        const polygon = `<polygon points="${valuePoints.join(' ')}" fill="var(--color-accent)" fill-opacity="0.2" stroke="var(--color-accent)" stroke-width="2"/>`;

        // Value dots and clickable areas
        let dots = '';
        principles.forEach((p, i) => {
          const angle = startAngle + i * angleStep;
          const r = maxR * (levels[p.level] || 0.66);
          const x = cx + r * Math.cos(angle);
          const y = cy + r * Math.sin(angle);

          // Label position (pushed further out)
          const labelR = maxR + 24;
          const lx = cx + labelR * Math.cos(angle);
          const ly = cy + labelR * Math.sin(angle);
          const anchor = Math.abs(Math.cos(angle)) < 0.1 ? 'middle' : Math.cos(angle) > 0 ? 'start' : 'end';

          const isSelected = selectedPrinciple && selectedPrinciple.number === p.number;

          dots += `
            <g id="radar-axis-${i}" class="radar-axis" tabindex="0" role="button"
               aria-label="${escapeHtml(p.name)} (${p.level})">
              <circle cx="${x}" cy="${y}" r="${isSelected ? 7 : 5}" fill="var(--color-accent)" stroke="${isSelected ? 'var(--color-text)' : 'none'}" stroke-width="2"/>
              <circle cx="${x}" cy="${y}" r="16" fill="transparent"/>
              <text x="${lx}" y="${ly}" text-anchor="${anchor}" dominant-baseline="middle"
                    font-size="11" font-weight="600" fill="var(--color-text-secondary)"
                    style="letter-spacing: 0.2px;">${escapeHtml(p.name)}</text>
            </g>`;
        });

        const ariaLabel = `Radar chart showing ${n} constitution principles: ${principles.map(p => p.name + ' (' + p.level + ')').join(', ')}`;

        return `<svg viewBox="0 0 ${size} ${size}" role="img" aria-label="${escapeHtml(ariaLabel)}">${rings}${axes}${polygon}${dots}</svg>`;
      }

      function renderDetailCard(principle) {
        // Strip rationale from main text to avoid duplication
        let mainText = principle.text;
        if (principle.rationale) {
          const ratIdx = mainText.indexOf('**Rationale**');
          if (ratIdx > -1) mainText = mainText.substring(0, ratIdx);
        }
        mainText = mainText.trim();

        return `
          <h3>${escapeHtml(principle.number)}. ${escapeHtml(principle.name)}</h3>
          <div class="detail-level"><span class="level-badge ${principle.level.toLowerCase()}">${principle.level}</span></div>
          <div class="detail-text">${escapeHtml(mainText).replace(/\n\n/g, '<br><br>').replace(/\n/g, ' ')}</div>
          ${principle.rationale ? `<div class="detail-rationale"><strong>Rationale:</strong> ${escapeHtml(principle.rationale)}</div>` : ''}`;
      }

      function renderTimeline(version) {
        return `
          <div class="amendment-timeline">
            <div class="amendment-timeline-label">Amendment History</div>
            <div class="amendment-timeline-content">
              <div class="amendment-timeline-dot"></div>
              <span>v${escapeHtml(version.version)} &mdash; Ratified ${escapeHtml(version.ratified)}</span>
              ${version.ratified !== version.lastAmended ? `
                <div class="amendment-timeline-line"></div>
                <div class="amendment-timeline-dot"></div>
                <span>Amended ${escapeHtml(version.lastAmended)}</span>
              ` : ''}
            </div>
          </div>`;
      }

      // ====== Analyze Consistency View ======

      async function renderAnalyzeView() {
        if (!currentFeature) return;
        try {
          if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {
            currentAnalyze = window.DASHBOARD_DATA.featureData[currentFeature].analyze;
          } else {
            const res = await fetch(`/api/analyze/${currentFeature}`);
            if (!res.ok) throw new Error('Failed to load');
            currentAnalyze = await res.json();
          }
          renderAnalyzeContent(currentAnalyze);
        } catch {
          contentArea.innerHTML = '<div class="analyze-empty"><div class="analyze-empty-title">Failed to load analyze data.</div></div>';
        }
      }

      function renderAnalyzeContent(data) {
        if (!data) return;

        if (!data.exists) {
          contentArea.innerHTML = `
            <div class="analyze-empty">
              <div class="analyze-empty-icon">&#128270;</div>
              <div class="analyze-empty-title">No analysis data for this feature</div>
              <div class="analyze-empty-text">Run <code>/iikit-06-analyze</code> to generate a cross-artifact consistency analysis report.</div>
            </div>`;
          return;
        }

        let html = '<div class="analyze-view">';

        // Health Gauge Section
        html += '<div class="analyze-section">';
        html += '<div class="analyze-section-header">Health Score</div>';
        html += renderHealthGauge(data.healthScore, data);
        html += '</div>';

        // Coverage Heatmap Section
        html += '<div class="analyze-section" style="animation-delay: 0.1s">';
        html += `<div class="cross-link-tip">Tip: ${CROSS_LINK_MOD_KEY}+click any identifier to navigate to its linked panel</div>`;
        html += '<div class="analyze-section-header">Coverage Heatmap</div>';
        html += renderHeatmap(data.heatmap);
        html += '</div>';

        // Severity Table Section
        html += '<div class="analyze-section slide-in" style="animation-delay: 0.2s">';
        html += '<div class="analyze-section-header">Issues</div>';
        html += renderSeverityTable(data.issues);
        html += '</div>';

        html += '</div>';
        contentArea.innerHTML = html;

        // Attach event listeners after rendering
        attachHeatmapListeners();
        attachSeverityListeners();

        // Trigger gauge animation after a frame
        requestAnimationFrame(() => {
          const needle = contentArea.querySelector('.gauge-needle');
          if (needle && data.healthScore) {
            const angle = (data.healthScore.score / 100) * 180;
            needle.style.transform = `rotate(${angle}deg)`;
          }
        });
      }

      function renderHealthGauge(healthScore, analyzeData) {
        if (!healthScore) {
          return '<div class="gauge-na">N/A</div>';
        }

        const { score, zone, factors, trend } = healthScore;

        // SVG semicircular gauge
        // Arc center at (100, 90), radius 70
        const cx = 100, cy = 90, r = 70;
        const startAngle = Math.PI;
        const endAngle = 0;

        function arcPath(startPct, endPct) {
          const a1 = Math.PI - (startPct / 100) * Math.PI;
          const a2 = Math.PI - (endPct / 100) * Math.PI;
          const x1 = cx + r * Math.cos(a1);
          const y1 = cy - r * Math.sin(a1);
          const x2 = cx + r * Math.cos(a2);
          const y2 = cy - r * Math.sin(a2);
          const large = (endPct - startPct) > 50 ? 1 : 0;
          return `M ${x1} ${y1} A ${r} ${r} 0 ${large} 1 ${x2} ${y2}`;
        }

        let trendHtml = '';
        if (trend === 'improving') {
          trendHtml = '<span class="gauge-trend gauge-trend-up">&#9650; improving</span>';
        } else if (trend === 'declining') {
          trendHtml = '<span class="gauge-trend gauge-trend-down">&#9660; declining</span>';
        }

        let gaugeHtml = '<div class="gauge-container">';
        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">`;

        // Zone arcs
        gaugeHtml += `<path class="gauge-arc gauge-zone-red" d="${arcPath(0, 40)}" />`;
        gaugeHtml += `<path class="gauge-arc gauge-zone-yellow" d="${arcPath(40, 70)}" />`;
        gaugeHtml += `<path class="gauge-arc gauge-zone-green" d="${arcPath(70, 100)}" />`;

        // Needle (starts at 0, animated via CSS transform)
        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);" />`;

        // Score text
        gaugeHtml += `<text class="gauge-score" x="${cx}" y="${cy - 15}">${score}</text>`;
        gaugeHtml += `<text class="gauge-label" x="${cx}" y="${cy + 5}">/ 100</text>`;

        gaugeHtml += '</svg>';

        // Breakdown with context
        gaugeHtml += '<div class="gauge-breakdown">';
        if (factors) {
          const metrics = analyzeData?.metrics;
          const issues = analyzeData?.issues || [];
          const alignment = analyzeData?.constitutionAlignment || [];

          for (const [key, factor] of Object.entries(factors)) {
            const isOk = factor.value === 100;
            const colorClass = factor.value >= 71 ? 'gauge-factor-green' : factor.value >= 41 ? 'gauge-factor-yellow' : 'gauge-factor-red';
            let context = '';

            if (key === 'requirementsCoverage' && metrics) {
              context = metrics.requirementCoverage || '';
            } else if (key === 'constitutionCompliance') {
              const violations = alignment.filter(a => a.status === 'VIOLATION');
              context = violations.length ? violations.map(v => v.principle).join(', ') : 'All aligned';
            } else if (key === 'phaseSeparation') {
              const resolved = issues.filter(i => i.category && /phase/i.test(i.category) && i.resolved);
              const unresolved = issues.filter(i => i.category && /phase/i.test(i.category) && !i.resolved);
              if (unresolved.length) context = `${unresolved.length} violation${unresolved.length > 1 ? 's' : ''} open`;
              else if (resolved.length) context = `${resolved.length} resolved`;
              else if (factor.value < 100) context = 'See issues below';
              else context = 'No violations';
            } else if (key === 'testCoverage' && metrics) {
              context = metrics.totalTestSpecs ? `${metrics.totalTestSpecs} test specs` : '';
            }

            gaugeHtml += `<div class="gauge-factor ${colorClass}">`;
            gaugeHtml += `<span class="gauge-factor-label">${escapeHtml(factor.label)}`;
            if (context) gaugeHtml += `<span class="gauge-factor-context">${escapeHtml(context)}</span>`;
            gaugeHtml += `</span>`;
            gaugeHtml += `<span class="gauge-factor-value">${factor.value}%</span>`;
            gaugeHtml += `</div>`;
          }
        }
        if (trendHtml) gaugeHtml += `<div style="text-align: center; margin-top: 4px;">${trendHtml}</div>`;
        gaugeHtml += '</div>';
        gaugeHtml += '</div>';

        return gaugeHtml;
      }

      function renderHeatmap(heatmap) {
        if (!heatmap || !heatmap.rows || heatmap.rows.length === 0) {
          return '<div class="heatmap-empty">No coverage data</div>';
        }

        // Only look at columns with real data (skip 'plan' if ALL plan cells are N/A)
        const activeCols = (heatmap.columns || []).filter(c => {
          if (c !== 'plan') return true;
          return heatmap.rows.some(r => r.cells.plan?.status !== 'na');
        });
        const total = heatmap.rows.length;

        // Find rows with gaps (missing or partial in any active column)
        const gaps = heatmap.rows.filter(row =>
          activeCols.some(col => {
            const cell = row.cells[col];
            return cell && (cell.status === 'missing' || cell.status === 'partial');
          })
        );

        // Coverage stats per column
        const colStats = {};
        for (const col of activeCols) {
          const covered = heatmap.rows.filter(r => r.cells[col]?.status === 'covered').length;
          colStats[col] = { covered, total, pct: total > 0 ? Math.round((covered / total) * 100) : 0 };
        }

        let html = '<div style="padding: 20px;">';

        // Summary bar
        html += '<div class="coverage-summary">';
        for (const col of activeCols) {
          const s = colStats[col];
          const colorClass = s.pct === 100 ? 'cov-bar-green' : s.pct >= 70 ? 'cov-bar-yellow' : 'cov-bar-red';
          html += `<div class="cov-stat">`;
          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>`;
          html += `<div class="cov-bar"><div class="cov-bar-fill ${colorClass}" style="width: ${s.pct}%"></div></div>`;
          html += `</div>`;
        }
        html += '</div>';

        if (gaps.length === 0) {
          // All covered — compact success
          html += `<div class="coverage-success">All ${total} requirements are covered by tasks and tests</div>`;
        } else {
          // Show only the gaps
          html += `<div class="coverage-gaps-label">${gaps.length} requirement${gaps.length > 1 ? 's' : ''} with gaps:</div>`;
          html += '<table class="heatmap-table" role="grid">';
          html += '<thead><tr><th scope="col">Requirement</th><th scope="col">Description</th>';
          for (const col of activeCols) {
            html += `<th scope="col">${escapeHtml(col.charAt(0).toUpperCase() + col.slice(1))}</th>`;
          }
          html += '</tr></thead><tbody>';

          gaps.forEach((row, i) => {
            html += `<tr class="heatmap-row" style="animation-delay: ${i * 0.04}s">`;
            html += `<th scope="row"><code>${escapeHtml(row.id)}</code></th>`;
            html += `<td class="heatmap-desc">${escapeHtml(row.text)}</td>`;

            for (const col of activeCols) {
              const cell = row.cells[col] || { status: 'na', refs: [] };
              const statusClass = 'coverage-' + cell.status;
              const statusLabel = cell.status === 'covered' ? 'Covered' : cell.status === 'partial' ? 'Partial' : cell.status === 'missing' ? 'Missing' : 'N/A';
              const refs = (cell.refs && cell.refs.length > 0) ? cell.refs.join(', ') : '';
              html += `<td>`;
              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>`;
              if (refs) {
                html += `<span class="heatmap-refs-inline">${refs}</span>`;
                html += `<div class="heatmap-refs" data-req="${escapeHtml(row.id)}" data-col="${escapeHtml(col)}">${refs}</div>`;
              }
              html += `</td>`;
            }
            html += '</tr>';
          });

          html += '</tbody></table>';
        }

        html += '</div>';
        return html;
      }

      function renderSeverityTable(issues) {
        if (!issues || issues.length === 0) {
          return '<div class="severity-empty">No issues found &mdash; all artifacts are consistent</div>';
        }

        // Collect unique categories and severities
        const categories = [...new Set(issues.map(i => i.category))].sort();
        const severities = ['critical', 'high', 'medium', 'low'];

        let html = '<div class="severity-wrapper">';

        // Filters
        html += '<div class="severity-controls">';
        html += '<select class="severity-filter" id="analyzeCategoryFilter"><option value="">All Categories</option>';
        for (const cat of categories) {
          html += `<option value="${escapeHtml(cat)}">${escapeHtml(cat)}</option>`;
        }
        html += '</select>';
        html += '<select class="severity-filter" id="analyzeSeverityFilter"><option value="">All Severities</option>';
        for (const sev of severities) {
          html += `<option value="${sev}">${sev.charAt(0).toUpperCase() + sev.slice(1)}</option>`;
        }
        html += '</select>';
        html += '</div>';

        // Table
        html += '<div class="severity-table-scroll"><table class="severity-table">';
        html += '<thead><tr>';
        html += '<th tabindex="0" data-sort="id">ID</th>';
        html += '<th tabindex="0" data-sort="category">Category</th>';
        html += '<th tabindex="0" data-sort="severity">Severity</th>';
        html += '<th tabindex="0" data-sort="location">Location</th>';
        html += '<th tabindex="0" data-sort="summary">Summary</th>';
        html += '</tr></thead><tbody>';

        // Sort by severity by default
        const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
        const sorted = [...issues].sort((a, b) => (severityOrder[a.severity] || 4) - (severityOrder[b.severity] || 4));

        for (const issue of sorted) {
          const resolvedClass = issue.resolved ? ' severity-resolved' : '';
          html += `<tr data-severity="${escapeHtml(issue.severity)}" data-category="${escapeHtml(issue.category)}" tabindex="0" class="${resolvedClass}">`;
          html += `<td>${escapeHtml(issue.id)}</td>`;
          html += `<td>${escapeHtml(issue.category)}</td>`;
          html += `<td><span class="severity-badge severity-${escapeHtml(issue.severity)}">${escapeHtml(issue.severity)}</span></td>`;
          html += `<td><code>${escapeHtml(issue.location)}</code></td>`;
          html += `<td>${escapeHtml(issue.summary)}</td>`;
          html += '</tr>';
          html += `<tr class="severity-recommendation" data-expand-for="${escapeHtml(issue.id)}"><td colspan="5">${escapeHtml(issue.recommendation)}</td></tr>`;
        }

        html += '</tbody></table></div></div>';
        return html;
      }

      function attachHeatmapListeners() {
        const cells = contentArea.querySelectorAll('.heatmap-cell');
        cells.forEach(cell => {
          const handler = () => {
            const req = cell.dataset.req;
            const col = cell.dataset.col;
            const ref = contentArea.querySelector(`.heatmap-refs[data-req="${req}"][data-col="${col}"]`);
            if (ref) ref.classList.toggle('expanded');
          };
          cell.addEventListener('click', handler);
          cell.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }
          });
        });
      }

      function attachSeverityListeners() {
        // Row expansion
        const rows = contentArea.querySelectorAll('.severity-table tr[data-severity]');
        rows.forEach(row => {
          const handler = () => {
            const nextRow = row.nextElementSibling;
            if (nextRow && nextRow.classList.contains('severity-recommendation')) {
              nextRow.classList.toggle('expanded');
            }
          };
          row.addEventListener('click', handler);
          row.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }
          });
        });

        // Column sorting
        const headers = contentArea.querySelectorAll('.severity-table th[data-sort]');
        headers.forEach(th => {
          const handler = () => {
            const sortKey = th.dataset.sort;
            const tbody = contentArea.querySelector('.severity-table tbody');
            if (!tbody) return;

            const dataRows = Array.from(tbody.querySelectorAll('tr[data-severity]'));
            const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };

            dataRows.sort((a, b) => {
              let aVal, bVal;
              if (sortKey === 'severity') {
                aVal = severityOrder[a.dataset.severity] || 4;
                bVal = severityOrder[b.dataset.severity] || 4;
                return aVal - bVal;
              }
              const aIdx = Array.from(th.parentElement.children).indexOf(th);
              aVal = a.children[aIdx]?.textContent || '';
              bVal = b.children[aIdx]?.textContent || '';
              return aVal.localeCompare(bVal);
            });

            // Rebuild tbody preserving recommendation rows
            const fragment = document.createDocumentFragment();
            for (const row of dataRows) {
              fragment.appendChild(row);
              const expandId = row.querySelector('td')?.textContent;
              const recRow = tbody.querySelector(`tr[data-expand-for="${expandId}"]`);
              if (recRow) fragment.appendChild(recRow);
            }
            tbody.innerHTML = '';
            tbody.appendChild(fragment);
          };
          th.addEventListener('click', handler);
          th.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }
          });
        });

        // Filters
        const catFilter = document.getElementById('analyzeCategoryFilter');
        const sevFilter = document.getElementById('analyzeSeverityFilter');

        function applyFilters() {
          const catVal = catFilter?.value || '';
          const sevVal = sevFilter?.value || '';
          const allRows = contentArea.querySelectorAll('.severity-table tbody tr');

          allRows.forEach(row => {
            if (row.classList.contains('severity-recommendation')) {
              // Hide recommendations when parent is hidden
              const expandFor = row.dataset.expandFor;
              const parent = contentArea.querySelector(`.severity-table tr[data-severity] td:first-child`);
              row.style.display = row.classList.contains('expanded') ? '' : 'none';
              return;
            }
            if (!row.dataset.severity) return;

            const matchCat = !catVal || row.dataset.category === catVal;
            const matchSev = !sevVal || row.dataset.severity === sevVal;
            row.style.display = (matchCat && matchSev) ? '' : 'none';

            // Also hide its recommendation row
            const nextRow = row.nextElementSibling;
            if (nextRow && nextRow.classList.contains('severity-recommendation')) {
              nextRow.style.display = (matchCat && matchSev) ? (nextRow.classList.contains('expanded') ? '' : 'none') : 'none';
            }
          });
        }

        if (catFilter) catFilter.addEventListener('change', applyFilters);
        if (sevFilter) sevFilter.addEventListener('change', applyFilters);
      }

      function selectDefaultTab(pipeline) {
        if (!pipeline || !pipeline.phases) return 'implement';

        const impl = pipeline.phases.find(p => p.id === 'implement');
        if (impl && (impl.status === 'in_progress' || impl.status === 'complete')) return 'implement';

        // Walk backward through all phases to find last completed
        const allPhases = ['constitution', 'spec', 'clarify', 'plan', 'checklist', 'testify', 'tasks', 'analyze', 'implement'];
        for (let i = allPhases.length - 1; i >= 0; i--) {
          const phase = pipeline.phases.find(p => p.id === allPhases[i]);
          if (phase && phase.status === 'complete') {
            return allPhases[i];
          }
        }

        return 'implement';
      }

      async function loadPipeline(featureId) {
        try {
          let pipeline;
          if (staticMode && window.DASHBOARD_DATA.featureData[featureId]) {
            pipeline = window.DASHBOARD_DATA.featureData[featureId].pipeline;
          } else {
            const res = await fetch(`/api/pipeline/${featureId}`);
            if (!res.ok) return;
            pipeline = await res.json();
          }
          currentPipeline = pipeline;

          if (!activeTab) {
            activeTab = selectDefaultTab(pipeline);
          }

          renderPipeline(pipeline);
          switchTab(activeTab);
        } catch (err) {
          console.error('Failed to load pipeline:', err);
        }
      }

      // ====== Feature Loading ======
      async function loadFeatures() {
        try {
          let features;
          if (staticMode) {
            features = window.DASHBOARD_DATA.features;
          } else {
            const res = await fetch('/api/features');
            features = await res.json();
          }
          updateFeatureSelector(features);

          if (features.length > 0 && !currentFeature) {
            currentFeature = features[0].id;
            featureSelect.value = currentFeature;
            loadPipeline(currentFeature);
            loadBoard(currentFeature);
            loadBugs(currentFeature);
          } else if (features.length === 0) {
            showEmptyState();
          }
        } catch (err) {
          console.error('Failed to load features:', err);
        }
      }

      function updateFeatureSelector(features) {
        featureSelect.innerHTML = '';
        if (features.length === 0) {
          featureSelect.innerHTML = '<option value="">No features found</option>';
          return;
        }
        for (const f of features) {
          const opt = document.createElement('option');
          opt.value = f.id;
          opt.textContent = `${f.id} — ${f.name} (${f.progress})`;
          featureSelect.appendChild(opt);
        }
      }

      featureSelect.addEventListener('change', () => {
        const val = featureSelect.value;
        if (val && val !== currentFeature) {
          currentFeature = val;
          previousCardColumns = {};
          activeTab = null; // Reset tab selection for new feature
          currentStoryMap = null; // Reset story map cache
          currentPlanView = null; // Reset plan view cache
          currentTestify = null; // Reset testify cache
          currentAnalyze = null; // Reset analyze cache
          currentBugs = null; // Reset bugs cache
          loadPipeline(val);
          loadBoard(val);
          loadBugs(val);
        }
      });

      featureSelect.addEventListener('keydown', (e) => {
        if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
          // Default browser behavior handles this
          return;
        }
      });

      // ====== Board Loading ======
      async function loadBoard(featureId) {
        try {
          showLoading();
          let board;
          if (staticMode && window.DASHBOARD_DATA.featureData[featureId]) {
            board = window.DASHBOARD_DATA.featureData[featureId].board;
          } else {
            const res = await fetch(`/api/board/${featureId}`);
            if (!res.ok) throw new Error(`HTTP ${res.status}`);
            board = await res.json();
          }
          currentBoard = board;
          renderBoard(board);
          updateIntegrity(board.integrity);
        } catch (err) {
          console.error('Failed to load board:', err);
          showEmptyState('Failed to load board data');
        }
      }

      async function loadBugs(featureId) {
        try {
          let bugs;
          if (staticMode && window.DASHBOARD_DATA.featureData[featureId]) {
            bugs = window.DASHBOARD_DATA.featureData[featureId].bugs;
          } else {
            const res = await fetch(`/api/bugs/${featureId}`);
            if (!res.ok) return;
            bugs = await res.json();
          }
          currentBugs = bugs;
          if (currentPipeline) renderPipeline(currentPipeline);
          if (activeTab === 'bugs') renderBugsContent(currentBugs);
        } catch {
          // Silently fail — bugs are optional
        }
      }

      function showLoading() {
        boardEl.innerHTML = '<div class="loading"><div class="loading-spinner"></div></div>';
      }

      function showEmptyState(msg) {
        boardEl.innerHTML = `
          <div class="empty-state">
            <div class="empty-state-icon">&#9776;</div>
            <div class="empty-state-title">${msg || 'No features found'}</div>
            <div class="empty-state-text">Create a feature with spec.md and tasks.md in your specs/ directory to get started.</div>
          </div>`;
      }

      // ====== Board Rendering ======
      function renderBoardInto(targetEl, board) {
        if (!board || !targetEl) return;

        const columns = [
          { key: 'todo', label: 'Todo', cards: board.todo || [] },
          { key: 'in_progress', label: 'In Progress', cards: board.in_progress || [] },
          { key: 'done', label: 'Done', cards: board.done || [] }
        ];

        // Build new card positions
        const newCardColumns = {};
        for (const col of columns) {
          for (const card of col.cards) {
            newCardColumns[card.id] = col.key;
          }
        }

        // Detect moved cards
        const movedCards = {};
        for (const [cardId, newCol] of Object.entries(newCardColumns)) {
          const oldCol = previousCardColumns[cardId];
          if (oldCol && oldCol !== newCol) {
            movedCards[cardId] = { from: oldCol, to: newCol };
          }
        }

        targetEl.innerHTML = '';

        for (const col of columns) {
          const colEl = document.createElement('div');
          colEl.className = `column ${col.key === 'in_progress' ? 'in-progress' : col.key}`;
          colEl.setAttribute('role', 'region');
          colEl.setAttribute('aria-label', `${col.label} column with ${col.cards.length} stories`);

          colEl.innerHTML = `
            <div class="column-header">
              <div class="column-title">
                <span class="column-dot" aria-hidden="true"></span>
                ${col.label}
              </div>
              <span class="column-count">${col.cards.length}</span>
            </div>
            <div class="column-body" id="col-${col.key}"></div>`;

          targetEl.appendChild(colEl);

          const bodyEl = colEl.querySelector('.column-body');

          if (col.cards.length === 0) {
            bodyEl.innerHTML = '<div class="column-empty">No stories</div>';
          } else {
            for (const card of col.cards) {
              const cardEl = createCardElement(card, col.key);

              // Add animation class if card just moved here
              if (movedCards[card.id]) {
                cardEl.classList.add('entering');
                if (movedCards[card.id].to === 'done') {
                  cardEl.classList.add('just-completed');
                }
                // Remove animation class after it completes
                cardEl.addEventListener('animationend', () => {
                  cardEl.classList.remove('entering', 'just-completed');
                }, { once: true });
              }

              bodyEl.appendChild(cardEl);
            }
          }
        }

        previousCardColumns = newCardColumns;
      }

      function renderBoard(board) {
        const targetEl = document.getElementById('board');
        if (targetEl) renderBoardInto(targetEl, board);
      }

      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>';

      function createCardElement(card, columnKey) {
        const el = document.createElement('div');
        el.className = 'card' + (card.isBugCard ? ' bug-card' : '');
        el.setAttribute('data-card-id', card.id);
        el.setAttribute('role', 'article');
        el.setAttribute('aria-label', `${card.title} - ${card.priority} - ${card.progress} tasks complete`);

        const progressParts = card.progress.split('/');
        const checked = parseInt(progressParts[0], 10);
        const total = parseInt(progressParts[1], 10);
        const pct = total > 0 ? Math.round((checked / total) * 100) : 0;

        const priorityClass = card.priority ? card.priority.toLowerCase() : 'p3';

        const cardIdHtml = card.isBugCard
          ? `<div class="card-id cross-link" data-cross-target="bugs" data-cross-id="${card.id}">${BUG_SVG_ICON}${card.id}</div>`
          : `<div class="card-id cross-link" data-cross-target="spec" data-cross-id="${card.id}">${card.id}</div>`;

        el.innerHTML = `
          ${cardIdHtml}
          <div class="card-header">
            <div class="card-title" title="${escapeHtml(card.title)}">${card.isBugCard ? BUG_SVG_ICON : ''}${escapeHtml(card.title)}</div>
            <span class="priority-badge ${priorityClass}" aria-label="Priority ${card.priority}">${card.priority}</span>
          </div>
          <div class="progress-container">
            <div class="progress-info">
              <span class="progress-label">Progress</span>
              <span class="progress-value">${card.progress} (${pct}%)</span>
            </div>
            <div class="progress-bar" role="progressbar" aria-valuenow="${pct}" aria-valuemin="0" aria-valuemax="100">
              <div class="progress-fill" style="width: ${pct}%"></div>
            </div>
          </div>
          <button class="task-toggle" onclick="toggleTasks(this)" aria-expanded="false" aria-controls="tasks-${card.id}">
            <span class="task-toggle-icon" aria-hidden="true">&#9654;</span>
            ${(card.tasks || []).length} tasks
          </button>
          <ul class="task-list collapsed" id="tasks-${card.id}" aria-label="Tasks for ${card.id}">
            ${(card.tasks || []).map(t => {
              const isBugTask = t.isBugFix || (t.id && t.id.startsWith('T-B'));
              const taskIcon = isBugTask ? BUG_SVG_ICON : '';
              const bugTagLink = t.bugTag
                ? ` <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>`
                : '';
              return `
              <li class="task-item ${t.checked ? 'checked' : ''}" data-task-id="${t.id}">
                <span class="task-checkbox ${t.checked ? 'checked' : ''}" aria-hidden="true"></span>
                ${taskIcon}<span class="task-id cross-link" data-cross-target="testify" data-cross-id="${t.id}">${t.id}</span>${bugTagLink}
                <span class="task-description">${escapeHtml(t.description)}</span>
              </li>`;
            }).join('')}
          </ul>`;

        return el;
      }

      function escapeHtml(str) {
        if (!str) return '';
        return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
      }

      // ====== Toast ======
      function showToast(message, duration = 2500) {
        const el = document.getElementById('toast');
        el.textContent = message;
        el.classList.add('visible');
        setTimeout(() => el.classList.remove('visible'), duration);
      }

      // ====== Integrity Badge ======
      function updateIntegrity(integrity) {
        if (!integrity) return;
        const badge = integrityBadge;
        const textEl = badge.querySelector('.integrity-text');

        badge.className = `integrity-badge ${integrity.status}`;

        switch (integrity.status) {
          case 'valid':
            textEl.textContent = 'Verified';
            badge.setAttribute('aria-label', 'Test integrity: verified');
            badge.title = 'Assertion hash matches stored hash';
            break;
          case 'tampered':
            textEl.textContent = 'Tampered';
            badge.setAttribute('aria-label', 'Test integrity: tampered - assertions may have been modified');
            badge.title = 'Assertion hash does not match stored hash!';
            break;
          case 'missing':
            textEl.textContent = 'Missing';
            badge.setAttribute('aria-label', 'Test integrity: no hash data available');
            badge.title = 'No test specifications or context.json found';
            break;
        }
      }

      // ====== Plan View ======
      let currentPlanView = null;

      async function renderPlanView() {
        if (!currentFeature) return;
        try {
          if (!currentPlanView) {
            if (staticMode && window.DASHBOARD_DATA.featureData[currentFeature]) {
              currentPlanView = window.DASHBOARD_DATA.featureData[currentFeature].planView;
            } else {
              const res = await fetch(`/api/planview/${currentFeature}`);
              currentPlanView = await res.json();
            }
          }
          renderPlanViewContent(currentPlanView);
        } catch (err) {
          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>`;
        }
      }

      function renderPlanViewContent(data) {
        if (!data || !data.exists) {
          contentArea.innerHTML = `<div class="planview-empty"><div class="planview-empty-title">No plan created yet</div><div class="planview-empty-text">Run /iikit-02-plan to create a technical implementation plan for this feature.</div></div>`;
          return;
        }

        let html = '<div class="planview-view">';

        // Badge Wall
        html += '<div class="planview-section">';
        html += '<div class="planview-section-title">Tech Stack</div>';
        if (data.techContext && data.techContext.length > 0) {
          html += '<div class="badge-wall">';
          for (const entry of data.techContext) {
            const tooltip = findResearchTooltip(entry.value, data.researchDecisions);
            html += `<div class="tech-badge" role="listitem">`;
            html += `<span class="tech-badge-label">${escapeHtml(entry.label)}</span>`;
            html += `<span class="tech-badge-value">${escapeHtml(entry.value)}</span>`;
            if (tooltip) {
              html += `<div class="tech-badge-tooltip" role="tooltip">${escapeHtml(tooltip)}</div>`;
            }
            html += `</div>`;
          }
          html += '</div>';
        } else {
          html += '<div class="planview-empty"><div class="planview-empty-text">No tech stack defined in plan</div></div>';
        }
        html += '</div>';

        // Tessl Tiles Panel
        if (data.tesslTiles && data.tesslTiles.length > 0) {
          html += '<div class="planview-section">';
          html += '<div class="planview-section-title">Tessl Tiles</div>';
          html += '<div class="tessl-tiles">';
          for (const tile of data.tesslTiles) {
            const bestScore = tile.eval ? tile.eval.score : tile.reviewScore;
            const colorClass = bestScore != null ? (bestScore >= 80 ? 'green' : bestScore >= 50 ? 'yellow' : 'red') : 'none';
            html += `<div class="tessl-tile-card score-${colorClass}">`;
            html += `<div class="tessl-tile-header"><span class="tessl-tile-name">${escapeHtml(tile.name)}</span></div>`;
            html += `<span class="tessl-tile-version">v${escapeHtml(tile.version)}</span>`;

            // Score badges
            const badges = [];
            if (tile.eval) {
              const ec = tile.eval.score >= 80 ? 'green' : tile.eval.score >= 50 ? 'yellow' : 'red';
              badges.push(`<span class="tessl-score-badge ${ec}"><span class="tessl-score-label">Eval</span> ${tile.eval.score}%</span>`);
            }
            if (tile.reviewScore != null) {
              const rc = tile.reviewScore >= 80 ? 'green' : tile.reviewScore >= 50 ? 'yellow' : 'red';
              badges.push(`<span class="tessl-score-badge ${rc}"><span class="tessl-score-label">Review</span> ${tile.reviewScore}</span>`);
            }
            if (badges.length === 0) {
              badges.push(`<span class="tessl-score-badge muted"><span class="tessl-score-label">No scores</span></span>`);
            }
            html += `<div class="tessl-tile-scores">${badges.join('')}</div>`;

            // Eval detail bar
            if (tile.eval && tile.eval.chartData) {
              const chartTotal = tile.eval.chartData.pass + tile.eval.chartData.fail;
              const barPct = chartTotal > 0 ? Math.round(tile.eval.chartData.pass / chartTotal * 100) : tile.eval.score;
              const bc = tile.eval.score >= 80 ? 'green' : tile.eval.score >= 50 ? 'yellow' : 'red';
              html += `<div class="tessl-tile-eval">`;
              html += `<div class="tessl-eval-bar" title="${tile.eval.chartData.pass} pass, ${tile.eval.chartData.fail} fail"><div class="tessl-eval-bar-fill ${bc}" style="width:${barPct}%"></div></div>`;
              html += `</div>`;
            }
            html += `</div>`;
          }
          html += '</div></div>';
        }

        // File Structure Tree
        if (data.fileStructure && data.fileStructure.entries && data.fileStructure.entries.length > 0) {
          html += '<div class="planview-section">';
          html += '<div class="planview-section-title">Project Structure</div>';
          html += '<div class="file-tree" role="tree" aria-label="Project file structure">';
          html += renderFileTree(data.fileStructure.entries, 0);
          html += '</div></div>';
        }

        // Architecture Diagram
        if (data.diagram && data.diagram.nodes && data.diagram.nodes.length > 0) {
          html += '<div class="planview-section">';
          html += '<div class="planview-section-title">Architecture</div>';
          html += '<div class="diagram-container">';
          html += renderDiagramSVG(data.diagram);
          html += '</div>';
          html += renderDiagramLegend(data.diagram);
          html += '<div id="diagram-detail" class="detail-panel-slot"></div>';
          html += '</div>';
        } else if (data.diagram && data.diagram.raw) {
          // Fallback: raw ASCII
          html += '<div class="planview-section">';
          html += '<div class="planview-section-title">Architecture</div>';
          html += `<pre class="diagram-raw">${escapeHtml(data.diagram.raw)}</pre>`;
          html += '</div>';
        }

        html += '</div>';
        contentArea.innerHTML = html;

        // Attach event handlers
        attachTreeHandlers();
        attachDiagramHandlers(data.diagram);
      }

      function findResearchTooltip(badgeValue, decisions) {
        if (!decisions || decisions.length === 0) return null;
        const valueLower = badgeValue.toLowerCase();
        for (const d of decisions) {
          if (d.title && valueLower.includes(d.title.toLowerCase().split(' ')[0])) {
            return d.rationale || d.decision;
          }
          // Also check if decision title words appear in badge value
          if (d.title) {
            const words = d.title.toLowerCase().split(/\s+/);
            for (const word of words) {
              if (word.length > 3 && valueLower.includes(word)) {
                return d.rationale || d.decision;
              }
            }
          }
        }
        return null;
      }

      function getFileIconInfo(name, isDir) {
        if (isDir) return { cls: 'dir', ch: '\uD83D\uDCC2' };
        const ext = name.split('.').pop();
        if (['js', 'mjs'].includes(ext)) return { cls: 'file-js', ch: 'JS' };
        if (ext === 'json') return { cls: 'file-json', ch: '{}' };
        if (ext === 'md') return { cls: 'file-md', ch: 'M\u2193' };
        if (ext === 'html') return { cls: 'file-html', ch: '</>' };
        return { cls: 'file', ch: '\u25CB' };
      }

      function renderFileTree(entries) {
        let html = '';
        let i = 0;
        while (i < entries.length) {
          const entry = entries[i];
          const isDir = entry.type === 'directory';
          const expanded = entry.depth < 2;

          // Indent guides
          let indent = '';
          for (let d = 0; d < entry.depth; d++) {
            indent += '<span class="file-tree-guide"></span>';
          }

          const { cls, ch } = getFileIconInfo(entry.name, isDir);

          if (isDir) {
            const children = [];
            let j = i + 1;
            while (j < entries.length && entries[j].depth > entry.depth) {
              children.push(entries[j]);
              j++;
            }
            const childId = `tree-${entry.depth}-${entry.name}`.replace(/[^a-zA-Z0-9-]/g, '_');

            html += `<div class="file-tree-entry">`;
            html += `<span class="file-tree-indent">${indent}</span>`;
            html += `<span class="file-tree-chevron" data-target="${childId}">${expanded ? '\u25BE' : '\u25B8'}</span>`;
            html += `<span class="file-tree-file-icon ${cls}">${ch}</span>`;
            html += `<span class="file-tree-label"><span class="file-tree-name">${escapeHtml(entry.name)}</span></span>`;
            if (entry.comment) html += `<span class="file-tree-comment" data-full="${escapeHtml(entry.comment)}">${escapeHtml(entry.comment)}</span>`;
            html += `</div>`;
            html += `<div id="${childId}" class="file-tree-children${expanded ? '' : ' collapsed'}">`;
            html += renderFileTree(children);
            html += `</div>`;
            i = j;
          } else {
            const isPlanned = entry.exists === false;
            const nameClass = isPlanned ? ' planned' : '';

            html += `<div class="file-tree-entry">`;
            html += `<span class="file-tree-indent">${indent}</span>`;
            html += `<span class="file-tree-chevron-spacer"></span>`;
            html += `<span class="file-tree-file-icon ${cls}">${ch}</span>`;
            html += `<span class="file-tree-label">`;
            html += `<span class="file-tree-name${nameClass}">${escapeHtml(entry.name)}</span>`;
            if (isPlanned) html += `<span class="file-tree-status planned-tag">planned</span>`;
            html += `</span>`;
            if (entry.comment) html += `<span class="file-tree-comment" data-full="${escapeHtml(entry.comment)}">${escapeHtml(entry.comment)}</span>`;
            html += `</div>`;
            i++;
          }
        }
        return html;
      }

      function renderDiagramLegend(diagram) {
        if (!diagram || !diagram.nodes) return '';
        const types = new Set(diagram.nodes.map(n => n.type).filter(t => t !== 'default'));
        if (types.size === 0) return '';
        const colors = { client: 'var(--color-accent)', server: 'var(--color-p2)', storage: 'var(--color-done)', external: 'var(--color-p1)' };
        let html = '<div class="diagram-legend">';
        for (const type of types) {
          html += `<div class="diagram-legend-item"><span class="diagram-legend-dot" style="background:${colors[type] || 'var(--color-text-muted)'}"></span>${type}</div>`;
        }
        html += '</div>';
        return html;
      }

      function attachTreeHandlers() {
        document.querySelectorAll('.file-tree-chevron').forEach(chevron => {
          chevron.addEventListener('click', () => {
            const targetId = chevron.dataset.target;
            const children = document.getElementById(targetId);
            if (!children) return;
            const isCollapsed = children.classList.contains('collapsed');
            children.classList.toggle('collapsed', !isCollapsed);
            chevron.textContent = isCollapsed ? '\u25BE' : '\u25B8';
          });
        });
        // Add title tooltip only on comments that are actually truncated
        document.querySelectorAll('.file-tree-comment').forEach(comment => {
          if (comment.scrollWidth > comment.clientWidth) {
            comment.classList.add('truncated');
            comment.title = comment.dataset.full || comment.textContent;
          }
        });
      }

      function renderDiagramSVG(diagram) {
        if (!diagram || !diagram.nodes || diagram.nodes.length === 0) return '';

        // Calculate SVG dimensions from node positions
        let maxX = 0, maxY = 0;
        for (const n of diagram.nodes) {
          const right = n.x + n.width;
          const bottom = n.y + n.height;
          if (right > maxX) maxX = right;
          if (bottom > maxY) maxY = bottom;
        }

        // Scale to SVG viewport
        const padding = 40;
        const svgWidth = 800;
        const scaleX = (svgWidth - padding * 2) / (maxX || 1);
        const scaleY = scaleX; // maintain aspect ratio
        const svgHeight = maxY * scaleY + padding * 2;

        const nodeTypeColors = {
          client: 'var(--color-accent)',
          server: 'var(--color-p2)',
          storage: 'var(--color-done)',
          external: 'var(--color-p1)',
          default: 'var(--color-text-muted)'
        };

        let svg = `<svg class="diagram-svg" viewBox="0 0 ${svgWidth} ${svgHeight}" role="figure" aria-label="Architecture diagram">`;

        // Draw edges first (behind nodes)
        for (const edge of diagram.edges) {
          const fromNode = diagram.nodes.find(n => n.id === edge.from);
          const toNode = diagram.nodes.find(n => n.id === edge.to);
          if (!fromNode || !toNode) continue;

          const x1 = (fromNode.x + fromNode.width / 2) * scaleX + padding;
          const y1 = (fromNode.y + fromNode.height) * scaleY + padding;
          const x2 = (toNode.x + toNode.width / 2) * scaleX + padding;
          const y2 = toNode.y * scaleY + padding;

          svg += `<line class="diagram-edge-line" x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" aria-hidden="true"/>`;
          if (edge.label) {
            const midX = (x1 + x2) / 2 + 8;
            const midY = (y1 + y2) / 2;
            svg += `<text class="diagram-edge-label" x="${midX}" y="${midY}">${escapeHtml(edge.label)}</text>`;
          }
        }

        // Draw nodes
        for (const node of diagram.nodes) {
          const x = node.x * scaleX + padding;
          const y = node.y * scaleY + padding;
          const w = node.width * scaleX;
          const h = node.height * scaleY;
          const color = nodeTypeColors[node.type] || nodeTypeColors.default;

          svg += `<g class="diagram-node" data-node-id="${node.id}" tabindex="0" role="img" aria-label="${escapeHtml(node.label)} (${node.type})">`;
          svg += `<rect class="diagram-node-rect" x="${x}" y="${y}" width="${w}" height="${h}" fill="var(--color-surface)" stroke="${color}"/>`;
          svg += `<text class="diagram-node-label" x="${x + w/2}" y="${y + h/2 + 5}" text-anchor="middle">${escapeHtml(node.label)}</text>`;
          svg += `</g>`;
        }

        svg += '</svg>';
        return svg;
      }

      function attachDiagramHandlers(diagram) {
        if (!diagram) return;
        document.querySelectorAll('.diagram-node').forEach(nodeEl => {
          const handler = () => {
            const nodeId = nodeEl.dataset.nodeId;
            const node = diagram.nodes.find(n => n.id === nodeId);
            if (!node) return;
            showPlanDetailPanel(node);
          };
          nodeEl.addEventListener('click', handler);
          nodeEl.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); }
          });
        });
      }

      function showPlanDetailPanel(node) {
        const slot = document.getElementById('diagram-detail');
        if (!slot) return;
        const typeLabel = node.type !== 'default' ? ` (${node.type})` : '';
        slot.innerHTML = `
          <div class="detail-panel">
            <div class="detail-panel-header">
              <span class="detail-panel-id">${escapeHtml(node.label)}${typeLabel}</span>
              <button class="detail-panel-close" aria-label="Close detail panel">\u00d7</button>
            </div>
            <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>
          </div>`;
        slot.querySelector('.detail-panel-close').addEventListener('click', () => { slot.innerHTML = ''; });
      }

      // ====== WebSocket (removed — replaced by meta-refresh in static mode) ======

      // ====== Theme Toggle ======
      const themeToggle = document.getElementById('themeToggle');
      const themeIcon = document.getElementById('themeIcon');
      const html = document.documentElement;

      // Three-state cycle: system -> light -> dark -> system
      let themeMode = localStorage.getItem('iikit-theme') || 'system';

      function getSystemTheme() {
        return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
      }

      function applyTheme(mode) {
        themeMode = mode;
        if (mode === 'system') {
          localStorage.removeItem('iikit-theme');
          const resolved = getSystemTheme();
          if (resolved === 'light') {
            html.setAttribute('data-theme', 'light');
          } else {
            html.removeAttribute('data-theme');
          }
          themeIcon.textContent = '\uD83D\uDDA5'; // monitor
          themeToggle.setAttribute('aria-label', 'Theme: System (click for Light)');
          themeToggle.title = 'Theme: System';
        } else if (mode === 'light') {
          localStorage.setItem('iikit-theme', 'light');
          html.setAttribute('data-theme', 'light');
          themeIcon.textContent = '\u2600'; // sun
          themeToggle.setAttribute('aria-label', 'Theme: Light (click for Dark)');
          themeToggle.title = 'Theme: Light';
        } else {
          localStorage.setItem('iikit-theme', 'dark');
          html.removeAttribute('data-theme');
          themeIcon.textContent = '\u263E'; // moon
          themeToggle.setAttribute('aria-label', 'Theme: Dark (click for System)');
          themeToggle.title = 'Theme: Dark';
        }
      }

      themeToggle.addEventListener('click', () => {
        const next = themeMode === 'system' ? 'light' : themeMode === 'light' ? 'dark' : 'system';
        applyTheme(next);
      });

      // Listen for OS theme changes — only react in system mode
      window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
        if (themeMode === 'system') applyTheme('system');
      });

      // Apply on load
      applyTheme(themeMode);

      // ====== Init ======
      if (staticMode) {
        // Bootstrap from inlined DASHBOARD_DATA (generated HTML)
        const D = window.DASHBOARD_DATA;
        const label = document.getElementById('projectLabel');
        const dirName = D.meta.projectPath.split('/').pop();
        label.textContent = dirName;
        label.title = D.meta.projectPath;
        document.title = dirName + ' — IIKit Dashboard';
        currentConstitution = D.constitution;
        currentPremise = D.premise;
        updateFeatureSelector(D.features);
        if (D.features.length > 0) {
          currentFeature = D.features[0].id;
          featureSelect.value = currentFeature;
          const fd = D.featureData[currentFeature];
          if (fd) {
            currentPipeline = fd.pipeline;
            currentBoard = fd.board;
            currentBugs = fd.bugs;
            currentStoryMap = fd.storyMap;
            if (!activeTab) activeTab = selectDefaultTab(fd.pipeline);
            renderPipeline(fd.pipeline);
            renderBoard(fd.board);
            updateIntegrity(fd.board.integrity);
            if (fd.bugs && currentPipeline) renderPipeline(currentPipeline);
            updateClarifyFab();
            switchTab(activeTab);
          }
        } else {
          showEmptyState('No features yet');
          // Still render constitution + premise when they exist
          if (D.constitution || D.premise) {
            activeTab = 'constitution';
            renderConstitutionView();
          }
        }
      } else {
        // Fetch mode (server-based)
        fetch('/api/meta').then(r => r.json()).then(meta => {
          const label = document.getElementById('projectLabel');
          const dirName = meta.projectPath.split('/').pop();
          label.textContent = dirName;
          label.title = meta.projectPath;
        }).catch(() => {});
        loadFeatures();
      }
    })();

    // ====== Task List Toggle (global, called from onclick) ======
    function toggleTasks(btn) {
      const list = btn.nextElementSibling;
      const icon = btn.querySelector('.task-toggle-icon');
      const isCollapsed = list.classList.contains('collapsed');

      if (isCollapsed) {
        list.classList.remove('collapsed');
        list.classList.add('expanded');
        icon.classList.add('expanded');
        btn.setAttribute('aria-expanded', 'true');
      } else {
        list.classList.remove('expanded');
        list.classList.add('collapsed');
        icon.classList.remove('expanded');
        btn.setAttribute('aria-expanded', 'false');
      }
    }
  </script>
</body>
</html>

skills

README.md

tile.json