@geotechcli/core 0.4.34 → 0.4.36

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