@geotechcli/core 0.4.35 → 0.4.37

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.
@@ -1,6 +1,6 @@
1
1
  import { GEOTECHCLI_VERSION } from '../meta/index.js';
2
2
  function escapeHtml(value) {
3
- return (value ?? '')
3
+ return String(value ?? '')
4
4
  .replace(/&/g, '&')
5
5
  .replace(/</g, '&lt;')
6
6
  .replace(/>/g, '&gt;')
@@ -28,6 +28,20 @@ function toneClass(tone) {
28
28
  return 'tone-neutral';
29
29
  }
30
30
  }
31
+ function toneColor(tone) {
32
+ switch (tone) {
33
+ case 'good':
34
+ return '#d9f5e8';
35
+ case 'warning':
36
+ return '#f7e4bd';
37
+ case 'danger':
38
+ return '#f9d3cf';
39
+ case 'accent':
40
+ return '#d8e9f7';
41
+ default:
42
+ return '#e8edf3';
43
+ }
44
+ }
31
45
  function parsePercent(value) {
32
46
  const match = value.match(/^(\d+(?:\.\d+)?)%$/);
33
47
  if (!match) {
@@ -39,65 +53,46 @@ function parsePercent(value) {
39
53
  function isOperationalAuditTable(title) {
40
54
  return title === 'Page audit matrix' || title === 'Segment execution';
41
55
  }
56
+ function sourceModeLabel(sourceHint) {
57
+ switch (sourceHint) {
58
+ case 'native-text':
59
+ case 'pdfjs-text':
60
+ return 'Text extraction used';
61
+ case 'local-ocr':
62
+ case 'glm-ocr':
63
+ return 'Layout/OCR extraction used';
64
+ case 'vision-ocr':
65
+ case 'vision-visual':
66
+ return 'Visual extraction used';
67
+ default:
68
+ return 'Extraction evidence retained';
69
+ }
70
+ }
71
+ function compactSvgText(value, maxLength = 28) {
72
+ const normalized = (value ?? '').replace(/\s+/g, ' ').trim();
73
+ if (normalized.length <= maxLength) {
74
+ return normalized;
75
+ }
76
+ return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
77
+ }
42
78
  function renderMetric(metric) {
43
79
  const percent = parsePercent(metric.value);
44
80
  return `
45
- <article class="metric ${toneClass(metric.tone)}">
46
- <span class="metric-label">${escapeHtml(metric.label)}</span>
47
- <strong class="metric-value">${escapeHtml(metric.value)}</strong>
81
+ <article class="metric-card ${toneClass(metric.tone)}">
82
+ <span>${escapeHtml(metric.label)}</span>
83
+ <strong>${escapeHtml(metric.value)}</strong>
48
84
  ${percent != null ? `
49
85
  <span class="meter" aria-label="${escapeHtml(metric.label)} ${escapeHtml(metric.value)}">
50
- <span style="width: ${escapeHtml(String(percent))}%"></span>
86
+ <span style="width: ${escapeHtml(percent)}%"></span>
51
87
  </span>
52
88
  ` : ''}
53
- ${metric.detail ? `<span class="metric-detail">${escapeHtml(metric.detail)}</span>` : ''}
89
+ ${metric.detail ? `<small>${escapeHtml(metric.detail)}</small>` : ''}
54
90
  </article>
55
91
  `;
56
92
  }
57
- function renderPageOutcomeStrip(dossier) {
58
- if (dossier.pageCards.length === 0) {
59
- return '';
60
- }
61
- const counts = dossier.pageCards.reduce((acc, page) => {
62
- if (page.parseStatus === 'parsed')
63
- acc.parsed += 1;
64
- else if (page.parseStatus === 'partial')
65
- acc.partial += 1;
66
- else
67
- acc.failed += 1;
68
- return acc;
69
- }, { parsed: 0, partial: 0, failed: 0 });
70
- return `
71
- <section class="panel section-card overview" id="extraction-overview">
72
- <div class="section-head">
73
- <h2>Extraction overview</h2>
74
- <p>Page outcomes at a glance. Each cell links the dossier confidence to the exact page-level status.</p>
75
- </div>
76
- <div class="outcome-strip" aria-label="Page extraction outcomes">
77
- ${dossier.pageCards.map((card) => `
78
- <span
79
- class="outcome-cell ${toneClass(card.tone)}"
80
- title="${escapeHtml(`${card.pageLabel}: ${card.parseStatus}, ${card.confidence}% confidence`)}"
81
- >${escapeHtml(card.pageLabel.replace(/^Page\s+/i, ''))}</span>
82
- `).join('')}
83
- </div>
84
- <div class="legend-row">
85
- <span><strong>${escapeHtml(String(counts.parsed))}</strong> parsed</span>
86
- <span><strong>${escapeHtml(String(counts.partial))}</strong> partial</span>
87
- <span><strong>${escapeHtml(String(counts.failed))}</strong> failed</span>
88
- </div>
89
- </section>
90
- `;
91
- }
92
- function renderTable(table) {
93
+ function renderTable(table, className = 'data-section') {
93
94
  const tableId = escapeHtml(idFromTitle(table.title));
94
- const tableHeader = `
95
- <div class="section-head">
96
- <h2>${escapeHtml(table.title)}</h2>
97
- ${table.description ? `<p>${escapeHtml(table.description)}</p>` : ''}
98
- </div>
99
- `;
100
- const tableBody = table.rows.length === 0
95
+ const body = table.rows.length === 0
101
96
  ? `<div class="empty-state">${escapeHtml(table.emptyState ?? 'No data available.')}</div>`
102
97
  : `
103
98
  <div class="table-shell">
@@ -111,208 +106,482 @@ function renderTable(table) {
111
106
  </table>
112
107
  </div>
113
108
  `;
114
- const content = `${tableHeader}${tableBody}`;
115
- if (isOperationalAuditTable(table.title)) {
109
+ return `
110
+ <section class="${className}" id="${tableId}">
111
+ <div class="section-heading">
112
+ <h2>${escapeHtml(table.title)}</h2>
113
+ ${table.description ? `<p>${escapeHtml(table.description)}</p>` : ''}
114
+ </div>
115
+ ${body}
116
+ </section>
117
+ `;
118
+ }
119
+ function renderTrustTable(dossier) {
120
+ const rows = dossier.trustItems.length === 0
121
+ ? '<tr><td colspan="6">No trust-layer rows were retained.</td></tr>'
122
+ : dossier.trustItems.map((item) => `
123
+ <tr>
124
+ <td><strong>${escapeHtml(item.item)}</strong></td>
125
+ <td>${escapeHtml(item.value)}</td>
126
+ <td>${escapeHtml(item.sourcePage)}</td>
127
+ <td><span class="status-pill ${toneClass(item.tone)}">${escapeHtml(item.confidence)}</span></td>
128
+ <td>${escapeHtml(item.review)}</td>
129
+ <td>${escapeHtml(item.evidence)}</td>
130
+ </tr>
131
+ `).join('');
132
+ return `
133
+ <section class="data-section" id="parameters">
134
+ <div class="section-heading">
135
+ <h2>Engineering Parameters</h2>
136
+ <p>Evidence-first review table. Every retained or missing item is shown with source, confidence, review posture, and an evidence snippet.</p>
137
+ </div>
138
+ <div class="table-shell trust-table">
139
+ <table>
140
+ <thead>
141
+ <tr>
142
+ <th>Item</th>
143
+ <th>Value</th>
144
+ <th>Source page</th>
145
+ <th>Confidence</th>
146
+ <th>Needs review?</th>
147
+ <th>Evidence snippet</th>
148
+ </tr>
149
+ </thead>
150
+ <tbody>${rows}</tbody>
151
+ </table>
152
+ </div>
153
+ </section>
154
+ `;
155
+ }
156
+ function renderBoreholeProfile(profile) {
157
+ if (!profile || profile.columns.length === 0 || profile.maxDepth <= 0) {
116
158
  return `
117
- <details class="panel section-card disclosure" id="${tableId}">
118
- <summary>
119
- ${tableHeader}
120
- <span class="disclosure-hint">Show audit table</span>
121
- </summary>
122
- ${tableBody}
123
- </details>
159
+ <section class="data-section" id="boreholes">
160
+ <div class="section-heading">
161
+ <h2>Borehole Stratigraphy Visualization</h2>
162
+ <p>No scaled borehole profile could be built from the retained evidence.</p>
163
+ </div>
164
+ <div class="empty-state">Borehole IDs, layer depths, or material intervals were not available in structured form.</div>
165
+ </section>
124
166
  `;
125
167
  }
126
- if (table.rows.length === 0) {
168
+ const plotTop = 44;
169
+ const plotHeight = 420;
170
+ const columnWidth = 132;
171
+ const gap = 42;
172
+ const axisWidth = 78;
173
+ const width = axisWidth + profile.columns.length * columnWidth + Math.max(0, profile.columns.length - 1) * gap + 42;
174
+ const height = plotTop + plotHeight + 74;
175
+ const ticks = Array.from({ length: 6 }, (_value, index) => Number((profile.maxDepth * index / 5).toFixed(2)));
176
+ const layerRect = (layer, columnIndex) => {
177
+ const x = axisWidth + columnIndex * (columnWidth + gap);
178
+ const y = plotTop + (Math.max(0, layer.depthFrom) / profile.maxDepth) * plotHeight;
179
+ const rectHeight = Math.max(18, ((Math.min(profile.maxDepth, layer.depthTo) - Math.max(0, layer.depthFrom)) / profile.maxDepth) * plotHeight);
180
+ const midY = y + rectHeight / 2;
181
+ return `
182
+ <g>
183
+ <title>${escapeHtml(`${layer.depthFrom.toFixed(2)}-${layer.depthTo.toFixed(2)} ${profile.depthUnit}: ${layer.description}`)}</title>
184
+ <rect x="${x}" y="${y.toFixed(2)}" width="${columnWidth}" height="${rectHeight.toFixed(2)}" rx="8"
185
+ fill="${toneColor(layer.tone)}" stroke="#7a8aa0" stroke-width="1.2" ${layer.uncertain ? 'stroke-dasharray="6 5"' : ''} />
186
+ <text x="${x + 10}" y="${midY.toFixed(2)}" class="profile-label">${escapeHtml(layer.label)}</text>
187
+ <text x="${x + 10}" y="${(midY + 16).toFixed(2)}" class="profile-small">${escapeHtml(compactSvgText(layer.description))}</text>
188
+ </g>
189
+ `;
190
+ };
191
+ return `
192
+ <section class="data-section" id="boreholes">
193
+ <div class="section-heading">
194
+ <h2>${escapeHtml(profile.title)}</h2>
195
+ <p>Depth-scaled borehole columns with colored stratigraphy blocks. Dashed boundaries indicate missing or uncertain intervals that need source-page verification.</p>
196
+ </div>
197
+ <div class="profile-shell">
198
+ <svg class="borehole-profile" viewBox="0 0 ${width} ${height}" role="img" aria-label="Borehole stratigraphy profile">
199
+ <rect x="0" y="0" width="${width}" height="${height}" rx="18" fill="#ffffff" />
200
+ ${ticks.map((tick) => {
201
+ const y = plotTop + (tick / profile.maxDepth) * plotHeight;
202
+ return `
203
+ <line x1="48" y1="${y.toFixed(2)}" x2="${width - 24}" y2="${y.toFixed(2)}" stroke="#d8e0ea" stroke-width="1" />
204
+ <text x="8" y="${(y + 4).toFixed(2)}" class="profile-axis">${escapeHtml(tick.toFixed(tick % 1 === 0 ? 0 : 1))} ${escapeHtml(profile.depthUnit)}</text>
205
+ `;
206
+ }).join('')}
207
+ ${profile.columns.map((column, columnIndex) => {
208
+ const x = axisWidth + columnIndex * (columnWidth + gap);
209
+ const waterY = column.waterTableDepth != null
210
+ ? plotTop + (column.waterTableDepth / profile.maxDepth) * plotHeight
211
+ : null;
127
212
  return `
128
- <section class="panel section-card" id="${tableId}">
129
- ${content}
213
+ <text x="${x}" y="24" class="profile-title">${escapeHtml(column.boreholeId)}</text>
214
+ ${column.layers.map((layer) => layerRect(layer, columnIndex)).join('')}
215
+ ${waterY != null ? `
216
+ <line x1="${x - 8}" y1="${waterY.toFixed(2)}" x2="${x + columnWidth + 8}" y2="${waterY.toFixed(2)}" stroke="#0b4f8a" stroke-width="2" />
217
+ <text x="${x + 8}" y="${(waterY - 6).toFixed(2)}" class="profile-water">Groundwater</text>
218
+ ` : ''}
219
+ <text x="${x}" y="${height - 24}" class="profile-small">TD ${escapeHtml(column.totalDepth != null ? `${column.totalDepth.toFixed(2)} m` : 'unavailable')}</text>
220
+ `;
221
+ }).join('')}
222
+ </svg>
223
+ </div>
224
+ ${profile.notes.length > 0 ? `<ul class="profile-notes">${profile.notes.map((note) => `<li>${escapeHtml(note)}</li>`).join('')}</ul>` : ''}
225
+ </section>
226
+ `;
227
+ }
228
+ function renderFindingGroups(dossier) {
229
+ if (dossier.findings.length === 0) {
230
+ return `
231
+ <section class="data-section" id="risks">
232
+ <div class="section-heading">
233
+ <h2>Risks and Limitations</h2>
234
+ <p>No blocking review finding was retained. Source verification is still recommended before engineering reuse.</p>
235
+ </div>
130
236
  </section>
131
237
  `;
132
238
  }
133
239
  return `
134
- <section class="panel section-card" id="${tableId}">
135
- ${content}
240
+ <section class="data-section" id="risks">
241
+ <div class="section-heading">
242
+ <h2>Risks and Limitations</h2>
243
+ <p>Review gates and limitations are grouped for engineering decision-making, not as raw extraction logs.</p>
244
+ </div>
245
+ <div class="card-grid">
246
+ ${dossier.findings.map((group) => `
247
+ <article class="insight-card ${toneClass(group.tone)}">
248
+ <h3>${escapeHtml(group.label)}</h3>
249
+ <ul>${group.items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
250
+ </article>
251
+ `).join('')}
252
+ </div>
253
+ </section>
254
+ `;
255
+ }
256
+ function renderSourceEvidence(dossier) {
257
+ return `
258
+ <section class="data-section" id="source-evidence">
259
+ <div class="section-heading">
260
+ <h2>Source Evidence</h2>
261
+ <p>Page-level status with product-facing extraction posture. Technical model stages are kept in the processing audit below.</p>
262
+ </div>
263
+ <div class="evidence-grid">
264
+ ${dossier.pageCards.map((card) => `
265
+ <article class="evidence-card ${toneClass(card.tone)}">
266
+ <div>
267
+ <strong>${escapeHtml(card.pageLabel)}</strong>
268
+ <span>${escapeHtml(card.parseStatus)} | ${escapeHtml(`${card.confidence}%`)}</span>
269
+ </div>
270
+ <h3>${escapeHtml(card.title)}</h3>
271
+ <p>${escapeHtml(sourceModeLabel(card.sourceHint))}</p>
272
+ ${card.highlights.length > 0 ? `<ul>${card.highlights.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` : '<p>No strong structured highlight was retained for this page.</p>'}
273
+ ${card.warnings.length > 0 ? `<small>Human verification recommended: ${escapeHtml(String(card.warnings.length))} retained warning(s).</small>` : ''}
274
+ </article>
275
+ `).join('')}
276
+ </div>
136
277
  </section>
137
278
  `;
138
279
  }
280
+ function renderProcessingAudit(dossier, auditTables) {
281
+ const counts = dossier.pageCards.reduce((acc, page) => {
282
+ if (page.parseStatus === 'parsed')
283
+ acc.parsed += 1;
284
+ else if (page.parseStatus === 'partial')
285
+ acc.partial += 1;
286
+ else
287
+ acc.failed += 1;
288
+ return acc;
289
+ }, { parsed: 0, partial: 0, failed: 0 });
290
+ return `
291
+ <details class="audit-drawer" id="processing-audit">
292
+ <summary>
293
+ <span>
294
+ <strong>Processing Audit</strong>
295
+ <small>Model stages, page audit matrix, warnings, and operational details</small>
296
+ </span>
297
+ <span class="disclosure-hint">Show audit</span>
298
+ </summary>
299
+ <section class="audit-content">
300
+ <div class="outcome-strip" aria-label="Page extraction outcomes">
301
+ ${dossier.pageCards.map((card) => `
302
+ <span class="outcome-cell ${toneClass(card.tone)}" title="${escapeHtml(`${card.pageLabel}: ${card.parseStatus}, ${card.confidence}% confidence`)}">${escapeHtml(card.pageLabel.replace(/^Page\s+/i, ''))}</span>
303
+ `).join('')}
304
+ </div>
305
+ <div class="legend-row">
306
+ <span><strong>${escapeHtml(counts.parsed)}</strong> parsed</span>
307
+ <span><strong>${escapeHtml(counts.partial)}</strong> partial</span>
308
+ <span><strong>${escapeHtml(counts.failed)}</strong> failed</span>
309
+ </div>
310
+ ${auditTables.map((table) => renderTable(table, 'audit-table')).join('')}
311
+ <div class="audit-page-grid">
312
+ ${dossier.pageCards.map((card) => `
313
+ <article class="audit-page-card ${toneClass(card.tone)}">
314
+ <div>
315
+ <strong>${escapeHtml(card.pageLabel)}</strong>
316
+ <span>${escapeHtml(card.classification)} | ${escapeHtml(card.parseStatus)} | ${escapeHtml(`${card.confidence}%`)}</span>
317
+ </div>
318
+ <div class="chip-row">
319
+ ${card.sourceHint ? `<span class="chip">${escapeHtml(card.sourceHint)}</span>` : ''}
320
+ ${card.sectionType ? `<span class="chip">${escapeHtml(card.sectionType)}</span>` : ''}
321
+ ${card.scope ? `<span class="chip">${escapeHtml(card.scope)}</span>` : ''}
322
+ ${(card.stageBadges ?? []).map((stage) => `<span class="chip stage-chip">${escapeHtml(stage)}</span>`).join('')}
323
+ </div>
324
+ ${card.warnings.length > 0 ? `<ul>${card.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>` : ''}
325
+ </article>
326
+ `).join('')}
327
+ </div>
328
+ </section>
329
+ </details>
330
+ `;
331
+ }
332
+ function renderActionBar() {
333
+ return `
334
+ <div class="action-bar" aria-label="Review actions">
335
+ <a href="#parameters" class="primary-action">Approve Extraction</a>
336
+ <a href="#risks">Flag for Review</a>
337
+ <a href="#source-evidence">Open Source Page</a>
338
+ <button type="button" onclick="window.print()">Export PDF</button>
339
+ <a href="#ground-model">Generate Ground Model</a>
340
+ <a href="#boreholes">Create Borehole Profile</a>
341
+ <a href="#source-evidence">Ask Geotech Agent</a>
342
+ </div>
343
+ `;
344
+ }
139
345
  export function renderIngestDossierAsHtml(dossier) {
140
346
  const generatedDate = new Date(dossier.generatedAt).toLocaleString('en-CA', {
141
347
  dateStyle: 'medium',
142
348
  timeStyle: 'short',
143
349
  });
350
+ const auditTables = dossier.tables.filter((table) => isOperationalAuditTable(table.title));
351
+ const mainTables = dossier.tables.filter((table) => !isOperationalAuditTable(table.title));
352
+ const subtitle = /borehole/i.test(`${dossier.subtitle} ${dossier.title}`)
353
+ ? 'Borehole Log Interpretation and Review Summary'
354
+ : 'AI-Assisted Geotechnical Review Summary';
144
355
  return `<!doctype html>
145
356
  <html lang="en">
146
357
  <head>
147
358
  <meta charset="utf-8" />
148
359
  <meta name="viewport" content="width=device-width, initial-scale=1" />
149
- <title>${escapeHtml(dossier.title)}</title>
360
+ <title>Geotechnical Intelligence Dossier - ${escapeHtml(dossier.title)}</title>
150
361
  <style>
151
362
  :root {
152
- --background: #f8fafc;
153
- --foreground: #172033;
154
- --card: #ffffff;
155
- --card-foreground: #172033;
156
- --muted-surface: #f1f5f9;
157
- --muted-foreground: #5d6b7c;
158
- --primary: #145ca8;
159
- --primary-soft: rgba(20, 92, 168, 0.12);
160
- --bg: var(--background);
161
- --panel: var(--card);
162
- --panel-strong: var(--card);
163
- --border: #d9e1ea;
164
- --border-strong: #b7c4d2;
165
- --text: var(--foreground);
166
- --muted: var(--muted-foreground);
167
- --accent: var(--primary);
168
- --accent-soft: var(--primary-soft);
169
- --good: #1d8f57;
170
- --good-soft: rgba(29, 143, 87, 0.12);
171
- --warning: #b26b00;
172
- --warning-soft: rgba(178, 107, 0, 0.14);
173
- --danger: #ba2d3f;
174
- --danger-soft: rgba(186, 45, 63, 0.12);
175
- --neutral: #526072;
176
- --neutral-soft: rgba(82, 96, 114, 0.12);
177
- --shadow: none;
178
- --radius: 8px;
179
- --radius-sm: 6px;
363
+ --bg: #f6f8fb;
364
+ --surface: #ffffff;
365
+ --surface-soft: #eef3f8;
366
+ --text: #101828;
367
+ --muted: #667085;
368
+ --primary: #0b4f8a;
369
+ --primary-soft: #e6f0fa;
370
+ --success: #16875d;
371
+ --success-soft: #e4f6ee;
372
+ --warning: #b7791f;
373
+ --warning-soft: #fff3d6;
374
+ --danger: #b42318;
375
+ --danger-soft: #fde5e2;
376
+ --neutral: #475467;
377
+ --neutral-soft: #edf1f6;
378
+ --border: #d8e0ea;
379
+ --shadow: 0 18px 45px rgba(16, 24, 40, 0.08);
380
+ --radius: 16px;
381
+ --radius-sm: 12px;
180
382
  }
181
383
 
182
384
  * { box-sizing: border-box; }
183
-
385
+ html { scroll-behavior: smooth; }
184
386
  html, body { margin: 0; min-height: 100%; }
185
387
 
186
388
  body {
187
- font-family: "Segoe UI Variable", "Segoe UI", "Inter", sans-serif;
389
+ font-family: Inter, "Segoe UI Variable", "Segoe UI", Arial, sans-serif;
188
390
  color: var(--text);
189
391
  background: var(--bg);
190
392
  font-variant-numeric: tabular-nums;
393
+ overflow-x: hidden;
191
394
  }
192
395
 
193
- .shell {
194
- max-width: 1520px;
396
+ .layout {
397
+ display: grid;
398
+ grid-template-columns: 280px minmax(0, 1fr);
399
+ gap: 28px;
400
+ width: 100%;
401
+ max-width: 1580px;
195
402
  margin: 0 auto;
196
403
  padding: 28px;
197
- display: grid;
198
- gap: 22px;
404
+ min-width: 0;
199
405
  }
200
406
 
201
- .panel {
202
- background: var(--panel);
407
+ .sidebar {
408
+ position: sticky;
409
+ top: 28px;
410
+ align-self: start;
411
+ min-height: calc(100vh - 56px);
412
+ min-width: 0;
413
+ padding: 22px;
203
414
  border: 1px solid var(--border);
204
415
  border-radius: var(--radius);
416
+ background: #0b1220;
417
+ color: #f8fafc;
205
418
  box-shadow: var(--shadow);
206
- color: var(--card-foreground);
207
419
  }
208
420
 
209
- .hero {
210
- padding: 28px;
421
+ .brand {
211
422
  display: grid;
212
- gap: 20px;
423
+ gap: 8px;
424
+ padding-bottom: 22px;
425
+ border-bottom: 1px solid #263244;
426
+ margin-bottom: 18px;
213
427
  }
214
428
 
215
- .hero-head {
216
- display: flex;
217
- justify-content: space-between;
218
- gap: 18px;
219
- flex-wrap: wrap;
220
- align-items: start;
429
+ .brand strong { font-size: 1.05rem; }
430
+ .brand span { color: #94a3b8; line-height: 1.5; font-size: 0.9rem; }
431
+
432
+ .nav-list {
433
+ display: grid;
434
+ gap: 6px;
435
+ }
436
+
437
+ .nav-list a {
438
+ color: #dbeafe;
439
+ text-decoration: none;
440
+ padding: 10px 12px;
441
+ border-radius: 10px;
442
+ font-weight: 650;
443
+ font-size: 0.94rem;
444
+ overflow-wrap: anywhere;
445
+ }
446
+
447
+ .nav-list a:hover, .nav-list a:focus {
448
+ background: #1f2937;
449
+ outline: none;
450
+ }
451
+
452
+ .content {
453
+ display: grid;
454
+ gap: 26px;
455
+ min-width: 0;
456
+ max-width: 100%;
457
+ }
458
+
459
+ .hero {
460
+ padding: 32px;
461
+ border: 1px solid var(--border);
462
+ border-radius: 18px;
463
+ background: linear-gradient(135deg, #ffffff 0%, #eef6ff 100%);
464
+ box-shadow: var(--shadow);
465
+ display: grid;
466
+ gap: 24px;
467
+ min-width: 0;
221
468
  }
222
469
 
223
470
  .eyebrow {
224
471
  display: inline-flex;
225
- align-items: center;
226
- gap: 10px;
472
+ width: fit-content;
227
473
  padding: 8px 12px;
228
- border-radius: var(--radius-sm);
229
- background: var(--muted-surface);
230
- border: 1px solid var(--border);
231
- color: var(--muted);
232
- font-size: 0.84rem;
233
- letter-spacing: 0;
474
+ border-radius: 999px;
475
+ background: var(--primary-soft);
476
+ color: var(--primary);
477
+ font-weight: 800;
478
+ font-size: 0.78rem;
234
479
  text-transform: uppercase;
235
480
  }
236
481
 
237
- .hero h1 {
238
- margin: 0;
239
- font-size: clamp(1.75rem, 3vw, 2.35rem);
240
- line-height: 1.1;
241
- max-width: 24ch;
482
+ .hero-main {
483
+ display: grid;
484
+ grid-template-columns: minmax(0, 1fr) 250px;
485
+ gap: 24px;
486
+ align-items: start;
487
+ min-width: 0;
242
488
  }
243
489
 
244
- .hero p {
245
- margin: 0;
490
+ .hero-main > div {
491
+ min-width: 0;
492
+ }
493
+
494
+ h1, h2, h3, p { margin: 0; }
495
+
496
+ h1 {
497
+ margin-top: 12px;
498
+ font-size: clamp(2rem, 4vw, 3rem);
499
+ line-height: 1.04;
500
+ max-width: 22ch;
501
+ overflow-wrap: anywhere;
502
+ }
503
+
504
+ .hero-subtitle {
505
+ margin-top: 12px;
506
+ color: var(--primary);
507
+ font-weight: 800;
508
+ font-size: 1.12rem;
509
+ }
510
+
511
+ .hero-summary {
512
+ margin-top: 14px;
246
513
  color: var(--muted);
514
+ line-height: 1.7;
247
515
  max-width: 92ch;
248
- line-height: 1.65;
249
- font-size: 1rem;
516
+ overflow-wrap: anywhere;
250
517
  }
251
518
 
252
519
  .hero-meta {
253
520
  display: grid;
254
- gap: 6px;
255
- min-width: 230px;
256
- text-align: right;
521
+ gap: 10px;
522
+ padding: 18px;
523
+ border: 1px solid var(--border);
524
+ border-radius: var(--radius-sm);
525
+ background: rgba(255, 255, 255, 0.72);
257
526
  color: var(--muted);
258
- font-size: 0.95rem;
527
+ font-size: 0.92rem;
528
+ min-width: 0;
529
+ overflow-wrap: anywhere;
259
530
  }
260
531
 
261
- .badge-row, .metric-grid, .finding-grid, .section-grid, .page-grid, .footer-grid {
532
+ .hero-meta strong { color: var(--text); }
533
+
534
+ .executive-grid, .metric-grid, .card-grid, .evidence-grid, .audit-page-grid, .footer-grid {
262
535
  display: grid;
263
536
  gap: 16px;
264
537
  }
265
538
 
266
- .badge-row {
267
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
539
+ .executive-grid {
540
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
268
541
  }
269
542
 
270
- .badge {
271
- border-radius: var(--radius-sm);
543
+ .fact-card, .metric-card, .insight-card, .evidence-card, .footer-card, .audit-page-card {
272
544
  border: 1px solid var(--border);
273
- padding: 14px 16px;
274
- background: var(--panel);
275
- display: grid;
276
- gap: 6px;
545
+ border-radius: var(--radius);
546
+ background: var(--surface);
547
+ padding: 18px;
277
548
  min-width: 0;
278
549
  }
279
550
 
280
- .badge-label {
281
- font-size: 0.8rem;
282
- text-transform: uppercase;
283
- letter-spacing: 0;
551
+ .fact-card {
552
+ display: grid;
553
+ gap: 8px;
554
+ }
555
+
556
+ .fact-card span, .metric-card span {
284
557
  color: var(--muted);
558
+ font-size: 0.78rem;
559
+ text-transform: uppercase;
560
+ font-weight: 800;
285
561
  }
286
562
 
287
- .badge-value {
563
+ .fact-card strong {
288
564
  font-size: 1rem;
289
- font-weight: 700;
565
+ line-height: 1.45;
566
+ overflow-wrap: anywhere;
290
567
  }
291
568
 
292
- .metric-grid {
293
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
569
+ .fact-card small, .metric-card small {
570
+ color: var(--muted);
571
+ line-height: 1.5;
294
572
  }
295
573
 
296
- .metric {
297
- padding: 18px;
298
- border-radius: var(--radius-sm);
299
- border: 1px solid var(--border);
300
- background: var(--panel);
301
- display: grid;
302
- gap: 8px;
303
- min-width: 0;
574
+ .metric-grid {
575
+ grid-template-columns: repeat(auto-fit, minmax(165px, 1fr));
304
576
  }
305
577
 
306
- .metric-label {
307
- font-size: 0.82rem;
308
- text-transform: uppercase;
309
- letter-spacing: 0;
310
- color: var(--muted);
578
+ .metric-card {
579
+ display: grid;
580
+ gap: 10px;
311
581
  }
312
582
 
313
- .metric-value {
314
- font-size: 1.6rem;
315
- font-weight: 800;
583
+ .metric-card strong {
584
+ font-size: 1.65rem;
316
585
  line-height: 1;
317
586
  }
318
587
 
@@ -321,7 +590,7 @@ export function renderIngestDossierAsHtml(dossier) {
321
590
  height: 8px;
322
591
  overflow: hidden;
323
592
  border-radius: 999px;
324
- background: rgba(82, 96, 114, 0.16);
593
+ background: rgba(102, 112, 133, 0.18);
325
594
  }
326
595
 
327
596
  .meter span {
@@ -329,286 +598,283 @@ export function renderIngestDossierAsHtml(dossier) {
329
598
  height: 100%;
330
599
  border-radius: inherit;
331
600
  background: currentColor;
332
- opacity: 0.72;
601
+ opacity: 0.75;
333
602
  }
334
603
 
335
- .metric-detail {
336
- color: var(--muted);
337
- font-size: 0.92rem;
338
- line-height: 1.5;
604
+ .tone-accent { background: var(--primary-soft); color: var(--primary); }
605
+ .tone-good { background: var(--success-soft); color: var(--success); }
606
+ .tone-warning { background: var(--warning-soft); color: var(--warning); }
607
+ .tone-danger { background: var(--danger-soft); color: var(--danger); }
608
+ .tone-neutral { background: var(--neutral-soft); color: var(--neutral); }
609
+
610
+ .action-bar {
611
+ display: flex;
612
+ flex-wrap: wrap;
613
+ gap: 10px;
339
614
  }
340
615
 
341
- .tone-accent { background: rgba(0, 104, 215, 0.08); }
342
- .tone-good { background: var(--good-soft); }
343
- .tone-warning { background: var(--warning-soft); }
344
- .tone-danger { background: var(--danger-soft); }
345
- .tone-neutral { background: var(--neutral-soft); }
616
+ .action-bar button, .action-bar a {
617
+ border: 1px solid var(--border);
618
+ border-radius: 12px;
619
+ background: var(--surface);
620
+ color: var(--text);
621
+ padding: 10px 13px;
622
+ font: inherit;
623
+ font-weight: 800;
624
+ text-decoration: none;
625
+ box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
626
+ cursor: pointer;
627
+ overflow-wrap: anywhere;
628
+ }
629
+
630
+ .action-bar .primary-action {
631
+ background: var(--primary);
632
+ border-color: var(--primary);
633
+ color: #ffffff;
634
+ }
346
635
 
347
- .section-card {
348
- padding: 24px;
636
+ .data-section {
349
637
  display: grid;
350
- gap: 18px;
638
+ gap: 16px;
639
+ scroll-margin-top: 24px;
640
+ min-width: 0;
351
641
  }
352
642
 
353
- .section-head {
643
+ .section-heading {
354
644
  display: grid;
355
645
  gap: 8px;
356
646
  }
357
647
 
358
- .section-head h2 {
359
- margin: 0;
360
- font-size: 1.2rem;
648
+ .section-heading h2 {
649
+ font-size: 1.35rem;
650
+ letter-spacing: 0;
361
651
  }
362
652
 
363
- .section-head p {
364
- margin: 0;
653
+ .section-heading p {
365
654
  color: var(--muted);
366
- line-height: 1.6;
655
+ line-height: 1.65;
656
+ max-width: 96ch;
367
657
  }
368
658
 
369
- .finding-grid {
659
+ .card-grid {
370
660
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
371
661
  }
372
662
 
373
- .overview {
374
- gap: 16px;
375
- }
376
-
377
- .outcome-strip {
663
+ .insight-card {
378
664
  display: grid;
379
- grid-template-columns: repeat(auto-fill, minmax(34px, 1fr));
380
- gap: 6px;
665
+ gap: 12px;
381
666
  }
382
667
 
383
- .outcome-cell {
384
- min-height: 30px;
385
- border: 1px solid var(--border);
386
- border-radius: var(--radius-sm);
387
- display: inline-flex;
388
- align-items: center;
389
- justify-content: center;
390
- font-size: 0.78rem;
391
- font-weight: 700;
668
+ .insight-card h3 {
392
669
  color: var(--text);
670
+ font-size: 1.06rem;
393
671
  }
394
672
 
395
- .legend-row {
396
- display: flex;
397
- gap: 16px;
398
- flex-wrap: wrap;
673
+ .insight-card p, .insight-card li, .evidence-card p, .evidence-card li {
399
674
  color: var(--muted);
400
- font-size: 0.92rem;
675
+ line-height: 1.62;
401
676
  }
402
677
 
403
- .legend-row strong {
404
- color: var(--text);
678
+ .insight-card ul, .evidence-card ul, .profile-notes, .footer-card ul, .audit-page-card ul {
679
+ margin: 0;
680
+ padding-left: 18px;
681
+ display: grid;
682
+ gap: 7px;
405
683
  }
406
684
 
407
- .finding-card {
408
- padding: 18px;
409
- border-radius: var(--radius-sm);
685
+ .profile-shell, .table-shell {
686
+ overflow-x: auto;
410
687
  border: 1px solid var(--border);
411
- display: grid;
412
- gap: 12px;
688
+ border-radius: var(--radius);
689
+ background: var(--surface);
690
+ min-width: 0;
691
+ max-width: 100%;
413
692
  }
414
693
 
415
- .finding-card h3 {
416
- margin: 0;
417
- font-size: 1rem;
694
+ .borehole-profile {
695
+ display: block;
696
+ min-width: 760px;
697
+ width: 100%;
698
+ height: auto;
418
699
  }
419
700
 
420
- .finding-card ul {
421
- margin: 0;
422
- padding-left: 18px;
423
- display: grid;
424
- gap: 8px;
425
- line-height: 1.55;
426
- }
701
+ .profile-title { font: 800 15px Inter, Segoe UI, sans-serif; fill: #101828; }
702
+ .profile-label { font: 800 13px Inter, Segoe UI, sans-serif; fill: #101828; }
703
+ .profile-small { font: 11px Inter, Segoe UI, sans-serif; fill: #475467; }
704
+ .profile-axis { font: 11px Inter, Segoe UI, sans-serif; fill: #667085; }
705
+ .profile-water { font: 800 11px Inter, Segoe UI, sans-serif; fill: #0b4f8a; }
427
706
 
428
- .table-shell {
429
- overflow-x: auto;
430
- border-radius: var(--radius-sm);
431
- border: 1px solid var(--border);
432
- background: var(--panel);
707
+ .profile-notes {
708
+ color: var(--muted);
709
+ line-height: 1.55;
433
710
  }
434
711
 
435
712
  table {
436
713
  width: 100%;
437
714
  border-collapse: collapse;
438
- min-width: 720px;
715
+ min-width: 920px;
439
716
  }
440
717
 
441
718
  th, td {
442
- padding: 12px 14px;
443
- border-bottom: 1px solid rgba(19, 63, 120, 0.08);
719
+ padding: 13px 14px;
720
+ border-bottom: 1px solid rgba(11, 79, 138, 0.09);
444
721
  text-align: left;
445
722
  vertical-align: top;
446
723
  font-size: 0.92rem;
447
- line-height: 1.45;
724
+ line-height: 1.48;
448
725
  overflow-wrap: anywhere;
449
726
  }
450
727
 
451
728
  th {
452
- font-size: 0.78rem;
453
- text-transform: uppercase;
454
- letter-spacing: 0;
455
- color: var(--muted);
456
- background: var(--muted-surface);
457
729
  position: sticky;
458
730
  top: 0;
459
731
  z-index: 1;
460
- }
461
-
462
- .empty-state {
463
- padding: 18px;
464
- border-radius: var(--radius-sm);
465
- background: var(--muted-surface);
732
+ background: var(--surface-soft);
466
733
  color: var(--muted);
467
- border: 1px dashed var(--border-strong);
468
- }
469
-
470
- .section-grid {
471
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
472
- }
473
-
474
- .narrative {
475
- padding: 22px;
476
- border-radius: var(--radius-sm);
477
- border: 1px solid var(--border);
478
- background: var(--panel);
479
- display: grid;
480
- gap: 12px;
481
- }
482
-
483
- .narrative h3 {
484
- margin: 0;
485
- font-size: 1.04rem;
734
+ font-size: 0.76rem;
735
+ text-transform: uppercase;
736
+ font-weight: 900;
737
+ white-space: nowrap;
738
+ overflow-wrap: normal;
486
739
  }
487
740
 
488
- .narrative p {
489
- margin: 0;
490
- color: var(--muted);
491
- line-height: 1.65;
741
+ .status-pill, .chip {
742
+ display: inline-flex;
743
+ align-items: center;
744
+ width: fit-content;
745
+ padding: 6px 10px;
746
+ border-radius: 999px;
747
+ border: 1px solid rgba(102, 112, 133, 0.2);
748
+ font-size: 0.78rem;
749
+ font-weight: 800;
750
+ line-height: 1;
492
751
  }
493
752
 
494
- .page-grid {
495
- grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
753
+ .evidence-grid {
754
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
496
755
  }
497
756
 
498
- .page-card {
499
- padding: 18px;
500
- border-radius: var(--radius-sm);
501
- border: 1px solid var(--border);
757
+ .evidence-card {
502
758
  display: grid;
503
- gap: 12px;
504
- background: var(--panel);
505
- min-width: 0;
759
+ gap: 11px;
506
760
  }
507
761
 
508
- .page-kicker {
762
+ .evidence-card > div {
509
763
  display: flex;
510
764
  justify-content: space-between;
511
765
  gap: 12px;
512
- align-items: start;
513
- flex-wrap: wrap;
766
+ color: var(--muted);
767
+ font-size: 0.86rem;
514
768
  }
515
769
 
516
- .page-kicker strong {
517
- font-size: 0.9rem;
770
+ .evidence-card h3 {
771
+ color: var(--text);
772
+ font-size: 1.02rem;
518
773
  }
519
774
 
520
- .page-kicker span {
775
+ .evidence-card small {
776
+ color: var(--warning);
777
+ font-weight: 800;
778
+ line-height: 1.45;
779
+ }
780
+
781
+ .empty-state {
782
+ padding: 18px;
783
+ border: 1px dashed var(--border);
784
+ border-radius: var(--radius);
785
+ background: var(--surface);
521
786
  color: var(--muted);
522
- font-size: 0.82rem;
787
+ line-height: 1.6;
523
788
  }
524
789
 
525
- .page-card h3 {
526
- margin: 0;
527
- font-size: 1.05rem;
790
+ .audit-drawer {
791
+ scroll-margin-top: 24px;
792
+ border: 1px solid var(--border);
793
+ border-radius: var(--radius);
794
+ background: var(--surface);
795
+ overflow: hidden;
528
796
  }
529
797
 
530
- .page-meta {
798
+ .audit-drawer summary {
799
+ cursor: pointer;
800
+ list-style: none;
801
+ padding: 20px;
531
802
  display: flex;
532
- gap: 8px;
533
- flex-wrap: wrap;
803
+ align-items: center;
804
+ justify-content: space-between;
805
+ gap: 16px;
534
806
  }
535
807
 
536
- .chip {
537
- padding: 6px 10px;
538
- border-radius: var(--radius-sm);
539
- border: 1px solid var(--border);
540
- font-size: 0.78rem;
541
- color: var(--muted);
542
- background: var(--muted-surface);
543
- line-height: 1.1;
544
- }
808
+ .audit-drawer summary::-webkit-details-marker { display: none; }
809
+ .audit-drawer summary span:first-child { display: grid; gap: 5px; }
810
+ .audit-drawer small { color: var(--muted); }
545
811
 
546
- .stage-chip {
547
- border-color: rgba(20, 92, 168, 0.24);
548
- color: var(--accent);
549
- background: var(--accent-soft);
550
- font-weight: 650;
812
+ .disclosure-hint {
813
+ border: 1px solid var(--border);
814
+ border-radius: 999px;
815
+ padding: 7px 11px;
816
+ color: var(--primary);
817
+ background: var(--primary-soft);
818
+ font-weight: 800;
819
+ white-space: nowrap;
551
820
  }
552
821
 
553
- .page-card ul {
554
- margin: 0;
555
- padding-left: 18px;
822
+ .audit-content {
823
+ border-top: 1px solid var(--border);
824
+ padding: 20px;
556
825
  display: grid;
557
- gap: 8px;
558
- line-height: 1.55;
559
- color: var(--muted);
826
+ gap: 20px;
560
827
  }
561
828
 
562
- .page-card details {
563
- border-top: 1px dashed var(--border-strong);
564
- padding-top: 10px;
829
+ .outcome-strip {
830
+ display: grid;
831
+ grid-template-columns: repeat(auto-fill, minmax(34px, 1fr));
832
+ gap: 6px;
565
833
  }
566
834
 
567
- .page-card summary {
568
- cursor: pointer;
569
- color: var(--accent);
570
- font-weight: 600;
571
- list-style: none;
835
+ .outcome-cell {
836
+ min-height: 30px;
837
+ border: 1px solid var(--border);
838
+ border-radius: 10px;
839
+ display: inline-flex;
840
+ align-items: center;
841
+ justify-content: center;
842
+ font-size: 0.78rem;
843
+ font-weight: 900;
572
844
  }
573
845
 
574
- .page-card summary::-webkit-details-marker {
575
- display: none;
846
+ .legend-row, .chip-row {
847
+ display: flex;
848
+ flex-wrap: wrap;
849
+ gap: 10px;
850
+ color: var(--muted);
576
851
  }
577
852
 
578
- .page-card details ul {
579
- margin-top: 10px;
580
- }
853
+ .legend-row strong { color: var(--text); }
581
854
 
582
- .disclosure {
583
- background: var(--panel);
855
+ .audit-page-grid {
856
+ grid-template-columns: repeat(auto-fit, minmax(270px, 1fr));
584
857
  }
585
858
 
586
- .disclosure summary {
587
- cursor: pointer;
859
+ .audit-page-card {
588
860
  display: grid;
589
- gap: 10px;
590
- list-style: none;
861
+ gap: 12px;
591
862
  }
592
863
 
593
- .disclosure summary::-webkit-details-marker {
594
- display: none;
864
+ .audit-page-card > div:first-child {
865
+ display: grid;
866
+ gap: 4px;
595
867
  }
596
868
 
597
- .disclosure-hint {
598
- width: fit-content;
599
- padding: 6px 10px;
600
- border-radius: var(--radius-sm);
601
- border: 1px solid var(--border);
602
- background: var(--muted-surface);
869
+ .audit-page-card > div:first-child span, .audit-page-card li {
603
870
  color: var(--muted);
604
- font-size: 0.82rem;
605
- font-weight: 650;
871
+ line-height: 1.5;
606
872
  }
607
873
 
608
- .disclosure[open] .disclosure-hint {
609
- color: var(--accent);
610
- border-color: rgba(20, 92, 168, 0.28);
611
- background: var(--accent-soft);
874
+ .stage-chip {
875
+ color: var(--primary);
876
+ background: var(--primary-soft);
877
+ border-color: #b9d4ec;
612
878
  }
613
879
 
614
880
  .footer-grid {
@@ -616,171 +882,134 @@ export function renderIngestDossierAsHtml(dossier) {
616
882
  }
617
883
 
618
884
  .footer-card {
619
- padding: 18px;
620
- border-radius: var(--radius-sm);
621
- border: 1px solid var(--border);
622
- background: var(--panel);
623
885
  display: grid;
624
- gap: 8px;
625
- }
626
-
627
- .footer-card h3 {
628
- margin: 0;
629
- font-size: 0.98rem;
886
+ gap: 9px;
630
887
  }
631
888
 
632
889
  .footer-card p, .footer-card li {
633
- margin: 0;
634
890
  color: var(--muted);
635
- line-height: 1.6;
891
+ line-height: 1.58;
636
892
  }
637
893
 
638
- .footer-card ul {
639
- margin: 0;
640
- padding-left: 18px;
641
- display: grid;
642
- gap: 6px;
894
+ @media (max-width: 980px) {
895
+ .layout { grid-template-columns: minmax(0, 1fr); padding: 18px; max-width: 100%; }
896
+ .sidebar { position: static; min-height: auto; }
897
+ .nav-list { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
898
+ .hero-main { grid-template-columns: 1fr; }
643
899
  }
644
900
 
645
- @media (max-width: 920px) {
646
- .shell { padding: 18px; }
901
+ @media (max-width: 620px) {
902
+ .layout { display: block; width: 100%; max-width: 390px; margin: 0; padding: 12px; }
903
+ .sidebar, .content, .hero, .data-section, .audit-drawer, .footer-grid {
904
+ width: 100%;
905
+ max-width: 100%;
906
+ }
907
+ .content { margin-top: 18px; }
647
908
  .hero { padding: 22px; }
648
- .hero-meta { text-align: left; }
909
+ .executive-grid, .metric-grid, .card-grid, .evidence-grid, .footer-grid { grid-template-columns: 1fr; }
910
+ .nav-list { grid-template-columns: 1fr; }
911
+ h1 { font-size: 1.72rem; max-width: 100%; }
649
912
  }
650
913
  </style>
651
914
  </head>
652
915
  <body>
653
- <main class="shell">
654
- <section class="panel hero">
655
- <div class="hero-head">
656
- <div>
657
- <div class="eyebrow">geotechCLI ingest dossier</div>
658
- <h1>${escapeHtml(dossier.title)}</h1>
659
- <p>${escapeHtml(dossier.summary)}</p>
660
- </div>
661
- <div class="hero-meta">
662
- <strong>${escapeHtml(dossier.subtitle)}</strong>
663
- <span>${escapeHtml(dossier.sourceLabel)}</span>
664
- <span>Generated ${escapeHtml(generatedDate)}</span>
665
- <span>geotechCLI v${escapeHtml(GEOTECHCLI_VERSION)}</span>
666
- </div>
916
+ <div class="layout">
917
+ <aside class="sidebar" aria-label="Dossier navigation">
918
+ <div class="brand">
919
+ <strong>Geotechnical Intelligence Dossier</strong>
920
+ <span>AI-assisted extraction, verification, and engineering interpretation from geotechnical reports.</span>
667
921
  </div>
668
-
669
- <div class="badge-row">
670
- ${dossier.badges.map((badge) => `
671
- <article class="badge ${toneClass(badge.tone)}">
672
- <span class="badge-label">${escapeHtml(badge.label)}</span>
673
- <span class="badge-value">${escapeHtml(badge.value)}</span>
674
- </article>
675
- `).join('')}
676
- </div>
677
-
678
- <div class="metric-grid">
679
- ${dossier.metrics.map(renderMetric).join('')}
680
- </div>
681
- </section>
682
-
683
- ${renderPageOutcomeStrip(dossier)}
684
-
685
- ${dossier.findings.length > 0 ? `
686
- <section class="panel section-card" id="review-findings">
687
- <div class="section-head">
688
- <h2>Review findings</h2>
689
- <p>What still needs attention before this ingest can be treated as fully trusted engineering input.</p>
922
+ <nav class="nav-list">
923
+ <a href="#overview">Overview</a>
924
+ <a href="#ground-model">Ground Model</a>
925
+ <a href="#boreholes">Boreholes</a>
926
+ <a href="#parameters">Engineering Parameters</a>
927
+ <a href="#risks">Risks and Limitations</a>
928
+ <a href="#source-evidence">Source Evidence</a>
929
+ <a href="#processing-audit">Audit Trail</a>
930
+ </nav>
931
+ </aside>
932
+
933
+ <main class="content">
934
+ <header class="hero" id="overview">
935
+ <div class="hero-main">
936
+ <div>
937
+ <span class="eyebrow">Geotechnical Intelligence Dossier</span>
938
+ <h1>${escapeHtml(dossier.title)}</h1>
939
+ <p class="hero-subtitle">${escapeHtml(subtitle)}</p>
940
+ <p class="hero-summary">${escapeHtml(dossier.summary)}</p>
941
+ </div>
942
+ <div class="hero-meta">
943
+ <strong>${escapeHtml(dossier.sourceLabel)}</strong>
944
+ <span>Generated ${escapeHtml(generatedDate)}</span>
945
+ <span>geotechCLI v${escapeHtml(GEOTECHCLI_VERSION)}</span>
946
+ <span>${escapeHtml(dossier.documentType)}</span>
947
+ </div>
690
948
  </div>
691
- <div class="finding-grid">
692
- ${dossier.findings.map((group) => `
693
- <article class="finding-card ${toneClass(group.tone)}">
694
- <h3>${escapeHtml(group.label)}</h3>
695
- <ul>${group.items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>
949
+ <div class="executive-grid">
950
+ ${dossier.executiveItems.map((item) => `
951
+ <article class="fact-card ${toneClass(item.tone)}">
952
+ <span>${escapeHtml(item.label)}</span>
953
+ <strong>${escapeHtml(item.value)}</strong>
954
+ ${item.detail ? `<small>${escapeHtml(item.detail)}</small>` : ''}
696
955
  </article>
697
956
  `).join('')}
698
957
  </div>
699
- </section>
700
- ` : ''}
701
-
702
- ${dossier.sections.length > 0 ? `
703
- <section class="panel section-card" id="narrative">
704
- <div class="section-head">
705
- <h2>Engineering brief</h2>
706
- <p>A concise narrative view designed for fast review before deeper page-by-page inspection.</p>
707
- </div>
708
- <div class="section-grid">
709
- ${dossier.sections.map((section) => `
710
- <article class="narrative">
711
- <h3>${escapeHtml(section.title)}</h3>
712
- ${section.paragraphs.map((paragraph) => `<p>${escapeHtml(paragraph)}</p>`).join('')}
713
- </article>
714
- `).join('')}
958
+ <div class="metric-grid">
959
+ ${dossier.metrics.map(renderMetric).join('')}
715
960
  </div>
716
- </section>
717
- ` : ''}
718
-
719
- ${dossier.tables.map(renderTable).join('')}
961
+ ${renderActionBar()}
962
+ </header>
720
963
 
721
- ${dossier.pageCards.length > 0 ? `
722
- <section class="panel section-card" id="page-map">
723
- <div class="section-head">
724
- <h2>Page map</h2>
725
- <p>Page-level outcome, extraction posture, and the strongest retained signals for each page.</p>
964
+ <section class="data-section" id="ground-model">
965
+ <div class="section-heading">
966
+ <h2>Ground Model</h2>
967
+ <p>Decision-focused interpretation cards. The main view prioritizes engineering meaning, missing data, and verification needs.</p>
726
968
  </div>
727
- <div class="page-grid">
728
- ${dossier.pageCards.map((card) => `
729
- <article class="page-card ${toneClass(card.tone)}">
730
- <div class="page-kicker">
731
- <div>
732
- <strong>${escapeHtml(card.pageLabel)}</strong>
733
- <div><span>${escapeHtml(card.classification)}</span></div>
734
- </div>
735
- <span>${escapeHtml(card.parseStatus)} | ${escapeHtml(`${card.confidence}%`)}</span>
736
- </div>
969
+ <div class="card-grid">
970
+ ${dossier.insightCards.map((card) => `
971
+ <article class="insight-card ${toneClass(card.tone)}">
737
972
  <h3>${escapeHtml(card.title)}</h3>
738
- <div class="page-meta">
739
- ${card.sourceHint ? `<span class="chip">${escapeHtml(card.sourceHint)}</span>` : ''}
740
- ${card.sectionType ? `<span class="chip">${escapeHtml(card.sectionType)}</span>` : ''}
741
- ${card.scope ? `<span class="chip">${escapeHtml(card.scope)}</span>` : ''}
742
- ${(card.stageBadges ?? []).map((stage) => `<span class="chip stage-chip">${escapeHtml(stage)}</span>`).join('')}
743
- </div>
744
- ${card.highlights.length > 0 ? `<ul>${card.highlights.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` : '<div class="empty-state">No strong structured highlights were retained for this page.</div>'}
745
- ${card.warnings.length > 0 ? `
746
- <details>
747
- <summary>Warnings (${card.warnings.length})</summary>
748
- <ul>${card.warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('')}</ul>
749
- </details>
750
- ` : ''}
973
+ <p>${escapeHtml(card.body)}</p>
974
+ ${card.detail ? `<p>${escapeHtml(card.detail)}</p>` : ''}
751
975
  </article>
752
976
  `).join('')}
753
977
  </div>
754
978
  </section>
755
- ` : ''}
756
-
757
- <section class="footer-grid">
758
- ${dossier.storedReview ? `
759
- <article class="footer-card">
760
- <h3>Stored review</h3>
761
- <p>Project: ${escapeHtml(dossier.storedReview.projectId)}</p>
762
- <p>Dataset: ${escapeHtml(dossier.storedReview.datasetName)}</p>
763
- <p>Review ID: ${escapeHtml(dossier.storedReview.reviewId)}</p>
764
- ${dossier.storedReview.createdAt ? `<p>Created: ${escapeHtml(dossier.storedReview.createdAt)}</p>` : ''}
765
- </article>
766
- ` : ''}
767
979
 
768
- ${dossier.approval ? `
980
+ ${renderBoreholeProfile(dossier.boreholeProfile)}
981
+ ${renderTrustTable(dossier)}
982
+ ${mainTables.map((table) => renderTable(table)).join('')}
983
+ ${renderFindingGroups(dossier)}
984
+ ${renderSourceEvidence(dossier)}
985
+ ${renderProcessingAudit(dossier, auditTables)}
986
+
987
+ <section class="footer-grid">
988
+ ${dossier.storedReview ? `
989
+ <article class="footer-card">
990
+ <h3>Stored review</h3>
991
+ <p>Project: ${escapeHtml(dossier.storedReview.projectId)}</p>
992
+ <p>Dataset: ${escapeHtml(dossier.storedReview.datasetName)}</p>
993
+ <p>Review ID: ${escapeHtml(dossier.storedReview.reviewId)}</p>
994
+ ${dossier.storedReview.createdAt ? `<p>Created: ${escapeHtml(dossier.storedReview.createdAt)}</p>` : ''}
995
+ </article>
996
+ ` : ''}
997
+ ${dossier.approval ? `
998
+ <article class="footer-card">
999
+ <h3>Approval</h3>
1000
+ <p>Dataset: ${escapeHtml(dossier.approval.datasetName)}</p>
1001
+ <p>Approved: ${escapeHtml(dossier.approval.approvedAt)}</p>
1002
+ <p>Approved by: ${escapeHtml(dossier.approval.approvedBy ?? 'Unspecified')}</p>
1003
+ ${dossier.approval.rationale ? `<p>${escapeHtml(dossier.approval.rationale)}</p>` : ''}
1004
+ </article>
1005
+ ` : ''}
769
1006
  <article class="footer-card">
770
- <h3>Approval</h3>
771
- <p>Dataset: ${escapeHtml(dossier.approval.datasetName)}</p>
772
- <p>Approved: ${escapeHtml(dossier.approval.approvedAt)}</p>
773
- <p>Approved by: ${escapeHtml(dossier.approval.approvedBy ?? 'Unspecified')}</p>
774
- ${dossier.approval.rationale ? `<p>${escapeHtml(dossier.approval.rationale)}</p>` : ''}
1007
+ <h3>Source notes</h3>
1008
+ <ul>${dossier.footerNotes.map((note) => `<li>${escapeHtml(note)}</li>`).join('')}</ul>
775
1009
  </article>
776
- ` : ''}
777
-
778
- <article class="footer-card">
779
- <h3>Source notes</h3>
780
- <ul>${dossier.footerNotes.map((note) => `<li>${escapeHtml(note)}</li>`).join('')}</ul>
781
- </article>
782
- </section>
783
- </main>
1010
+ </section>
1011
+ </main>
1012
+ </div>
784
1013
  </body>
785
1014
  </html>`;
786
1015
  }