@puredesktop/puredesktop-ui-bridge 2.1.7 → 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +55 -1
  2. package/src/bridge/agentTypes.ts +21 -1
  3. package/src/bridge/collectionImagePaste.ts +19 -1
  4. package/src/bridge/context.mjs +32 -0
  5. package/src/bridge/documents.d.mts +76 -0
  6. package/src/bridge/documents.mjs +40 -0
  7. package/src/bridge/methods.d.mts +23 -0
  8. package/src/bridge/methods.mjs +23 -0
  9. package/src/bridge/pureRender/base.css +44 -0
  10. package/src/bridge/pureRender/document.ts +29 -2
  11. package/src/bridge/pureRender/index.ts +3 -0
  12. package/src/bridge/pureRender/normalize.ts +167 -0
  13. package/src/bridge/pureRender/sanitizeExportBody.test.ts +51 -0
  14. package/src/bridge/pureRender/sanitizeExportBody.ts +61 -0
  15. package/src/bridge/pureRender/styles.ts +125 -5
  16. package/src/bridge/pureRender/theme.test.ts +259 -0
  17. package/src/bridge/pureRender/theme.ts +19 -1
  18. package/src/bridge/pureRender/types.ts +33 -0
  19. package/src/bridge/pureRender/visualLayoutInspection.test.ts +26 -0
  20. package/src/bridge/pureRender/visualLayoutInspection.ts +75 -0
  21. package/src/bridge/react/useDocumentHotkeys.ts +41 -0
  22. package/src/bridge/react/useDocumentLifecycle.tsx +359 -0
  23. package/src/bridge/react/usePlatformAppSettings.test.tsx +105 -0
  24. package/src/bridge/react/usePlatformAppSettings.tsx +104 -0
  25. package/src/bridge/react/usePlatformJsonStore.test.tsx +152 -0
  26. package/src/bridge/react/usePlatformJsonStore.tsx +149 -0
  27. package/src/bridge/storage.d.mts +6 -6
  28. package/src/bridge/storage.test.ts +6 -6
  29. package/src/bridge/types.ts +7 -0
  30. package/src/components/agents/AgentDrawerPanel.tsx +2 -0
  31. package/src/components/agents/AgentMessageList.tsx +6 -1
  32. package/src/components/agents/AgentToolCallCard.tsx +20 -20
  33. package/src/components/agents/ContextPicker.tsx +239 -0
  34. package/src/components/agents/agentPanelStyles.ts +3 -2
  35. package/src/components/agents/agentTypes.ts +2 -0
  36. package/src/components/chrome/WorkspaceTabStrip.tsx +18 -1
  37. package/src/components/chrome/workspaceTabTypes.ts +2 -0
  38. package/src/components/common/containers/AppFrame.test.tsx +4 -4
  39. package/src/components/common/containers/AppFrame.tsx +10 -1
  40. package/src/components/common/containers/AppHeader.tsx +22 -0
  41. package/src/components/common/documents/DocumentHeaderActions.tsx +206 -0
  42. package/src/components/common/documents/DocumentSwitcher.tsx +1176 -0
  43. package/src/components/common/documents/index.ts +8 -0
  44. package/src/components/common/research/EvidenceDossier.tsx +1232 -150
  45. package/src/components/common/research/index.ts +2 -0
  46. package/src/components/print-design/PrintDesignPanel.test.tsx +430 -40
  47. package/src/components/print-design/PrintDesignPanel.tsx +1770 -207
  48. package/src/components/print-design/index.ts +2 -0
  49. package/src/components/print-design/previewContent.test.ts +32 -0
  50. package/src/components/print-design/previewContent.ts +31 -0
  51. package/src/context/buildContextDocument.test.ts +240 -0
  52. package/src/context/buildContextDocument.ts +309 -0
  53. package/src/context/contextAccess.ts +66 -0
  54. package/src/context/contextConfig.ts +72 -0
  55. package/src/context/contextDocument.test.ts +155 -0
  56. package/src/context/contextDocument.ts +300 -0
  57. package/src/context/contextSections.test.ts +88 -0
  58. package/src/context/contextSections.ts +84 -0
  59. package/src/context/htmlToContextMarkdown.test.ts +134 -0
  60. package/src/context/htmlToContextMarkdown.ts +369 -0
  61. package/src/theme/appAccents.test.ts +36 -35
  62. package/src/theme/appAccents.ts +87 -104
  63. package/src/theme/appIdentityCss.mjs +169 -231
  64. package/src/theme/appIdentityCss.ts +213 -233
@@ -1,4 +1,4 @@
1
- import { useState, type ReactNode } from 'react'
1
+ import { Fragment, useState, type ReactNode } from 'react'
2
2
  import { styled } from 'styled-components'
3
3
 
4
4
  export type EvidenceStrength = 'strong' | 'working' | 'weak'
@@ -10,16 +10,32 @@ export interface EvidenceSource {
10
10
  url?: string
11
11
  note: string
12
12
  quote?: string
13
+ sourceDate?: string
14
+ freshness?: 'fresh' | 'stale' | 'undated'
15
+ freshnessMode?: 'recent' | 'open'
13
16
  strength?: EvidenceStrength
14
17
  }
15
18
 
19
+ /** A jargon term found in a finding's claim with a plain-language note. */
20
+ export interface EvidenceTermNote {
21
+ term: string
22
+ explanation: string
23
+ /** Spelled-out form when the term is or contains an acronym. */
24
+ expansion?: string
25
+ /** Authoritative explainer link, included only when clearly canonical. */
26
+ url?: string
27
+ }
28
+
16
29
  export interface EvidenceFinding {
17
30
  id: string
18
31
  title: string
19
32
  claim: string
20
33
  confidence: EvidenceStrength
21
34
  annotation?: string
35
+ freshnessMode?: 'recent' | 'open'
22
36
  evidence: EvidenceSource[]
37
+ /** Plain-language notes for specialist terms, rendered as inline annotations. */
38
+ glossary?: EvidenceTermNote[]
23
39
  }
24
40
 
25
41
  export interface EvidenceDossierProps {
@@ -33,6 +49,7 @@ export interface EvidenceDossierProps {
33
49
  nextSteps: string[]
34
50
  onTitleChange?: (title: string) => void
35
51
  onDeleteFinding?: (findingId: string) => void
52
+ onUpdate?: () => void
36
53
  footer?: ReactNode
37
54
  className?: string
38
55
  }
@@ -43,26 +60,28 @@ const strengthLabels: Record<EvidenceStrength, string> = {
43
60
  weak: 'needs support',
44
61
  }
45
62
 
46
- type DossierRenderMode = 'read' | 'review' | 'audit'
63
+ type DossierRenderMode = 'read' | 'review' | 'audit' | 'markdown'
47
64
 
48
65
  const modeLabels: Record<DossierRenderMode, string> = {
49
66
  read: 'Read',
50
67
  review: 'Claims',
51
68
  audit: 'Sources',
69
+ markdown: 'MD',
52
70
  }
53
71
 
54
72
  const modeDescriptions: Record<DossierRenderMode, string> = {
55
73
  read: 'Polished dossier with quiet evidence links.',
56
74
  review: 'Every claim opened with its supporting evidence.',
57
75
  audit: 'Source-first trace for verification.',
76
+ markdown: 'Markdown handoff for agents, slides, and reports.',
58
77
  }
59
78
 
60
79
  function cleanDisplayText(value: unknown): string {
61
80
  return String(value ?? '')
62
- .replace(/\*\*([^*]+)\*\*/g, '$1')
63
- .replace(/\*([^*]+)\*/g, '$1')
64
- .replace(/__([^_]+)__/g, '$1')
65
- .replace(/_([^_]+)_/g, '$1')
81
+ .replace(/\*\*([^*\n]+)\*\*/g, '$1')
82
+ .replace(/\*([^*\n]+)\*/g, '$1')
83
+ .replace(/__([^_\n]+)__/g, '$1')
84
+ .replace(/_([^_\n]+)_/g, '$1')
66
85
  .replace(/\s*\[(?:\d+(?:,\s*)?)+\]/g, '')
67
86
  .replace(/\s*\[\d+\](?=\[\d+\])/g, '')
68
87
  .replace(/\[(\d+)\]/g, '')
@@ -71,13 +90,241 @@ function cleanDisplayText(value: unknown): string {
71
90
  .trim()
72
91
  }
73
92
 
93
+ function displayPrompt(value: string): string {
94
+ return cleanDisplayText(value)
95
+ .replace(/\bcodign\b/gi, 'coding')
96
+ .replace(/\bmodles\b/gi, 'models')
97
+ .replace(/\bmdoels\b/gi, 'models')
98
+ .replace(/\bresearhc\b/gi, 'research')
99
+ .replace(/\bquesiton\b/gi, 'question')
100
+ .replace(/\bquesitons\b/gi, 'questions')
101
+ .replace(/\bsoruce\b/gi, 'source')
102
+ .replace(/\bsoruces\b/gi, 'sources')
103
+ .replace(/\s+/g, ' ')
104
+ .trim()
105
+ }
106
+
107
+ function displayTitle(value: string, fallback = 'Research finding'): string {
108
+ const cleaned = cleanDisplayText(value) || fallback
109
+ return cleaned.replace(/[A-Za-z]/, letter => letter.toUpperCase())
110
+ }
111
+
112
+ function cleanRichText(value: unknown): string {
113
+ return String(value ?? '')
114
+ .replace(/\*\*([^*\n]+)\*\*/g, '$1')
115
+ .replace(/\*([^*\n]+)\*/g, '$1')
116
+ .replace(/__([^_\n]+)__/g, '$1')
117
+ .replace(/_([^_\n]+)_/g, '$1')
118
+ .replace(/\s*\[(?:\d+(?:,\s*)?)+\]/g, '')
119
+ .replace(/\s*\[\d+\](?=\[\d+\])/g, '')
120
+ .replace(/\[(\d+)\]/g, '')
121
+ .replace(/[^\S\r\n]+([,.;:])/g, '$1')
122
+ .replace(/[^\S\r\n]{2,}/g, ' ')
123
+ .replace(/\n{3,}/g, '\n\n')
124
+ .trim()
125
+ }
126
+
127
+ function structuredFindingFields(value: string): {
128
+ title?: string
129
+ claim?: string
130
+ annotation?: string
131
+ } | null {
132
+ const decodeJsonString = (raw: string): string => {
133
+ try {
134
+ return JSON.parse(`"${raw}"`) as string
135
+ } catch {
136
+ return raw.replace(/\\"/g, '"').replace(/\\n/g, '\n')
137
+ }
138
+ }
139
+ const readField = (source: string, key: string): string => {
140
+ const match = source.match(
141
+ new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)`, 'is'),
142
+ )
143
+ return match?.[1] ? decodeJsonString(match[1]) : ''
144
+ }
145
+ const trimmed = value
146
+ .trim()
147
+ .replace(/^```(?:json)?\s*/i, '')
148
+ .replace(/\s*```$/i, '')
149
+ .trim()
150
+ const objectStart = trimmed.indexOf('{')
151
+ if (objectStart < 0) return null
152
+ const jsonish = trimmed.slice(objectStart)
153
+ try {
154
+ const parsed = JSON.parse(jsonish) as Record<string, unknown>
155
+ return {
156
+ title:
157
+ typeof parsed.title === 'string'
158
+ ? cleanDisplayText(parsed.title)
159
+ : undefined,
160
+ claim:
161
+ typeof parsed.claim === 'string'
162
+ ? cleanRichText(parsed.claim)
163
+ : typeof parsed.summary === 'string'
164
+ ? cleanRichText(parsed.summary)
165
+ : undefined,
166
+ annotation:
167
+ typeof parsed.annotation === 'string'
168
+ ? cleanDisplayText(parsed.annotation)
169
+ : typeof parsed.caveat === 'string'
170
+ ? cleanDisplayText(parsed.caveat)
171
+ : undefined,
172
+ }
173
+ } catch {
174
+ const title = cleanDisplayText(readField(jsonish, 'title'))
175
+ const claim =
176
+ cleanRichText(readField(jsonish, 'claim')) ||
177
+ cleanRichText(readField(jsonish, 'summary')) ||
178
+ cleanRichText(readField(jsonish, 'finding'))
179
+ const annotation =
180
+ cleanDisplayText(readField(jsonish, 'annotation')) ||
181
+ cleanDisplayText(readField(jsonish, 'caveat'))
182
+ return title || claim || annotation ? { title, claim, annotation } : null
183
+ }
184
+ }
185
+
186
+ function tableCells(line: string): string[] {
187
+ const trimmed = line.trim()
188
+ if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) return []
189
+ return trimmed
190
+ .slice(1, -1)
191
+ .split('|')
192
+ .map(cell => cleanDisplayText(cell))
193
+ }
194
+
195
+ function isTableSeparator(line: string): boolean {
196
+ const cells = tableCells(line)
197
+ return Boolean(cells.length) && cells.every(cell => /^:?-{3,}:?$/.test(cell))
198
+ }
199
+
200
+ function readableParagraphs(text: string): string[] {
201
+ const cleaned = cleanDisplayText(text)
202
+ if (!cleaned) return []
203
+ const numberedSections = cleaned
204
+ .replace(/\s*;\s*(?=\(\d+\)\s)/g, '\n')
205
+ .split(/\n+/)
206
+ .map(section => section.trim())
207
+ .filter(Boolean)
208
+ if (
209
+ numberedSections.length >= 3 &&
210
+ numberedSections
211
+ .slice(1)
212
+ .every(section => /^\(\d+\)\s/.test(section))
213
+ ) {
214
+ return numberedSections
215
+ }
216
+ if (cleaned.length < 420) return [cleaned]
217
+ // Split only at whitespace AFTER sentence punctuation — lossless, and never
218
+ // breaks inside tokens like "2.5", "AGENTS.md", or "e.g." (no space there).
219
+ const sentences = cleaned.split(/(?<=[.!?])\s+/)
220
+ if (sentences.length < 4) return [cleaned]
221
+ const paragraphs: string[] = []
222
+ let current = ''
223
+ for (const sentence of sentences.map(item => item.trim()).filter(Boolean)) {
224
+ const next = current ? `${current} ${sentence}` : sentence
225
+ if (current && next.length > 260) {
226
+ paragraphs.push(current)
227
+ current = sentence
228
+ } else {
229
+ current = next
230
+ }
231
+ }
232
+ if (current) paragraphs.push(current)
233
+ return paragraphs
234
+ }
235
+
236
+ function summarySentence(value: string, fallback: string): string {
237
+ const cleaned = cleanDisplayText(value || fallback)
238
+ if (!cleaned) return fallback
239
+ const firstSentence = cleaned.split(/(?<=[.!?])\s+/)[0]?.trim()
240
+ const summary = firstSentence && firstSentence.length >= 70 ? firstSentence : cleaned
241
+ return summary.length > 280 ? `${summary.slice(0, 277).trim()}...` : summary
242
+ }
243
+
244
+ function sourceNoteSummary(value: string): string {
245
+ const cleaned = cleanDisplayText(value)
246
+ .replace(/^Source date:\s*[^.]+\.\s*/i, '')
247
+ .replace(/^https?:\/\/\S+\s*/i, '')
248
+ .trim()
249
+ if (!cleaned) return 'Linked source.'
250
+ const tableStart = cleaned.indexOf('|')
251
+ if (tableStart > 0 && tableStart < 180) {
252
+ return cleaned.slice(0, tableStart).trim()
253
+ }
254
+ const firstSentence = cleaned.split(/(?<=[.!?])\s+/)[0]?.trim() || cleaned
255
+ return firstSentence.length > 150
256
+ ? `${firstSentence.slice(0, 147).trim()}...`
257
+ : firstSentence
258
+ }
259
+
260
+ function pipeSummaryRows(value: string): Array<[string, string]> {
261
+ const cleaned = cleanDisplayText(value)
262
+ const pipeIndex = cleaned.indexOf('|')
263
+ if (pipeIndex < 0) return []
264
+ const pipeText = cleaned.slice(pipeIndex)
265
+ const cells = pipeText
266
+ .split('|')
267
+ .map(cell => cleanDisplayText(cell))
268
+ .filter(Boolean)
269
+ .filter(cell => !/^-+$/.test(cell))
270
+ if (cells.length < 4) return []
271
+ const rows: Array<[string, string]> = []
272
+ for (let index = 0; index < Math.min(cells.length - 1, 10); index += 2) {
273
+ rows.push([cells[index], cells[index + 1]])
274
+ }
275
+ return rows.slice(0, 4)
276
+ }
277
+
278
+ function sourceUrlDisplay(url: string): string {
279
+ try {
280
+ const parsed = new URL(url)
281
+ return `${parsed.hostname.replace(/^www\./, '')}${parsed.pathname}`
282
+ } catch {
283
+ return url
284
+ }
285
+ }
286
+
287
+ function LinkifiedText({ text }: { text: string }): ReactNode {
288
+ const parts = text.split(/(https?:\/\/[^\s]+)/g)
289
+ return (
290
+ <>
291
+ {parts.map((part, index) => {
292
+ if (!/^https?:\/\//i.test(part)) return part
293
+ const trailing = part.match(/[),.;:]+$/)?.[0] ?? ''
294
+ const href = trailing ? part.slice(0, -trailing.length) : part
295
+ return (
296
+ <span key={`${href}-${index}`}>
297
+ <a href={href} target="_blank" rel="noreferrer">
298
+ {sourceUrlDisplay(href)}
299
+ </a>
300
+ {trailing}
301
+ </span>
302
+ )
303
+ })}
304
+ </>
305
+ )
306
+ }
307
+
308
+ /** Untrusted model/citation URLs: only http(s) may become a clickable link
309
+ * (a `javascript:`/`data:` href would execute script on click). */
310
+ function safeHttpUrl(url: string | undefined): string | undefined {
311
+ if (!url) return undefined
312
+ try {
313
+ const protocol = new URL(url).protocol
314
+ return protocol === 'http:' || protocol === 'https:' ? url : undefined
315
+ } catch {
316
+ return undefined
317
+ }
318
+ }
319
+
74
320
  function sourceUrlWithTextFragment(source: EvidenceSource): string | undefined {
75
- if (!source.url) return undefined
321
+ const safe = safeHttpUrl(source.url)
322
+ if (!safe) return undefined
76
323
  const quote = cleanDisplayText(source.quote ?? '')
77
- if (!quote) return source.url
324
+ if (!quote) return safe
78
325
  const fragment = quote.slice(0, 140).trim()
79
- if (!fragment) return source.url
80
- const baseUrl = source.url.split('#')[0]
326
+ if (!fragment) return safe
327
+ const baseUrl = safe.split('#')[0]
81
328
  return `${baseUrl}#:~:text=${encodeURIComponent(fragment)}`
82
329
  }
83
330
 
@@ -112,6 +359,17 @@ function normalizeEvidenceSource(
112
359
  url: cleanDisplayText(value.url) || undefined,
113
360
  note: cleanDisplayText(value.note),
114
361
  quote: cleanDisplayText(value.quote) || undefined,
362
+ sourceDate: cleanDisplayText(value.sourceDate) || undefined,
363
+ freshness:
364
+ value.freshness === 'fresh' ||
365
+ value.freshness === 'stale' ||
366
+ value.freshness === 'undated'
367
+ ? value.freshness
368
+ : undefined,
369
+ freshnessMode:
370
+ value.freshnessMode === 'recent' || value.freshnessMode === 'open'
371
+ ? value.freshnessMode
372
+ : undefined,
115
373
  strength:
116
374
  value.strength === 'strong' ||
117
375
  value.strength === 'working' ||
@@ -128,15 +386,20 @@ function normalizeEvidenceFinding(
128
386
  if (!finding || typeof finding !== 'object') return null
129
387
  const value = finding as Partial<EvidenceFinding>
130
388
  const title = cleanDisplayText(value.title)
131
- const claim = cleanDisplayText(value.claim)
389
+ const claim = cleanRichText(value.claim)
132
390
  if (!title && !claim) return null
391
+ const structured =
392
+ structuredFindingFields(claim) || structuredFindingFields(title)
133
393
  const evidence = Array.isArray(value.evidence)
134
394
  ? value.evidence.map(normalizeEvidenceSource)
135
395
  : []
136
396
  return {
137
397
  id: cleanDisplayText(value.id) || `finding-${index}`,
138
- title: title || `Finding ${index + 1}`,
139
- claim,
398
+ title:
399
+ (!title || /^research finding$/i.test(title)) && structured?.title
400
+ ? displayTitle(structured.title)
401
+ : displayTitle(title || structured?.title || `Finding ${index + 1}`),
402
+ claim: structured?.claim || claim,
140
403
  confidence:
141
404
  value.confidence === 'strong' ||
142
405
  value.confidence === 'working' ||
@@ -145,8 +408,23 @@ function normalizeEvidenceFinding(
145
408
  : evidence.length
146
409
  ? 'working'
147
410
  : 'weak',
148
- annotation: cleanDisplayText(value.annotation) || undefined,
411
+ annotation:
412
+ structured?.annotation || cleanDisplayText(value.annotation) || undefined,
413
+ freshnessMode:
414
+ value.freshnessMode === 'recent' || value.freshnessMode === 'open'
415
+ ? value.freshnessMode
416
+ : undefined,
149
417
  evidence,
418
+ glossary: Array.isArray(value.glossary)
419
+ ? value.glossary.filter(
420
+ (note): note is EvidenceTermNote =>
421
+ Boolean(note) &&
422
+ typeof note.term === 'string' &&
423
+ typeof note.explanation === 'string' &&
424
+ Boolean(note.term.trim()) &&
425
+ Boolean(note.explanation.trim()),
426
+ )
427
+ : undefined,
150
428
  }
151
429
  }
152
430
 
@@ -159,11 +437,32 @@ function isEvidenceFinding(
159
437
  //#region styled-components
160
438
 
161
439
  const Dossier = styled.article`
440
+ --dossier-serif: var(
441
+ --platform-typography-font-family-content,
442
+ 'Source Serif 4',
443
+ Georgia,
444
+ serif
445
+ );
446
+ --dossier-sans: var(
447
+ --platform-typography-font-family,
448
+ 'Archivo',
449
+ system-ui,
450
+ sans-serif
451
+ );
452
+ --dossier-mono: var(
453
+ --platform-typography-font-family-mono,
454
+ 'JetBrains Mono',
455
+ ui-monospace,
456
+ monospace
457
+ );
458
+
162
459
  display: grid;
163
460
  gap: 24px;
164
461
  min-width: 0;
165
462
  position: relative;
166
463
  color: var(--platform-colors-text, #1c1a17);
464
+ font-family: var(--dossier-serif);
465
+ font-optical-sizing: auto;
167
466
  `
168
467
 
169
468
  const Header = styled.header`
@@ -180,6 +479,7 @@ const ModeDock = styled.div`
180
479
  z-index: 3;
181
480
  justify-self: end;
182
481
  gap: 5px;
482
+ font-family: var(--dossier-sans);
183
483
 
184
484
  @media (max-width: 760px) {
185
485
  justify-self: start;
@@ -216,11 +516,11 @@ const ModeButton = styled.button<{ $active: boolean }>`
216
516
  border: 0;
217
517
  border-radius: var(--platform-radius-sm);
218
518
  background: ${({ $active }) =>
219
- $active ? 'var(--platform-colors-surface-hover)' : 'transparent'};
519
+ $active ? 'var(--app-bg, var(--platform-colors-surface-hover))' : 'transparent'};
220
520
  padding: 0 10px;
221
521
  color: ${({ $active }) =>
222
522
  $active
223
- ? 'var(--platform-colors-text, #1c1a17)'
523
+ ? 'var(--app-text, var(--platform-colors-text, #1c1a17))'
224
524
  : 'var(--platform-colors-text-secondary, #8f98a4)'};
225
525
  font: inherit;
226
526
  font-size: 12px;
@@ -230,6 +530,7 @@ const ModeButton = styled.button<{ $active: boolean }>`
230
530
 
231
531
  const Kicker = styled.div`
232
532
  color: var(--platform-colors-text-secondary, #8f897f);
533
+ font-family: var(--dossier-mono);
233
534
  font-size: 11px;
234
535
  font-weight: 800;
235
536
  letter-spacing: 0.08em;
@@ -238,11 +539,12 @@ const Kicker = styled.div`
238
539
  `
239
540
 
240
541
  const Title = styled.h1`
241
- max-width: 22ch;
542
+ max-width: 28ch;
242
543
  margin: 0;
243
544
  color: var(--platform-colors-text, #20242a);
545
+ font-family: var(--dossier-serif);
244
546
  font-size: clamp(34px, 4.2vw, 54px);
245
- font-weight: 800;
547
+ font-weight: 600;
246
548
  letter-spacing: 0;
247
549
  line-height: 1.05;
248
550
  overflow-wrap: anywhere;
@@ -251,15 +553,15 @@ const Title = styled.h1`
251
553
  const TitleInput = styled.textarea`
252
554
  display: block;
253
555
  width: 100%;
254
- max-width: 22ch;
556
+ max-width: 28ch;
255
557
  min-height: 1.1em;
256
558
  margin: 0;
257
559
  border: 0;
258
560
  background: transparent;
259
561
  color: var(--platform-colors-text, #20242a);
260
- font: inherit;
562
+ font-family: var(--dossier-serif);
261
563
  font-size: clamp(34px, 4.2vw, 54px);
262
- font-weight: 800;
564
+ font-weight: 600;
263
565
  letter-spacing: 0;
264
566
  line-height: 1.05;
265
567
  outline: none;
@@ -276,12 +578,22 @@ const TitleInput = styled.textarea`
276
578
  const Question = styled.div`
277
579
  max-width: 72ch;
278
580
  color: var(--platform-colors-text-secondary, #6b665e);
279
- font-size: 14px;
581
+ font-size: 15px;
280
582
  line-height: 1.55;
281
583
  `
282
584
 
585
+ const PromptLabel = styled.span`
586
+ color: var(--platform-colors-text-secondary, #8f897f);
587
+ font-family: var(--dossier-mono);
588
+ font-size: 11px;
589
+ font-weight: 800;
590
+ letter-spacing: 0.06em;
591
+ text-transform: uppercase;
592
+ `
593
+
283
594
  const UpdatedStamp = styled.div`
284
595
  color: var(--platform-colors-text-secondary, #8f897f);
596
+ font-family: var(--dossier-mono);
285
597
  font-size: 11px;
286
598
  font-weight: 750;
287
599
  letter-spacing: 0.05em;
@@ -289,25 +601,80 @@ const UpdatedStamp = styled.div`
289
601
  text-transform: uppercase;
290
602
  `
291
603
 
604
+ const HeaderMeta = styled.div`
605
+ display: flex;
606
+ flex-wrap: wrap;
607
+ gap: 12px;
608
+ align-items: center;
609
+ `
610
+
611
+ const UpdateButton = styled.button`
612
+ border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 14%));
613
+ border-radius: var(--platform-radius-sm);
614
+ background: transparent;
615
+ padding: 6px 9px;
616
+ color: var(--platform-colors-text, #20242a);
617
+ font-family: var(--dossier-sans);
618
+ font-size: 11px;
619
+ font-weight: 850;
620
+ letter-spacing: 0.06em;
621
+ line-height: 1;
622
+ text-transform: uppercase;
623
+ cursor: pointer;
624
+
625
+ &:hover,
626
+ &:focus-visible {
627
+ outline: none;
628
+ border-color: currentColor;
629
+ }
630
+ `
631
+
292
632
  const Narrative = styled.section`
293
633
  display: grid;
294
- gap: 18px;
295
- max-width: 78ch;
296
- padding: 2px 0 8px;
634
+ gap: 20px;
635
+ max-width: min(100%, 1180px);
636
+ border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
637
+ border-bottom: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
638
+ padding: 24px 0 28px;
297
639
  `
298
640
 
299
641
  const Lead = styled.div`
642
+ max-width: 72ch;
300
643
  margin: 0;
301
644
  color: var(--platform-colors-text, #1c1a17);
302
- font-size: clamp(18px, 1.6vw, 22px);
303
- line-height: 1.58;
645
+ font-size: clamp(17px, 1.12vw, 20px);
646
+ line-height: 1.64;
304
647
  `
305
648
 
306
- const NarrativeText = styled.div`
649
+ const ExecutiveList = styled.ul`
650
+ display: grid;
651
+ gap: 15px;
307
652
  margin: 0;
308
- color: var(--platform-colors-text, #3a4047);
309
- font-size: 14.5px;
310
- line-height: 1.72;
653
+ padding: 0;
654
+ list-style: none;
655
+ `
656
+
657
+ const ExecutiveItem = styled.li`
658
+ display: grid;
659
+ grid-template-columns: 26px minmax(0, 1fr);
660
+ gap: 12px;
661
+ max-width: 72ch;
662
+ color: var(--platform-colors-text, #20242a);
663
+ font-size: 16px;
664
+ line-height: 1.62;
665
+ `
666
+
667
+ const ExecutiveText = styled.div`
668
+ margin: 0;
669
+ overflow-wrap: anywhere;
670
+ `
671
+
672
+ const ExecutiveBullet = styled.span`
673
+ color: var(--platform-colors-text-secondary, #8f897f);
674
+ font-family: var(--dossier-mono);
675
+ font-size: 12px;
676
+ font-weight: 850;
677
+ letter-spacing: 0.04em;
311
678
  `
312
679
 
313
680
  const Findings = styled.section`
@@ -318,21 +685,24 @@ const Findings = styled.section`
318
685
  const Finding = styled.article`
319
686
  display: grid;
320
687
  grid-template-columns: 34px minmax(0, 1fr);
321
- gap: 18px;
322
- padding: 18px 0 20px;
688
+ gap: 22px;
689
+ padding: 24px 0 28px;
323
690
  border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
324
691
  `
325
692
 
326
693
  const FindingNumber = styled.div`
694
+ padding-top: 0.38em;
327
695
  color: var(--platform-colors-text-secondary, #8f897f);
696
+ font-family: var(--dossier-mono);
328
697
  font-size: 13px;
329
698
  font-weight: 800;
330
699
  letter-spacing: 0.05em;
700
+ line-height: 1;
331
701
  `
332
702
 
333
703
  const FindingBody = styled.div`
334
704
  display: grid;
335
- gap: 10px;
705
+ gap: 12px;
336
706
  min-width: 0;
337
707
  `
338
708
 
@@ -358,24 +728,72 @@ const FindingTitleGroup = styled.div`
358
728
  `
359
729
 
360
730
  const FindingTitle = styled.h2`
731
+ flex: 1 1 auto;
732
+ min-width: 0;
361
733
  margin: 0;
362
734
  color: var(--platform-colors-text, #20242a);
363
- font-size: clamp(17px, 1.5vw, 21px);
364
- font-weight: 750;
735
+ font-family: var(--dossier-serif);
736
+ font-size: clamp(21px, 1.8vw, 30px);
737
+ font-weight: 600;
365
738
  letter-spacing: 0;
366
- line-height: 1.2;
739
+ line-height: 1.18;
367
740
  overflow-wrap: anywhere;
368
741
  `
369
742
 
743
+ const FindingBadges = styled.div`
744
+ display: flex;
745
+ flex-wrap: wrap;
746
+ gap: 7px;
747
+ align-items: center;
748
+ margin-top: 2px;
749
+ `
750
+
751
+ const FindingBadge = styled.span<{ $tone?: EvidenceStrength | 'fresh' | 'stale' | 'undated' }>`
752
+ display: inline-flex;
753
+ align-items: center;
754
+ min-height: 24px;
755
+ border: 1px solid
756
+ ${({ $tone }) =>
757
+ $tone === 'strong' || $tone === 'fresh'
758
+ ? 'rgb(61 107 74 / 24%)'
759
+ : $tone === 'weak' || $tone === 'stale'
760
+ ? 'rgb(154 105 79 / 26%)'
761
+ : 'var(--platform-colors-border, rgb(46 51 56 / 12%))'};
762
+ border-radius: 999px;
763
+ background: ${({ $tone }) =>
764
+ $tone === 'strong' || $tone === 'fresh'
765
+ ? 'rgb(61 107 74 / 6%)'
766
+ : $tone === 'weak' || $tone === 'stale'
767
+ ? 'rgb(154 105 79 / 7%)'
768
+ : 'rgb(46 51 56 / 3%)'};
769
+ padding: 0 9px;
770
+ color: ${({ $tone }) =>
771
+ $tone === 'strong' || $tone === 'fresh'
772
+ ? '#3d6b4a'
773
+ : $tone === 'weak' || $tone === 'stale'
774
+ ? '#8a5f49'
775
+ : 'var(--platform-colors-text-secondary, #6b665e)'};
776
+ font-size: 11px;
777
+ font-family: var(--dossier-mono);
778
+ font-weight: 800;
779
+ letter-spacing: 0.04em;
780
+ line-height: 1;
781
+ text-transform: uppercase;
782
+ `
783
+
370
784
  const DeleteAction = styled.button`
785
+ flex: 0 0 auto;
371
786
  padding: 0;
372
787
  border: 0;
373
788
  background: transparent;
374
789
  color: var(--platform-colors-text-secondary, #8f897f);
375
- font: inherit;
790
+ font-family: var(--dossier-sans);
376
791
  font-size: 11px;
377
792
  font-weight: 800;
378
793
  letter-spacing: 0.05em;
794
+ line-height: 1;
795
+ white-space: nowrap;
796
+ word-break: keep-all;
379
797
  text-transform: uppercase;
380
798
  opacity: 0;
381
799
  cursor: pointer;
@@ -394,6 +812,7 @@ const DeleteAction = styled.button`
394
812
 
395
813
  const Confidence = styled.span<{ $strength: EvidenceStrength }>`
396
814
  flex: 0 0 auto;
815
+ min-width: 0;
397
816
  color: ${({ $strength }) =>
398
817
  $strength === 'strong'
399
818
  ? '#3d6b4a'
@@ -401,38 +820,228 @@ const Confidence = styled.span<{ $strength: EvidenceStrength }>`
401
820
  ? '#9a694f'
402
821
  : 'var(--platform-colors-text-secondary, #6b665e)'};
403
822
  font-size: 11px;
823
+ font-family: var(--dossier-mono);
404
824
  font-weight: 800;
405
825
  letter-spacing: 0.05em;
406
826
  text-transform: uppercase;
827
+ overflow-wrap: anywhere;
407
828
  `
408
829
 
409
- const Claim = styled.p`
410
- max-width: 76ch;
830
+ const Claim = styled.div`
831
+ max-width: min(100%, 72ch);
411
832
  margin: 0;
412
833
  color: var(--platform-colors-text, #1c1a17);
413
- font-size: 14.5px;
414
- line-height: 1.72;
834
+ font-size: 16px;
835
+ line-height: 1.64;
415
836
  `
416
837
 
417
838
  const ClaimLine = styled.div`
418
- display: inline;
839
+ display: block;
840
+ `
841
+
842
+ const SourceSupportLine = styled.div`
843
+ display: flex;
844
+ flex-wrap: wrap;
845
+ gap: 6px 7px;
846
+ align-items: center;
847
+ margin-top: 10px;
848
+ max-width: 100%;
849
+ color: var(--platform-colors-text-secondary, #6b665e);
850
+ font-size: 13px;
851
+ line-height: 1.35;
852
+ `
853
+
854
+ const SourceSupportLabel = styled.span`
855
+ color: var(--platform-colors-text-secondary, #8f98a4);
856
+ font-family: var(--dossier-mono);
857
+ font-size: 10px;
858
+ font-weight: 800;
859
+ letter-spacing: 0.05em;
860
+ text-transform: uppercase;
861
+ `
862
+
863
+ const SourceSupportLink = styled.a`
864
+ display: inline-block;
865
+ max-width: min(30ch, 42vw);
866
+ overflow: hidden;
867
+ color: inherit;
868
+ font-family: var(--dossier-serif);
869
+ text-decoration: underline;
870
+ text-decoration-color: var(--platform-colors-border-strong, rgb(46 51 56 / 24%));
871
+ text-decoration-thickness: 1px;
872
+ text-underline-offset: 3px;
873
+ text-overflow: ellipsis;
874
+ white-space: nowrap;
875
+
876
+ &:hover,
877
+ &:focus-visible {
878
+ color: var(--platform-colors-text, #1c1a17);
879
+ outline: none;
880
+ text-decoration-color: currentColor;
881
+ }
882
+ `
883
+
884
+ const SourceSupportChip = styled.span`
885
+ display: inline-flex;
886
+ align-items: center;
887
+ max-width: min(32ch, 44vw);
888
+ border: 0;
889
+ background: transparent;
890
+ padding: 0;
891
+ overflow: hidden;
892
+ color: var(--platform-colors-text-secondary, #5f6670);
893
+ `
894
+
895
+ const ClaimContent = styled.div`
896
+ display: grid;
897
+ gap: 12px;
898
+ max-width: 100%;
899
+ min-width: 0;
900
+ `
901
+
902
+ const ClaimParagraph = styled.p`
903
+ margin: 0;
904
+
905
+ & + & {
906
+ margin-top: 0.72em;
907
+ }
908
+ `
909
+
910
+ const TableScroll = styled.div`
911
+ max-width: 100%;
912
+ overflow-x: auto;
913
+ `
914
+
915
+ const ClaimList = styled.ul`
916
+ margin: 0;
917
+ padding-left: 20px;
918
+ display: grid;
919
+ gap: 6px;
920
+
921
+ li {
922
+ margin: 0;
923
+ }
924
+ `
925
+
926
+ const ClaimHeading = styled.h4`
927
+ margin: 10px 0 0;
928
+ font-family: var(--dossier-sans);
929
+ font-size: 14px;
930
+ font-weight: 750;
931
+ letter-spacing: -0.01em;
932
+ color: var(--platform-colors-text, #1c1a17);
933
+ `
934
+
935
+ /** Mirrors the editor's comment-mark styling: accent underline, hover tint,
936
+ * with the explanation surfaced as a floating note. The note is a real
937
+ * element (not a CSS tooltip) so an authoritative explainer link stays
938
+ * clickable; it opens on hover and stays open while hovered or focused. */
939
+ const TermNoteMark = styled.span`
940
+ position: relative;
941
+ border-bottom: 1px solid
942
+ rgb(from var(--platform-colors-accent, #0067b8) r g b / 46%);
943
+ border-radius: 2px;
944
+ box-decoration-break: clone;
945
+ cursor: help;
946
+ padding: 0 1px;
947
+
948
+ &:hover,
949
+ &:focus-within {
950
+ background: rgb(from var(--platform-colors-accent, #0067b8) r g b / 12%);
951
+ outline: none;
952
+ }
953
+ `
954
+
955
+ const TermNotePopover = styled.span`
956
+ display: none;
957
+ position: absolute;
958
+ bottom: 100%;
959
+ left: 0;
960
+ z-index: 12;
961
+ /* Transparent bridge keeps hover alive between term and note. */
962
+ padding-bottom: 6px;
963
+ width: max-content;
964
+ max-width: min(340px, 72vw);
965
+
966
+ ${TermNoteMark}:hover &,
967
+ ${TermNoteMark}:focus-within & {
968
+ display: block;
969
+ }
970
+ `
971
+
972
+ const TermNoteBubble = styled.span`
973
+ display: block;
974
+ padding: 8px 10px;
975
+ border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 16%));
976
+ border-radius: var(--platform-radius-sm, 4px);
977
+ background: var(--platform-colors-surface, #fff);
978
+ box-shadow: 0 6px 20px rgb(0 0 0 / 14%);
979
+ color: var(--platform-colors-text, #1c1a17);
980
+ font-family: var(--dossier-sans);
981
+ font-size: 12.5px;
982
+ font-weight: 400;
983
+ line-height: 1.45;
984
+ white-space: normal;
985
+ `
986
+
987
+ const TermNoteLink = styled.a`
988
+ display: inline-block;
989
+ margin-top: 4px;
990
+ color: var(--platform-colors-accent, #0067b8);
991
+ font-family: var(--dossier-mono);
992
+ font-size: 11px;
993
+ letter-spacing: 0.02em;
994
+ text-decoration: none;
995
+
996
+ &:hover,
997
+ &:focus-visible {
998
+ text-decoration: underline;
999
+ }
1000
+ `
1001
+
1002
+ const ClaimTable = styled.table`
1003
+ width: 100%;
1004
+ min-width: 560px;
1005
+ border-collapse: collapse;
1006
+ color: var(--platform-colors-text, #1c1a17);
1007
+ font-family: var(--dossier-sans);
1008
+ font-size: 12.5px;
1009
+ line-height: 1.45;
1010
+
1011
+ th,
1012
+ td {
1013
+ border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 12%));
1014
+ padding: 7px 8px;
1015
+ text-align: left;
1016
+ vertical-align: top;
1017
+ }
1018
+
1019
+ th {
1020
+ background: var(--platform-colors-surface-hover, rgb(46 51 56 / 5%));
1021
+ font-family: var(--dossier-mono);
1022
+ font-size: 11px;
1023
+ font-weight: 800;
1024
+ letter-spacing: 0.04em;
1025
+ text-transform: uppercase;
1026
+ }
419
1027
  `
420
1028
 
421
1029
  const Annotation = styled.aside`
422
- max-width: 72ch;
1030
+ max-width: min(100%, 88ch);
423
1031
  border-left: 3px solid rgb(82 90 101 / 36%);
424
1032
  padding: 4px 0 4px 14px;
425
1033
  color: var(--platform-colors-text-secondary, #6b665e);
426
- font-size: 13px;
427
- line-height: 1.55;
1034
+ font-family: var(--dossier-serif);
1035
+ font-size: 14px;
1036
+ line-height: 1.6;
428
1037
  `
429
1038
 
430
1039
  const EvidenceHandle = styled.button<{ $strength: EvidenceStrength }>`
431
1040
  display: inline;
432
1041
  align-items: center;
433
1042
  min-height: 0;
434
- margin-left: 6px;
435
- transform: translateY(-1px);
1043
+ margin-left: 0;
1044
+ transform: none;
436
1045
  border: 0;
437
1046
  border-bottom: 1px solid
438
1047
  ${({ $strength }) =>
@@ -450,7 +1059,7 @@ const EvidenceHandle = styled.button<{ $strength: EvidenceStrength }>`
450
1059
  : $strength === 'weak'
451
1060
  ? '#9a694f'
452
1061
  : 'var(--platform-colors-text-secondary, #6b665e)'};
453
- font: inherit;
1062
+ font-family: var(--dossier-mono);
454
1063
  font-size: 0.78em;
455
1064
  font-weight: 750;
456
1065
  letter-spacing: 0.02em;
@@ -467,22 +1076,33 @@ const EvidenceHandle = styled.button<{ $strength: EvidenceStrength }>`
467
1076
  const EvidenceDrawer = styled.aside`
468
1077
  display: grid;
469
1078
  gap: 10px;
470
- max-width: 64ch;
471
- margin-top: 4px;
1079
+ max-width: min(100%, 1180px);
1080
+ margin-top: 10px;
472
1081
  border-left: 2px solid var(--platform-colors-border, rgb(46 51 56 / 18%));
473
1082
  padding: 2px 0 2px 14px;
474
1083
  `
475
1084
 
476
1085
  const EvidenceGrid = styled.div`
477
- display: grid;
478
- grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
479
- gap: 10px;
1086
+ column-count: 3;
1087
+ column-gap: 10px;
1088
+ min-width: 0;
1089
+ max-width: 100%;
480
1090
  margin-top: 2px;
1091
+
1092
+ @media (max-width: 980px) {
1093
+ column-count: 2;
1094
+ }
1095
+
1096
+ @media (max-width: 640px) {
1097
+ column-count: 1;
1098
+ }
481
1099
  `
482
1100
 
483
1101
  const AuditList = styled.section`
484
1102
  display: grid;
485
1103
  gap: 14px;
1104
+ min-width: 0;
1105
+ max-width: 100%;
486
1106
  border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
487
1107
  padding-top: 18px;
488
1108
  `
@@ -490,53 +1110,85 @@ const AuditList = styled.section`
490
1110
  const AuditIntro = styled.div`
491
1111
  max-width: 70ch;
492
1112
  color: var(--platform-colors-text-secondary, #6b665e);
493
- font-size: 13px;
1113
+ font-size: 15px;
494
1114
  line-height: 1.55;
495
1115
  `
496
1116
 
1117
+ const MarkdownPane = styled.pre`
1118
+ max-width: min(100%, 1180px);
1119
+ margin: 0;
1120
+ overflow: auto;
1121
+ border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 12%));
1122
+ border-radius: var(--platform-radius-md, 0px);
1123
+ background: var(--platform-colors-surface, rgb(255 255 255 / 58%));
1124
+ padding: 18px;
1125
+ color: var(--platform-colors-text, #1c1a17);
1126
+ font-family:
1127
+ var(--platform-typography-font-family-mono, ui-monospace, SFMono-Regular),
1128
+ monospace;
1129
+ font-size: 12.5px;
1130
+ line-height: 1.55;
1131
+ white-space: pre-wrap;
1132
+ word-break: break-word;
1133
+ `
1134
+
497
1135
  const AuditFinding = styled.div`
498
1136
  display: grid;
499
1137
  gap: 9px;
1138
+ min-width: 0;
1139
+ max-width: 100%;
500
1140
  `
501
1141
 
502
1142
  const EvidenceCard = styled.div`
503
1143
  display: grid;
1144
+ break-inside: avoid;
504
1145
  gap: 8px;
505
1146
  min-width: 0;
1147
+ max-width: 100%;
1148
+ margin: 0 0 10px;
1149
+ overflow: hidden;
506
1150
  border: 1px solid var(--platform-colors-border, rgb(46 51 56 / 10%));
507
1151
  border-radius: var(--platform-radius-md, 0px);
508
1152
  background: var(--platform-colors-surface, rgb(255 255 255 / 56%));
509
1153
  padding: 12px 13px;
1154
+ font-family: var(--dossier-sans);
510
1155
  `
511
1156
 
512
1157
  const EvidenceTop = styled.div`
513
1158
  display: flex;
1159
+ flex-wrap: wrap;
514
1160
  align-items: baseline;
515
1161
  justify-content: space-between;
516
1162
  gap: 10px;
1163
+ min-width: 0;
517
1164
  `
518
1165
 
519
1166
  const EvidenceLabel = styled.span`
1167
+ min-width: 0;
520
1168
  color: var(--platform-colors-text-secondary, #8f98a4);
1169
+ font-family: var(--dossier-mono);
521
1170
  font-size: 10px;
522
1171
  font-weight: 850;
523
1172
  letter-spacing: 0.08em;
524
1173
  text-transform: uppercase;
1174
+ overflow-wrap: anywhere;
525
1175
  `
526
1176
 
527
1177
  const EvidenceTitle = styled.a`
528
1178
  min-width: 0;
529
- overflow: hidden;
530
1179
  color: var(--platform-colors-text, #1c1a17);
1180
+ font-family: var(--dossier-serif);
531
1181
  font-size: 14px;
532
- font-weight: 750;
1182
+ font-weight: 600;
533
1183
  line-height: 1.3;
534
- text-decoration: none;
535
- text-overflow: ellipsis;
536
- white-space: nowrap;
1184
+ text-decoration: underline;
1185
+ text-decoration-color: var(--platform-colors-border-strong, rgb(46 51 56 / 24%));
1186
+ text-decoration-thickness: 1px;
1187
+ text-underline-offset: 3px;
1188
+ overflow-wrap: anywhere;
1189
+ word-break: break-word;
537
1190
 
538
1191
  &:hover {
539
- text-decoration: underline;
540
1192
  text-decoration-color: var(
541
1193
  --platform-colors-border-strong,
542
1194
  rgb(46 51 56 / 28%)
@@ -546,15 +1198,40 @@ const EvidenceTitle = styled.a`
546
1198
 
547
1199
  const EvidenceNote = styled.div`
548
1200
  color: var(--platform-colors-text-secondary, #6b665e);
1201
+ font-family: var(--dossier-serif);
549
1202
  font-size: 13px;
550
1203
  line-height: 1.45;
551
1204
  `
552
1205
 
1206
+ const EvidenceMiniTable = styled.dl`
1207
+ display: grid;
1208
+ grid-template-columns: minmax(64px, auto) minmax(0, 1fr);
1209
+ gap: 4px 8px;
1210
+ margin: 0;
1211
+ color: var(--platform-colors-text-secondary, #6b665e);
1212
+ font-family: var(--dossier-sans);
1213
+ font-size: 12px;
1214
+ line-height: 1.35;
1215
+
1216
+ dt {
1217
+ color: var(--platform-colors-text, #20242a);
1218
+ font-family: var(--dossier-mono);
1219
+ font-weight: 750;
1220
+ }
1221
+
1222
+ dd {
1223
+ margin: 0;
1224
+ min-width: 0;
1225
+ overflow-wrap: anywhere;
1226
+ }
1227
+ `
1228
+
553
1229
  const Quote = styled.blockquote`
554
1230
  margin: 0;
555
1231
  border-left: 2px solid var(--platform-colors-border, rgb(46 51 56 / 16%));
556
1232
  padding-left: 10px;
557
1233
  color: var(--platform-colors-text, #3a4047);
1234
+ font-family: var(--dossier-serif);
558
1235
  font-size: 12.5px;
559
1236
  line-height: 1.45;
560
1237
  `
@@ -584,7 +1261,7 @@ const QuietListItems = styled.div`
584
1261
 
585
1262
  const QuietListItem = styled.div`
586
1263
  color: var(--platform-colors-text-secondary, #6b665e);
587
- font-size: 14px;
1264
+ font-size: 15px;
588
1265
  line-height: 1.55;
589
1266
  `
590
1267
 
@@ -592,6 +1269,7 @@ const Footer = styled.footer`
592
1269
  border-top: 1px solid var(--platform-colors-divider, rgb(46 51 56 / 10%));
593
1270
  padding-top: 14px;
594
1271
  color: var(--platform-colors-text-secondary, #8f98a4);
1272
+ font-family: var(--dossier-sans);
595
1273
  font-size: 12px;
596
1274
  line-height: 1.5;
597
1275
  `
@@ -618,9 +1296,10 @@ function SourceTitle({
618
1296
  function evidenceHandleLabel(finding: EvidenceFinding): string {
619
1297
  const count = finding?.evidence?.length
620
1298
  const sourceLabel = count === 1 ? 'source' : 'sources'
621
- if (finding.confidence === 'strong') return `checked · ${count}`
622
- if (finding.confidence === 'weak') return `weak · ${count}`
623
- return `${count} ${sourceLabel}`
1299
+ if (finding.confidence === 'weak') {
1300
+ return `Review sources · needs support · ${count} ${sourceLabel}`
1301
+ }
1302
+ return `Review sources · ${count} ${sourceLabel}`
624
1303
  }
625
1304
 
626
1305
  function evidenceHandleTitle(finding: EvidenceFinding): string {
@@ -629,15 +1308,107 @@ function evidenceHandleTitle(finding: EvidenceFinding): string {
629
1308
  return `Show evidence: ${cleanDisplayText(strongest.title)}`
630
1309
  }
631
1310
 
1311
+ function topEvidenceSources(finding: EvidenceFinding): EvidenceSource[] {
1312
+ return finding.evidence.slice(0, 3)
1313
+ }
1314
+
1315
+ function markdownEscape(value: string): string {
1316
+ return cleanDisplayText(value).replace(/\[/g, '\\[').replace(/\]/g, '\\]')
1317
+ }
1318
+
1319
+ function markdownLink(label: string, url?: string): string {
1320
+ const cleanedLabel = markdownEscape(label || url || 'Source')
1321
+ if (!url) return cleanedLabel
1322
+ return `[${cleanedLabel}](${url.replace(/\)/g, '%29')})`
1323
+ }
1324
+
1325
+ function markdownFreshnessLabel(
1326
+ freshness?: EvidenceSource['freshness'],
1327
+ ): string {
1328
+ if (freshness === 'fresh') return 'fresh evidence'
1329
+ if (freshness === 'stale') return 'older evidence'
1330
+ return 'undated evidence'
1331
+ }
1332
+
1333
+ function findingMarkdown(finding: EvidenceFinding, index: number): string {
1334
+ const sources = topEvidenceSources(finding)
1335
+ const lines = [
1336
+ `## ${index + 1}. ${cleanDisplayText(finding.title)}`,
1337
+ '',
1338
+ cleanRichText(finding.claim),
1339
+ '',
1340
+ `Evidence: ${strengthLabels[finding.confidence]}; ${
1341
+ finding.evidence.length
1342
+ } source${finding.evidence.length === 1 ? '' : 's'}.`,
1343
+ ]
1344
+ if (finding.annotation) {
1345
+ lines.push('', `Note: ${cleanDisplayText(finding.annotation)}`)
1346
+ }
1347
+ if (sources.length) {
1348
+ lines.push('', 'Top supporting sources:')
1349
+ sources.forEach(source => {
1350
+ const date = source.sourceDate ? `, ${source.sourceDate}` : ''
1351
+ const freshness = source.freshness
1352
+ ? `, ${markdownFreshnessLabel(source.freshness)}`
1353
+ : ''
1354
+ const note = sourceNoteSummary(source.note)
1355
+ lines.push(
1356
+ `- ${markdownLink(source.title || source.url || 'Source', source.url)}${date}${freshness}${note ? ` — ${note}` : ''}`,
1357
+ )
1358
+ })
1359
+ }
1360
+ return lines.join('\n')
1361
+ }
1362
+
1363
+ export function dossierMarkdown(input: {
1364
+ title: string
1365
+ question: string
1366
+ updatedLabel: string | null
1367
+ leadText: string
1368
+ findings: EvidenceFinding[]
1369
+ }): string {
1370
+ const lines = [
1371
+ `# ${cleanDisplayText(input.title) || 'Untitled dossier'}`,
1372
+ '',
1373
+ `Original prompt: ${displayPrompt(input.question) || 'Not provided.'}`,
1374
+ ]
1375
+ if (input.updatedLabel) lines.push(`Updated: ${input.updatedLabel}`)
1376
+ lines.push(
1377
+ '',
1378
+ '## Executive summary',
1379
+ '',
1380
+ cleanRichText(input.leadText),
1381
+ '',
1382
+ '## Findings',
1383
+ '',
1384
+ ...input.findings.map(findingMarkdown),
1385
+ )
1386
+ return `${lines.join('\n').replace(/\n{3,}/g, '\n\n').trim()}\n`
1387
+ }
1388
+
1389
+ function findingFreshnessBadge(finding: EvidenceFinding): {
1390
+ label: string
1391
+ tone: 'fresh' | 'stale' | 'undated'
1392
+ } {
1393
+ if (finding.freshnessMode === 'open') {
1394
+ return { label: 'sources checked', tone: 'undated' }
1395
+ }
1396
+ if (finding.evidence.some(source => source.freshness === 'fresh')) {
1397
+ return { label: 'fresh sources', tone: 'fresh' }
1398
+ }
1399
+ if (finding.evidence.some(source => source.freshness === 'stale')) {
1400
+ return { label: 'older sources', tone: 'stale' }
1401
+ }
1402
+ return { label: 'check dates', tone: 'undated' }
1403
+ }
1404
+
632
1405
  function EvidenceCitation({
633
1406
  finding,
634
1407
  open,
635
- onOpen,
636
1408
  onToggle,
637
1409
  }: {
638
1410
  finding: EvidenceFinding
639
1411
  open: boolean
640
- onOpen: () => void
641
1412
  onToggle: () => void
642
1413
  }): React.ReactElement | null {
643
1414
  if (!finding?.evidence?.length) return null
@@ -645,17 +1416,56 @@ function EvidenceCitation({
645
1416
  <EvidenceHandle
646
1417
  type="button"
647
1418
  $strength={finding.confidence}
648
- onMouseEnter={onOpen}
649
- onFocus={onOpen}
650
1419
  onClick={onToggle}
651
1420
  aria-expanded={open}
652
1421
  title={evidenceHandleTitle(finding)}
653
1422
  >
654
- {evidenceHandleLabel(finding)}
1423
+ {open ? 'Hide sources' : evidenceHandleLabel(finding)}
655
1424
  </EvidenceHandle>
656
1425
  )
657
1426
  }
658
1427
 
1428
+ function SourceSupport({
1429
+ finding,
1430
+ open,
1431
+ onToggle,
1432
+ }: {
1433
+ finding: EvidenceFinding
1434
+ open: boolean
1435
+ onToggle: () => void
1436
+ }): React.ReactElement | null {
1437
+ const sources = topEvidenceSources(finding)
1438
+ if (!finding.evidence.length) return null
1439
+ return (
1440
+ <SourceSupportLine>
1441
+ <EvidenceCitation
1442
+ finding={finding}
1443
+ open={open}
1444
+ onToggle={onToggle}
1445
+ />
1446
+ {sources.length ? <SourceSupportLabel>Top sources</SourceSupportLabel> : null}
1447
+ {sources.map((source, index) => {
1448
+ const label =
1449
+ cleanDisplayText(source.title) ||
1450
+ cleanDisplayText(source.url) ||
1451
+ `Source ${index + 1}`
1452
+ const href = sourceUrlWithTextFragment(source)
1453
+ return (
1454
+ <SourceSupportChip key={source.id}>
1455
+ {href ? (
1456
+ <SourceSupportLink href={href} target="_blank" rel="noreferrer">
1457
+ {label}
1458
+ </SourceSupportLink>
1459
+ ) : (
1460
+ label
1461
+ )}
1462
+ </SourceSupportChip>
1463
+ )
1464
+ })}
1465
+ </SourceSupportLine>
1466
+ )
1467
+ }
1468
+
659
1469
  function EvidenceCards({
660
1470
  sources,
661
1471
  }: {
@@ -667,6 +1477,7 @@ function EvidenceCards({
667
1477
  const showQuote = Boolean(
668
1478
  source.quote && source.label !== 'Web evidence',
669
1479
  )
1480
+ const rows = pipeSummaryRows(source.note)
670
1481
  return (
671
1482
  <EvidenceCard key={source.id}>
672
1483
  <EvidenceTop>
@@ -676,7 +1487,19 @@ function EvidenceCards({
676
1487
  </Confidence>
677
1488
  </EvidenceTop>
678
1489
  <SourceTitle source={source} />
679
- <EvidenceNote>{cleanDisplayText(source.note)}</EvidenceNote>
1490
+ <EvidenceNote>
1491
+ <LinkifiedText text={sourceNoteSummary(source.note)} />
1492
+ </EvidenceNote>
1493
+ {rows.length ? (
1494
+ <EvidenceMiniTable>
1495
+ {rows.map(([label, value], index) => (
1496
+ <Fragment key={`${label}-${index}`}>
1497
+ <dt>{label}</dt>
1498
+ <dd>{value}</dd>
1499
+ </Fragment>
1500
+ ))}
1501
+ </EvidenceMiniTable>
1502
+ ) : null}
680
1503
  {showQuote ? (
681
1504
  <Quote>{cleanDisplayText(source.quote ?? '')}</Quote>
682
1505
  ) : null}
@@ -687,17 +1510,239 @@ function EvidenceCards({
687
1510
  )
688
1511
  }
689
1512
 
1513
+ /**
1514
+ * Repair markdown tables whose newlines were collapsed upstream (older
1515
+ * sanitizers flattened `\n` to spaces, leaving pipe rows glued into prose).
1516
+ * Only runs when a `|---|` separator run is present; row boundaries in
1517
+ * flattened text appear as `| |` (closing pipe, space, opening pipe).
1518
+ */
1519
+ function inflateFlattenedTables(value: string): string {
1520
+ if (!/\|[ \t]*:?-{3,}/.test(value)) return value
1521
+ return value.replace(/\|[ \t]+\|/g, '|\n|')
1522
+ }
1523
+
1524
+ /**
1525
+ * Heading markers that survive mid-line come from legacy flattened text where
1526
+ * the heading's line boundary is unrecoverable — drop the marker and its
1527
+ * section number ("### 2. " leaves a dangling "2." otherwise), keep the
1528
+ * words. Line-start headings are left intact for ClaimText to render.
1529
+ */
1530
+ function stripInlineHeadingMarkers(value: string): string {
1531
+ return value.replace(
1532
+ /([^\n#])[ \t]+#{1,6}[ \t]+(?:\d{1,2}[.)][ \t]+)?/g,
1533
+ '$1 ',
1534
+ )
1535
+ }
1536
+
1537
+ function escapeRegExp(value: string): string {
1538
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
1539
+ }
1540
+
1541
+ /** Wrap glossary-term occurrences in an annotated mark with the explanation. */
1542
+ function annotateTerms(
1543
+ text: string,
1544
+ glossary: EvidenceTermNote[] | undefined,
1545
+ ): ReactNode {
1546
+ const terms = (glossary ?? []).filter(
1547
+ item => item.term.trim() && item.explanation.trim(),
1548
+ )
1549
+ if (!terms.length) return text
1550
+ const byTerm = new Map(terms.map(item => [item.term.toLowerCase(), item]))
1551
+ const pattern = new RegExp(
1552
+ `(${[...terms]
1553
+ .sort((left, right) => right.term.length - left.term.length)
1554
+ .map(item => escapeRegExp(item.term))
1555
+ .join('|')})`,
1556
+ 'gi',
1557
+ )
1558
+ const parts = text.split(pattern)
1559
+ if (parts.length === 1) return text
1560
+ return parts.map((part, partIndex) => {
1561
+ const note = byTerm.get(part.toLowerCase())
1562
+ if (!note) return part
1563
+ return (
1564
+ <TermNoteMark
1565
+ key={`term-${partIndex}`}
1566
+ tabIndex={0}
1567
+ data-explanation={note.explanation}
1568
+ aria-label={`${part}: ${note.explanation}`}
1569
+ >
1570
+ {part}
1571
+ <TermNotePopover role="note">
1572
+ <TermNoteBubble>
1573
+ {note.expansion ? <strong>{note.expansion} — </strong> : null}
1574
+ {note.explanation}
1575
+ {note.url ? (
1576
+ <TermNoteLink
1577
+ href={note.url}
1578
+ target="_blank"
1579
+ rel="noreferrer"
1580
+ aria-label={`Authoritative explanation of ${part}`}
1581
+ >
1582
+ {explainerLinkLabel(note.url)} ↗
1583
+ </TermNoteLink>
1584
+ ) : null}
1585
+ </TermNoteBubble>
1586
+ </TermNotePopover>
1587
+ </TermNoteMark>
1588
+ )
1589
+ })
1590
+ }
1591
+
1592
+ function explainerLinkLabel(url: string): string {
1593
+ try {
1594
+ return new URL(url).hostname.replace(/^www\./, '')
1595
+ } catch {
1596
+ return 'Learn more'
1597
+ }
1598
+ }
1599
+
1600
+ function ClaimText({
1601
+ value,
1602
+ glossary,
1603
+ }: {
1604
+ value: string
1605
+ glossary?: EvidenceTermNote[]
1606
+ }): React.ReactElement {
1607
+ const lines = stripInlineHeadingMarkers(
1608
+ inflateFlattenedTables(cleanRichText(value)),
1609
+ )
1610
+ .split('\n')
1611
+ .map(line => line.trim())
1612
+ const nodes: ReactNode[] = []
1613
+ let index = 0
1614
+ let paragraph: string[] = []
1615
+ const flushParagraph = (): void => {
1616
+ readableParagraphs(paragraph.join(' ')).forEach(text => {
1617
+ nodes.push(
1618
+ <ClaimParagraph key={`p-${nodes.length}`}>
1619
+ {annotateTerms(text, glossary)}
1620
+ </ClaimParagraph>,
1621
+ )
1622
+ })
1623
+ paragraph = []
1624
+ }
1625
+
1626
+ while (index < lines.length) {
1627
+ // Detach prose glued to a table header ("…workflows: | Harness | …")
1628
+ // so the header row is recognized when the next line is a separator.
1629
+ const glued = lines[index]
1630
+ const nextLine = lines[index + 1]
1631
+ if (
1632
+ glued &&
1633
+ !glued.startsWith('|') &&
1634
+ glued.includes('|') &&
1635
+ glued.endsWith('|') &&
1636
+ nextLine &&
1637
+ isTableSeparator(nextLine)
1638
+ ) {
1639
+ const pipeAt = glued.indexOf('|')
1640
+ const prose = glued.slice(0, pipeAt).trim()
1641
+ if (prose) paragraph.push(prose)
1642
+ lines[index] = glued.slice(pipeAt).trim()
1643
+ }
1644
+ const header = lines[index]
1645
+ const separator = lines[index + 1]
1646
+ if (header?.startsWith('|') && separator && isTableSeparator(separator)) {
1647
+ const headers = tableCells(header)
1648
+ const rows: string[][] = []
1649
+ index += 2
1650
+ while (index < lines.length && lines[index]?.startsWith('|')) {
1651
+ let rowLine = lines[index]
1652
+ // A final row may have trailing prose glued after its closing pipe
1653
+ // (flattened upstream); keep the row and requeue the prose.
1654
+ if (!rowLine.endsWith('|')) {
1655
+ const lastPipe = rowLine.lastIndexOf('|')
1656
+ if (lastPipe <= 0) break
1657
+ const trailing = rowLine.slice(lastPipe + 1).trim()
1658
+ rowLine = rowLine.slice(0, lastPipe + 1)
1659
+ if (trailing) lines.splice(index + 1, 0, trailing)
1660
+ }
1661
+ const cells = tableCells(rowLine)
1662
+ if (!cells.length) break
1663
+ rows.push(cells)
1664
+ index += 1
1665
+ }
1666
+ flushParagraph()
1667
+ nodes.push(
1668
+ <TableScroll key={`table-${nodes.length}`}>
1669
+ <ClaimTable>
1670
+ <thead>
1671
+ <tr>
1672
+ {headers.map((cell, cellIndex) => (
1673
+ <th key={`${cell}-${cellIndex}`}>{cell}</th>
1674
+ ))}
1675
+ </tr>
1676
+ </thead>
1677
+ <tbody>
1678
+ {rows.map((row, rowIndex) => (
1679
+ <tr key={`row-${rowIndex}`}>
1680
+ {headers.map((_, cellIndex) => (
1681
+ <td key={`cell-${rowIndex}-${cellIndex}`}>
1682
+ {row[cellIndex] ?? ''}
1683
+ </td>
1684
+ ))}
1685
+ </tr>
1686
+ ))}
1687
+ </tbody>
1688
+ </ClaimTable>
1689
+ </TableScroll>,
1690
+ )
1691
+ continue
1692
+ }
1693
+ const headingMatch = header?.match(/^#{1,6}\s+(.+?)\s*#*$/)
1694
+ if (headingMatch && headingMatch[1].length <= 120) {
1695
+ flushParagraph()
1696
+ nodes.push(
1697
+ <ClaimHeading key={`heading-${nodes.length}`}>
1698
+ {headingMatch[1]}
1699
+ </ClaimHeading>,
1700
+ )
1701
+ index += 1
1702
+ continue
1703
+ }
1704
+ if (headingMatch) {
1705
+ // Over-long "heading" is legacy flattened text with prose glued after
1706
+ // the title — drop the marker and enumeration, reprocess as paragraph.
1707
+ lines[index] = headingMatch[1].replace(/^\d{1,2}[.)]\s+/, '')
1708
+ continue
1709
+ }
1710
+ if (header && /^[*•-]\s+/.test(header)) {
1711
+ flushParagraph()
1712
+ const items: string[] = []
1713
+ while (index < lines.length && /^[*•-]\s+/.test(lines[index])) {
1714
+ items.push(lines[index].replace(/^[*•-]\s+/, ''))
1715
+ index += 1
1716
+ }
1717
+ nodes.push(
1718
+ <ClaimList key={`list-${nodes.length}`}>
1719
+ {items.map((item, itemIndex) => (
1720
+ <li key={`item-${itemIndex}`}>{annotateTerms(item, glossary)}</li>
1721
+ ))}
1722
+ </ClaimList>,
1723
+ )
1724
+ continue
1725
+ }
1726
+ if (header) paragraph.push(header)
1727
+ else flushParagraph()
1728
+ index += 1
1729
+ }
1730
+ flushParagraph()
1731
+
1732
+ return <ClaimContent>{nodes}</ClaimContent>
1733
+ }
1734
+
690
1735
  export function EvidenceDossier({
691
1736
  title,
692
1737
  question,
693
1738
  answer,
694
1739
  updatedAt,
695
- narrative,
696
1740
  findings,
697
1741
  uncertainties,
698
1742
  nextSteps,
699
1743
  onTitleChange,
700
1744
  onDeleteFinding,
1745
+ onUpdate,
701
1746
  footer,
702
1747
  className,
703
1748
  }: EvidenceDossierProps): React.ReactElement {
@@ -707,7 +1752,6 @@ export function EvidenceDossier({
707
1752
  const safeFindings = Array.isArray(findings)
708
1753
  ? findings.map(normalizeEvidenceFinding).filter(isEvidenceFinding)
709
1754
  : []
710
- const safeNarrative = Array.isArray(narrative) ? narrative : []
711
1755
  const safeUncertainties = Array.isArray(uncertainties) ? uncertainties : []
712
1756
  const safeNextSteps = Array.isArray(nextSteps) ? nextSteps : []
713
1757
  const activeEvidence =
@@ -718,14 +1762,23 @@ export function EvidenceDossier({
718
1762
  setOpenEvidenceId(current => (current === findingId ? null : findingId))
719
1763
  }
720
1764
  const leadEvidence = safeFindings[0]
721
- const narrativeEvidence = (index: number): EvidenceFinding | undefined =>
722
- safeFindings[index + 1] ?? safeFindings[index] ?? safeFindings[0]
1765
+ const leadText =
1766
+ leadEvidence?.claim ||
1767
+ cleanDisplayText(answer) ||
1768
+ 'No executive summary available yet.'
1769
+ const markdownText = dossierMarkdown({
1770
+ title,
1771
+ question,
1772
+ updatedLabel,
1773
+ leadText,
1774
+ findings: safeFindings,
1775
+ })
723
1776
 
724
1777
  return (
725
1778
  <Dossier className={className}>
726
1779
  <ModeDock>
727
1780
  <ModeSwitch aria-label="Dossier reading density">
728
- {(['read', 'review', 'audit'] as DossierRenderMode[]).map(item => (
1781
+ {(['read', 'review', 'audit', 'markdown'] as DossierRenderMode[]).map(item => (
729
1782
  <ModeButton
730
1783
  key={item}
731
1784
  type="button"
@@ -754,60 +1807,76 @@ export function EvidenceDossier({
754
1807
  ) : (
755
1808
  <Title>{cleanDisplayText(title)}</Title>
756
1809
  )}
757
- <Question>{cleanDisplayText(question)}</Question>
758
- {updatedLabel ? (
759
- <UpdatedStamp>Updated {updatedLabel}</UpdatedStamp>
760
- ) : null}
1810
+ <Question>
1811
+ <PromptLabel>Original prompt: </PromptLabel>
1812
+ {displayPrompt(question)}
1813
+ </Question>
1814
+ <HeaderMeta>
1815
+ {updatedLabel ? (
1816
+ <UpdatedStamp>Updated {updatedLabel}</UpdatedStamp>
1817
+ ) : null}
1818
+ {onUpdate ? (
1819
+ <UpdateButton type="button" onClick={onUpdate}>
1820
+ Update
1821
+ </UpdateButton>
1822
+ ) : null}
1823
+ </HeaderMeta>
761
1824
  </Header>
762
1825
 
763
- {mode !== 'audit' ? (
1826
+ {mode === 'markdown' ? (
1827
+ <MarkdownPane aria-label="Markdown dossier handoff">
1828
+ {markdownText}
1829
+ </MarkdownPane>
1830
+ ) : null}
1831
+
1832
+ {mode !== 'audit' && mode !== 'markdown' ? (
764
1833
  <Narrative aria-label="Dossier narrative">
765
- <Kicker>Dossier</Kicker>
1834
+ <Kicker>Executive summary</Kicker>
766
1835
  <Lead>
767
- {cleanDisplayText(answer)}
1836
+ <ClaimText value={leadText} glossary={leadEvidence?.glossary} />
768
1837
  {leadEvidence ? (
769
- <EvidenceCitation
1838
+ <SourceSupport
770
1839
  finding={leadEvidence}
771
1840
  open={openEvidenceId === leadEvidence.id}
772
- onOpen={() => setOpenEvidenceId(leadEvidence.id)}
773
1841
  onToggle={() => toggleEvidence(leadEvidence.id)}
774
1842
  />
775
1843
  ) : null}
1844
+ {activeEvidence &&
1845
+ leadEvidence &&
1846
+ activeEvidence.id === leadEvidence.id ? (
1847
+ <EvidenceDrawer>
1848
+ <EvidenceCards sources={activeEvidence?.evidence} />
1849
+ </EvidenceDrawer>
1850
+ ) : null}
776
1851
  </Lead>
777
- {activeEvidence &&
778
- leadEvidence &&
779
- activeEvidence.id === leadEvidence.id ? (
780
- <EvidenceDrawer>
781
- <EvidenceCards sources={activeEvidence?.evidence} />
782
- </EvidenceDrawer>
783
- ) : null}
784
- {safeNarrative.map((item, index) => {
785
- const relatedFinding = narrativeEvidence(index)
786
- const paragraphEvidence =
787
- activeEvidence &&
788
- relatedFinding &&
789
- activeEvidence.id === relatedFinding.id
790
- ? activeEvidence
791
- : null
1852
+ <ExecutiveList aria-label="Executive summary points">
1853
+ {safeFindings.map((finding, index) => {
1854
+ const paragraphEvidence =
1855
+ activeEvidence?.id === finding.id ? activeEvidence : null
792
1856
  return (
793
- <NarrativeText key={`${index}-${item}`}>
794
- {cleanDisplayText(item)}
795
- {relatedFinding ? (
796
- <EvidenceCitation
797
- finding={relatedFinding}
798
- open={openEvidenceId === relatedFinding.id}
799
- onOpen={() => setOpenEvidenceId(relatedFinding.id)}
800
- onToggle={() => toggleEvidence(relatedFinding.id)}
1857
+ <ExecutiveItem key={finding.id}>
1858
+ <ExecutiveBullet>
1859
+ {String(index + 1).padStart(2, '0')}
1860
+ </ExecutiveBullet>
1861
+ <div>
1862
+ <ExecutiveText>
1863
+ {summarySentence(finding.claim, finding.title)}
1864
+ </ExecutiveText>
1865
+ <SourceSupport
1866
+ finding={finding}
1867
+ open={openEvidenceId === finding.id}
1868
+ onToggle={() => toggleEvidence(finding.id)}
801
1869
  />
802
- ) : null}
803
- {paragraphEvidence ? (
804
- <EvidenceDrawer>
805
- <EvidenceCards sources={paragraphEvidence.evidence} />
806
- </EvidenceDrawer>
807
- ) : null}
808
- </NarrativeText>
1870
+ {paragraphEvidence ? (
1871
+ <EvidenceDrawer>
1872
+ <EvidenceCards sources={paragraphEvidence.evidence} />
1873
+ </EvidenceDrawer>
1874
+ ) : null}
1875
+ </div>
1876
+ </ExecutiveItem>
809
1877
  )
810
- })}
1878
+ })}
1879
+ </ExecutiveList>
811
1880
  </Narrative>
812
1881
  ) : null}
813
1882
 
@@ -821,12 +1890,14 @@ export function EvidenceDossier({
821
1890
  {safeFindings.map(finding => (
822
1891
  <AuditFinding key={finding.id}>
823
1892
  <FindingTitle>{cleanDisplayText(finding.title)}</FindingTitle>
824
- <Claim>{cleanDisplayText(finding.claim)}</Claim>
1893
+ <Claim>
1894
+ <ClaimText value={finding.claim} glossary={finding.glossary} />
1895
+ </Claim>
825
1896
  <EvidenceCards sources={finding.evidence} />
826
1897
  </AuditFinding>
827
1898
  ))}
828
1899
  </AuditList>
829
- ) : (
1900
+ ) : mode !== 'markdown' ? (
830
1901
  <Findings aria-label="Evidence-backed positions">
831
1902
  <Kicker>{mode === 'read' ? 'Argument' : 'Claim review'}</Kicker>
832
1903
  {safeFindings.map((finding, index) => (
@@ -850,20 +1921,33 @@ export function EvidenceDossier({
850
1921
  </DeleteAction>
851
1922
  ) : null}
852
1923
  </FindingTitleGroup>
853
- {mode === 'review' ? (
854
- <Confidence $strength={finding.confidence}>
855
- {strengthLabels[finding.confidence]}
856
- </Confidence>
857
- ) : null}
858
1924
  </FindingHeader>
1925
+ <FindingBadges aria-label="Finding review signals">
1926
+ <FindingBadge $tone={finding.confidence}>
1927
+ {strengthLabels[finding.confidence]}
1928
+ </FindingBadge>
1929
+ <FindingBadge $tone={findingFreshnessBadge(finding).tone}>
1930
+ {findingFreshnessBadge(finding).label}
1931
+ </FindingBadge>
1932
+ <FindingBadge>
1933
+ {finding.evidence.length} source
1934
+ {finding.evidence.length === 1 ? '' : 's'}
1935
+ </FindingBadge>
1936
+ </FindingBadges>
859
1937
  <Claim>
860
- <ClaimLine>{cleanDisplayText(finding.claim)}</ClaimLine>
861
- <EvidenceCitation
1938
+ <ClaimLine>
1939
+ <ClaimText value={finding.claim} glossary={finding.glossary} />
1940
+ </ClaimLine>
1941
+ <SourceSupport
862
1942
  finding={finding}
863
1943
  open={openEvidenceId === finding.id}
864
- onOpen={() => setOpenEvidenceId(finding.id)}
865
1944
  onToggle={() => toggleEvidence(finding.id)}
866
1945
  />
1946
+ {activeEvidence?.id === finding.id && mode !== 'review' ? (
1947
+ <EvidenceDrawer>
1948
+ <EvidenceCards sources={activeEvidence.evidence} />
1949
+ </EvidenceDrawer>
1950
+ ) : null}
867
1951
  </Claim>
868
1952
  {finding.annotation ? (
869
1953
  <Annotation>
@@ -872,35 +1956,33 @@ export function EvidenceDossier({
872
1956
  ) : null}
873
1957
  {mode === 'review' ? (
874
1958
  <EvidenceCards sources={finding.evidence} />
875
- ) : activeEvidence?.id === finding.id ? (
876
- <EvidenceDrawer>
877
- <EvidenceCards sources={activeEvidence.evidence} />
878
- </EvidenceDrawer>
879
1959
  ) : null}
880
1960
  </FindingBody>
881
1961
  </Finding>
882
1962
  ))}
883
1963
  </Findings>
884
- )}
885
-
886
- <BottomGrid>
887
- <QuietList>
888
- <Kicker>Uncertainty</Kicker>
889
- <QuietListItems>
890
- {safeUncertainties.map(item => (
891
- <QuietListItem key={item}>{cleanDisplayText(item)}</QuietListItem>
892
- ))}
893
- </QuietListItems>
894
- </QuietList>
895
- <QuietList>
896
- <Kicker>Next</Kicker>
897
- <QuietListItems>
898
- {safeNextSteps.map(item => (
899
- <QuietListItem key={item}>{cleanDisplayText(item)}</QuietListItem>
900
- ))}
901
- </QuietListItems>
902
- </QuietList>
903
- </BottomGrid>
1964
+ ) : null}
1965
+
1966
+ {mode !== 'markdown' ? (
1967
+ <BottomGrid>
1968
+ <QuietList>
1969
+ <Kicker>Uncertainty</Kicker>
1970
+ <QuietListItems>
1971
+ {safeUncertainties.map(item => (
1972
+ <QuietListItem key={item}>{cleanDisplayText(item)}</QuietListItem>
1973
+ ))}
1974
+ </QuietListItems>
1975
+ </QuietList>
1976
+ <QuietList>
1977
+ <Kicker>Next</Kicker>
1978
+ <QuietListItems>
1979
+ {safeNextSteps.map(item => (
1980
+ <QuietListItem key={item}>{cleanDisplayText(item)}</QuietListItem>
1981
+ ))}
1982
+ </QuietListItems>
1983
+ </QuietList>
1984
+ </BottomGrid>
1985
+ ) : null}
904
1986
 
905
1987
  {footer ? <Footer>{footer}</Footer> : null}
906
1988
  </Dossier>