privateboard 0.1.8 → 0.1.9
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.
- package/dist/cli.js +879 -180
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +2 -1
- package/public/agent-profile.css +525 -0
- package/public/agent-profile.js +278 -1
- package/public/app.js +269 -113
- package/public/index.html +304 -126
- package/public/magazine.html +1257 -838
- package/public/newspaper.html +1389 -1267
- package/public/ppt.html +2623 -0
- package/public/room-settings.css +40 -4
- package/public/room-settings.js +44 -2
- package/public/user-settings.css +23 -21
- package/public/user-settings.js +1 -4
- package/public/bento.html +0 -1396
package/public/ppt.html
ADDED
|
@@ -0,0 +1,2623 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=900, initial-scale=1">
|
|
6
|
+
<title>Slides · PrivateBoard</title>
|
|
7
|
+
<link rel="icon" href="/avatars/chair.svg" type="image/svg+xml">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
|
+
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400;1,700&display=swap" rel="stylesheet">
|
|
11
|
+
<style>
|
|
12
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
13
|
+
PPT · two distinct slide-deck templates picked deterministically:
|
|
14
|
+
· ANTHROPIC · research-note register · warm cream paper · mono
|
|
15
|
+
kickers above every section · roman serif headlines with
|
|
16
|
+
italic-clay <em> · italic lower-roman / decimal-leading-zero
|
|
17
|
+
numerals · top + bottom rules only (no border-left)
|
|
18
|
+
· KEYNOTE · cinematic register · near-black slides · cream type
|
|
19
|
+
· big display serif · gold accent · one idea per slide
|
|
20
|
+
Both render 7-12 slides (cover · agenda · timeline · director
|
|
21
|
+
voices · milestones · data · consensus · conflicts · recommendations
|
|
22
|
+
· what to watch · takeaway). Reader steps through with arrow keys /
|
|
23
|
+
clicks / thumb strip / fullscreen.
|
|
24
|
+
─────────────────────────────────────────────────────────────────── */
|
|
25
|
+
:root {
|
|
26
|
+
--bg: #0E0E10;
|
|
27
|
+
--paper: #FFFFFF;
|
|
28
|
+
--paper-soft: #F8F6EE;
|
|
29
|
+
--paper-edge: #ECE7D6;
|
|
30
|
+
--ink: #14110B;
|
|
31
|
+
--ink-soft: #2D281D;
|
|
32
|
+
--ink-mid: #5C5645;
|
|
33
|
+
--ink-faint: #8B8470;
|
|
34
|
+
--ink-muted: #B0A893;
|
|
35
|
+
--rule: #D4CDB6;
|
|
36
|
+
--rule-strong: #918966;
|
|
37
|
+
--accent: #1A3A6B;
|
|
38
|
+
--accent-deep: #0F2647;
|
|
39
|
+
--accent-soft: #99B0CD;
|
|
40
|
+
|
|
41
|
+
--shadow-slide: 0 2px 8px rgba(0, 0, 0, 0.20),
|
|
42
|
+
0 18px 48px rgba(0, 0, 0, 0.28);
|
|
43
|
+
|
|
44
|
+
--serif-display: "Playfair Display", "Tiempos Headline",
|
|
45
|
+
"Bodoni 72", "Source Serif Pro", "Charter",
|
|
46
|
+
Georgia, "Source Han Serif SC", "Songti SC",
|
|
47
|
+
"STSong", serif;
|
|
48
|
+
--serif: "Charter", "Source Serif Pro", "Iowan Old Style",
|
|
49
|
+
"Source Han Serif SC", Georgia, "Songti SC", "STSong", serif;
|
|
50
|
+
--sans: "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont,
|
|
51
|
+
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
52
|
+
"Source Han Sans CN", "Noto Sans CJK SC", sans-serif;
|
|
53
|
+
--mono: "SF Mono", "JetBrains Mono", "Menlo",
|
|
54
|
+
"PingFang SC", "Source Han Sans CN", monospace;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
body[data-ppt-variant="keynote"] {
|
|
58
|
+
--paper: #0A0A0A;
|
|
59
|
+
--paper-soft: #161514;
|
|
60
|
+
--paper-edge: #2C2A26;
|
|
61
|
+
--ink: #F4EFDF;
|
|
62
|
+
--ink-soft: #C8C2AE;
|
|
63
|
+
--ink-mid: #8A856F;
|
|
64
|
+
--ink-faint: #5A5546;
|
|
65
|
+
--ink-muted: #3F3B30;
|
|
66
|
+
--rule: #2C2A26;
|
|
67
|
+
--rule-strong: #4A4738;
|
|
68
|
+
--accent: #D4A24A;
|
|
69
|
+
--accent-deep: #A9802E;
|
|
70
|
+
--accent-soft: #E5BD78;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
74
|
+
html, body {
|
|
75
|
+
background: var(--bg);
|
|
76
|
+
color: var(--ink);
|
|
77
|
+
font-family: var(--sans);
|
|
78
|
+
font-size: 14px;
|
|
79
|
+
line-height: 1.5;
|
|
80
|
+
-webkit-font-smoothing: antialiased;
|
|
81
|
+
text-rendering: optimizeLegibility;
|
|
82
|
+
min-height: 100vh;
|
|
83
|
+
overflow-x: hidden;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* ─── Top chrome ───────────────────────────────────────────────── */
|
|
87
|
+
.ppt-top-bar {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
justify-content: space-between;
|
|
91
|
+
gap: 14px;
|
|
92
|
+
padding: 14px 28px;
|
|
93
|
+
background: rgba(14, 14, 16, 0.92);
|
|
94
|
+
backdrop-filter: blur(10px);
|
|
95
|
+
-webkit-backdrop-filter: blur(10px);
|
|
96
|
+
border-bottom: 1px solid rgba(244, 239, 223, 0.12);
|
|
97
|
+
flex-wrap: wrap;
|
|
98
|
+
position: sticky;
|
|
99
|
+
top: 0;
|
|
100
|
+
z-index: 20;
|
|
101
|
+
color: #F4EFDF;
|
|
102
|
+
}
|
|
103
|
+
.ppt-crumb {
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
gap: 12px;
|
|
107
|
+
font-family: var(--serif-display);
|
|
108
|
+
font-size: 16px;
|
|
109
|
+
font-weight: 700;
|
|
110
|
+
color: #F4EFDF;
|
|
111
|
+
text-decoration: none;
|
|
112
|
+
}
|
|
113
|
+
.ppt-crumb::before {
|
|
114
|
+
content: "";
|
|
115
|
+
width: 9px;
|
|
116
|
+
height: 9px;
|
|
117
|
+
background: #D4A24A;
|
|
118
|
+
flex: 0 0 auto;
|
|
119
|
+
border-radius: 50%;
|
|
120
|
+
}
|
|
121
|
+
.ppt-crumb-accent {
|
|
122
|
+
color: #8B8470;
|
|
123
|
+
font-style: italic;
|
|
124
|
+
font-weight: 400;
|
|
125
|
+
}
|
|
126
|
+
.ppt-actions { display: flex; gap: 8px; align-items: center; }
|
|
127
|
+
.ppt-counter {
|
|
128
|
+
font-family: var(--mono);
|
|
129
|
+
font-size: 11px;
|
|
130
|
+
letter-spacing: 0.16em;
|
|
131
|
+
color: #C8C2AE;
|
|
132
|
+
margin-right: 14px;
|
|
133
|
+
min-width: 48px;
|
|
134
|
+
text-align: center;
|
|
135
|
+
}
|
|
136
|
+
.ppt-variant-badge {
|
|
137
|
+
font-family: var(--mono);
|
|
138
|
+
font-size: 10px;
|
|
139
|
+
letter-spacing: 0.16em;
|
|
140
|
+
text-transform: uppercase;
|
|
141
|
+
color: #E5BD78;
|
|
142
|
+
margin-right: 12px;
|
|
143
|
+
padding: 4px 10px;
|
|
144
|
+
background: rgba(244, 239, 223, 0.06);
|
|
145
|
+
border: 1px solid rgba(244, 239, 223, 0.16);
|
|
146
|
+
}
|
|
147
|
+
.ppt-btn {
|
|
148
|
+
font-family: var(--mono);
|
|
149
|
+
font-size: 11px;
|
|
150
|
+
letter-spacing: 0.04em;
|
|
151
|
+
padding: 7px 12px;
|
|
152
|
+
background: transparent;
|
|
153
|
+
border: 1px solid rgba(244, 239, 223, 0.30);
|
|
154
|
+
color: #F4EFDF;
|
|
155
|
+
cursor: pointer;
|
|
156
|
+
text-decoration: none;
|
|
157
|
+
text-transform: uppercase;
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
transition: background 0.15s, color 0.15s;
|
|
160
|
+
}
|
|
161
|
+
.ppt-btn:hover { background: #F4EFDF; color: #0E0E10; }
|
|
162
|
+
.ppt-btn .glyph { margin-right: 4px; }
|
|
163
|
+
|
|
164
|
+
/* ─── Stage · slide is centered with shadow ────────────────────── */
|
|
165
|
+
.ppt-stage {
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
min-height: calc(100vh - 60px - 100px);
|
|
170
|
+
padding: 28px 28px 16px;
|
|
171
|
+
}
|
|
172
|
+
.ppt-deck {
|
|
173
|
+
width: 100%;
|
|
174
|
+
max-width: 1120px;
|
|
175
|
+
aspect-ratio: 16 / 9;
|
|
176
|
+
position: relative;
|
|
177
|
+
box-shadow: var(--shadow-slide);
|
|
178
|
+
}
|
|
179
|
+
.ppt-slide {
|
|
180
|
+
position: absolute;
|
|
181
|
+
inset: 0;
|
|
182
|
+
background: var(--paper);
|
|
183
|
+
color: var(--ink);
|
|
184
|
+
display: none;
|
|
185
|
+
flex-direction: column;
|
|
186
|
+
overflow: hidden;
|
|
187
|
+
}
|
|
188
|
+
.ppt-slide.is-active { display: flex; }
|
|
189
|
+
/* Click halves · invisible regions on each slide for prev / next
|
|
190
|
+
navigation. Sit above tile content with z-index lower than
|
|
191
|
+
chrome but above text · text is still selectable. */
|
|
192
|
+
.ppt-slide-click {
|
|
193
|
+
position: absolute;
|
|
194
|
+
top: 0;
|
|
195
|
+
bottom: 0;
|
|
196
|
+
width: 18%;
|
|
197
|
+
z-index: 2;
|
|
198
|
+
cursor: pointer;
|
|
199
|
+
background: transparent;
|
|
200
|
+
border: 0;
|
|
201
|
+
padding: 0;
|
|
202
|
+
margin: 0;
|
|
203
|
+
appearance: none;
|
|
204
|
+
-webkit-appearance: none;
|
|
205
|
+
font: inherit;
|
|
206
|
+
color: inherit;
|
|
207
|
+
outline: none;
|
|
208
|
+
}
|
|
209
|
+
.ppt-slide-click:focus { outline: none; }
|
|
210
|
+
.ppt-slide-click:focus-visible { outline: none; }
|
|
211
|
+
.ppt-slide-click::-moz-focus-inner { border: 0; padding: 0; }
|
|
212
|
+
.ppt-slide-click.prev { left: 0; }
|
|
213
|
+
.ppt-slide-click.next { right: 0; }
|
|
214
|
+
|
|
215
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
216
|
+
ANTHROPIC-ESSAY variant · research-note register
|
|
217
|
+
· warm cream surface · mono kickers above every section
|
|
218
|
+
· roman serif H2 with italic-clay <em> emphasis
|
|
219
|
+
· italic lower-roman + decimal-leading-zero numerals
|
|
220
|
+
· top/bottom rules only — no border-left, no decorative chrome
|
|
221
|
+
═══════════════════════════════════════════════════════════════ */
|
|
222
|
+
|
|
223
|
+
body[data-ppt-variant="anthropic"] {
|
|
224
|
+
--paper: #F8F6EE;
|
|
225
|
+
--paper-soft: #F1ECDB;
|
|
226
|
+
--paper-edge: #E5DEC2;
|
|
227
|
+
--ink: #14110B;
|
|
228
|
+
--ink-soft: #2D281D;
|
|
229
|
+
--ink-mid: #5C5645;
|
|
230
|
+
--ink-faint: #8B8470;
|
|
231
|
+
--ink-muted: #B0A893;
|
|
232
|
+
--rule: #D4CDB6;
|
|
233
|
+
--rule-strong: #918966;
|
|
234
|
+
--accent: #A0492C;
|
|
235
|
+
--accent-deep: #7E371F;
|
|
236
|
+
--accent-soft: #C97554;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
body[data-ppt-variant="anthropic"] .ppt-slide {
|
|
240
|
+
background: var(--paper);
|
|
241
|
+
color: var(--ink);
|
|
242
|
+
font-family: var(--sans);
|
|
243
|
+
padding: 0;
|
|
244
|
+
}
|
|
245
|
+
/* Floating chrome · mono kicker top-left, counter bottom-right.
|
|
246
|
+
The slide itself is the focus · no top bar / no footer band. */
|
|
247
|
+
.ant-eye-top {
|
|
248
|
+
position: absolute;
|
|
249
|
+
top: 26px;
|
|
250
|
+
left: 56px;
|
|
251
|
+
font-family: var(--mono);
|
|
252
|
+
font-size: 11px;
|
|
253
|
+
font-weight: 700;
|
|
254
|
+
letter-spacing: 0.18em;
|
|
255
|
+
text-transform: uppercase;
|
|
256
|
+
color: var(--accent);
|
|
257
|
+
z-index: 3;
|
|
258
|
+
}
|
|
259
|
+
.ant-foot {
|
|
260
|
+
position: absolute;
|
|
261
|
+
bottom: 26px;
|
|
262
|
+
left: 56px;
|
|
263
|
+
font-family: var(--mono);
|
|
264
|
+
font-size: 10px;
|
|
265
|
+
letter-spacing: 0.20em;
|
|
266
|
+
text-transform: uppercase;
|
|
267
|
+
color: var(--ink-faint);
|
|
268
|
+
z-index: 3;
|
|
269
|
+
}
|
|
270
|
+
.ant-foot b {
|
|
271
|
+
font-family: var(--serif-display);
|
|
272
|
+
font-style: italic;
|
|
273
|
+
font-weight: 700;
|
|
274
|
+
letter-spacing: 0;
|
|
275
|
+
font-size: 13px;
|
|
276
|
+
text-transform: none;
|
|
277
|
+
color: var(--ink);
|
|
278
|
+
}
|
|
279
|
+
.ant-counter {
|
|
280
|
+
position: absolute;
|
|
281
|
+
bottom: 26px;
|
|
282
|
+
right: 56px;
|
|
283
|
+
font-family: var(--mono);
|
|
284
|
+
font-size: 10px;
|
|
285
|
+
letter-spacing: 0.20em;
|
|
286
|
+
text-transform: uppercase;
|
|
287
|
+
color: var(--ink-faint);
|
|
288
|
+
z-index: 3;
|
|
289
|
+
}
|
|
290
|
+
.ant-counter b {
|
|
291
|
+
color: var(--accent);
|
|
292
|
+
font-weight: 700;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* Body container · 1080px max so essay-width text doesn't sprawl on
|
|
296
|
+
wide slides; padded generously top/bottom so kickers don't crowd
|
|
297
|
+
the chrome. */
|
|
298
|
+
.ant-body {
|
|
299
|
+
flex: 1;
|
|
300
|
+
display: flex;
|
|
301
|
+
flex-direction: column;
|
|
302
|
+
justify-content: center;
|
|
303
|
+
padding: 72px 96px;
|
|
304
|
+
min-height: 0;
|
|
305
|
+
max-width: 1080px;
|
|
306
|
+
width: 100%;
|
|
307
|
+
margin: 0 auto;
|
|
308
|
+
}
|
|
309
|
+
.ant-headline {
|
|
310
|
+
font-family: var(--serif-display);
|
|
311
|
+
font-weight: 700;
|
|
312
|
+
font-size: 46px;
|
|
313
|
+
line-height: 1.1;
|
|
314
|
+
letter-spacing: -0.012em;
|
|
315
|
+
color: var(--ink);
|
|
316
|
+
word-break: break-word;
|
|
317
|
+
overflow-wrap: anywhere;
|
|
318
|
+
}
|
|
319
|
+
.ant-headline em {
|
|
320
|
+
font-style: italic;
|
|
321
|
+
color: var(--accent);
|
|
322
|
+
}
|
|
323
|
+
.ant-deck {
|
|
324
|
+
font-family: var(--serif);
|
|
325
|
+
font-style: italic;
|
|
326
|
+
font-size: 18px;
|
|
327
|
+
line-height: 1.5;
|
|
328
|
+
color: var(--ink-soft);
|
|
329
|
+
margin-top: 18px;
|
|
330
|
+
max-width: 880px;
|
|
331
|
+
}
|
|
332
|
+
.ant-deck em { color: var(--accent); }
|
|
333
|
+
|
|
334
|
+
/* Cover · hero serif title with italic emphasis · stat strip below */
|
|
335
|
+
.ant-slide-cover .ant-body {
|
|
336
|
+
justify-content: center;
|
|
337
|
+
padding: 64px 96px 72px;
|
|
338
|
+
}
|
|
339
|
+
.ant-cover-kicker {
|
|
340
|
+
font-family: var(--mono);
|
|
341
|
+
font-size: 11px;
|
|
342
|
+
font-weight: 700;
|
|
343
|
+
letter-spacing: 0.20em;
|
|
344
|
+
text-transform: uppercase;
|
|
345
|
+
color: var(--accent);
|
|
346
|
+
margin-bottom: 18px;
|
|
347
|
+
}
|
|
348
|
+
.ant-slide-cover .ant-headline {
|
|
349
|
+
font-size: 64px;
|
|
350
|
+
}
|
|
351
|
+
.ant-slide-cover .ant-deck {
|
|
352
|
+
font-size: 22px;
|
|
353
|
+
max-width: 920px;
|
|
354
|
+
margin-top: 22px;
|
|
355
|
+
}
|
|
356
|
+
.ant-byline {
|
|
357
|
+
font-family: var(--mono);
|
|
358
|
+
font-size: 11px;
|
|
359
|
+
letter-spacing: 0.20em;
|
|
360
|
+
text-transform: uppercase;
|
|
361
|
+
color: var(--ink-mid);
|
|
362
|
+
margin-top: 28px;
|
|
363
|
+
}
|
|
364
|
+
.ant-byline em {
|
|
365
|
+
font-family: var(--serif);
|
|
366
|
+
font-style: italic;
|
|
367
|
+
text-transform: none;
|
|
368
|
+
letter-spacing: 0;
|
|
369
|
+
color: var(--ink);
|
|
370
|
+
}
|
|
371
|
+
.ant-cover-stats {
|
|
372
|
+
display: grid;
|
|
373
|
+
grid-template-columns: repeat(3, 1fr);
|
|
374
|
+
gap: 0;
|
|
375
|
+
margin-top: 36px;
|
|
376
|
+
padding-top: 24px;
|
|
377
|
+
border-top: 1px solid var(--rule);
|
|
378
|
+
}
|
|
379
|
+
.ant-cover-stat-num {
|
|
380
|
+
font-family: var(--serif-display);
|
|
381
|
+
font-style: italic;
|
|
382
|
+
font-weight: 700;
|
|
383
|
+
font-size: 48px;
|
|
384
|
+
line-height: 0.96;
|
|
385
|
+
color: var(--accent);
|
|
386
|
+
letter-spacing: -0.02em;
|
|
387
|
+
}
|
|
388
|
+
.ant-cover-stat-cap {
|
|
389
|
+
font-family: var(--mono);
|
|
390
|
+
font-size: 10px;
|
|
391
|
+
font-weight: 700;
|
|
392
|
+
letter-spacing: 0.20em;
|
|
393
|
+
text-transform: uppercase;
|
|
394
|
+
color: var(--ink-mid);
|
|
395
|
+
margin-top: 12px;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* Agenda · decimal-leading-zero markers in italic serif */
|
|
399
|
+
.ant-agenda {
|
|
400
|
+
list-style: none;
|
|
401
|
+
margin: 24px 0 0;
|
|
402
|
+
padding: 0;
|
|
403
|
+
display: grid;
|
|
404
|
+
grid-template-columns: 1fr 1fr;
|
|
405
|
+
gap: 6px 36px;
|
|
406
|
+
counter-reset: aagenda;
|
|
407
|
+
}
|
|
408
|
+
.ant-agenda li {
|
|
409
|
+
display: grid;
|
|
410
|
+
grid-template-columns: 64px 1fr;
|
|
411
|
+
gap: 14px;
|
|
412
|
+
align-items: baseline;
|
|
413
|
+
padding: 14px 0;
|
|
414
|
+
border-bottom: 1px solid var(--rule);
|
|
415
|
+
counter-increment: aagenda;
|
|
416
|
+
}
|
|
417
|
+
.ant-agenda li::before {
|
|
418
|
+
content: counter(aagenda, decimal-leading-zero);
|
|
419
|
+
font-family: var(--serif-display);
|
|
420
|
+
font-style: italic;
|
|
421
|
+
font-size: 28px;
|
|
422
|
+
font-weight: 700;
|
|
423
|
+
color: var(--accent);
|
|
424
|
+
line-height: 1;
|
|
425
|
+
}
|
|
426
|
+
.ant-agenda li span {
|
|
427
|
+
font-family: var(--serif);
|
|
428
|
+
font-size: 17px;
|
|
429
|
+
line-height: 1.36;
|
|
430
|
+
color: var(--ink);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/* Milestone · headline + body + side stat or roman numeral */
|
|
434
|
+
.ant-milestone {
|
|
435
|
+
display: grid;
|
|
436
|
+
grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
|
|
437
|
+
gap: 56px;
|
|
438
|
+
align-items: center;
|
|
439
|
+
flex: 1;
|
|
440
|
+
}
|
|
441
|
+
.ant-milestone.no-side {
|
|
442
|
+
grid-template-columns: 1fr;
|
|
443
|
+
}
|
|
444
|
+
.ant-milestone-body {
|
|
445
|
+
font-family: var(--serif);
|
|
446
|
+
font-size: 18px;
|
|
447
|
+
line-height: 1.55;
|
|
448
|
+
color: var(--ink-soft);
|
|
449
|
+
margin-top: 18px;
|
|
450
|
+
max-width: 720px;
|
|
451
|
+
}
|
|
452
|
+
.ant-milestone-body em {
|
|
453
|
+
font-style: italic;
|
|
454
|
+
color: var(--accent);
|
|
455
|
+
}
|
|
456
|
+
.ant-callout-num {
|
|
457
|
+
font-family: var(--serif-display);
|
|
458
|
+
font-style: italic;
|
|
459
|
+
font-weight: 700;
|
|
460
|
+
font-size: 124px;
|
|
461
|
+
line-height: 0.92;
|
|
462
|
+
letter-spacing: -0.04em;
|
|
463
|
+
color: var(--accent);
|
|
464
|
+
text-align: center;
|
|
465
|
+
word-break: break-word;
|
|
466
|
+
overflow-wrap: anywhere;
|
|
467
|
+
}
|
|
468
|
+
.ant-callout-cap {
|
|
469
|
+
font-family: var(--mono);
|
|
470
|
+
font-size: 10px;
|
|
471
|
+
font-weight: 700;
|
|
472
|
+
letter-spacing: 0.22em;
|
|
473
|
+
text-transform: uppercase;
|
|
474
|
+
color: var(--ink-mid);
|
|
475
|
+
margin-top: 12px;
|
|
476
|
+
text-align: center;
|
|
477
|
+
}
|
|
478
|
+
.ant-roman {
|
|
479
|
+
text-align: center;
|
|
480
|
+
}
|
|
481
|
+
.ant-roman-num {
|
|
482
|
+
font-family: var(--serif-display);
|
|
483
|
+
font-style: italic;
|
|
484
|
+
font-weight: 700;
|
|
485
|
+
font-size: 168px;
|
|
486
|
+
line-height: 0.86;
|
|
487
|
+
letter-spacing: -0.04em;
|
|
488
|
+
color: var(--accent);
|
|
489
|
+
}
|
|
490
|
+
.ant-roman-cap {
|
|
491
|
+
font-family: var(--mono);
|
|
492
|
+
font-size: 11px;
|
|
493
|
+
font-weight: 700;
|
|
494
|
+
letter-spacing: 0.22em;
|
|
495
|
+
text-transform: uppercase;
|
|
496
|
+
color: var(--ink-mid);
|
|
497
|
+
margin-top: 14px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/* Timeline · italic roman markers on hairline rule */
|
|
501
|
+
.ant-timeline {
|
|
502
|
+
flex: 1;
|
|
503
|
+
display: grid;
|
|
504
|
+
grid-template-columns: repeat(var(--n, 3), 1fr);
|
|
505
|
+
gap: 0;
|
|
506
|
+
margin-top: 32px;
|
|
507
|
+
position: relative;
|
|
508
|
+
}
|
|
509
|
+
.ant-timeline::before {
|
|
510
|
+
content: "";
|
|
511
|
+
position: absolute;
|
|
512
|
+
top: 80px;
|
|
513
|
+
left: 24px;
|
|
514
|
+
right: 24px;
|
|
515
|
+
height: 1px;
|
|
516
|
+
background: var(--rule-strong);
|
|
517
|
+
}
|
|
518
|
+
.ant-tl-cell {
|
|
519
|
+
padding: 0 18px;
|
|
520
|
+
}
|
|
521
|
+
.ant-tl-cell:first-child { padding-left: 0; }
|
|
522
|
+
.ant-tl-cell:last-child { padding-right: 0; }
|
|
523
|
+
.ant-tl-period {
|
|
524
|
+
font-family: var(--mono);
|
|
525
|
+
font-size: 10px;
|
|
526
|
+
font-weight: 700;
|
|
527
|
+
letter-spacing: 0.20em;
|
|
528
|
+
text-transform: uppercase;
|
|
529
|
+
color: var(--accent);
|
|
530
|
+
margin-bottom: 14px;
|
|
531
|
+
min-height: 14px;
|
|
532
|
+
}
|
|
533
|
+
.ant-tl-marker {
|
|
534
|
+
font-family: var(--serif-display);
|
|
535
|
+
font-style: italic;
|
|
536
|
+
font-weight: 700;
|
|
537
|
+
font-size: 56px;
|
|
538
|
+
line-height: 0.86;
|
|
539
|
+
letter-spacing: -0.03em;
|
|
540
|
+
color: var(--accent);
|
|
541
|
+
background: var(--paper);
|
|
542
|
+
padding-right: 14px;
|
|
543
|
+
margin-bottom: 18px;
|
|
544
|
+
display: inline-block;
|
|
545
|
+
}
|
|
546
|
+
.ant-tl-title {
|
|
547
|
+
font-family: var(--serif-display);
|
|
548
|
+
font-size: 20px;
|
|
549
|
+
font-weight: 700;
|
|
550
|
+
line-height: 1.22;
|
|
551
|
+
color: var(--ink);
|
|
552
|
+
margin-bottom: 8px;
|
|
553
|
+
letter-spacing: -0.005em;
|
|
554
|
+
}
|
|
555
|
+
.ant-tl-body {
|
|
556
|
+
font-family: var(--serif);
|
|
557
|
+
font-style: italic;
|
|
558
|
+
font-size: 14px;
|
|
559
|
+
line-height: 1.45;
|
|
560
|
+
color: var(--ink-mid);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/* Data · ranked bars */
|
|
564
|
+
.ant-bars {
|
|
565
|
+
display: flex;
|
|
566
|
+
flex-direction: column;
|
|
567
|
+
gap: 16px;
|
|
568
|
+
margin-top: 24px;
|
|
569
|
+
}
|
|
570
|
+
.ant-bar {
|
|
571
|
+
display: grid;
|
|
572
|
+
grid-template-columns: 220px 1fr 90px;
|
|
573
|
+
gap: 16px;
|
|
574
|
+
align-items: center;
|
|
575
|
+
}
|
|
576
|
+
.ant-bar-label {
|
|
577
|
+
font-family: var(--serif);
|
|
578
|
+
font-size: 17px;
|
|
579
|
+
color: var(--ink);
|
|
580
|
+
}
|
|
581
|
+
.ant-bar-track {
|
|
582
|
+
height: 14px;
|
|
583
|
+
background: var(--paper-soft);
|
|
584
|
+
position: relative;
|
|
585
|
+
}
|
|
586
|
+
.ant-bar-fill {
|
|
587
|
+
position: absolute;
|
|
588
|
+
inset: 0 auto 0 0;
|
|
589
|
+
background: var(--accent);
|
|
590
|
+
}
|
|
591
|
+
.ant-bar-value {
|
|
592
|
+
font-family: var(--mono);
|
|
593
|
+
font-size: 12px;
|
|
594
|
+
letter-spacing: 0.04em;
|
|
595
|
+
color: var(--ink-soft);
|
|
596
|
+
text-align: right;
|
|
597
|
+
font-weight: 700;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/* Director voices · italic-clay name, mono role, italic stance.
|
|
601
|
+
Pull-quote uses TOP + BOTTOM rules only — no border-left. */
|
|
602
|
+
.ant-voices {
|
|
603
|
+
display: grid;
|
|
604
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
605
|
+
gap: 22px 28px;
|
|
606
|
+
margin-top: 18px;
|
|
607
|
+
align-content: start;
|
|
608
|
+
}
|
|
609
|
+
.ant-voices.is-three-up { grid-template-columns: repeat(3, 1fr); }
|
|
610
|
+
.ant-voices.is-two-up { grid-template-columns: repeat(2, 1fr); }
|
|
611
|
+
.ant-voice {
|
|
612
|
+
border-top: 1px solid var(--rule);
|
|
613
|
+
padding-top: 16px;
|
|
614
|
+
}
|
|
615
|
+
.ant-voice-name {
|
|
616
|
+
font-family: var(--serif-display);
|
|
617
|
+
font-style: italic;
|
|
618
|
+
font-weight: 700;
|
|
619
|
+
font-size: 26px;
|
|
620
|
+
line-height: 1.1;
|
|
621
|
+
color: var(--accent);
|
|
622
|
+
letter-spacing: -0.01em;
|
|
623
|
+
}
|
|
624
|
+
.ant-voice-role {
|
|
625
|
+
font-family: var(--mono);
|
|
626
|
+
font-size: 10px;
|
|
627
|
+
font-weight: 700;
|
|
628
|
+
letter-spacing: 0.22em;
|
|
629
|
+
text-transform: uppercase;
|
|
630
|
+
color: var(--ink-mid);
|
|
631
|
+
margin-top: 6px;
|
|
632
|
+
}
|
|
633
|
+
.ant-voice-stance {
|
|
634
|
+
font-family: var(--serif);
|
|
635
|
+
font-style: italic;
|
|
636
|
+
font-size: 15px;
|
|
637
|
+
line-height: 1.42;
|
|
638
|
+
color: var(--accent-deep);
|
|
639
|
+
margin-top: 12px;
|
|
640
|
+
}
|
|
641
|
+
.ant-voice-position {
|
|
642
|
+
font-family: var(--serif);
|
|
643
|
+
font-size: 14px;
|
|
644
|
+
line-height: 1.5;
|
|
645
|
+
color: var(--ink-soft);
|
|
646
|
+
margin-top: 10px;
|
|
647
|
+
}
|
|
648
|
+
.ant-voice-quote {
|
|
649
|
+
font-family: var(--serif);
|
|
650
|
+
font-style: italic;
|
|
651
|
+
font-size: 14px;
|
|
652
|
+
line-height: 1.46;
|
|
653
|
+
color: var(--ink-mid);
|
|
654
|
+
margin-top: 14px;
|
|
655
|
+
padding: 12px 0;
|
|
656
|
+
border-top: 1px solid var(--rule);
|
|
657
|
+
border-bottom: 1px solid var(--rule);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* Where we agreed · italic-roman markers in serif italic */
|
|
661
|
+
.ant-aligns {
|
|
662
|
+
list-style: none;
|
|
663
|
+
margin: 22px 0 0;
|
|
664
|
+
padding: 0;
|
|
665
|
+
display: flex;
|
|
666
|
+
flex-direction: column;
|
|
667
|
+
gap: 22px;
|
|
668
|
+
counter-reset: aalign;
|
|
669
|
+
}
|
|
670
|
+
.ant-align {
|
|
671
|
+
border-top: 1px solid var(--rule);
|
|
672
|
+
padding-top: 16px;
|
|
673
|
+
display: grid;
|
|
674
|
+
grid-template-columns: 56px 1fr;
|
|
675
|
+
gap: 18px;
|
|
676
|
+
counter-increment: aalign;
|
|
677
|
+
}
|
|
678
|
+
.ant-align::before {
|
|
679
|
+
content: counter(aalign, lower-roman) ".";
|
|
680
|
+
font-family: var(--serif-display);
|
|
681
|
+
font-style: italic;
|
|
682
|
+
font-weight: 700;
|
|
683
|
+
font-size: 32px;
|
|
684
|
+
line-height: 1;
|
|
685
|
+
color: var(--accent);
|
|
686
|
+
}
|
|
687
|
+
.ant-align-point {
|
|
688
|
+
font-family: var(--serif-display);
|
|
689
|
+
font-size: 22px;
|
|
690
|
+
font-weight: 700;
|
|
691
|
+
line-height: 1.22;
|
|
692
|
+
color: var(--ink);
|
|
693
|
+
letter-spacing: -0.012em;
|
|
694
|
+
}
|
|
695
|
+
.ant-align-point em { color: var(--accent); font-style: italic; }
|
|
696
|
+
.ant-align-note {
|
|
697
|
+
font-family: var(--serif);
|
|
698
|
+
font-style: italic;
|
|
699
|
+
font-size: 14px;
|
|
700
|
+
line-height: 1.5;
|
|
701
|
+
color: var(--ink-soft);
|
|
702
|
+
margin-top: 10px;
|
|
703
|
+
}
|
|
704
|
+
.ant-chips {
|
|
705
|
+
display: flex;
|
|
706
|
+
gap: 6px;
|
|
707
|
+
flex-wrap: wrap;
|
|
708
|
+
margin-top: 12px;
|
|
709
|
+
}
|
|
710
|
+
.ant-chip {
|
|
711
|
+
font-family: var(--mono);
|
|
712
|
+
font-size: 10px;
|
|
713
|
+
font-weight: 700;
|
|
714
|
+
letter-spacing: 0.20em;
|
|
715
|
+
text-transform: uppercase;
|
|
716
|
+
color: var(--accent);
|
|
717
|
+
border: 1px solid var(--rule-strong);
|
|
718
|
+
padding: 4px 10px;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/* Where we split · divergence */
|
|
722
|
+
.ant-splits {
|
|
723
|
+
display: flex;
|
|
724
|
+
flex-direction: column;
|
|
725
|
+
gap: 24px;
|
|
726
|
+
margin-top: 14px;
|
|
727
|
+
}
|
|
728
|
+
.ant-split {
|
|
729
|
+
border-top: 1px solid var(--rule);
|
|
730
|
+
padding-top: 16px;
|
|
731
|
+
}
|
|
732
|
+
.ant-split-hinge {
|
|
733
|
+
font-family: var(--serif-display);
|
|
734
|
+
font-style: italic;
|
|
735
|
+
font-size: 22px;
|
|
736
|
+
font-weight: 700;
|
|
737
|
+
line-height: 1.22;
|
|
738
|
+
color: var(--ink);
|
|
739
|
+
letter-spacing: -0.012em;
|
|
740
|
+
margin-bottom: 14px;
|
|
741
|
+
}
|
|
742
|
+
.ant-split-hinge em { color: var(--accent); }
|
|
743
|
+
.ant-split-sides {
|
|
744
|
+
display: grid;
|
|
745
|
+
grid-template-columns: 1fr 1fr;
|
|
746
|
+
gap: 18px;
|
|
747
|
+
}
|
|
748
|
+
.ant-split-sides.is-three { grid-template-columns: 1fr 1fr 1fr; }
|
|
749
|
+
.ant-split-side {
|
|
750
|
+
border-top: 1px solid var(--rule-strong);
|
|
751
|
+
padding-top: 10px;
|
|
752
|
+
}
|
|
753
|
+
.ant-split-side-label {
|
|
754
|
+
font-family: var(--mono);
|
|
755
|
+
font-size: 10px;
|
|
756
|
+
font-weight: 700;
|
|
757
|
+
letter-spacing: 0.22em;
|
|
758
|
+
text-transform: uppercase;
|
|
759
|
+
color: var(--accent);
|
|
760
|
+
margin-bottom: 8px;
|
|
761
|
+
}
|
|
762
|
+
.ant-split-side-stance {
|
|
763
|
+
font-family: var(--serif);
|
|
764
|
+
font-size: 14px;
|
|
765
|
+
line-height: 1.5;
|
|
766
|
+
color: var(--ink);
|
|
767
|
+
}
|
|
768
|
+
.ant-split-side-names {
|
|
769
|
+
margin-top: 10px;
|
|
770
|
+
display: flex;
|
|
771
|
+
gap: 4px;
|
|
772
|
+
flex-wrap: wrap;
|
|
773
|
+
}
|
|
774
|
+
.ant-split-resolve {
|
|
775
|
+
font-family: var(--serif);
|
|
776
|
+
font-style: italic;
|
|
777
|
+
font-size: 13px;
|
|
778
|
+
line-height: 1.46;
|
|
779
|
+
color: var(--ink-mid);
|
|
780
|
+
margin-top: 12px;
|
|
781
|
+
}
|
|
782
|
+
.ant-split-resolve b {
|
|
783
|
+
font-family: var(--mono);
|
|
784
|
+
font-style: normal;
|
|
785
|
+
font-weight: 700;
|
|
786
|
+
font-size: 10px;
|
|
787
|
+
letter-spacing: 0.22em;
|
|
788
|
+
text-transform: uppercase;
|
|
789
|
+
color: var(--accent);
|
|
790
|
+
margin-right: 8px;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/* Bullets · italic lower-roman (recommendations) /
|
|
794
|
+
decimal-leading-zero (verification) */
|
|
795
|
+
.ant-bullets {
|
|
796
|
+
list-style: none;
|
|
797
|
+
margin: 24px 0 0;
|
|
798
|
+
padding: 0;
|
|
799
|
+
display: flex;
|
|
800
|
+
flex-direction: column;
|
|
801
|
+
gap: 18px;
|
|
802
|
+
counter-reset: abullets;
|
|
803
|
+
}
|
|
804
|
+
.ant-bullets li {
|
|
805
|
+
display: grid;
|
|
806
|
+
grid-template-columns: 56px 1fr;
|
|
807
|
+
gap: 18px;
|
|
808
|
+
align-items: baseline;
|
|
809
|
+
counter-increment: abullets;
|
|
810
|
+
}
|
|
811
|
+
.ant-bullets.is-roman li::before {
|
|
812
|
+
content: counter(abullets, lower-roman) ".";
|
|
813
|
+
font-family: var(--serif-display);
|
|
814
|
+
font-style: italic;
|
|
815
|
+
font-weight: 700;
|
|
816
|
+
font-size: 30px;
|
|
817
|
+
color: var(--accent);
|
|
818
|
+
line-height: 1;
|
|
819
|
+
}
|
|
820
|
+
.ant-bullets.is-decimal li::before {
|
|
821
|
+
content: counter(abullets, decimal-leading-zero);
|
|
822
|
+
font-family: var(--serif-display);
|
|
823
|
+
font-style: italic;
|
|
824
|
+
font-weight: 700;
|
|
825
|
+
font-size: 30px;
|
|
826
|
+
color: var(--accent);
|
|
827
|
+
line-height: 1;
|
|
828
|
+
}
|
|
829
|
+
.ant-bullets li span {
|
|
830
|
+
font-family: var(--serif);
|
|
831
|
+
font-size: 18px;
|
|
832
|
+
line-height: 1.4;
|
|
833
|
+
color: var(--ink);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/* Takeaway · pull-quote register · italic serif hero */
|
|
837
|
+
.ant-slide-takeaway .ant-body {
|
|
838
|
+
justify-content: center;
|
|
839
|
+
text-align: center;
|
|
840
|
+
align-items: center;
|
|
841
|
+
padding: 64px 96px;
|
|
842
|
+
}
|
|
843
|
+
.ant-takeaway-mark {
|
|
844
|
+
font-family: var(--serif-display);
|
|
845
|
+
font-style: italic;
|
|
846
|
+
font-size: 96px;
|
|
847
|
+
color: var(--accent);
|
|
848
|
+
line-height: 0.5;
|
|
849
|
+
margin-bottom: 28px;
|
|
850
|
+
}
|
|
851
|
+
.ant-takeaway-text {
|
|
852
|
+
font-family: var(--serif-display);
|
|
853
|
+
font-style: italic;
|
|
854
|
+
font-size: 44px;
|
|
855
|
+
font-weight: 500;
|
|
856
|
+
line-height: 1.15;
|
|
857
|
+
letter-spacing: -0.012em;
|
|
858
|
+
color: var(--ink);
|
|
859
|
+
max-width: 940px;
|
|
860
|
+
word-break: break-word;
|
|
861
|
+
}
|
|
862
|
+
.ant-takeaway-cite {
|
|
863
|
+
font-family: var(--mono);
|
|
864
|
+
font-size: 11px;
|
|
865
|
+
font-weight: 700;
|
|
866
|
+
letter-spacing: 0.22em;
|
|
867
|
+
text-transform: uppercase;
|
|
868
|
+
color: var(--ink-mid);
|
|
869
|
+
margin-top: 28px;
|
|
870
|
+
}
|
|
871
|
+
.ant-takeaway-cite em {
|
|
872
|
+
font-family: var(--serif);
|
|
873
|
+
font-style: italic;
|
|
874
|
+
text-transform: none;
|
|
875
|
+
letter-spacing: 0;
|
|
876
|
+
color: var(--accent);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
@media (max-width: 1080px) {
|
|
880
|
+
.ant-slide-cover .ant-headline { font-size: 48px; }
|
|
881
|
+
.ant-headline { font-size: 36px; }
|
|
882
|
+
.ant-deck { font-size: 16px; }
|
|
883
|
+
.ant-takeaway-text { font-size: 32px; }
|
|
884
|
+
.ant-roman-num { font-size: 132px; }
|
|
885
|
+
.ant-callout-num { font-size: 96px; }
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
889
|
+
KEYNOTE variant · cinematic register
|
|
890
|
+
═══════════════════════════════════════════════════════════════ */
|
|
891
|
+
|
|
892
|
+
body[data-ppt-variant="keynote"] .ppt-slide {
|
|
893
|
+
background: var(--paper);
|
|
894
|
+
color: var(--ink);
|
|
895
|
+
font-family: var(--serif-display);
|
|
896
|
+
}
|
|
897
|
+
.key-floater {
|
|
898
|
+
position: absolute;
|
|
899
|
+
bottom: 24px;
|
|
900
|
+
right: 32px;
|
|
901
|
+
font-family: var(--mono);
|
|
902
|
+
font-size: 10px;
|
|
903
|
+
letter-spacing: 0.16em;
|
|
904
|
+
text-transform: uppercase;
|
|
905
|
+
color: var(--ink-mid);
|
|
906
|
+
}
|
|
907
|
+
.key-floater b { color: var(--accent); font-weight: 700; }
|
|
908
|
+
.key-eye-top {
|
|
909
|
+
position: absolute;
|
|
910
|
+
top: 24px;
|
|
911
|
+
left: 36px;
|
|
912
|
+
font-family: var(--sans);
|
|
913
|
+
font-size: 10px;
|
|
914
|
+
font-weight: 700;
|
|
915
|
+
letter-spacing: 0.22em;
|
|
916
|
+
text-transform: uppercase;
|
|
917
|
+
color: var(--accent);
|
|
918
|
+
}
|
|
919
|
+
.key-body {
|
|
920
|
+
flex: 1;
|
|
921
|
+
display: flex;
|
|
922
|
+
flex-direction: column;
|
|
923
|
+
justify-content: center;
|
|
924
|
+
padding: 64px 80px;
|
|
925
|
+
min-height: 0;
|
|
926
|
+
}
|
|
927
|
+
.key-cover-mark {
|
|
928
|
+
font-family: var(--serif-display);
|
|
929
|
+
font-style: italic;
|
|
930
|
+
font-size: 64px;
|
|
931
|
+
color: var(--accent);
|
|
932
|
+
line-height: 0.5;
|
|
933
|
+
margin-bottom: 18px;
|
|
934
|
+
}
|
|
935
|
+
.key-headline {
|
|
936
|
+
font-family: var(--serif-display);
|
|
937
|
+
font-size: 64px;
|
|
938
|
+
font-weight: 700;
|
|
939
|
+
line-height: 1.04;
|
|
940
|
+
letter-spacing: -0.018em;
|
|
941
|
+
color: var(--ink);
|
|
942
|
+
word-break: break-word;
|
|
943
|
+
overflow-wrap: anywhere;
|
|
944
|
+
}
|
|
945
|
+
.key-headline em {
|
|
946
|
+
font-style: italic;
|
|
947
|
+
color: var(--accent);
|
|
948
|
+
}
|
|
949
|
+
.key-deck {
|
|
950
|
+
font-family: var(--serif-display);
|
|
951
|
+
font-style: italic;
|
|
952
|
+
font-size: 24px;
|
|
953
|
+
line-height: 1.4;
|
|
954
|
+
color: var(--ink-soft);
|
|
955
|
+
margin-top: 18px;
|
|
956
|
+
max-width: 880px;
|
|
957
|
+
}
|
|
958
|
+
.key-byline {
|
|
959
|
+
font-family: var(--sans);
|
|
960
|
+
font-size: 11px;
|
|
961
|
+
letter-spacing: 0.22em;
|
|
962
|
+
text-transform: uppercase;
|
|
963
|
+
color: var(--ink-mid);
|
|
964
|
+
margin-top: 22px;
|
|
965
|
+
}
|
|
966
|
+
.key-byline em { color: var(--ink-soft); font-style: italic; text-transform: none; letter-spacing: 0; }
|
|
967
|
+
|
|
968
|
+
/* Cover · centered display title */
|
|
969
|
+
.key-slide-cover .key-body {
|
|
970
|
+
align-items: center;
|
|
971
|
+
text-align: center;
|
|
972
|
+
padding: 64px 56px;
|
|
973
|
+
}
|
|
974
|
+
.key-slide-cover .key-headline { font-size: 84px; }
|
|
975
|
+
.key-slide-cover .key-deck { text-align: center; }
|
|
976
|
+
|
|
977
|
+
@media (max-width: 1080px) {
|
|
978
|
+
.key-slide-cover .key-headline { font-size: 56px; }
|
|
979
|
+
.key-headline { font-size: 44px; }
|
|
980
|
+
.key-deck { font-size: 19px; }
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/* Agenda · centered short list */
|
|
984
|
+
.key-agenda {
|
|
985
|
+
list-style: none;
|
|
986
|
+
margin: 18px 0 0;
|
|
987
|
+
padding: 0;
|
|
988
|
+
display: flex;
|
|
989
|
+
flex-direction: column;
|
|
990
|
+
gap: 8px;
|
|
991
|
+
counter-reset: kagenda;
|
|
992
|
+
}
|
|
993
|
+
.key-agenda li {
|
|
994
|
+
display: grid;
|
|
995
|
+
grid-template-columns: 64px 1fr;
|
|
996
|
+
gap: 14px;
|
|
997
|
+
align-items: baseline;
|
|
998
|
+
padding: 10px 0;
|
|
999
|
+
border-bottom: 1px solid var(--rule);
|
|
1000
|
+
counter-increment: kagenda;
|
|
1001
|
+
}
|
|
1002
|
+
.key-agenda li::before {
|
|
1003
|
+
content: counter(kagenda, decimal-leading-zero);
|
|
1004
|
+
font-family: var(--serif-display);
|
|
1005
|
+
font-style: italic;
|
|
1006
|
+
font-size: 32px;
|
|
1007
|
+
color: var(--accent);
|
|
1008
|
+
line-height: 1;
|
|
1009
|
+
}
|
|
1010
|
+
.key-agenda li span {
|
|
1011
|
+
font-family: var(--serif-display);
|
|
1012
|
+
font-size: 22px;
|
|
1013
|
+
line-height: 1.32;
|
|
1014
|
+
color: var(--ink);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/* Milestone slide · headline + body + optional big callout */
|
|
1018
|
+
.key-milestone {
|
|
1019
|
+
display: grid;
|
|
1020
|
+
grid-template-columns: minmax(0, 1.8fr) minmax(0, 1fr);
|
|
1021
|
+
gap: 48px;
|
|
1022
|
+
align-items: center;
|
|
1023
|
+
flex: 1;
|
|
1024
|
+
}
|
|
1025
|
+
.key-milestone.no-side {
|
|
1026
|
+
grid-template-columns: 1fr;
|
|
1027
|
+
}
|
|
1028
|
+
.key-milestone-body {
|
|
1029
|
+
font-family: var(--serif-display);
|
|
1030
|
+
font-style: italic;
|
|
1031
|
+
font-size: 22px;
|
|
1032
|
+
line-height: 1.45;
|
|
1033
|
+
color: var(--ink-soft);
|
|
1034
|
+
margin-top: 16px;
|
|
1035
|
+
}
|
|
1036
|
+
.key-callout-num {
|
|
1037
|
+
font-family: var(--serif-display);
|
|
1038
|
+
font-weight: 900;
|
|
1039
|
+
font-size: 132px;
|
|
1040
|
+
line-height: 0.94;
|
|
1041
|
+
letter-spacing: -0.04em;
|
|
1042
|
+
color: var(--accent);
|
|
1043
|
+
word-break: break-word;
|
|
1044
|
+
overflow-wrap: anywhere;
|
|
1045
|
+
text-align: center;
|
|
1046
|
+
}
|
|
1047
|
+
.key-callout-cap {
|
|
1048
|
+
font-family: var(--serif-display);
|
|
1049
|
+
font-style: italic;
|
|
1050
|
+
font-size: 16px;
|
|
1051
|
+
line-height: 1.4;
|
|
1052
|
+
color: var(--ink-mid);
|
|
1053
|
+
margin-top: 8px;
|
|
1054
|
+
text-align: center;
|
|
1055
|
+
}
|
|
1056
|
+
.key-roman {
|
|
1057
|
+
text-align: center;
|
|
1058
|
+
align-self: center;
|
|
1059
|
+
}
|
|
1060
|
+
.key-roman-num {
|
|
1061
|
+
font-family: var(--serif-display);
|
|
1062
|
+
font-style: italic;
|
|
1063
|
+
font-weight: 700;
|
|
1064
|
+
font-size: 192px;
|
|
1065
|
+
line-height: 0.86;
|
|
1066
|
+
letter-spacing: -0.04em;
|
|
1067
|
+
color: var(--accent);
|
|
1068
|
+
}
|
|
1069
|
+
.key-roman-cap {
|
|
1070
|
+
font-family: var(--mono);
|
|
1071
|
+
font-size: 11px;
|
|
1072
|
+
font-weight: 700;
|
|
1073
|
+
letter-spacing: 0.22em;
|
|
1074
|
+
text-transform: uppercase;
|
|
1075
|
+
color: var(--ink-mid);
|
|
1076
|
+
margin-top: 14px;
|
|
1077
|
+
}
|
|
1078
|
+
.key-cover-mark { /* override the overly-cramped quote · keep tasteful */ }
|
|
1079
|
+
.key-cover-stats {
|
|
1080
|
+
display: flex;
|
|
1081
|
+
gap: 28px;
|
|
1082
|
+
margin-top: 28px;
|
|
1083
|
+
padding-top: 18px;
|
|
1084
|
+
border-top: 1px solid var(--rule);
|
|
1085
|
+
}
|
|
1086
|
+
.key-cover-stat {
|
|
1087
|
+
text-align: center;
|
|
1088
|
+
}
|
|
1089
|
+
.key-cover-stat-num {
|
|
1090
|
+
font-family: var(--serif-display);
|
|
1091
|
+
font-style: italic;
|
|
1092
|
+
font-weight: 700;
|
|
1093
|
+
font-size: 44px;
|
|
1094
|
+
line-height: 1;
|
|
1095
|
+
letter-spacing: -0.02em;
|
|
1096
|
+
color: var(--accent);
|
|
1097
|
+
}
|
|
1098
|
+
.key-cover-stat-cap {
|
|
1099
|
+
font-family: var(--mono);
|
|
1100
|
+
font-size: 10px;
|
|
1101
|
+
font-weight: 700;
|
|
1102
|
+
letter-spacing: 0.22em;
|
|
1103
|
+
text-transform: uppercase;
|
|
1104
|
+
color: var(--ink-mid);
|
|
1105
|
+
margin-top: 8px;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/* Bullets (talking points / verification) · big sparse list */
|
|
1109
|
+
.key-bullets {
|
|
1110
|
+
list-style: none;
|
|
1111
|
+
margin: 24px 0 0;
|
|
1112
|
+
padding: 0;
|
|
1113
|
+
display: flex;
|
|
1114
|
+
flex-direction: column;
|
|
1115
|
+
gap: 16px;
|
|
1116
|
+
counter-reset: kbullets;
|
|
1117
|
+
}
|
|
1118
|
+
.key-bullets li {
|
|
1119
|
+
display: grid;
|
|
1120
|
+
grid-template-columns: 56px 1fr;
|
|
1121
|
+
gap: 18px;
|
|
1122
|
+
align-items: baseline;
|
|
1123
|
+
counter-increment: kbullets;
|
|
1124
|
+
}
|
|
1125
|
+
.key-bullets li::before {
|
|
1126
|
+
content: counter(kbullets, decimal-leading-zero);
|
|
1127
|
+
font-family: var(--serif-display);
|
|
1128
|
+
font-style: italic;
|
|
1129
|
+
font-weight: 700;
|
|
1130
|
+
font-size: 32px;
|
|
1131
|
+
color: var(--accent);
|
|
1132
|
+
line-height: 1;
|
|
1133
|
+
}
|
|
1134
|
+
.key-bullets li span {
|
|
1135
|
+
font-family: var(--serif-display);
|
|
1136
|
+
font-size: 22px;
|
|
1137
|
+
line-height: 1.36;
|
|
1138
|
+
color: var(--ink);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/* Timeline · keynote register · italic roman markers */
|
|
1142
|
+
.key-timeline {
|
|
1143
|
+
flex: 1;
|
|
1144
|
+
display: grid;
|
|
1145
|
+
grid-template-columns: repeat(var(--n, 3), 1fr);
|
|
1146
|
+
gap: 0;
|
|
1147
|
+
margin-top: 36px;
|
|
1148
|
+
position: relative;
|
|
1149
|
+
}
|
|
1150
|
+
.key-timeline::before {
|
|
1151
|
+
content: "";
|
|
1152
|
+
position: absolute;
|
|
1153
|
+
top: 92px;
|
|
1154
|
+
left: 28px;
|
|
1155
|
+
right: 28px;
|
|
1156
|
+
height: 1px;
|
|
1157
|
+
background: var(--rule-strong);
|
|
1158
|
+
}
|
|
1159
|
+
.key-tl-cell {
|
|
1160
|
+
display: flex;
|
|
1161
|
+
flex-direction: column;
|
|
1162
|
+
align-items: flex-start;
|
|
1163
|
+
padding: 0 18px;
|
|
1164
|
+
}
|
|
1165
|
+
.key-tl-cell:first-child { padding-left: 0; }
|
|
1166
|
+
.key-tl-cell:last-child { padding-right: 0; }
|
|
1167
|
+
.key-tl-period {
|
|
1168
|
+
font-family: var(--mono);
|
|
1169
|
+
font-size: 10px;
|
|
1170
|
+
font-weight: 700;
|
|
1171
|
+
letter-spacing: 0.22em;
|
|
1172
|
+
text-transform: uppercase;
|
|
1173
|
+
color: var(--accent);
|
|
1174
|
+
margin-bottom: 14px;
|
|
1175
|
+
min-height: 14px;
|
|
1176
|
+
}
|
|
1177
|
+
.key-tl-marker {
|
|
1178
|
+
font-family: var(--serif-display);
|
|
1179
|
+
font-style: italic;
|
|
1180
|
+
font-weight: 700;
|
|
1181
|
+
font-size: 64px;
|
|
1182
|
+
line-height: 0.86;
|
|
1183
|
+
letter-spacing: -0.03em;
|
|
1184
|
+
color: var(--accent);
|
|
1185
|
+
background: var(--paper);
|
|
1186
|
+
padding-right: 14px;
|
|
1187
|
+
margin-bottom: 18px;
|
|
1188
|
+
z-index: 1;
|
|
1189
|
+
}
|
|
1190
|
+
.key-tl-title {
|
|
1191
|
+
font-family: var(--serif-display);
|
|
1192
|
+
font-size: 22px;
|
|
1193
|
+
font-weight: 700;
|
|
1194
|
+
line-height: 1.2;
|
|
1195
|
+
color: var(--ink);
|
|
1196
|
+
margin-bottom: 8px;
|
|
1197
|
+
letter-spacing: -0.01em;
|
|
1198
|
+
}
|
|
1199
|
+
.key-tl-body {
|
|
1200
|
+
font-family: var(--serif-display);
|
|
1201
|
+
font-style: italic;
|
|
1202
|
+
font-size: 15px;
|
|
1203
|
+
line-height: 1.45;
|
|
1204
|
+
color: var(--ink-soft);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/* Data · ranked bars · keynote register */
|
|
1208
|
+
.key-bars {
|
|
1209
|
+
display: flex;
|
|
1210
|
+
flex-direction: column;
|
|
1211
|
+
gap: 18px;
|
|
1212
|
+
margin-top: 18px;
|
|
1213
|
+
}
|
|
1214
|
+
.key-bar {
|
|
1215
|
+
display: grid;
|
|
1216
|
+
grid-template-columns: 200px 1fr 100px;
|
|
1217
|
+
gap: 16px;
|
|
1218
|
+
align-items: center;
|
|
1219
|
+
}
|
|
1220
|
+
.key-bar-label {
|
|
1221
|
+
font-family: var(--serif-display);
|
|
1222
|
+
font-size: 19px;
|
|
1223
|
+
color: var(--ink);
|
|
1224
|
+
}
|
|
1225
|
+
.key-bar-track {
|
|
1226
|
+
height: 14px;
|
|
1227
|
+
background: var(--paper-soft);
|
|
1228
|
+
position: relative;
|
|
1229
|
+
}
|
|
1230
|
+
.key-bar-fill {
|
|
1231
|
+
position: absolute;
|
|
1232
|
+
inset: 0 auto 0 0;
|
|
1233
|
+
background: var(--accent);
|
|
1234
|
+
}
|
|
1235
|
+
.key-bar-value {
|
|
1236
|
+
font-family: var(--mono);
|
|
1237
|
+
font-size: 13px;
|
|
1238
|
+
color: var(--ink-soft);
|
|
1239
|
+
text-align: right;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/* Director voices · keynote register */
|
|
1243
|
+
.key-voices {
|
|
1244
|
+
display: grid;
|
|
1245
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
1246
|
+
gap: 24px 28px;
|
|
1247
|
+
margin-top: 18px;
|
|
1248
|
+
align-content: start;
|
|
1249
|
+
}
|
|
1250
|
+
.key-voices.is-three-up { grid-template-columns: repeat(3, 1fr); }
|
|
1251
|
+
.key-voices.is-two-up { grid-template-columns: repeat(2, 1fr); }
|
|
1252
|
+
.key-voice {
|
|
1253
|
+
border-top: 1px solid var(--rule-strong);
|
|
1254
|
+
padding-top: 14px;
|
|
1255
|
+
}
|
|
1256
|
+
.key-voice-name {
|
|
1257
|
+
font-family: var(--serif-display);
|
|
1258
|
+
font-style: italic;
|
|
1259
|
+
font-weight: 700;
|
|
1260
|
+
font-size: 28px;
|
|
1261
|
+
line-height: 1.1;
|
|
1262
|
+
color: var(--accent);
|
|
1263
|
+
letter-spacing: -0.01em;
|
|
1264
|
+
}
|
|
1265
|
+
.key-voice-role {
|
|
1266
|
+
font-family: var(--mono);
|
|
1267
|
+
font-size: 10px;
|
|
1268
|
+
font-weight: 700;
|
|
1269
|
+
letter-spacing: 0.22em;
|
|
1270
|
+
text-transform: uppercase;
|
|
1271
|
+
color: var(--ink-mid);
|
|
1272
|
+
margin-top: 6px;
|
|
1273
|
+
}
|
|
1274
|
+
.key-voice-stance {
|
|
1275
|
+
font-family: var(--serif-display);
|
|
1276
|
+
font-style: italic;
|
|
1277
|
+
font-size: 16px;
|
|
1278
|
+
line-height: 1.36;
|
|
1279
|
+
color: var(--ink);
|
|
1280
|
+
margin-top: 12px;
|
|
1281
|
+
}
|
|
1282
|
+
.key-voice-position {
|
|
1283
|
+
font-family: var(--serif-display);
|
|
1284
|
+
font-size: 14px;
|
|
1285
|
+
line-height: 1.52;
|
|
1286
|
+
color: var(--ink-soft);
|
|
1287
|
+
margin-top: 10px;
|
|
1288
|
+
}
|
|
1289
|
+
.key-voice-quote {
|
|
1290
|
+
font-family: var(--serif-display);
|
|
1291
|
+
font-style: italic;
|
|
1292
|
+
font-size: 14px;
|
|
1293
|
+
line-height: 1.46;
|
|
1294
|
+
color: var(--ink-mid);
|
|
1295
|
+
margin-top: 12px;
|
|
1296
|
+
padding-top: 10px;
|
|
1297
|
+
border-top: 1px solid var(--rule);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/* Where we agreed · keynote */
|
|
1301
|
+
.key-aligns {
|
|
1302
|
+
list-style: none;
|
|
1303
|
+
margin: 18px 0 0;
|
|
1304
|
+
padding: 0;
|
|
1305
|
+
display: flex;
|
|
1306
|
+
flex-direction: column;
|
|
1307
|
+
gap: 22px;
|
|
1308
|
+
}
|
|
1309
|
+
.key-align {
|
|
1310
|
+
border-top: 1px solid var(--rule);
|
|
1311
|
+
padding-top: 16px;
|
|
1312
|
+
}
|
|
1313
|
+
.key-align-point {
|
|
1314
|
+
font-family: var(--serif-display);
|
|
1315
|
+
font-size: 26px;
|
|
1316
|
+
font-weight: 700;
|
|
1317
|
+
line-height: 1.18;
|
|
1318
|
+
color: var(--ink);
|
|
1319
|
+
letter-spacing: -0.012em;
|
|
1320
|
+
}
|
|
1321
|
+
.key-align-point em { color: var(--accent); font-style: italic; }
|
|
1322
|
+
.key-align-note {
|
|
1323
|
+
font-family: var(--serif-display);
|
|
1324
|
+
font-style: italic;
|
|
1325
|
+
font-size: 14px;
|
|
1326
|
+
line-height: 1.5;
|
|
1327
|
+
color: var(--ink-soft);
|
|
1328
|
+
margin-top: 10px;
|
|
1329
|
+
}
|
|
1330
|
+
.key-chips {
|
|
1331
|
+
display: flex;
|
|
1332
|
+
gap: 6px;
|
|
1333
|
+
flex-wrap: wrap;
|
|
1334
|
+
margin-top: 12px;
|
|
1335
|
+
}
|
|
1336
|
+
.key-chip {
|
|
1337
|
+
font-family: var(--mono);
|
|
1338
|
+
font-size: 10px;
|
|
1339
|
+
font-weight: 700;
|
|
1340
|
+
letter-spacing: 0.22em;
|
|
1341
|
+
text-transform: uppercase;
|
|
1342
|
+
color: var(--accent);
|
|
1343
|
+
border: 1px solid var(--rule-strong);
|
|
1344
|
+
padding: 4px 10px;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
/* Where we split · keynote */
|
|
1348
|
+
.key-splits {
|
|
1349
|
+
display: flex;
|
|
1350
|
+
flex-direction: column;
|
|
1351
|
+
gap: 22px;
|
|
1352
|
+
margin-top: 14px;
|
|
1353
|
+
}
|
|
1354
|
+
.key-split {
|
|
1355
|
+
border-top: 1px solid var(--rule);
|
|
1356
|
+
padding-top: 16px;
|
|
1357
|
+
}
|
|
1358
|
+
.key-split-hinge {
|
|
1359
|
+
font-family: var(--serif-display);
|
|
1360
|
+
font-style: italic;
|
|
1361
|
+
font-size: 22px;
|
|
1362
|
+
font-weight: 700;
|
|
1363
|
+
line-height: 1.22;
|
|
1364
|
+
color: var(--ink);
|
|
1365
|
+
letter-spacing: -0.012em;
|
|
1366
|
+
margin-bottom: 14px;
|
|
1367
|
+
}
|
|
1368
|
+
.key-split-hinge em { color: var(--accent); }
|
|
1369
|
+
.key-split-sides {
|
|
1370
|
+
display: grid;
|
|
1371
|
+
grid-template-columns: 1fr 1fr;
|
|
1372
|
+
gap: 18px;
|
|
1373
|
+
}
|
|
1374
|
+
.key-split-sides.is-three { grid-template-columns: 1fr 1fr 1fr; }
|
|
1375
|
+
.key-split-side {
|
|
1376
|
+
border-top: 1px solid var(--rule-strong);
|
|
1377
|
+
padding-top: 10px;
|
|
1378
|
+
}
|
|
1379
|
+
.key-split-side-label {
|
|
1380
|
+
font-family: var(--mono);
|
|
1381
|
+
font-size: 10px;
|
|
1382
|
+
font-weight: 700;
|
|
1383
|
+
letter-spacing: 0.22em;
|
|
1384
|
+
text-transform: uppercase;
|
|
1385
|
+
color: var(--accent);
|
|
1386
|
+
margin-bottom: 8px;
|
|
1387
|
+
}
|
|
1388
|
+
.key-split-side-stance {
|
|
1389
|
+
font-family: var(--serif-display);
|
|
1390
|
+
font-size: 14px;
|
|
1391
|
+
line-height: 1.5;
|
|
1392
|
+
color: var(--ink);
|
|
1393
|
+
}
|
|
1394
|
+
.key-split-side-names {
|
|
1395
|
+
margin-top: 10px;
|
|
1396
|
+
display: flex;
|
|
1397
|
+
gap: 4px;
|
|
1398
|
+
flex-wrap: wrap;
|
|
1399
|
+
}
|
|
1400
|
+
.key-split-resolve {
|
|
1401
|
+
font-family: var(--serif-display);
|
|
1402
|
+
font-style: italic;
|
|
1403
|
+
font-size: 13px;
|
|
1404
|
+
line-height: 1.46;
|
|
1405
|
+
color: var(--ink-mid);
|
|
1406
|
+
margin-top: 12px;
|
|
1407
|
+
}
|
|
1408
|
+
.key-split-resolve b {
|
|
1409
|
+
font-family: var(--mono);
|
|
1410
|
+
font-style: normal;
|
|
1411
|
+
font-size: 10px;
|
|
1412
|
+
font-weight: 700;
|
|
1413
|
+
letter-spacing: 0.22em;
|
|
1414
|
+
text-transform: uppercase;
|
|
1415
|
+
color: var(--accent);
|
|
1416
|
+
margin-right: 8px;
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/* Takeaway slide · big italic quote */
|
|
1420
|
+
.key-slide-takeaway .key-body {
|
|
1421
|
+
align-items: center;
|
|
1422
|
+
text-align: center;
|
|
1423
|
+
}
|
|
1424
|
+
.key-takeaway-mark {
|
|
1425
|
+
font-family: var(--serif-display);
|
|
1426
|
+
font-style: italic;
|
|
1427
|
+
font-size: 96px;
|
|
1428
|
+
color: var(--accent);
|
|
1429
|
+
line-height: 0.5;
|
|
1430
|
+
margin-bottom: 28px;
|
|
1431
|
+
}
|
|
1432
|
+
.key-takeaway-text {
|
|
1433
|
+
font-family: var(--serif-display);
|
|
1434
|
+
font-style: italic;
|
|
1435
|
+
font-size: 56px;
|
|
1436
|
+
font-weight: 500;
|
|
1437
|
+
line-height: 1.12;
|
|
1438
|
+
letter-spacing: -0.014em;
|
|
1439
|
+
color: var(--ink);
|
|
1440
|
+
max-width: 940px;
|
|
1441
|
+
word-break: break-word;
|
|
1442
|
+
}
|
|
1443
|
+
@media (max-width: 1080px) {
|
|
1444
|
+
.key-takeaway-text { font-size: 38px; }
|
|
1445
|
+
}
|
|
1446
|
+
.key-takeaway-cite {
|
|
1447
|
+
font-family: var(--sans);
|
|
1448
|
+
font-size: 11px;
|
|
1449
|
+
letter-spacing: 0.22em;
|
|
1450
|
+
text-transform: uppercase;
|
|
1451
|
+
color: var(--ink-mid);
|
|
1452
|
+
margin-top: 28px;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
/* ─── Thumb strip · all slides preview at the bottom ─────────── */
|
|
1456
|
+
.ppt-thumbs {
|
|
1457
|
+
display: flex;
|
|
1458
|
+
gap: 8px;
|
|
1459
|
+
padding: 12px 28px 18px;
|
|
1460
|
+
overflow-x: auto;
|
|
1461
|
+
background: rgba(14, 14, 16, 0.92);
|
|
1462
|
+
backdrop-filter: blur(10px);
|
|
1463
|
+
-webkit-backdrop-filter: blur(10px);
|
|
1464
|
+
border-top: 1px solid rgba(244, 239, 223, 0.10);
|
|
1465
|
+
position: sticky;
|
|
1466
|
+
bottom: 0;
|
|
1467
|
+
z-index: 10;
|
|
1468
|
+
}
|
|
1469
|
+
.ppt-thumb {
|
|
1470
|
+
flex: 0 0 auto;
|
|
1471
|
+
width: 116px;
|
|
1472
|
+
aspect-ratio: 16 / 9;
|
|
1473
|
+
background: rgba(244, 239, 223, 0.06);
|
|
1474
|
+
border: 1px solid rgba(244, 239, 223, 0.16);
|
|
1475
|
+
cursor: pointer;
|
|
1476
|
+
transition: border-color 0.12s, transform 0.12s;
|
|
1477
|
+
color: #C8C2AE;
|
|
1478
|
+
padding: 8px 10px;
|
|
1479
|
+
display: flex;
|
|
1480
|
+
flex-direction: column;
|
|
1481
|
+
justify-content: space-between;
|
|
1482
|
+
overflow: hidden;
|
|
1483
|
+
}
|
|
1484
|
+
.ppt-thumb:hover {
|
|
1485
|
+
border-color: rgba(244, 239, 223, 0.40);
|
|
1486
|
+
transform: translateY(-1px);
|
|
1487
|
+
}
|
|
1488
|
+
.ppt-thumb.is-active {
|
|
1489
|
+
border-color: #D4A24A;
|
|
1490
|
+
background: rgba(212, 162, 74, 0.12);
|
|
1491
|
+
}
|
|
1492
|
+
.ppt-thumb-num {
|
|
1493
|
+
font-family: var(--mono);
|
|
1494
|
+
font-size: 9px;
|
|
1495
|
+
letter-spacing: 0.16em;
|
|
1496
|
+
color: #8B8470;
|
|
1497
|
+
}
|
|
1498
|
+
.ppt-thumb.is-active .ppt-thumb-num { color: #D4A24A; }
|
|
1499
|
+
.ppt-thumb-title {
|
|
1500
|
+
font-family: var(--serif-display);
|
|
1501
|
+
font-size: 10px;
|
|
1502
|
+
font-weight: 700;
|
|
1503
|
+
line-height: 1.2;
|
|
1504
|
+
color: #F4EFDF;
|
|
1505
|
+
overflow: hidden;
|
|
1506
|
+
text-overflow: ellipsis;
|
|
1507
|
+
display: -webkit-box;
|
|
1508
|
+
-webkit-line-clamp: 2;
|
|
1509
|
+
-webkit-box-orient: vertical;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/* ─── Fullscreen mode · hide chrome ─────────────────────────── */
|
|
1513
|
+
body.is-fullscreen .ppt-top-bar,
|
|
1514
|
+
body.is-fullscreen .ppt-thumbs { display: none; }
|
|
1515
|
+
body.is-fullscreen .ppt-stage { padding: 0; min-height: 100vh; }
|
|
1516
|
+
body.is-fullscreen .ppt-deck { max-width: none; height: 100vh; aspect-ratio: auto; box-shadow: none; }
|
|
1517
|
+
|
|
1518
|
+
/* ─── States · loading / error ─────────────────────────── */
|
|
1519
|
+
.ppt-state {
|
|
1520
|
+
max-width: 560px;
|
|
1521
|
+
margin: 80px auto;
|
|
1522
|
+
padding: 48px 36px;
|
|
1523
|
+
text-align: center;
|
|
1524
|
+
background: #FFFFFF;
|
|
1525
|
+
box-shadow: var(--shadow-slide);
|
|
1526
|
+
color: var(--ink);
|
|
1527
|
+
}
|
|
1528
|
+
.ppt-state-mark {
|
|
1529
|
+
font-family: var(--mono);
|
|
1530
|
+
font-size: 10px;
|
|
1531
|
+
letter-spacing: 0.18em;
|
|
1532
|
+
text-transform: uppercase;
|
|
1533
|
+
color: #14110B;
|
|
1534
|
+
border: 1px solid #14110B;
|
|
1535
|
+
padding: 5px 14px;
|
|
1536
|
+
display: inline-block;
|
|
1537
|
+
margin-bottom: 16px;
|
|
1538
|
+
font-weight: 700;
|
|
1539
|
+
}
|
|
1540
|
+
.ppt-state-title {
|
|
1541
|
+
font-family: var(--serif-display);
|
|
1542
|
+
font-size: 24px;
|
|
1543
|
+
font-weight: 700;
|
|
1544
|
+
color: #14110B;
|
|
1545
|
+
margin-bottom: 10px;
|
|
1546
|
+
line-height: 1.2;
|
|
1547
|
+
text-transform: uppercase;
|
|
1548
|
+
letter-spacing: 0.02em;
|
|
1549
|
+
}
|
|
1550
|
+
.ppt-state-body {
|
|
1551
|
+
font-family: var(--serif);
|
|
1552
|
+
font-size: 14px;
|
|
1553
|
+
color: #2D281D;
|
|
1554
|
+
line-height: 1.6;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
@media print {
|
|
1558
|
+
.ppt-top-bar, .ppt-thumbs { display: none; }
|
|
1559
|
+
body, html { background: white; }
|
|
1560
|
+
.ppt-stage { padding: 0; min-height: 0; }
|
|
1561
|
+
.ppt-deck { box-shadow: none; max-width: none; aspect-ratio: auto; }
|
|
1562
|
+
.ppt-slide { display: flex !important; page-break-after: always; position: relative; min-height: 100vh; }
|
|
1563
|
+
.ppt-slide:last-child { page-break-after: auto; }
|
|
1564
|
+
}
|
|
1565
|
+
</style>
|
|
1566
|
+
</head>
|
|
1567
|
+
<body>
|
|
1568
|
+
|
|
1569
|
+
<header class="ppt-top-bar" data-ppt-chrome>
|
|
1570
|
+
<a href="/" class="ppt-crumb">PrivateBoard <span class="ppt-crumb-accent">· slides</span></a>
|
|
1571
|
+
<div class="ppt-actions">
|
|
1572
|
+
<span class="ppt-variant-badge" data-ppt-variant-badge>Loading…</span>
|
|
1573
|
+
<span class="ppt-counter" data-ppt-counter>— / —</span>
|
|
1574
|
+
<button type="button" class="ppt-btn" data-ppt-fullscreen title="Fullscreen (F)">
|
|
1575
|
+
<span class="glyph">⛶</span>FS
|
|
1576
|
+
</button>
|
|
1577
|
+
<button type="button" class="ppt-btn" data-ppt-png title="Download active slide as PNG">
|
|
1578
|
+
<span class="glyph">↓</span>PNG
|
|
1579
|
+
</button>
|
|
1580
|
+
<button type="button" class="ppt-btn" data-ppt-print title="Print all slides as PDF">
|
|
1581
|
+
<span class="glyph">↓</span>PDF
|
|
1582
|
+
</button>
|
|
1583
|
+
</div>
|
|
1584
|
+
</header>
|
|
1585
|
+
|
|
1586
|
+
<main data-ppt-root>
|
|
1587
|
+
<div class="ppt-state">
|
|
1588
|
+
<div class="ppt-state-mark">Loading</div>
|
|
1589
|
+
<div class="ppt-state-title">Loading deck…</div>
|
|
1590
|
+
<div class="ppt-state-body">Fetching the slides for this brief.</div>
|
|
1591
|
+
</div>
|
|
1592
|
+
</main>
|
|
1593
|
+
|
|
1594
|
+
<script>
|
|
1595
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
1596
|
+
PPT renderer · two distinct templates (Anthropic-essay / Keynote)
|
|
1597
|
+
picked deterministically per brief id. Each renders 7-12 slides.
|
|
1598
|
+
Reader steps through with arrow keys, click navigation, thumb
|
|
1599
|
+
strip, or fullscreen mode.
|
|
1600
|
+
────────────────────────────────────────────────────────────── */
|
|
1601
|
+
(function () {
|
|
1602
|
+
const params = new URLSearchParams(location.search);
|
|
1603
|
+
const briefId = (params.get("b") || "").trim();
|
|
1604
|
+
const roomId = (params.get("r") || "").trim();
|
|
1605
|
+
const root = document.querySelector("[data-ppt-root]");
|
|
1606
|
+
const variantBadge = document.querySelector("[data-ppt-variant-badge]");
|
|
1607
|
+
const counterEl = document.querySelector("[data-ppt-counter]");
|
|
1608
|
+
|
|
1609
|
+
function escape(s) {
|
|
1610
|
+
return String(s == null ? "" : s)
|
|
1611
|
+
.replace(/&/g, "&")
|
|
1612
|
+
.replace(/</g, "<")
|
|
1613
|
+
.replace(/>/g, ">")
|
|
1614
|
+
.replace(/"/g, """)
|
|
1615
|
+
.replace(/'/g, "'");
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function showState(mark, title, body) {
|
|
1619
|
+
root.innerHTML = `
|
|
1620
|
+
<div class="ppt-state">
|
|
1621
|
+
<div class="ppt-state-mark">${escape(mark)}</div>
|
|
1622
|
+
<div class="ppt-state-title">${escape(title)}</div>
|
|
1623
|
+
<div class="ppt-state-body">${escape(body)}</div>
|
|
1624
|
+
</div>`;
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
async function loadBrief() {
|
|
1628
|
+
let url;
|
|
1629
|
+
if (briefId) url = `/api/briefs/${encodeURIComponent(briefId)}`;
|
|
1630
|
+
else if (roomId) url = `/api/rooms/${encodeURIComponent(roomId)}/brief`;
|
|
1631
|
+
else { showState("Missing query", "No brief specified", "Add ?b=<briefId> or ?r=<roomId> to the URL."); return null; }
|
|
1632
|
+
const res = await fetch(url);
|
|
1633
|
+
if (!res.ok) {
|
|
1634
|
+
const e = await res.json().catch(() => ({}));
|
|
1635
|
+
showState("Not found", "Brief not found", e.error || "The requested brief doesn't exist or is no longer available.");
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
return await res.json();
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
/** Pick "anthropic" or "keynote" deterministically from the brief
|
|
1642
|
+
* id · same id always renders the same template. The
|
|
1643
|
+
* `?v=anthropic|keynote` URL parameter forces a specific template. */
|
|
1644
|
+
function pickVariant(id) {
|
|
1645
|
+
const force = (params.get("v") || "").trim().toLowerCase();
|
|
1646
|
+
if (force === "anthropic" || force === "keynote") return force;
|
|
1647
|
+
const s = String(id || "");
|
|
1648
|
+
if (!s) return "anthropic";
|
|
1649
|
+
let h = 0;
|
|
1650
|
+
for (let i = 0; i < s.length; i++) {
|
|
1651
|
+
h = ((h << 5) - h) + s.charCodeAt(i);
|
|
1652
|
+
h |= 0;
|
|
1653
|
+
}
|
|
1654
|
+
return (Math.abs(h) % 2) === 0 ? "anthropic" : "keynote";
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function formatDate(footerTag) {
|
|
1658
|
+
const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
1659
|
+
const m = String(footerTag || "").match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
1660
|
+
const target = m ? new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00`) : new Date();
|
|
1661
|
+
const d = isNaN(target.getTime()) ? new Date() : target;
|
|
1662
|
+
return {
|
|
1663
|
+
long: `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`,
|
|
1664
|
+
monthYear: `${months[d.getMonth()]} ${d.getFullYear()}`,
|
|
1665
|
+
year: String(d.getFullYear()),
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/** Strict stat picker · only short numeric callouts. */
|
|
1670
|
+
function pickStat(milestone) {
|
|
1671
|
+
if (!milestone || !milestone.callout) return null;
|
|
1672
|
+
const c = String(milestone.callout).trim();
|
|
1673
|
+
if (!c || c.length > 14) return null;
|
|
1674
|
+
return { num: c, cap: milestone.title || milestone.period || "" };
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
/** Lower-case roman numerals i / ii / iii / iv / v · used for
|
|
1678
|
+
* side-pane phase indicators when no numeric callout exists. */
|
|
1679
|
+
function toRoman(n) {
|
|
1680
|
+
const map = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix"];
|
|
1681
|
+
return map[n] || String(n);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
/** Count meaningful items for the cover stat-strip · falls back
|
|
1685
|
+
* silently when the brief has fewer signals to surface. */
|
|
1686
|
+
function coverStats(m) {
|
|
1687
|
+
const out = [];
|
|
1688
|
+
const ms = (m.milestones || []).filter((x) => x && (x.title || x.body || x.callout));
|
|
1689
|
+
if (ms.length) out.push({ num: String(ms.length).padStart(2, "0"), cap: ms.length === 1 ? "Milestone" : "Milestones" });
|
|
1690
|
+
const tp = (m.talkingPoints && m.talkingPoints.bullets) ? m.talkingPoints.bullets.length : 0;
|
|
1691
|
+
if (tp) out.push({ num: String(tp).padStart(2, "0"), cap: tp === 1 ? "Recommendation" : "Recommendations" });
|
|
1692
|
+
const ver = (m.verification && m.verification.bullets) ? m.verification.bullets.length : 0;
|
|
1693
|
+
if (ver) out.push({ num: String(ver).padStart(2, "0"), cap: ver === 1 ? "Watch item" : "Watch items" });
|
|
1694
|
+
return out.slice(0, 3);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/** Split a milestone body into 2-3 bullet phrases on natural
|
|
1698
|
+
* punctuation breaks (sentence end, semicolon, em-dash). When
|
|
1699
|
+
* the body is short / unsplittable, returns a single-item
|
|
1700
|
+
* array · the renderer prints it as one bullet. */
|
|
1701
|
+
function splitToBullets(body, max) {
|
|
1702
|
+
const s = String(body || "").trim();
|
|
1703
|
+
if (!s) return [];
|
|
1704
|
+
const cap = max || 3;
|
|
1705
|
+
// Split on sentence enders + ; + em-dash · keep splits >= 16
|
|
1706
|
+
// chars so we don't fragment on stray punctuation.
|
|
1707
|
+
const parts = s.split(/(?<=[.!?。!?])\s+|\s*;\s+|\s*—\s+/)
|
|
1708
|
+
.map((x) => x.trim())
|
|
1709
|
+
.filter((x) => x.length >= 16);
|
|
1710
|
+
if (parts.length === 0) return [s];
|
|
1711
|
+
return parts.slice(0, cap);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/* ═════════════════════════════════════════════════════════════
|
|
1715
|
+
Slide builders · each returns one slide's HTML string ·
|
|
1716
|
+
wrapped in a .ppt-slide container.
|
|
1717
|
+
═════════════════════════════════════════════════════════════ */
|
|
1718
|
+
|
|
1719
|
+
function wrapSlide(idx, classes, content, opts) {
|
|
1720
|
+
// The clickable navigation halves sit at the slide's edges so
|
|
1721
|
+
// the user can advance / step back without finding a precise
|
|
1722
|
+
// button. Active state toggles via the container's class.
|
|
1723
|
+
const o = opts || {};
|
|
1724
|
+
return `
|
|
1725
|
+
<section class="ppt-slide ${classes}" data-slide-index="${idx}" data-slide-title="${escape(o.title || "")}">
|
|
1726
|
+
<button type="button" class="ppt-slide-click prev" data-ppt-prev aria-label="Previous slide"></button>
|
|
1727
|
+
<button type="button" class="ppt-slide-click next" data-ppt-next aria-label="Next slide"></button>
|
|
1728
|
+
${content}
|
|
1729
|
+
</section>`;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/* ─── Anthropic-essay variant slides ─────────────────────────
|
|
1733
|
+
Modeled after mvp/screen-7-report-anthropic.html: warm cream
|
|
1734
|
+
paper, mono kickers above every section, roman serif headlines
|
|
1735
|
+
with italic-clay <em>, italic lower-roman / decimal-leading-zero
|
|
1736
|
+
numerals, top + bottom rules only (no border-left). */
|
|
1737
|
+
|
|
1738
|
+
function antChrome(idx, total) {
|
|
1739
|
+
// Anthropic · floating mono kicker top-left, brand foot
|
|
1740
|
+
// bottom-left, counter bottom-right. The kicker is placed by
|
|
1741
|
+
// each builder so it can encode the section name; we only emit
|
|
1742
|
+
// foot + counter here.
|
|
1743
|
+
return `
|
|
1744
|
+
<div class="ant-foot"><b>PrivateBoard</b> · The Boardroom</div>
|
|
1745
|
+
<div class="ant-counter"><b>${idx + 1}</b> / ${total}</div>`;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
function antKicker(label) {
|
|
1749
|
+
// Mono-kicker prefix · "01 — section name" / "— introduction".
|
|
1750
|
+
// The em-dash separator and clay-deep colour are the spine's
|
|
1751
|
+
// signature opener.
|
|
1752
|
+
return `<div class="ant-eye-top">${escape(label)}</div>`;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
function antCover(idx, total, m, dateInfo, briefIdShort) {
|
|
1756
|
+
const stats = coverStats(m);
|
|
1757
|
+
const statsHtml = stats.length ? `
|
|
1758
|
+
<div class="ant-cover-stats">
|
|
1759
|
+
${stats.map((s) => `
|
|
1760
|
+
<div>
|
|
1761
|
+
<div class="ant-cover-stat-num">${escape(s.num)}</div>
|
|
1762
|
+
<div class="ant-cover-stat-cap">${escape(s.cap)}</div>
|
|
1763
|
+
</div>`).join("")}
|
|
1764
|
+
</div>` : "";
|
|
1765
|
+
const content = `
|
|
1766
|
+
${antKicker(`The Boardroom · Brief ${briefIdShort.replace(/^#/, "")}`)}
|
|
1767
|
+
<div class="ant-body ant-slide-cover">
|
|
1768
|
+
<div class="ant-cover-kicker">— a working hypothesis</div>
|
|
1769
|
+
<h1 class="ant-headline">${escape(m.title || "")}</h1>
|
|
1770
|
+
${m.kicker ? `<div class="ant-deck">${escape(m.kicker)}</div>` : ""}
|
|
1771
|
+
<div class="ant-byline">By <em>${escape(m.source || "the chair")}</em> · ${escape(dateInfo.long)}</div>
|
|
1772
|
+
${statsHtml}
|
|
1773
|
+
</div>
|
|
1774
|
+
${antChrome(idx, total)}`;
|
|
1775
|
+
return wrapSlide(idx, "ant-slide ant-slide-cover", content, { title: m.title || "Cover" });
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
function antAgenda(idx, total, m, agendaItems) {
|
|
1779
|
+
const items = agendaItems.map((it) => `<li><span>${escape(it)}</span></li>`).join("");
|
|
1780
|
+
const content = `
|
|
1781
|
+
${antKicker("— Outline")}
|
|
1782
|
+
<div class="ant-body">
|
|
1783
|
+
<h2 class="ant-headline">What we'll <em>cover</em>.</h2>
|
|
1784
|
+
<ul class="ant-agenda">${items}</ul>
|
|
1785
|
+
</div>
|
|
1786
|
+
${antChrome(idx, total)}`;
|
|
1787
|
+
return wrapSlide(idx, "ant-slide", content, { title: "Agenda" });
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
function antMilestone(idx, total, ms, n) {
|
|
1791
|
+
const stat = pickStat(ms);
|
|
1792
|
+
// Side pane · prefer numeric callout, otherwise a giant italic
|
|
1793
|
+
// roman numeral as phase indicator. The reference reserves big
|
|
1794
|
+
// numerals for considerations / open questions; we use the same
|
|
1795
|
+
// typographic gesture for milestone phases.
|
|
1796
|
+
const sideHtml = stat ? `
|
|
1797
|
+
<aside class="ant-roman">
|
|
1798
|
+
<div class="ant-callout-num">${escape(stat.num)}</div>
|
|
1799
|
+
<div class="ant-callout-cap">— ${escape(stat.cap)}</div>
|
|
1800
|
+
</aside>` : `
|
|
1801
|
+
<aside class="ant-roman">
|
|
1802
|
+
<div class="ant-roman-num">${toRoman(n)}.</div>
|
|
1803
|
+
<div class="ant-roman-cap">— Phase ${n}</div>
|
|
1804
|
+
</aside>`;
|
|
1805
|
+
const kickerLabel = `${String(n).padStart(2, "0")} — ${ms.period || `Phase ${n}`}`;
|
|
1806
|
+
const content = `
|
|
1807
|
+
${antKicker(kickerLabel)}
|
|
1808
|
+
<div class="ant-body">
|
|
1809
|
+
<div class="ant-milestone">
|
|
1810
|
+
<div>
|
|
1811
|
+
<h2 class="ant-headline">${escape(ms.title || "")}</h2>
|
|
1812
|
+
${ms.body ? `<div class="ant-milestone-body">${escape(ms.body)}</div>` : ""}
|
|
1813
|
+
</div>
|
|
1814
|
+
${sideHtml}
|
|
1815
|
+
</div>
|
|
1816
|
+
</div>
|
|
1817
|
+
${antChrome(idx, total)}`;
|
|
1818
|
+
return wrapSlide(idx, "ant-slide", content, { title: ms.title || `Milestone ${n}` });
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
function antTimeline(idx, total, items) {
|
|
1822
|
+
const cells = items.map((it, i) => {
|
|
1823
|
+
const oneLine = String(it.body || "").split(/(?<=[.!?。!?])\s+/)[0] || "";
|
|
1824
|
+
return `
|
|
1825
|
+
<div class="ant-tl-cell">
|
|
1826
|
+
<div class="ant-tl-period">${escape(it.period || `Phase ${i + 1}`)}</div>
|
|
1827
|
+
<div class="ant-tl-marker">${toRoman(i + 1)}.</div>
|
|
1828
|
+
<div class="ant-tl-title">${escape(it.title || "")}</div>
|
|
1829
|
+
<div class="ant-tl-body">${escape(oneLine)}</div>
|
|
1830
|
+
</div>`;
|
|
1831
|
+
}).join("");
|
|
1832
|
+
const content = `
|
|
1833
|
+
${antKicker("01 — The arc")}
|
|
1834
|
+
<div class="ant-body">
|
|
1835
|
+
<h2 class="ant-headline">How the work <em>unfolds</em>.</h2>
|
|
1836
|
+
<div class="ant-timeline" style="--n: ${items.length}">${cells}</div>
|
|
1837
|
+
</div>
|
|
1838
|
+
${antChrome(idx, total)}`;
|
|
1839
|
+
return wrapSlide(idx, "ant-slide", content, { title: "Timeline" });
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function antDataSlide(idx, total, rb) {
|
|
1843
|
+
const rows = rb.entries.slice(0, 5).map((e) => {
|
|
1844
|
+
const ratio = Math.max(0, Math.min(1, Number(e.ratio) || 0));
|
|
1845
|
+
const pct = (ratio * 100).toFixed(1);
|
|
1846
|
+
return `
|
|
1847
|
+
<div class="ant-bar">
|
|
1848
|
+
<span class="ant-bar-label">${escape(e.label || "")}</span>
|
|
1849
|
+
<span class="ant-bar-track"><span class="ant-bar-fill" style="width: ${pct}%"></span></span>
|
|
1850
|
+
<span class="ant-bar-value">${escape(e.value || "")}</span>
|
|
1851
|
+
</div>`;
|
|
1852
|
+
}).join("");
|
|
1853
|
+
const content = `
|
|
1854
|
+
${antKicker("— By the numbers")}
|
|
1855
|
+
<div class="ant-body">
|
|
1856
|
+
<h2 class="ant-headline">${escape(rb.title || "By the numbers")}</h2>
|
|
1857
|
+
<div class="ant-bars">${rows}</div>
|
|
1858
|
+
</div>
|
|
1859
|
+
${antChrome(idx, total)}`;
|
|
1860
|
+
return wrapSlide(idx, "ant-slide", content, { title: rb.title || "By the numbers" });
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function antBulletsSlide(idx, total, eyebrow, headline, bullets, kind) {
|
|
1864
|
+
// kind = "roman" for talking-points (italic lower-roman), or
|
|
1865
|
+
// "decimal" for verification (decimal-leading-zero). Both are
|
|
1866
|
+
// legitimate Anthropic-spine numeral patterns from the reference.
|
|
1867
|
+
const variantClass = kind === "roman" ? " is-roman" : " is-decimal";
|
|
1868
|
+
const items = bullets.slice(0, 5).map((b) => `<li><span>${escape(b)}</span></li>`).join("");
|
|
1869
|
+
// Headline gets italic emphasis on its operative word: simplest
|
|
1870
|
+
// heuristic is to italicise the first noun-ish phrase. Reference
|
|
1871
|
+
// does this manually in copy; we approximate by italicising the
|
|
1872
|
+
// last word, which usually carries the load in imperative or
|
|
1873
|
+
// claim-form headings ("What to <em>watch</em>" / "How to
|
|
1874
|
+
// <em>act</em>").
|
|
1875
|
+
const headlineHtml = (() => {
|
|
1876
|
+
const h = String(headline || "").trim();
|
|
1877
|
+
if (!h) return "";
|
|
1878
|
+
const m = h.match(/^(.+?)\s+(\S+)([.?!]?)$/);
|
|
1879
|
+
if (!m) return escape(h);
|
|
1880
|
+
return `${escape(m[1])} <em>${escape(m[2].replace(/[.?!]$/, ""))}</em>${escape(m[3])}`;
|
|
1881
|
+
})();
|
|
1882
|
+
const content = `
|
|
1883
|
+
${antKicker(`— ${eyebrow}`)}
|
|
1884
|
+
<div class="ant-body">
|
|
1885
|
+
<h2 class="ant-headline">${headlineHtml}</h2>
|
|
1886
|
+
<ol class="ant-bullets${variantClass}">${items}</ol>
|
|
1887
|
+
</div>
|
|
1888
|
+
${antChrome(idx, total)}`;
|
|
1889
|
+
return wrapSlide(idx, "ant-slide", content, { title: headline });
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
function antVoicesSlide(idx, total, persp) {
|
|
1893
|
+
const upClass = persp.length === 2 ? " is-two-up" : (persp.length === 3 ? " is-three-up" : "");
|
|
1894
|
+
const cards = persp.slice(0, 4).map((p) => `
|
|
1895
|
+
<div class="ant-voice">
|
|
1896
|
+
<div class="ant-voice-name">${escape(p.directorName || "")}</div>
|
|
1897
|
+
${p.directorRole ? `<div class="ant-voice-role">${escape(p.directorRole)}</div>` : ""}
|
|
1898
|
+
${p.stance ? `<div class="ant-voice-stance">${escape(p.stance)}</div>` : ""}
|
|
1899
|
+
${p.position ? `<div class="ant-voice-position">${escape(p.position)}</div>` : ""}
|
|
1900
|
+
${p.quote ? `<div class="ant-voice-quote">"${escape(p.quote)}"</div>` : ""}
|
|
1901
|
+
</div>`).join("");
|
|
1902
|
+
const content = `
|
|
1903
|
+
${antKicker("— The room")}
|
|
1904
|
+
<div class="ant-body">
|
|
1905
|
+
<h2 class="ant-headline">How each <em>read</em> it.</h2>
|
|
1906
|
+
<div class="ant-voices${upClass}">${cards}</div>
|
|
1907
|
+
</div>
|
|
1908
|
+
${antChrome(idx, total)}`;
|
|
1909
|
+
return wrapSlide(idx, "ant-slide", content, { title: "Director voices" });
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
function antConsensusSlide(idx, total, aligns, chairSyn) {
|
|
1913
|
+
const items = aligns.slice(0, 3).map((a) => `
|
|
1914
|
+
<li class="ant-align">
|
|
1915
|
+
<div>
|
|
1916
|
+
<div class="ant-align-point">${escape(a.pointOfAgreement || "")}</div>
|
|
1917
|
+
${a.note ? `<div class="ant-align-note">${escape(a.note)}</div>` : ""}
|
|
1918
|
+
<div class="ant-chips">${(a.directorNames || []).map((n) => `<span class="ant-chip">${escape(n)}</span>`).join("")}</div>
|
|
1919
|
+
</div>
|
|
1920
|
+
</li>`).join("");
|
|
1921
|
+
const content = `
|
|
1922
|
+
${antKicker("— Convergence")}
|
|
1923
|
+
<div class="ant-body">
|
|
1924
|
+
<h2 class="ant-headline">Where the room <em>agreed</em>.</h2>
|
|
1925
|
+
<ul class="ant-aligns">${items}</ul>
|
|
1926
|
+
${chairSyn ? `<div class="ant-voice-quote" style="margin-top: 22px;">— ${escape(chairSyn)}</div>` : ""}
|
|
1927
|
+
</div>
|
|
1928
|
+
${antChrome(idx, total)}`;
|
|
1929
|
+
return wrapSlide(idx, "ant-slide", content, { title: "Where we agreed" });
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function antSplitsSlide(idx, total, divs) {
|
|
1933
|
+
const items = divs.slice(0, 2).map((d) => {
|
|
1934
|
+
const sides = (d.sides || []).slice(0, 3);
|
|
1935
|
+
const sideClass = sides.length >= 3 ? " is-three" : "";
|
|
1936
|
+
const sidesHtml = sides.map((s) => `
|
|
1937
|
+
<div class="ant-split-side">
|
|
1938
|
+
<div class="ant-split-side-label">${escape(s.label || "")}</div>
|
|
1939
|
+
<div class="ant-split-side-stance">${escape(s.stance || "")}</div>
|
|
1940
|
+
<div class="ant-split-side-names">${(s.directorNames || []).map((n) => `<span class="ant-chip">${escape(n)}</span>`).join("")}</div>
|
|
1941
|
+
</div>`).join("");
|
|
1942
|
+
return `
|
|
1943
|
+
<div class="ant-split">
|
|
1944
|
+
<div class="ant-split-hinge">${escape(d.hinge || "")}</div>
|
|
1945
|
+
<div class="ant-split-sides${sideClass}">${sidesHtml}</div>
|
|
1946
|
+
${d.resolution ? `<div class="ant-split-resolve"><b>To resolve</b>${escape(d.resolution)}</div>` : ""}
|
|
1947
|
+
</div>`;
|
|
1948
|
+
}).join("");
|
|
1949
|
+
const content = `
|
|
1950
|
+
${antKicker("— Divergence")}
|
|
1951
|
+
<div class="ant-body">
|
|
1952
|
+
<h2 class="ant-headline">The <em>hinge</em> points.</h2>
|
|
1953
|
+
<div class="ant-splits">${items}</div>
|
|
1954
|
+
</div>
|
|
1955
|
+
${antChrome(idx, total)}`;
|
|
1956
|
+
return wrapSlide(idx, "ant-slide", content, { title: "Where we split" });
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function antTakeaway(idx, total, m) {
|
|
1960
|
+
const content = `
|
|
1961
|
+
${antKicker("— The takeaway")}
|
|
1962
|
+
<div class="ant-body ant-slide-takeaway">
|
|
1963
|
+
<div class="ant-takeaway-mark">"</div>
|
|
1964
|
+
<div class="ant-takeaway-text">${escape(m.conclusion || m.title || "")}</div>
|
|
1965
|
+
<div class="ant-takeaway-cite">— signed, <em>${escape(m.source || "the chair")}</em></div>
|
|
1966
|
+
</div>
|
|
1967
|
+
${antChrome(idx, total)}`;
|
|
1968
|
+
return wrapSlide(idx, "ant-slide ant-slide-takeaway", content, { title: "Takeaway" });
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
/* ─── Keynote variant slides ──────────────────────────────── */
|
|
1972
|
+
|
|
1973
|
+
function keyChrome(idx, total) {
|
|
1974
|
+
// Keynote · minimal floating chrome at the bottom-right only,
|
|
1975
|
+
// no top bar / no footer bar · the slide is the focus.
|
|
1976
|
+
return `<div class="key-floater"><b>${idx + 1}</b> / ${total}</div>`;
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function keyCover(idx, total, m, dateInfo) {
|
|
1980
|
+
const stats = coverStats(m);
|
|
1981
|
+
const statsHtml = stats.length ? `
|
|
1982
|
+
<div class="key-cover-stats">
|
|
1983
|
+
${stats.map((s) => `
|
|
1984
|
+
<div class="key-cover-stat">
|
|
1985
|
+
<div class="key-cover-stat-num">${escape(s.num)}</div>
|
|
1986
|
+
<div class="key-cover-stat-cap">${escape(s.cap)}</div>
|
|
1987
|
+
</div>`).join("")}
|
|
1988
|
+
</div>` : "";
|
|
1989
|
+
const content = `
|
|
1990
|
+
<div class="key-eye-top">The Boardroom</div>
|
|
1991
|
+
<div class="key-body key-slide-cover">
|
|
1992
|
+
<div class="key-cover-mark">"</div>
|
|
1993
|
+
<h1 class="key-headline">${escape(m.title || "")}</h1>
|
|
1994
|
+
${m.kicker ? `<div class="key-deck">${escape(m.kicker)}</div>` : ""}
|
|
1995
|
+
<div class="key-byline">By <em>${escape(m.source || "the chair")}</em> · ${escape(dateInfo.long)}</div>
|
|
1996
|
+
${statsHtml}
|
|
1997
|
+
</div>
|
|
1998
|
+
${keyChrome(idx, total)}`;
|
|
1999
|
+
return wrapSlide(idx, "key-slide key-slide-cover", content, { title: m.title || "Cover" });
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
function keyAgenda(idx, total, m, agendaItems) {
|
|
2003
|
+
const items = agendaItems.map((it) => `<li><span>${escape(it)}</span></li>`).join("");
|
|
2004
|
+
const content = `
|
|
2005
|
+
<div class="key-eye-top">Agenda</div>
|
|
2006
|
+
<div class="key-body">
|
|
2007
|
+
<h2 class="key-headline">What we'll <em>cover</em>.</h2>
|
|
2008
|
+
<ul class="key-agenda">${items}</ul>
|
|
2009
|
+
</div>
|
|
2010
|
+
${keyChrome(idx, total)}`;
|
|
2011
|
+
return wrapSlide(idx, "key-slide", content, { title: "Agenda" });
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
function keyMilestone(idx, total, ms, n) {
|
|
2015
|
+
const stat = pickStat(ms);
|
|
2016
|
+
// Side pane · prefer numeric callout, otherwise show a giant
|
|
2017
|
+
// italic roman numeral as the phase indicator. Empty milestones
|
|
2018
|
+
// already get filtered out by the dispatcher.
|
|
2019
|
+
const sideHtml = stat ? `
|
|
2020
|
+
<aside>
|
|
2021
|
+
<div class="key-callout-num">${escape(stat.num)}</div>
|
|
2022
|
+
<div class="key-callout-cap">${escape(stat.cap)}</div>
|
|
2023
|
+
</aside>` : `
|
|
2024
|
+
<aside class="key-roman">
|
|
2025
|
+
<div class="key-roman-num">${toRoman(n)}.</div>
|
|
2026
|
+
<div class="key-roman-cap">— Phase ${n}</div>
|
|
2027
|
+
</aside>`;
|
|
2028
|
+
const content = `
|
|
2029
|
+
<div class="key-eye-top">${escape(ms.period || `Phase ${n}`)}</div>
|
|
2030
|
+
<div class="key-body">
|
|
2031
|
+
<div class="key-milestone">
|
|
2032
|
+
<div>
|
|
2033
|
+
<h2 class="key-headline">${escape(ms.title || "")}</h2>
|
|
2034
|
+
${ms.body ? `<div class="key-milestone-body">${escape(ms.body)}</div>` : ""}
|
|
2035
|
+
</div>
|
|
2036
|
+
${sideHtml}
|
|
2037
|
+
</div>
|
|
2038
|
+
</div>
|
|
2039
|
+
${keyChrome(idx, total)}`;
|
|
2040
|
+
return wrapSlide(idx, "key-slide", content, { title: ms.title || `Milestone ${n}` });
|
|
2041
|
+
}
|
|
2042
|
+
|
|
2043
|
+
function keyTimeline(idx, total, items) {
|
|
2044
|
+
const cells = items.map((it, i) => {
|
|
2045
|
+
const oneLine = String(it.body || "").split(/(?<=[.!?。!?])\s+/)[0] || "";
|
|
2046
|
+
return `
|
|
2047
|
+
<div class="key-tl-cell">
|
|
2048
|
+
<div class="key-tl-period">${escape(it.period || `Phase ${i + 1}`)}</div>
|
|
2049
|
+
<div class="key-tl-marker">${toRoman(i + 1)}.</div>
|
|
2050
|
+
<div class="key-tl-title">${escape(it.title || "")}</div>
|
|
2051
|
+
<div class="key-tl-body">${escape(oneLine)}</div>
|
|
2052
|
+
</div>`;
|
|
2053
|
+
}).join("");
|
|
2054
|
+
const content = `
|
|
2055
|
+
<div class="key-eye-top">The arc</div>
|
|
2056
|
+
<div class="key-body">
|
|
2057
|
+
<h2 class="key-headline">How the work <em>unfolds</em>.</h2>
|
|
2058
|
+
<div class="key-timeline" style="--n: ${items.length}">${cells}</div>
|
|
2059
|
+
</div>
|
|
2060
|
+
${keyChrome(idx, total)}`;
|
|
2061
|
+
return wrapSlide(idx, "key-slide", content, { title: "Timeline" });
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
function keyDataSlide(idx, total, rb) {
|
|
2065
|
+
const rows = rb.entries.slice(0, 5).map((e) => {
|
|
2066
|
+
const ratio = Math.max(0, Math.min(1, Number(e.ratio) || 0));
|
|
2067
|
+
const pct = (ratio * 100).toFixed(1);
|
|
2068
|
+
return `
|
|
2069
|
+
<div class="key-bar">
|
|
2070
|
+
<span class="key-bar-label">${escape(e.label || "")}</span>
|
|
2071
|
+
<span class="key-bar-track"><span class="key-bar-fill" style="width: ${pct}%"></span></span>
|
|
2072
|
+
<span class="key-bar-value">${escape(e.value || "")}</span>
|
|
2073
|
+
</div>`;
|
|
2074
|
+
}).join("");
|
|
2075
|
+
const content = `
|
|
2076
|
+
<div class="key-eye-top">By the numbers</div>
|
|
2077
|
+
<div class="key-body">
|
|
2078
|
+
<h2 class="key-headline">${escape(rb.title || "By the numbers")}</h2>
|
|
2079
|
+
<div class="key-bars">${rows}</div>
|
|
2080
|
+
</div>
|
|
2081
|
+
${keyChrome(idx, total)}`;
|
|
2082
|
+
return wrapSlide(idx, "key-slide", content, { title: rb.title || "By the numbers" });
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
function keyBulletsSlide(idx, total, eyebrow, headline, bullets) {
|
|
2086
|
+
const items = bullets.slice(0, 5).map((b) => `<li><span>${escape(b)}</span></li>`).join("");
|
|
2087
|
+
const content = `
|
|
2088
|
+
<div class="key-eye-top">${escape(eyebrow)}</div>
|
|
2089
|
+
<div class="key-body">
|
|
2090
|
+
<h2 class="key-headline">${escape(headline)}</h2>
|
|
2091
|
+
<ul class="key-bullets">${items}</ul>
|
|
2092
|
+
</div>
|
|
2093
|
+
${keyChrome(idx, total)}`;
|
|
2094
|
+
return wrapSlide(idx, "key-slide", content, { title: headline });
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function keyVoicesSlide(idx, total, persp) {
|
|
2098
|
+
const upClass = persp.length === 2 ? " is-two-up" : (persp.length === 3 ? " is-three-up" : "");
|
|
2099
|
+
const cards = persp.slice(0, 4).map((p) => `
|
|
2100
|
+
<div class="key-voice">
|
|
2101
|
+
<div class="key-voice-name">${escape(p.directorName || "")}</div>
|
|
2102
|
+
${p.directorRole ? `<div class="key-voice-role">${escape(p.directorRole)}</div>` : ""}
|
|
2103
|
+
${p.stance ? `<div class="key-voice-stance">${escape(p.stance)}</div>` : ""}
|
|
2104
|
+
${p.position ? `<div class="key-voice-position">${escape(p.position)}</div>` : ""}
|
|
2105
|
+
${p.quote ? `<div class="key-voice-quote">"${escape(p.quote)}"</div>` : ""}
|
|
2106
|
+
</div>`).join("");
|
|
2107
|
+
const content = `
|
|
2108
|
+
<div class="key-eye-top">The room</div>
|
|
2109
|
+
<div class="key-body">
|
|
2110
|
+
<h2 class="key-headline">How each <em>read</em> it.</h2>
|
|
2111
|
+
<div class="key-voices${upClass}">${cards}</div>
|
|
2112
|
+
</div>
|
|
2113
|
+
${keyChrome(idx, total)}`;
|
|
2114
|
+
return wrapSlide(idx, "key-slide", content, { title: "Director voices" });
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function keyConsensusSlide(idx, total, aligns, chairSyn) {
|
|
2118
|
+
const items = aligns.slice(0, 3).map((a) => `
|
|
2119
|
+
<li class="key-align">
|
|
2120
|
+
<div class="key-align-point">${escape(a.pointOfAgreement || "")}</div>
|
|
2121
|
+
${a.note ? `<div class="key-align-note">${escape(a.note)}</div>` : ""}
|
|
2122
|
+
<div class="key-chips">${(a.directorNames || []).map((n) => `<span class="key-chip">${escape(n)}</span>`).join("")}</div>
|
|
2123
|
+
</li>`).join("");
|
|
2124
|
+
const content = `
|
|
2125
|
+
<div class="key-eye-top">Convergence</div>
|
|
2126
|
+
<div class="key-body">
|
|
2127
|
+
<h2 class="key-headline">Where the room <em>agreed</em>.</h2>
|
|
2128
|
+
<ul class="key-aligns">${items}</ul>
|
|
2129
|
+
${chairSyn ? `<div class="key-voice-quote" style="margin-top: 22px;">— ${escape(chairSyn)}</div>` : ""}
|
|
2130
|
+
</div>
|
|
2131
|
+
${keyChrome(idx, total)}`;
|
|
2132
|
+
return wrapSlide(idx, "key-slide", content, { title: "Where we agreed" });
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
function keySplitsSlide(idx, total, divs) {
|
|
2136
|
+
const items = divs.slice(0, 2).map((d) => {
|
|
2137
|
+
const sides = (d.sides || []).slice(0, 3);
|
|
2138
|
+
const sideClass = sides.length >= 3 ? " is-three" : "";
|
|
2139
|
+
const sidesHtml = sides.map((s) => `
|
|
2140
|
+
<div class="key-split-side">
|
|
2141
|
+
<div class="key-split-side-label">${escape(s.label || "")}</div>
|
|
2142
|
+
<div class="key-split-side-stance">${escape(s.stance || "")}</div>
|
|
2143
|
+
<div class="key-split-side-names">${(s.directorNames || []).map((n) => `<span class="key-chip">${escape(n)}</span>`).join("")}</div>
|
|
2144
|
+
</div>`).join("");
|
|
2145
|
+
return `
|
|
2146
|
+
<div class="key-split">
|
|
2147
|
+
<div class="key-split-hinge">${escape(d.hinge || "")}</div>
|
|
2148
|
+
<div class="key-split-sides${sideClass}">${sidesHtml}</div>
|
|
2149
|
+
${d.resolution ? `<div class="key-split-resolve"><b>To resolve</b>${escape(d.resolution)}</div>` : ""}
|
|
2150
|
+
</div>`;
|
|
2151
|
+
}).join("");
|
|
2152
|
+
const content = `
|
|
2153
|
+
<div class="key-eye-top">Divergence</div>
|
|
2154
|
+
<div class="key-body">
|
|
2155
|
+
<h2 class="key-headline">The <em>hinge</em> points.</h2>
|
|
2156
|
+
<div class="key-splits">${items}</div>
|
|
2157
|
+
</div>
|
|
2158
|
+
${keyChrome(idx, total)}`;
|
|
2159
|
+
return wrapSlide(idx, "key-slide", content, { title: "Where we split" });
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
function keyTakeaway(idx, total, m) {
|
|
2163
|
+
const content = `
|
|
2164
|
+
<div class="key-eye-top">The takeaway</div>
|
|
2165
|
+
<div class="key-body key-slide-takeaway">
|
|
2166
|
+
<div class="key-takeaway-mark">"</div>
|
|
2167
|
+
<div class="key-takeaway-text">${escape(m.conclusion || m.title || "")}</div>
|
|
2168
|
+
<div class="key-takeaway-cite">— Bottom line</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
${keyChrome(idx, total)}`;
|
|
2171
|
+
return wrapSlide(idx, "key-slide key-slide-takeaway", content, { title: "Takeaway" });
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
/* ─── Build the deck ─────────────────────────────────────── */
|
|
2175
|
+
|
|
2176
|
+
function buildSlides(brief, variant) {
|
|
2177
|
+
const m = brief.bodyJson;
|
|
2178
|
+
const ms = (m.milestones || []).slice(0, 3);
|
|
2179
|
+
const tp = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
|
|
2180
|
+
const ver = (m.verification && Array.isArray(m.verification.bullets)) ? m.verification.bullets : [];
|
|
2181
|
+
const rb = m.rankedBars;
|
|
2182
|
+
const hasChart = rb && Array.isArray(rb.entries) && rb.entries.length > 0;
|
|
2183
|
+
const dateInfo = formatDate(m.footerTag);
|
|
2184
|
+
const briefIdShort = brief.id ? `#${String(brief.id).slice(0, 10).toUpperCase()}` : "";
|
|
2185
|
+
|
|
2186
|
+
// Director-comparison block · only present in PPT-mode briefs
|
|
2187
|
+
// generated by the new schema. Each section is gated on having
|
|
2188
|
+
// its own content.
|
|
2189
|
+
const db = m.directorBlock && typeof m.directorBlock === "object" ? m.directorBlock : null;
|
|
2190
|
+
const persp = (db && Array.isArray(db.perspectives)) ? db.perspectives : [];
|
|
2191
|
+
const aligns = (db && Array.isArray(db.alignment)) ? db.alignment : [];
|
|
2192
|
+
const splits = (db && Array.isArray(db.divergence)) ? db.divergence : [];
|
|
2193
|
+
const chairSyn = (db && typeof db.chairSynthesis === "string") ? db.chairSynthesis : "";
|
|
2194
|
+
const showVoices = persp.length >= 2;
|
|
2195
|
+
const showConsensus = aligns.length >= 1;
|
|
2196
|
+
const showSplits = splits.length >= 1;
|
|
2197
|
+
|
|
2198
|
+
// Milestone slides only when the milestone has actual content
|
|
2199
|
+
const filledMilestones = ms.filter((x) => x && (x.title || x.body || x.callout));
|
|
2200
|
+
|
|
2201
|
+
// Timeline overview · skipped when fewer than 2 milestones
|
|
2202
|
+
// (a timeline of one phase is not a timeline).
|
|
2203
|
+
const showTimeline = filledMilestones.length >= 2;
|
|
2204
|
+
|
|
2205
|
+
// Build agenda labels in the same order the slides will render.
|
|
2206
|
+
const agenda = [];
|
|
2207
|
+
if (showTimeline) agenda.push("The arc");
|
|
2208
|
+
if (showVoices) agenda.push("Director voices");
|
|
2209
|
+
ms.forEach((x) => {
|
|
2210
|
+
if (x && (x.title || x.period)) agenda.push(x.title || x.period);
|
|
2211
|
+
});
|
|
2212
|
+
if (hasChart) agenda.push("By the numbers");
|
|
2213
|
+
if (showConsensus) agenda.push("Where we agreed");
|
|
2214
|
+
if (showSplits) agenda.push("Where we split");
|
|
2215
|
+
if (tp.length) agenda.push((m.talkingPoints && m.talkingPoints.title) || "Recommendations");
|
|
2216
|
+
if (ver.length) agenda.push((m.verification && m.verification.title) || "What to watch");
|
|
2217
|
+
if (m.conclusion) agenda.push("The takeaway");
|
|
2218
|
+
|
|
2219
|
+
// Total slide count · cover + agenda + (timeline) + (voices) +
|
|
2220
|
+
// milestones + (data) + (consensus) + (splits) + (talking) +
|
|
2221
|
+
// (verify) + (takeaway).
|
|
2222
|
+
let total = 2 + filledMilestones.length;
|
|
2223
|
+
if (showTimeline) total += 1;
|
|
2224
|
+
if (showVoices) total += 1;
|
|
2225
|
+
if (hasChart) total += 1;
|
|
2226
|
+
if (showConsensus) total += 1;
|
|
2227
|
+
if (showSplits) total += 1;
|
|
2228
|
+
if (tp.length) total += 1;
|
|
2229
|
+
if (ver.length) total += 1;
|
|
2230
|
+
if (m.conclusion) total += 1;
|
|
2231
|
+
|
|
2232
|
+
const slides = [];
|
|
2233
|
+
let idx = 0;
|
|
2234
|
+
const cover = variant === "keynote" ? keyCover : antCover;
|
|
2235
|
+
const agendaSlide = variant === "keynote" ? keyAgenda : antAgenda;
|
|
2236
|
+
const timelineSlide = variant === "keynote" ? keyTimeline : antTimeline;
|
|
2237
|
+
const voicesSlide = variant === "keynote" ? keyVoicesSlide : antVoicesSlide;
|
|
2238
|
+
const consensusSlide = variant === "keynote" ? keyConsensusSlide : antConsensusSlide;
|
|
2239
|
+
const splitsSlide = variant === "keynote" ? keySplitsSlide : antSplitsSlide;
|
|
2240
|
+
const milestoneSlide = variant === "keynote" ? keyMilestone : antMilestone;
|
|
2241
|
+
const dataSlide = variant === "keynote" ? keyDataSlide : antDataSlide;
|
|
2242
|
+
const bulletsSlide = variant === "keynote" ? keyBulletsSlide : antBulletsSlide;
|
|
2243
|
+
const takeawaySlide = variant === "keynote" ? keyTakeaway : antTakeaway;
|
|
2244
|
+
|
|
2245
|
+
slides.push(cover(idx++, total, m, dateInfo, briefIdShort));
|
|
2246
|
+
slides.push(agendaSlide(idx++, total, m, agenda));
|
|
2247
|
+
if (showTimeline) {
|
|
2248
|
+
slides.push(timelineSlide(idx++, total, filledMilestones));
|
|
2249
|
+
}
|
|
2250
|
+
if (showVoices) {
|
|
2251
|
+
slides.push(voicesSlide(idx++, total, persp));
|
|
2252
|
+
}
|
|
2253
|
+
filledMilestones.forEach((ms_, i) => {
|
|
2254
|
+
slides.push(milestoneSlide(idx++, total, ms_, i + 1));
|
|
2255
|
+
});
|
|
2256
|
+
if (hasChart) {
|
|
2257
|
+
slides.push(dataSlide(idx++, total, rb));
|
|
2258
|
+
}
|
|
2259
|
+
if (showConsensus) {
|
|
2260
|
+
slides.push(consensusSlide(idx++, total, aligns, chairSyn));
|
|
2261
|
+
}
|
|
2262
|
+
if (showSplits) {
|
|
2263
|
+
slides.push(splitsSlide(idx++, total, splits));
|
|
2264
|
+
}
|
|
2265
|
+
if (tp.length) {
|
|
2266
|
+
const tpHead = (m.talkingPoints && m.talkingPoints.title) || "Recommendations";
|
|
2267
|
+
slides.push(bulletsSlide(idx++, total, "Recommendations", tpHead, tp, "roman"));
|
|
2268
|
+
}
|
|
2269
|
+
if (ver.length) {
|
|
2270
|
+
const verHead = (m.verification && m.verification.title) || "What to watch";
|
|
2271
|
+
slides.push(bulletsSlide(idx++, total, "What to watch", verHead, ver, "decimal"));
|
|
2272
|
+
}
|
|
2273
|
+
if (m.conclusion) {
|
|
2274
|
+
slides.push(takeawaySlide(idx++, total, m));
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
return slides;
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
/* ─── Active-slide state · keyboard / click / hash sync ─── */
|
|
2281
|
+
|
|
2282
|
+
let _state = { current: 0, total: 0 };
|
|
2283
|
+
|
|
2284
|
+
function updateCounter() {
|
|
2285
|
+
if (counterEl) counterEl.textContent = `${_state.current + 1} / ${_state.total}`;
|
|
2286
|
+
}
|
|
2287
|
+
function updateActive() {
|
|
2288
|
+
const slides = document.querySelectorAll(".ppt-slide");
|
|
2289
|
+
slides.forEach((el, i) => {
|
|
2290
|
+
el.classList.toggle("is-active", i === _state.current);
|
|
2291
|
+
});
|
|
2292
|
+
const thumbs = document.querySelectorAll(".ppt-thumb");
|
|
2293
|
+
thumbs.forEach((el, i) => {
|
|
2294
|
+
el.classList.toggle("is-active", i === _state.current);
|
|
2295
|
+
if (i === _state.current) {
|
|
2296
|
+
el.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" });
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
updateCounter();
|
|
2300
|
+
// Sync URL hash without scrolling
|
|
2301
|
+
const newHash = `#slide-${_state.current + 1}`;
|
|
2302
|
+
if (location.hash !== newHash) {
|
|
2303
|
+
history.replaceState(null, "", newHash);
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
function goTo(i) {
|
|
2307
|
+
const n = Math.max(0, Math.min(_state.total - 1, i));
|
|
2308
|
+
_state.current = n;
|
|
2309
|
+
updateActive();
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
/** Read `#slide-N` from the URL and clamp to a valid index. */
|
|
2313
|
+
function readHashIndex() {
|
|
2314
|
+
const m = String(location.hash || "").match(/^#slide-(\d+)$/);
|
|
2315
|
+
if (!m) return 0;
|
|
2316
|
+
const n = parseInt(m[1], 10) - 1;
|
|
2317
|
+
return isNaN(n) ? 0 : Math.max(0, Math.min(_state.total - 1, n));
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
function attachHandlers() {
|
|
2321
|
+
// Keyboard
|
|
2322
|
+
document.addEventListener("keydown", (e) => {
|
|
2323
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
2324
|
+
// Skip when typing inside a form field
|
|
2325
|
+
const t = e.target;
|
|
2326
|
+
if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
|
2327
|
+
switch (e.key) {
|
|
2328
|
+
case "ArrowRight":
|
|
2329
|
+
case "PageDown":
|
|
2330
|
+
case " ":
|
|
2331
|
+
e.preventDefault(); goTo(_state.current + 1); break;
|
|
2332
|
+
case "ArrowLeft":
|
|
2333
|
+
case "PageUp":
|
|
2334
|
+
e.preventDefault(); goTo(_state.current - 1); break;
|
|
2335
|
+
case "Home":
|
|
2336
|
+
e.preventDefault(); goTo(0); break;
|
|
2337
|
+
case "End":
|
|
2338
|
+
e.preventDefault(); goTo(_state.total - 1); break;
|
|
2339
|
+
case "f": case "F":
|
|
2340
|
+
e.preventDefault(); toggleFullscreen(); break;
|
|
2341
|
+
case "Escape":
|
|
2342
|
+
if (document.body.classList.contains("is-fullscreen")) {
|
|
2343
|
+
e.preventDefault();
|
|
2344
|
+
document.body.classList.remove("is-fullscreen");
|
|
2345
|
+
}
|
|
2346
|
+
break;
|
|
2347
|
+
default:
|
|
2348
|
+
if (/^[1-9]$/.test(e.key)) {
|
|
2349
|
+
e.preventDefault();
|
|
2350
|
+
goTo(parseInt(e.key, 10) - 1);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
});
|
|
2354
|
+
// Click halves on slides
|
|
2355
|
+
document.addEventListener("click", (e) => {
|
|
2356
|
+
if (e.target.closest("[data-ppt-prev]")) {
|
|
2357
|
+
e.preventDefault();
|
|
2358
|
+
goTo(_state.current - 1);
|
|
2359
|
+
return;
|
|
2360
|
+
}
|
|
2361
|
+
if (e.target.closest("[data-ppt-next]")) {
|
|
2362
|
+
e.preventDefault();
|
|
2363
|
+
goTo(_state.current + 1);
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
const thumb = e.target.closest("[data-ppt-thumb]");
|
|
2367
|
+
if (thumb) {
|
|
2368
|
+
e.preventDefault();
|
|
2369
|
+
const idx = parseInt(thumb.getAttribute("data-ppt-thumb"), 10);
|
|
2370
|
+
if (!isNaN(idx)) goTo(idx);
|
|
2371
|
+
return;
|
|
2372
|
+
}
|
|
2373
|
+
if (e.target.closest("[data-ppt-fullscreen]")) {
|
|
2374
|
+
e.preventDefault(); toggleFullscreen(); return;
|
|
2375
|
+
}
|
|
2376
|
+
if (e.target.closest("[data-ppt-png]")) {
|
|
2377
|
+
e.preventDefault(); exportPng(); return;
|
|
2378
|
+
}
|
|
2379
|
+
if (e.target.closest("[data-ppt-print]")) {
|
|
2380
|
+
e.preventDefault(); exportPdf(); return;
|
|
2381
|
+
}
|
|
2382
|
+
});
|
|
2383
|
+
// Hash sync · external nav (link click) updates the slide
|
|
2384
|
+
window.addEventListener("hashchange", () => {
|
|
2385
|
+
goTo(readHashIndex());
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
function toggleFullscreen() {
|
|
2390
|
+
const body = document.body;
|
|
2391
|
+
const isFs = body.classList.toggle("is-fullscreen");
|
|
2392
|
+
// Try to also use the browser fullscreen API for a real
|
|
2393
|
+
// OS-level fullscreen experience (esc-key still exits).
|
|
2394
|
+
try {
|
|
2395
|
+
if (isFs) {
|
|
2396
|
+
if (document.documentElement.requestFullscreen) {
|
|
2397
|
+
document.documentElement.requestFullscreen().catch(() => {});
|
|
2398
|
+
}
|
|
2399
|
+
} else {
|
|
2400
|
+
if (document.fullscreenElement && document.exitFullscreen) {
|
|
2401
|
+
document.exitFullscreen().catch(() => {});
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
} catch { /* no-op · the body class still toggles for a CSS-only fallback */ }
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
document.addEventListener("fullscreenchange", () => {
|
|
2408
|
+
// Keep body class in sync with browser fullscreen state
|
|
2409
|
+
if (!document.fullscreenElement) {
|
|
2410
|
+
document.body.classList.remove("is-fullscreen");
|
|
2411
|
+
}
|
|
2412
|
+
});
|
|
2413
|
+
|
|
2414
|
+
/* ─── Render the deck ─────────────────────────────────────── */
|
|
2415
|
+
|
|
2416
|
+
function render(brief) {
|
|
2417
|
+
if (brief.mode !== "ppt") {
|
|
2418
|
+
showState("Wrong mode", "This brief isn't a slide deck",
|
|
2419
|
+
"Open it in the matching viewer instead. Slides, magazine, newspaper, and research-note are separate output modes.");
|
|
2420
|
+
return;
|
|
2421
|
+
}
|
|
2422
|
+
const m = brief.bodyJson;
|
|
2423
|
+
if (!m || typeof m !== "object") {
|
|
2424
|
+
if (brief.isGenerating) {
|
|
2425
|
+
showState("Generating", "Deck is still being prepared",
|
|
2426
|
+
"The chair is currently composing the slides. Refresh in a few seconds.");
|
|
2427
|
+
} else {
|
|
2428
|
+
showState("Empty", "This brief has no deck data",
|
|
2429
|
+
"It may have failed to generate. Try regenerating from the room view.");
|
|
2430
|
+
}
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
if (m.title) document.title = `${m.title} · Slides`;
|
|
2435
|
+
|
|
2436
|
+
const variant = pickVariant(brief.id);
|
|
2437
|
+
document.body.setAttribute("data-ppt-variant", variant);
|
|
2438
|
+
if (variantBadge) {
|
|
2439
|
+
variantBadge.textContent = variant === "keynote" ? "Keynote Edition" : "Anthropic Edition";
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
const slides = buildSlides(brief, variant);
|
|
2443
|
+
_state.total = slides.length;
|
|
2444
|
+
_state.current = readHashIndex();
|
|
2445
|
+
if (_state.current >= _state.total) _state.current = 0;
|
|
2446
|
+
|
|
2447
|
+
// Build thumb strip · short labels with slide index
|
|
2448
|
+
const thumbs = slides.map((html, i) => {
|
|
2449
|
+
const titleMatch = html.match(/data-slide-title="([^"]*)"/);
|
|
2450
|
+
const title = titleMatch ? titleMatch[1] : `Slide ${i + 1}`;
|
|
2451
|
+
return `
|
|
2452
|
+
<div class="ppt-thumb" data-ppt-thumb="${i}">
|
|
2453
|
+
<div class="ppt-thumb-num">Slide ${String(i + 1).padStart(2, "0")}</div>
|
|
2454
|
+
<div class="ppt-thumb-title">${title}</div>
|
|
2455
|
+
</div>`;
|
|
2456
|
+
}).join("");
|
|
2457
|
+
|
|
2458
|
+
root.innerHTML = `
|
|
2459
|
+
<div class="ppt-stage">
|
|
2460
|
+
<article class="ppt-deck" data-ppt-paper>${slides.join("")}</article>
|
|
2461
|
+
</div>
|
|
2462
|
+
<div class="ppt-thumbs">${thumbs}</div>`;
|
|
2463
|
+
|
|
2464
|
+
updateActive();
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/* ─── Export wiring ────────────────────────────────────── */
|
|
2468
|
+
let _h2iLoaded = null;
|
|
2469
|
+
async function ensureHtmlToImage() {
|
|
2470
|
+
if (window.htmlToImage) return;
|
|
2471
|
+
if (!_h2iLoaded) {
|
|
2472
|
+
_h2iLoaded = new Promise((res, rej) => {
|
|
2473
|
+
const s = document.createElement("script");
|
|
2474
|
+
s.src = "https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js";
|
|
2475
|
+
s.onload = res;
|
|
2476
|
+
s.onerror = rej;
|
|
2477
|
+
document.head.appendChild(s);
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
await _h2iLoaded;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
/** Capture the active slide as a PNG data URL. */
|
|
2484
|
+
async function captureSlidePng(slideEl) {
|
|
2485
|
+
await ensureHtmlToImage();
|
|
2486
|
+
if (document.fonts && document.fonts.ready) {
|
|
2487
|
+
try { await document.fonts.ready; } catch { /* best-effort */ }
|
|
2488
|
+
}
|
|
2489
|
+
const width = Math.max(slideEl.scrollWidth, slideEl.offsetWidth, slideEl.clientWidth);
|
|
2490
|
+
const height = Math.max(slideEl.scrollHeight, slideEl.offsetHeight, slideEl.clientHeight);
|
|
2491
|
+
// Force the slide into display:flex temporarily so html-to-image
|
|
2492
|
+
// can capture even non-active slides. Remember the old display
|
|
2493
|
+
// and restore after.
|
|
2494
|
+
const prevDisplay = slideEl.style.display;
|
|
2495
|
+
slideEl.style.display = "flex";
|
|
2496
|
+
try {
|
|
2497
|
+
return await window.htmlToImage.toPng(slideEl, {
|
|
2498
|
+
pixelRatio: 2,
|
|
2499
|
+
backgroundColor: getComputedStyle(slideEl).backgroundColor || "#FFFFFF",
|
|
2500
|
+
cacheBust: true,
|
|
2501
|
+
width, height,
|
|
2502
|
+
canvasWidth: width, canvasHeight: height,
|
|
2503
|
+
style: { margin: "0", width: `${width}px`, height: `${height}px`, display: "flex" },
|
|
2504
|
+
});
|
|
2505
|
+
} finally {
|
|
2506
|
+
slideEl.style.display = prevDisplay;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
|
|
2510
|
+
function slugTitle() {
|
|
2511
|
+
return (document.title || "deck").replace(/[^a-z0-9]+/gi, "-").slice(0, 60) || "deck";
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2514
|
+
async function exportPng() {
|
|
2515
|
+
try {
|
|
2516
|
+
const active = document.querySelector(".ppt-slide.is-active");
|
|
2517
|
+
if (!active) throw new Error("no active slide");
|
|
2518
|
+
const dataUrl = await captureSlidePng(active);
|
|
2519
|
+
const a = document.createElement("a");
|
|
2520
|
+
a.download = `${slugTitle()}-slide-${_state.current + 1}.png`;
|
|
2521
|
+
a.href = dataUrl;
|
|
2522
|
+
a.click();
|
|
2523
|
+
} catch (e) {
|
|
2524
|
+
console.warn("[ppt] PNG export failed:", e);
|
|
2525
|
+
alert("PNG export failed · see browser console.");
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
/** Export every slide as a separate page in a printable popup ·
|
|
2530
|
+
* user picks "Save as PDF" in the print dialog. Each slide is
|
|
2531
|
+
* captured as a PNG (in display order) and embedded in the
|
|
2532
|
+
* popup as a one-image-per-page layout. */
|
|
2533
|
+
async function exportPdf() {
|
|
2534
|
+
try {
|
|
2535
|
+
const slides = Array.from(document.querySelectorAll(".ppt-slide"));
|
|
2536
|
+
if (!slides.length) throw new Error("no slides");
|
|
2537
|
+
// Capture all slides in order · this can take a few seconds
|
|
2538
|
+
const dataUrls = [];
|
|
2539
|
+
for (const slide of slides) {
|
|
2540
|
+
// eslint-disable-next-line no-await-in-loop
|
|
2541
|
+
dataUrls.push(await captureSlidePng(slide));
|
|
2542
|
+
}
|
|
2543
|
+
const win = window.open("", "_blank", "width=1280,height=720");
|
|
2544
|
+
if (!win) {
|
|
2545
|
+
alert("PDF export needs to open a new window — please allow popups for this site.");
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
const slug = slugTitle();
|
|
2549
|
+
const imgs = dataUrls.map((url, i) => `<img alt="${slug} slide ${i + 1}" src="${url}">`).join("");
|
|
2550
|
+
win.document.open();
|
|
2551
|
+
win.document.write(`<!doctype html><html lang="en"><head>
|
|
2552
|
+
<meta charset="utf-8">
|
|
2553
|
+
<title>${slug}</title>
|
|
2554
|
+
<style>
|
|
2555
|
+
@page { size: 16in 9in landscape; margin: 0; }
|
|
2556
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
2557
|
+
html, body { background: #0E0E10; }
|
|
2558
|
+
body {
|
|
2559
|
+
display: flex;
|
|
2560
|
+
flex-direction: column;
|
|
2561
|
+
align-items: center;
|
|
2562
|
+
padding: 24px 0;
|
|
2563
|
+
gap: 24px;
|
|
2564
|
+
min-height: 100vh;
|
|
2565
|
+
}
|
|
2566
|
+
img {
|
|
2567
|
+
display: block;
|
|
2568
|
+
width: 100%;
|
|
2569
|
+
max-width: 1200px;
|
|
2570
|
+
aspect-ratio: 16 / 9;
|
|
2571
|
+
object-fit: contain;
|
|
2572
|
+
box-shadow: 0 0 24px rgba(0, 0, 0, 0.40);
|
|
2573
|
+
}
|
|
2574
|
+
@media print {
|
|
2575
|
+
body { padding: 0; gap: 0; background: white; }
|
|
2576
|
+
img { box-shadow: none; max-width: none; width: 100vw; height: 100vh; aspect-ratio: auto; page-break-after: always; }
|
|
2577
|
+
img:last-child { page-break-after: auto; }
|
|
2578
|
+
}
|
|
2579
|
+
.hint {
|
|
2580
|
+
position: fixed; top: 12px; left: 12px;
|
|
2581
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
2582
|
+
font-size: 11px; color: #C8C2AE;
|
|
2583
|
+
background: rgba(20,20,20,0.85); padding: 6px 10px;
|
|
2584
|
+
border: 1px solid rgba(255,255,255,0.18);
|
|
2585
|
+
}
|
|
2586
|
+
@media print { .hint { display: none; } }
|
|
2587
|
+
</style>
|
|
2588
|
+
</head><body>
|
|
2589
|
+
<div class="hint">// press <kbd>⌘P</kbd> / <kbd>Ctrl+P</kbd> · save as PDF · landscape</div>
|
|
2590
|
+
${imgs}
|
|
2591
|
+
<script>
|
|
2592
|
+
(function () {
|
|
2593
|
+
var imgs = document.querySelectorAll("img");
|
|
2594
|
+
var loaded = 0;
|
|
2595
|
+
function go() { setTimeout(function () { window.print(); }, 200); }
|
|
2596
|
+
if (!imgs.length) { go(); return; }
|
|
2597
|
+
imgs.forEach(function (img) {
|
|
2598
|
+
if (img.complete) { loaded++; if (loaded === imgs.length) go(); }
|
|
2599
|
+
else { img.addEventListener("load", function () { loaded++; if (loaded === imgs.length) go(); }, { once: true }); }
|
|
2600
|
+
});
|
|
2601
|
+
})();
|
|
2602
|
+
<\/script>
|
|
2603
|
+
</body></html>`);
|
|
2604
|
+
win.document.close();
|
|
2605
|
+
} catch (e) {
|
|
2606
|
+
console.warn("[ppt] PDF export failed:", e);
|
|
2607
|
+
alert("PDF export failed · see browser console.");
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
/* ─── Boot ──────────────────────────────────────────────────── */
|
|
2612
|
+
attachHandlers();
|
|
2613
|
+
loadBrief().then((brief) => {
|
|
2614
|
+
if (brief) render(brief);
|
|
2615
|
+
}).catch((e) => {
|
|
2616
|
+
console.error("[ppt] load failed:", e);
|
|
2617
|
+
showState("Error", "Couldn't load this brief", e instanceof Error ? e.message : String(e));
|
|
2618
|
+
});
|
|
2619
|
+
})();
|
|
2620
|
+
</script>
|
|
2621
|
+
|
|
2622
|
+
</body>
|
|
2623
|
+
</html>
|