privateboard 0.1.7 → 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 +1423 -46
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +195 -0
- package/public/agent-profile.css +525 -0
- package/public/agent-profile.js +278 -1
- package/public/app.js +634 -118
- package/public/home.html +389 -17
- package/public/index.html +325 -130
- package/public/magazine.html +1685 -0
- package/public/newspaper.html +1892 -0
- package/public/ppt.html +2623 -0
- package/public/report.html +366 -52
- package/public/room-settings.css +40 -4
- package/public/room-settings.js +44 -2
- package/public/user-settings.css +117 -68
- package/public/user-settings.js +77 -46
|
@@ -0,0 +1,1892 @@
|
|
|
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>Newspaper · PrivateBoard</title>
|
|
7
|
+
<link rel="icon" href="/avatars/chair.svg" type="image/svg+xml">
|
|
8
|
+
<!-- NYT blackletter nameplate · UnifrakturMaguntia is the free
|
|
9
|
+
open-source font that reads as "Old English / Fraktur" the way
|
|
10
|
+
the iconic NYT mast does. Loaded from Google Fonts; system
|
|
11
|
+
fallback to Old English Text MT when offline. -->
|
|
12
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
13
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
14
|
+
<link href="https://fonts.googleapis.com/css2?family=UnifrakturMaguntia&display=swap" rel="stylesheet">
|
|
15
|
+
<style>
|
|
16
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
17
|
+
Newspaper · two distinct broadsheet templates picked deterministi-
|
|
18
|
+
cally from the brief id (so a refresh always shows the same look
|
|
19
|
+
for the same brief). Both render at LEAST 2 pages so the report
|
|
20
|
+
reads as a paper rather than a single sheet.
|
|
21
|
+
|
|
22
|
+
· POST · The Boardroom Post · Washington-Post register · clean
|
|
23
|
+
modern broadsheet · cream paper · deep ink · RED section
|
|
24
|
+
accents · sans-y kickers · big hero photo zone.
|
|
25
|
+
· TIMES · The Boardroom · New York Times register · classical
|
|
26
|
+
broadsheet · BLACKLETTER nameplate · "All the news that's
|
|
27
|
+
fit to print" tagline · star-ornament dividers · NAVY accent
|
|
28
|
+
· italic decks · 6-col grid front page.
|
|
29
|
+
|
|
30
|
+
Both variants share base typography and helpers; the masthead +
|
|
31
|
+
section system + accent palette differ to give each its own feel.
|
|
32
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
33
|
+
:root {
|
|
34
|
+
/* Page bg + paper + ink palette · shared across variants */
|
|
35
|
+
--bg: #1F1E1A;
|
|
36
|
+
--paper: #F4EFDF;
|
|
37
|
+
--paper-soft: #FAF6E8;
|
|
38
|
+
--paper-edge: #E8DFC4;
|
|
39
|
+
--ink: #14110B;
|
|
40
|
+
--ink-soft: #3A2F1E;
|
|
41
|
+
--ink-mid: #5E5238;
|
|
42
|
+
--ink-faint: #8E8160;
|
|
43
|
+
--ink-muted: #B0A380;
|
|
44
|
+
|
|
45
|
+
--inv-bg: #1A2734;
|
|
46
|
+
--inv-ink: #F4EFDF;
|
|
47
|
+
--inv-ink-soft: #B7C0CB;
|
|
48
|
+
|
|
49
|
+
--rule: #C3B891;
|
|
50
|
+
--rule-soft: #D2C7A0;
|
|
51
|
+
--rule-strong: #8C7A56;
|
|
52
|
+
|
|
53
|
+
/* Default accents · POST variant (Washington Post · red) */
|
|
54
|
+
--accent: #C42126;
|
|
55
|
+
--accent-deep: #951C1F;
|
|
56
|
+
--accent-soft: #E6BFC1;
|
|
57
|
+
--accent-blue: #2C5282;
|
|
58
|
+
|
|
59
|
+
/* Shadow for paper stacks */
|
|
60
|
+
--shadow-page: 0 2px 8px rgba(20, 17, 11, 0.18),
|
|
61
|
+
0 16px 48px rgba(20, 17, 11, 0.20);
|
|
62
|
+
|
|
63
|
+
--serif-display: "Tiempos Headline", "Playfair Display", "Bodoni 72",
|
|
64
|
+
"Didot", "Source Serif Pro", "Charter", Georgia,
|
|
65
|
+
"Source Han Serif SC", "Songti SC", "STSong", serif;
|
|
66
|
+
--serif: "Charter", "Source Serif Pro", "Iowan Old Style",
|
|
67
|
+
"Tiempos Text", "Source Han Serif SC", Georgia,
|
|
68
|
+
"Songti SC", "STSong", serif;
|
|
69
|
+
--sans: "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont,
|
|
70
|
+
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
71
|
+
"Source Han Sans CN", "Noto Sans CJK SC", sans-serif;
|
|
72
|
+
--mono: "SF Mono", "JetBrains Mono", "Menlo",
|
|
73
|
+
"PingFang SC", "Source Han Sans CN", monospace;
|
|
74
|
+
--blackletter: "UnifrakturMaguntia", "Old English Text MT",
|
|
75
|
+
"Cloister Black", "Goudy Old Style",
|
|
76
|
+
"Tiempos Headline", Georgia, serif;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* TIMES variant · accent shifts from red to navy, paper goes very
|
|
80
|
+
slightly cooler (NYT pages have a less yellowed tint than WaPo's). */
|
|
81
|
+
body[data-np-variant="times"] {
|
|
82
|
+
--paper: #F8F5E8;
|
|
83
|
+
--paper-soft: #FCF9EE;
|
|
84
|
+
--paper-edge: #ECE5CE;
|
|
85
|
+
--rule: #BCB29A;
|
|
86
|
+
--rule-soft: #C9C0AA;
|
|
87
|
+
--rule-strong: #6E6549;
|
|
88
|
+
--accent: #14366A;
|
|
89
|
+
--accent-deep: #0A2148;
|
|
90
|
+
--accent-soft: #B8C5D9;
|
|
91
|
+
--accent-blue: #14366A;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
95
|
+
html, body {
|
|
96
|
+
background: var(--bg);
|
|
97
|
+
color: var(--ink);
|
|
98
|
+
font-family: var(--serif);
|
|
99
|
+
font-size: 14px;
|
|
100
|
+
line-height: 1.55;
|
|
101
|
+
-webkit-font-smoothing: antialiased;
|
|
102
|
+
text-rendering: optimizeLegibility;
|
|
103
|
+
min-height: 100vh;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ─── Top chrome ───────────────────────────────────────────────── */
|
|
107
|
+
.np-top-bar {
|
|
108
|
+
display: flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
justify-content: space-between;
|
|
111
|
+
gap: 14px;
|
|
112
|
+
padding: 14px 28px;
|
|
113
|
+
background: rgba(31, 30, 26, 0.92);
|
|
114
|
+
backdrop-filter: blur(10px);
|
|
115
|
+
-webkit-backdrop-filter: blur(10px);
|
|
116
|
+
border-bottom: 1px solid rgba(244, 239, 223, 0.12);
|
|
117
|
+
flex-wrap: wrap;
|
|
118
|
+
position: sticky;
|
|
119
|
+
top: 0;
|
|
120
|
+
z-index: 10;
|
|
121
|
+
color: var(--paper);
|
|
122
|
+
}
|
|
123
|
+
.np-crumb {
|
|
124
|
+
display: inline-flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: 12px;
|
|
127
|
+
font-family: var(--serif-display);
|
|
128
|
+
font-size: 16px;
|
|
129
|
+
font-weight: 700;
|
|
130
|
+
color: var(--paper);
|
|
131
|
+
text-decoration: none;
|
|
132
|
+
}
|
|
133
|
+
.np-crumb::before {
|
|
134
|
+
content: "";
|
|
135
|
+
width: 9px;
|
|
136
|
+
height: 9px;
|
|
137
|
+
background: var(--accent);
|
|
138
|
+
flex: 0 0 auto;
|
|
139
|
+
border-radius: 50%;
|
|
140
|
+
}
|
|
141
|
+
.np-crumb-accent {
|
|
142
|
+
color: var(--ink-faint);
|
|
143
|
+
font-style: italic;
|
|
144
|
+
font-weight: 400;
|
|
145
|
+
}
|
|
146
|
+
.np-actions { display: flex; gap: 8px; align-items: center; }
|
|
147
|
+
.np-page-nav {
|
|
148
|
+
display: inline-flex;
|
|
149
|
+
gap: 4px;
|
|
150
|
+
margin-right: 12px;
|
|
151
|
+
font-family: var(--mono);
|
|
152
|
+
font-size: 10px;
|
|
153
|
+
letter-spacing: 0.12em;
|
|
154
|
+
text-transform: uppercase;
|
|
155
|
+
}
|
|
156
|
+
.np-page-nav a {
|
|
157
|
+
color: var(--ink-muted);
|
|
158
|
+
padding: 4px 8px;
|
|
159
|
+
border: 1px solid rgba(244, 239, 223, 0.16);
|
|
160
|
+
text-decoration: none;
|
|
161
|
+
transition: all 0.12s;
|
|
162
|
+
}
|
|
163
|
+
.np-page-nav a:hover {
|
|
164
|
+
background: rgba(244, 239, 223, 0.10);
|
|
165
|
+
color: var(--paper);
|
|
166
|
+
}
|
|
167
|
+
.np-variant-badge {
|
|
168
|
+
font-family: var(--mono);
|
|
169
|
+
font-size: 10px;
|
|
170
|
+
letter-spacing: 0.16em;
|
|
171
|
+
text-transform: uppercase;
|
|
172
|
+
color: var(--accent-soft);
|
|
173
|
+
margin-right: 12px;
|
|
174
|
+
padding: 4px 10px;
|
|
175
|
+
background: rgba(244, 239, 223, 0.06);
|
|
176
|
+
border: 1px solid rgba(244, 239, 223, 0.16);
|
|
177
|
+
}
|
|
178
|
+
.np-btn {
|
|
179
|
+
font-family: var(--mono);
|
|
180
|
+
font-size: 10.5px;
|
|
181
|
+
letter-spacing: 0.04em;
|
|
182
|
+
padding: 7px 12px;
|
|
183
|
+
background: transparent;
|
|
184
|
+
border: 1px solid rgba(244, 239, 223, 0.30);
|
|
185
|
+
color: var(--paper);
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
text-decoration: none;
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
transition: background 0.15s, color 0.15s;
|
|
191
|
+
}
|
|
192
|
+
.np-btn:hover {
|
|
193
|
+
background: var(--paper);
|
|
194
|
+
color: var(--bg);
|
|
195
|
+
}
|
|
196
|
+
.np-btn .glyph { margin-right: 4px; }
|
|
197
|
+
|
|
198
|
+
/* ─── Doc · paper-stack frame ──────────────────────────────────── */
|
|
199
|
+
.np-doc {
|
|
200
|
+
max-width: 920px;
|
|
201
|
+
margin: 0 auto;
|
|
202
|
+
padding: 28px 12px 56px;
|
|
203
|
+
}
|
|
204
|
+
.np-page {
|
|
205
|
+
background: var(--paper);
|
|
206
|
+
padding: 26px 32px 30px;
|
|
207
|
+
margin-bottom: 24px;
|
|
208
|
+
box-shadow: var(--shadow-page);
|
|
209
|
+
position: relative;
|
|
210
|
+
}
|
|
211
|
+
.np-page:last-child { margin-bottom: 0; }
|
|
212
|
+
body[data-np-variant="times"] .np-page {
|
|
213
|
+
padding: 28px 36px 30px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ─── Rules · single + double · used as section dividers ─────── */
|
|
217
|
+
.np-rule { height: 1px; background: var(--ink); margin: 12px 0; }
|
|
218
|
+
.np-rule-thin { height: 1px; background: var(--rule); margin: 8px 0; }
|
|
219
|
+
.np-rule-double {
|
|
220
|
+
height: 5px;
|
|
221
|
+
background:
|
|
222
|
+
linear-gradient(to bottom,
|
|
223
|
+
var(--ink) 0,
|
|
224
|
+
var(--ink) 1px,
|
|
225
|
+
transparent 1px,
|
|
226
|
+
transparent 4px,
|
|
227
|
+
var(--ink) 4px,
|
|
228
|
+
var(--ink) 5px);
|
|
229
|
+
margin: 12px 0;
|
|
230
|
+
}
|
|
231
|
+
.np-rule-thick { height: 2px; background: var(--ink); margin: 14px 0; }
|
|
232
|
+
|
|
233
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
234
|
+
POST variant · The Boardroom Post · Washington-Post register
|
|
235
|
+
═══════════════════════════════════════════════════════════════ */
|
|
236
|
+
|
|
237
|
+
/* Section dots row · small accent dots above the masthead, each
|
|
238
|
+
paired with a section name. WaPo uses a colorful section nav
|
|
239
|
+
above the mast as a navigation aid. */
|
|
240
|
+
.post-sect-row {
|
|
241
|
+
display: flex;
|
|
242
|
+
justify-content: center;
|
|
243
|
+
gap: 22px;
|
|
244
|
+
padding: 2px 0 8px;
|
|
245
|
+
border-bottom: 1px solid var(--rule-soft);
|
|
246
|
+
margin-bottom: 10px;
|
|
247
|
+
flex-wrap: wrap;
|
|
248
|
+
}
|
|
249
|
+
.post-sect-item {
|
|
250
|
+
display: inline-flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
gap: 6px;
|
|
253
|
+
font-family: var(--sans);
|
|
254
|
+
font-size: 10px;
|
|
255
|
+
letter-spacing: 0.14em;
|
|
256
|
+
text-transform: uppercase;
|
|
257
|
+
color: var(--ink-mid);
|
|
258
|
+
font-weight: 700;
|
|
259
|
+
}
|
|
260
|
+
.post-sect-item::before {
|
|
261
|
+
content: "";
|
|
262
|
+
width: 6px;
|
|
263
|
+
height: 6px;
|
|
264
|
+
border-radius: 50%;
|
|
265
|
+
background: var(--accent);
|
|
266
|
+
}
|
|
267
|
+
.post-sect-item.is-blue::before { background: var(--accent-blue); }
|
|
268
|
+
.post-sect-item.is-gold::before { background: #C8A36F; }
|
|
269
|
+
.post-sect-item.is-teal::before { background: #2C7B7A; }
|
|
270
|
+
|
|
271
|
+
/* Tagline + masthead (POST) · smaller / cleaner than NYT */
|
|
272
|
+
.post-tagline {
|
|
273
|
+
font-family: var(--serif);
|
|
274
|
+
font-style: italic;
|
|
275
|
+
font-size: 12px;
|
|
276
|
+
color: var(--ink-mid);
|
|
277
|
+
text-align: center;
|
|
278
|
+
margin-bottom: 6px;
|
|
279
|
+
}
|
|
280
|
+
.post-mast-name {
|
|
281
|
+
font-family: var(--serif-display);
|
|
282
|
+
font-size: 50px;
|
|
283
|
+
font-weight: 800;
|
|
284
|
+
line-height: 0.96;
|
|
285
|
+
letter-spacing: -0.018em;
|
|
286
|
+
color: var(--ink);
|
|
287
|
+
text-align: center;
|
|
288
|
+
margin: 2px 0 6px;
|
|
289
|
+
}
|
|
290
|
+
@media (max-width: 720px) {
|
|
291
|
+
.post-mast-name { font-size: 36px; }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* WaPo meta strip · 3-cell with date/edition/copy */
|
|
295
|
+
.post-meta {
|
|
296
|
+
display: grid;
|
|
297
|
+
grid-template-columns: 1fr auto 1fr;
|
|
298
|
+
align-items: center;
|
|
299
|
+
gap: 14px;
|
|
300
|
+
padding: 6px 0;
|
|
301
|
+
border-top: 2px solid var(--ink);
|
|
302
|
+
border-bottom: 1px solid var(--ink);
|
|
303
|
+
font-family: var(--sans);
|
|
304
|
+
font-size: 10.5px;
|
|
305
|
+
letter-spacing: 0.08em;
|
|
306
|
+
text-transform: uppercase;
|
|
307
|
+
color: var(--ink-mid);
|
|
308
|
+
font-weight: 600;
|
|
309
|
+
}
|
|
310
|
+
.post-meta-l { text-align: left; }
|
|
311
|
+
.post-meta-c { text-align: center; }
|
|
312
|
+
.post-meta-r { text-align: right; }
|
|
313
|
+
|
|
314
|
+
/* Hero zone · big headline with deck + lead body + standfirst */
|
|
315
|
+
.post-hero {
|
|
316
|
+
padding: 14px 0 8px;
|
|
317
|
+
}
|
|
318
|
+
.post-hero-flag {
|
|
319
|
+
display: inline-block;
|
|
320
|
+
background: var(--accent);
|
|
321
|
+
color: var(--paper);
|
|
322
|
+
font-family: var(--sans);
|
|
323
|
+
font-size: 10px;
|
|
324
|
+
font-weight: 800;
|
|
325
|
+
letter-spacing: 0.18em;
|
|
326
|
+
text-transform: uppercase;
|
|
327
|
+
padding: 4px 10px;
|
|
328
|
+
margin-bottom: 8px;
|
|
329
|
+
}
|
|
330
|
+
.post-hero-title {
|
|
331
|
+
font-family: var(--serif-display);
|
|
332
|
+
font-size: 42px;
|
|
333
|
+
font-weight: 800;
|
|
334
|
+
line-height: 1.04;
|
|
335
|
+
letter-spacing: -0.012em;
|
|
336
|
+
color: var(--ink);
|
|
337
|
+
margin-bottom: 8px;
|
|
338
|
+
max-width: 760px;
|
|
339
|
+
}
|
|
340
|
+
@media (max-width: 720px) {
|
|
341
|
+
.post-hero-title { font-size: 30px; }
|
|
342
|
+
}
|
|
343
|
+
.post-hero-deck {
|
|
344
|
+
font-family: var(--serif);
|
|
345
|
+
font-style: italic;
|
|
346
|
+
font-size: 16px;
|
|
347
|
+
line-height: 1.45;
|
|
348
|
+
color: var(--ink-mid);
|
|
349
|
+
max-width: 720px;
|
|
350
|
+
margin-bottom: 8px;
|
|
351
|
+
}
|
|
352
|
+
.post-hero-byline {
|
|
353
|
+
font-family: var(--sans);
|
|
354
|
+
font-size: 11px;
|
|
355
|
+
letter-spacing: 0.06em;
|
|
356
|
+
text-transform: uppercase;
|
|
357
|
+
color: var(--ink-mid);
|
|
358
|
+
font-weight: 600;
|
|
359
|
+
margin-bottom: 4px;
|
|
360
|
+
}
|
|
361
|
+
.post-hero-byline em {
|
|
362
|
+
font-style: italic;
|
|
363
|
+
text-transform: none;
|
|
364
|
+
color: var(--ink-soft);
|
|
365
|
+
font-weight: 400;
|
|
366
|
+
letter-spacing: 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Standfirst panel · solid colored block carrying a pull-quote
|
|
370
|
+
(replaces the prior gradient portrait fake-photo). */
|
|
371
|
+
.post-stand {
|
|
372
|
+
background: var(--inv-bg);
|
|
373
|
+
color: var(--paper);
|
|
374
|
+
padding: 18px 22px;
|
|
375
|
+
margin: 10px 0;
|
|
376
|
+
display: grid;
|
|
377
|
+
grid-template-columns: 48px 1fr;
|
|
378
|
+
gap: 14px;
|
|
379
|
+
align-items: center;
|
|
380
|
+
}
|
|
381
|
+
.post-stand-mark {
|
|
382
|
+
font-family: var(--serif-display);
|
|
383
|
+
font-style: italic;
|
|
384
|
+
font-size: 54px;
|
|
385
|
+
line-height: 0.6;
|
|
386
|
+
color: var(--accent-soft);
|
|
387
|
+
}
|
|
388
|
+
.post-stand-text {
|
|
389
|
+
font-family: var(--serif-display);
|
|
390
|
+
font-style: italic;
|
|
391
|
+
font-size: 19px;
|
|
392
|
+
line-height: 1.32;
|
|
393
|
+
color: var(--paper);
|
|
394
|
+
letter-spacing: -0.005em;
|
|
395
|
+
}
|
|
396
|
+
.post-stand-cite {
|
|
397
|
+
font-family: var(--sans);
|
|
398
|
+
font-size: 10px;
|
|
399
|
+
letter-spacing: 0.12em;
|
|
400
|
+
text-transform: uppercase;
|
|
401
|
+
color: var(--inv-ink-soft);
|
|
402
|
+
margin-top: 8px;
|
|
403
|
+
display: block;
|
|
404
|
+
}
|
|
405
|
+
@media (max-width: 720px) {
|
|
406
|
+
.post-stand { grid-template-columns: 1fr; }
|
|
407
|
+
.post-stand-mark { display: none; }
|
|
408
|
+
.post-stand-text { font-size: 18px; }
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* 2-col body grid (POST) · single-column main story (no internal
|
|
412
|
+
multi-column · the brief data isn't dense enough to fill a
|
|
413
|
+
2-col body without leaving gaps) · sidebar with stacked tiles. */
|
|
414
|
+
.post-grid {
|
|
415
|
+
display: grid;
|
|
416
|
+
grid-template-columns: minmax(0, 1.7fr) minmax(180px, 1fr);
|
|
417
|
+
gap: 22px;
|
|
418
|
+
margin-top: 4px;
|
|
419
|
+
align-items: start;
|
|
420
|
+
}
|
|
421
|
+
@media (max-width: 720px) {
|
|
422
|
+
.post-grid { grid-template-columns: 1fr; gap: 18px; }
|
|
423
|
+
}
|
|
424
|
+
.post-body {
|
|
425
|
+
font-family: var(--serif);
|
|
426
|
+
font-size: 13.5px;
|
|
427
|
+
line-height: 1.62;
|
|
428
|
+
color: var(--ink-soft);
|
|
429
|
+
text-align: justify;
|
|
430
|
+
hyphens: auto;
|
|
431
|
+
-webkit-hyphens: auto;
|
|
432
|
+
}
|
|
433
|
+
.post-body p + p { margin-top: 8px; }
|
|
434
|
+
.post-body.has-drop::first-letter {
|
|
435
|
+
font-family: var(--serif-display);
|
|
436
|
+
font-size: 56px;
|
|
437
|
+
line-height: 0.85;
|
|
438
|
+
font-weight: 800;
|
|
439
|
+
color: var(--accent);
|
|
440
|
+
float: left;
|
|
441
|
+
margin: 6px 8px 0 0;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.post-side {
|
|
445
|
+
display: flex;
|
|
446
|
+
flex-direction: column;
|
|
447
|
+
gap: 12px;
|
|
448
|
+
padding-left: 22px;
|
|
449
|
+
border-left: 1px solid var(--rule);
|
|
450
|
+
}
|
|
451
|
+
@media (max-width: 720px) {
|
|
452
|
+
.post-side { border-left: 0; padding-left: 0; padding-top: 16px; border-top: 1px solid var(--rule); }
|
|
453
|
+
}
|
|
454
|
+
.post-side-flag {
|
|
455
|
+
display: inline-block;
|
|
456
|
+
font-family: var(--sans);
|
|
457
|
+
font-size: 10px;
|
|
458
|
+
font-weight: 800;
|
|
459
|
+
letter-spacing: 0.18em;
|
|
460
|
+
text-transform: uppercase;
|
|
461
|
+
color: var(--accent);
|
|
462
|
+
padding-bottom: 6px;
|
|
463
|
+
border-bottom: 2px solid var(--accent);
|
|
464
|
+
margin-bottom: 6px;
|
|
465
|
+
align-self: flex-start;
|
|
466
|
+
}
|
|
467
|
+
.post-side-head {
|
|
468
|
+
font-family: var(--serif-display);
|
|
469
|
+
font-size: 18px;
|
|
470
|
+
font-weight: 700;
|
|
471
|
+
line-height: 1.2;
|
|
472
|
+
letter-spacing: -0.008em;
|
|
473
|
+
color: var(--ink);
|
|
474
|
+
}
|
|
475
|
+
.post-side-body {
|
|
476
|
+
font-family: var(--serif);
|
|
477
|
+
font-size: 12.5px;
|
|
478
|
+
line-height: 1.55;
|
|
479
|
+
color: var(--ink-soft);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* Stat tile (POST register) · cream box with hairline border +
|
|
483
|
+
red bar accent. Numeric size sits at 36px to fit the narrow
|
|
484
|
+
sidebar (~200px) without wrapping awkwardly. word-break
|
|
485
|
+
ensures any 12-char callout still fits cleanly. */
|
|
486
|
+
.post-stat {
|
|
487
|
+
background: var(--paper-soft);
|
|
488
|
+
padding: 16px 16px 16px;
|
|
489
|
+
border-top: 4px solid var(--accent);
|
|
490
|
+
margin-top: 6px;
|
|
491
|
+
}
|
|
492
|
+
.post-stat-eye {
|
|
493
|
+
font-family: var(--sans);
|
|
494
|
+
font-size: 10px;
|
|
495
|
+
font-weight: 800;
|
|
496
|
+
letter-spacing: 0.18em;
|
|
497
|
+
text-transform: uppercase;
|
|
498
|
+
color: var(--accent);
|
|
499
|
+
margin-bottom: 6px;
|
|
500
|
+
}
|
|
501
|
+
.post-stat-num {
|
|
502
|
+
font-family: var(--serif-display);
|
|
503
|
+
font-size: 36px;
|
|
504
|
+
font-weight: 800;
|
|
505
|
+
line-height: 1.0;
|
|
506
|
+
letter-spacing: -0.022em;
|
|
507
|
+
color: var(--ink);
|
|
508
|
+
word-break: break-word;
|
|
509
|
+
overflow-wrap: anywhere;
|
|
510
|
+
}
|
|
511
|
+
.post-stat-cap {
|
|
512
|
+
font-family: var(--serif);
|
|
513
|
+
font-style: italic;
|
|
514
|
+
font-size: 12.5px;
|
|
515
|
+
line-height: 1.4;
|
|
516
|
+
color: var(--ink-mid);
|
|
517
|
+
margin-top: 6px;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Index strip at bottom of front page · "Inside this issue" */
|
|
521
|
+
.post-index {
|
|
522
|
+
display: grid;
|
|
523
|
+
grid-template-columns: auto 1fr;
|
|
524
|
+
gap: 18px;
|
|
525
|
+
align-items: start;
|
|
526
|
+
margin-top: 16px;
|
|
527
|
+
padding-top: 12px;
|
|
528
|
+
border-top: 2px solid var(--ink);
|
|
529
|
+
}
|
|
530
|
+
.post-index-flag {
|
|
531
|
+
font-family: var(--sans);
|
|
532
|
+
font-size: 10px;
|
|
533
|
+
font-weight: 800;
|
|
534
|
+
letter-spacing: 0.18em;
|
|
535
|
+
text-transform: uppercase;
|
|
536
|
+
color: var(--ink);
|
|
537
|
+
padding: 4px 10px;
|
|
538
|
+
border: 1px solid var(--ink);
|
|
539
|
+
background: var(--paper);
|
|
540
|
+
align-self: flex-start;
|
|
541
|
+
}
|
|
542
|
+
.post-index-list {
|
|
543
|
+
list-style: none;
|
|
544
|
+
margin: 0;
|
|
545
|
+
padding: 0;
|
|
546
|
+
display: grid;
|
|
547
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
548
|
+
gap: 8px 22px;
|
|
549
|
+
}
|
|
550
|
+
.post-index-list li {
|
|
551
|
+
display: grid;
|
|
552
|
+
grid-template-columns: 1fr auto;
|
|
553
|
+
gap: 6px;
|
|
554
|
+
align-items: baseline;
|
|
555
|
+
font-family: var(--serif);
|
|
556
|
+
font-size: 12.5px;
|
|
557
|
+
line-height: 1.4;
|
|
558
|
+
color: var(--ink-soft);
|
|
559
|
+
border-bottom: 1px dotted var(--rule-strong);
|
|
560
|
+
padding-bottom: 4px;
|
|
561
|
+
}
|
|
562
|
+
.post-index-list li b {
|
|
563
|
+
font-family: var(--serif-display);
|
|
564
|
+
font-weight: 700;
|
|
565
|
+
color: var(--ink);
|
|
566
|
+
}
|
|
567
|
+
.post-index-list li .pg {
|
|
568
|
+
font-family: var(--sans);
|
|
569
|
+
font-size: 10px;
|
|
570
|
+
letter-spacing: 0.1em;
|
|
571
|
+
text-transform: uppercase;
|
|
572
|
+
color: var(--accent);
|
|
573
|
+
font-weight: 700;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/* POST inside running header · running mast + page indicator */
|
|
577
|
+
.post-running {
|
|
578
|
+
display: grid;
|
|
579
|
+
grid-template-columns: auto 1fr auto;
|
|
580
|
+
gap: 18px;
|
|
581
|
+
align-items: center;
|
|
582
|
+
padding-bottom: 8px;
|
|
583
|
+
border-bottom: 2px solid var(--ink);
|
|
584
|
+
}
|
|
585
|
+
.post-running-mast {
|
|
586
|
+
font-family: var(--serif-display);
|
|
587
|
+
font-size: 18px;
|
|
588
|
+
font-weight: 800;
|
|
589
|
+
color: var(--ink);
|
|
590
|
+
letter-spacing: -0.005em;
|
|
591
|
+
}
|
|
592
|
+
.post-running-mast::before {
|
|
593
|
+
content: "";
|
|
594
|
+
display: inline-block;
|
|
595
|
+
width: 8px;
|
|
596
|
+
height: 8px;
|
|
597
|
+
background: var(--accent);
|
|
598
|
+
margin-right: 8px;
|
|
599
|
+
vertical-align: middle;
|
|
600
|
+
border-radius: 50%;
|
|
601
|
+
}
|
|
602
|
+
.post-running-meta {
|
|
603
|
+
text-align: center;
|
|
604
|
+
font-family: var(--sans);
|
|
605
|
+
font-size: 10.5px;
|
|
606
|
+
letter-spacing: 0.06em;
|
|
607
|
+
text-transform: uppercase;
|
|
608
|
+
color: var(--ink-mid);
|
|
609
|
+
}
|
|
610
|
+
.post-running-page {
|
|
611
|
+
font-family: var(--sans);
|
|
612
|
+
font-size: 11px;
|
|
613
|
+
letter-spacing: 0.14em;
|
|
614
|
+
text-transform: uppercase;
|
|
615
|
+
color: var(--accent);
|
|
616
|
+
font-weight: 800;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* Inside section banner */
|
|
620
|
+
.post-sect-banner {
|
|
621
|
+
display: flex;
|
|
622
|
+
align-items: center;
|
|
623
|
+
gap: 14px;
|
|
624
|
+
margin: 14px 0 12px;
|
|
625
|
+
}
|
|
626
|
+
.post-sect-banner-tag {
|
|
627
|
+
font-family: var(--sans);
|
|
628
|
+
font-size: 11px;
|
|
629
|
+
font-weight: 800;
|
|
630
|
+
letter-spacing: 0.18em;
|
|
631
|
+
text-transform: uppercase;
|
|
632
|
+
color: var(--paper);
|
|
633
|
+
background: var(--accent);
|
|
634
|
+
padding: 5px 12px;
|
|
635
|
+
}
|
|
636
|
+
.post-sect-banner-deck {
|
|
637
|
+
font-family: var(--serif);
|
|
638
|
+
font-style: italic;
|
|
639
|
+
font-size: 13px;
|
|
640
|
+
color: var(--ink-mid);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* Inside article (POST) · headline + body */
|
|
644
|
+
.post-article {
|
|
645
|
+
display: grid;
|
|
646
|
+
grid-template-columns: minmax(0, 1.6fr) minmax(180px, 1fr);
|
|
647
|
+
gap: 28px;
|
|
648
|
+
margin-top: 6px;
|
|
649
|
+
}
|
|
650
|
+
@media (max-width: 720px) {
|
|
651
|
+
.post-article { grid-template-columns: 1fr; gap: 20px; }
|
|
652
|
+
}
|
|
653
|
+
.post-article-head {
|
|
654
|
+
font-family: var(--serif-display);
|
|
655
|
+
font-size: 32px;
|
|
656
|
+
font-weight: 800;
|
|
657
|
+
line-height: 1.08;
|
|
658
|
+
letter-spacing: -0.012em;
|
|
659
|
+
color: var(--ink);
|
|
660
|
+
margin-bottom: 8px;
|
|
661
|
+
}
|
|
662
|
+
.post-article-deck {
|
|
663
|
+
font-family: var(--serif);
|
|
664
|
+
font-style: italic;
|
|
665
|
+
font-size: 14px;
|
|
666
|
+
line-height: 1.5;
|
|
667
|
+
color: var(--ink-mid);
|
|
668
|
+
margin-bottom: 8px;
|
|
669
|
+
}
|
|
670
|
+
.post-article-byline {
|
|
671
|
+
font-family: var(--sans);
|
|
672
|
+
font-size: 10.5px;
|
|
673
|
+
letter-spacing: 0.08em;
|
|
674
|
+
text-transform: uppercase;
|
|
675
|
+
color: var(--ink-mid);
|
|
676
|
+
font-weight: 700;
|
|
677
|
+
padding-bottom: 10px;
|
|
678
|
+
border-bottom: 1px solid var(--rule);
|
|
679
|
+
margin-bottom: 12px;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/* Pull-quote inline (POST) · vertical red bar + italic */
|
|
683
|
+
.post-pull {
|
|
684
|
+
border-left: 4px solid var(--accent);
|
|
685
|
+
padding: 8px 18px;
|
|
686
|
+
margin: 14px 0;
|
|
687
|
+
}
|
|
688
|
+
.post-pull-text {
|
|
689
|
+
font-family: var(--serif-display);
|
|
690
|
+
font-style: italic;
|
|
691
|
+
font-size: 19px;
|
|
692
|
+
line-height: 1.35;
|
|
693
|
+
color: var(--ink);
|
|
694
|
+
letter-spacing: -0.005em;
|
|
695
|
+
}
|
|
696
|
+
.post-pull-cite {
|
|
697
|
+
font-family: var(--sans);
|
|
698
|
+
font-size: 10px;
|
|
699
|
+
letter-spacing: 0.14em;
|
|
700
|
+
text-transform: uppercase;
|
|
701
|
+
color: var(--accent);
|
|
702
|
+
font-weight: 700;
|
|
703
|
+
margin-top: 6px;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
707
|
+
TIMES variant · The Boardroom · New York Times register
|
|
708
|
+
═══════════════════════════════════════════════════════════════ */
|
|
709
|
+
|
|
710
|
+
.times-tagline {
|
|
711
|
+
font-family: var(--serif);
|
|
712
|
+
font-style: italic;
|
|
713
|
+
font-size: 11.5px;
|
|
714
|
+
color: var(--ink-mid);
|
|
715
|
+
text-align: center;
|
|
716
|
+
padding: 2px 0 6px;
|
|
717
|
+
}
|
|
718
|
+
.times-mast-name {
|
|
719
|
+
font-family: var(--blackletter);
|
|
720
|
+
font-weight: 400;
|
|
721
|
+
font-size: 72px;
|
|
722
|
+
line-height: 1;
|
|
723
|
+
letter-spacing: 0.005em;
|
|
724
|
+
color: var(--ink);
|
|
725
|
+
text-align: center;
|
|
726
|
+
margin: 4px 0 8px;
|
|
727
|
+
}
|
|
728
|
+
@media (max-width: 720px) {
|
|
729
|
+
.times-mast-name { font-size: 46px; }
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/* TIMES volume strip · "VOL. CXC...No 12,345 ★ Late Edition ★
|
|
733
|
+
{Date} ★ $4.00" */
|
|
734
|
+
.times-vol-strip {
|
|
735
|
+
display: flex;
|
|
736
|
+
align-items: center;
|
|
737
|
+
justify-content: center;
|
|
738
|
+
gap: 18px;
|
|
739
|
+
padding: 6px 0;
|
|
740
|
+
border-top: 2px solid var(--ink);
|
|
741
|
+
border-bottom: 1px solid var(--ink);
|
|
742
|
+
font-family: var(--serif);
|
|
743
|
+
font-size: 11.5px;
|
|
744
|
+
color: var(--ink-soft);
|
|
745
|
+
flex-wrap: wrap;
|
|
746
|
+
}
|
|
747
|
+
.times-vol-strip .star {
|
|
748
|
+
color: var(--ink);
|
|
749
|
+
font-size: 12px;
|
|
750
|
+
line-height: 1;
|
|
751
|
+
}
|
|
752
|
+
.times-vol-strip em {
|
|
753
|
+
font-style: italic;
|
|
754
|
+
color: var(--ink);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/* TIMES front · 4-col grid (1+2+1 spans) · packs better than the
|
|
758
|
+
classical 6-col when content is brief-sized; the lead is the
|
|
759
|
+
visual center, sidebars stay narrow but full of content. */
|
|
760
|
+
.times-front {
|
|
761
|
+
display: grid;
|
|
762
|
+
grid-template-columns: repeat(4, 1fr);
|
|
763
|
+
gap: 22px;
|
|
764
|
+
margin-top: 14px;
|
|
765
|
+
align-items: start;
|
|
766
|
+
}
|
|
767
|
+
@media (max-width: 720px) {
|
|
768
|
+
.times-front { grid-template-columns: 1fr; }
|
|
769
|
+
}
|
|
770
|
+
.times-col {
|
|
771
|
+
display: flex;
|
|
772
|
+
flex-direction: column;
|
|
773
|
+
gap: 8px;
|
|
774
|
+
min-width: 0;
|
|
775
|
+
}
|
|
776
|
+
.times-col + .times-col {
|
|
777
|
+
padding-left: 22px;
|
|
778
|
+
border-left: 1px solid var(--rule);
|
|
779
|
+
}
|
|
780
|
+
@media (max-width: 720px) {
|
|
781
|
+
.times-col + .times-col {
|
|
782
|
+
padding-left: 0;
|
|
783
|
+
border-left: 0;
|
|
784
|
+
padding-top: 16px;
|
|
785
|
+
border-top: 1px solid var(--rule);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
.times-col-1 { grid-column: span 1; }
|
|
789
|
+
.times-col-2 { grid-column: span 1; }
|
|
790
|
+
.times-col-3 { grid-column: span 2; }
|
|
791
|
+
|
|
792
|
+
.times-section-eye {
|
|
793
|
+
font-family: var(--serif);
|
|
794
|
+
font-size: 10px;
|
|
795
|
+
letter-spacing: 0.16em;
|
|
796
|
+
text-transform: uppercase;
|
|
797
|
+
color: var(--ink-mid);
|
|
798
|
+
font-weight: 700;
|
|
799
|
+
text-align: center;
|
|
800
|
+
padding-bottom: 6px;
|
|
801
|
+
border-bottom: 1px solid var(--ink);
|
|
802
|
+
margin-bottom: 8px;
|
|
803
|
+
}
|
|
804
|
+
.times-section-eye::before { content: "★ "; color: var(--ink); margin-right: 4px; }
|
|
805
|
+
.times-section-eye::after { content: " ★"; color: var(--ink); margin-left: 4px; }
|
|
806
|
+
|
|
807
|
+
.times-head-large {
|
|
808
|
+
font-family: var(--serif-display);
|
|
809
|
+
font-size: 34px;
|
|
810
|
+
font-weight: 700;
|
|
811
|
+
line-height: 1.05;
|
|
812
|
+
letter-spacing: -0.014em;
|
|
813
|
+
color: var(--ink);
|
|
814
|
+
text-align: center;
|
|
815
|
+
}
|
|
816
|
+
@media (max-width: 720px) {
|
|
817
|
+
.times-head-large { font-size: 26px; }
|
|
818
|
+
}
|
|
819
|
+
.times-head-medium {
|
|
820
|
+
font-family: var(--serif-display);
|
|
821
|
+
font-size: 24px;
|
|
822
|
+
font-weight: 700;
|
|
823
|
+
line-height: 1.12;
|
|
824
|
+
letter-spacing: -0.008em;
|
|
825
|
+
color: var(--ink);
|
|
826
|
+
}
|
|
827
|
+
.times-head-small {
|
|
828
|
+
font-family: var(--serif-display);
|
|
829
|
+
font-size: 17px;
|
|
830
|
+
font-weight: 700;
|
|
831
|
+
line-height: 1.18;
|
|
832
|
+
letter-spacing: -0.005em;
|
|
833
|
+
color: var(--ink);
|
|
834
|
+
}
|
|
835
|
+
.times-deck {
|
|
836
|
+
font-family: var(--serif);
|
|
837
|
+
font-style: italic;
|
|
838
|
+
font-size: 14.5px;
|
|
839
|
+
line-height: 1.4;
|
|
840
|
+
color: var(--ink-mid);
|
|
841
|
+
text-align: center;
|
|
842
|
+
margin: 6px 0 10px;
|
|
843
|
+
}
|
|
844
|
+
.times-byline {
|
|
845
|
+
font-family: var(--serif);
|
|
846
|
+
font-size: 11px;
|
|
847
|
+
letter-spacing: 0.12em;
|
|
848
|
+
text-transform: uppercase;
|
|
849
|
+
color: var(--ink-mid);
|
|
850
|
+
text-align: center;
|
|
851
|
+
margin-bottom: 8px;
|
|
852
|
+
}
|
|
853
|
+
.times-byline em {
|
|
854
|
+
font-style: italic;
|
|
855
|
+
color: var(--ink);
|
|
856
|
+
}
|
|
857
|
+
.times-body {
|
|
858
|
+
font-family: var(--serif);
|
|
859
|
+
font-size: 12.5px;
|
|
860
|
+
line-height: 1.55;
|
|
861
|
+
color: var(--ink-soft);
|
|
862
|
+
text-align: justify;
|
|
863
|
+
hyphens: auto;
|
|
864
|
+
-webkit-hyphens: auto;
|
|
865
|
+
}
|
|
866
|
+
.times-body p + p { margin-top: 6px; text-indent: 1.2em; }
|
|
867
|
+
.times-body.has-drop::first-letter {
|
|
868
|
+
font-family: var(--serif-display);
|
|
869
|
+
font-size: 48px;
|
|
870
|
+
line-height: 0.85;
|
|
871
|
+
font-weight: 700;
|
|
872
|
+
color: var(--ink);
|
|
873
|
+
float: left;
|
|
874
|
+
margin: 4px 6px 0 0;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/* TIMES standfirst · solid block with serif italic quote · used
|
|
878
|
+
in the centered hero column */
|
|
879
|
+
.times-stand {
|
|
880
|
+
background: var(--ink);
|
|
881
|
+
color: var(--paper);
|
|
882
|
+
padding: 16px 20px;
|
|
883
|
+
margin: 10px 0;
|
|
884
|
+
text-align: center;
|
|
885
|
+
}
|
|
886
|
+
.times-stand-mark {
|
|
887
|
+
font-family: var(--blackletter);
|
|
888
|
+
font-size: 32px;
|
|
889
|
+
color: var(--paper);
|
|
890
|
+
line-height: 1;
|
|
891
|
+
margin-bottom: 8px;
|
|
892
|
+
}
|
|
893
|
+
.times-stand-text {
|
|
894
|
+
font-family: var(--serif-display);
|
|
895
|
+
font-style: italic;
|
|
896
|
+
font-size: 18px;
|
|
897
|
+
line-height: 1.35;
|
|
898
|
+
color: var(--paper);
|
|
899
|
+
letter-spacing: -0.005em;
|
|
900
|
+
}
|
|
901
|
+
.times-stand-cite {
|
|
902
|
+
font-family: var(--serif);
|
|
903
|
+
font-size: 10.5px;
|
|
904
|
+
letter-spacing: 0.14em;
|
|
905
|
+
text-transform: uppercase;
|
|
906
|
+
color: var(--inv-ink-soft);
|
|
907
|
+
margin-top: 10px;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/* TIMES stat tile · serif numeric on cream · narrow sidebar
|
|
911
|
+
constraint forces 30px max for the numeric so 5-char strings
|
|
912
|
+
("$120M" / "2,000") fit cleanly without breaking. */
|
|
913
|
+
.times-stat {
|
|
914
|
+
background: var(--paper-soft);
|
|
915
|
+
padding: 14px 14px;
|
|
916
|
+
border: 1px solid var(--ink);
|
|
917
|
+
text-align: center;
|
|
918
|
+
margin: 8px 0;
|
|
919
|
+
}
|
|
920
|
+
.times-stat-eye {
|
|
921
|
+
font-family: var(--serif);
|
|
922
|
+
font-size: 10px;
|
|
923
|
+
letter-spacing: 0.16em;
|
|
924
|
+
text-transform: uppercase;
|
|
925
|
+
color: var(--ink-mid);
|
|
926
|
+
font-weight: 700;
|
|
927
|
+
padding-bottom: 6px;
|
|
928
|
+
border-bottom: 1px solid var(--rule);
|
|
929
|
+
margin-bottom: 8px;
|
|
930
|
+
}
|
|
931
|
+
.times-stat-eye::before { content: "★ "; color: var(--ink); }
|
|
932
|
+
.times-stat-eye::after { content: " ★"; color: var(--ink); }
|
|
933
|
+
.times-stat-num {
|
|
934
|
+
font-family: var(--serif-display);
|
|
935
|
+
font-size: 30px;
|
|
936
|
+
font-weight: 700;
|
|
937
|
+
line-height: 1.0;
|
|
938
|
+
letter-spacing: -0.018em;
|
|
939
|
+
color: var(--ink);
|
|
940
|
+
word-break: break-word;
|
|
941
|
+
overflow-wrap: anywhere;
|
|
942
|
+
}
|
|
943
|
+
.times-stat-cap {
|
|
944
|
+
font-family: var(--serif);
|
|
945
|
+
font-style: italic;
|
|
946
|
+
font-size: 12px;
|
|
947
|
+
line-height: 1.4;
|
|
948
|
+
color: var(--ink-mid);
|
|
949
|
+
margin-top: 6px;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/* TIMES inside running header · plain minimal */
|
|
953
|
+
.times-running {
|
|
954
|
+
display: grid;
|
|
955
|
+
grid-template-columns: auto 1fr auto;
|
|
956
|
+
align-items: center;
|
|
957
|
+
gap: 16px;
|
|
958
|
+
padding-bottom: 8px;
|
|
959
|
+
border-bottom: 1px solid var(--ink);
|
|
960
|
+
font-family: var(--serif);
|
|
961
|
+
font-size: 11px;
|
|
962
|
+
color: var(--ink);
|
|
963
|
+
}
|
|
964
|
+
.times-running-l {
|
|
965
|
+
font-family: var(--serif-display);
|
|
966
|
+
font-size: 14px;
|
|
967
|
+
font-weight: 700;
|
|
968
|
+
letter-spacing: 0.005em;
|
|
969
|
+
}
|
|
970
|
+
.times-running-c {
|
|
971
|
+
text-align: center;
|
|
972
|
+
font-style: italic;
|
|
973
|
+
color: var(--ink-mid);
|
|
974
|
+
}
|
|
975
|
+
.times-running-r {
|
|
976
|
+
font-family: var(--serif);
|
|
977
|
+
font-size: 11px;
|
|
978
|
+
letter-spacing: 0.16em;
|
|
979
|
+
text-transform: uppercase;
|
|
980
|
+
color: var(--ink);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/* TIMES inside section banner · centered with rules + caps */
|
|
984
|
+
.times-section-banner {
|
|
985
|
+
text-align: center;
|
|
986
|
+
margin: 18px 0 14px;
|
|
987
|
+
}
|
|
988
|
+
.times-section-banner-rule {
|
|
989
|
+
height: 1px;
|
|
990
|
+
background: var(--ink);
|
|
991
|
+
margin: 6px 0;
|
|
992
|
+
}
|
|
993
|
+
.times-section-banner-name {
|
|
994
|
+
font-family: var(--serif-display);
|
|
995
|
+
font-size: 22px;
|
|
996
|
+
font-weight: 700;
|
|
997
|
+
letter-spacing: 0.16em;
|
|
998
|
+
text-transform: uppercase;
|
|
999
|
+
color: var(--ink);
|
|
1000
|
+
padding: 0 4px;
|
|
1001
|
+
}
|
|
1002
|
+
.times-section-banner-deck {
|
|
1003
|
+
font-family: var(--serif);
|
|
1004
|
+
font-style: italic;
|
|
1005
|
+
font-size: 12.5px;
|
|
1006
|
+
color: var(--ink-mid);
|
|
1007
|
+
margin-top: 4px;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/* TIMES inside · 4-col article spread */
|
|
1011
|
+
.times-inside {
|
|
1012
|
+
display: grid;
|
|
1013
|
+
grid-template-columns: minmax(0, 2.6fr) minmax(180px, 1fr);
|
|
1014
|
+
gap: 28px;
|
|
1015
|
+
margin-top: 4px;
|
|
1016
|
+
}
|
|
1017
|
+
@media (max-width: 720px) {
|
|
1018
|
+
.times-inside { grid-template-columns: 1fr; }
|
|
1019
|
+
}
|
|
1020
|
+
.times-inside-main {
|
|
1021
|
+
column-count: 2;
|
|
1022
|
+
column-gap: 22px;
|
|
1023
|
+
column-rule: 1px solid var(--rule);
|
|
1024
|
+
font-family: var(--serif);
|
|
1025
|
+
font-size: 12.5px;
|
|
1026
|
+
line-height: 1.6;
|
|
1027
|
+
color: var(--ink-soft);
|
|
1028
|
+
text-align: justify;
|
|
1029
|
+
hyphens: auto;
|
|
1030
|
+
-webkit-hyphens: auto;
|
|
1031
|
+
}
|
|
1032
|
+
@media (max-width: 540px) {
|
|
1033
|
+
.times-inside-main { column-count: 1; }
|
|
1034
|
+
}
|
|
1035
|
+
.times-inside-main p + p { margin-top: 6px; text-indent: 1.2em; }
|
|
1036
|
+
.times-inside-main.has-drop::first-letter {
|
|
1037
|
+
font-family: var(--serif-display);
|
|
1038
|
+
font-size: 52px;
|
|
1039
|
+
line-height: 0.85;
|
|
1040
|
+
font-weight: 700;
|
|
1041
|
+
color: var(--ink);
|
|
1042
|
+
float: left;
|
|
1043
|
+
margin: 4px 6px 0 0;
|
|
1044
|
+
}
|
|
1045
|
+
.times-inside-side {
|
|
1046
|
+
display: flex;
|
|
1047
|
+
flex-direction: column;
|
|
1048
|
+
gap: 14px;
|
|
1049
|
+
padding-left: 22px;
|
|
1050
|
+
border-left: 1px solid var(--rule);
|
|
1051
|
+
}
|
|
1052
|
+
@media (max-width: 720px) {
|
|
1053
|
+
.times-inside-side { border-left: 0; padding-left: 0; padding-top: 18px; border-top: 1px solid var(--rule); }
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/* TIMES "Continued from page A1" tag */
|
|
1057
|
+
.times-continued {
|
|
1058
|
+
font-family: var(--serif);
|
|
1059
|
+
font-style: italic;
|
|
1060
|
+
font-size: 11px;
|
|
1061
|
+
color: var(--accent);
|
|
1062
|
+
margin-bottom: 8px;
|
|
1063
|
+
}
|
|
1064
|
+
.times-continued::before { content: "→ "; font-style: normal; color: var(--ink); }
|
|
1065
|
+
|
|
1066
|
+
/* TIMES pull-quote inline · star ornaments */
|
|
1067
|
+
.times-pull {
|
|
1068
|
+
text-align: center;
|
|
1069
|
+
margin: 14px 0;
|
|
1070
|
+
padding: 10px 0;
|
|
1071
|
+
border-top: 1px solid var(--ink);
|
|
1072
|
+
border-bottom: 1px solid var(--ink);
|
|
1073
|
+
}
|
|
1074
|
+
.times-pull-text {
|
|
1075
|
+
font-family: var(--serif-display);
|
|
1076
|
+
font-style: italic;
|
|
1077
|
+
font-size: 19px;
|
|
1078
|
+
line-height: 1.4;
|
|
1079
|
+
color: var(--ink);
|
|
1080
|
+
letter-spacing: -0.005em;
|
|
1081
|
+
}
|
|
1082
|
+
.times-pull-cite {
|
|
1083
|
+
font-family: var(--serif);
|
|
1084
|
+
font-size: 10.5px;
|
|
1085
|
+
letter-spacing: 0.16em;
|
|
1086
|
+
text-transform: uppercase;
|
|
1087
|
+
color: var(--ink-mid);
|
|
1088
|
+
margin-top: 6px;
|
|
1089
|
+
}
|
|
1090
|
+
.times-pull-cite::before { content: "★ "; color: var(--accent); }
|
|
1091
|
+
.times-pull-cite::after { content: " ★"; color: var(--accent); }
|
|
1092
|
+
|
|
1093
|
+
/* TIMES sidebar · "What's inside" + verification list */
|
|
1094
|
+
.times-side-flag {
|
|
1095
|
+
font-family: var(--serif-display);
|
|
1096
|
+
font-size: 14px;
|
|
1097
|
+
font-weight: 700;
|
|
1098
|
+
letter-spacing: 0.04em;
|
|
1099
|
+
color: var(--ink);
|
|
1100
|
+
text-align: center;
|
|
1101
|
+
padding: 4px 0;
|
|
1102
|
+
border-top: 2px solid var(--ink);
|
|
1103
|
+
border-bottom: 1px solid var(--ink);
|
|
1104
|
+
}
|
|
1105
|
+
.times-side-list {
|
|
1106
|
+
list-style: none;
|
|
1107
|
+
margin: 0;
|
|
1108
|
+
padding: 0;
|
|
1109
|
+
display: flex;
|
|
1110
|
+
flex-direction: column;
|
|
1111
|
+
}
|
|
1112
|
+
.times-side-list li {
|
|
1113
|
+
padding: 10px 0;
|
|
1114
|
+
border-bottom: 1px solid var(--rule);
|
|
1115
|
+
font-family: var(--serif);
|
|
1116
|
+
font-size: 12px;
|
|
1117
|
+
line-height: 1.55;
|
|
1118
|
+
color: var(--ink-soft);
|
|
1119
|
+
}
|
|
1120
|
+
.times-side-list li:last-child { border-bottom: 0; }
|
|
1121
|
+
.times-side-list li b {
|
|
1122
|
+
display: block;
|
|
1123
|
+
font-family: var(--serif-display);
|
|
1124
|
+
font-size: 13.5px;
|
|
1125
|
+
font-weight: 700;
|
|
1126
|
+
color: var(--ink);
|
|
1127
|
+
letter-spacing: -0.004em;
|
|
1128
|
+
margin-bottom: 4px;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/* ═══════════════════════════════════════════════════════════════
|
|
1132
|
+
Shared · page footer + states + print
|
|
1133
|
+
═══════════════════════════════════════════════════════════════ */
|
|
1134
|
+
|
|
1135
|
+
.np-page-foot {
|
|
1136
|
+
display: grid;
|
|
1137
|
+
grid-template-columns: 1fr auto 1fr;
|
|
1138
|
+
align-items: baseline;
|
|
1139
|
+
gap: 14px;
|
|
1140
|
+
padding-top: 14px;
|
|
1141
|
+
margin-top: 18px;
|
|
1142
|
+
border-top: 1px solid var(--rule);
|
|
1143
|
+
font-family: var(--mono);
|
|
1144
|
+
font-size: 10px;
|
|
1145
|
+
letter-spacing: 0.08em;
|
|
1146
|
+
text-transform: uppercase;
|
|
1147
|
+
color: var(--ink-faint);
|
|
1148
|
+
}
|
|
1149
|
+
.np-page-foot-left { text-align: left; }
|
|
1150
|
+
.np-page-foot-mid { text-align: center; color: var(--ink-mid); font-weight: 700; }
|
|
1151
|
+
.np-page-foot-right { text-align: right; font-style: italic; text-transform: none; }
|
|
1152
|
+
|
|
1153
|
+
.np-state {
|
|
1154
|
+
max-width: 560px;
|
|
1155
|
+
margin: 80px auto;
|
|
1156
|
+
padding: 48px 36px;
|
|
1157
|
+
text-align: center;
|
|
1158
|
+
background: var(--paper);
|
|
1159
|
+
box-shadow: var(--shadow-page);
|
|
1160
|
+
}
|
|
1161
|
+
.np-state-mark {
|
|
1162
|
+
font-family: var(--mono);
|
|
1163
|
+
font-size: 10px;
|
|
1164
|
+
letter-spacing: 0.18em;
|
|
1165
|
+
text-transform: uppercase;
|
|
1166
|
+
color: var(--ink);
|
|
1167
|
+
border: 1px solid var(--ink);
|
|
1168
|
+
padding: 5px 14px;
|
|
1169
|
+
display: inline-block;
|
|
1170
|
+
margin-bottom: 16px;
|
|
1171
|
+
font-weight: 700;
|
|
1172
|
+
}
|
|
1173
|
+
.np-state-title {
|
|
1174
|
+
font-family: var(--serif-display);
|
|
1175
|
+
font-size: 24px;
|
|
1176
|
+
font-weight: 700;
|
|
1177
|
+
color: var(--ink);
|
|
1178
|
+
margin-bottom: 10px;
|
|
1179
|
+
line-height: 1.2;
|
|
1180
|
+
text-transform: uppercase;
|
|
1181
|
+
letter-spacing: 0.02em;
|
|
1182
|
+
}
|
|
1183
|
+
.np-state-body {
|
|
1184
|
+
font-family: var(--serif);
|
|
1185
|
+
font-size: 13.5px;
|
|
1186
|
+
color: var(--ink-soft);
|
|
1187
|
+
line-height: 1.6;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
@media print {
|
|
1191
|
+
.np-top-bar { display: none; }
|
|
1192
|
+
body, html { background: white; }
|
|
1193
|
+
.np-doc { max-width: none; padding: 0; margin: 0; }
|
|
1194
|
+
.np-page {
|
|
1195
|
+
box-shadow: none;
|
|
1196
|
+
margin: 0;
|
|
1197
|
+
padding: 16mm 18mm 18mm;
|
|
1198
|
+
page-break-after: always;
|
|
1199
|
+
}
|
|
1200
|
+
.np-page:last-child { page-break-after: auto; }
|
|
1201
|
+
}
|
|
1202
|
+
</style>
|
|
1203
|
+
</head>
|
|
1204
|
+
<body>
|
|
1205
|
+
|
|
1206
|
+
<header class="np-top-bar" data-np-chrome>
|
|
1207
|
+
<a href="/" class="np-crumb">PrivateBoard <span class="np-crumb-accent">· newspaper</span></a>
|
|
1208
|
+
<div class="np-actions">
|
|
1209
|
+
<span class="np-variant-badge" data-np-variant-badge>Loading…</span>
|
|
1210
|
+
<nav class="np-page-nav" aria-label="Pages">
|
|
1211
|
+
<a href="#np-page-1">P. 1</a>
|
|
1212
|
+
<a href="#np-page-2">P. 2</a>
|
|
1213
|
+
</nav>
|
|
1214
|
+
<button type="button" class="np-btn" data-np-png>
|
|
1215
|
+
<span class="glyph">↓</span>PNG
|
|
1216
|
+
</button>
|
|
1217
|
+
<button type="button" class="np-btn" data-np-print>
|
|
1218
|
+
<span class="glyph">↓</span>PDF
|
|
1219
|
+
</button>
|
|
1220
|
+
</div>
|
|
1221
|
+
</header>
|
|
1222
|
+
|
|
1223
|
+
<main data-np-root>
|
|
1224
|
+
<div class="np-state">
|
|
1225
|
+
<div class="np-state-mark">Loading</div>
|
|
1226
|
+
<div class="np-state-title">Loading newspaper…</div>
|
|
1227
|
+
<div class="np-state-body">Fetching the broadsheet for this brief.</div>
|
|
1228
|
+
</div>
|
|
1229
|
+
</main>
|
|
1230
|
+
|
|
1231
|
+
<script>
|
|
1232
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
1233
|
+
Newspaper renderer · two distinct templates picked deterministi-
|
|
1234
|
+
cally from the brief id:
|
|
1235
|
+
· POST · Washington-Post register (red accents · sans kickers ·
|
|
1236
|
+
display-serif nameplate · 3-col body grid)
|
|
1237
|
+
· TIMES · New York Times register (blackletter nameplate ·
|
|
1238
|
+
"All the news that's fit to print" tagline · navy accent ·
|
|
1239
|
+
star ornaments · 6-col grid front + 2-col inside)
|
|
1240
|
+
|
|
1241
|
+
Each variant renders 2 pages (front + inside) so the report
|
|
1242
|
+
reads as a paper, not a single sheet. The variant + page count
|
|
1243
|
+
are stable across refreshes (hash of brief id).
|
|
1244
|
+
────────────────────────────────────────────────────────────── */
|
|
1245
|
+
(function () {
|
|
1246
|
+
const params = new URLSearchParams(location.search);
|
|
1247
|
+
const briefId = (params.get("b") || "").trim();
|
|
1248
|
+
const roomId = (params.get("r") || "").trim();
|
|
1249
|
+
const root = document.querySelector("[data-np-root]");
|
|
1250
|
+
const variantBadge = document.querySelector("[data-np-variant-badge]");
|
|
1251
|
+
|
|
1252
|
+
function escape(s) {
|
|
1253
|
+
return String(s == null ? "" : s)
|
|
1254
|
+
.replace(/&/g, "&")
|
|
1255
|
+
.replace(/</g, "<")
|
|
1256
|
+
.replace(/>/g, ">")
|
|
1257
|
+
.replace(/"/g, """)
|
|
1258
|
+
.replace(/'/g, "'");
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function showState(mark, title, body) {
|
|
1262
|
+
root.innerHTML = `
|
|
1263
|
+
<div class="np-state">
|
|
1264
|
+
<div class="np-state-mark">${escape(mark)}</div>
|
|
1265
|
+
<div class="np-state-title">${escape(title)}</div>
|
|
1266
|
+
<div class="np-state-body">${escape(body)}</div>
|
|
1267
|
+
</div>`;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
async function loadBrief() {
|
|
1271
|
+
let url;
|
|
1272
|
+
if (briefId) url = `/api/briefs/${encodeURIComponent(briefId)}`;
|
|
1273
|
+
else if (roomId) url = `/api/rooms/${encodeURIComponent(roomId)}/brief`;
|
|
1274
|
+
else { showState("Missing query", "No brief specified", "Add ?b=<briefId> or ?r=<roomId> to the URL."); return null; }
|
|
1275
|
+
const res = await fetch(url);
|
|
1276
|
+
if (!res.ok) {
|
|
1277
|
+
const e = await res.json().catch(() => ({}));
|
|
1278
|
+
showState("Not found", "Brief not found", e.error || "The requested brief doesn't exist or is no longer available.");
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
return await res.json();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** Pick "post" or "times" deterministically from the brief id ·
|
|
1285
|
+
* same brief id always renders the same template so refreshes /
|
|
1286
|
+
* share-links / PNG exports stay stable. The `?v=post|times`
|
|
1287
|
+
* URL parameter forces a specific template (debug / preview). */
|
|
1288
|
+
function pickVariant(id) {
|
|
1289
|
+
const force = (params.get("v") || "").trim().toLowerCase();
|
|
1290
|
+
if (force === "post" || force === "times") return force;
|
|
1291
|
+
const s = String(id || "");
|
|
1292
|
+
if (!s) return "post";
|
|
1293
|
+
let h = 0;
|
|
1294
|
+
for (let i = 0; i < s.length; i++) {
|
|
1295
|
+
h = ((h << 5) - h) + s.charCodeAt(i);
|
|
1296
|
+
h |= 0;
|
|
1297
|
+
}
|
|
1298
|
+
return (Math.abs(h) % 2) === 0 ? "post" : "times";
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/** Format date for masthead · returns formatted strings used by
|
|
1302
|
+
* both variants but in slightly different shapes. */
|
|
1303
|
+
function formatDate(footerTag) {
|
|
1304
|
+
const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
|
|
1305
|
+
const monthsShort = ["Jan.","Feb.","March","April","May","June","July","Aug.","Sept.","Oct.","Nov.","Dec."];
|
|
1306
|
+
const days = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
|
|
1307
|
+
const m = String(footerTag || "").match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
1308
|
+
const target = m ? new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00`) : new Date();
|
|
1309
|
+
const d = isNaN(target.getTime()) ? new Date() : target;
|
|
1310
|
+
return {
|
|
1311
|
+
weekday: days[d.getDay()],
|
|
1312
|
+
weekdayShort: days[d.getDay()].toUpperCase().slice(0, 3),
|
|
1313
|
+
long: `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`,
|
|
1314
|
+
short: `${monthsShort[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`,
|
|
1315
|
+
year: String(d.getFullYear()),
|
|
1316
|
+
nytDate: `${days[d.getDay()]}, ${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`,
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function splitHeading(raw) {
|
|
1321
|
+
const s = String(raw || "").trim();
|
|
1322
|
+
if (!s) return { heading: "", body: "" };
|
|
1323
|
+
const seps = [": ", " · ", " — ", " - "];
|
|
1324
|
+
for (const sep of seps) {
|
|
1325
|
+
const idx = s.indexOf(sep);
|
|
1326
|
+
if (idx > 0 && idx < 60) {
|
|
1327
|
+
return { heading: s.slice(0, idx).trim(), body: s.slice(idx + sep.length).trim() };
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return { heading: s, body: "" };
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/** Pick a stat tile's numeric + caption from the brief data.
|
|
1334
|
+
* Strict: only returns a tile when a milestone has a SHORT
|
|
1335
|
+
* numeric-y callout (≤ 14 chars, the "$120M" / "10×" / "Q4"
|
|
1336
|
+
* shape). The previous version fell back to verification
|
|
1337
|
+
* headings as the "num" slot, which rendered long phrases
|
|
1338
|
+
* ("Compliance review remains the gating constraint") at
|
|
1339
|
+
* 38-48px display-serif in a 200px-wide column · severely
|
|
1340
|
+
* cramped. Skipping is honest. */
|
|
1341
|
+
function pickStat(m) {
|
|
1342
|
+
const ms = m.milestones || [];
|
|
1343
|
+
const milestoneWithCallout = ms.find((x) => x && x.callout && String(x.callout).trim().length > 0 && String(x.callout).trim().length <= 14);
|
|
1344
|
+
if (milestoneWithCallout) {
|
|
1345
|
+
return {
|
|
1346
|
+
num: String(milestoneWithCallout.callout).trim(),
|
|
1347
|
+
cap: milestoneWithCallout.title || milestoneWithCallout.period || "",
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
return null;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function pageFoot(pageNum, totalPages, briefIdShort) {
|
|
1354
|
+
return `
|
|
1355
|
+
<div class="np-page-foot">
|
|
1356
|
+
<span class="np-page-foot-left">${escape(briefIdShort)}</span>
|
|
1357
|
+
<span class="np-page-foot-mid">Page ${pageNum} of ${totalPages}</span>
|
|
1358
|
+
<span class="np-page-foot-right">privateboard.ai</span>
|
|
1359
|
+
</div>`;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/* ═════════════════════════════════════════════════════════════
|
|
1363
|
+
POST · The Boardroom Post · Washington Post register
|
|
1364
|
+
═════════════════════════════════════════════════════════════ */
|
|
1365
|
+
|
|
1366
|
+
function renderPostPage1(brief, parts) {
|
|
1367
|
+
const { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
|
|
1368
|
+
const tpAll = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
|
|
1369
|
+
const tpFirst = tpAll[0] || "";
|
|
1370
|
+
const tpSecond = tpAll[1] || "";
|
|
1371
|
+
const stat = pickStat(m);
|
|
1372
|
+
const indexItems = [
|
|
1373
|
+
{ name: ms1.title || ms1.period || "Inside the room", page: "A2" },
|
|
1374
|
+
{ name: ms2.title || ms2.period || "More from the desk", page: "A2" },
|
|
1375
|
+
];
|
|
1376
|
+
if (m.verification && m.verification.title) {
|
|
1377
|
+
indexItems.push({ name: m.verification.title, page: "A2" });
|
|
1378
|
+
}
|
|
1379
|
+
const indexHtml = indexItems.map((it) => `
|
|
1380
|
+
<li><b>${escape(it.name)}</b><span class="pg">Page ${escape(it.page)}</span></li>
|
|
1381
|
+
`).join("");
|
|
1382
|
+
|
|
1383
|
+
// Pack the body cell with extra content so it doesn't end
|
|
1384
|
+
// short relative to the sidebar (which has flag + head +
|
|
1385
|
+
// body + stat + pull = 4-5 stacked items). Mid-article
|
|
1386
|
+
// sub-headline + tail talking-point keeps body height in
|
|
1387
|
+
// reach of the sidebar.
|
|
1388
|
+
const bodySubHead = ms2.title || ms2.period || "";
|
|
1389
|
+
const bodySubBody = ms2.body || "";
|
|
1390
|
+
|
|
1391
|
+
return `
|
|
1392
|
+
<section class="np-page" id="np-page-1">
|
|
1393
|
+
<!-- Section dots row · WaPo nav strip above the mast -->
|
|
1394
|
+
<div class="post-sect-row">
|
|
1395
|
+
<span class="post-sect-item">Top Story</span>
|
|
1396
|
+
<span class="post-sect-item is-blue">Markets</span>
|
|
1397
|
+
<span class="post-sect-item is-gold">Opinion</span>
|
|
1398
|
+
<span class="post-sect-item is-teal">Inside</span>
|
|
1399
|
+
</div>
|
|
1400
|
+
|
|
1401
|
+
<div class="post-tagline">From the boardroom — insights, every issue.</div>
|
|
1402
|
+
<h1 class="post-mast-name">The Boardroom Post</h1>
|
|
1403
|
+
|
|
1404
|
+
<div class="post-meta">
|
|
1405
|
+
<span class="post-meta-l">${escape(dateInfo.weekday)}, ${escape(dateInfo.long)}</span>
|
|
1406
|
+
<span class="post-meta-c">privateboard.ai</span>
|
|
1407
|
+
<span class="post-meta-r">${escape(briefIdShort)}</span>
|
|
1408
|
+
</div>
|
|
1409
|
+
|
|
1410
|
+
<div class="post-hero">
|
|
1411
|
+
<span class="post-hero-flag">Top Story</span>
|
|
1412
|
+
<h2 class="post-hero-title">${escape(m.title || "")}</h2>
|
|
1413
|
+
${m.kicker ? `<div class="post-hero-deck">${escape(m.kicker)}</div>` : ""}
|
|
1414
|
+
<div class="post-hero-byline">By the chair · <em>From the boardroom</em></div>
|
|
1415
|
+
</div>
|
|
1416
|
+
|
|
1417
|
+
${m.conclusion ? `
|
|
1418
|
+
<aside class="post-stand">
|
|
1419
|
+
<div class="post-stand-mark">"</div>
|
|
1420
|
+
<div>
|
|
1421
|
+
<div class="post-stand-text">${escape(m.conclusion)}</div>
|
|
1422
|
+
<span class="post-stand-cite">— Bottom line · ${escape(m.title || "")}</span>
|
|
1423
|
+
</div>
|
|
1424
|
+
</aside>` : ""}
|
|
1425
|
+
|
|
1426
|
+
<div class="post-grid">
|
|
1427
|
+
<div>
|
|
1428
|
+
${ms0.body ? `
|
|
1429
|
+
<div class="post-body has-drop">
|
|
1430
|
+
<p>${escape(ms0.body)}</p>
|
|
1431
|
+
${ms1.body ? `<p>${escape(ms1.body)}</p>` : ""}
|
|
1432
|
+
</div>` : ""}
|
|
1433
|
+
${tpSecond ? `
|
|
1434
|
+
<div class="post-pull" style="margin: 14px 0 8px;">
|
|
1435
|
+
<div class="post-pull-text">"${escape(tpSecond)}"</div>
|
|
1436
|
+
<div class="post-pull-cite">Editorial · second take</div>
|
|
1437
|
+
</div>` : ""}
|
|
1438
|
+
${bodySubHead || bodySubBody ? `
|
|
1439
|
+
<div style="margin-top: 12px;">
|
|
1440
|
+
${bodySubHead ? `<h4 style="font-family: var(--serif-display); font-size: 18px; font-weight: 700; line-height: 1.2; color: var(--ink); margin-bottom: 6px; padding-top: 10px; border-top: 1px solid var(--rule);">${escape(bodySubHead)}</h4>` : ""}
|
|
1441
|
+
${bodySubBody ? `<p class="post-body" style="margin: 0;">${escape(bodySubBody)}</p>` : ""}
|
|
1442
|
+
</div>` : ""}
|
|
1443
|
+
</div>
|
|
1444
|
+
<aside class="post-side">
|
|
1445
|
+
<span class="post-side-flag">Breaking</span>
|
|
1446
|
+
<h3 class="post-side-head">${escape(ms1.title || ms1.period || "Late dispatches")}</h3>
|
|
1447
|
+
${ms1.body ? `<p class="post-side-body">${escape(ms1.body)}</p>` : ""}
|
|
1448
|
+
${stat ? `
|
|
1449
|
+
<div class="post-stat">
|
|
1450
|
+
<div class="post-stat-eye">By the numbers</div>
|
|
1451
|
+
<div class="post-stat-num">${escape(stat.num)}</div>
|
|
1452
|
+
<div class="post-stat-cap">${escape(stat.cap)}</div>
|
|
1453
|
+
</div>` : ""}
|
|
1454
|
+
${tpFirst ? `
|
|
1455
|
+
<div class="post-pull">
|
|
1456
|
+
<div class="post-pull-text">"${escape(tpFirst)}"</div>
|
|
1457
|
+
<div class="post-pull-cite">From the editorial</div>
|
|
1458
|
+
</div>` : ""}
|
|
1459
|
+
</aside>
|
|
1460
|
+
</div>
|
|
1461
|
+
|
|
1462
|
+
${indexHtml ? `
|
|
1463
|
+
<div class="post-index">
|
|
1464
|
+
<span class="post-index-flag">Inside this issue</span>
|
|
1465
|
+
<ul class="post-index-list">${indexHtml}</ul>
|
|
1466
|
+
</div>` : ""}
|
|
1467
|
+
|
|
1468
|
+
${pageFoot(1, totalPages, briefIdShort)}
|
|
1469
|
+
</section>`;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function renderPostPage2(brief, parts) {
|
|
1473
|
+
const { m, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
|
|
1474
|
+
const tpAll = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
|
|
1475
|
+
const pullQuote = tpAll[1] || tpAll[0] || "";
|
|
1476
|
+
const verifs = (m.verification && Array.isArray(m.verification.bullets)) ? m.verification.bullets : [];
|
|
1477
|
+
const verifsHtml = verifs.slice(0, 4).map((b) => {
|
|
1478
|
+
const parts = splitHeading(b);
|
|
1479
|
+
const inner = parts.body
|
|
1480
|
+
? `<b>${escape(parts.heading)}</b>${escape(parts.body)}`
|
|
1481
|
+
: escape(parts.heading);
|
|
1482
|
+
return `<li>${inner}</li>`;
|
|
1483
|
+
}).join("");
|
|
1484
|
+
const sideHtml = verifsHtml ? `
|
|
1485
|
+
<span class="post-side-flag">More headings</span>
|
|
1486
|
+
<ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;">
|
|
1487
|
+
${verifs.slice(0, 5).map((b) => {
|
|
1488
|
+
const p = splitHeading(b);
|
|
1489
|
+
return `
|
|
1490
|
+
<li style="font-family:var(--serif);font-size:12.5px;line-height:1.55;color:var(--ink-soft);padding:10px 0;border-bottom:1px solid var(--rule);">
|
|
1491
|
+
${p.body
|
|
1492
|
+
? `<b style="display:block;font-family:var(--serif-display);font-size:13.5px;font-weight:700;color:var(--ink);margin-bottom:4px;">${escape(p.heading)}</b>${escape(p.body)}`
|
|
1493
|
+
: escape(p.heading)}
|
|
1494
|
+
</li>`;
|
|
1495
|
+
}).join("")}
|
|
1496
|
+
</ul>` : "";
|
|
1497
|
+
|
|
1498
|
+
const bodyParts = [];
|
|
1499
|
+
if (ms1.body) bodyParts.push(`<p>${escape(ms1.body)}</p>`);
|
|
1500
|
+
if (ms2.body) bodyParts.push(`<p>${escape(ms2.body)}</p>`);
|
|
1501
|
+
const bodyHtml = bodyParts.join("");
|
|
1502
|
+
|
|
1503
|
+
const headline = ms1.title || ms2.title || "Inside the room";
|
|
1504
|
+
const deck = m.kicker || "";
|
|
1505
|
+
|
|
1506
|
+
return `
|
|
1507
|
+
<section class="np-page" id="np-page-2">
|
|
1508
|
+
<div class="post-running">
|
|
1509
|
+
<span class="post-running-mast">The Boardroom Post</span>
|
|
1510
|
+
<span class="post-running-meta">${escape(dateInfo.weekday)}, ${escape(dateInfo.long)} · privateboard.ai</span>
|
|
1511
|
+
<span class="post-running-page">A2</span>
|
|
1512
|
+
</div>
|
|
1513
|
+
|
|
1514
|
+
<div class="post-sect-banner">
|
|
1515
|
+
<span class="post-sect-banner-tag">${escape(ms1.period || ms2.period || "Continued")}</span>
|
|
1516
|
+
<span class="post-sect-banner-deck">From the room · continued from the front page</span>
|
|
1517
|
+
</div>
|
|
1518
|
+
|
|
1519
|
+
<div class="post-article">
|
|
1520
|
+
<div>
|
|
1521
|
+
<h2 class="post-article-head">${escape(headline)}</h2>
|
|
1522
|
+
${deck ? `<div class="post-article-deck">${escape(deck)}</div>` : ""}
|
|
1523
|
+
<div class="post-article-byline">By the chair · Continued from A1</div>
|
|
1524
|
+
${bodyHtml ? `<div class="post-body has-drop">${bodyHtml}</div>` : ""}
|
|
1525
|
+
${pullQuote ? `
|
|
1526
|
+
<div class="post-pull">
|
|
1527
|
+
<div class="post-pull-text">"${escape(pullQuote)}"</div>
|
|
1528
|
+
<div class="post-pull-cite">Editorial</div>
|
|
1529
|
+
</div>` : ""}
|
|
1530
|
+
</div>
|
|
1531
|
+
<aside class="post-side">
|
|
1532
|
+
${sideHtml}
|
|
1533
|
+
${m.flow && Array.isArray(m.flow.nodes) && m.flow.nodes.length >= 2 ? `
|
|
1534
|
+
<div class="post-stat">
|
|
1535
|
+
<div class="post-stat-eye">Transformation</div>
|
|
1536
|
+
<div style="font-family: var(--serif-display); font-size: 17px; font-weight: 700; color: var(--ink); line-height: 1.25;">
|
|
1537
|
+
${m.flow.nodes.map(escape).join('<span style="color: var(--accent); margin: 0 6px;">→</span>')}
|
|
1538
|
+
</div>
|
|
1539
|
+
${m.flow.caption ? `<div class="post-stat-cap">${escape(m.flow.caption)}</div>` : ""}
|
|
1540
|
+
</div>` : ""}
|
|
1541
|
+
</aside>
|
|
1542
|
+
</div>
|
|
1543
|
+
|
|
1544
|
+
${pageFoot(2, totalPages, briefIdShort)}
|
|
1545
|
+
</section>`;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
/* ═════════════════════════════════════════════════════════════
|
|
1549
|
+
TIMES · The Boardroom · New York Times register
|
|
1550
|
+
═════════════════════════════════════════════════════════════ */
|
|
1551
|
+
|
|
1552
|
+
function renderTimesPage1(brief, parts) {
|
|
1553
|
+
const { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
|
|
1554
|
+
const tpAll = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
|
|
1555
|
+
const tpFirst = tpAll[0] || "";
|
|
1556
|
+
const tpSecond = tpAll[1] || "";
|
|
1557
|
+
const stat = pickStat(m);
|
|
1558
|
+
|
|
1559
|
+
// Verification preview · 2 short bullets at the bottom of the
|
|
1560
|
+
// lead column so the lead extends to roughly match the side
|
|
1561
|
+
// cols' height (the lead is twice as wide so its body wraps
|
|
1562
|
+
// half as tall · packing more content here keeps the columns
|
|
1563
|
+
// visually balanced).
|
|
1564
|
+
const vArr = (m.verification && Array.isArray(m.verification.bullets)) ? m.verification.bullets : [];
|
|
1565
|
+
const vPreview = vArr.slice(0, 2).map((b) => {
|
|
1566
|
+
const p = splitHeading(b);
|
|
1567
|
+
return `<li>${p.body
|
|
1568
|
+
? `<b>${escape(p.heading)}</b> ${escape(p.body)}`
|
|
1569
|
+
: escape(p.heading)}</li>`;
|
|
1570
|
+
}).join("");
|
|
1571
|
+
|
|
1572
|
+
// Volume info · classic NYT pattern · "VOL. CXC..No. {n} ★ Late
|
|
1573
|
+
// City Edition ★ {date} ★ {price/id}". We use the brief id
|
|
1574
|
+
// as a serial number and "Late City Edition" as the standard
|
|
1575
|
+
// copy.
|
|
1576
|
+
const volStrip = `
|
|
1577
|
+
<div class="times-vol-strip">
|
|
1578
|
+
<span><em>VOL. CXC . . No.</em> ${escape(briefIdShort.replace(/^#/, ""))}</span>
|
|
1579
|
+
<span class="star">★</span>
|
|
1580
|
+
<span>Late City Edition</span>
|
|
1581
|
+
<span class="star">★</span>
|
|
1582
|
+
<span>${escape(dateInfo.nytDate)}</span>
|
|
1583
|
+
<span class="star">★</span>
|
|
1584
|
+
<span><em>${escape(briefIdShort)}</em></span>
|
|
1585
|
+
</div>`;
|
|
1586
|
+
|
|
1587
|
+
return `
|
|
1588
|
+
<section class="np-page" id="np-page-1">
|
|
1589
|
+
<div class="times-tagline">"All the analysis that's fit to print"</div>
|
|
1590
|
+
<h1 class="times-mast-name">The Boardroom</h1>
|
|
1591
|
+
${volStrip}
|
|
1592
|
+
|
|
1593
|
+
<div class="times-front">
|
|
1594
|
+
<!-- Col 1 · sidebar story (ms2) -->
|
|
1595
|
+
<div class="times-col times-col-1">
|
|
1596
|
+
<div class="times-section-eye">Inside</div>
|
|
1597
|
+
<h3 class="times-head-small">${escape(ms2.title || ms2.period || "Late Dispatches")}</h3>
|
|
1598
|
+
${ms2.body ? `<div class="times-body">${escape(ms2.body)}</div>` : ""}
|
|
1599
|
+
${stat ? `
|
|
1600
|
+
<div class="times-stat">
|
|
1601
|
+
<div class="times-stat-eye">By the numbers</div>
|
|
1602
|
+
<div class="times-stat-num">${escape(stat.num)}</div>
|
|
1603
|
+
<div class="times-stat-cap">${escape(stat.cap)}</div>
|
|
1604
|
+
</div>` : ""}
|
|
1605
|
+
</div>
|
|
1606
|
+
|
|
1607
|
+
<!-- Col 2-3 · the lead story · centered headline + body
|
|
1608
|
+
+ standfirst + verification preview · packed so the
|
|
1609
|
+
lead matches the side cols' height -->
|
|
1610
|
+
<div class="times-col times-col-3">
|
|
1611
|
+
<div class="times-section-eye">The Boardroom</div>
|
|
1612
|
+
<h2 class="times-head-large">${escape(m.title || "")}</h2>
|
|
1613
|
+
${m.kicker ? `<div class="times-deck">${escape(m.kicker)}</div>` : ""}
|
|
1614
|
+
<div class="times-byline">By <em>The Chair</em></div>
|
|
1615
|
+
${ms0.body ? `<div class="times-body has-drop">${escape(ms0.body)}</div>` : ""}
|
|
1616
|
+
${m.conclusion ? `
|
|
1617
|
+
<aside class="times-stand">
|
|
1618
|
+
<div class="times-stand-mark">¶</div>
|
|
1619
|
+
<div class="times-stand-text">${escape(m.conclusion)}</div>
|
|
1620
|
+
<div class="times-stand-cite">Bottom line</div>
|
|
1621
|
+
</aside>` : ""}
|
|
1622
|
+
${tpSecond ? `
|
|
1623
|
+
<div class="times-pull">
|
|
1624
|
+
<div class="times-pull-text">"${escape(tpSecond)}"</div>
|
|
1625
|
+
<div class="times-pull-cite">Editorial · second take</div>
|
|
1626
|
+
</div>` : ""}
|
|
1627
|
+
${vPreview ? `
|
|
1628
|
+
<div style="margin-top:6px;">
|
|
1629
|
+
<div class="times-section-eye" style="text-align:left;border:0;padding-bottom:0;margin-bottom:6px;">More headings</div>
|
|
1630
|
+
<ul class="times-side-list" style="font-size:12px;">${vPreview}</ul>
|
|
1631
|
+
</div>` : ""}
|
|
1632
|
+
</div>
|
|
1633
|
+
|
|
1634
|
+
<!-- Col 5-6 · secondary stories stacked -->
|
|
1635
|
+
<div class="times-col times-col-2">
|
|
1636
|
+
<div class="times-section-eye">Markets</div>
|
|
1637
|
+
<h3 class="times-head-medium">${escape(ms1.title || ms1.period || "On the markets")}</h3>
|
|
1638
|
+
${ms1.body ? `<div class="times-body">${escape(ms1.body)}</div>` : ""}
|
|
1639
|
+
${tpFirst ? `
|
|
1640
|
+
<div class="times-pull">
|
|
1641
|
+
<div class="times-pull-text">"${escape(tpFirst)}"</div>
|
|
1642
|
+
<div class="times-pull-cite">From the editorial</div>
|
|
1643
|
+
</div>` : ""}
|
|
1644
|
+
<div class="times-continued">Continued on Page A2</div>
|
|
1645
|
+
</div>
|
|
1646
|
+
</div>
|
|
1647
|
+
|
|
1648
|
+
${pageFoot(1, totalPages, briefIdShort)}
|
|
1649
|
+
</section>`;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function renderTimesPage2(brief, parts) {
|
|
1653
|
+
const { m, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
|
|
1654
|
+
const tpAll = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
|
|
1655
|
+
const pullQuote = tpAll[1] || tpAll[0] || "";
|
|
1656
|
+
const verifs = (m.verification && Array.isArray(m.verification.bullets)) ? m.verification.bullets : [];
|
|
1657
|
+
const verifsHtml = verifs.slice(0, 5).map((b) => {
|
|
1658
|
+
const p = splitHeading(b);
|
|
1659
|
+
return `<li>${p.body
|
|
1660
|
+
? `<b>${escape(p.heading)}</b>${escape(p.body)}`
|
|
1661
|
+
: escape(p.heading)}</li>`;
|
|
1662
|
+
}).join("");
|
|
1663
|
+
|
|
1664
|
+
const bodyParts = [];
|
|
1665
|
+
if (ms1.body) bodyParts.push(`<p>${escape(ms1.body)}</p>`);
|
|
1666
|
+
if (ms2.body) bodyParts.push(`<p>${escape(ms2.body)}</p>`);
|
|
1667
|
+
const bodyHtml = bodyParts.join("");
|
|
1668
|
+
|
|
1669
|
+
const headline = ms1.title || ms2.title || "Continued from the front page";
|
|
1670
|
+
const deck = m.kicker || "";
|
|
1671
|
+
const sectionName = (ms1.period || ms2.period || "International").toUpperCase();
|
|
1672
|
+
|
|
1673
|
+
const flowHtml = (m.flow && Array.isArray(m.flow.nodes) && m.flow.nodes.length >= 2)
|
|
1674
|
+
? `<div class="times-stat">
|
|
1675
|
+
<div class="times-stat-eye">Transformation</div>
|
|
1676
|
+
<div style="font-family: var(--serif-display); font-size: 17px; font-weight: 700; color: var(--ink); line-height: 1.3; padding: 6px 0;">
|
|
1677
|
+
${m.flow.nodes.map(escape).join('<span style="color: var(--accent); margin: 0 8px;">→</span>')}
|
|
1678
|
+
</div>
|
|
1679
|
+
${m.flow.caption ? `<div class="times-stat-cap">${escape(m.flow.caption)}</div>` : ""}
|
|
1680
|
+
</div>` : "";
|
|
1681
|
+
|
|
1682
|
+
return `
|
|
1683
|
+
<section class="np-page" id="np-page-2">
|
|
1684
|
+
<div class="times-running">
|
|
1685
|
+
<span class="times-running-l">The Boardroom</span>
|
|
1686
|
+
<span class="times-running-c">${escape(dateInfo.long)}</span>
|
|
1687
|
+
<span class="times-running-r">A2</span>
|
|
1688
|
+
</div>
|
|
1689
|
+
|
|
1690
|
+
<!-- Section banner · drops the rule ABOVE the section
|
|
1691
|
+
name because the running header already provides a
|
|
1692
|
+
horizontal line right above this block (its
|
|
1693
|
+
border-bottom). Two close-together rules read as a
|
|
1694
|
+
doubled border. -->
|
|
1695
|
+
<div class="times-section-banner">
|
|
1696
|
+
<div class="times-section-banner-name">${escape(sectionName)}</div>
|
|
1697
|
+
<div class="times-section-banner-rule"></div>
|
|
1698
|
+
<div class="times-section-banner-deck">Continued from the front page</div>
|
|
1699
|
+
</div>
|
|
1700
|
+
|
|
1701
|
+
<div class="times-inside">
|
|
1702
|
+
<div>
|
|
1703
|
+
<h2 class="times-head-medium" style="text-align:left;">${escape(headline)}</h2>
|
|
1704
|
+
${deck ? `<div class="times-deck" style="text-align:left;">${escape(deck)}</div>` : ""}
|
|
1705
|
+
<div class="times-byline" style="text-align:left;">By <em>The Chair</em> · Continued from A1</div>
|
|
1706
|
+
<div class="times-continued">Continued from Page A1</div>
|
|
1707
|
+
${bodyHtml ? `<div class="times-inside-main has-drop">${bodyHtml}</div>` : ""}
|
|
1708
|
+
${pullQuote ? `
|
|
1709
|
+
<div class="times-pull">
|
|
1710
|
+
<div class="times-pull-text">"${escape(pullQuote)}"</div>
|
|
1711
|
+
<div class="times-pull-cite">From the editorial</div>
|
|
1712
|
+
</div>` : ""}
|
|
1713
|
+
</div>
|
|
1714
|
+
|
|
1715
|
+
<aside class="times-inside-side">
|
|
1716
|
+
<div class="times-side-flag">★ More Headings ★</div>
|
|
1717
|
+
${verifsHtml ? `<ul class="times-side-list">${verifsHtml}</ul>` : ""}
|
|
1718
|
+
${flowHtml}
|
|
1719
|
+
</aside>
|
|
1720
|
+
</div>
|
|
1721
|
+
|
|
1722
|
+
${pageFoot(2, totalPages, briefIdShort)}
|
|
1723
|
+
</section>`;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
/* ═════════════════════════════════════════════════════════════ */
|
|
1727
|
+
|
|
1728
|
+
function render(brief) {
|
|
1729
|
+
if (brief.mode !== "newspaper") {
|
|
1730
|
+
showState("Wrong mode", "This brief isn't a newspaper",
|
|
1731
|
+
"Open it in the matching viewer instead. Newspaper, magazine, and research-note are separate output modes.");
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const m = brief.bodyJson;
|
|
1735
|
+
if (!m || typeof m !== "object") {
|
|
1736
|
+
if (brief.isGenerating) {
|
|
1737
|
+
showState("Generating", "Newspaper is still being prepared",
|
|
1738
|
+
"The chair is currently composing the front page. Refresh in a few seconds.");
|
|
1739
|
+
} else {
|
|
1740
|
+
showState("Empty", "This brief has no newspaper data",
|
|
1741
|
+
"It may have failed to generate. Try regenerating from the room view.");
|
|
1742
|
+
}
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
if (m.title) document.title = `${m.title} · Newspaper`;
|
|
1747
|
+
|
|
1748
|
+
const variant = pickVariant(brief.id);
|
|
1749
|
+
document.body.setAttribute("data-np-variant", variant);
|
|
1750
|
+
if (variantBadge) {
|
|
1751
|
+
variantBadge.textContent = variant === "times" ? "Times Edition" : "Post Edition";
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const milestones = (m.milestones || []).slice(0, 3);
|
|
1755
|
+
const ms0 = milestones[0] || {};
|
|
1756
|
+
const ms1 = milestones[1] || {};
|
|
1757
|
+
const ms2 = milestones[2] || {};
|
|
1758
|
+
const dateInfo = formatDate(m.footerTag);
|
|
1759
|
+
const briefIdShort = brief.id ? `#${String(brief.id).slice(0, 10).toUpperCase()}` : "";
|
|
1760
|
+
const totalPages = 2;
|
|
1761
|
+
|
|
1762
|
+
const parts = { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages };
|
|
1763
|
+
|
|
1764
|
+
const pages = variant === "times"
|
|
1765
|
+
? [renderTimesPage1(brief, parts), renderTimesPage2(brief, parts)]
|
|
1766
|
+
: [renderPostPage1(brief, parts), renderPostPage2(brief, parts)];
|
|
1767
|
+
|
|
1768
|
+
root.innerHTML = `<article class="np-doc" data-np-paper>${pages.join("")}</article>`;
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/* ─── Export wiring · same PNG-as-PDF strategy ─────────── */
|
|
1772
|
+
let _h2iLoaded = null;
|
|
1773
|
+
async function ensureHtmlToImage() {
|
|
1774
|
+
if (window.htmlToImage) return;
|
|
1775
|
+
if (!_h2iLoaded) {
|
|
1776
|
+
_h2iLoaded = new Promise((res, rej) => {
|
|
1777
|
+
const s = document.createElement("script");
|
|
1778
|
+
s.src = "https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js";
|
|
1779
|
+
s.onload = res;
|
|
1780
|
+
s.onerror = rej;
|
|
1781
|
+
document.head.appendChild(s);
|
|
1782
|
+
});
|
|
1783
|
+
}
|
|
1784
|
+
await _h2iLoaded;
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
async function captureNpPng() {
|
|
1788
|
+
const el = document.querySelector("[data-np-paper]");
|
|
1789
|
+
if (!el) throw new Error("newspaper doc not found");
|
|
1790
|
+
await ensureHtmlToImage();
|
|
1791
|
+
if (document.fonts && document.fonts.ready) {
|
|
1792
|
+
try { await document.fonts.ready; } catch { /* best-effort */ }
|
|
1793
|
+
}
|
|
1794
|
+
const width = Math.max(el.scrollWidth, el.offsetWidth, el.clientWidth);
|
|
1795
|
+
const height = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight);
|
|
1796
|
+
return window.htmlToImage.toPng(el, {
|
|
1797
|
+
pixelRatio: 2,
|
|
1798
|
+
backgroundColor: "#1F1E1A",
|
|
1799
|
+
cacheBust: true,
|
|
1800
|
+
width,
|
|
1801
|
+
height,
|
|
1802
|
+
canvasWidth: width,
|
|
1803
|
+
canvasHeight: height,
|
|
1804
|
+
style: { margin: "0", width: `${width}px`, height: `${height}px` },
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
function slugTitle() {
|
|
1809
|
+
return (document.title || "newspaper").replace(/[^a-z0-9]+/gi, "-").slice(0, 60) || "newspaper";
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
async function exportPng() {
|
|
1813
|
+
try {
|
|
1814
|
+
const dataUrl = await captureNpPng();
|
|
1815
|
+
const a = document.createElement("a");
|
|
1816
|
+
a.download = `${slugTitle()}.png`;
|
|
1817
|
+
a.href = dataUrl;
|
|
1818
|
+
a.click();
|
|
1819
|
+
} catch (e) {
|
|
1820
|
+
console.warn("[newspaper] PNG export failed:", e);
|
|
1821
|
+
alert("PNG export failed · see browser console.");
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
async function exportPdf() {
|
|
1826
|
+
try {
|
|
1827
|
+
const dataUrl = await captureNpPng();
|
|
1828
|
+
const win = window.open("", "_blank", "width=1024,height=720");
|
|
1829
|
+
if (!win) {
|
|
1830
|
+
alert("PDF export needs to open a new window — please allow popups for this site.");
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const slug = slugTitle();
|
|
1834
|
+
win.document.open();
|
|
1835
|
+
win.document.write(`<!doctype html><html lang="en"><head>
|
|
1836
|
+
<meta charset="utf-8">
|
|
1837
|
+
<title>${slug}</title>
|
|
1838
|
+
<style>
|
|
1839
|
+
@page { size: auto; margin: 10mm; }
|
|
1840
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1841
|
+
html, body { background: #FFFFFF; }
|
|
1842
|
+
body { display: flex; align-items: flex-start; justify-content: center; padding: 20px; min-height: 100vh; }
|
|
1843
|
+
img { display: block; width: 100%; max-width: 880px; height: auto; box-shadow: 0 0 24px rgba(0, 0, 0, 0.08); }
|
|
1844
|
+
@media print {
|
|
1845
|
+
body { padding: 0; }
|
|
1846
|
+
img { box-shadow: none; max-width: none; width: 100%; }
|
|
1847
|
+
}
|
|
1848
|
+
.hint {
|
|
1849
|
+
position: fixed; top: 12px; left: 12px;
|
|
1850
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
1851
|
+
font-size: 11px; color: #8E8B83;
|
|
1852
|
+
background: rgba(255,255,255,0.9); padding: 6px 10px;
|
|
1853
|
+
border: 1px solid #E5E2DA;
|
|
1854
|
+
}
|
|
1855
|
+
@media print { .hint { display: none; } }
|
|
1856
|
+
</style>
|
|
1857
|
+
</head><body>
|
|
1858
|
+
<div class="hint">// press <kbd>⌘P</kbd> / <kbd>Ctrl+P</kbd> · save as PDF</div>
|
|
1859
|
+
<img alt="${slug}" src="${dataUrl}">
|
|
1860
|
+
<script>
|
|
1861
|
+
(function () {
|
|
1862
|
+
var img = document.querySelector("img");
|
|
1863
|
+
function go() { setTimeout(function () { window.print(); }, 200); }
|
|
1864
|
+
if (img && img.complete) { go(); }
|
|
1865
|
+
else if (img) { img.addEventListener("load", go, { once: true }); }
|
|
1866
|
+
})();
|
|
1867
|
+
<\/script>
|
|
1868
|
+
</body></html>`);
|
|
1869
|
+
win.document.close();
|
|
1870
|
+
} catch (e) {
|
|
1871
|
+
console.warn("[newspaper] PDF export failed:", e);
|
|
1872
|
+
alert("PDF export failed · see browser console.");
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
document.addEventListener("click", (e) => {
|
|
1877
|
+
if (e.target.closest("[data-np-png]")) { e.preventDefault(); exportPng(); return; }
|
|
1878
|
+
if (e.target.closest("[data-np-print]")) { e.preventDefault(); exportPdf(); return; }
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
/* ─── Boot ──────────────────────────────────────────────────── */
|
|
1882
|
+
loadBrief().then((brief) => {
|
|
1883
|
+
if (brief) render(brief);
|
|
1884
|
+
}).catch((e) => {
|
|
1885
|
+
console.error("[newspaper] load failed:", e);
|
|
1886
|
+
showState("Error", "Couldn't load this brief", e instanceof Error ? e.message : String(e));
|
|
1887
|
+
});
|
|
1888
|
+
})();
|
|
1889
|
+
</script>
|
|
1890
|
+
|
|
1891
|
+
</body>
|
|
1892
|
+
</html>
|