guardlink 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/CHANGELOG.md +83 -9
  2. package/README.md +38 -1
  3. package/dist/agents/config.d.ts +7 -0
  4. package/dist/agents/config.d.ts.map +1 -1
  5. package/dist/agents/config.js.map +1 -1
  6. package/dist/agents/index.d.ts +1 -1
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +1 -1
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/agents/prompts.d.ts +14 -0
  11. package/dist/agents/prompts.d.ts.map +1 -1
  12. package/dist/agents/prompts.js +445 -2
  13. package/dist/agents/prompts.js.map +1 -1
  14. package/dist/analyze/format.d.ts +72 -0
  15. package/dist/analyze/format.d.ts.map +1 -0
  16. package/dist/analyze/format.js +176 -0
  17. package/dist/analyze/format.js.map +1 -0
  18. package/dist/analyze/index.d.ts +76 -0
  19. package/dist/analyze/index.d.ts.map +1 -1
  20. package/dist/analyze/index.js +165 -2
  21. package/dist/analyze/index.js.map +1 -1
  22. package/dist/analyze/prompts.d.ts +3 -2
  23. package/dist/analyze/prompts.d.ts.map +1 -1
  24. package/dist/analyze/prompts.js +16 -2
  25. package/dist/analyze/prompts.js.map +1 -1
  26. package/dist/analyzer/sarif.d.ts +3 -2
  27. package/dist/analyzer/sarif.d.ts.map +1 -1
  28. package/dist/analyzer/sarif.js +29 -3
  29. package/dist/analyzer/sarif.js.map +1 -1
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +380 -28
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/dashboard/data.d.ts +11 -0
  35. package/dist/dashboard/data.d.ts.map +1 -1
  36. package/dist/dashboard/data.js +12 -0
  37. package/dist/dashboard/data.js.map +1 -1
  38. package/dist/dashboard/diagrams.d.ts +81 -12
  39. package/dist/dashboard/diagrams.d.ts.map +1 -1
  40. package/dist/dashboard/diagrams.js +750 -362
  41. package/dist/dashboard/diagrams.js.map +1 -1
  42. package/dist/dashboard/generate.d.ts +5 -2
  43. package/dist/dashboard/generate.d.ts.map +1 -1
  44. package/dist/dashboard/generate.js +2516 -244
  45. package/dist/dashboard/generate.js.map +1 -1
  46. package/dist/diff/engine.d.ts +2 -1
  47. package/dist/diff/engine.d.ts.map +1 -1
  48. package/dist/diff/engine.js +3 -2
  49. package/dist/diff/engine.js.map +1 -1
  50. package/dist/init/index.d.ts.map +1 -1
  51. package/dist/init/index.js +24 -5
  52. package/dist/init/index.js.map +1 -1
  53. package/dist/init/migrate.d.ts +39 -0
  54. package/dist/init/migrate.d.ts.map +1 -0
  55. package/dist/init/migrate.js +45 -0
  56. package/dist/init/migrate.js.map +1 -0
  57. package/dist/init/templates.d.ts +8 -0
  58. package/dist/init/templates.d.ts.map +1 -1
  59. package/dist/init/templates.js +71 -9
  60. package/dist/init/templates.js.map +1 -1
  61. package/dist/mcp/lookup.d.ts +1 -0
  62. package/dist/mcp/lookup.d.ts.map +1 -1
  63. package/dist/mcp/lookup.js +138 -10
  64. package/dist/mcp/lookup.js.map +1 -1
  65. package/dist/mcp/server.d.ts +2 -1
  66. package/dist/mcp/server.d.ts.map +1 -1
  67. package/dist/mcp/server.js +20 -8
  68. package/dist/mcp/server.js.map +1 -1
  69. package/dist/parser/clear.js +1 -1
  70. package/dist/parser/clear.js.map +1 -1
  71. package/dist/parser/feature-filter.d.ts +42 -0
  72. package/dist/parser/feature-filter.d.ts.map +1 -0
  73. package/dist/parser/feature-filter.js +109 -0
  74. package/dist/parser/feature-filter.js.map +1 -0
  75. package/dist/parser/format.d.ts +24 -0
  76. package/dist/parser/format.d.ts.map +1 -0
  77. package/dist/parser/format.js +29 -0
  78. package/dist/parser/format.js.map +1 -0
  79. package/dist/parser/index.d.ts +2 -0
  80. package/dist/parser/index.d.ts.map +1 -1
  81. package/dist/parser/index.js +1 -0
  82. package/dist/parser/index.js.map +1 -1
  83. package/dist/parser/parse-file.d.ts.map +1 -1
  84. package/dist/parser/parse-file.js +3 -1
  85. package/dist/parser/parse-file.js.map +1 -1
  86. package/dist/parser/parse-line.d.ts +3 -0
  87. package/dist/parser/parse-line.d.ts.map +1 -1
  88. package/dist/parser/parse-line.js +78 -22
  89. package/dist/parser/parse-line.js.map +1 -1
  90. package/dist/parser/parse-project.js +19 -0
  91. package/dist/parser/parse-project.js.map +1 -1
  92. package/dist/parser/validate.d.ts +3 -0
  93. package/dist/parser/validate.d.ts.map +1 -1
  94. package/dist/parser/validate.js +7 -0
  95. package/dist/parser/validate.js.map +1 -1
  96. package/dist/report/index.d.ts +1 -0
  97. package/dist/report/index.d.ts.map +1 -1
  98. package/dist/report/index.js +1 -0
  99. package/dist/report/index.js.map +1 -1
  100. package/dist/report/report.d.ts.map +1 -1
  101. package/dist/report/report.js +924 -24
  102. package/dist/report/report.js.map +1 -1
  103. package/dist/report/sequence.d.ts +11 -0
  104. package/dist/report/sequence.d.ts.map +1 -0
  105. package/dist/report/sequence.js +140 -0
  106. package/dist/report/sequence.js.map +1 -0
  107. package/dist/tui/commands.d.ts +1 -0
  108. package/dist/tui/commands.d.ts.map +1 -1
  109. package/dist/tui/commands.js +83 -4
  110. package/dist/tui/commands.js.map +1 -1
  111. package/dist/tui/index.d.ts.map +1 -1
  112. package/dist/tui/index.js +7 -2
  113. package/dist/tui/index.js.map +1 -1
  114. package/dist/types/index.d.ts +57 -3
  115. package/dist/types/index.d.ts.map +1 -1
  116. package/dist/workspace/merge.d.ts.map +1 -1
  117. package/dist/workspace/merge.js +6 -2
  118. package/dist/workspace/merge.js.map +1 -1
  119. package/package.json +1 -1
@@ -10,33 +10,44 @@
10
10
  * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context"
11
11
  * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access"
12
12
  * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input"
13
+ * @flows ThreatModel -> #dashboard via topologyData -- "Serialized diagram graph consumed by client-side D3 renderer"
13
14
  * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads"
14
15
  * @flows #dashboard -> HTML via return -- "Generated HTML output"
16
+ * @mitigates #dashboard against #xss using #output-encoding -- "Serialized diagram data escapes closing script tags; D3 writes labels as text"
15
17
  * @handles internal on #dashboard -- "Processes and displays threat model data"
18
+ * @feature "Dashboard" -- "Interactive HTML threat model dashboard"
16
19
  */
17
- import { computeStats, computeSeverity, computeExposures, computeAssetHeatmap } from './data.js';
18
- import { generateThreatGraph, generateDataFlowDiagram, generateAttackSurface } from './diagrams.js';
20
+ import { listFeatures } from '../parser/feature-filter.js';
21
+ import { computeStats, computeSeverity, computeExposures, computeConfirmed, computeAssetHeatmap } from './data.js';
22
+ import { generateThreatGraph, generateDataFlowDiagram, generateAttackSurface, generateTopologyData } from './diagrams.js';
23
+ import { formatConfidence } from '../analyze/format.js';
19
24
  import { readFileSync } from 'fs';
20
25
  import { resolve, isAbsolute } from 'path';
21
- export function generateDashboardHTML(model, root, analyses) {
26
+ export function generateDashboardHTML(model, root, analyses, pentestData) {
22
27
  const stats = computeStats(model);
23
28
  const severity = computeSeverity(model);
24
29
  const exposures = computeExposures(model);
30
+ const confirmedRows = computeConfirmed(model);
25
31
  const heatmap = computeAssetHeatmap(model);
26
32
  const threatGraph = generateThreatGraph(model);
33
+ const threatGraphFull = generateThreatGraph(model, { showAll: true });
27
34
  const dataFlow = generateDataFlowDiagram(model);
28
35
  const attackSurface = generateAttackSurface(model);
36
+ const topology = generateTopologyData(model);
37
+ const featureNames = listFeatures(model);
29
38
  const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' ');
30
39
  const unmitigated = exposures.filter(e => !e.mitigated && !e.accepted);
31
40
  const mitigatedCount = exposures.filter(e => e.mitigated).length;
32
41
  const mitigationCoveragePercent = exposures.length > 0
33
42
  ? Math.round((mitigatedCount / exposures.length) * 100)
34
43
  : 0;
35
- const riskScore = computeRiskGrade(severity, unmitigated.length, exposures.length);
44
+ const riskScore = computeRiskGrade(severity, unmitigated.length, exposures.length, confirmedRows.length);
36
45
  // Build file annotations data for code browser + drawer
37
46
  const fileAnnotations = buildFileAnnotations(model, root);
38
47
  // Build analysis data for drawer
39
48
  const analysisData = buildAnalysisData(model, exposures);
49
+ // Pentest data (may be null/empty)
50
+ const pentest = pentestData || { scans: [], templates: [], totalFindings: 0, findingsBySeverity: {} };
40
51
  // Check for saved AI analyses
41
52
  // (we embed the latest one if model has it, otherwise empty)
42
53
  const aiAnalysis = ''; // Will be loaded from .guardlink/analyses/ by CLI
@@ -64,16 +75,32 @@ ${CSS_CONTENT}
64
75
  <span class="badge">Threat Model</span>
65
76
  </div>
66
77
  <div class="topnav-right">
67
- <div class="tn-stat"><span>Assets</span> <span class="tn-v blue">${stats.assets}</span></div>
68
- <div class="tn-stat"><span>Open</span> <span class="tn-v red">${unmitigated.length}</span></div>
69
- <div class="tn-stat"><span>Controls</span> <span class="tn-v green">${stats.controls}</span></div>
70
- <div class="tn-stat"><span>Coverage</span> <span class="tn-v ${stats.coveragePercent >= 70 ? 'green' : stats.coveragePercent >= 40 ? 'yellow' : 'red'}">${stats.coveragePercent}%</span></div>
78
+ <div class="topnav-metrics">
79
+ <div class="tn-stat"><span class="tn-k">Assets</span> <span class="tn-v blue">${stats.assets}</span></div>
80
+ <div class="tn-stat"><span class="tn-k">Open</span> <span class="tn-v red">${unmitigated.length}</span></div>
81
+ <div class="tn-stat"><span class="tn-k">Controls</span> <span class="tn-v green">${stats.controls}</span></div>
82
+ <div class="tn-stat"><span class="tn-k">Coverage</span> <span class="tn-v ${stats.coveragePercent >= 70 ? 'green' : stats.coveragePercent >= 40 ? 'yellow' : 'red'}">${stats.coveragePercent}%</span></div>
83
+ </div>
84
+ ${featureNames.length > 0 ? ` <div class="feature-filter-wrap">
85
+ <select id="featureFilter" class="feature-filter-select" onchange="applyFeatureFilter(this.value)" title="Filter by feature">
86
+ <option value="">All Features</option>
87
+ ${featureNames.map(f => ` <option value="${esc(f)}">${esc(f)}</option>`).join('\n')}
88
+ </select>
89
+ </div>` : ''}
71
90
  <button id="themeToggle" onclick="toggleTheme()" title="Toggle light/dark mode">
72
91
  <span class="icon-sun">☀️</span><span class="icon-moon">🌙</span>
73
92
  </button>
74
93
  </div>
75
94
  </div>
76
95
 
96
+ <!-- Feature filter banner -->
97
+ <div id="feature-banner" class="feature-banner">
98
+ <span>Filtered to feature:</span>
99
+ <strong id="feature-banner-name"></strong>
100
+ <span id="feature-banner-files" class="feature-banner-files"></span>
101
+ <button class="feature-banner-clear" onclick="document.getElementById('featureFilter').value='';applyFeatureFilter('')">Clear Filter</button>
102
+ </div>
103
+
77
104
  <div class="layout">
78
105
 
79
106
  <!-- ═══════════ SIDEBAR ═══════════ -->
@@ -81,6 +108,7 @@ ${CSS_CONTENT}
81
108
  <div class="sidebar-nav">
82
109
  <a class="active" onclick="showSection('summary',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2l6 4v6l-6 4-6-4V6l6-4z"/></svg></span> <span class="nav-text">Executive Summary</span></a>
83
110
  <a onclick="showSection('ai-analysis',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l2 5h5l-4 3 2 5-5-3-5 3 2-5-4-3h5l2-5z"/></svg></span> <span class="nav-text">Threat Reports</span></a>
111
+ <a onclick="showSection('pentest',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1a7 7 0 100 14A7 7 0 008 1zm0 2a1.5 1.5 0 110 3 1.5 1.5 0 010-3zM6 7h4v1.5H8.5V13h-1V8.5H6V7z"/></svg></span> <span class="nav-text">Pentest Findings</span></a>
84
112
  <a onclick="showSection('threats',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1L1 15h14L8 1zm0 4l3 8H5l3-8z"/></svg></span> <span class="nav-text">Threats &amp; Exposures</span></a>
85
113
  <a onclick="showSection('diagrams',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="1.5" fill="none"/><circle cx="8" cy="8" r="2"/></svg></span> <span class="nav-text">Diagrams</span></a>
86
114
  <a onclick="showSection('code',this)"><span class="nav-icon"><svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M5 4L1 8l4 4v-2L3 8l2-2V4zm6 0v2l2 2-2 2v2l4-4-4-4z"/></svg></span> <span class="nav-text">Code &amp; Annotations</span></a>
@@ -99,8 +127,9 @@ ${CSS_CONTENT}
99
127
 
100
128
  ${renderSummaryPage(stats, severity, riskScore, unmitigated, exposures, model, mitigatedCount, mitigationCoveragePercent)}
101
129
  ${renderAIAnalysisPage(analyses || [])}
102
- ${renderThreatsPage(exposures, model)}
103
- ${renderDiagramsPage(threatGraph, dataFlow, attackSurface)}
130
+ ${renderPentestPage(pentest)}
131
+ ${renderThreatsPage(exposures, confirmedRows, model)}
132
+ ${renderDiagramsPage(threatGraph, threatGraphFull, dataFlow, attackSurface, topology)}
104
133
  ${renderCodePage(fileAnnotations, model)}
105
134
  ${renderDataPage(model)}
106
135
  ${renderAssetsPage(heatmap)}
@@ -123,9 +152,12 @@ ${renderAssetsPage(heatmap)}
123
152
  const fileAnnotations = ${JSON.stringify(fileAnnotations).replace(/<\//g, '<\\/')};
124
153
  const analysisData = ${JSON.stringify(analysisData).replace(/<\//g, '<\\/')};
125
154
  const exposuresData = ${JSON.stringify(exposures).replace(/<\//g, '<\\/')};
155
+ const confirmedData = ${JSON.stringify(confirmedRows).replace(/<\//g, '<\\/')};
126
156
  const savedAnalyses = ${JSON.stringify(analyses || []).replace(/<\//g, '<\\/')};
157
+ const pentestData = ${JSON.stringify(pentest).replace(/<\//g, '<\\/')};
127
158
  const heatmapData = ${JSON.stringify(heatmap).replace(/<\//g, '<\\/')};
128
159
  const threatModel = ${JSON.stringify(model).replace(/<\//g, '<\\/')};
160
+ const topologyData = ${JSON.stringify(topology).replace(/<\//g, '<\\/')};
129
161
  /* ===== SECTION NAV ===== */
130
162
  function showSection(id, el) {
131
163
  document.querySelectorAll('.section-content').forEach(s => s.classList.remove('active'));
@@ -134,8 +166,8 @@ function showSection(id, el) {
134
166
  if (sec) sec.classList.add('active');
135
167
  if (el) el.classList.add('active');
136
168
  closeDrawer();
137
- if (id === 'diagrams' && !window._mermaidRendered) {
138
- setTimeout(() => { renderMermaid(); }, 100);
169
+ if (id === 'diagrams') {
170
+ setTimeout(() => { renderActiveDiagram(); }, 100);
139
171
  }
140
172
  if (id === 'ai-analysis' && !window._aiAnalysisRendered) {
141
173
  renderAIAnalysis();
@@ -185,6 +217,17 @@ function openDrawer(type, idx) {
185
217
  h += sec('Asset', '<code>' + esc(e.asset) + '</code>');
186
218
  if (e.description) h += sec('Description', esc(e.description));
187
219
  h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(e.file) + ':' + e.line + '</span>');
220
+ } else if (type === 'confirmed') {
221
+ const c = confirmedData[idx];
222
+ title.textContent = c.threat + ' (Confirmed)';
223
+ h += '<div style="background:var(--badge-red-bg);border:1px solid var(--sev-crit);border-radius:6px;padding:.6rem;margin-bottom:1rem"><div style="font-size:.82rem;font-weight:700;color:var(--sev-crit)">🔴 CONFIRMED EXPLOITABLE</div><div style="font-size:.75rem;margin-top:.2rem;color:var(--muted)">Verified through testing — not a false positive</div></div>';
224
+ h += sec('Severity', '<span class="fc-sev ' + sevCls(c.severity) + '">' + esc(c.severity) + '</span>');
225
+ h += sec('Asset', '<code>' + esc(c.asset) + '</code>');
226
+ h += sec('Threat', '<code>' + esc(c.threat) + '</code>');
227
+ if (c.description) h += sec('Evidence', esc(c.description));
228
+ if (c.external_refs && c.external_refs.length) h += sec('References', c.external_refs.map(r => '<code>' + esc(r) + '</code>').join(', '));
229
+ h += sec('Location', '<span style="font-family:var(--font-mono);font-size:.78rem;color:var(--muted)">' + esc(c.file) + ':' + c.line + '</span>');
230
+ h += '<div class="d-section" style="margin-top:1rem;padding:.6rem;background:var(--badge-red-bg);border:1px solid var(--sev-crit);border-radius:6px;opacity:.85"><div style="font-size:.78rem;color:var(--sev-crit);font-weight:600">Immediate Action Required</div><div style="font-size:.78rem;margin-top:.3rem">This threat has been verified exploitable. Apply a <code>@mitigates</code> control urgently, or <code>@accepts</code> with explicit risk sign-off from security.</div></div>';
188
231
  } else if (type === 'exposure') {
189
232
  const e = exposuresData[idx];
190
233
  title.textContent = e.threat;
@@ -409,6 +452,110 @@ function openAnnotationDrawer(fileIdx, annIdx) {
409
452
  document.getElementById('drawer-overlay').classList.add('open');
410
453
  }
411
454
 
455
+ function openPentestDrawer(scanIdx, findingIdx) {
456
+ // Mirror of server-side formatConfidence — keep these two in sync.
457
+ // CXG emits confidence as integer (most versions), severity-style string
458
+ // ("high"), or missing entirely. Render whatever it is, never crash.
459
+ function formatConf(v) {
460
+ if (v == null || v === '') return '\u2014';
461
+ if (typeof v === 'number' && isFinite(v)) {
462
+ return Math.max(0, Math.min(100, Math.round(v))) + '%';
463
+ }
464
+ if (typeof v === 'string') {
465
+ var t = v.trim();
466
+ if (!t) return '\u2014';
467
+ var m = t.match(/^(-?\d+(?:\.\d+)?)\s*%?$/);
468
+ if (m) return Math.max(0, Math.min(100, Math.round(parseFloat(m[1])))) + '%';
469
+ return t.toUpperCase();
470
+ }
471
+ return '\u2014';
472
+ }
473
+ var title = document.getElementById('drawer-title');
474
+ var body = document.getElementById('drawer-body');
475
+ var scan = pentestData.scans[scanIdx];
476
+ if (!scan) return;
477
+ var f = scan.findings[findingIdx];
478
+ if (!f) return;
479
+ title.textContent = f.title;
480
+ var h = '';
481
+ var sevColor = f.severity === 'critical' ? 'var(--sev-crit)' : f.severity === 'high' ? 'var(--sev-high)' : f.severity === 'medium' ? 'var(--sev-med)' : 'var(--sev-low)';
482
+ h += sec('Severity', '<span style="color:' + sevColor + ';font-weight:600;text-transform:uppercase">' + esc(f.severity) + '</span>');
483
+ h += sec('Confidence', '<span style="font-weight:600">' + formatConf(f.confidence) + '</span>');
484
+ h += sec('Template', '<code>' + esc(f.template_id) + '</code>');
485
+ if (f.cwe_ids && f.cwe_ids.length) h += sec('CWE', f.cwe_ids.map(function(c){return '<code>' + esc(c) + '</code>'}).join(', '));
486
+ h += sec('Description', '<div style="line-height:1.5">' + esc(f.description) + '</div>');
487
+
488
+ // Evidence section
489
+ if (f.evidence) {
490
+ h += '<div class="sub-h" style="margin-top:1rem">Evidence</div>';
491
+ if (f.evidence.request) {
492
+ h += sec('Request / Payload', '<div class="code-block" style="max-height:200px;overflow:auto;white-space:pre-wrap">' + esc(String(f.evidence.request).slice(0, 2000)) + '</div>');
493
+ }
494
+ if (f.evidence.response) {
495
+ h += sec('Response / Output', '<div class="code-block" style="max-height:200px;overflow:auto;white-space:pre-wrap">' + esc(String(f.evidence.response).slice(0, 2000)) + '</div>');
496
+ }
497
+ if (f.evidence.matched_patterns && f.evidence.matched_patterns.length) {
498
+ h += sec('Matched Patterns', f.evidence.matched_patterns.map(function(p){return '<span class="ann-badge ann-threat" style="margin-right:4px;margin-bottom:2px">' + esc(String(p)) + '</span>'}).join(''));
499
+ }
500
+ if (f.evidence.data && Object.keys(f.evidence.data).length > 0) {
501
+ var dataHtml = '<table style="font-size:.75rem"><tbody>';
502
+ Object.keys(f.evidence.data).forEach(function(k) {
503
+ var val = f.evidence.data[k];
504
+ var vs = typeof val === 'string' ? val : JSON.stringify(val);
505
+ if (vs && vs.length > 500) vs = vs.slice(0, 500) + '...';
506
+ dataHtml += '<tr><td style="font-weight:600;white-space:nowrap;vertical-align:top;padding-right:.5rem">' + esc(k) + '</td><td style="word-break:break-all">' + esc(vs) + '</td></tr>';
507
+ });
508
+ dataHtml += '</tbody></table>';
509
+ h += sec('Evidence Data', dataHtml);
510
+ }
511
+ }
512
+
513
+ if (f.remediation) h += sec('Remediation', '<div style="line-height:1.5;white-space:pre-line">' + esc(f.remediation) + '</div>');
514
+ h += sec('Scan ID', '<code style="font-size:.72rem">' + esc(scan.scan_id) + '</code>');
515
+ h += sec('Timestamp', esc(f.timestamp || scan.completed_at || ''));
516
+
517
+ body.innerHTML = h;
518
+ document.getElementById('drawer').classList.add('open');
519
+ document.getElementById('drawer-overlay').classList.add('open');
520
+ }
521
+
522
+ function openTemplateDrawer(idx) {
523
+ var title = document.getElementById('drawer-title');
524
+ var body = document.getElementById('drawer-body');
525
+ var t = pentestData.templates[idx];
526
+ if (!t) return;
527
+ title.textContent = t.id;
528
+ var h = '';
529
+ h += sec('File', '<code>' + esc(t.filename) + '</code>');
530
+ h += sec('Language', '<span class="ann-badge ann-control">' + esc(t.language) + '</span>');
531
+ h += sec('Severity', '<span class="fc-sev ' + (t.severity === 'critical' ? 'crit' : t.severity === 'high' ? 'high' : t.severity === 'medium' ? 'med' : 'low') + '">' + esc(t.severity) + '</span>');
532
+ if (t.tags && t.tags.length) h += sec('Tags', t.tags.map(function(tag){return '<span class="ann-badge ann-asset" style="margin-right:4px">' + esc(tag) + '</span>'}).join(''));
533
+
534
+ // Find related findings
535
+ var relatedFindings = [];
536
+ pentestData.scans.forEach(function(scan) {
537
+ scan.findings.forEach(function(f) {
538
+ if (f.template_id === t.id) relatedFindings.push(f);
539
+ });
540
+ });
541
+ if (relatedFindings.length > 0) {
542
+ h += '<div class="sub-h" style="margin-top:1rem;color:var(--red)">Findings from this Template (' + relatedFindings.length + ')</div>';
543
+ relatedFindings.forEach(function(f) {
544
+ var sc = f.severity === 'critical' ? 'var(--sev-crit)' : f.severity === 'high' ? 'var(--sev-high)' : f.severity === 'medium' ? 'var(--sev-med)' : 'var(--sev-low)';
545
+ h += '<div style="background:var(--surface2);border:1px solid var(--border);border-left:3px solid ' + sc + ';padding:0.5rem 0.8rem;border-radius:4px;margin-bottom:0.5rem">';
546
+ h += '<strong>' + esc(f.title) + '</strong>';
547
+ h += '<div style="font-size:.75rem;color:var(--muted);margin-top:.2rem">' + esc(f.description.slice(0, 200)) + '</div>';
548
+ h += '</div>';
549
+ });
550
+ } else {
551
+ h += sec('Findings', '<span class="empty-state">No findings from this template yet — run CXG scan to test</span>');
552
+ }
553
+
554
+ body.innerHTML = h;
555
+ document.getElementById('drawer').classList.add('open');
556
+ document.getElementById('drawer-overlay').classList.add('open');
557
+ }
558
+
412
559
  function closeDrawer() {
413
560
  document.getElementById('drawer').classList.remove('open');
414
561
  document.getElementById('drawer-overlay').classList.remove('open');
@@ -425,6 +572,289 @@ function sevCls(s) {
425
572
  return 'unset';
426
573
  }
427
574
 
575
+ /* ===== FEATURE FILTER ===== */
576
+ var _activeFeature = '';
577
+
578
+ function _featureFilesFor(featureName) {
579
+ var files = new Set();
580
+ if (!featureName) return files;
581
+ if (threatModel.features) {
582
+ threatModel.features.forEach(function(f) {
583
+ if (f.feature.toLowerCase() === featureName.toLowerCase()) {
584
+ files.add(f.location.file);
585
+ }
586
+ });
587
+ }
588
+ return files;
589
+ }
590
+
591
+ function applyFeatureFilter(featureName) {
592
+ _activeFeature = featureName;
593
+ var banner = document.getElementById('feature-banner');
594
+
595
+ if (!featureName) {
596
+ // ── Clear filter ──────────────────────────────────────────────
597
+ if (banner) banner.style.display = 'none';
598
+ document.querySelectorAll('[data-ff]').forEach(function(el) { el.style.display = ''; });
599
+ document.querySelectorAll('[data-ff-asset]').forEach(function(el) { el.style.display = ''; });
600
+ _restoreFullStats();
601
+ return;
602
+ }
603
+
604
+ // ── Compute matching file set ─────────────────────────────────
605
+ var featureFiles = _featureFilesFor(featureName);
606
+
607
+ // ── Show banner ───────────────────────────────────────────────
608
+ if (banner) {
609
+ banner.style.display = 'flex';
610
+ document.getElementById('feature-banner-name').textContent = featureName;
611
+ document.getElementById('feature-banner-files').textContent = featureFiles.size + ' file(s)';
612
+ }
613
+
614
+ // ── Filter rows/cards by file ─────────────────────────────────
615
+ document.querySelectorAll('[data-ff]').forEach(function(el) {
616
+ var file = el.getAttribute('data-ff');
617
+ // Empty data-ff means the annotation had no location — keep visible
618
+ el.style.display = (!file || featureFiles.has(file)) ? '' : 'none';
619
+ });
620
+
621
+ // ── Filter asset heatmap cells by asset name ──────────────────
622
+ // An asset belongs to the feature if any of the feature files contains
623
+ // an annotation that references that asset.
624
+ var featureAssets = new Set();
625
+ exposuresData.forEach(function(e) { if (featureFiles.has(e.file)) { featureAssets.add(e.asset); } });
626
+ threatModel.flows.forEach(function(f) {
627
+ if (f.location && featureFiles.has(f.location.file)) {
628
+ featureAssets.add(f.source); featureAssets.add(f.target);
629
+ }
630
+ });
631
+ threatModel.exposures.forEach(function(e) {
632
+ if (e.location && featureFiles.has(e.location.file)) featureAssets.add(e.asset);
633
+ });
634
+ threatModel.mitigations.forEach(function(m) {
635
+ if (m.location && featureFiles.has(m.location.file)) featureAssets.add(m.asset);
636
+ });
637
+
638
+ document.querySelectorAll('[data-ff-asset]').forEach(function(el) {
639
+ var asset = el.getAttribute('data-ff-asset');
640
+ el.style.display = featureAssets.has(asset) ? '' : 'none';
641
+ });
642
+
643
+ // ── Recompute & update all live stats ─────────────────────────
644
+ _updateStatsForFilter(featureFiles);
645
+ }
646
+
647
+ function _updateStatsForFilter(featureFiles) {
648
+ // Compute filtered exposure subsets from the raw data arrays
649
+ var visExp = exposuresData.filter(function(e) { return !featureFiles.size || featureFiles.has(e.file); });
650
+ var visOpen = visExp.filter(function(e) { return !e.mitigated && !e.accepted; });
651
+ var visMit = visExp.filter(function(e) { return e.mitigated; });
652
+
653
+ var sev = { critical: 0, high: 0, medium: 0, low: 0, unset: 0 };
654
+ visExp.forEach(function(e) {
655
+ var s = (e.severity || '').toLowerCase();
656
+ if (s === 'critical' || s === 'p0') sev.critical++;
657
+ else if (s === 'high' || s === 'p1') sev.high++;
658
+ else if (s === 'medium' || s === 'p2') sev.medium++;
659
+ else if (s === 'low' || s === 'p3') sev.low++;
660
+ else sev.unset++;
661
+ });
662
+
663
+ var totalExp = visExp.length;
664
+ var mitPct = totalExp > 0 ? Math.round(visMit.length / totalExp * 100) : 0;
665
+
666
+ // ── Top nav ───────────────────────────────────────────────────
667
+ var tnStats = document.querySelectorAll('.tn-stat');
668
+ tnStats.forEach(function(s) {
669
+ var label = s.querySelector('span:first-child');
670
+ var val = s.querySelector('.tn-v');
671
+ if (!label || !val) return;
672
+ var lbl = label.textContent.trim();
673
+ if (lbl === 'Open') val.textContent = visOpen.length;
674
+ if (lbl === 'Coverage') {
675
+ val.textContent = mitPct + '%';
676
+ val.className = 'tn-v ' + (mitPct >= 70 ? 'green' : mitPct >= 40 ? 'yellow' : 'red');
677
+ }
678
+ });
679
+
680
+ // ── Summary page stats grid ───────────────────────────────────
681
+ _setStat('Open Threats', visOpen.length);
682
+ _setStat('Mitigated', visMit.length);
683
+
684
+ // ── Coverage bar ──────────────────────────────────────────────
685
+ var covPct = document.querySelector('.coverage-pct');
686
+ if (covPct) {
687
+ covPct.textContent = mitPct + '%';
688
+ covPct.className = 'coverage-pct ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
689
+ }
690
+ var covLabel = document.querySelector('.posture-fill')?.parentElement?.nextElementSibling;
691
+ var covFill = document.querySelector('.posture-fill');
692
+ if (covFill) {
693
+ covFill.style.width = Math.min(mitPct, 100) + '%';
694
+ covFill.className = 'posture-fill ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
695
+ }
696
+ // Update "X of Y exposures mitigated" label
697
+ document.querySelectorAll('#sec-summary span').forEach(function(sp) {
698
+ if (sp.textContent.includes('exposures mitigated')) {
699
+ sp.textContent = visMit.length + ' of ' + totalExp + ' exposures mitigated';
700
+ }
701
+ });
702
+
703
+ // ── Severity bars ─────────────────────────────────────────────
704
+ _updateSevBar('Critical', sev.critical, totalExp);
705
+ _updateSevBar('High', sev.high, totalExp);
706
+ _updateSevBar('Medium', sev.medium, totalExp);
707
+ _updateSevBar('Low', sev.low, totalExp);
708
+ _updateSevBar('Unset', sev.unset, totalExp);
709
+
710
+ // ── Section headings with counts ─────────────────────────────
711
+ _updateHeading('sec-threats', 'Open Threats', visOpen.length);
712
+ _updateHeading('sec-threats', 'Mitigated Threats', visMit.length);
713
+ _updateHeading('sec-threats', 'All Exposures', totalExp);
714
+
715
+ // ── Risk banner (recompute grade) ─────────────────────────────
716
+ var visConf = confirmedData.filter(function(c) { return !featureFiles.size || featureFiles.has(c.file); });
717
+ _updateRiskBanner(sev, visOpen.length, totalExp, visConf.length);
718
+ }
719
+
720
+ function _restoreFullStats() {
721
+ // Restore all counts from the original full data sets
722
+ var allOpen = exposuresData.filter(function(e) { return !e.mitigated && !e.accepted; });
723
+ var allMit = exposuresData.filter(function(e) { return e.mitigated; });
724
+ var totalExp = exposuresData.length;
725
+ var mitPct = totalExp > 0 ? Math.round(allMit.length / totalExp * 100) : 0;
726
+
727
+ var sev = { critical: 0, high: 0, medium: 0, low: 0, unset: 0 };
728
+ exposuresData.forEach(function(e) {
729
+ var s = (e.severity || '').toLowerCase();
730
+ if (s === 'critical' || s === 'p0') sev.critical++;
731
+ else if (s === 'high' || s === 'p1') sev.high++;
732
+ else if (s === 'medium' || s === 'p2') sev.medium++;
733
+ else if (s === 'low' || s === 'p3') sev.low++;
734
+ else sev.unset++;
735
+ });
736
+
737
+ // Top nav
738
+ var tnStats = document.querySelectorAll('.tn-stat');
739
+ tnStats.forEach(function(s) {
740
+ var label = s.querySelector('span:first-child');
741
+ var val = s.querySelector('.tn-v');
742
+ if (!label || !val) return;
743
+ var lbl = label.textContent.trim();
744
+ if (lbl === 'Open') val.textContent = allOpen.length;
745
+ if (lbl === 'Coverage') {
746
+ val.textContent = mitPct + '%';
747
+ val.className = 'tn-v ' + (mitPct >= 70 ? 'green' : mitPct >= 40 ? 'yellow' : 'red');
748
+ }
749
+ });
750
+
751
+ _setStat('Open Threats', allOpen.length);
752
+ _setStat('Mitigated', allMit.length);
753
+
754
+ var covPct = document.querySelector('.coverage-pct');
755
+ if (covPct) {
756
+ covPct.textContent = mitPct + '%';
757
+ covPct.className = 'coverage-pct ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
758
+ }
759
+ var covFill = document.querySelector('.posture-fill');
760
+ if (covFill) {
761
+ covFill.style.width = Math.min(mitPct, 100) + '%';
762
+ covFill.className = 'posture-fill ' + (mitPct >= 70 ? 'good' : mitPct >= 40 ? 'warn' : 'bad');
763
+ }
764
+ document.querySelectorAll('#sec-summary span').forEach(function(sp) {
765
+ if (sp.textContent.includes('exposures mitigated')) {
766
+ sp.textContent = allMit.length + ' of ' + totalExp + ' exposures mitigated';
767
+ }
768
+ });
769
+
770
+ _updateSevBar('Critical', sev.critical, totalExp);
771
+ _updateSevBar('High', sev.high, totalExp);
772
+ _updateSevBar('Medium', sev.medium, totalExp);
773
+ _updateSevBar('Low', sev.low, totalExp);
774
+ _updateSevBar('Unset', sev.unset, totalExp);
775
+
776
+ _updateHeading('sec-threats', 'Open Threats', allOpen.length);
777
+ _updateHeading('sec-threats', 'Mitigated Threats', allMit.length);
778
+ _updateHeading('sec-threats', 'All Exposures', totalExp);
779
+
780
+ _updateRiskBanner(sev, allOpen.length, totalExp, confirmedData.length);
781
+ }
782
+
783
+ /* ── Helpers ──────────────────────────────────────────────────────── */
784
+
785
+ function _setStat(label, value) {
786
+ document.querySelectorAll('.stat-card').forEach(function(card) {
787
+ var lbl = card.querySelector('.label');
788
+ var val = card.querySelector('.value');
789
+ if (lbl && val && lbl.textContent.trim() === label) {
790
+ val.textContent = value;
791
+ }
792
+ });
793
+ }
794
+
795
+ function _updateSevBar(label, count, total) {
796
+ var pct = total > 0 ? Math.round(count / total * 100) : 0;
797
+ document.querySelectorAll('.sev-row').forEach(function(row) {
798
+ var lbl = row.querySelector('.sev-label');
799
+ if (!lbl || lbl.textContent.trim() !== label) return;
800
+ var fill = row.querySelector('.sev-fill');
801
+ var cnt = row.querySelector('.sev-count');
802
+ if (fill) fill.style.width = pct + '%';
803
+ if (cnt) cnt.textContent = count;
804
+ });
805
+ }
806
+
807
+ function _updateHeading(sectionId, prefix, count) {
808
+ var sec = document.getElementById(sectionId);
809
+ if (!sec) return;
810
+ sec.querySelectorAll('.sub-h').forEach(function(h) {
811
+ if (h.textContent.trim().startsWith(prefix)) {
812
+ // Replace trailing (N) count
813
+ h.textContent = h.textContent.replace(/\(\d+\)$/, '(' + count + ')').replace(/\s+\d+$/, ' ' + count);
814
+ }
815
+ });
816
+ }
817
+
818
+ function _updateRiskBanner(sev, openCount, totalExp, confirmedCount) {
819
+ var grade, label, summary;
820
+ if (confirmedCount > 0) {
821
+ grade = 'F'; label = 'Critical Risk';
822
+ summary = confirmedCount + ' confirmed exploitable finding(s) — immediate remediation required';
823
+ } else if (sev.critical > 0) {
824
+ grade = 'F'; label = 'Critical Risk';
825
+ summary = sev.critical + ' critical exposure(s) require immediate attention';
826
+ } else if (sev.high >= 3 || openCount >= 5) {
827
+ grade = 'D'; label = 'High Risk';
828
+ summary = openCount + ' unmitigated exposure(s), ' + sev.high + ' high severity';
829
+ } else if (sev.high >= 1 || openCount >= 3) {
830
+ grade = 'C'; label = 'Moderate Risk';
831
+ summary = openCount + ' unmitigated exposure(s) need remediation';
832
+ } else if (openCount >= 1) {
833
+ grade = 'B'; label = 'Low Risk';
834
+ summary = openCount + ' minor unmitigated exposure(s)';
835
+ } else if (totalExp === 0) {
836
+ grade = 'A'; label = 'Excellent';
837
+ summary = 'No exposures detected — consider adding more annotations';
838
+ } else {
839
+ grade = 'A'; label = 'Excellent';
840
+ summary = 'All exposures mitigated or accepted';
841
+ }
842
+
843
+ var banner = document.querySelector('.risk-banner');
844
+ if (!banner) return;
845
+ // Update grade class
846
+ banner.className = banner.className.replace(/risk-[a-z]/g, 'risk-' + grade.toLowerCase());
847
+ var gradeEl = banner.querySelector('.risk-grade');
848
+ if (gradeEl) gradeEl.textContent = grade;
849
+ var detail = banner.querySelector('.risk-detail');
850
+ if (detail) {
851
+ var strong = detail.querySelector('strong');
852
+ var span = detail.querySelector('span');
853
+ if (strong) strong.textContent = label;
854
+ if (span) span.textContent = summary;
855
+ }
856
+ }
857
+
428
858
  /* ===== THEME ===== */
429
859
  function toggleTheme() {
430
860
  const html = document.documentElement;
@@ -433,7 +863,7 @@ function toggleTheme() {
433
863
  // Re-render mermaid with new theme
434
864
  window._mermaidRendered = false;
435
865
  if (document.getElementById('sec-diagrams').classList.contains('active')) {
436
- renderMermaid();
866
+ renderActiveDiagram();
437
867
  }
438
868
  }
439
869
 
@@ -444,8 +874,529 @@ function switchDiagramTab(id, btn) {
444
874
  const panel = document.getElementById('dtab-' + id);
445
875
  if (panel) panel.classList.add('active');
446
876
  if (btn) btn.classList.add('active');
447
- // Always re-render mermaid for newly visible panel
448
- setTimeout(() => { renderMermaidPanel(panel); }, 50);
877
+ setTimeout(() => { renderActiveDiagram(); }, 50);
878
+ }
879
+
880
+ function toggleThreatGraphAll(btn) {
881
+ const panel = document.getElementById('dtab-threat-graph');
882
+ if (!panel) return;
883
+ const filtered = panel.querySelector('.mermaid[data-variant="filtered"]');
884
+ const full = panel.querySelector('.mermaid[data-variant="full"]');
885
+ if (!filtered || !full) return;
886
+ const showFull = full.style.display === 'none';
887
+ filtered.style.display = showFull ? 'none' : '';
888
+ full.style.display = showFull ? '' : 'none';
889
+ if (btn) {
890
+ btn.classList.toggle('active', showFull);
891
+ btn.textContent = showFull ? 'High/Critical only' : 'All severities';
892
+ }
893
+ // Force mermaid to re-render the now-visible variant
894
+ panel._diagramZoom = null;
895
+ setTimeout(() => { renderActiveDiagram(); }, 50);
896
+ }
897
+
898
+ function diagramZoom(action) {
899
+ const panel = document.querySelector('.diagram-panel.active');
900
+ if (panel && panel.id === 'dtab-risk-topology' && window._topologyZoom) {
901
+ const state = window._topologyZoom;
902
+ if (action === 'fit') {
903
+ state.svg.transition().duration(420).call(state.zoom.transform, d3.zoomIdentity);
904
+ return;
905
+ }
906
+ const topologyFactor = action === 'in' ? 1.18 : 1 / 1.18;
907
+ state.svg.transition().duration(220).call(state.zoom.scaleBy, topologyFactor);
908
+ return;
909
+ }
910
+ if (!panel || !panel._diagramZoom) return;
911
+ const state = panel._diagramZoom;
912
+ const svg = state.svg;
913
+ const zoom = state.zoom;
914
+ if (!svg || !zoom) return;
915
+
916
+ if (action === 'fit') {
917
+ svg.transition().duration(420).call(zoom.transform, d3.zoomIdentity);
918
+ return;
919
+ }
920
+ const factor = action === 'in' ? 1.2 : 1 / 1.2;
921
+ svg.transition().duration(220).call(zoom.scaleBy, factor);
922
+ }
923
+
924
+ /* ===== TOPOLOGY DIAGRAM ===== */
925
+ function topologyColor(name, fallback) {
926
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
927
+ }
928
+
929
+ function topologyEnabledKinds() {
930
+ const boxes = document.querySelectorAll('[data-topology-kind]');
931
+ const enabled = new Set();
932
+ boxes.forEach(box => { if (box.checked) enabled.add(box.getAttribute('data-topology-kind')); });
933
+ return enabled;
934
+ }
935
+
936
+ function topologyEnabledLinkKinds() {
937
+ const boxes = document.querySelectorAll('[data-topology-link]');
938
+ if (boxes.length === 0) return null; // null = "all enabled"
939
+ const enabled = new Set();
940
+ boxes.forEach(box => { if (box.checked) enabled.add(box.getAttribute('data-topology-link')); });
941
+ return enabled;
942
+ }
943
+
944
+ function topologyShort(s, max) {
945
+ s = String(s || '');
946
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
947
+ }
948
+
949
+ // Severity- and risk-scaled radius. Bigger = more risk, so the eye goes to it first.
950
+ function topologyRadius(node) {
951
+ const base = node.kind === 'asset' ? 20 : node.kind === 'threat' ? 18 : 15;
952
+ const sevBoost = (node.severity === 'critical' || node.severity === 'p0') ? 6
953
+ : (node.severity === 'high' || node.severity === 'p1') ? 4
954
+ : (node.severity === 'medium' || node.severity === 'p2') ? 2 : 0;
955
+ const riskLoad = Math.min(10, Math.sqrt((node.openExposures || 0) * 2 + (node.confirmed || 0) * 5) * 2.8);
956
+ return base + sevBoost + riskLoad;
957
+ }
958
+
959
+ function topologyNodeClass(node) {
960
+ const classes = [
961
+ 'topology-node',
962
+ 'topology-' + node.kind,
963
+ 'topology-status-' + (node.status || 'none'),
964
+ 'topology-sev-' + (node.severity || 'unset'),
965
+ ];
966
+ if (node.confirmed > 0) classes.push('topology-has-confirmed');
967
+ if (node.openExposures > 0) classes.push('topology-has-open');
968
+ if (node.mitigations > 0 && node.openExposures === 0 && node.confirmed === 0) classes.push('topology-fully-covered');
969
+ return classes.join(' ');
970
+ }
971
+
972
+ function topologyLinkClass(link) {
973
+ return 'topology-link topology-link-' + link.kind + ' topology-link-status-' + (link.status || 'none');
974
+ }
975
+
976
+ function topologyLabelFor(id) {
977
+ const n = topologyData.nodes.find(node => node.id === id);
978
+ return n ? n.label : id;
979
+ }
980
+
981
+ function topologyFiltered() {
982
+ const enabled = topologyEnabledKinds();
983
+ const enabledLinks = topologyEnabledLinkKinds();
984
+ const queryEl = document.getElementById('topologySearch');
985
+ const openOnlyEl = document.getElementById('topologyOpenOnly');
986
+ const query = queryEl ? queryEl.value.trim().toLowerCase() : '';
987
+ const openOnly = openOnlyEl ? openOnlyEl.checked : false;
988
+
989
+ let nodes = topologyData.nodes.filter(node => enabled.has(node.kind));
990
+ if (openOnly) {
991
+ nodes = nodes.filter(node => node.status === 'open' || node.status === 'confirmed' || node.openExposures > 0 || node.confirmed > 0);
992
+ }
993
+ if (query) {
994
+ const direct = new Set(nodes.filter(node => {
995
+ const refs = (node.refs || []).join(' ').toLowerCase();
996
+ return node.label.toLowerCase().includes(query) || refs.includes(query) || node.kind.includes(query);
997
+ }).map(node => node.id));
998
+ const expanded = new Set(direct);
999
+ topologyData.links.forEach(link => {
1000
+ if (direct.has(link.source) || direct.has(link.target)) {
1001
+ expanded.add(link.source);
1002
+ expanded.add(link.target);
1003
+ }
1004
+ });
1005
+ nodes = nodes.filter(node => expanded.has(node.id));
1006
+ }
1007
+
1008
+ const ids = new Set(nodes.map(node => node.id));
1009
+ const links = topologyData.links.filter(link => {
1010
+ if (!ids.has(link.source) || !ids.has(link.target)) return false;
1011
+ if (enabledLinks && !enabledLinks.has(link.kind)) return false;
1012
+ return true;
1013
+ });
1014
+ return { nodes, links };
1015
+ }
1016
+
1017
+ // Lane-based x target: assets left, threats middle, controls right.
1018
+ function topologyLaneX(node, width) {
1019
+ if (node.kind === 'asset') return width * 0.22;
1020
+ if (node.kind === 'threat') return width * 0.5;
1021
+ return width * 0.78;
1022
+ }
1023
+
1024
+ // Vertical lane pull: critical/high float up, low/unset sink down.
1025
+ function topologyLaneY(node, height) {
1026
+ const rank = (node.severity === 'critical' || node.severity === 'p0') ? 0.2
1027
+ : (node.severity === 'high' || node.severity === 'p1') ? 0.35
1028
+ : (node.severity === 'medium' || node.severity === 'p2') ? 0.55
1029
+ : (node.severity === 'low' || node.severity === 'p3') ? 0.7 : 0.55;
1030
+ return height * rank;
1031
+ }
1032
+
1033
+ function topologyNeighborhood(nodeId) {
1034
+ const neighbors = new Set([nodeId]);
1035
+ const linkSet = new Set();
1036
+ topologyData.links.forEach(l => {
1037
+ const s = l.source.id || l.source;
1038
+ const t = l.target.id || l.target;
1039
+ if (s === nodeId || t === nodeId) {
1040
+ neighbors.add(s);
1041
+ neighbors.add(t);
1042
+ linkSet.add(l);
1043
+ }
1044
+ });
1045
+ return { neighbors, linkSet };
1046
+ }
1047
+
1048
+ function showTopologyOverview(nodeCount, linkCount) {
1049
+ const inspector = document.getElementById('topologyInspector');
1050
+ if (!inspector) return;
1051
+ const s = topologyData.summary;
1052
+ const totalExposures = s.open + s.mitigated + s.accepted;
1053
+ const coveragePct = totalExposures === 0 ? 100 : Math.round(((s.mitigated + s.accepted) / totalExposures) * 100);
1054
+ const barCls = coveragePct >= 75 ? 'good' : coveragePct >= 40 ? 'warn' : 'bad';
1055
+ let h = '';
1056
+ h += '<div class="topology-inspector-k">Topology</div>';
1057
+ h += '<h3>Threat Model Map</h3>';
1058
+ h += '<div class="topology-bar-wrap"><div class="topology-bar-label"><span>Coverage</span><strong>' + coveragePct + '%</strong></div>';
1059
+ h += '<div class="topology-bar"><div class="topology-bar-fill ' + barCls + '" style="width:' + coveragePct + '%"></div></div></div>';
1060
+ h += '<div class="topology-detail-grid">';
1061
+ h += '<span>Assets</span><strong>' + s.assets + '</strong>';
1062
+ h += '<span>Threats</span><strong>' + s.threats + '</strong>';
1063
+ h += '<span>Controls</span><strong>' + s.controls + '</strong>';
1064
+ h += '<span>Open</span><strong class="danger">' + s.open + '</strong>';
1065
+ if (s.confirmed) h += '<span>Confirmed</span><strong class="danger">' + s.confirmed + '</strong>';
1066
+ h += '<span>Mitigated</span><strong class="good">' + s.mitigated + '</strong>';
1067
+ if (s.accepted) h += '<span>Accepted</span><strong>' + s.accepted + '</strong>';
1068
+ if (s.criticalAssets) h += '<span>Critical</span><strong class="danger">' + s.criticalAssets + ' asset' + (s.criticalAssets > 1 ? 's' : '') + '</strong>';
1069
+ h += '<span>Visible</span><strong>' + nodeCount + ' / ' + linkCount + '</strong>';
1070
+ h += '</div>';
1071
+ h += '<div class="topology-inspector-note">Hover a node to highlight its neighborhood. Click to pin. Drag to reposition.</div>';
1072
+ inspector.innerHTML = h;
1073
+ }
1074
+
1075
+ function showTopologyDetails(node) {
1076
+ const inspector = document.getElementById('topologyInspector');
1077
+ if (!inspector) return;
1078
+ const related = topologyData.links.filter(link => link.source === node.id || link.target === node.id);
1079
+ const shown = related.slice(0, 12);
1080
+ const mitCoverage = node.exposures > 0 ? Math.round(Math.min(1, node.mitigations / node.exposures) * 100) : (node.mitigations > 0 ? 100 : 0);
1081
+ const barCls = mitCoverage >= 75 ? 'good' : mitCoverage >= 40 ? 'warn' : 'bad';
1082
+
1083
+ let h = '';
1084
+ h += '<div class="topology-inspector-head"><span class="topology-inspector-k">' + esc(node.kind) + '</span>';
1085
+ if (node.confirmed > 0) h += '<span class="topology-badge topology-badge-confirmed">💥 Confirmed</span>';
1086
+ else if (node.openExposures > 0) h += '<span class="topology-badge topology-badge-open">⚠ Open</span>';
1087
+ else if (node.mitigations > 0) h += '<span class="topology-badge topology-badge-covered">✓ Covered</span>';
1088
+ h += '</div>';
1089
+ h += '<h3>' + esc(node.label) + '</h3>';
1090
+
1091
+ if (node.kind === 'asset' && (node.exposures > 0 || node.mitigations > 0)) {
1092
+ h += '<div class="topology-bar-wrap"><div class="topology-bar-label"><span>Mitigation</span><strong>' + mitCoverage + '%</strong></div>';
1093
+ h += '<div class="topology-bar"><div class="topology-bar-fill ' + barCls + '" style="width:' + mitCoverage + '%"></div></div></div>';
1094
+ }
1095
+
1096
+ h += '<div class="topology-detail-grid">';
1097
+ h += '<span>Status</span><strong class="status-' + esc(node.status || 'none') + '">' + esc(node.status || 'none') + '</strong>';
1098
+ h += '<span>Severity</span><strong class="sev-' + esc(node.severity || 'unset') + '">' + esc(node.severity || 'unset') + '</strong>';
1099
+ if (node.exposures) h += '<span>Exposures</span><strong>' + node.exposures + '</strong>';
1100
+ if (node.openExposures) h += '<span>Open</span><strong class="danger">' + node.openExposures + '</strong>';
1101
+ if (node.mitigations) h += '<span>Mitigations</span><strong class="good">' + node.mitigations + '</strong>';
1102
+ if (node.confirmed) h += '<span>Confirmed</span><strong class="danger">' + node.confirmed + '</strong>';
1103
+ if (node.flows) h += '<span>Flows</span><strong>' + node.flows + '</strong>';
1104
+ if (node.riskScore) h += '<span>Risk score</span><strong>' + node.riskScore + '</strong>';
1105
+ if (node.owner) h += '<span>Owner</span><strong>' + esc(node.owner) + '</strong>';
1106
+ h += '</div>';
1107
+ if (node.classifications && node.classifications.length) {
1108
+ h += '<div class="topology-chip-row">' + node.classifications.map(c => '<span>' + esc(c) + '</span>').join('') + '</div>';
1109
+ }
1110
+ if (shown.length) {
1111
+ h += '<div class="topology-related-title">Relationships <em style="color:var(--muted);font-family:var(--font-mono);font-size:.66rem">(' + related.length + ')</em></div>';
1112
+ h += '<div class="topology-related">';
1113
+ shown.forEach(link => {
1114
+ const other = link.source === node.id ? link.target : link.source;
1115
+ const isOut = link.source === node.id;
1116
+ h += '<div class="topology-related-row topology-related-' + esc(link.kind) + '">';
1117
+ h += '<span class="topology-related-kind">' + (isOut ? '→' : '←') + ' ' + esc(link.kind) + '</span>';
1118
+ h += '<span>' + esc(topologyLabelFor(other)) + '</span>';
1119
+ if (link.count > 1) h += '<em>×' + link.count + '</em>';
1120
+ else h += '<em></em>';
1121
+ h += '</div>';
1122
+ });
1123
+ h += '</div>';
1124
+ }
1125
+ inspector.innerHTML = h;
1126
+ }
1127
+
1128
+ function renderTopologyDiagram() {
1129
+ const host = document.getElementById('topologyGraph');
1130
+ if (!host) return;
1131
+ if (typeof d3 === 'undefined') {
1132
+ host.innerHTML = '<div class="empty-state">Topology renderer unavailable.</div>';
1133
+ return;
1134
+ }
1135
+
1136
+ const filtered = topologyFiltered();
1137
+ const nodes = filtered.nodes.map(node => Object.assign({}, node));
1138
+ const links = filtered.links.map(link => Object.assign({}, link));
1139
+ const counter = document.getElementById('topologyVisibleCount');
1140
+ if (counter) counter.textContent = nodes.length + ' nodes · ' + links.length + ' links';
1141
+
1142
+ if (window._topologySimulation) window._topologySimulation.stop();
1143
+ host.innerHTML = '';
1144
+ if (nodes.length === 0) {
1145
+ host.innerHTML = '<div class="empty-state">No matching topology data.</div>';
1146
+ showTopologyOverview(0, 0);
1147
+ return;
1148
+ }
1149
+
1150
+ const bounds = host.getBoundingClientRect();
1151
+ const width = Math.max(760, Math.floor(bounds.width || 960));
1152
+ const height = Math.max(560, Math.min(820, Math.floor(window.innerHeight * 0.66)));
1153
+ const nodeById = new Map(nodes.map(node => [node.id, node]));
1154
+ const linksByNodeId = new Map();
1155
+ nodes.forEach(n => linksByNodeId.set(n.id, new Set()));
1156
+ links.forEach(l => {
1157
+ const s = typeof l.source === 'string' ? l.source : l.source.id;
1158
+ const t = typeof l.target === 'string' ? l.target : l.target.id;
1159
+ if (linksByNodeId.has(s)) linksByNodeId.get(s).add(t);
1160
+ if (linksByNodeId.has(t)) linksByNodeId.get(t).add(s);
1161
+ });
1162
+
1163
+ const svg = d3.select(host).append('svg')
1164
+ .attr('viewBox', '0 0 ' + width + ' ' + height)
1165
+ .attr('width', width)
1166
+ .attr('height', height)
1167
+ .attr('role', 'img')
1168
+ .attr('aria-label', 'Threat model topology');
1169
+ const stage = svg.append('g');
1170
+ const zoom = d3.zoom().scaleExtent([0.25, 3.8]).on('zoom', event => {
1171
+ stage.attr('transform', event.transform);
1172
+ });
1173
+ svg.call(zoom);
1174
+ window._topologyZoom = { svg, zoom };
1175
+
1176
+ const defs = svg.append('defs');
1177
+ // Arrow markers — one per link kind so colour matches the edge.
1178
+ const arrowKinds = [
1179
+ { id: 'exposes', color: topologyColor('--sev-crit', '#ea1d1d') },
1180
+ { id: 'confirmed', color: topologyColor('--sev-crit', '#ea1d1d') },
1181
+ { id: 'mitigates', color: topologyColor('--green', '#33d49d') },
1182
+ { id: 'protects', color: topologyColor('--green', '#33d49d') },
1183
+ { id: 'validates', color: topologyColor('--green', '#33d49d') },
1184
+ { id: 'flows', color: topologyColor('--blue', '#0360a2') },
1185
+ { id: 'boundary', color: topologyColor('--purple', '#7d5cff') },
1186
+ { id: 'accepts', color: topologyColor('--sev-med', '#55899e') },
1187
+ { id: 'transfers', color: topologyColor('--sev-med', '#55899e') },
1188
+ ];
1189
+ arrowKinds.forEach(a => {
1190
+ defs.append('marker')
1191
+ .attr('id', 'topology-arrow-' + a.id)
1192
+ .attr('viewBox', '0 -5 10 10')
1193
+ .attr('refX', 20)
1194
+ .attr('refY', 0)
1195
+ .attr('markerWidth', 6)
1196
+ .attr('markerHeight', 6)
1197
+ .attr('orient', 'auto')
1198
+ .append('path')
1199
+ .attr('d', 'M0,-5L10,0L0,5')
1200
+ .attr('fill', a.color);
1201
+ });
1202
+ // Halo drop-shadow for confirmed/critical nodes
1203
+ const glow = defs.append('filter').attr('id', 'topology-glow').attr('x', '-50%').attr('y', '-50%').attr('width', '200%').attr('height', '200%');
1204
+ glow.append('feGaussianBlur').attr('stdDeviation', '3.5').attr('result', 'blur');
1205
+ const feMerge = glow.append('feMerge');
1206
+ feMerge.append('feMergeNode').attr('in', 'blur');
1207
+ feMerge.append('feMergeNode').attr('in', 'SourceGraphic');
1208
+
1209
+ // Link distances bias flows/boundaries to stretch out; exposures stay tight.
1210
+ const linkDistance = l => {
1211
+ if (l.kind === 'flows' || l.kind === 'boundary') return 160;
1212
+ if (l.kind === 'mitigates' || l.kind === 'protects' || l.kind === 'validates') return 120;
1213
+ if (l.kind === 'transfers') return 140;
1214
+ return 95;
1215
+ };
1216
+
1217
+ const simulation = d3.forceSimulation(nodes)
1218
+ .force('link', d3.forceLink(links).id(node => node.id).distance(linkDistance).strength(0.72))
1219
+ .force('charge', d3.forceManyBody().strength(node => node.kind === 'control' ? -320 : node.kind === 'threat' ? -520 : -460))
1220
+ .force('x', d3.forceX(n => topologyLaneX(n, width)).strength(0.22))
1221
+ .force('y', d3.forceY(n => topologyLaneY(n, height)).strength(0.10))
1222
+ .force('collide', d3.forceCollide().radius(node => topologyRadius(node) + 22).iterations(2));
1223
+ window._topologySimulation = simulation;
1224
+
1225
+ // Lane labels — subtle background text showing the three kind lanes.
1226
+ const laneLabels = stage.append('g').attr('class', 'topology-lane-labels');
1227
+ const lanes = [
1228
+ { label: 'ASSETS', x: width * 0.22 },
1229
+ { label: 'THREATS', x: width * 0.5 },
1230
+ { label: 'CONTROLS', x: width * 0.78 },
1231
+ ];
1232
+ laneLabels.selectAll('text')
1233
+ .data(lanes)
1234
+ .join('text')
1235
+ .attr('class', 'topology-lane-label')
1236
+ .attr('x', d => d.x)
1237
+ .attr('y', 22)
1238
+ .attr('text-anchor', 'middle')
1239
+ .text(d => d.label);
1240
+
1241
+ const link = stage.append('g')
1242
+ .attr('class', 'topology-links')
1243
+ .selectAll('line')
1244
+ .data(links)
1245
+ .join('line')
1246
+ .attr('class', topologyLinkClass)
1247
+ .attr('marker-end', d => 'url(#topology-arrow-' + d.kind + ')');
1248
+ link.append('title').text(d => topologyLabelFor(d.source.id || d.source) + ' → ' + topologyLabelFor(d.target.id || d.target) + ' · ' + d.label);
1249
+
1250
+ const node = stage.append('g')
1251
+ .attr('class', 'topology-nodes')
1252
+ .selectAll('g')
1253
+ .data(nodes)
1254
+ .join('g')
1255
+ .attr('class', topologyNodeClass)
1256
+ .call(d3.drag()
1257
+ .on('start', (event, d) => {
1258
+ if (!event.active) simulation.alphaTarget(0.25).restart();
1259
+ d.fx = d.x;
1260
+ d.fy = d.y;
1261
+ })
1262
+ .on('drag', (event, d) => {
1263
+ d.fx = event.x;
1264
+ d.fy = event.y;
1265
+ })
1266
+ .on('end', (event, d) => {
1267
+ if (!event.active) simulation.alphaTarget(0);
1268
+ d.fx = null;
1269
+ d.fy = null;
1270
+ }));
1271
+
1272
+ // Pulsing halo for confirmed / critical-open nodes
1273
+ node.filter(d => d.confirmed > 0 || ((d.severity === 'critical' || d.severity === 'p0') && d.openExposures > 0))
1274
+ .insert('circle', ':first-child')
1275
+ .attr('class', 'topology-node-halo')
1276
+ .attr('r', d => topologyRadius(d) + 8);
1277
+
1278
+ node.append('circle')
1279
+ .attr('class', 'topology-node-body')
1280
+ .attr('r', topologyRadius);
1281
+
1282
+ // Mitigation coverage ring (arc) — only for assets with both exposures and mitigations
1283
+ node.filter(d => d.kind === 'asset' && d.exposures > 0)
1284
+ .append('path')
1285
+ .attr('class', 'topology-coverage-arc')
1286
+ .attr('d', d => {
1287
+ const mit = d.exposures > 0 ? Math.min(1, d.mitigations / d.exposures) : 0;
1288
+ if (mit <= 0) return '';
1289
+ const r = topologyRadius(d) + 4;
1290
+ const angle = mit * 2 * Math.PI;
1291
+ const x1 = r * Math.sin(0);
1292
+ const y1 = -r * Math.cos(0);
1293
+ const x2 = r * Math.sin(angle);
1294
+ const y2 = -r * Math.cos(angle);
1295
+ const largeArc = mit > 0.5 ? 1 : 0;
1296
+ return 'M 0 ' + (-r) + ' A ' + r + ' ' + r + ' 0 ' + largeArc + ' 1 ' + x2.toFixed(2) + ' ' + y2.toFixed(2);
1297
+ });
1298
+
1299
+ node.append('text')
1300
+ .attr('class', 'topology-node-icon')
1301
+ .attr('text-anchor', 'middle')
1302
+ .attr('dy', '0.35em')
1303
+ .text(d => d.kind === 'asset' ? 'A' : d.kind === 'threat' ? 'T' : 'C');
1304
+ node.append('text')
1305
+ .attr('class', 'topology-node-label')
1306
+ .attr('text-anchor', 'middle')
1307
+ .attr('dy', d => topologyRadius(d) + 16)
1308
+ .text(d => topologyShort(d.label, 24));
1309
+ node.append('title').text(d => d.label + ' · ' + d.kind + ' · ' + d.status + (d.openExposures ? ' · ' + d.openExposures + ' open' : '') + (d.confirmed ? ' · ' + d.confirmed + ' confirmed' : ''));
1310
+
1311
+ // Connected-subgraph dim on hover
1312
+ const applyDim = (activeId) => {
1313
+ if (!activeId) {
1314
+ node.classed('dim', false).classed('emphasis', false);
1315
+ link.classed('dim', false).classed('emphasis', false);
1316
+ return;
1317
+ }
1318
+ const neighbors = new Set([activeId]);
1319
+ links.forEach(l => {
1320
+ const s = l.source.id || l.source;
1321
+ const t = l.target.id || l.target;
1322
+ if (s === activeId || t === activeId) {
1323
+ neighbors.add(s);
1324
+ neighbors.add(t);
1325
+ }
1326
+ });
1327
+ node.classed('dim', n => !neighbors.has(n.id)).classed('emphasis', n => neighbors.has(n.id));
1328
+ link.classed('dim', l => {
1329
+ const s = l.source.id || l.source;
1330
+ const t = l.target.id || l.target;
1331
+ return s !== activeId && t !== activeId;
1332
+ }).classed('emphasis', l => {
1333
+ const s = l.source.id || l.source;
1334
+ const t = l.target.id || l.target;
1335
+ return s === activeId || t === activeId;
1336
+ });
1337
+ };
1338
+
1339
+ node.on('mouseover', (event, d) => {
1340
+ if (window._topologyPinnedId) return;
1341
+ applyDim(d.id);
1342
+ }).on('mouseout', () => {
1343
+ if (window._topologyPinnedId) return;
1344
+ applyDim(null);
1345
+ });
1346
+
1347
+ node.on('click', (event, d) => {
1348
+ event.stopPropagation();
1349
+ window._topologyActiveNodeId = d.id;
1350
+ window._topologyPinnedId = d.id;
1351
+ node.classed('active', n => n.id === d.id);
1352
+ applyDim(d.id);
1353
+ showTopologyDetails(d);
1354
+ });
1355
+ svg.on('click', () => {
1356
+ window._topologyActiveNodeId = null;
1357
+ window._topologyPinnedId = null;
1358
+ node.classed('active', false);
1359
+ applyDim(null);
1360
+ showTopologyOverview(nodes.length, links.length);
1361
+ });
1362
+
1363
+ simulation.on('tick', () => {
1364
+ link
1365
+ .attr('x1', d => d.source.x)
1366
+ .attr('y1', d => d.source.y)
1367
+ .attr('x2', d => d.target.x)
1368
+ .attr('y2', d => d.target.y);
1369
+ node.attr('transform', d => 'translate(' + d.x + ',' + d.y + ')');
1370
+ });
1371
+
1372
+ const active = window._topologyActiveNodeId && nodeById.get(window._topologyActiveNodeId);
1373
+ if (active) {
1374
+ node.classed('active', n => n.id === active.id);
1375
+ applyDim(active.id);
1376
+ showTopologyDetails(active);
1377
+ } else {
1378
+ showTopologyOverview(nodes.length, links.length);
1379
+ }
1380
+ }
1381
+
1382
+ function exportTopologySVG() {
1383
+ const host = document.getElementById('topologyGraph');
1384
+ if (!host) return;
1385
+ const svg = host.querySelector('svg');
1386
+ if (!svg) return;
1387
+ const clone = svg.cloneNode(true);
1388
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
1389
+ const xmlHeader = '<?xml version="1.0" standalone="no"?>';
1390
+ const source = xmlHeader + '\\n' + new XMLSerializer().serializeToString(clone);
1391
+ const blob = new Blob([source], { type: 'image/svg+xml;charset=utf-8' });
1392
+ const url = URL.createObjectURL(blob);
1393
+ const a = document.createElement('a');
1394
+ a.href = url;
1395
+ a.download = 'guardlink-topology.svg';
1396
+ document.body.appendChild(a);
1397
+ a.click();
1398
+ document.body.removeChild(a);
1399
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
449
1400
  }
450
1401
 
451
1402
  /* ===== MERMAID ===== */
@@ -460,17 +1411,23 @@ async function getMermaidInstance() {
460
1411
  startOnLoad: false,
461
1412
  theme: isDark ? 'dark' : 'default',
462
1413
  themeVariables: isDark ? {
463
- primaryColor: '#1a2228', primaryTextColor: '#f0f0f0', primaryBorderColor: '#3b6779',
464
- lineColor: '#55899e', secondaryColor: '#1e2830', tertiaryColor: '#0d1117',
465
- background: '#0a0d10', mainBkg: '#1a2228', nodeBorder: '#3b6779',
466
- clusterBkg: '#0d1117', clusterBorder: '#1f3943', fontSize: '12px', fontFamily: 'var(--font-ui)'
1414
+ primaryColor: '#17272e', primaryTextColor: '#f0f0f0', primaryBorderColor: '#55899e',
1415
+ lineColor: '#6b93a6', secondaryColor: '#1f3943', tertiaryColor: '#0f1b20',
1416
+ background: '#0f1b20', mainBkg: '#17272e', nodeBorder: '#55899e', secondBkg: '#1f3943',
1417
+ clusterBkg: 'rgba(23,39,46,.55)', clusterBorder: '#3b6779',
1418
+ titleColor: '#f0f0f0', edgeLabelBackground: '#0f1b20', labelBackground: '#0f1b20',
1419
+ nodeTextColor: '#f0f0f0',
1420
+ fontSize: '12px', fontFamily: 'Inter, system-ui, sans-serif',
467
1421
  } : {
468
- primaryColor: '#e8f4f8', primaryTextColor: '#1a1a2e', primaryBorderColor: '#94a3b8',
469
- lineColor: '#64748b', secondaryColor: '#f1f5f9', tertiaryColor: '#ffffff',
470
- background: '#ffffff', mainBkg: '#e8f4f8', nodeBorder: '#94a3b8',
471
- clusterBkg: '#f8fafc', clusterBorder: '#cbd5e1', fontSize: '12px', fontFamily: 'var(--font-ui)'
1422
+ primaryColor: '#ffffff', primaryTextColor: '#1f3943', primaryBorderColor: '#55899e',
1423
+ lineColor: '#3b6779', secondaryColor: '#f4f7f8', tertiaryColor: '#ffffff',
1424
+ background: '#ffffff', mainBkg: '#ffffff', nodeBorder: '#55899e', secondBkg: '#e8eef0',
1425
+ clusterBkg: '#f7fafb', clusterBorder: '#d9e4e8',
1426
+ titleColor: '#1f3943', edgeLabelBackground: '#ffffff', labelBackground: '#ffffff',
1427
+ nodeTextColor: '#1f3943',
1428
+ fontSize: '12px', fontFamily: 'Inter, system-ui, sans-serif',
472
1429
  },
473
- flowchart: { curve: 'basis', padding: 15, nodeSpacing: 40, rankSpacing: 50, htmlLabels: false, useMaxWidth: false },
1430
+ flowchart: { curve: 'monotoneX', padding: 20, nodeSpacing: 48, rankSpacing: 62, htmlLabels: false, useMaxWidth: false, defaultRenderer: 'dagre-d3' },
474
1431
  securityLevel: 'loose',
475
1432
  });
476
1433
  return mermaid;
@@ -479,18 +1436,23 @@ async function getMermaidInstance() {
479
1436
  async function renderMermaidPanel(panel) {
480
1437
  if (!panel) return;
481
1438
  const mermaid = await getMermaidInstance();
482
- const els = panel.querySelectorAll('.mermaid');
483
-
1439
+ // Render only the currently visible mermaid block(s). Hidden variants (e.g. the
1440
+ // full threat graph behind the "All severities" toggle) would otherwise fail
1441
+ // getBBox during layout sizing.
1442
+ const allEls = Array.from(panel.querySelectorAll('.mermaid'));
1443
+ const els = allEls.filter(el => el.offsetParent !== null || el.style.display !== 'none');
1444
+ const targets = els.length > 0 ? els : allEls;
1445
+
484
1446
  // Re-run mermaid
485
- els.forEach(el => {
1447
+ targets.forEach(el => {
486
1448
  el.removeAttribute('data-processed');
487
1449
  el.innerHTML = el.getAttribute('data-original') || el.textContent;
488
1450
  });
489
- await mermaid.run({ nodes: Array.from(els) });
1451
+ await mermaid.run({ nodes: targets });
490
1452
 
491
1453
  // Add interactive zoom/pan to the rendered SVG
492
1454
  if (typeof d3 !== 'undefined') {
493
- els.forEach(el => {
1455
+ targets.forEach(el => {
494
1456
  const svg = d3.select(el).select('svg');
495
1457
  if (!svg.empty()) {
496
1458
  const inner = svg.select('.root'); // Mermaid puts everything in a .root group
@@ -501,6 +1463,7 @@ async function renderMermaidPanel(panel) {
501
1463
  inner.attr('transform', e.transform);
502
1464
  });
503
1465
  svg.call(zoom);
1466
+ panel._diagramZoom = { svg, zoom };
504
1467
 
505
1468
  // Preserve natural SVG size so long labels are not clipped.
506
1469
  // Container scrolling handles overflow for large graphs.
@@ -538,6 +1501,16 @@ async function renderMermaid() {
538
1501
  window._mermaidRendered = true;
539
1502
  }
540
1503
 
1504
+ function renderActiveDiagram() {
1505
+ const active = document.querySelector('.diagram-panel.active');
1506
+ if (!active) return;
1507
+ if (active.id === 'dtab-risk-topology') {
1508
+ renderTopologyDiagram();
1509
+ return;
1510
+ }
1511
+ renderMermaidPanel(active).then(() => { window._mermaidRendered = true; });
1512
+ }
1513
+
541
1514
  // Save original diagram source
542
1515
  document.querySelectorAll('.mermaid').forEach(el => {
543
1516
  el.setAttribute('data-original', el.textContent.trim());
@@ -647,6 +1620,7 @@ function renderSummaryPage(stats, severity, risk, unmitigated, exposures, model,
647
1620
  <div class="stats-grid">
648
1621
  ${statCard(stats.assets, 'Assets')}
649
1622
  ${statCard(unmitigated.length, 'Open Threats', 'danger')}
1623
+ ${stats.confirmed > 0 ? statCard(stats.confirmed, 'Confirmed', 'danger') : ''}
650
1624
  ${statCard(mitigatedCount, 'Mitigated', 'success')}
651
1625
  ${statCard(stats.controls, 'Controls', 'success')}
652
1626
  ${statCard(stats.flows, 'Data Flows')}
@@ -660,15 +1634,19 @@ function renderSummaryPage(stats, severity, risk, unmitigated, exposures, model,
660
1634
  ${stats.shields > 0 ? statCard(stats.shields, 'Shields', 'muted') : ''}
661
1635
  </div>
662
1636
 
1637
+ <div class="summary-panels">
663
1638
  <!-- Coverage Bar -->
1639
+ <div class="panel">
664
1640
  <div class="sub-h">Threat Mitigation Coverage</div>
665
- <div style="display:flex;align-items:center;gap:.8rem;margin-bottom:.3rem">
1641
+ <div class="panel-row">
666
1642
  <span class="coverage-pct ${mitigationCoveragePercent >= 70 ? 'good' : mitigationCoveragePercent >= 40 ? 'warn' : 'bad'}">${mitigationCoveragePercent}%</span>
667
- <span style="color:var(--muted);font-size:.82rem">${mitigatedCount} of ${exposures.length} exposures mitigated</span>
1643
+ <span class="panel-muted">${mitigatedCount} of ${exposures.length} exposures mitigated</span>
668
1644
  </div>
669
1645
  <div class="posture-bar"><div class="posture-fill ${mitigationCoveragePercent >= 70 ? 'good' : mitigationCoveragePercent >= 40 ? 'warn' : 'bad'}" style="width:${Math.min(mitigationCoveragePercent, 100)}%"></div></div>
1646
+ </div>
670
1647
 
671
1648
  <!-- Severity Breakdown -->
1649
+ <div class="panel">
672
1650
  <div class="sub-h">Severity Breakdown</div>
673
1651
  <div class="severity-chart">
674
1652
  ${severityBar('Critical', severity.critical, stats.exposures, 'crit')}
@@ -677,50 +1655,163 @@ function renderSummaryPage(stats, severity, risk, unmitigated, exposures, model,
677
1655
  ${severityBar('Low', severity.low, stats.exposures, 'low')}
678
1656
  ${severity.unset > 0 ? severityBar('Unset', severity.unset, stats.exposures, 'unset') : ''}
679
1657
  </div>
1658
+ </div>
1659
+ </div>
680
1660
 
681
1661
  ${unmitigated.length > 0 ? `
682
1662
  <!-- Open Threats -->
683
- <div class="sub-h" style="color:var(--red)">⚠ Open Threats (No Mitigation)</div>
1663
+ <div class="panel">
1664
+ <div class="sub-h sub-h-alert">⚠ Open Threats (No Mitigation)</div>
684
1665
  ${unmitigated.map((e, i) => `
685
- <div class="finding-card" onclick="openDrawer('open_exposure', ${i})">
1666
+ <div class="finding-card" data-ff="${esc(e.file)}" onclick="openDrawer('open_exposure', ${i})">
686
1667
  <div class="fc-top">
687
1668
  <span class="fc-risk">${esc(e.threat)}</span>
688
1669
  <span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span>
689
1670
  </div>
690
1671
  ${e.description ? `<div class="fc-desc">${esc(e.description)}</div>` : ''}
691
1672
  <div class="fc-assets">Asset: ${esc(e.asset)}</div>
692
- </div>`).join('')}` : ''}
1673
+ </div>`).join('')}
1674
+ </div>` : ''}
693
1675
 
694
1676
  ${model.flows.length > 0 ? `
695
1677
  <!-- Data Flows -->
1678
+ <div class="panel">
696
1679
  <div class="sub-h">Data Flows</div>
697
1680
  <table>
698
1681
  <thead><tr><th>Source</th><th></th><th>Target</th><th>Mechanism</th><th>Location</th></tr></thead>
699
1682
  <tbody>
700
1683
  ${model.flows.map(f => `
701
- <tr>
1684
+ <tr data-ff="${f.location ? esc(f.location.file) : ''}">
702
1685
  <td><code>${esc(f.source)}</code></td>
703
- <td style="color:var(--muted)">→</td>
1686
+ <td class="flow-arrow">→</td>
704
1687
  <td><code>${esc(f.target)}</code></td>
705
1688
  <td>${esc(f.mechanism || '—')}</td>
706
1689
  <td class="loc">${f.location ? `${esc(f.location.file)}:${f.location.line}` : ''}</td>
707
1690
  </tr>`).join('')}
708
1691
  </tbody>
709
- </table>` : ''}
1692
+ </table>
1693
+ </div>` : ''}
710
1694
  </div>`;
711
1695
  }
712
1696
  function renderAIAnalysisPage(analyses) {
713
1697
  return `
714
1698
  <div id="sec-ai-analysis" class="section-content">
715
1699
  <div class="sec-h"><span class="sec-icon">✨</span> Threat Reports</div>
1700
+ <div class="panel ai-analysis-panel">
716
1701
  <div class="ai-analysis-controls">
717
1702
  <label for="report-selector" class="report-selector-label">Select Report:</label>
718
1703
  <select id="report-selector" class="report-selector" aria-label="Select threat report"></select>
719
1704
  </div>
720
1705
  <div id="ai-content" class="md-content ai-analysis-main"></div>
1706
+ </div>
721
1707
  </div>`;
722
1708
  }
723
- function renderThreatsPage(exposures, model) {
1709
+ function renderPentestPage(pentest) {
1710
+ const hasScanData = pentest.scans.length > 0;
1711
+ const hasTemplates = pentest.templates.length > 0;
1712
+ const latestScan = pentest.scans[0];
1713
+ let findingsHtml = '';
1714
+ if (hasScanData) {
1715
+ pentest.scans.forEach((scan, si) => {
1716
+ const scanDate = scan.completed_at ? scan.completed_at.slice(0, 19).replace('T', ' ') : 'unknown';
1717
+ const duration = scan.statistics?.duration
1718
+ ? `${scan.statistics.duration.secs}s`
1719
+ : '?';
1720
+ findingsHtml += `
1721
+ <div style="margin-bottom:1.5rem">
1722
+ <div style="display:flex;align-items:center;gap:.6rem;margin-bottom:.5rem">
1723
+ <span style="font-weight:600;font-size:.9rem">Scan ${esc(scan.scan_id.slice(0, 8))}</span>
1724
+ <span style="font-size:.72rem;color:var(--muted)">${esc(scanDate)} &middot; ${esc(duration)} &middot; ${scan.findings.length} finding(s)</span>
1725
+ <span style="font-size:.68rem;color:var(--muted);background:var(--surface2);padding:1px 6px;border-radius:4px">${esc(scan.source_file)}</span>
1726
+ </div>`;
1727
+ if (scan.findings.length > 0) {
1728
+ findingsHtml += `
1729
+ <table>
1730
+ <thead><tr><th>Severity</th><th>Title</th><th>Template</th><th>CWE</th><th>Confidence</th></tr></thead>
1731
+ <tbody>
1732
+ ${scan.findings.map((f, fi) => `
1733
+ <tr class="clickable" onclick="openPentestDrawer(${si}, ${fi})">
1734
+ <td><span class="fc-sev ${f.severity === 'critical' ? 'crit' : f.severity === 'high' ? 'high' : f.severity === 'medium' ? 'med' : 'low'}">${esc(f.severity)}</span></td>
1735
+ <td>${esc(f.title)}</td>
1736
+ <td><code style="font-size:.72rem">${esc(f.template_id)}</code></td>
1737
+ <td>${f.cwe_ids?.length ? f.cwe_ids.map(c => `<code style="font-size:.7rem">${esc(c)}</code>`).join(' ') : '—'}</td>
1738
+ <td>${formatConfidence(f.confidence)}</td>
1739
+ </tr>`).join('')}
1740
+ </tbody>
1741
+ </table>`;
1742
+ }
1743
+ else {
1744
+ findingsHtml += '<p class="empty-state">No findings in this scan — all checks passed.</p>';
1745
+ }
1746
+ findingsHtml += '</div>';
1747
+ });
1748
+ }
1749
+ let templatesHtml = '';
1750
+ if (hasTemplates) {
1751
+ templatesHtml = `
1752
+ <div class="sub-h">Templates (${pentest.templates.length})</div>
1753
+ <p style="color:var(--muted);font-size:.78rem;margin-bottom:.5rem">CXG templates in <code>.guardlink/cxg-templates/</code> — click for details.</p>
1754
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:8px">
1755
+ ${pentest.templates.map((t, i) => `
1756
+ <div class="finding-card" onclick="openTemplateDrawer(${i})">
1757
+ <div class="fc-top">
1758
+ <span class="fc-risk">${esc(t.id)}</span>
1759
+ <span class="fc-sev ${t.severity === 'critical' ? 'crit' : t.severity === 'high' ? 'high' : t.severity === 'medium' ? 'med' : 'low'}">${esc(t.severity)}</span>
1760
+ </div>
1761
+ <div class="fc-assets">${esc(t.filename)} &middot; ${esc(t.language)}</div>
1762
+ ${t.tags.length > 0 ? `<div style="margin-top:.3rem">${t.tags.slice(0, 4).map(tag => `<span class="data-badge">${esc(tag)}</span>`).join(' ')}</div>` : ''}
1763
+ </div>`).join('')}
1764
+ </div>`;
1765
+ }
1766
+ const sevBreakdown = pentest.findingsBySeverity;
1767
+ const maxSevCount = Math.max(1, ...Object.values(sevBreakdown));
1768
+ return `
1769
+ <div id="sec-pentest" class="section-content">
1770
+ <div class="sec-h"><span class="sec-icon">🔬</span> Pentest Findings</div>
1771
+
1772
+ ${pentest.redactionApplied ? `
1773
+ <div style="background:linear-gradient(135deg,rgba(56,178,172,0.08),rgba(56,178,172,0.03));border:1px solid rgba(56,178,172,0.35);border-radius:6px;padding:.55rem .8rem;margin-bottom:1rem;display:flex;align-items:center;gap:.55rem;font-size:.8rem">
1774
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="rgb(56,178,172)" aria-hidden="true"><path d="M8 1a3 3 0 00-3 3v3H4a1 1 0 00-1 1v6a1 1 0 001 1h8a1 1 0 001-1V8a1 1 0 00-1-1h-1V4a3 3 0 00-3-3zM6 4a2 2 0 014 0v3H6V4z"/></svg>
1775
+ <span><strong style="color:rgb(56,178,172)">Evidence redaction: enabled</strong> — JWT signatures stripped, credential values masked. Claims and exploit payloads preserved. See <code>docs/handling-evidence.md</code>.</span>
1776
+ </div>
1777
+ ` : ''}
1778
+
1779
+ ${hasScanData || hasTemplates ? `
1780
+ <!-- Stats bar -->
1781
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:10px;margin-bottom:1.2rem">
1782
+ <div class="stat-card stat-red"><span class="value">${pentest.totalFindings}</span><span class="label">Total Findings</span></div>
1783
+ <div class="stat-card"><span class="value">${sevBreakdown['critical'] || 0}</span><span class="label" style="color:var(--sev-crit)">Critical</span></div>
1784
+ <div class="stat-card"><span class="value">${sevBreakdown['high'] || 0}</span><span class="label" style="color:var(--sev-high)">High</span></div>
1785
+ <div class="stat-card"><span class="value">${sevBreakdown['medium'] || 0}</span><span class="label" style="color:var(--sev-med)">Medium</span></div>
1786
+ <div class="stat-card"><span class="value">${sevBreakdown['low'] || 0}</span><span class="label" style="color:var(--sev-low)">Low</span></div>
1787
+ <div class="stat-card stat-muted"><span class="value">${pentest.templates.length}</span><span class="label">Templates</span></div>
1788
+ <div class="stat-card stat-muted"><span class="value">${pentest.scans.length}</span><span class="label">Scans</span></div>
1789
+ </div>
1790
+
1791
+ ${hasScanData ? `
1792
+ <div class="sub-h" style="color:var(--red)">Findings (${pentest.totalFindings})</div>
1793
+ <p style="color:var(--muted);font-size:.78rem;margin-bottom:.5rem">Results from CXG security scans — click a finding for full evidence details.</p>
1794
+ ${findingsHtml}
1795
+ ` : ''}
1796
+
1797
+ ${templatesHtml}
1798
+ ` : `
1799
+ <div class="empty-state" style="text-align:center;padding:3rem 1rem">
1800
+ <div style="font-size:3rem;margin-bottom:1rem;opacity:0.5">🔬</div>
1801
+ <div style="font-size:1.1rem;font-weight:600;margin-bottom:0.5rem">No Pentest Data Yet</div>
1802
+ <div style="color:var(--muted);margin-bottom:1.5rem">Generate CXG templates and run scans to see findings here</div>
1803
+ <div style="display:flex;flex-direction:column;gap:0.5rem;max-width:550px;margin:0 auto;text-align:left">
1804
+ <div style="font-size:0.88rem;color:var(--muted)"><strong>Step 1 — Generate templates:</strong></div>
1805
+ <code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink translate "Create templates for critical threats" --claude-code</code>
1806
+ <div style="font-size:0.88rem;color:var(--muted);margin-top:0.5rem"><strong>Step 2 — Run CXG scan:</strong></div>
1807
+ <code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">cxg scan --scope local://. --template-dir .guardlink/cxg-templates/ --output .guardlink/pentest-findings/guardlink-pentest --output-format json,sarif</code>
1808
+ <div style="font-size:0.88rem;color:var(--muted);margin-top:0.5rem"><strong>Step 3 — View results:</strong></div>
1809
+ <code style="display:block;padding:0.5rem;background:var(--surface2);border-radius:4px;font-size:0.82rem">guardlink dashboard</code>
1810
+ </div>
1811
+ </div>`}
1812
+ </div>`;
1813
+ }
1814
+ function renderThreatsPage(exposures, confirmed, model) {
724
1815
  const open = exposures.filter(e => !e.mitigated && !e.accepted);
725
1816
  const mitigated = exposures.filter(e => e.mitigated);
726
1817
  const accepted = exposures.filter(e => e.accepted);
@@ -728,14 +1819,31 @@ function renderThreatsPage(exposures, model) {
728
1819
  <div id="sec-threats" class="section-content">
729
1820
  <div class="sec-h"><span class="sec-icon">⚠</span> Threats &amp; Exposures</div>
730
1821
 
731
- <div class="sub-h" style="color:var(--red)">Open Threats (${open.length})</div>
732
- <p style="color:var(--muted);font-size:.78rem;margin-bottom:.5rem">Exposed in code but <strong>not mitigated</strong> by any control.</p>
1822
+ ${confirmed.length > 0 ? `
1823
+ <div class="sub-h sub-h-critical">🔴 Confirmed Exploitable (${confirmed.length})</div>
1824
+ <p class="section-note">Verified through pentest, scanning, or manual reproduction — <strong>not false positives</strong>.</p>
1825
+ <table>
1826
+ <thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
1827
+ <tbody>
1828
+ ${confirmed.map((c, i) => `
1829
+ <tr class="clickable row-open" data-ff="${esc(c.file)}" onclick="openDrawer('confirmed', ${i})" style="border-left:3px solid var(--sev-crit, #e74c3c)">
1830
+ <td><code>${esc(c.asset)}</code></td>
1831
+ <td><code>${esc(c.threat)}</code></td>
1832
+ <td><span class="fc-sev ${sevClass(c.severity)}">${esc(c.severity)}</span></td>
1833
+ <td>${esc(c.description || '—')}</td>
1834
+ <td class="loc">${esc(c.file)}:${c.line}</td>
1835
+ </tr>`).join('')}
1836
+ </tbody>
1837
+ </table>` : ''}
1838
+
1839
+ <div class="sub-h sub-h-alert">Open Threats (${open.length})</div>
1840
+ <p class="section-note">Exposed in code but <strong>not mitigated</strong> by any control.</p>
733
1841
  ${open.length > 0 ? `
734
1842
  <table>
735
1843
  <thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
736
1844
  <tbody>
737
1845
  ${open.map((e, i) => `
738
- <tr class="clickable" onclick="openDrawer('open_exposure', ${i})">
1846
+ <tr class="clickable" data-ff="${esc(e.file)}" onclick="openDrawer('open_exposure', ${i})">
739
1847
  <td><code>${esc(e.asset)}</code></td>
740
1848
  <td><code>${esc(e.threat)}</code></td>
741
1849
  <td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
@@ -745,13 +1853,13 @@ function renderThreatsPage(exposures, model) {
745
1853
  </tbody>
746
1854
  </table>` : '<p class="empty-state">All exposed threats are mitigated or accepted.</p>'}
747
1855
 
748
- <div class="sub-h" style="color:var(--green)">Mitigated Threats (${mitigated.length})</div>
1856
+ <div class="sub-h sub-h-ok">Mitigated Threats (${mitigated.length})</div>
749
1857
  ${mitigated.length > 0 ? `
750
1858
  <table>
751
1859
  <thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
752
1860
  <tbody>
753
1861
  ${mitigated.map((e, i) => `
754
- <tr class="clickable" onclick="openDrawer('mitigated_exposure', ${i})">
1862
+ <tr class="clickable" data-ff="${esc(e.file)}" onclick="openDrawer('mitigated_exposure', ${i})">
755
1863
  <td><code>${esc(e.asset)}</code></td>
756
1864
  <td><code>${esc(e.threat)}</code></td>
757
1865
  <td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
@@ -762,12 +1870,12 @@ function renderThreatsPage(exposures, model) {
762
1870
  </table>` : '<p class="empty-state">No mitigations found.</p>'}
763
1871
 
764
1872
  ${accepted.length > 0 ? `
765
- <div class="sub-h" style="color:var(--yellow)">Accepted Risks (${accepted.length})</div>
1873
+ <div class="sub-h sub-h-neutral">Accepted Risks (${accepted.length})</div>
766
1874
  <table>
767
1875
  <thead><tr><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
768
1876
  <tbody>
769
1877
  ${accepted.map(e => `
770
- <tr>
1878
+ <tr data-ff="${esc(e.file)}">
771
1879
  <td><code>${esc(e.asset)}</code></td>
772
1880
  <td><code>${esc(e.threat)}</code></td>
773
1881
  <td><span class="fc-sev ${sevClass(e.severity)}">${esc(e.severity)}</span></td>
@@ -778,12 +1886,12 @@ function renderThreatsPage(exposures, model) {
778
1886
  </table>` : ''}
779
1887
 
780
1888
  ${model.transfers.length > 0 ? `
781
- <div class="sub-h" style="color:var(--purple)">Transferred Risks (${model.transfers.length})</div>
1889
+ <div class="sub-h sub-h-info">Transferred Risks (${model.transfers.length})</div>
782
1890
  <table>
783
1891
  <thead><tr><th>Source</th><th>Threat</th><th>Target</th><th>Description</th><th>Location</th></tr></thead>
784
1892
  <tbody>
785
1893
  ${model.transfers.map(t => `
786
- <tr>
1894
+ <tr data-ff="${t.location ? esc(t.location.file) : ''}">
787
1895
  <td><code>${esc(t.source)}</code></td>
788
1896
  <td><code>${esc(t.threat)}</code></td>
789
1897
  <td><code>${esc(t.target)}</code></td>
@@ -799,7 +1907,7 @@ function renderThreatsPage(exposures, model) {
799
1907
  <thead><tr><th>Status</th><th>Asset</th><th>Threat</th><th>Severity</th><th>Description</th><th>Location</th></tr></thead>
800
1908
  <tbody>
801
1909
  ${exposures.map((e, i) => `
802
- <tr class="clickable ${!e.mitigated && !e.accepted ? 'row-open' : ''}" onclick="openDrawer('exposure', ${i})">
1910
+ <tr class="clickable ${!e.mitigated && !e.accepted ? 'row-open' : ''}" data-ff="${esc(e.file)}" onclick="openDrawer('exposure', ${i})">
803
1911
  <td>${e.mitigated ? '<span class="badge badge-green">Mitigated</span>' : e.accepted ? '<span class="badge badge-blue">Accepted</span>' : '<span class="badge badge-red">Open</span>'}</td>
804
1912
  <td><code>${esc(e.asset)}</code></td>
805
1913
  <td><code>${esc(e.threat)}</code></td>
@@ -811,20 +1919,126 @@ function renderThreatsPage(exposures, model) {
811
1919
  </table>` : ''}
812
1920
  </div>`;
813
1921
  }
814
- function renderDiagramsPage(threatGraph, dataFlow, attackSurface) {
1922
+ function renderDiagramsPage(threatGraph, threatGraphFull, dataFlow, attackSurface, topology) {
815
1923
  const tabs = [];
816
1924
  const panels = [];
1925
+ const diagramActions = `
1926
+ <div class="diagram-actions">
1927
+ <button class="diagram-btn" onclick="diagramZoom('out')" title="Zoom out">−</button>
1928
+ <button class="diagram-btn" onclick="diagramZoom('in')" title="Zoom in">+</button>
1929
+ <button class="diagram-btn" onclick="diagramZoom('fit')" title="Reset view">Reset</button>
1930
+ </div>`;
1931
+ if (topology.nodes.length > 0) {
1932
+ tabs.push({ id: 'risk-topology', label: 'Risk Topology', icon: '◎' });
1933
+ // Which link kinds are actually present? Only show filters that apply.
1934
+ const availableLinkKinds = new Set(topology.links.map(l => l.kind));
1935
+ const linkKindCatalog = [
1936
+ ['exposes', 'Exposures'],
1937
+ ['confirmed', 'Confirmed'],
1938
+ ['mitigates', 'Mitigations'],
1939
+ ['protects', 'Protects'],
1940
+ ['validates', 'Validates'],
1941
+ ['flows', 'Flows'],
1942
+ ['boundary', 'Boundaries'],
1943
+ ['transfers', 'Transfers'],
1944
+ ['accepts', 'Accepts'],
1945
+ ];
1946
+ const linkFilters = linkKindCatalog
1947
+ .filter(([k]) => availableLinkKinds.has(k))
1948
+ .map(([k, label]) => `<label class="topology-link-pill topology-link-pill-${k}"><input type="checkbox" data-topology-link="${k}" checked onchange="renderTopologyDiagram()"> ${label}</label>`)
1949
+ .join('\n ');
1950
+ panels.push(`<div id="dtab-risk-topology" class="diagram-panel active">
1951
+ <div class="diagram-shell topology-shell">
1952
+ <div class="diagram-toolbar topology-toolbar">
1953
+ <span class="diagram-title">Risk Topology</span>
1954
+ <div class="diagram-actions">
1955
+ <button class="diagram-btn" onclick="exportTopologySVG()" title="Download SVG">⤓ SVG</button>
1956
+ <button class="diagram-btn" onclick="diagramZoom('out')" title="Zoom out">−</button>
1957
+ <button class="diagram-btn" onclick="diagramZoom('in')" title="Zoom in">+</button>
1958
+ <button class="diagram-btn" onclick="diagramZoom('fit')" title="Reset view">Reset</button>
1959
+ </div>
1960
+ </div>
1961
+ <div class="topology-controls">
1962
+ <input id="topologySearch" class="diagram-search" type="search" placeholder="Search assets, threats, controls…" oninput="renderTopologyDiagram()">
1963
+ <div class="topology-kind-toggles">
1964
+ <label class="topology-kind-pill topology-kind-asset"><input type="checkbox" data-topology-kind="asset" checked onchange="renderTopologyDiagram()"> Assets</label>
1965
+ <label class="topology-kind-pill topology-kind-threat"><input type="checkbox" data-topology-kind="threat" checked onchange="renderTopologyDiagram()"> Threats</label>
1966
+ <label class="topology-kind-pill topology-kind-control"><input type="checkbox" data-topology-kind="control" checked onchange="renderTopologyDiagram()"> Controls</label>
1967
+ <label class="topology-kind-pill topology-kind-openonly"><input id="topologyOpenOnly" type="checkbox" onchange="renderTopologyDiagram()"> Open only</label>
1968
+ </div>
1969
+ <span id="topologyVisibleCount" class="topology-count">${topology.nodes.length} nodes · ${topology.links.length} links</span>
1970
+ </div>
1971
+ <div class="topology-link-filters">
1972
+ <span class="topology-link-filters-k">Edges</span>
1973
+ ${linkFilters}
1974
+ </div>
1975
+ <div class="topology-layout">
1976
+ <div id="topologyGraph" class="topology-graph"></div>
1977
+ <aside id="topologyInspector" class="topology-inspector"></aside>
1978
+ </div>
1979
+ <div class="diagram-meta topology-legend">
1980
+ <span><i class="legend-dot asset"></i>Asset</span>
1981
+ <span><i class="legend-dot threat"></i>Threat</span>
1982
+ <span><i class="legend-dot control"></i>Control</span>
1983
+ <span><i class="legend-dot confirmed"></i>Confirmed</span>
1984
+ <span><i class="legend-line exposes"></i>Exposure</span>
1985
+ <span><i class="legend-line mitigates"></i>Mitigates</span>
1986
+ <span><i class="legend-line flow"></i>Flow</span>
1987
+ <span><i class="legend-line boundary"></i>Boundary</span>
1988
+ </div>
1989
+ </div>
1990
+ </div>`);
1991
+ }
817
1992
  if (threatGraph) {
818
1993
  tabs.push({ id: 'threat-graph', label: 'Threat Graph', icon: '🔷' });
819
- panels.push(`<div id="dtab-threat-graph" class="diagram-panel active"><div class="mermaid-wrap"><pre class="mermaid">\n${threatGraph}\n</pre></div></div>`);
1994
+ const hasFullVariant = threatGraphFull && threatGraphFull !== threatGraph;
1995
+ const showAllBtn = hasFullVariant
1996
+ ? `<button id="threatGraphToggle" class="diagram-btn" onclick="toggleThreatGraphAll(this)" title="Show all threat severities (not just high/critical)">All severities</button>`
1997
+ : '';
1998
+ panels.push(`<div id="dtab-threat-graph" class="diagram-panel${panels.length === 0 ? ' active' : ''}">
1999
+ <div class="diagram-shell">
2000
+ <div class="diagram-toolbar">
2001
+ <span class="diagram-title">Threat Graph</span>
2002
+ <div class="diagram-actions">
2003
+ ${showAllBtn}
2004
+ <button class="diagram-btn" onclick="diagramZoom('out')" title="Zoom out">−</button>
2005
+ <button class="diagram-btn" onclick="diagramZoom('in')" title="Zoom in">+</button>
2006
+ <button class="diagram-btn" onclick="diagramZoom('fit')" title="Reset view">Reset</button>
2007
+ </div>
2008
+ </div>
2009
+ <div class="mermaid-wrap">
2010
+ <pre class="mermaid" data-variant="filtered">\n${esc(threatGraph)}\n</pre>
2011
+ ${hasFullVariant ? `<pre class="mermaid" data-variant="full" style="display:none">\n${esc(threatGraphFull)}\n</pre>` : ''}
2012
+ </div>
2013
+ <div class="diagram-meta">Assets, threats, controls, and mitigations. ${hasFullVariant ? 'Filtered to high/critical by default — click <em>All severities</em> to expand.' : ''}</div>
2014
+ </div>
2015
+ </div>`);
820
2016
  }
821
2017
  if (dataFlow) {
822
2018
  tabs.push({ id: 'data-flow', label: 'Data Flow', icon: '↔' });
823
- panels.push(`<div id="dtab-data-flow" class="diagram-panel"><div class="mermaid-wrap"><pre class="mermaid">\n${dataFlow}\n</pre></div></div>`);
2019
+ panels.push(`<div id="dtab-data-flow" class="diagram-panel">
2020
+ <div class="diagram-shell">
2021
+ <div class="diagram-toolbar">
2022
+ <span class="diagram-title">Data Flow</span>
2023
+ ${diagramActions}
2024
+ </div>
2025
+ <div class="mermaid-wrap"><pre class="mermaid">\n${esc(dataFlow)}\n</pre></div>
2026
+ <div class="diagram-meta">Trust zones (🧱) and data movement across system boundaries. Each boundary shows both sides of the trust line.</div>
2027
+ </div>
2028
+ </div>`);
824
2029
  }
825
2030
  if (attackSurface) {
826
2031
  tabs.push({ id: 'attack-surface', label: 'Attack Surface', icon: '⚠' });
827
- panels.push(`<div id="dtab-attack-surface" class="diagram-panel"><div class="mermaid-wrap"><pre class="mermaid">\n${attackSurface}\n</pre></div></div>`);
2032
+ panels.push(`<div id="dtab-attack-surface" class="diagram-panel">
2033
+ <div class="diagram-shell">
2034
+ <div class="diagram-toolbar">
2035
+ <span class="diagram-title">Attack Surface</span>
2036
+ ${diagramActions}
2037
+ </div>
2038
+ <div class="mermaid-wrap"><pre class="mermaid">\n${esc(attackSurface)}\n</pre></div>
2039
+ <div class="diagram-meta">Exposures per asset, severity-coloured. <strong>💥 confirmed</strong>, <strong>⚠️ open</strong>, <strong>✅ mitigated</strong>, <strong>🟦 accepted</strong>.</div>
2040
+ </div>
2041
+ </div>`);
828
2042
  }
829
2043
  if (tabs.length === 0) {
830
2044
  return `<div id="sec-diagrams" class="section-content">
@@ -835,7 +2049,7 @@ function renderDiagramsPage(threatGraph, dataFlow, attackSurface) {
835
2049
  return `
836
2050
  <div id="sec-diagrams" class="section-content">
837
2051
  <div class="sec-h"><span class="sec-icon">◉</span> Diagrams</div>
838
- <p style="color:var(--muted);font-size:.78rem;margin-bottom:.8rem">Interactive diagrams from annotations. Scroll to zoom, drag to pan, double-click to reset.</p>
2052
+ <p class="diagram-hint">Interactive diagrams generated from annotations. The topology view supports search, filtering, drag, pan, and node inspection.</p>
839
2053
  <div class="diagram-tabs">
840
2054
  ${tabs.map((t, i) => `<button class="diagram-tab${i === 0 ? ' active' : ''}" onclick="switchDiagramTab('${t.id}', this)">${t.icon} ${t.label}</button>`).join('')}
841
2055
  </div>
@@ -853,7 +2067,7 @@ function renderCodePage(fileAnnotations, model) {
853
2067
  Every file with GuardLink annotations. Click any annotation to see details.
854
2068
  </p>
855
2069
  ${fileAnnotations.length > 0 ? fileAnnotations.map((f, fi) => `
856
- <div class="file-card">
2070
+ <div class="file-card" data-ff="${esc(f.file)}">
857
2071
  <div class="file-card-header" onclick="toggleFile(this)">
858
2072
  <span class="file-path">${esc(f.file)}</span>
859
2073
  <span style="display:flex;align-items:center;gap:.4rem">
@@ -904,7 +2118,7 @@ function renderDataPage(model) {
904
2118
  <thead><tr><th>Side A</th><th></th><th>Side B</th><th>Description</th><th>Location</th></tr></thead>
905
2119
  <tbody>
906
2120
  ${model.boundaries.map(b => `
907
- <tr>
2121
+ <tr data-ff="${b.location ? esc(b.location.file) : ''}">
908
2122
  <td><code>${esc(b.asset_a)}</code></td>
909
2123
  <td style="color:var(--purple)">↔</td>
910
2124
  <td><code>${esc(b.asset_b)}</code></td>
@@ -920,7 +2134,7 @@ function renderDataPage(model) {
920
2134
  <thead><tr><th>Classification</th><th>Asset</th><th>Description</th><th>Location</th></tr></thead>
921
2135
  <tbody>
922
2136
  ${model.data_handling.map(d => `
923
- <tr>
2137
+ <tr data-ff="${d.location ? esc(d.location.file) : ''}">
924
2138
  <td><span class="ann-badge ann-data">${esc(d.classification)}</span></td>
925
2139
  <td><code>${esc(d.asset || '—')}</code></td>
926
2140
  <td>${esc(d.description || '—')}</td>
@@ -935,7 +2149,7 @@ function renderDataPage(model) {
935
2149
  <thead><tr><th>Control</th><th>Asset</th><th>Description</th><th>Location</th></tr></thead>
936
2150
  <tbody>
937
2151
  ${model.validations.map(v => `
938
- <tr>
2152
+ <tr data-ff="${v.location ? esc(v.location.file) : ''}">
939
2153
  <td><code>${esc(v.control)}</code></td>
940
2154
  <td><code>${esc(v.asset)}</code></td>
941
2155
  <td>${esc(v.description || '—')}</td>
@@ -950,7 +2164,7 @@ function renderDataPage(model) {
950
2164
  <thead><tr><th>Asset</th><th>Owner</th><th>Description</th><th>Location</th></tr></thead>
951
2165
  <tbody>
952
2166
  ${model.ownership.map(o => `
953
- <tr>
2167
+ <tr data-ff="${o.location ? esc(o.location.file) : ''}">
954
2168
  <td><code>${esc(o.asset)}</code></td>
955
2169
  <td><strong>${esc(o.owner)}</strong></td>
956
2170
  <td>${esc(o.description || '—')}</td>
@@ -965,7 +2179,7 @@ function renderDataPage(model) {
965
2179
  <thead><tr><th>Asset</th><th>Description</th><th>Location</th></tr></thead>
966
2180
  <tbody>
967
2181
  ${model.audits.map(a => `
968
- <tr>
2182
+ <tr data-ff="${a.location ? esc(a.location.file) : ''}">
969
2183
  <td><code>${esc(a.asset)}</code></td>
970
2184
  <td>${esc(a.description || 'Needs review')}</td>
971
2185
  <td class="loc">${a.location ? `${esc(a.location.file)}:${a.location.line}` : ''}</td>
@@ -980,7 +2194,7 @@ function renderDataPage(model) {
980
2194
  <thead><tr><th>Asset</th><th>Assumption</th><th>Location</th></tr></thead>
981
2195
  <tbody>
982
2196
  ${model.assumptions.map(a => `
983
- <tr>
2197
+ <tr data-ff="${a.location ? esc(a.location.file) : ''}">
984
2198
  <td><code>${esc(a.asset)}</code></td>
985
2199
  <td>${esc(a.description || 'Unverified assumption')}</td>
986
2200
  <td class="loc">${a.location ? `${esc(a.location.file)}:${a.location.line}` : ''}</td>
@@ -995,7 +2209,7 @@ function renderDataPage(model) {
995
2209
  <thead><tr><th>Reason</th><th>Location</th></tr></thead>
996
2210
  <tbody>
997
2211
  ${model.shields.map(s => `
998
- <tr>
2212
+ <tr data-ff="${s.location ? esc(s.location.file) : ''}">
999
2213
  <td>${esc(s.reason || 'No reason provided')}</td>
1000
2214
  <td class="loc">${s.location ? `${esc(s.location.file)}:${s.location.line}` : ''}</td>
1001
2215
  </tr>`).join('')}
@@ -1008,7 +2222,7 @@ function renderDataPage(model) {
1008
2222
  <thead><tr><th>Comment</th><th>Location</th></tr></thead>
1009
2223
  <tbody>
1010
2224
  ${model.comments.map(c => `
1011
- <tr>
2225
+ <tr data-ff="${c.location ? esc(c.location.file) : ''}">
1012
2226
  <td>${esc(c.description || '(no description)')}</td>
1013
2227
  <td class="loc">${c.location ? `${esc(c.location.file)}:${c.location.line}` : ''}</td>
1014
2228
  </tr>`).join('')}
@@ -1029,7 +2243,7 @@ function renderAssetsPage(heatmap) {
1029
2243
  ${heatmap.length > 0 ? `
1030
2244
  <div class="heatmap">
1031
2245
  ${heatmap.map((a, i) => `
1032
- <div class="heatmap-cell risk-cell-${a.riskLevel} clickable" onclick="openDrawer('asset', ${i})">
2246
+ <div class="heatmap-cell risk-cell-${a.riskLevel} clickable" data-ff-asset="${esc(a.name)}" onclick="openDrawer('asset', ${i})">
1033
2247
  <div class="heatmap-name">${esc(a.name)}</div>
1034
2248
  <div class="heatmap-stats">
1035
2249
  <span title="Exposures">⚠ ${a.exposures}</span>
@@ -1086,6 +2300,8 @@ function buildFileAnnotations(model, root) {
1086
2300
  addEntry('control', c, c.name);
1087
2301
  for (const e of model.exposures)
1088
2302
  addEntry('exposes', e, `${e.asset} → ${e.threat}`);
2303
+ for (const cf of (model.confirmed || []))
2304
+ addEntry('confirmed', cf, `${cf.asset} confirmed ${cf.threat}`);
1089
2305
  for (const m of model.mitigations)
1090
2306
  addEntry('mitigates', m, `${m.control} mitigates ${m.threat}`);
1091
2307
  for (const a of model.acceptances)
@@ -1150,7 +2366,9 @@ function sevClass(s) {
1150
2366
  return 'low';
1151
2367
  return 'unset';
1152
2368
  }
1153
- function computeRiskGrade(sev, unmitigatedCount, totalExposures) {
2369
+ function computeRiskGrade(sev, unmitigatedCount, totalExposures, confirmedCount = 0) {
2370
+ if (confirmedCount > 0)
2371
+ return { grade: 'F', label: 'Critical Risk', summary: `${confirmedCount} confirmed exploitable finding(s) — immediate remediation required` };
1154
2372
  if (sev.critical > 0)
1155
2373
  return { grade: 'F', label: 'Critical Risk', summary: `${sev.critical} critical exposure(s) require immediate attention` };
1156
2374
  if (sev.high >= 3 || unmitigatedCount >= 5)
@@ -1167,248 +2385,1302 @@ function computeRiskGrade(sev, unmitigatedCount, totalExposures) {
1167
2385
  const CSS_CONTENT = `
1168
2386
  /* ── Reset ── */
1169
2387
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
1170
- :root { --font-ui: 'Inter', system-ui, -apple-system, sans-serif; --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace; }
2388
+ *::selection { background: color-mix(in oklab, var(--accent) 35%, transparent); color: var(--text); }
2389
+ :root {
2390
+ --font-ui: 'Inter', system-ui, -apple-system, sans-serif;
2391
+ --font-mono: 'JetBrains Mono', 'SF Mono', Menlo, Consolas, monospace;
2392
+ --ease: cubic-bezier(.2,.8,.2,1);
2393
+ --radius-sm: 6px; --radius-md: 8px; --radius-lg: 12px;
2394
+ --drawer-w: 440px; --sidebar-w: 220px;
2395
+ }
1171
2396
 
1172
- /* ══ DARK THEME (Bravos) ══ */
2397
+ /* ══ DARK THEME Modern deep-slate ══ */
1173
2398
  [data-theme="dark"] {
1174
- --bg: #0a0d10; --surface: #0d1117; --surface2: #1a1f25; --border: #1f3943; --border-subtle: #162028;
1175
- --text: #f0f0f0; --muted: #55899e; --text-dim: #3b6779;
1176
- --accent: #2dd4a7; --blue: #0360a2; --green: #10b981; --red: #ea1d1d;
1177
- --orange: #f97316; --yellow: #f59e0b; --purple: #a78bfa;
1178
- --sev-crit: #ef4444; --sev-high: #f97316; --sev-med: #f59e0b; --sev-low: #3b82f6; --sev-unset: #6b7280;
1179
- --badge-red-bg: #7f1d1d; --badge-green-bg: #065f46; --badge-blue-bg: #1e3a5f;
1180
- --risk-f: #7f1d1d; --risk-d: #7c2d12; --risk-c: #78350f; --risk-b: #1e3a5f; --risk-a: #065f46;
1181
- --heatmap-crit: #7f1d1d; --heatmap-high: #7c2d12; --heatmap-med: #78350f;
1182
- --heatmap-low: #1e3a5f; --heatmap-none: #1a1f25;
1183
- --table-alt: #141a20; --table-hover: #1e2830; --shadow: 0 1px 3px rgba(0,0,0,.4);
1184
- --logo-bg: #2dd4a7; --logo-text: #0a0d10;
1185
- --drawer-w: 420px; --sidebar-w: 210px;
1186
- }
1187
-
1188
- /* ══ LIGHT THEME ══ */
2399
+ --bg: #000000;
2400
+ --bg-gradient: radial-gradient(ellipse 1000px 500px at 15% 0%, rgba(51,212,157,.09), transparent 58%),
2401
+ radial-gradient(ellipse 800px 400px at 95% 10%, rgba(3,96,162,.10), transparent 60%);
2402
+ --surface: #0f1b20;
2403
+ --surface2: #172930;
2404
+ --surface3: #1f3943;
2405
+ --border: #1f3943;
2406
+ --border-subtle: #1a3139;
2407
+ --border-strong: #55899e;
2408
+
2409
+ --text: #f0f0f0;
2410
+ --muted: #55899e;
2411
+ --text-dim: #3b6779;
2412
+
2413
+ --accent: #33d49d;
2414
+ --accent-soft: rgba(51,212,157,.12);
2415
+ --accent-dim: rgba(51,212,157,.22);
2416
+ --accent-hover: #ffffff;
2417
+
2418
+ --blue: #0360a2;
2419
+ --green: #33d49d;
2420
+ --red: #ea1d1d;
2421
+ --orange: #ea1d1d;
2422
+ --yellow: #55899e;
2423
+ --purple: #0360a2;
2424
+
2425
+ --sev-crit: #ea1d1d;
2426
+ --sev-high: #ea1d1d;
2427
+ --sev-med: #55899e;
2428
+ --sev-low: #0360a2;
2429
+ --sev-unset:#3b6779;
2430
+
2431
+ --sev-crit-bg: rgba(234,29,29,.14);
2432
+ --sev-high-bg: rgba(234,29,29,.10);
2433
+ --sev-med-bg: rgba(85,137,158,.16);
2434
+ --sev-low-bg: rgba(3,96,162,.16);
2435
+
2436
+ --badge-red-bg: rgba(234,29,29,.16);
2437
+ --badge-green-bg: rgba(51,212,157,.16);
2438
+ --badge-blue-bg: rgba(3,96,162,.16);
2439
+ --badge-red-fg: #f0f0f0;
2440
+ --badge-green-fg: #33d49d;
2441
+ --badge-blue-fg: #f0f0f0;
2442
+
2443
+ --risk-f: linear-gradient(135deg, rgba(234,29,29,.20), rgba(234,29,29,.05));
2444
+ --risk-d: linear-gradient(135deg, rgba(234,29,29,.14), rgba(234,29,29,.04));
2445
+ --risk-c: linear-gradient(135deg, rgba(85,137,158,.18), rgba(85,137,158,.05));
2446
+ --risk-b: linear-gradient(135deg, rgba(3,96,162,.18), rgba(3,96,162,.05));
2447
+ --risk-a: linear-gradient(135deg, rgba(51,212,157,.20), rgba(51,212,157,.05));
2448
+ --risk-border-f: rgba(234,29,29,.40);
2449
+ --risk-border-d: rgba(234,29,29,.28);
2450
+ --risk-border-c: rgba(85,137,158,.35);
2451
+ --risk-border-b: rgba(3,96,162,.35);
2452
+ --risk-border-a: rgba(51,212,157,.35);
2453
+
2454
+ --heatmap-crit: linear-gradient(135deg, rgba(234,29,29,.20), rgba(234,29,29,.05));
2455
+ --heatmap-high: linear-gradient(135deg, rgba(234,29,29,.14), rgba(234,29,29,.04));
2456
+ --heatmap-med: linear-gradient(135deg, rgba(85,137,158,.16), rgba(85,137,158,.04));
2457
+ --heatmap-low: linear-gradient(135deg, rgba(3,96,162,.16), rgba(3,96,162,.04));
2458
+ --heatmap-none: #172930;
2459
+
2460
+ --table-alt: #13232a;
2461
+ --table-hover: #1c323a;
2462
+ --shadow-sm: 0 1px 2px rgba(0,0,0,.3);
2463
+ --shadow-md: 0 4px 12px rgba(0,0,0,.35), 0 1px 2px rgba(0,0,0,.4);
2464
+ --shadow-lg: 0 12px 32px rgba(0,0,0,.45), 0 2px 6px rgba(0,0,0,.4);
2465
+ --glow-accent: 0 0 0 1px rgba(45,212,191,.25), 0 8px 24px rgba(45,212,191,.12);
2466
+
2467
+ --logo-bg: linear-gradient(135deg, #33d49d, #0360a2);
2468
+ --logo-text: #000000;
2469
+ }
2470
+
2471
+ /* ══ LIGHT THEME — Refined off-white ══ */
1189
2472
  [data-theme="light"] {
1190
- --bg: #f8fafc; --surface: #ffffff; --surface2: #f1f5f9; --border: #d1d5db; --border-subtle: #e5e7eb;
1191
- --text: #1a1a2e; --muted: #4a5568; --text-dim: #9ca3af;
1192
- --accent: #0d9373; --blue: #2563eb; --green: #059669; --red: #dc2626;
1193
- --orange: #ea580c; --yellow: #d97706; --purple: #7c3aed;
1194
- --sev-crit: #dc2626; --sev-high: #ea580c; --sev-med: #d97706; --sev-low: #2563eb; --sev-unset: #6b7280;
1195
- --badge-red-bg: #fef2f2; --badge-green-bg: #ecfdf5; --badge-blue-bg: #eff6ff;
1196
- --risk-f: #fef2f2; --risk-d: #fff7ed; --risk-c: #fffbeb; --risk-b: #eff6ff; --risk-a: #ecfdf5;
1197
- --heatmap-crit: #fef2f2; --heatmap-high: #fff7ed; --heatmap-med: #fffbeb;
1198
- --heatmap-low: #eff6ff; --heatmap-none: #f9fafb;
1199
- --table-alt: #f9fafb; --table-hover: #f1f5f9; --shadow: 0 1px 3px rgba(0,0,0,.08);
1200
- --logo-bg: #0d9373; --logo-text: #ffffff;
1201
- --drawer-w: 420px; --sidebar-w: 210px;
1202
- }
1203
-
1204
- body { font-family: var(--font-ui); background: var(--bg); color: var(--text); line-height: 1.5; overflow: hidden; height: 100vh; }
1205
- a { color: var(--accent); text-decoration: none; }
1206
- code { background: var(--border); padding: 1px 4px; border-radius: 3px; font-size: .75rem; font-family: var(--font-mono); }
2473
+ --bg: #f0f0f0;
2474
+ --bg-gradient: radial-gradient(ellipse 1000px 500px at 15% 0%, rgba(51,212,157,.10), transparent 58%),
2475
+ radial-gradient(ellipse 800px 400px at 95% 10%, rgba(3,96,162,.08), transparent 60%);
2476
+ --surface: #ffffff;
2477
+ --surface2: #f0f0f0;
2478
+ --surface3: #e8eef0;
2479
+ --border: #55899e;
2480
+ --border-subtle: #d9e4e8;
2481
+ --border-strong: #1f3943;
2482
+
2483
+ --text: #1f3943;
2484
+ --muted: #55899e;
2485
+ --text-dim: #3b6779;
2486
+
2487
+ --accent: #33d49d;
2488
+ --accent-soft: rgba(51,212,157,.12);
2489
+ --accent-dim: rgba(51,212,157,.20);
2490
+ --accent-hover: #0360a2;
2491
+
2492
+ --blue: #0360a2;
2493
+ --green: #33d49d;
2494
+ --red: #ea1d1d;
2495
+ --orange: #ea1d1d;
2496
+ --yellow: #55899e;
2497
+ --purple: #0360a2;
2498
+
2499
+ --sev-crit: #ea1d1d;
2500
+ --sev-high: #ea1d1d;
2501
+ --sev-med: #3b6779;
2502
+ --sev-low: #0360a2;
2503
+ --sev-unset:#55899e;
2504
+
2505
+ --sev-crit-bg: rgba(234,29,29,.10);
2506
+ --sev-high-bg: rgba(234,29,29,.08);
2507
+ --sev-med-bg: rgba(59,103,121,.10);
2508
+ --sev-low-bg: rgba(3,96,162,.10);
2509
+
2510
+ --badge-red-bg: rgba(234,29,29,.12);
2511
+ --badge-green-bg: rgba(51,212,157,.14);
2512
+ --badge-blue-bg: rgba(3,96,162,.14);
2513
+ --badge-red-fg: #ea1d1d;
2514
+ --badge-green-fg: #1f3943;
2515
+ --badge-blue-fg: #0360a2;
2516
+
2517
+ --risk-f: linear-gradient(135deg, rgba(234,29,29,.12), rgba(234,29,29,.02));
2518
+ --risk-d: linear-gradient(135deg, rgba(234,29,29,.09), rgba(234,29,29,.02));
2519
+ --risk-c: linear-gradient(135deg, rgba(59,103,121,.12), rgba(59,103,121,.02));
2520
+ --risk-b: linear-gradient(135deg, rgba(3,96,162,.12), rgba(3,96,162,.02));
2521
+ --risk-a: linear-gradient(135deg, rgba(51,212,157,.14), rgba(51,212,157,.02));
2522
+ --risk-border-f: rgba(234,29,29,.28);
2523
+ --risk-border-d: rgba(234,29,29,.2);
2524
+ --risk-border-c: rgba(59,103,121,.26);
2525
+ --risk-border-b: rgba(3,96,162,.24);
2526
+ --risk-border-a: rgba(51,212,157,.28);
2527
+
2528
+ --heatmap-crit: linear-gradient(135deg, rgba(234,29,29,.14), rgba(234,29,29,.02));
2529
+ --heatmap-high: linear-gradient(135deg, rgba(234,29,29,.10), rgba(234,29,29,.02));
2530
+ --heatmap-med: linear-gradient(135deg, rgba(59,103,121,.10), rgba(59,103,121,.02));
2531
+ --heatmap-low: linear-gradient(135deg, rgba(3,96,162,.10), rgba(3,96,162,.02));
2532
+ --heatmap-none: #f0f0f0;
2533
+
2534
+ --table-alt: #f7f9fa;
2535
+ --table-hover: #edf3f5;
2536
+ --shadow-sm: 0 1px 2px rgba(15,23,42,.04);
2537
+ --shadow-md: 0 4px 12px rgba(15,23,42,.06), 0 1px 2px rgba(15,23,42,.04);
2538
+ --shadow-lg: 0 12px 32px rgba(15,23,42,.08), 0 2px 6px rgba(15,23,42,.04);
2539
+ --glow-accent: 0 0 0 1px rgba(13,148,136,.20), 0 8px 24px rgba(13,148,136,.10);
2540
+
2541
+ --logo-bg: linear-gradient(135deg, #33d49d, #0360a2);
2542
+ --logo-text: #ffffff;
2543
+ }
2544
+
2545
+ html, body { height: 100%; }
2546
+ body {
2547
+ font-family: var(--font-ui);
2548
+ background: var(--bg-gradient), var(--bg);
2549
+ color: var(--text);
2550
+ line-height: 1.5;
2551
+ font-size: 13.5px;
2552
+ overflow: hidden;
2553
+ -webkit-font-smoothing: antialiased;
2554
+ -moz-osx-font-smoothing: grayscale;
2555
+ letter-spacing: -0.005em;
2556
+ }
2557
+ a { color: var(--accent); text-decoration: none; transition: color .15s var(--ease); }
2558
+ a:hover { color: var(--accent-hover); }
2559
+ code {
2560
+ background: var(--surface2);
2561
+ border: 1px solid var(--border-subtle);
2562
+ padding: 1px 5px;
2563
+ border-radius: 4px;
2564
+ font-size: .76rem;
2565
+ font-family: var(--font-mono);
2566
+ color: var(--text);
2567
+ }
2568
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
2569
+ ::-webkit-scrollbar-track { background: transparent; }
2570
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; border: 2px solid var(--bg); }
2571
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-strong); }
1207
2572
 
1208
2573
  /* ── Top Nav ── */
1209
- .topnav { height: 48px; background: var(--surface); border-bottom: 1px solid var(--border); display: flex; align-items: center; padding: 0 1.2rem; gap: 1rem; z-index: 100; }
1210
- .topnav-left { display: flex; align-items: center; gap: .6rem; }
1211
- .topnav-right { margin-left: auto; display: flex; align-items: center; gap: 1rem; }
1212
- .topnav h1 { font-size: 1.1rem; font-weight: 700; white-space: nowrap; }
1213
- .badge { background: var(--accent); color: var(--logo-text); padding: 2px 8px; border-radius: 10px; font-size: .65rem; font-weight: 600; }
1214
- .tn-stat { font-size: .72rem; color: var(--muted); display: flex; align-items: center; gap: 4px; }
1215
- .tn-stat .tn-v { font-weight: 700; font-size: .82rem; }
1216
- .tn-v.red { color: var(--red); } .tn-v.green { color: var(--green); } .tn-v.blue { color: var(--accent); } .tn-v.yellow { color: var(--yellow); }
1217
- .logo { width: 32px; height: 32px; background: var(--logo-bg); color: var(--logo-text); border-radius: 6px; display: flex; align-items: center; justify-content: center; font-weight: 700; font-size: 13px; }
1218
- #themeToggle { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: 4px 8px; cursor: pointer; font-size: 14px; line-height: 1; }
1219
- #themeToggle:hover { background: var(--border); }
2574
+ .topnav {
2575
+ height: 52px;
2576
+ background: color-mix(in oklab, var(--surface) 92%, transparent);
2577
+ backdrop-filter: saturate(160%) blur(12px);
2578
+ -webkit-backdrop-filter: saturate(160%) blur(12px);
2579
+ border-bottom: 1px solid var(--border);
2580
+ display: flex; align-items: center;
2581
+ padding: 0 1.4rem; gap: 1rem;
2582
+ z-index: 100;
2583
+ position: relative;
2584
+ }
2585
+ .topnav::after {
2586
+ content: ''; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px;
2587
+ background: linear-gradient(90deg, transparent, var(--accent-dim) 40%, var(--accent-dim) 60%, transparent);
2588
+ opacity: .55; pointer-events: none;
2589
+ }
2590
+ .topnav-left { display: flex; align-items: center; gap: .7rem; }
2591
+ .topnav-right { margin-left: auto; display: flex; align-items: center; gap: .75rem; }
2592
+ .topnav-metrics { display: flex; align-items: center; gap: .5rem; }
2593
+ .topnav h1 { font-size: 1.02rem; font-weight: 650; white-space: nowrap; letter-spacing: -0.01em; }
2594
+ .badge {
2595
+ background: var(--accent-soft);
2596
+ color: var(--accent);
2597
+ border: 1px solid var(--accent-dim);
2598
+ padding: 2px 9px; border-radius: 999px;
2599
+ font-size: .64rem; font-weight: 600;
2600
+ text-transform: uppercase; letter-spacing: .6px;
2601
+ }
2602
+ .tn-stat {
2603
+ font-size: .72rem; color: var(--muted); display: flex; align-items: center; gap: 6px;
2604
+ background: color-mix(in oklab, var(--surface2) 88%, transparent);
2605
+ border: 1px solid var(--border);
2606
+ border-radius: 999px;
2607
+ padding: 4px 10px;
2608
+ backdrop-filter: blur(4px);
2609
+ }
2610
+ .tn-stat .tn-k { letter-spacing: .2px; }
2611
+ .tn-stat .tn-v { font-weight: 700; font-size: .85rem; color: var(--text); font-variant-numeric: tabular-nums; }
2612
+ .tn-v.red { color: var(--sev-crit); } .tn-v.green { color: var(--green); }
2613
+ .tn-v.blue { color: var(--accent); } .tn-v.yellow { color: var(--yellow); }
2614
+ .feature-filter-wrap { display: flex; align-items: center; }
2615
+ .feature-filter-select {
2616
+ max-width: 170px;
2617
+ background: var(--surface2);
2618
+ color: var(--text);
2619
+ border: 1px solid var(--border);
2620
+ border-radius: 999px;
2621
+ padding: 5px 10px;
2622
+ font-size: .74rem;
2623
+ font-family: var(--font-ui);
2624
+ cursor: pointer;
2625
+ transition: all .15s var(--ease);
2626
+ }
2627
+ .feature-filter-select:hover { border-color: var(--border-strong); background: var(--surface3); }
2628
+ .feature-filter-select:focus { outline: none; border-color: var(--accent); box-shadow: var(--glow-accent); }
2629
+ .logo {
2630
+ width: 34px; height: 34px;
2631
+ background: var(--logo-bg); color: var(--logo-text);
2632
+ border-radius: 9px;
2633
+ display: flex; align-items: center; justify-content: center;
2634
+ font-weight: 700; font-size: 12px; letter-spacing: .3px;
2635
+ box-shadow: var(--shadow-sm);
2636
+ }
2637
+ #themeToggle {
2638
+ background: var(--surface2); border: 1px solid var(--border);
2639
+ border-radius: 8px; padding: 5px 9px; cursor: pointer;
2640
+ font-size: 14px; line-height: 1; color: var(--text);
2641
+ transition: all .15s var(--ease);
2642
+ }
2643
+ #themeToggle:hover { background: var(--surface3); border-color: var(--border-strong); }
1220
2644
  [data-theme="dark"] .icon-sun { display: none; }
1221
2645
  [data-theme="light"] .icon-moon { display: none; }
2646
+ .feature-banner {
2647
+ display: none;
2648
+ align-items: center;
2649
+ gap: 8px;
2650
+ padding: 8px 16px;
2651
+ background: color-mix(in oklab, var(--accent) 88%, var(--surface));
2652
+ color: #fff;
2653
+ font-size: .8rem;
2654
+ font-weight: 600;
2655
+ border-bottom: 1px solid color-mix(in oklab, var(--accent) 45%, var(--border));
2656
+ }
2657
+ .feature-banner-files { opacity: .75; font-weight: 500; }
2658
+ .feature-banner-clear {
2659
+ margin-left: auto;
2660
+ background: rgba(255,255,255,.18);
2661
+ border: 1px solid rgba(255,255,255,.28);
2662
+ color: #fff;
2663
+ padding: 4px 10px;
2664
+ border-radius: 999px;
2665
+ cursor: pointer;
2666
+ font-size: .72rem;
2667
+ font-weight: 600;
2668
+ }
2669
+ .feature-banner-clear:hover { background: rgba(255,255,255,.28); }
1222
2670
 
1223
2671
  /* ── Layout ── */
1224
- .layout { display: flex; height: calc(100vh - 48px); position: relative; }
1225
- .sidebar { width: var(--sidebar-w); min-width: var(--sidebar-w); background: var(--surface); border-right: 1px solid var(--border); display: flex; flex-direction: column; transition: all .25s ease; }
1226
- .sidebar-nav { flex: 1; overflow-y: auto; padding: .6rem 0; }
1227
- .sidebar.collapsed { width: 50px; min-width: 50px; }
2672
+ .layout { display: flex; height: calc(100vh - 54px); position: relative; }
2673
+ .sidebar {
2674
+ width: var(--sidebar-w); min-width: var(--sidebar-w);
2675
+ background: var(--surface);
2676
+ border-right: 1px solid var(--border);
2677
+ display: flex; flex-direction: column;
2678
+ transition: width .25s var(--ease), min-width .25s var(--ease);
2679
+ }
2680
+ .sidebar-nav { flex: 1; overflow-y: auto; padding: .75rem .55rem; }
2681
+ .sidebar.collapsed { width: 52px; min-width: 52px; }
1228
2682
  .sidebar.collapsed .nav-text { display: none; }
1229
2683
  .sidebar.collapsed .sep { margin: .5rem .5rem; }
1230
2684
  .sidebar.collapsed .chevron-left { display: none; }
1231
2685
  .sidebar.collapsed .chevron-right { display: block; }
1232
- #sidebarToggle { background: var(--surface2); border: none; border-top: 1px solid var(--border); padding: .8rem; cursor: pointer; color: var(--muted); transition: all .2s; display: flex; align-items: center; justify-content: center; width: 100%; }
1233
- #sidebarToggle:hover { background: var(--border); color: var(--accent); }
2686
+ #sidebarToggle {
2687
+ background: transparent; border: none; border-top: 1px solid var(--border);
2688
+ padding: .7rem; cursor: pointer; color: var(--muted);
2689
+ transition: all .15s var(--ease);
2690
+ display: flex; align-items: center; justify-content: center; width: 100%;
2691
+ }
2692
+ #sidebarToggle:hover { background: var(--surface2); color: var(--accent); }
1234
2693
  #sidebarToggle svg { display: block; }
1235
2694
  #sidebarToggle .chevron-right { display: none; }
1236
- .sidebar a { display: flex; align-items: center; gap: .6rem; padding: .55rem 1rem; font-size: .8rem; color: var(--muted); cursor: pointer; border-left: 3px solid transparent; transition: all .12s; user-select: none; }
2695
+ .sidebar a {
2696
+ display: flex; align-items: center; gap: .65rem;
2697
+ padding: .52rem .75rem; margin: 1px .1rem;
2698
+ font-size: .8rem; color: var(--muted); cursor: pointer;
2699
+ border-radius: 7px;
2700
+ transition: background .12s var(--ease), color .12s var(--ease);
2701
+ user-select: none;
2702
+ position: relative;
2703
+ }
1237
2704
  .sidebar a:hover { background: var(--surface2); color: var(--text); }
1238
- .sidebar a.active { color: var(--accent); border-left-color: var(--accent); background: rgba(45,212,167,.08); }
1239
- .sidebar .nav-icon { width: 20px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
2705
+ .sidebar a.active {
2706
+ color: var(--accent);
2707
+ background: var(--accent-soft);
2708
+ font-weight: 550;
2709
+ }
2710
+ .sidebar a.active::before {
2711
+ content: ''; position: absolute; left: -.1rem; top: 20%; bottom: 20%; width: 2px;
2712
+ background: var(--accent); border-radius: 2px;
2713
+ }
2714
+ .sidebar .nav-icon { width: 18px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; opacity: .85; }
2715
+ .sidebar a.active .nav-icon { opacity: 1; }
1240
2716
  .sidebar .nav-icon svg { display: block; }
1241
- .sidebar .sep { height: 1px; background: var(--border); margin: .5rem 1rem; }
2717
+ .sidebar .sep { height: 1px; background: var(--border); margin: .6rem .8rem; }
1242
2718
  .main { flex: 1; overflow-y: auto; padding: 0; }
1243
- .section-content { display: none; padding: 1.2rem 1.5rem; } .section-content.active { display: block; }
2719
+ .section-content { display: none; padding: 1.6rem 2rem 3rem; max-width: 1400px; }
2720
+ .section-content.active { display: block; animation: fadeIn .2s var(--ease); }
2721
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
2722
+ .panel {
2723
+ background: color-mix(in oklab, var(--surface) 94%, transparent);
2724
+ border: 1px solid var(--border);
2725
+ border-radius: var(--radius-lg);
2726
+ padding: 1rem 1.1rem;
2727
+ box-shadow: var(--shadow-sm);
2728
+ margin-bottom: 1rem;
2729
+ }
2730
+ .panel-row { display: flex; align-items: center; gap: .8rem; margin-bottom: .35rem; }
2731
+ .panel-muted { color: var(--muted); font-size: .82rem; }
2732
+ .summary-panels {
2733
+ display: grid;
2734
+ grid-template-columns: minmax(250px, 1fr) minmax(320px, 1.3fr);
2735
+ gap: .9rem;
2736
+ }
2737
+ .ai-analysis-panel { padding-top: .9rem; }
1244
2738
 
1245
2739
  /* ── Drawer ── */
1246
- .drawer-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,.4); z-index: 200; display: none; }
1247
- .drawer-overlay.open { display: block; }
1248
- .drawer { position: fixed; top: 0; right: 0; width: var(--drawer-w); height: 100vh; background: var(--surface); border-left: 1px solid var(--border); z-index: 201; transform: translateX(100%); transition: transform .25s ease; overflow-y: auto; }
2740
+ .drawer-overlay {
2741
+ position: fixed; inset: 0;
2742
+ background: rgba(0,0,0,.48);
2743
+ backdrop-filter: blur(2px);
2744
+ -webkit-backdrop-filter: blur(2px);
2745
+ z-index: 200; display: none;
2746
+ }
2747
+ .drawer-overlay.open { display: block; animation: fadeIn .15s var(--ease); }
2748
+ .drawer {
2749
+ position: fixed; top: 0; right: 0;
2750
+ width: var(--drawer-w); height: 100vh;
2751
+ background: var(--surface);
2752
+ border-left: 1px solid var(--border);
2753
+ z-index: 201;
2754
+ transform: translateX(100%);
2755
+ transition: transform .28s var(--ease);
2756
+ overflow-y: auto;
2757
+ box-shadow: var(--shadow-lg);
2758
+ }
1249
2759
  .drawer.open { transform: translateX(0); }
1250
- .drawer-header { display: flex; align-items: center; justify-content: space-between; padding: .8rem 1rem; border-bottom: 1px solid var(--border); position: sticky; top: 0; background: var(--surface); z-index: 1; }
1251
- .drawer-header h3 { font-size: .95rem; color: var(--accent); }
1252
- .drawer-close { background: none; border: 1px solid var(--border); color: var(--muted); cursor: pointer; padding: 4px 10px; border-radius: 4px; font-size: .8rem; }
1253
- .drawer-close:hover { color: var(--text); border-color: var(--muted); }
1254
- .drawer-body { padding: 1rem; }
1255
- .d-section { margin-bottom: 1rem; } .d-label { font-size: .7rem; text-transform: uppercase; color: var(--muted); letter-spacing: .5px; margin-bottom: .3rem; } .d-value { font-size: .82rem; }
1256
- .d-code { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: .5rem .7rem; font-family: var(--font-mono); font-size: .72rem; line-height: 1.6; color: var(--muted); white-space: pre; overflow-x: auto; }
2760
+ .drawer-header {
2761
+ display: flex; align-items: center; justify-content: space-between;
2762
+ padding: 1rem 1.2rem;
2763
+ border-bottom: 1px solid var(--border);
2764
+ position: sticky; top: 0;
2765
+ background: color-mix(in oklab, var(--surface) 94%, transparent);
2766
+ backdrop-filter: blur(8px);
2767
+ -webkit-backdrop-filter: blur(8px);
2768
+ z-index: 1;
2769
+ }
2770
+ .drawer-header h3 { font-size: .95rem; color: var(--text); font-weight: 650; letter-spacing: -.01em; }
2771
+ .drawer-close {
2772
+ background: var(--surface2); border: 1px solid var(--border);
2773
+ color: var(--muted); cursor: pointer;
2774
+ padding: 5px 11px; border-radius: 6px; font-size: .78rem;
2775
+ transition: all .15s var(--ease);
2776
+ }
2777
+ .drawer-close:hover { color: var(--text); border-color: var(--border-strong); background: var(--surface3); }
2778
+ .drawer-body { padding: 1.2rem; }
2779
+ .d-section { margin-bottom: 1.2rem; }
2780
+ .d-label { font-size: .68rem; text-transform: uppercase; color: var(--muted); letter-spacing: .8px; margin-bottom: .35rem; font-weight: 600; }
2781
+ .d-value { font-size: .85rem; }
2782
+ .d-code {
2783
+ background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-md);
2784
+ padding: .65rem .8rem; font-family: var(--font-mono); font-size: .72rem;
2785
+ line-height: 1.65; color: var(--muted); white-space: pre; overflow-x: auto;
2786
+ }
1257
2787
 
1258
2788
  /* ── Section headings ── */
1259
- .sec-h { font-size: 1.1rem; font-weight: 700; margin-bottom: .8rem; display: flex; align-items: center; gap: .5rem; }
1260
- .sec-icon { font-size: 1.2rem; }
1261
- .sub-h { font-size: .9rem; font-weight: 600; color: var(--accent); margin: 1rem 0 .5rem 0; }
2789
+ .sec-h {
2790
+ font-size: 1.25rem; font-weight: 700;
2791
+ margin-bottom: 1rem;
2792
+ display: flex; align-items: center; gap: .6rem;
2793
+ letter-spacing: -.02em;
2794
+ }
2795
+ .sec-icon {
2796
+ font-size: 1rem;
2797
+ width: 30px; height: 30px;
2798
+ display: inline-flex; align-items: center; justify-content: center;
2799
+ background: var(--accent-soft);
2800
+ color: var(--accent);
2801
+ border: 1px solid var(--accent-dim);
2802
+ border-radius: 8px;
2803
+ }
2804
+ .sub-h {
2805
+ font-size: .78rem; font-weight: 600; color: var(--muted);
2806
+ margin: 1.2rem 0 .55rem 0;
2807
+ text-transform: uppercase; letter-spacing: .7px;
2808
+ display: flex; align-items: center; gap: .5rem;
2809
+ }
2810
+ .sub-h::after {
2811
+ content: ''; flex: 1; height: 1px;
2812
+ background: linear-gradient(90deg, var(--border), transparent);
2813
+ }
2814
+ .sub-h-alert { color: var(--red); }
2815
+ .sub-h-critical { color: var(--sev-crit); }
2816
+ .sub-h-ok { color: var(--green); }
2817
+ .sub-h-neutral { color: var(--yellow); }
2818
+ .sub-h-info { color: var(--blue); }
2819
+ .section-note { color: var(--muted); font-size: .78rem; margin-bottom: .55rem; }
2820
+ .flow-arrow { color: var(--muted); font-weight: 700; font-size: .88rem; text-align: center; }
1262
2821
 
1263
2822
  /* ── Stats Grid ── */
1264
- .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: .5rem; margin-bottom: 1.2rem; }
1265
- .stat-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: .5rem; text-align: center; }
1266
- .stat-card .value { font-size: 1.3rem; font-weight: 700; color: var(--accent); }
1267
- .stat-card .label { font-size: .65rem; color: var(--muted); margin-top: 2px; }
1268
- .stat-danger .value { color: var(--red); } .stat-success .value { color: var(--green); }
1269
- .stat-muted .value { color: var(--muted); } .stat-muted .label { color: var(--text-dim); }
2823
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(118px, 1fr)); gap: .6rem; margin-bottom: 1.2rem; }
2824
+ .stat-card {
2825
+ background: var(--surface);
2826
+ border: 1px solid var(--border);
2827
+ border-radius: var(--radius-md);
2828
+ padding: .75rem .7rem;
2829
+ text-align: center;
2830
+ transition: transform .15s var(--ease), border-color .15s var(--ease), box-shadow .15s var(--ease);
2831
+ position: relative; overflow: hidden;
2832
+ }
2833
+ .stat-card::before {
2834
+ content: ''; position: absolute; inset: 0 0 auto 0; height: 2px;
2835
+ background: var(--accent); opacity: 0; transition: opacity .15s var(--ease);
2836
+ }
2837
+ .stat-card:hover { transform: translateY(-1px); border-color: var(--border-strong); box-shadow: var(--shadow-sm); }
2838
+ .stat-card:hover::before { opacity: .8; }
2839
+ .stat-card .value { font-size: 1.5rem; font-weight: 700; color: var(--accent); font-variant-numeric: tabular-nums; letter-spacing: -.02em; line-height: 1.1; }
2840
+ .stat-card .label { font-size: .68rem; color: var(--muted); margin-top: 4px; text-transform: uppercase; letter-spacing: .4px; font-weight: 500; }
2841
+ .stat-danger .value { color: var(--sev-crit); } .stat-danger::before { background: var(--sev-crit); }
2842
+ .stat-success .value { color: var(--green); } .stat-success::before { background: var(--green); }
2843
+ .stat-muted .value { color: var(--muted); } .stat-muted::before { background: var(--muted); }
2844
+ .stat-muted .label { color: var(--text-dim); }
1270
2845
 
1271
2846
  /* ── Risk Banner ── */
1272
- .risk-banner { display: flex; align-items: center; gap: 20px; padding: 16px 20px; border-radius: 10px; border: 1px solid var(--border); margin-bottom: 1rem; }
1273
- .risk-grade { font-size: 40px; font-weight: 700; width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; border-radius: 10px; }
1274
- .risk-detail { display: flex; flex-direction: column; gap: 2px; }
1275
- .risk-detail strong { font-size: 15px; } .risk-detail span { font-size: 13px; color: var(--muted); }
1276
- .risk-f { background: var(--risk-f); } .risk-f .risk-grade { background: var(--sev-crit); color: #fff; }
1277
- .risk-d { background: var(--risk-d); } .risk-d .risk-grade { background: var(--sev-high); color: #fff; }
1278
- .risk-c { background: var(--risk-c); } .risk-c .risk-grade { background: var(--sev-med); color: #fff; }
1279
- .risk-b { background: var(--risk-b); } .risk-b .risk-grade { background: var(--sev-low); color: #fff; }
1280
- .risk-a { background: var(--risk-a); } .risk-a .risk-grade { background: var(--green); color: #fff; }
2847
+ .risk-banner {
2848
+ display: flex; align-items: center; gap: 22px;
2849
+ padding: 18px 22px;
2850
+ border-radius: var(--radius-lg);
2851
+ border: 1px solid var(--border);
2852
+ margin-bottom: 1.2rem;
2853
+ box-shadow: var(--shadow-sm);
2854
+ }
2855
+ .risk-grade {
2856
+ font-size: 34px; font-weight: 800;
2857
+ width: 58px; height: 58px;
2858
+ display: flex; align-items: center; justify-content: center;
2859
+ border-radius: var(--radius-md);
2860
+ letter-spacing: -.03em;
2861
+ box-shadow: 0 6px 16px rgba(0,0,0,.22), inset 0 1px 0 rgba(255,255,255,.15);
2862
+ }
2863
+ .risk-detail { display: flex; flex-direction: column; gap: 3px; }
2864
+ .risk-detail strong { font-size: 15px; font-weight: 650; letter-spacing: -.01em; }
2865
+ .risk-detail span { font-size: 13px; color: var(--muted); }
2866
+ .risk-f { background: var(--risk-f); border-color: var(--risk-border-f); } .risk-f .risk-grade { background: var(--sev-crit); color: #fff; }
2867
+ .risk-d { background: var(--risk-d); border-color: var(--risk-border-d); } .risk-d .risk-grade { background: var(--sev-high); color: #fff; }
2868
+ .risk-c { background: var(--risk-c); border-color: var(--risk-border-c); } .risk-c .risk-grade { background: var(--sev-med); color: #fff; }
2869
+ .risk-b { background: var(--risk-b); border-color: var(--risk-border-b); } .risk-b .risk-grade { background: var(--sev-low); color: #fff; }
2870
+ .risk-a { background: var(--risk-a); border-color: var(--risk-border-a); } .risk-a .risk-grade { background: var(--green); color: #fff; }
1281
2871
 
1282
2872
  /* ── Coverage Bar ── */
1283
- .coverage-pct { font-size: 1.8rem; font-weight: 700; }
1284
- .coverage-pct.good { color: var(--green); } .coverage-pct.warn { color: var(--yellow); } .coverage-pct.bad { color: var(--red); }
1285
- .posture-bar { height: 8px; border-radius: 4px; background: var(--border); margin: .6rem 0; overflow: hidden; }
1286
- .posture-fill { height: 100%; border-radius: 4px; transition: width .4s; }
1287
- .posture-fill.good { background: var(--green); } .posture-fill.warn { background: var(--yellow); } .posture-fill.bad { background: var(--red); }
2873
+ .coverage-pct { font-size: 2rem; font-weight: 700; font-variant-numeric: tabular-nums; letter-spacing: -.03em; }
2874
+ .coverage-pct.good { color: var(--green); } .coverage-pct.warn { color: var(--yellow); } .coverage-pct.bad { color: var(--sev-crit); }
2875
+ .posture-bar { height: 10px; border-radius: 999px; background: var(--surface2); margin: .7rem 0; overflow: hidden; border: 1px solid var(--border-subtle); }
2876
+ .posture-fill { height: 100%; border-radius: 999px; transition: width .6s var(--ease); }
2877
+ .posture-fill.good { background: linear-gradient(90deg, var(--green), var(--accent)); }
2878
+ .posture-fill.warn { background: linear-gradient(90deg, var(--yellow), var(--muted)); }
2879
+ .posture-fill.bad { background: linear-gradient(90deg, var(--sev-crit), var(--red)); }
1288
2880
 
1289
2881
  /* ── Severity Chart ── */
1290
- .severity-chart { display: flex; flex-direction: column; gap: 8px; margin-bottom: 1rem; }
1291
- .sev-row { display: flex; align-items: center; gap: 10px; }
1292
- .sev-label { width: 55px; font-size: 13px; font-weight: 500; text-align: right; }
1293
- .sev-track { flex: 1; height: 22px; background: var(--surface2); border-radius: 5px; overflow: hidden; }
1294
- .sev-fill { height: 100%; border-radius: 5px; min-width: 2px; transition: width .6s; }
1295
- .sev-fill-crit { background: var(--sev-crit); } .sev-fill-high { background: var(--sev-high); }
1296
- .sev-fill-med { background: var(--sev-med); } .sev-fill-low { background: var(--sev-low); }
2882
+ .severity-chart { display: flex; flex-direction: column; gap: 9px; margin-bottom: 1rem; }
2883
+ .sev-row { display: flex; align-items: center; gap: 12px; }
2884
+ .sev-label { width: 62px; font-size: 12.5px; font-weight: 550; text-align: right; color: var(--muted); }
2885
+ .sev-track { flex: 1; height: 22px; background: var(--surface2); border-radius: 6px; overflow: hidden; border: 1px solid var(--border-subtle); }
2886
+ .sev-fill { height: 100%; border-radius: 6px; min-width: 2px; transition: width .6s var(--ease); }
2887
+ .sev-fill-crit { background: linear-gradient(90deg, var(--sev-crit), var(--red)); }
2888
+ .sev-fill-high { background: linear-gradient(90deg, var(--sev-high), var(--red)); }
2889
+ .sev-fill-med { background: linear-gradient(90deg, var(--sev-med), var(--text-dim)); }
2890
+ .sev-fill-low { background: linear-gradient(90deg, var(--sev-low), var(--blue)); }
1297
2891
  .sev-fill-unset { background: var(--sev-unset); }
1298
- .sev-count { width: 28px; font-size: 14px; font-weight: 600; font-family: var(--font-mono); }
2892
+ .sev-count { width: 32px; font-size: 14px; font-weight: 700; font-family: var(--font-mono); color: var(--text); font-variant-numeric: tabular-nums; }
1299
2893
 
1300
2894
  /* ── Finding Cards ── */
1301
- .finding-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; padding: .7rem 1rem; margin-bottom: .5rem; cursor: pointer; transition: border-color .15s; }
1302
- .finding-card:hover { border-color: var(--accent); }
1303
- .fc-top { display: flex; align-items: center; gap: .5rem; margin-bottom: .2rem; }
1304
- .fc-risk { font-weight: 600; font-size: .85rem; } .fc-desc { font-size: .78rem; color: var(--muted); }
1305
- .fc-assets { font-size: .72rem; color: var(--muted); margin-top: .2rem; font-family: var(--font-mono); }
1306
- .fc-sev { font-size: .7rem; padding: 1px 6px; border-radius: 3px; font-weight: 600; }
1307
- .fc-sev.crit { background: var(--sev-crit); color: #fff; } .fc-sev.high { background: var(--sev-high); color: #fff; }
1308
- .fc-sev.med { background: var(--sev-med); color: #000; } .fc-sev.low { background: var(--border); color: var(--muted); }
1309
- .fc-sev.unset { background: var(--border); color: var(--muted); }
2895
+ .finding-card {
2896
+ background: var(--surface);
2897
+ border: 1px solid var(--border);
2898
+ border-radius: var(--radius-md);
2899
+ padding: .8rem 1rem;
2900
+ margin-bottom: .55rem;
2901
+ cursor: pointer;
2902
+ transition: all .15s var(--ease);
2903
+ position: relative;
2904
+ }
2905
+ .finding-card::before {
2906
+ content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
2907
+ background: var(--sev-crit); border-radius: 3px 0 0 3px;
2908
+ opacity: .7;
2909
+ }
2910
+ .finding-card:hover {
2911
+ border-color: var(--border-strong);
2912
+ transform: translateX(2px);
2913
+ box-shadow: var(--shadow-sm);
2914
+ }
2915
+ .fc-top { display: flex; align-items: center; gap: .55rem; margin-bottom: .25rem; }
2916
+ .fc-risk { font-weight: 600; font-size: .88rem; letter-spacing: -.005em; }
2917
+ .fc-desc { font-size: .8rem; color: var(--muted); line-height: 1.5; }
2918
+ .fc-assets { font-size: .72rem; color: var(--muted); margin-top: .3rem; font-family: var(--font-mono); }
2919
+ .fc-sev {
2920
+ font-size: .66rem;
2921
+ padding: 2px 8px;
2922
+ border-radius: 999px;
2923
+ font-weight: 700;
2924
+ text-transform: uppercase;
2925
+ letter-spacing: .4px;
2926
+ border: 1px solid transparent;
2927
+ }
2928
+ .fc-sev.crit { background: var(--sev-crit-bg); color: var(--sev-crit); border-color: color-mix(in oklab, var(--sev-crit) 35%, transparent); }
2929
+ .fc-sev.high { background: var(--sev-high-bg); color: var(--sev-high); border-color: color-mix(in oklab, var(--sev-high) 35%, transparent); }
2930
+ .fc-sev.med { background: var(--sev-med-bg); color: var(--sev-med); border-color: color-mix(in oklab, var(--sev-med) 35%, transparent); }
2931
+ .fc-sev.low { background: var(--sev-low-bg); color: var(--sev-low); border-color: color-mix(in oklab, var(--sev-low) 35%, transparent); }
2932
+ .fc-sev.unset { background: var(--surface2); color: var(--muted); border-color: var(--border); }
1310
2933
 
1311
2934
  /* ── Tables ── */
1312
- table { width: 100%; border-collapse: collapse; background: var(--surface2); border-radius: 6px; overflow: hidden; margin-bottom: .8rem; }
1313
- th, td { padding: .45rem .7rem; text-align: left; border-bottom: 1px solid var(--border); font-size: .78rem; }
1314
- th { background: var(--border); color: var(--muted); font-weight: 600; text-transform: uppercase; font-size: .68rem; }
1315
- tr.clickable { cursor: pointer; } tr.clickable:hover { background: var(--table-hover); }
1316
- .row-open { border-left: 3px solid var(--red); }
2935
+ table {
2936
+ width: 100%; border-collapse: separate; border-spacing: 0;
2937
+ background: var(--surface);
2938
+ border: 1px solid var(--border);
2939
+ border-radius: var(--radius-md);
2940
+ overflow: hidden;
2941
+ margin-bottom: 1rem;
2942
+ box-shadow: var(--shadow-sm);
2943
+ }
2944
+ th, td { padding: .6rem .85rem; text-align: left; border-bottom: 1px solid var(--border-subtle); font-size: .8rem; }
2945
+ tr:last-child td { border-bottom: none; }
2946
+ th {
2947
+ background: var(--surface2);
2948
+ color: var(--muted); font-weight: 650;
2949
+ text-transform: uppercase; font-size: .66rem; letter-spacing: .7px;
2950
+ position: sticky; top: 0;
2951
+ }
2952
+ tbody tr { transition: background .12s var(--ease); }
2953
+ tbody tr:nth-child(even) { background: var(--table-alt); }
2954
+ tr.clickable { cursor: pointer; }
2955
+ tr.clickable:hover { background: var(--table-hover); }
2956
+ .row-open { box-shadow: inset 3px 0 0 var(--sev-crit); }
1317
2957
  .loc { color: var(--muted); font-family: var(--font-mono); font-size: .72rem; white-space: nowrap; }
1318
- .empty-state { color: var(--muted); font-style: italic; padding: .8rem; font-size: .82rem; }
2958
+ .empty-state {
2959
+ color: var(--muted); font-style: italic;
2960
+ padding: 1.2rem;
2961
+ font-size: .82rem; text-align: center;
2962
+ background: var(--surface); border: 1px dashed var(--border);
2963
+ border-radius: var(--radius-md);
2964
+ }
1319
2965
 
1320
2966
  /* ── Badges ── */
1321
- .badge-red { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; background: var(--badge-red-bg); color: var(--sev-crit); }
1322
- .badge-green { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; background: var(--badge-green-bg); color: var(--green); }
1323
- .badge-blue { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; background: var(--badge-blue-bg); color: var(--sev-low); }
1324
-
1325
- /* ── Annotation badges ── */
1326
- .ann-badge { display: inline-block; padding: 1px 7px; border-radius: 4px; font-size: .68rem; font-weight: 600; text-transform: uppercase; letter-spacing: .3px; }
1327
- .ann-asset { background: #1c3a5e; color: #58a6ff; } .ann-threat { background: #4a1a1a; color: #f85149; }
1328
- .ann-control { background: #1a3a1a; color: #3fb950; } .ann-exposes { background: #4a1a1a; color: #f85149; }
1329
- .ann-mitigates { background: #1a3a1a; color: #3fb950; } .ann-accepts { background: #3a3a1a; color: #d29922; }
1330
- .ann-transfers { background: #2a1a3a; color: #bc8cff; } .ann-flow { background: #2a2a2a; color: #8b949e; }
1331
- .ann-boundary { background: #2a1a3a; color: #bc8cff; } .ann-data { background: #3a2a1a; color: #db6d28; }
1332
- .ann-handles { background: #3a2a1a; color: #db6d28; } .ann-validates { background: #1a3a1a; color: #3fb950; }
1333
- .ann-owns { background: #1c3a5e; color: #58a6ff; } .ann-audit { background: #3a3a1a; color: #d29922; }
1334
- .ann-assumes { background: #3a3a1a; color: #d29922; } .ann-shield { background: #2a2a2a; color: #8b949e; }
1335
- .ann-comment { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
2967
+ .badge-red, .badge-green, .badge-blue {
2968
+ display: inline-block; padding: 2px 9px;
2969
+ border-radius: 999px;
2970
+ font-size: .66rem; font-weight: 650;
2971
+ text-transform: uppercase; letter-spacing: .4px;
2972
+ border: 1px solid transparent;
2973
+ }
2974
+ .badge-red { background: var(--badge-red-bg); color: var(--badge-red-fg); border-color: color-mix(in oklab, var(--sev-crit) 25%, transparent); }
2975
+ .badge-green { background: var(--badge-green-bg); color: var(--badge-green-fg); border-color: color-mix(in oklab, var(--green) 25%, transparent); }
2976
+ .badge-blue { background: var(--badge-blue-bg); color: var(--badge-blue-fg); border-color: color-mix(in oklab, var(--sev-low) 25%, transparent); }
2977
+
2978
+ /* ── Annotation badges (theme-aware) ── */
2979
+ .ann-badge {
2980
+ display: inline-block; padding: 2px 8px;
2981
+ border-radius: 5px;
2982
+ font-size: .66rem; font-weight: 650;
2983
+ text-transform: uppercase; letter-spacing: .35px;
2984
+ border: 1px solid transparent;
2985
+ }
2986
+ .ann-asset { background: rgba(3,96,162,.14); color: #f0f0f0; border-color: rgba(3,96,162,.3); }
2987
+ .ann-threat { background: rgba(234,29,29,.14); color: #f0f0f0; border-color: rgba(234,29,29,.3); }
2988
+ .ann-control { background: rgba(51,212,157,.14); color: #33d49d; border-color: rgba(51,212,157,.3); }
2989
+ .ann-exposes { background: rgba(234,29,29,.14); color: #f0f0f0; border-color: rgba(234,29,29,.3); }
2990
+ .ann-mitigates{ background: rgba(51,212,157,.14); color: #33d49d; border-color: rgba(51,212,157,.3); }
2991
+ .ann-accepts { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
2992
+ .ann-transfers{ background: rgba(59,103,121,.18); color: #f0f0f0; border-color: rgba(59,103,121,.3); }
2993
+ .ann-flow { background: var(--surface2); color: var(--muted); border-color: var(--border); }
2994
+ .ann-boundary { background: rgba(59,103,121,.18); color: #f0f0f0; border-color: rgba(59,103,121,.3); }
2995
+ .ann-data { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
2996
+ .ann-handles { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
2997
+ .ann-validates{ background: rgba(51,212,157,.14); color: #33d49d; border-color: rgba(51,212,157,.3); }
2998
+ .ann-owns { background: rgba(3,96,162,.14); color: #f0f0f0; border-color: rgba(3,96,162,.3); }
2999
+ .ann-audit { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
3000
+ .ann-assumes { background: rgba(85,137,158,.16); color: #f0f0f0; border-color: rgba(85,137,158,.3); }
3001
+ .ann-shield { background: var(--surface2); color: var(--muted); border-color: var(--border); }
3002
+ .ann-comment { background: var(--surface2); color: var(--muted); border: 1px solid var(--border); }
3003
+ [data-theme="light"] .ann-asset { color: #0360a2; background: rgba(3,96,162,.10); border-color: rgba(3,96,162,.25); }
3004
+ [data-theme="light"] .ann-threat,
3005
+ [data-theme="light"] .ann-exposes { color: #ea1d1d; background: rgba(234,29,29,.10); border-color: rgba(234,29,29,.25); }
3006
+ [data-theme="light"] .ann-control,
3007
+ [data-theme="light"] .ann-mitigates,
3008
+ [data-theme="light"] .ann-validates{ color: #1f3943; background: rgba(51,212,157,.16); border-color: rgba(51,212,157,.25); }
3009
+ [data-theme="light"] .ann-accepts,
3010
+ [data-theme="light"] .ann-audit,
3011
+ [data-theme="light"] .ann-assumes { color: #3b6779; background: rgba(85,137,158,.14); border-color: rgba(85,137,158,.25); }
3012
+ [data-theme="light"] .ann-transfers,
3013
+ [data-theme="light"] .ann-boundary { color: #1f3943; background: rgba(59,103,121,.12); border-color: rgba(59,103,121,.25); }
3014
+ [data-theme="light"] .ann-data,
3015
+ [data-theme="light"] .ann-handles { color: #3b6779; background: rgba(85,137,158,.14); border-color: rgba(85,137,158,.25); }
3016
+ [data-theme="light"] .ann-owns { color: #0360a2; background: rgba(3,96,162,.10); border-color: rgba(3,96,162,.25); }
1336
3017
 
1337
3018
  /* ── File Cards (Code Browser) ── */
1338
- .file-card { background: var(--surface2); border: 1px solid var(--border); border-radius: 6px; margin-bottom: .7rem; overflow: hidden; }
1339
- .file-card-header { display: flex; align-items: center; justify-content: space-between; padding: .5rem .8rem; background: var(--surface); cursor: pointer; user-select: none; }
1340
- .file-card-header:hover { background: var(--border); }
3019
+ .file-card {
3020
+ background: var(--surface);
3021
+ border: 1px solid var(--border);
3022
+ border-radius: var(--radius-md);
3023
+ margin-bottom: .7rem;
3024
+ overflow: hidden;
3025
+ transition: border-color .15s var(--ease);
3026
+ }
3027
+ .file-card:hover { border-color: var(--border-strong); }
3028
+ .file-card-header {
3029
+ display: flex; align-items: center; justify-content: space-between;
3030
+ padding: .65rem .9rem;
3031
+ background: var(--surface2);
3032
+ cursor: pointer; user-select: none;
3033
+ transition: background .12s var(--ease);
3034
+ }
3035
+ .file-card-header:hover { background: var(--surface3); }
1341
3036
  .file-path { font-family: var(--font-mono); font-size: .78rem; color: var(--accent); font-weight: 600; }
1342
- .file-count { font-size: .68rem; color: var(--muted); background: var(--border); padding: 1px 7px; border-radius: 10px; }
1343
- .chevron { color: var(--muted); transition: transform .2s; font-size: .75rem; }
3037
+ .file-count {
3038
+ font-size: .68rem; color: var(--muted);
3039
+ background: var(--surface); border: 1px solid var(--border);
3040
+ padding: 2px 9px; border-radius: 999px; font-weight: 600;
3041
+ }
3042
+ .chevron { color: var(--muted); transition: transform .2s var(--ease); font-size: .75rem; }
1344
3043
  .file-card-header.open .chevron { transform: rotate(90deg); }
1345
- .file-card-body { display: none; border-top: 1px solid var(--border); } .file-card-body.open { display: block; }
1346
- .ann-entry { padding: .6rem .8rem; border-bottom: 1px solid var(--border); cursor: pointer; }
1347
- .ann-entry:hover { background: rgba(45,212,167,.04); } .ann-entry:last-child { border-bottom: none; }
1348
- .ann-header { display: flex; align-items: center; gap: .4rem; margin-bottom: .2rem; }
1349
- .ann-line { font-family: var(--font-mono); font-size: .68rem; color: var(--muted); min-width: 35px; }
1350
- .ann-summary { font-size: .78rem; font-weight: 500; }
1351
- .ann-desc { font-size: .75rem; color: var(--muted); margin: .15rem 0 .25rem 0; padding-left: .5rem; border-left: 2px solid var(--border); }
3044
+ .file-card-body { display: none; border-top: 1px solid var(--border); }
3045
+ .file-card-body.open { display: block; animation: fadeIn .2s var(--ease); }
3046
+ .ann-entry {
3047
+ padding: .7rem .9rem;
3048
+ border-bottom: 1px solid var(--border-subtle);
3049
+ cursor: pointer;
3050
+ transition: background .1s var(--ease);
3051
+ }
3052
+ .ann-entry:hover { background: var(--accent-soft); }
3053
+ .ann-entry:last-child { border-bottom: none; }
3054
+ .ann-header { display: flex; align-items: center; gap: .5rem; margin-bottom: .25rem; }
3055
+ .ann-line { font-family: var(--font-mono); font-size: .68rem; color: var(--muted); min-width: 38px; }
3056
+ .ann-summary { font-size: .8rem; font-weight: 550; }
3057
+ .ann-desc {
3058
+ font-size: .76rem; color: var(--muted);
3059
+ margin: .2rem 0 .3rem 0;
3060
+ padding-left: .6rem; border-left: 2px solid var(--border);
3061
+ }
1352
3062
 
1353
3063
  /* ── Diagrams ── */
1354
- .diagram-hint { font-size: .75rem; color: var(--muted); margin-bottom: .6rem; }
1355
- .mermaid-wrap { background: var(--surface); border: 1px solid var(--border); border-radius: 6px; padding: 16px; overflow: auto; margin-bottom: 1rem; }
3064
+ .diagram-hint { font-size: .78rem; color: var(--muted); margin-bottom: .7rem; }
3065
+ .diagram-shell {
3066
+ background: color-mix(in oklab, var(--surface) 92%, transparent);
3067
+ border: 1px solid var(--border);
3068
+ border-radius: var(--radius-lg);
3069
+ box-shadow: var(--shadow-sm);
3070
+ overflow: hidden;
3071
+ margin-bottom: 1rem;
3072
+ }
3073
+ .diagram-toolbar {
3074
+ display: flex;
3075
+ align-items: center;
3076
+ justify-content: space-between;
3077
+ gap: .8rem;
3078
+ padding: .65rem .8rem;
3079
+ border-bottom: 1px solid var(--border);
3080
+ background: color-mix(in oklab, var(--surface2) 92%, transparent);
3081
+ }
3082
+ .diagram-title {
3083
+ font-size: .8rem;
3084
+ font-weight: 650;
3085
+ letter-spacing: .3px;
3086
+ color: var(--text);
3087
+ text-transform: uppercase;
3088
+ }
3089
+ .diagram-actions { display: flex; align-items: center; gap: .35rem; }
3090
+ .diagram-btn {
3091
+ min-width: 28px;
3092
+ height: 28px;
3093
+ border-radius: 7px;
3094
+ border: 1px solid var(--border);
3095
+ background: var(--surface);
3096
+ color: var(--text);
3097
+ font-size: .78rem;
3098
+ font-weight: 650;
3099
+ cursor: pointer;
3100
+ transition: all .15s var(--ease);
3101
+ }
3102
+ .diagram-btn:hover { border-color: var(--border-strong); background: var(--surface3); }
3103
+ .diagram-meta {
3104
+ padding: .55rem .8rem;
3105
+ border-top: 1px solid var(--border);
3106
+ color: var(--muted);
3107
+ font-size: .74rem;
3108
+ }
3109
+ .mermaid-wrap {
3110
+ background: var(--surface);
3111
+ border: none;
3112
+ border-radius: 0;
3113
+ padding: 16px 18px;
3114
+ overflow: auto;
3115
+ margin-bottom: 0;
3116
+ box-shadow: none;
3117
+ }
1356
3118
  .mermaid { text-align: left; width: max-content; min-width: 100%; }
1357
3119
  .mermaid svg { max-width: none; height: auto; display: block; }
3120
+ .mermaid svg .cluster rect,
3121
+ .mermaid svg .cluster polygon {
3122
+ rx: 10;
3123
+ ry: 10;
3124
+ stroke-dasharray: 4 3;
3125
+ stroke-opacity: .72;
3126
+ }
3127
+ .mermaid svg .cluster-label .nodeLabel,
3128
+ .mermaid svg .cluster .cluster-label text {
3129
+ font-weight: 700 !important;
3130
+ letter-spacing: .3px;
3131
+ }
3132
+ .mermaid svg .edgeLabel { font-size: 11px !important; }
3133
+ .mermaid svg .edgeLabel rect { opacity: .92; }
3134
+ .mermaid svg .node rect,
3135
+ .mermaid svg .node polygon,
3136
+ .mermaid svg .node circle {
3137
+ filter: drop-shadow(0 2px 4px rgba(0,0,0,.18));
3138
+ }
3139
+ .mermaid svg path.flowchart-link {
3140
+ stroke-linecap: round;
3141
+ }
3142
+ .topology-shell { min-height: 720px; }
3143
+ .topology-toolbar { border-bottom-color: var(--border-subtle); }
3144
+ .topology-controls {
3145
+ display: flex;
3146
+ align-items: center;
3147
+ gap: .7rem;
3148
+ flex-wrap: wrap;
3149
+ padding: .7rem .85rem;
3150
+ border-bottom: 1px solid var(--border-subtle);
3151
+ background: color-mix(in oklab, var(--surface) 96%, transparent);
3152
+ }
3153
+ .diagram-search {
3154
+ min-width: 240px;
3155
+ flex: 1;
3156
+ max-width: 380px;
3157
+ height: 34px;
3158
+ border-radius: 8px;
3159
+ border: 1px solid var(--border);
3160
+ background: var(--surface2);
3161
+ color: var(--text);
3162
+ padding: 0 .85rem;
3163
+ font-family: var(--font-ui);
3164
+ font-size: .82rem;
3165
+ outline: none;
3166
+ transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
3167
+ }
3168
+ .diagram-search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
3169
+ .topology-kind-toggles { display: inline-flex; align-items: center; gap: .35rem; flex-wrap: wrap; }
3170
+ .topology-kind-pill {
3171
+ display: inline-flex; align-items: center; gap: .4rem;
3172
+ padding: .3rem .65rem;
3173
+ border-radius: 999px;
3174
+ border: 1px solid var(--border);
3175
+ background: var(--surface2);
3176
+ color: var(--muted);
3177
+ font-size: .72rem;
3178
+ font-weight: 600;
3179
+ cursor: pointer;
3180
+ user-select: none;
3181
+ transition: all .15s var(--ease);
3182
+ }
3183
+ .topology-kind-pill:hover { border-color: var(--border-strong); color: var(--text); }
3184
+ .topology-kind-pill:has(input:checked) { background: var(--accent-soft); border-color: var(--accent-dim); color: var(--text); }
3185
+ .topology-kind-pill.topology-kind-asset:has(input:checked) { border-color: color-mix(in oklab, var(--blue) 50%, transparent); }
3186
+ .topology-kind-pill.topology-kind-threat:has(input:checked) { border-color: color-mix(in oklab, var(--sev-crit) 50%, transparent); }
3187
+ .topology-kind-pill.topology-kind-control:has(input:checked) { border-color: color-mix(in oklab, var(--green) 50%, transparent); }
3188
+ .topology-kind-pill input { position: absolute; opacity: 0; pointer-events: none; }
3189
+ .topology-kind-pill::before {
3190
+ content: '';
3191
+ width: 8px; height: 8px; border-radius: 50%;
3192
+ background: var(--muted);
3193
+ transition: background .15s var(--ease);
3194
+ }
3195
+ .topology-kind-pill.topology-kind-asset::before { background: var(--blue); }
3196
+ .topology-kind-pill.topology-kind-threat::before { background: var(--sev-crit); }
3197
+ .topology-kind-pill.topology-kind-control::before { background: var(--green); }
3198
+ .topology-kind-pill:not(:has(input:checked))::before { background: var(--text-dim); opacity: .5; }
3199
+
3200
+ .topology-link-filters {
3201
+ display: flex;
3202
+ align-items: center;
3203
+ gap: .5rem;
3204
+ flex-wrap: wrap;
3205
+ padding: .55rem .85rem;
3206
+ border-bottom: 1px solid var(--border-subtle);
3207
+ background: color-mix(in oklab, var(--surface2) 55%, var(--surface));
3208
+ }
3209
+ .topology-link-filters-k {
3210
+ color: var(--muted);
3211
+ font-size: .65rem;
3212
+ text-transform: uppercase;
3213
+ letter-spacing: .8px;
3214
+ font-weight: 700;
3215
+ margin-right: .2rem;
3216
+ }
3217
+ .topology-link-pill {
3218
+ display: inline-flex; align-items: center; gap: .35rem;
3219
+ padding: .22rem .55rem;
3220
+ border-radius: 999px;
3221
+ border: 1px solid var(--border);
3222
+ background: var(--surface);
3223
+ color: var(--muted);
3224
+ font-size: .68rem;
3225
+ font-weight: 600;
3226
+ cursor: pointer;
3227
+ user-select: none;
3228
+ transition: all .12s var(--ease);
3229
+ }
3230
+ .topology-link-pill input { position: absolute; opacity: 0; pointer-events: none; }
3231
+ .topology-link-pill::before {
3232
+ content: '';
3233
+ display: inline-block;
3234
+ width: 14px; height: 0;
3235
+ border-top: 2px solid currentColor;
3236
+ }
3237
+ .topology-link-pill:has(input:checked) { color: var(--text); border-color: var(--border-strong); }
3238
+ .topology-link-pill:not(:has(input:checked)) { opacity: .45; }
3239
+ .topology-link-pill-exposes::before,
3240
+ .topology-link-pill-confirmed::before { border-color: var(--sev-crit); border-top-style: solid; }
3241
+ .topology-link-pill-mitigates::before,
3242
+ .topology-link-pill-protects::before,
3243
+ .topology-link-pill-validates::before { border-color: var(--green); border-top-style: solid; }
3244
+ .topology-link-pill-flows::before { border-color: var(--blue); border-top-style: dashed; }
3245
+ .topology-link-pill-boundary::before { border-color: var(--purple); border-top-style: dashed; }
3246
+ .topology-link-pill-transfers::before,
3247
+ .topology-link-pill-accepts::before { border-color: var(--sev-med); border-top-style: dashed; }
3248
+
3249
+ .topology-count {
3250
+ margin-left: auto;
3251
+ color: var(--muted);
3252
+ font-size: .7rem;
3253
+ font-family: var(--font-mono);
3254
+ white-space: nowrap;
3255
+ letter-spacing: .4px;
3256
+ }
3257
+ .topology-layout {
3258
+ display: grid;
3259
+ grid-template-columns: minmax(0, 1fr) 300px;
3260
+ min-height: 600px;
3261
+ background: var(--surface);
3262
+ }
3263
+ .topology-graph {
3264
+ min-height: 600px;
3265
+ overflow: hidden;
3266
+ border-right: 1px solid var(--border-subtle);
3267
+ background:
3268
+ radial-gradient(ellipse 600px 320px at 30% 40%, color-mix(in oklab, var(--accent) 4%, transparent), transparent 70%),
3269
+ radial-gradient(ellipse 500px 280px at 75% 70%, color-mix(in oklab, var(--sev-crit) 5%, transparent), transparent 70%),
3270
+ linear-gradient(color-mix(in oklab, var(--border-subtle) 55%, transparent) 1px, transparent 1px),
3271
+ linear-gradient(90deg, color-mix(in oklab, var(--border-subtle) 55%, transparent) 1px, transparent 1px),
3272
+ var(--surface);
3273
+ background-size: auto, auto, 36px 36px, 36px 36px, auto;
3274
+ background-position: 0 0, 0 0, -1px -1px, -1px -1px, 0 0;
3275
+ }
3276
+ .topology-graph svg { width: 100%; height: 100%; display: block; cursor: grab; }
3277
+ .topology-graph svg:active { cursor: grabbing; }
3278
+
3279
+ .topology-lane-label {
3280
+ fill: color-mix(in oklab, var(--muted) 55%, transparent);
3281
+ font-size: 10px;
3282
+ font-weight: 800;
3283
+ letter-spacing: 3.5px;
3284
+ text-transform: uppercase;
3285
+ pointer-events: none;
3286
+ user-select: none;
3287
+ }
3288
+
3289
+ .topology-link {
3290
+ stroke: var(--muted);
3291
+ stroke-width: 1.4;
3292
+ stroke-opacity: .42;
3293
+ transition: stroke-opacity .2s var(--ease), stroke-width .2s var(--ease);
3294
+ }
3295
+ .topology-link-exposes { stroke: var(--sev-crit); stroke-opacity: .58; stroke-width: 1.6; }
3296
+ .topology-link-confirmed { stroke: var(--sev-crit); stroke-opacity: .9; stroke-width: 2.6; }
3297
+ .topology-link-mitigates,
3298
+ .topology-link-validates { stroke: var(--green); stroke-opacity: .62; stroke-width: 1.6; }
3299
+ .topology-link-protects { stroke: var(--green); stroke-opacity: .48; stroke-dasharray: 4 3; }
3300
+ .topology-link-flows { stroke: var(--blue); stroke-dasharray: 6 4; stroke-opacity: .6; stroke-width: 1.5; }
3301
+ .topology-link-boundary { stroke: var(--purple); stroke-dasharray: 2 5; stroke-width: 1.9; stroke-opacity: .75; }
3302
+ .topology-link-accepts { stroke: var(--sev-med); stroke-dasharray: 6 4; stroke-opacity: .7; }
3303
+ .topology-link-transfers { stroke: var(--sev-med); stroke-dasharray: 8 3; stroke-opacity: .7; }
3304
+ .topology-link-status-mitigated { stroke-opacity: .32; }
3305
+ .topology-link.emphasis { stroke-opacity: 1 !important; stroke-width: 2.4 !important; }
3306
+ .topology-link.dim { stroke-opacity: .08 !important; }
3307
+
3308
+ .topology-node { cursor: pointer; transition: opacity .2s var(--ease); }
3309
+ .topology-node circle.topology-node-body {
3310
+ stroke: var(--border-strong);
3311
+ stroke-width: 1.4;
3312
+ filter: drop-shadow(0 4px 10px rgba(0,0,0,.28));
3313
+ transition: stroke-width .15s var(--ease), filter .15s var(--ease), r .25s var(--ease);
3314
+ }
3315
+ .topology-node:hover circle.topology-node-body,
3316
+ .topology-node.active circle.topology-node-body,
3317
+ .topology-node.emphasis circle.topology-node-body {
3318
+ stroke-width: 2.6;
3319
+ filter: drop-shadow(0 0 14px color-mix(in oklab, var(--accent) 48%, transparent));
3320
+ }
3321
+ .topology-node.active circle.topology-node-body { stroke: var(--accent); }
3322
+ .topology-node.dim { opacity: .18; }
3323
+
3324
+ .topology-node-halo {
3325
+ fill: transparent;
3326
+ stroke: var(--sev-crit);
3327
+ stroke-width: 2;
3328
+ stroke-opacity: .55;
3329
+ pointer-events: none;
3330
+ filter: url(#topology-glow);
3331
+ animation: topology-pulse 1.9s ease-in-out infinite;
3332
+ }
3333
+ .topology-fully-covered .topology-node-halo { display: none; }
3334
+
3335
+ @keyframes topology-pulse {
3336
+ 0%, 100% { stroke-opacity: .3; transform: scale(1); }
3337
+ 50% { stroke-opacity: .75; transform: scale(1.08); }
3338
+ }
3339
+
3340
+ .topology-coverage-arc {
3341
+ fill: none;
3342
+ stroke: var(--green);
3343
+ stroke-width: 2.5;
3344
+ stroke-linecap: round;
3345
+ opacity: .85;
3346
+ pointer-events: none;
3347
+ }
3348
+
3349
+ .topology-asset circle.topology-node-body { fill: color-mix(in oklab, var(--blue) 60%, var(--surface)); }
3350
+ .topology-threat circle.topology-node-body { fill: color-mix(in oklab, var(--sev-med) 60%, var(--surface)); }
3351
+ .topology-control circle.topology-node-body { fill: color-mix(in oklab, var(--green) 58%, var(--surface)); }
3352
+ .topology-has-open circle.topology-node-body { stroke: var(--sev-crit); }
3353
+ .topology-has-confirmed circle.topology-node-body { stroke: var(--sev-crit); stroke-width: 2; }
3354
+ .topology-fully-covered circle.topology-node-body { stroke: var(--green); }
3355
+
3356
+ .topology-sev-critical circle.topology-node-body,
3357
+ .topology-sev-p0 circle.topology-node-body { fill: color-mix(in oklab, var(--sev-crit) 72%, var(--surface)); }
3358
+ .topology-sev-high circle.topology-node-body,
3359
+ .topology-sev-p1 circle.topology-node-body { fill: color-mix(in oklab, var(--red) 60%, var(--surface)); }
3360
+
3361
+ .topology-node-icon {
3362
+ fill: #fff;
3363
+ font-size: 11px;
3364
+ font-weight: 800;
3365
+ letter-spacing: .4px;
3366
+ pointer-events: none;
3367
+ }
3368
+ .topology-node-label {
3369
+ fill: var(--text);
3370
+ paint-order: stroke;
3371
+ stroke: var(--surface);
3372
+ stroke-width: 4px;
3373
+ stroke-linejoin: round;
3374
+ font-size: 11px;
3375
+ font-weight: 650;
3376
+ pointer-events: none;
3377
+ }
3378
+ .topology-node.emphasis .topology-node-label { font-weight: 750; }
3379
+
3380
+ .topology-inspector {
3381
+ padding: 1rem .95rem;
3382
+ overflow-y: auto;
3383
+ background: color-mix(in oklab, var(--surface2) 78%, var(--surface));
3384
+ border-left: 1px solid var(--border-subtle);
3385
+ }
3386
+ .topology-inspector-head {
3387
+ display: flex;
3388
+ align-items: center;
3389
+ justify-content: space-between;
3390
+ gap: .6rem;
3391
+ margin-bottom: .35rem;
3392
+ }
3393
+ .topology-inspector-k {
3394
+ color: var(--muted);
3395
+ font-size: .66rem;
3396
+ text-transform: uppercase;
3397
+ letter-spacing: .8px;
3398
+ font-weight: 700;
3399
+ }
3400
+ .topology-badge {
3401
+ font-size: .62rem;
3402
+ font-weight: 800;
3403
+ text-transform: uppercase;
3404
+ letter-spacing: .5px;
3405
+ padding: 2px 7px;
3406
+ border-radius: 999px;
3407
+ border: 1px solid transparent;
3408
+ }
3409
+ .topology-badge-open { background: var(--sev-crit-bg); color: var(--sev-crit); border-color: color-mix(in oklab, var(--sev-crit) 35%, transparent); }
3410
+ .topology-badge-confirmed { background: var(--sev-crit-bg); color: var(--sev-crit); border-color: var(--sev-crit); animation: topology-pulse 2s ease-in-out infinite; }
3411
+ .topology-badge-covered { background: color-mix(in oklab, var(--green) 15%, transparent); color: var(--green); border-color: color-mix(in oklab, var(--green) 40%, transparent); }
3412
+
3413
+ .topology-inspector h3 {
3414
+ font-size: 1rem;
3415
+ line-height: 1.25;
3416
+ margin-bottom: .75rem;
3417
+ word-break: break-word;
3418
+ }
3419
+ .topology-bar-wrap { margin: 0 0 .9rem 0; }
3420
+ .topology-bar-label {
3421
+ display: flex; justify-content: space-between; align-items: baseline;
3422
+ font-size: .68rem;
3423
+ color: var(--muted);
3424
+ text-transform: uppercase;
3425
+ letter-spacing: .8px;
3426
+ font-weight: 700;
3427
+ margin-bottom: .3rem;
3428
+ }
3429
+ .topology-bar-label strong { color: var(--text); font-family: var(--font-mono); }
3430
+ .topology-bar {
3431
+ height: 7px;
3432
+ border-radius: 999px;
3433
+ background: var(--surface3);
3434
+ overflow: hidden;
3435
+ }
3436
+ .topology-bar-fill { height: 100%; border-radius: 999px; transition: width .5s var(--ease); }
3437
+ .topology-bar-fill.good { background: linear-gradient(90deg, color-mix(in oklab, var(--green) 70%, transparent), var(--green)); }
3438
+ .topology-bar-fill.warn { background: linear-gradient(90deg, color-mix(in oklab, var(--sev-med) 65%, transparent), var(--sev-med)); }
3439
+ .topology-bar-fill.bad { background: linear-gradient(90deg, color-mix(in oklab, var(--sev-crit) 65%, transparent), var(--sev-crit)); }
3440
+
3441
+ .topology-detail-grid {
3442
+ display: grid;
3443
+ grid-template-columns: 88px 1fr;
3444
+ gap: .4rem .7rem;
3445
+ align-items: baseline;
3446
+ font-size: .78rem;
3447
+ }
3448
+ .topology-detail-grid span { color: var(--muted); }
3449
+ .topology-detail-grid strong { color: var(--text); font-weight: 700; word-break: break-word; }
3450
+ .topology-detail-grid .danger,
3451
+ .topology-detail-grid .status-open,
3452
+ .topology-detail-grid .status-confirmed,
3453
+ .topology-detail-grid .sev-critical,
3454
+ .topology-detail-grid .sev-p0,
3455
+ .topology-detail-grid .sev-high,
3456
+ .topology-detail-grid .sev-p1 { color: var(--sev-crit); }
3457
+ .topology-detail-grid .good,
3458
+ .topology-detail-grid .status-mitigated { color: var(--green); }
3459
+ .topology-chip-row {
3460
+ display: flex;
3461
+ flex-wrap: wrap;
3462
+ gap: .35rem;
3463
+ margin-top: .9rem;
3464
+ }
3465
+ .topology-chip-row span {
3466
+ border: 1px solid var(--accent-dim);
3467
+ background: var(--accent-soft);
3468
+ color: var(--accent);
3469
+ border-radius: 999px;
3470
+ padding: 2px 8px;
3471
+ font-size: .65rem;
3472
+ font-weight: 700;
3473
+ }
3474
+ .topology-related-title {
3475
+ margin: 1rem 0 .45rem;
3476
+ color: var(--muted);
3477
+ font-size: .68rem;
3478
+ text-transform: uppercase;
3479
+ letter-spacing: .7px;
3480
+ font-weight: 700;
3481
+ }
3482
+ .topology-related { display: flex; flex-direction: column; gap: .35rem; }
3483
+ .topology-related-row {
3484
+ display: grid;
3485
+ grid-template-columns: 88px 1fr auto;
3486
+ gap: .45rem;
3487
+ align-items: center;
3488
+ padding: .42rem .55rem;
3489
+ border: 1px solid var(--border-subtle);
3490
+ border-radius: 7px;
3491
+ background: var(--surface);
3492
+ font-size: .72rem;
3493
+ transition: border-color .12s var(--ease);
3494
+ }
3495
+ .topology-related-row:hover { border-color: var(--border-strong); }
3496
+ .topology-related-exposes,
3497
+ .topology-related-confirmed { border-left: 2px solid var(--sev-crit); }
3498
+ .topology-related-mitigates,
3499
+ .topology-related-protects,
3500
+ .topology-related-validates { border-left: 2px solid var(--green); }
3501
+ .topology-related-flows { border-left: 2px solid var(--blue); }
3502
+ .topology-related-boundary { border-left: 2px solid var(--purple); }
3503
+ .topology-related-transfers,
3504
+ .topology-related-accepts { border-left: 2px solid var(--sev-med); }
3505
+ .topology-related-kind {
3506
+ color: var(--muted);
3507
+ font-family: var(--font-mono);
3508
+ font-size: .64rem;
3509
+ font-weight: 600;
3510
+ }
3511
+ .topology-related-row em { color: var(--muted); font-style: normal; font-family: var(--font-mono); }
3512
+ .topology-inspector-note {
3513
+ margin-top: .9rem;
3514
+ color: var(--muted);
3515
+ font-size: .74rem;
3516
+ line-height: 1.5;
3517
+ }
3518
+ .topology-legend {
3519
+ display: flex;
3520
+ align-items: center;
3521
+ gap: .9rem;
3522
+ flex-wrap: wrap;
3523
+ }
3524
+ .topology-legend span { display: inline-flex; align-items: center; gap: .35rem; }
3525
+ .legend-dot {
3526
+ width: 10px;
3527
+ height: 10px;
3528
+ border-radius: 50%;
3529
+ display: inline-block;
3530
+ border: 1px solid color-mix(in oklab, currentColor 30%, transparent);
3531
+ }
3532
+ .legend-dot.asset { background: var(--blue); }
3533
+ .legend-dot.threat { background: var(--sev-med); }
3534
+ .legend-dot.control { background: var(--green); }
3535
+ .legend-dot.confirmed { background: var(--sev-crit); box-shadow: 0 0 8px color-mix(in oklab, var(--sev-crit) 60%, transparent); }
3536
+ .legend-line {
3537
+ display: inline-block;
3538
+ width: 20px;
3539
+ height: 0;
3540
+ border-top: 2px solid var(--muted);
3541
+ }
3542
+ .legend-line.exposes { border-color: var(--sev-crit); }
3543
+ .legend-line.open { border-color: var(--sev-crit); }
3544
+ .legend-line.mitigates { border-color: var(--green); }
3545
+ .legend-line.flow { border-color: var(--blue); border-top-style: dashed; }
3546
+ .legend-line.boundary { border-color: var(--purple); border-top-style: dashed; }
1358
3547
 
1359
3548
  /* ── Heatmap ── */
1360
- .heatmap { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px; }
1361
- .heatmap-cell { border-radius: 8px; padding: 12px; border: 1px solid var(--border); transition: border-color 0.15s; }
3549
+ .heatmap { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
3550
+ .heatmap-cell {
3551
+ border-radius: var(--radius-md);
3552
+ padding: 14px;
3553
+ border: 1px solid var(--border);
3554
+ transition: all .15s var(--ease);
3555
+ box-shadow: var(--shadow-sm);
3556
+ }
1362
3557
  .heatmap-cell.clickable { cursor: pointer; }
1363
- .heatmap-cell.clickable:hover { border-color: var(--accent); }
1364
- .heatmap-name { font-weight: 600; font-size: 13px; margin-bottom: 4px; font-family: var(--font-mono); word-break: break-all; }
1365
- .heatmap-stats { display: flex; gap: 10px; font-size: 12px; color: var(--muted); }
1366
- .heatmap-data { margin-top: 4px; display: flex; gap: 4px; flex-wrap: wrap; }
1367
- .data-badge { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: rgba(45,212,167,.15); color: var(--accent); font-weight: 600; text-transform: uppercase; }
1368
- .risk-cell-critical { background: var(--heatmap-crit); } .risk-cell-high { background: var(--heatmap-high); }
1369
- .risk-cell-medium { background: var(--heatmap-med); } .risk-cell-low { background: var(--heatmap-low); }
1370
- .risk-cell-none { background: var(--heatmap-none); }
3558
+ .heatmap-cell.clickable:hover {
3559
+ border-color: var(--border-strong);
3560
+ transform: translateY(-2px);
3561
+ box-shadow: var(--shadow-md);
3562
+ }
3563
+ .heatmap-name { font-weight: 650; font-size: 13px; margin-bottom: 6px; font-family: var(--font-mono); word-break: break-all; color: var(--text); }
3564
+ .heatmap-stats { display: flex; gap: 12px; font-size: 12px; color: var(--muted); }
3565
+ .heatmap-data { margin-top: 6px; display: flex; gap: 4px; flex-wrap: wrap; }
3566
+ .data-badge {
3567
+ font-size: 10px; padding: 2px 7px;
3568
+ border-radius: 999px;
3569
+ background: var(--accent-soft);
3570
+ color: var(--accent);
3571
+ border: 1px solid var(--accent-dim);
3572
+ font-weight: 650; text-transform: uppercase; letter-spacing: .3px;
3573
+ }
3574
+ .risk-cell-critical { background: var(--heatmap-crit); border-color: var(--risk-border-f); }
3575
+ .risk-cell-high { background: var(--heatmap-high); border-color: var(--risk-border-d); }
3576
+ .risk-cell-medium { background: var(--heatmap-med); border-color: var(--risk-border-c); }
3577
+ .risk-cell-low { background: var(--heatmap-low); border-color: var(--risk-border-b); }
3578
+ .risk-cell-none { background: var(--heatmap-none); }
1371
3579
 
1372
3580
  /* ── Code Blocks ── */
1373
- .code-block { background: var(--bg); border: 1px solid var(--border); border-radius: 5px; padding: .3rem .6rem; overflow-x: auto; margin-top: .25rem; font-family: var(--font-mono); font-size: .72rem; line-height: 1.45; tab-size: 2; }
3581
+ .code-block {
3582
+ background: var(--bg);
3583
+ border: 1px solid var(--border);
3584
+ border-radius: var(--radius-md);
3585
+ padding: .4rem .7rem;
3586
+ overflow-x: auto;
3587
+ margin-top: .3rem;
3588
+ font-family: var(--font-mono);
3589
+ font-size: .72rem; line-height: 1.5;
3590
+ tab-size: 2;
3591
+ }
1374
3592
  .code-line-code { display: block; color: var(--muted); white-space: pre; }
1375
- .code-line-ann { display: block; color: var(--accent); background: rgba(45,212,167,.08); margin: 0 -.6rem; padding: 0 .6rem; border-left: 2px solid var(--accent); white-space: pre; }
3593
+ .code-line-ann {
3594
+ display: block; color: var(--accent);
3595
+ background: var(--accent-soft);
3596
+ margin: 0 -.7rem; padding: 0 .7rem;
3597
+ border-left: 2px solid var(--accent);
3598
+ white-space: pre;
3599
+ }
1376
3600
 
1377
3601
  /* ── Diagram Tabs ── */
1378
- .diagram-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 1rem; }
1379
- .diagram-tab { background: none; border: none; border-bottom: 2px solid transparent; padding: .5rem 1rem; color: var(--muted); font-size: .82rem; cursor: pointer; font-family: var(--font-ui); transition: all .15s; }
3602
+ .diagram-tabs { display: flex; gap: .25rem; border-bottom: 1px solid var(--border); margin-bottom: 1rem; padding: 0 .2rem; }
3603
+ .diagram-tab {
3604
+ background: none; border: none; border-bottom: 2px solid transparent;
3605
+ padding: .55rem 1rem; color: var(--muted);
3606
+ font-size: .82rem; font-weight: 550;
3607
+ cursor: pointer; font-family: var(--font-ui);
3608
+ transition: all .15s var(--ease);
3609
+ border-radius: 6px 6px 0 0;
3610
+ }
1380
3611
  .diagram-tab:hover { color: var(--text); background: var(--surface2); }
1381
- .diagram-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
3612
+ .diagram-tab.active {
3613
+ color: var(--accent);
3614
+ border-bottom-color: var(--accent);
3615
+ background: var(--accent-soft);
3616
+ }
1382
3617
  .diagram-panel { display: none; } .diagram-panel.active { display: block; }
1383
3618
 
1384
3619
  /* ── AI Analysis Controls ── */
1385
- .ai-analysis-controls { display: flex; align-items: center; gap: 0.75rem; margin: 0.75rem 0 1.25rem; }
1386
- .report-selector-label { font-weight: 600; font-size: 0.88rem; color: var(--text); }
1387
- .report-selector { flex: 1; max-width: 600px; padding: 0.5rem 0.75rem; font-size: 0.88rem; font-family: var(--font-base); background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; transition: border-color 0.15s, background 0.15s; }
1388
- .report-selector:hover { background: var(--surface3); border-color: var(--accent); }
1389
- .report-selector:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(45,212,167,0.1); }
1390
- .report-selector option { background: var(--surface); color: var(--text); padding: 0.5rem; }
1391
- .ai-analysis-main { margin-top: 0.5rem; }
1392
- .md-content h1 { font-size: 1.4rem; font-weight: 700; margin: 1.2rem 0 .6rem; color: var(--text); }
1393
- .md-content h2 { font-size: 1.15rem; font-weight: 600; margin: 1rem 0 .5rem; color: var(--text); border-bottom: 1px solid var(--border); padding-bottom: .3rem; }
1394
- .md-content h3 { font-size: 1rem; font-weight: 600; margin: .8rem 0 .4rem; color: var(--text); }
1395
- .md-content p { margin: .4rem 0; line-height: 1.6; }
1396
- .md-content ul, .md-content ol { margin: .4rem 0 .4rem 1.5rem; }
1397
- .md-content li { margin: .2rem 0; line-height: 1.5; }
1398
- .md-content code { font-family: var(--font-mono); font-size: .82rem; background: var(--surface2); padding: 1px 5px; border-radius: 3px; }
1399
- .md-content pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: .8rem; overflow-x: auto; margin: .6rem 0; }
1400
- .md-content pre code { background: none; padding: 0; }
1401
- .md-content blockquote { border-left: 3px solid var(--accent); padding-left: .8rem; margin: .6rem 0; color: var(--muted); }
1402
- .md-content table { width: 100%; border-collapse: collapse; margin: .6rem 0; font-size: .82rem; }
1403
- .md-content th, .md-content td { padding: .4rem .6rem; border: 1px solid var(--border); text-align: left; }
1404
- .md-content th { background: var(--surface2); font-weight: 600; }
1405
- .md-content strong { color: var(--text); }
3620
+ .ai-analysis-controls { display: flex; align-items: center; gap: .85rem; margin: .75rem 0 1.5rem; }
3621
+ .report-selector-label { font-weight: 600; font-size: .85rem; color: var(--text); }
3622
+ .report-selector {
3623
+ flex: 1; max-width: 600px;
3624
+ padding: .55rem .85rem;
3625
+ font-size: .88rem; font-family: var(--font-ui);
3626
+ background: var(--surface);
3627
+ color: var(--text);
3628
+ border: 1px solid var(--border);
3629
+ border-radius: 8px;
3630
+ cursor: pointer;
3631
+ transition: all .15s var(--ease);
3632
+ box-shadow: var(--shadow-sm);
3633
+ }
3634
+ .report-selector:hover { background: var(--surface2); border-color: var(--border-strong); }
3635
+ .report-selector:focus {
3636
+ outline: none; border-color: var(--accent);
3637
+ box-shadow: 0 0 0 3px var(--accent-soft);
3638
+ }
3639
+ .report-selector option { background: var(--surface); color: var(--text); padding: .5rem; }
3640
+ .ai-analysis-main { margin-top: .5rem; }
3641
+
3642
+ /* ── Markdown content ── */
3643
+ .md-content h1 { font-size: 1.5rem; font-weight: 700; margin: 1.3rem 0 .7rem; color: var(--text); letter-spacing: -.02em; }
3644
+ .md-content h2 {
3645
+ font-size: 1.2rem; font-weight: 650; margin: 1.2rem 0 .6rem;
3646
+ color: var(--text); border-bottom: 1px solid var(--border);
3647
+ padding-bottom: .4rem; letter-spacing: -.01em;
3648
+ }
3649
+ .md-content h3 { font-size: 1.02rem; font-weight: 650; margin: 1rem 0 .45rem; color: var(--text); letter-spacing: -.01em; }
3650
+ .md-content p { margin: .5rem 0; line-height: 1.65; color: var(--text); }
3651
+ .md-content ul, .md-content ol { margin: .5rem 0 .5rem 1.6rem; }
3652
+ .md-content li { margin: .25rem 0; line-height: 1.6; }
3653
+ .md-content code { font-family: var(--font-mono); font-size: .82rem; background: var(--surface2); border: 1px solid var(--border-subtle); padding: 1px 6px; border-radius: 4px; }
3654
+ .md-content pre { background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius-md); padding: .9rem 1rem; overflow-x: auto; margin: .8rem 0; box-shadow: var(--shadow-sm); }
3655
+ .md-content pre code { background: none; padding: 0; border: none; font-size: .8rem; }
3656
+ .md-content blockquote { border-left: 3px solid var(--accent); padding: .1rem 0 .1rem .9rem; margin: .7rem 0; color: var(--muted); background: var(--accent-soft); border-radius: 0 6px 6px 0; }
3657
+ .md-content table { width: 100%; border-collapse: separate; border-spacing: 0; margin: .7rem 0; font-size: .82rem; border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; }
3658
+ .md-content th, .md-content td { padding: .5rem .75rem; border-bottom: 1px solid var(--border-subtle); text-align: left; }
3659
+ .md-content tr:last-child td { border-bottom: none; }
3660
+ .md-content th { background: var(--surface2); font-weight: 650; color: var(--muted); text-transform: uppercase; font-size: .68rem; letter-spacing: .5px; }
3661
+ .md-content strong { color: var(--text); font-weight: 650; }
1406
3662
 
1407
3663
  /* ── Responsive ── */
3664
+ @media (max-width: 900px) {
3665
+ .section-content { padding: 1.2rem 1rem 2rem; }
3666
+ .summary-panels { grid-template-columns: 1fr; }
3667
+ .topology-layout { grid-template-columns: 1fr; }
3668
+ .topology-graph { border-right: none; border-bottom: 1px solid var(--border); }
3669
+ .topology-inspector { min-height: 220px; }
3670
+ }
1408
3671
  @media (max-width: 768px) {
1409
- .sidebar { width: 50px; min-width: 50px; } .sidebar .nav-text { display: none; }
1410
- .topnav .tn-stat { display: none; }
3672
+ .sidebar { width: 52px; min-width: 52px; } .sidebar .nav-text { display: none; }
3673
+ .topnav .topnav-metrics { display: none; }
3674
+ .feature-filter-select { max-width: 130px; }
3675
+ .risk-banner { flex-direction: column; align-items: flex-start; gap: 12px; }
3676
+ .diagram-search { min-width: 100%; max-width: none; }
3677
+ .topology-count { margin-left: 0; width: 100%; }
3678
+ :root { --drawer-w: 100vw; }
3679
+ }
3680
+ @media print {
3681
+ .topnav, .sidebar, #sidebarToggle, #themeToggle { display: none; }
3682
+ .main { margin: 0; } .layout { display: block; }
3683
+ body { overflow: auto; height: auto; background: #fff; color: #000; }
1411
3684
  }
1412
- @media print { .topnav, .sidebar, #sidebarToggle { display: none; } .main { margin: 0; } .layout { display: block; } #themeToggle { display: none; } }
1413
3685
  `;
1414
3686
  //# sourceMappingURL=generate.js.map