@geotechcli/core 0.4.37 → 0.4.40

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.
@@ -68,6 +68,15 @@ function sourceModeLabel(sourceHint) {
68
68
  return 'Extraction evidence retained';
69
69
  }
70
70
  }
71
+ function reviewFilterValue(item) {
72
+ if (/not extracted|missing/i.test(item.value) || /required/i.test(item.review)) {
73
+ return 'missing';
74
+ }
75
+ if (/recommended|verify|manual|visual|review/i.test(item.review) && !/ready/i.test(item.review)) {
76
+ return 'needs_review';
77
+ }
78
+ return 'verified';
79
+ }
71
80
  function compactSvgText(value, maxLength = 28) {
72
81
  const normalized = (value ?? '').replace(/\s+/g, ' ').trim();
73
82
  if (normalized.length <= maxLength) {
@@ -90,6 +99,21 @@ function renderMetric(metric) {
90
99
  </article>
91
100
  `;
92
101
  }
102
+ function renderStatusBadges(dossier) {
103
+ if (dossier.badges.length === 0) {
104
+ return '';
105
+ }
106
+ return `
107
+ <div class="status-badge-row" aria-label="Document status">
108
+ ${dossier.badges.map((badge) => `
109
+ <span class="status-badge ${toneClass(badge.tone)}">
110
+ <span>${escapeHtml(badge.label)}</span>
111
+ <strong>${escapeHtml(badge.value)}</strong>
112
+ </span>
113
+ `).join('')}
114
+ </div>
115
+ `;
116
+ }
93
117
  function renderTable(table, className = 'data-section') {
94
118
  const tableId = escapeHtml(idFromTitle(table.title));
95
119
  const body = table.rows.length === 0
@@ -119,8 +143,18 @@ function renderTable(table, className = 'data-section') {
119
143
  function renderTrustTable(dossier) {
120
144
  const rows = dossier.trustItems.length === 0
121
145
  ? '<tr><td colspan="6">No trust-layer rows were retained.</td></tr>'
122
- : dossier.trustItems.map((item) => `
123
- <tr>
146
+ : dossier.trustItems.map((item) => {
147
+ const reviewFilter = reviewFilterValue(item);
148
+ const searchText = [
149
+ item.item,
150
+ item.value,
151
+ item.sourcePage,
152
+ item.confidence,
153
+ item.review,
154
+ item.evidence,
155
+ ].join(' ').toLowerCase();
156
+ return `
157
+ <tr data-trust-row data-review="${escapeHtml(reviewFilter)}" data-search="${escapeHtml(searchText)}">
124
158
  <td><strong>${escapeHtml(item.item)}</strong></td>
125
159
  <td>${escapeHtml(item.value)}</td>
126
160
  <td>${escapeHtml(item.sourcePage)}</td>
@@ -128,13 +162,26 @@ function renderTrustTable(dossier) {
128
162
  <td>${escapeHtml(item.review)}</td>
129
163
  <td>${escapeHtml(item.evidence)}</td>
130
164
  </tr>
131
- `).join('');
165
+ `;
166
+ }).join('');
132
167
  return `
133
168
  <section class="data-section" id="parameters">
134
169
  <div class="section-heading">
135
170
  <h2>Engineering Parameters</h2>
136
171
  <p>Evidence-first review table. Every retained or missing item is shown with source, confidence, review posture, and an evidence snippet.</p>
137
172
  </div>
173
+ <div class="trust-controls" aria-label="Parameter table controls">
174
+ <label>
175
+ <span>Search parameters</span>
176
+ <input id="trust-search" type="search" placeholder="Filter by parameter, value, source, or evidence" />
177
+ </label>
178
+ <div class="filter-row" aria-label="Review status filters">
179
+ <button type="button" class="filter-button active" data-trust-filter="all">All</button>
180
+ <button type="button" class="filter-button" data-trust-filter="needs_review">Needs review</button>
181
+ <button type="button" class="filter-button" data-trust-filter="missing">Missing</button>
182
+ <button type="button" class="filter-button" data-trust-filter="verified">Verified</button>
183
+ </div>
184
+ </div>
138
185
  <div class="table-shell trust-table">
139
186
  <table>
140
187
  <thead>
@@ -153,6 +200,97 @@ function renderTrustTable(dossier) {
153
200
  </section>
154
201
  `;
155
202
  }
203
+ function renderGroundModelCrossSection(profile) {
204
+ if (!profile || profile.columns.length < 2 || profile.maxDepth <= 0) {
205
+ return `
206
+ <section class="data-section" id="ground-cross-section">
207
+ <div class="section-heading">
208
+ <h2>Ground Model Cross-Section</h2>
209
+ <p>A schematic cross-section needs at least two boreholes with retained depth evidence.</p>
210
+ </div>
211
+ <div class="empty-state">Ground-model bands were not drawn because borehole depth and layer evidence were incomplete.</div>
212
+ </section>
213
+ `;
214
+ }
215
+ const width = 940;
216
+ const height = 310;
217
+ const plotTop = 58;
218
+ const plotHeight = 182;
219
+ const left = 84;
220
+ const right = width - 44;
221
+ const usableWidth = right - left;
222
+ const referenceLayers = profile.columns
223
+ .flatMap((column) => column.layers)
224
+ .sort((leftLayer, rightLayer) => leftLayer.depthFrom - rightLayer.depthFrom)
225
+ .slice(0, 8);
226
+ const layers = referenceLayers.length > 0
227
+ ? referenceLayers
228
+ : [{
229
+ depthFrom: 0,
230
+ depthTo: profile.maxDepth,
231
+ label: 'Ground profile',
232
+ description: 'Layer boundaries were not available in structured form.',
233
+ tone: 'neutral',
234
+ uncertain: true,
235
+ }];
236
+ const yForDepth = (depth) => plotTop + (Math.max(0, Math.min(profile.maxDepth, depth)) / profile.maxDepth) * plotHeight;
237
+ const ticks = Array.from({ length: 6 }, (_value, index) => Number((profile.maxDepth * index / 5).toFixed(2)));
238
+ const columnX = (index) => profile.columns.length === 1
239
+ ? left + usableWidth / 2
240
+ : left + (usableWidth * index / (profile.columns.length - 1));
241
+ return `
242
+ <section class="data-section" id="ground-cross-section">
243
+ <div class="section-heading">
244
+ <h2>Ground Model Cross-Section</h2>
245
+ <p>Conceptual cross-section connecting retained borehole evidence. Bands are schematic and should be verified against source logs before design use.</p>
246
+ </div>
247
+ <div class="profile-shell cross-section-shell">
248
+ <svg class="ground-cross-section" viewBox="0 0 ${width} ${height}" role="img" aria-label="AI-assisted ground model cross-section">
249
+ <rect x="0" y="0" width="${width}" height="${height}" rx="18" fill="#ffffff" />
250
+ ${ticks.map((tick) => {
251
+ const y = yForDepth(tick);
252
+ return `
253
+ <line x1="${left - 34}" y1="${y.toFixed(2)}" x2="${right + 12}" y2="${y.toFixed(2)}" stroke="#d8e0ea" stroke-width="1" />
254
+ <text x="14" y="${(y + 4).toFixed(2)}" class="profile-axis">${escapeHtml(tick.toFixed(tick % 1 === 0 ? 0 : 1))} ${escapeHtml(profile.depthUnit)}</text>
255
+ `;
256
+ }).join('')}
257
+ ${layers.map((layer) => {
258
+ const y1 = yForDepth(layer.depthFrom);
259
+ const y2 = Math.max(y1 + 18, yForDepth(layer.depthTo));
260
+ return `
261
+ <g>
262
+ <title>${escapeHtml(`${layer.label}: ${layer.description}`)}</title>
263
+ <path d="M ${left} ${y1.toFixed(2)} L ${right} ${y1.toFixed(2)} L ${right} ${y2.toFixed(2)} L ${left} ${y2.toFixed(2)} Z"
264
+ fill="${toneColor(layer.tone)}" stroke="#7a8aa0" stroke-width="1.2" ${layer.uncertain ? 'stroke-dasharray="8 6"' : ''} opacity="0.88" />
265
+ <text x="${left + 18}" y="${(y1 + Math.min(34, (y2 - y1) / 2 + 5)).toFixed(2)}" class="profile-label">${escapeHtml(compactSvgText(layer.label, 32))}</text>
266
+ <text x="${left + 18}" y="${(y1 + Math.min(52, (y2 - y1) / 2 + 23)).toFixed(2)}" class="profile-small">${escapeHtml(compactSvgText(layer.description, 64))}</text>
267
+ </g>
268
+ `;
269
+ }).join('')}
270
+ ${profile.columns.map((column, index) => {
271
+ const x = columnX(index);
272
+ const depth = column.totalDepth ?? profile.maxDepth;
273
+ const bottomY = yForDepth(depth);
274
+ const waterY = column.waterTableDepth != null ? yForDepth(column.waterTableDepth) : null;
275
+ return `
276
+ <g>
277
+ <line x1="${x.toFixed(2)}" y1="${plotTop - 10}" x2="${x.toFixed(2)}" y2="${bottomY.toFixed(2)}" stroke="#0b4f8a" stroke-width="3" />
278
+ <circle cx="${x.toFixed(2)}" cy="${plotTop - 12}" r="6" fill="#0b4f8a" />
279
+ <text x="${(x - 24).toFixed(2)}" y="28" class="profile-title">${escapeHtml(column.boreholeId)}</text>
280
+ <text x="${(x - 36).toFixed(2)}" y="${height - 32}" class="profile-small">TD ${escapeHtml(depth.toFixed(2))} ${escapeHtml(profile.depthUnit)}</text>
281
+ ${waterY != null ? `
282
+ <line x1="${(x - 34).toFixed(2)}" y1="${waterY.toFixed(2)}" x2="${(x + 34).toFixed(2)}" y2="${waterY.toFixed(2)}" stroke="#0891b2" stroke-width="2" />
283
+ <text x="${(x - 40).toFixed(2)}" y="${(waterY - 7).toFixed(2)}" class="profile-water">GW</text>
284
+ ` : ''}
285
+ </g>
286
+ `;
287
+ }).join('')}
288
+ </svg>
289
+ </div>
290
+ <p class="verification-note">AI-assisted ground model. Use source logs and engineering judgment before adopting layer continuity, groundwater, or design parameters.</p>
291
+ </section>
292
+ `;
293
+ }
156
294
  function renderBoreholeProfile(profile) {
157
295
  if (!profile || profile.columns.length === 0 || profile.maxDepth <= 0) {
158
296
  return `
@@ -268,9 +406,21 @@ function renderSourceEvidence(dossier) {
268
406
  <span>${escapeHtml(card.parseStatus)} | ${escapeHtml(`${card.confidence}%`)}</span>
269
407
  </div>
270
408
  <h3>${escapeHtml(card.title)}</h3>
271
- <p>${escapeHtml(sourceModeLabel(card.sourceHint))}</p>
409
+ <p>${escapeHtml([
410
+ sourceModeLabel(card.sourceHint),
411
+ card.cacheStatus === 'hit'
412
+ ? 'Evidence cache reused'
413
+ : card.cacheStatus === 'stored'
414
+ ? 'Evidence cached for reruns'
415
+ : null,
416
+ ].filter(Boolean).join(' | '))}</p>
272
417
  ${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
418
  ${card.warnings.length > 0 ? `<small>Human verification recommended: ${escapeHtml(String(card.warnings.length))} retained warning(s).</small>` : ''}
419
+ <div class="evidence-actions" aria-label="${escapeHtml(`${card.pageLabel} review actions`)}">
420
+ <a href="#processing-audit">Open source page</a>
421
+ <button type="button" data-dossier-action="verified">Mark verified</button>
422
+ <button type="button" data-dossier-action="flagged">Flag issue</button>
423
+ </div>
274
424
  </article>
275
425
  `).join('')}
276
426
  </div>
@@ -329,6 +479,28 @@ function renderProcessingAudit(dossier, auditTables) {
329
479
  </details>
330
480
  `;
331
481
  }
482
+ function renderReviewBar(dossier) {
483
+ const missingCount = dossier.trustItems.filter((item) => reviewFilterValue(item) === 'missing').length;
484
+ const needsReviewCount = dossier.trustItems.filter((item) => reviewFilterValue(item) === 'needs_review').length;
485
+ const reviewBadgeValue = dossier.badges.find((badge) => /review/i.test(badge.label))?.value ?? 'Yes';
486
+ const reviewRequired = /^no$/i.test(reviewBadgeValue) ? 'Review ready' : 'Review required';
487
+ return `
488
+ <div class="review-bar" role="region" aria-label="Human review workflow">
489
+ <div>
490
+ <strong>${escapeHtml(reviewRequired)}</strong>
491
+ <span>${escapeHtml(`${dossier.pageCards.length} page(s), ${dossier.trustItems.length} review item(s), ${needsReviewCount} need review, ${missingCount} missing`)}</span>
492
+ </div>
493
+ <div class="review-actions">
494
+ <a href="#parameters" class="primary-action">Approve Extraction</a>
495
+ <a href="#risks">Flag for Review</a>
496
+ <button type="button" onclick="window.print()">Export Dossier</button>
497
+ <a href="#ground-cross-section">Generate Ground Model</a>
498
+ <a href="#source-evidence">Ask Geotech Agent</a>
499
+ <a href="#processing-audit">Open Audit Trail</a>
500
+ </div>
501
+ </div>
502
+ `;
503
+ }
332
504
  function renderActionBar() {
333
505
  return `
334
506
  <div class="action-bar" aria-label="Review actions">
@@ -391,6 +563,7 @@ export function renderIngestDossierAsHtml(dossier) {
391
563
  background: var(--bg);
392
564
  font-variant-numeric: tabular-nums;
393
565
  overflow-x: hidden;
566
+ padding-bottom: 98px;
394
567
  }
395
568
 
396
569
  .layout {
@@ -531,6 +704,37 @@ export function renderIngestDossierAsHtml(dossier) {
531
704
 
532
705
  .hero-meta strong { color: var(--text); }
533
706
 
707
+ .status-badge-row {
708
+ display: flex;
709
+ flex-wrap: wrap;
710
+ gap: 9px;
711
+ margin-top: 16px;
712
+ }
713
+
714
+ .status-badge {
715
+ display: inline-grid;
716
+ gap: 4px;
717
+ min-width: 126px;
718
+ padding: 10px 12px;
719
+ border: 1px solid rgba(102, 112, 133, 0.18);
720
+ border-radius: 14px;
721
+ line-height: 1.2;
722
+ }
723
+
724
+ .status-badge span {
725
+ color: currentColor;
726
+ opacity: 0.72;
727
+ font-size: 0.68rem;
728
+ text-transform: uppercase;
729
+ font-weight: 900;
730
+ }
731
+
732
+ .status-badge strong {
733
+ color: var(--text);
734
+ font-size: 0.86rem;
735
+ overflow-wrap: anywhere;
736
+ }
737
+
534
738
  .executive-grid, .metric-grid, .card-grid, .evidence-grid, .audit-page-grid, .footer-grid {
535
739
  display: grid;
536
740
  gap: 16px;
@@ -633,6 +837,67 @@ export function renderIngestDossierAsHtml(dossier) {
633
837
  color: #ffffff;
634
838
  }
635
839
 
840
+ .review-bar {
841
+ position: fixed;
842
+ left: 50%;
843
+ bottom: 0;
844
+ z-index: 45;
845
+ display: flex;
846
+ align-items: center;
847
+ justify-content: space-between;
848
+ gap: 18px;
849
+ width: min(1180px, calc(100vw - 36px));
850
+ margin: 10px auto 0;
851
+ padding: 13px 14px;
852
+ border: 1px solid #263244;
853
+ border-radius: 18px 18px 0 0;
854
+ background: rgba(11, 18, 32, 0.94);
855
+ color: #f8fafc;
856
+ box-shadow: 0 -18px 38px rgba(11, 18, 32, 0.2);
857
+ backdrop-filter: blur(14px);
858
+ transform: translate(-50%, 120%);
859
+ transition: transform 180ms ease;
860
+ }
861
+
862
+ .review-bar.visible {
863
+ transform: translate(-50%, 0);
864
+ }
865
+
866
+ .review-bar > div:first-child {
867
+ display: grid;
868
+ gap: 3px;
869
+ min-width: 220px;
870
+ }
871
+
872
+ .review-bar strong { font-size: 0.95rem; }
873
+ .review-bar span { color: #94a3b8; font-size: 0.84rem; line-height: 1.4; }
874
+
875
+ .review-actions {
876
+ display: flex;
877
+ flex-wrap: wrap;
878
+ justify-content: flex-end;
879
+ gap: 8px;
880
+ }
881
+
882
+ .review-actions a, .review-actions button {
883
+ border: 1px solid #334155;
884
+ border-radius: 10px;
885
+ background: #111827;
886
+ color: #dbeafe;
887
+ padding: 8px 10px;
888
+ font: inherit;
889
+ font-size: 0.78rem;
890
+ font-weight: 850;
891
+ text-decoration: none;
892
+ cursor: pointer;
893
+ }
894
+
895
+ .review-actions .primary-action {
896
+ background: #38bdf8;
897
+ border-color: #38bdf8;
898
+ color: #0b1220;
899
+ }
900
+
636
901
  .data-section {
637
902
  display: grid;
638
903
  gap: 16px;
@@ -698,6 +963,27 @@ export function renderIngestDossierAsHtml(dossier) {
698
963
  height: auto;
699
964
  }
700
965
 
966
+ .cross-section-shell {
967
+ background:
968
+ linear-gradient(180deg, rgba(11, 79, 138, 0.04), rgba(22, 135, 93, 0.04)),
969
+ var(--surface);
970
+ }
971
+
972
+ .ground-cross-section {
973
+ display: block;
974
+ min-width: 840px;
975
+ width: 100%;
976
+ height: auto;
977
+ }
978
+
979
+ .verification-note {
980
+ color: var(--muted);
981
+ font-weight: 700;
982
+ line-height: 1.55;
983
+ padding-left: 14px;
984
+ border-left: 3px solid var(--warning);
985
+ }
986
+
701
987
  .profile-title { font: 800 15px Inter, Segoe UI, sans-serif; fill: #101828; }
702
988
  .profile-label { font: 800 13px Inter, Segoe UI, sans-serif; fill: #101828; }
703
989
  .profile-small { font: 11px Inter, Segoe UI, sans-serif; fill: #475467; }
@@ -750,6 +1036,71 @@ export function renderIngestDossierAsHtml(dossier) {
750
1036
  line-height: 1;
751
1037
  }
752
1038
 
1039
+ .trust-controls {
1040
+ display: flex;
1041
+ align-items: end;
1042
+ justify-content: space-between;
1043
+ gap: 12px;
1044
+ flex-wrap: wrap;
1045
+ padding: 14px;
1046
+ border: 1px solid var(--border);
1047
+ border-radius: var(--radius);
1048
+ background: var(--surface);
1049
+ }
1050
+
1051
+ .trust-controls label {
1052
+ display: grid;
1053
+ gap: 7px;
1054
+ flex: 1 1 320px;
1055
+ color: var(--muted);
1056
+ font-size: 0.75rem;
1057
+ text-transform: uppercase;
1058
+ font-weight: 900;
1059
+ }
1060
+
1061
+ .trust-controls input {
1062
+ width: 100%;
1063
+ border: 1px solid var(--border);
1064
+ border-radius: 12px;
1065
+ background: #ffffff;
1066
+ color: var(--text);
1067
+ padding: 11px 12px;
1068
+ font: inherit;
1069
+ font-size: 0.92rem;
1070
+ outline: none;
1071
+ text-transform: none;
1072
+ font-weight: 600;
1073
+ }
1074
+
1075
+ .trust-controls input:focus {
1076
+ border-color: #93c5fd;
1077
+ box-shadow: 0 0 0 4px rgba(11, 79, 138, 0.08);
1078
+ }
1079
+
1080
+ .filter-row {
1081
+ display: flex;
1082
+ gap: 8px;
1083
+ flex-wrap: wrap;
1084
+ }
1085
+
1086
+ .filter-button {
1087
+ border: 1px solid var(--border);
1088
+ border-radius: 999px;
1089
+ background: var(--surface-soft);
1090
+ color: var(--muted);
1091
+ padding: 9px 12px;
1092
+ font: inherit;
1093
+ font-size: 0.8rem;
1094
+ font-weight: 850;
1095
+ cursor: pointer;
1096
+ }
1097
+
1098
+ .filter-button.active {
1099
+ background: var(--primary);
1100
+ color: #ffffff;
1101
+ border-color: var(--primary);
1102
+ }
1103
+
753
1104
  .evidence-grid {
754
1105
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
755
1106
  }
@@ -778,6 +1129,38 @@ export function renderIngestDossierAsHtml(dossier) {
778
1129
  line-height: 1.45;
779
1130
  }
780
1131
 
1132
+ .evidence-actions {
1133
+ display: flex;
1134
+ flex-wrap: wrap;
1135
+ gap: 8px;
1136
+ padding-top: 4px;
1137
+ }
1138
+
1139
+ .evidence-actions a, .evidence-actions button {
1140
+ border: 1px solid var(--border);
1141
+ border-radius: 10px;
1142
+ background: rgba(255, 255, 255, 0.62);
1143
+ color: var(--primary);
1144
+ padding: 8px 9px;
1145
+ font: inherit;
1146
+ font-size: 0.78rem;
1147
+ font-weight: 850;
1148
+ text-decoration: none;
1149
+ cursor: pointer;
1150
+ }
1151
+
1152
+ .evidence-actions button[data-state="verified"] {
1153
+ color: var(--success);
1154
+ border-color: rgba(22, 135, 93, 0.28);
1155
+ background: var(--success-soft);
1156
+ }
1157
+
1158
+ .evidence-actions button[data-state="flagged"] {
1159
+ color: var(--warning);
1160
+ border-color: rgba(183, 121, 31, 0.28);
1161
+ background: var(--warning-soft);
1162
+ }
1163
+
781
1164
  .empty-state {
782
1165
  padding: 18px;
783
1166
  border: 1px dashed var(--border);
@@ -891,6 +1274,30 @@ export function renderIngestDossierAsHtml(dossier) {
891
1274
  line-height: 1.58;
892
1275
  }
893
1276
 
1277
+ tr[hidden] { display: none; }
1278
+
1279
+ .toast-region {
1280
+ position: fixed;
1281
+ top: 18px;
1282
+ right: 18px;
1283
+ z-index: 60;
1284
+ display: grid;
1285
+ gap: 8px;
1286
+ pointer-events: none;
1287
+ }
1288
+
1289
+ .toast {
1290
+ max-width: 320px;
1291
+ padding: 11px 14px;
1292
+ border: 1px solid #334155;
1293
+ border-radius: 12px;
1294
+ background: #0b1220;
1295
+ color: #f8fafc;
1296
+ box-shadow: 0 18px 38px rgba(11, 18, 32, 0.22);
1297
+ font-size: 0.86rem;
1298
+ font-weight: 700;
1299
+ }
1300
+
894
1301
  @media (max-width: 980px) {
895
1302
  .layout { grid-template-columns: minmax(0, 1fr); padding: 18px; max-width: 100%; }
896
1303
  .sidebar { position: static; min-height: auto; }
@@ -899,15 +1306,23 @@ export function renderIngestDossierAsHtml(dossier) {
899
1306
  }
900
1307
 
901
1308
  @media (max-width: 620px) {
1309
+ body { padding-bottom: 0; }
902
1310
  .layout { display: block; width: 100%; max-width: 390px; margin: 0; padding: 12px; }
903
1311
  .sidebar, .content, .hero, .data-section, .audit-drawer, .footer-grid {
904
1312
  width: 100%;
905
1313
  max-width: 100%;
906
1314
  }
1315
+ .sidebar { padding: 16px; min-height: auto; border-radius: 14px; }
1316
+ .brand { gap: 5px; padding-bottom: 12px; margin-bottom: 10px; }
1317
+ .brand span { display: none; }
1318
+ .nav-list { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
1319
+ .nav-list a { padding: 8px 9px; font-size: 0.82rem; }
907
1320
  .content { margin-top: 18px; }
908
1321
  .hero { padding: 22px; }
909
1322
  .executive-grid, .metric-grid, .card-grid, .evidence-grid, .footer-grid { grid-template-columns: 1fr; }
910
- .nav-list { grid-template-columns: 1fr; }
1323
+ .review-bar { position: static; transform: none; width: 100%; margin-top: 18px; border-radius: 18px; align-items: stretch; }
1324
+ .review-bar.visible { transform: none; }
1325
+ .review-actions { justify-content: flex-start; }
911
1326
  h1 { font-size: 1.72rem; max-width: 100%; }
912
1327
  }
913
1328
  </style>
@@ -922,6 +1337,7 @@ export function renderIngestDossierAsHtml(dossier) {
922
1337
  <nav class="nav-list">
923
1338
  <a href="#overview">Overview</a>
924
1339
  <a href="#ground-model">Ground Model</a>
1340
+ <a href="#ground-cross-section">Cross-Section</a>
925
1341
  <a href="#boreholes">Boreholes</a>
926
1342
  <a href="#parameters">Engineering Parameters</a>
927
1343
  <a href="#risks">Risks and Limitations</a>
@@ -938,6 +1354,7 @@ export function renderIngestDossierAsHtml(dossier) {
938
1354
  <h1>${escapeHtml(dossier.title)}</h1>
939
1355
  <p class="hero-subtitle">${escapeHtml(subtitle)}</p>
940
1356
  <p class="hero-summary">${escapeHtml(dossier.summary)}</p>
1357
+ ${renderStatusBadges(dossier)}
941
1358
  </div>
942
1359
  <div class="hero-meta">
943
1360
  <strong>${escapeHtml(dossier.sourceLabel)}</strong>
@@ -977,6 +1394,7 @@ export function renderIngestDossierAsHtml(dossier) {
977
1394
  </div>
978
1395
  </section>
979
1396
 
1397
+ ${renderGroundModelCrossSection(dossier.boreholeProfile)}
980
1398
  ${renderBoreholeProfile(dossier.boreholeProfile)}
981
1399
  ${renderTrustTable(dossier)}
982
1400
  ${mainTables.map((table) => renderTable(table)).join('')}
@@ -1010,6 +1428,67 @@ export function renderIngestDossierAsHtml(dossier) {
1010
1428
  </section>
1011
1429
  </main>
1012
1430
  </div>
1431
+ ${renderReviewBar(dossier)}
1432
+ <div class="toast-region" aria-live="polite" aria-atomic="true"></div>
1433
+ <script>
1434
+ (() => {
1435
+ const search = document.getElementById('trust-search');
1436
+ const rows = Array.from(document.querySelectorAll('[data-trust-row]'));
1437
+ const buttons = Array.from(document.querySelectorAll('[data-trust-filter]'));
1438
+ const toastRegion = document.querySelector('.toast-region');
1439
+ const reviewBar = document.querySelector('.review-bar');
1440
+ let activeFilter = 'all';
1441
+
1442
+ const showToast = (message) => {
1443
+ if (!toastRegion) return;
1444
+ const toast = document.createElement('div');
1445
+ toast.className = 'toast';
1446
+ toast.textContent = message;
1447
+ toastRegion.appendChild(toast);
1448
+ window.setTimeout(() => toast.remove(), 2600);
1449
+ };
1450
+
1451
+ const applyTrustFilter = () => {
1452
+ const query = String(search?.value ?? '').trim().toLowerCase();
1453
+ rows.forEach((row) => {
1454
+ const text = row.getAttribute('data-search') ?? '';
1455
+ const review = row.getAttribute('data-review') ?? '';
1456
+ const queryMatch = !query || text.includes(query);
1457
+ const filterMatch = activeFilter === 'all' || review === activeFilter;
1458
+ row.hidden = !(queryMatch && filterMatch);
1459
+ });
1460
+ };
1461
+
1462
+ const syncReviewBar = () => {
1463
+ if (!reviewBar) return;
1464
+ reviewBar.classList.toggle('visible', window.innerWidth > 620 && window.scrollY > 420);
1465
+ };
1466
+
1467
+ syncReviewBar();
1468
+ window.addEventListener('scroll', syncReviewBar, { passive: true });
1469
+ window.addEventListener('resize', syncReviewBar);
1470
+
1471
+ search?.addEventListener('input', applyTrustFilter);
1472
+ buttons.forEach((button) => {
1473
+ button.addEventListener('click', () => {
1474
+ activeFilter = button.getAttribute('data-trust-filter') ?? 'all';
1475
+ buttons.forEach((candidate) => candidate.classList.toggle('active', candidate === button));
1476
+ applyTrustFilter();
1477
+ });
1478
+ });
1479
+
1480
+ document.querySelectorAll('[data-dossier-action]').forEach((control) => {
1481
+ control.addEventListener('click', () => {
1482
+ const state = control.getAttribute('data-dossier-action') ?? '';
1483
+ control.setAttribute('data-state', state);
1484
+ control.textContent = state === 'verified' ? 'Verified' : 'Issue flagged';
1485
+ showToast(state === 'verified'
1486
+ ? 'Evidence item marked verified in this local dossier view.'
1487
+ : 'Evidence item flagged for engineering review in this local dossier view.');
1488
+ });
1489
+ });
1490
+ })();
1491
+ </script>
1013
1492
  </body>
1014
1493
  </html>`;
1015
1494
  }