privateboard 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +694 -16
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/public/adjourn-overlay.css +194 -0
- package/public/app.js +426 -66
- package/public/bento.html +1396 -0
- package/public/home.html +389 -17
- package/public/index.html +22 -5
- package/public/magazine.html +1266 -0
- package/public/newspaper.html +1770 -0
- package/public/report.html +366 -52
- package/public/user-settings.css +94 -47
- package/public/user-settings.js +76 -42
|
@@ -0,0 +1,1770 @@
|
|
|
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
|
+
<style>
|
|
9
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
10
|
+
Newspaper · broadsheet single-page front page.
|
|
11
|
+
· Masthead band · two flanking decoration blocks + huge display
|
|
12
|
+
serif nameplate ("BOARDROOM") + thin double-rule below
|
|
13
|
+
· Meta strip · date | brand domain | brief id (mono)
|
|
14
|
+
· Front-page band · all-caps banner headline (bento.title) +
|
|
15
|
+
subdeck (bento.kicker) + hairline rule
|
|
16
|
+
· Three-column editorial grid · each column is one milestone
|
|
17
|
+
· LEFT col carries the inverted "BOTTOM LINE" callout
|
|
18
|
+
(bento.conclusion) above its body
|
|
19
|
+
· MIDDLE col carries an inverted date callout in the middle
|
|
20
|
+
(footerTag stacked vertically)
|
|
21
|
+
· RIGHT col carries the rankedBars chart in the "image slot"
|
|
22
|
+
· Bottom band · 3-column "more from the room" continuations
|
|
23
|
+
· LEFT · "MORE HEADINGS" stacked (verification bullets)
|
|
24
|
+
· MIDDLE · editorial paragraphs (talkingPoints)
|
|
25
|
+
· RIGHT · sign-off / brief id
|
|
26
|
+
Reads BentoScaffold JSON from body_json. Same data shape as bento /
|
|
27
|
+
magazine modes · only the renderer differs.
|
|
28
|
+
─────────────────────────────────────────────────────────────────── */
|
|
29
|
+
:root {
|
|
30
|
+
/* Default = Variant 1 (Broadsheet · WSJ / FT weekend register).
|
|
31
|
+
Two sibling variants override these tokens at the body level
|
|
32
|
+
(see the `[data-np-variant="2"]` and `="3"` blocks at the
|
|
33
|
+
bottom of this stylesheet). */
|
|
34
|
+
|
|
35
|
+
/* Page bg + soft radial accent that rides on top */
|
|
36
|
+
--bg: #DCC9A1;
|
|
37
|
+
--bg-radial: rgba(176, 160, 133, 0.10);
|
|
38
|
+
|
|
39
|
+
/* Surfaces · cream paper + deep brown ink */
|
|
40
|
+
--paper: #EEE2C6;
|
|
41
|
+
--paper-soft: #F2E8D2;
|
|
42
|
+
--paper-edge: #E5D8B7;
|
|
43
|
+
--surface: #FFFFFF;
|
|
44
|
+
--ink: #2A1D10;
|
|
45
|
+
--ink-soft: #4A3725;
|
|
46
|
+
--ink-mid: #6B5440;
|
|
47
|
+
--ink-faint: #8E7A60;
|
|
48
|
+
--ink-muted: #B0A085;
|
|
49
|
+
|
|
50
|
+
/* Inverted register · dark callouts */
|
|
51
|
+
--inv-bg: #2A1D10;
|
|
52
|
+
--inv-bg-soft: #3A2A1A;
|
|
53
|
+
--inv-ink: #F5EBD2;
|
|
54
|
+
--inv-ink-soft: #C7B894;
|
|
55
|
+
|
|
56
|
+
/* Rules · brown hairlines */
|
|
57
|
+
--rule: #B5A483;
|
|
58
|
+
--rule-soft: #C9BB9D;
|
|
59
|
+
--rule-strong: #8C7A56;
|
|
60
|
+
|
|
61
|
+
/* Accent · sparing · used for chart bars + small marks */
|
|
62
|
+
--accent: #6B4515;
|
|
63
|
+
|
|
64
|
+
/* Radii · zero everywhere · newspapers don't round */
|
|
65
|
+
--r-0: 0;
|
|
66
|
+
|
|
67
|
+
/* Shadow · only on the doc itself (lift it off the page) */
|
|
68
|
+
--shadow-doc: 0 2px 8px rgba(40, 25, 10, 0.08),
|
|
69
|
+
0 12px 36px rgba(40, 25, 10, 0.10);
|
|
70
|
+
|
|
71
|
+
--serif-display: "Tiempos Headline", "Playfair Display", "Bodoni 72",
|
|
72
|
+
"Didot", "Source Serif Pro", "Charter", Georgia,
|
|
73
|
+
"Source Han Serif SC", "Songti SC", "STSong", serif;
|
|
74
|
+
--serif: "Charter", "Source Serif Pro", "Iowan Old Style",
|
|
75
|
+
"Tiempos Text", "Source Han Serif SC", Georgia,
|
|
76
|
+
"Songti SC", "STSong", serif;
|
|
77
|
+
--sans: "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont,
|
|
78
|
+
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
|
|
79
|
+
"Source Han Sans CN", "Noto Sans CJK SC", sans-serif;
|
|
80
|
+
--mono: "SF Mono", "JetBrains Mono", "Menlo",
|
|
81
|
+
"PingFang SC", "Source Han Sans CN", monospace;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
85
|
+
html, body {
|
|
86
|
+
/* Three variants override `--bg` and `--bg-radial` to retint the
|
|
87
|
+
broadsheet · the radial gradient sits on top of the flat fill
|
|
88
|
+
to give the page a slight printed-press hint without committing
|
|
89
|
+
to a noise texture. */
|
|
90
|
+
background:
|
|
91
|
+
radial-gradient(120% 80% at 50% 0%, var(--bg-radial) 0%, transparent 60%),
|
|
92
|
+
var(--bg);
|
|
93
|
+
color: var(--ink);
|
|
94
|
+
font-family: var(--serif);
|
|
95
|
+
font-size: 14px;
|
|
96
|
+
line-height: 1.55;
|
|
97
|
+
-webkit-font-smoothing: antialiased;
|
|
98
|
+
text-rendering: optimizeLegibility;
|
|
99
|
+
min-height: 100vh;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ─── Top chrome · brand crumb + actions ────────────────────────── */
|
|
103
|
+
.np-top-bar {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: space-between;
|
|
107
|
+
gap: 14px;
|
|
108
|
+
padding: 18px 32px;
|
|
109
|
+
background: var(--top-bar-bg, rgba(220, 201, 161, 0.85));
|
|
110
|
+
backdrop-filter: blur(10px);
|
|
111
|
+
-webkit-backdrop-filter: blur(10px);
|
|
112
|
+
border-bottom: 1px solid var(--rule);
|
|
113
|
+
flex-wrap: wrap;
|
|
114
|
+
position: sticky;
|
|
115
|
+
top: 0;
|
|
116
|
+
z-index: 10;
|
|
117
|
+
font-family: var(--serif);
|
|
118
|
+
}
|
|
119
|
+
.np-crumb {
|
|
120
|
+
display: inline-flex;
|
|
121
|
+
align-items: center;
|
|
122
|
+
gap: 12px;
|
|
123
|
+
font-family: var(--serif-display);
|
|
124
|
+
font-size: 16px;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
color: var(--ink);
|
|
127
|
+
text-decoration: none;
|
|
128
|
+
}
|
|
129
|
+
.np-crumb::before {
|
|
130
|
+
content: "";
|
|
131
|
+
width: 10px;
|
|
132
|
+
height: 10px;
|
|
133
|
+
background: var(--ink);
|
|
134
|
+
flex: 0 0 auto;
|
|
135
|
+
}
|
|
136
|
+
.np-crumb-accent {
|
|
137
|
+
color: var(--ink-faint);
|
|
138
|
+
font-style: italic;
|
|
139
|
+
font-weight: 400;
|
|
140
|
+
}
|
|
141
|
+
.np-actions { display: flex; gap: 8px; }
|
|
142
|
+
.np-btn {
|
|
143
|
+
font-family: var(--mono);
|
|
144
|
+
font-size: 11px;
|
|
145
|
+
letter-spacing: 0.04em;
|
|
146
|
+
padding: 8px 14px;
|
|
147
|
+
background: var(--surface);
|
|
148
|
+
border: 1px solid var(--rule-strong);
|
|
149
|
+
color: var(--ink);
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
text-decoration: none;
|
|
152
|
+
transition: color 0.15s, background 0.15s, transform 0.15s;
|
|
153
|
+
}
|
|
154
|
+
.np-btn:hover {
|
|
155
|
+
background: var(--ink);
|
|
156
|
+
color: var(--paper);
|
|
157
|
+
border-color: var(--ink);
|
|
158
|
+
transform: translateY(-1px);
|
|
159
|
+
}
|
|
160
|
+
.np-btn:active { transform: translateY(0); }
|
|
161
|
+
.np-btn .glyph { margin-right: 4px; }
|
|
162
|
+
|
|
163
|
+
/* ─── Doc · the broadsheet sheet ───────────────────────────────── */
|
|
164
|
+
.np-doc {
|
|
165
|
+
max-width: 880px;
|
|
166
|
+
margin: 28px auto;
|
|
167
|
+
background: var(--paper);
|
|
168
|
+
box-shadow: var(--shadow-doc);
|
|
169
|
+
padding: 36px 44px 40px;
|
|
170
|
+
position: relative;
|
|
171
|
+
}
|
|
172
|
+
/* Subtle paper edge · slight darkening at the very edge gives the
|
|
173
|
+
impression of a printed sheet rather than a flat web rectangle. */
|
|
174
|
+
.np-doc::before {
|
|
175
|
+
content: "";
|
|
176
|
+
position: absolute;
|
|
177
|
+
inset: 0;
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
background:
|
|
180
|
+
linear-gradient(180deg, rgba(120, 90, 50, 0.04) 0%, transparent 30px),
|
|
181
|
+
linear-gradient(0deg, rgba(120, 90, 50, 0.04) 0%, transparent 30px);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ─── Rules · single + double hairlines ──────────────────────── */
|
|
185
|
+
.np-rule {
|
|
186
|
+
height: 1px;
|
|
187
|
+
background: var(--ink);
|
|
188
|
+
margin: 14px 0;
|
|
189
|
+
}
|
|
190
|
+
.np-rule-double {
|
|
191
|
+
height: 6px;
|
|
192
|
+
background:
|
|
193
|
+
linear-gradient(to bottom,
|
|
194
|
+
var(--ink) 0,
|
|
195
|
+
var(--ink) 1px,
|
|
196
|
+
transparent 1px,
|
|
197
|
+
transparent 5px,
|
|
198
|
+
var(--ink) 5px,
|
|
199
|
+
var(--ink) 6px);
|
|
200
|
+
margin: 14px 0;
|
|
201
|
+
}
|
|
202
|
+
.np-rule-thick {
|
|
203
|
+
height: 3px;
|
|
204
|
+
background: var(--ink);
|
|
205
|
+
margin: 18px 0;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ─── Masthead · flanks + nameplate ─────────────────────────── */
|
|
209
|
+
.np-masthead {
|
|
210
|
+
display: grid;
|
|
211
|
+
grid-template-columns: 1fr auto 1fr;
|
|
212
|
+
gap: 24px;
|
|
213
|
+
align-items: center;
|
|
214
|
+
padding: 4px 0 18px;
|
|
215
|
+
}
|
|
216
|
+
.np-mast-flank {
|
|
217
|
+
font-family: var(--serif);
|
|
218
|
+
font-size: 9.5px;
|
|
219
|
+
line-height: 1.45;
|
|
220
|
+
color: var(--ink-soft);
|
|
221
|
+
text-align: justify;
|
|
222
|
+
text-justify: inter-word;
|
|
223
|
+
/* Vertical hairlines bracket the flanks · one inside edge each ·
|
|
224
|
+
evokes the nameplate "advertorial" boxes in classic broadsheets. */
|
|
225
|
+
border-left: 1px solid var(--ink);
|
|
226
|
+
border-right: 1px solid var(--ink);
|
|
227
|
+
padding: 4px 8px;
|
|
228
|
+
max-width: 160px;
|
|
229
|
+
word-spacing: -0.03em;
|
|
230
|
+
}
|
|
231
|
+
.np-mast-flank-left { justify-self: end; }
|
|
232
|
+
.np-mast-flank-right { justify-self: start; }
|
|
233
|
+
.np-mast-flank b {
|
|
234
|
+
display: block;
|
|
235
|
+
font-weight: 700;
|
|
236
|
+
color: var(--ink);
|
|
237
|
+
margin-bottom: 3px;
|
|
238
|
+
font-size: 9.5px;
|
|
239
|
+
}
|
|
240
|
+
.np-mast-name {
|
|
241
|
+
font-family: var(--serif-display);
|
|
242
|
+
font-size: 64px;
|
|
243
|
+
font-weight: 900;
|
|
244
|
+
line-height: 1;
|
|
245
|
+
letter-spacing: 0.04em;
|
|
246
|
+
text-transform: uppercase;
|
|
247
|
+
color: var(--ink);
|
|
248
|
+
text-align: center;
|
|
249
|
+
/* Optical centering · serif display fonts often want a tiny nudge */
|
|
250
|
+
padding: 0 4px;
|
|
251
|
+
white-space: nowrap;
|
|
252
|
+
}
|
|
253
|
+
@media (max-width: 720px) {
|
|
254
|
+
.np-mast-name { font-size: 44px; letter-spacing: 0.03em; }
|
|
255
|
+
.np-mast-flank { display: none; }
|
|
256
|
+
.np-masthead { grid-template-columns: 1fr; }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/* ─── Meta strip · date | url | id ──────────────────────────── */
|
|
260
|
+
.np-meta-strip {
|
|
261
|
+
display: grid;
|
|
262
|
+
grid-template-columns: 1fr auto 1fr;
|
|
263
|
+
gap: 18px;
|
|
264
|
+
align-items: center;
|
|
265
|
+
font-family: var(--mono);
|
|
266
|
+
font-size: 11px;
|
|
267
|
+
letter-spacing: 0.05em;
|
|
268
|
+
color: var(--ink);
|
|
269
|
+
padding: 4px 0;
|
|
270
|
+
}
|
|
271
|
+
.np-meta-left { text-align: left; }
|
|
272
|
+
.np-meta-center { text-align: center; }
|
|
273
|
+
.np-meta-right { text-align: right; }
|
|
274
|
+
|
|
275
|
+
/* ─── Front page · headline + deck ──────────────────────────── */
|
|
276
|
+
.np-frontpage {
|
|
277
|
+
text-align: center;
|
|
278
|
+
padding: 18px 24px 14px;
|
|
279
|
+
}
|
|
280
|
+
.np-frontpage-title {
|
|
281
|
+
font-family: var(--serif-display);
|
|
282
|
+
font-size: 38px;
|
|
283
|
+
font-weight: 800;
|
|
284
|
+
line-height: 1.05;
|
|
285
|
+
letter-spacing: 0.005em;
|
|
286
|
+
text-transform: uppercase;
|
|
287
|
+
color: var(--ink);
|
|
288
|
+
margin: 0 0 8px;
|
|
289
|
+
}
|
|
290
|
+
.np-frontpage-deck {
|
|
291
|
+
font-family: var(--serif-display);
|
|
292
|
+
font-size: 20px;
|
|
293
|
+
font-weight: 500;
|
|
294
|
+
line-height: 1.2;
|
|
295
|
+
letter-spacing: 0.02em;
|
|
296
|
+
text-transform: uppercase;
|
|
297
|
+
color: var(--ink-soft);
|
|
298
|
+
margin: 0;
|
|
299
|
+
}
|
|
300
|
+
@media (max-width: 720px) {
|
|
301
|
+
.np-frontpage-title { font-size: 28px; }
|
|
302
|
+
.np-frontpage-deck { font-size: 15px; }
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/* ─── Editorial 3-column grid ───────────────────────────────── */
|
|
306
|
+
.np-grid {
|
|
307
|
+
display: grid;
|
|
308
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
309
|
+
gap: 22px;
|
|
310
|
+
margin: 18px 0;
|
|
311
|
+
}
|
|
312
|
+
/* Vertical hairline column rules between adjacent columns · the
|
|
313
|
+
classic newspaper spine. The rules are drawn via right border on
|
|
314
|
+
non-last children so resizing reflows correctly. */
|
|
315
|
+
.np-grid > .np-col {
|
|
316
|
+
padding-right: 22px;
|
|
317
|
+
border-right: 1px solid var(--rule);
|
|
318
|
+
}
|
|
319
|
+
.np-grid > .np-col:last-child {
|
|
320
|
+
padding-right: 0;
|
|
321
|
+
border-right: 0;
|
|
322
|
+
}
|
|
323
|
+
@media (max-width: 720px) {
|
|
324
|
+
.np-grid {
|
|
325
|
+
grid-template-columns: 1fr;
|
|
326
|
+
gap: 18px;
|
|
327
|
+
}
|
|
328
|
+
.np-grid > .np-col {
|
|
329
|
+
padding-right: 0;
|
|
330
|
+
border-right: 0;
|
|
331
|
+
border-bottom: 1px solid var(--rule);
|
|
332
|
+
padding-bottom: 18px;
|
|
333
|
+
}
|
|
334
|
+
.np-grid > .np-col:last-child { border-bottom: 0; }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.np-col {
|
|
338
|
+
display: flex;
|
|
339
|
+
flex-direction: column;
|
|
340
|
+
gap: 12px;
|
|
341
|
+
min-width: 0;
|
|
342
|
+
}
|
|
343
|
+
.np-col-heading {
|
|
344
|
+
font-family: var(--serif-display);
|
|
345
|
+
font-size: 16px;
|
|
346
|
+
font-weight: 700;
|
|
347
|
+
text-transform: uppercase;
|
|
348
|
+
letter-spacing: 0.02em;
|
|
349
|
+
color: var(--ink);
|
|
350
|
+
line-height: 1.15;
|
|
351
|
+
margin: 0;
|
|
352
|
+
/* Tight underline rule below section heading */
|
|
353
|
+
padding-bottom: 6px;
|
|
354
|
+
border-bottom: 1px solid var(--ink);
|
|
355
|
+
}
|
|
356
|
+
.np-col-heading-large {
|
|
357
|
+
font-size: 22px;
|
|
358
|
+
line-height: 1.1;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Editorial body prose · justified, narrow, drop cap on first
|
|
362
|
+
paragraph of the column. */
|
|
363
|
+
.np-prose {
|
|
364
|
+
font-family: var(--serif);
|
|
365
|
+
font-size: 13px;
|
|
366
|
+
line-height: 1.55;
|
|
367
|
+
color: var(--ink-soft);
|
|
368
|
+
text-align: justify;
|
|
369
|
+
text-justify: inter-word;
|
|
370
|
+
hyphens: auto;
|
|
371
|
+
-webkit-hyphens: auto;
|
|
372
|
+
}
|
|
373
|
+
.np-prose p + p { margin-top: 8px; }
|
|
374
|
+
.np-prose-lead::first-letter {
|
|
375
|
+
font-family: var(--serif-display);
|
|
376
|
+
font-size: 42px;
|
|
377
|
+
line-height: 0.9;
|
|
378
|
+
font-weight: 700;
|
|
379
|
+
color: var(--ink);
|
|
380
|
+
float: left;
|
|
381
|
+
margin: 4px 6px 0 0;
|
|
382
|
+
padding: 0;
|
|
383
|
+
}
|
|
384
|
+
.np-prose strong, .np-prose b {
|
|
385
|
+
color: var(--ink);
|
|
386
|
+
font-weight: 700;
|
|
387
|
+
}
|
|
388
|
+
.np-pagehint {
|
|
389
|
+
font-family: var(--serif);
|
|
390
|
+
font-size: 11px;
|
|
391
|
+
font-style: italic;
|
|
392
|
+
color: var(--ink-mid);
|
|
393
|
+
margin-top: 4px;
|
|
394
|
+
}
|
|
395
|
+
.np-pagehint::before { content: "→ "; font-style: normal; color: var(--ink); }
|
|
396
|
+
|
|
397
|
+
/* ─── Inverted callouts · IMPORTANT DETAILS / date stack ───── */
|
|
398
|
+
.np-callout {
|
|
399
|
+
background: var(--inv-bg);
|
|
400
|
+
color: var(--inv-ink);
|
|
401
|
+
padding: 14px 16px 16px;
|
|
402
|
+
}
|
|
403
|
+
.np-callout-label {
|
|
404
|
+
font-family: var(--serif-display);
|
|
405
|
+
font-size: 12px;
|
|
406
|
+
font-weight: 700;
|
|
407
|
+
text-transform: uppercase;
|
|
408
|
+
letter-spacing: 0.18em;
|
|
409
|
+
color: var(--inv-ink-soft);
|
|
410
|
+
margin-bottom: 8px;
|
|
411
|
+
line-height: 1.2;
|
|
412
|
+
}
|
|
413
|
+
.np-callout-text {
|
|
414
|
+
font-family: var(--serif-display);
|
|
415
|
+
font-size: 18px;
|
|
416
|
+
font-weight: 700;
|
|
417
|
+
line-height: 1.18;
|
|
418
|
+
letter-spacing: -0.005em;
|
|
419
|
+
color: var(--inv-ink);
|
|
420
|
+
text-transform: uppercase;
|
|
421
|
+
}
|
|
422
|
+
/* Vertical date stack · used in the middle column of the upper grid */
|
|
423
|
+
.np-callout-date {
|
|
424
|
+
text-align: center;
|
|
425
|
+
padding: 18px 14px 16px;
|
|
426
|
+
}
|
|
427
|
+
.np-callout-date .np-date-day {
|
|
428
|
+
font-family: var(--serif-display);
|
|
429
|
+
font-size: 11px;
|
|
430
|
+
font-weight: 700;
|
|
431
|
+
letter-spacing: 0.18em;
|
|
432
|
+
text-transform: uppercase;
|
|
433
|
+
color: var(--inv-ink-soft);
|
|
434
|
+
line-height: 1;
|
|
435
|
+
margin-bottom: 8px;
|
|
436
|
+
}
|
|
437
|
+
.np-callout-date .np-date-month {
|
|
438
|
+
font-family: var(--serif-display);
|
|
439
|
+
font-size: 16px;
|
|
440
|
+
font-weight: 700;
|
|
441
|
+
letter-spacing: 0.18em;
|
|
442
|
+
text-transform: uppercase;
|
|
443
|
+
color: var(--inv-ink);
|
|
444
|
+
line-height: 1;
|
|
445
|
+
margin-bottom: 12px;
|
|
446
|
+
}
|
|
447
|
+
.np-callout-date .np-date-num {
|
|
448
|
+
font-family: var(--serif-display);
|
|
449
|
+
font-size: 56px;
|
|
450
|
+
font-weight: 900;
|
|
451
|
+
line-height: 1;
|
|
452
|
+
color: var(--inv-ink);
|
|
453
|
+
letter-spacing: -0.02em;
|
|
454
|
+
margin-bottom: 8px;
|
|
455
|
+
}
|
|
456
|
+
.np-callout-date .np-date-year {
|
|
457
|
+
font-family: var(--serif-display);
|
|
458
|
+
font-size: 18px;
|
|
459
|
+
font-weight: 600;
|
|
460
|
+
line-height: 1;
|
|
461
|
+
letter-spacing: 0.04em;
|
|
462
|
+
color: var(--inv-ink);
|
|
463
|
+
margin-bottom: 10px;
|
|
464
|
+
}
|
|
465
|
+
.np-callout-date .np-date-source {
|
|
466
|
+
font-family: var(--serif);
|
|
467
|
+
font-size: 11px;
|
|
468
|
+
font-style: italic;
|
|
469
|
+
letter-spacing: 0.04em;
|
|
470
|
+
color: var(--inv-ink-soft);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* ─── Figure · ranked-bars chart in the image slot ──────────── */
|
|
474
|
+
.np-figure {
|
|
475
|
+
background: var(--paper-soft);
|
|
476
|
+
border: 1px solid var(--ink);
|
|
477
|
+
padding: 12px 14px 10px;
|
|
478
|
+
}
|
|
479
|
+
.np-figure-bars {
|
|
480
|
+
display: flex;
|
|
481
|
+
flex-direction: column;
|
|
482
|
+
gap: 8px;
|
|
483
|
+
margin-bottom: 10px;
|
|
484
|
+
}
|
|
485
|
+
.np-bar {
|
|
486
|
+
display: grid;
|
|
487
|
+
grid-template-columns: 1fr auto;
|
|
488
|
+
gap: 6px;
|
|
489
|
+
align-items: baseline;
|
|
490
|
+
font-family: var(--serif);
|
|
491
|
+
font-size: 11.5px;
|
|
492
|
+
color: var(--ink);
|
|
493
|
+
}
|
|
494
|
+
.np-bar-row {
|
|
495
|
+
grid-column: 1 / -1;
|
|
496
|
+
height: 10px;
|
|
497
|
+
background:
|
|
498
|
+
repeating-linear-gradient(
|
|
499
|
+
45deg,
|
|
500
|
+
var(--paper-edge) 0,
|
|
501
|
+
var(--paper-edge) 4px,
|
|
502
|
+
var(--paper-soft) 4px,
|
|
503
|
+
var(--paper-soft) 8px
|
|
504
|
+
);
|
|
505
|
+
border: 1px solid var(--ink);
|
|
506
|
+
position: relative;
|
|
507
|
+
margin-top: 2px;
|
|
508
|
+
}
|
|
509
|
+
.np-bar-fill {
|
|
510
|
+
position: absolute;
|
|
511
|
+
inset: 0 auto 0 0;
|
|
512
|
+
background: var(--ink);
|
|
513
|
+
}
|
|
514
|
+
.np-bar-value {
|
|
515
|
+
font-family: var(--mono);
|
|
516
|
+
font-size: 10.5px;
|
|
517
|
+
color: var(--ink-soft);
|
|
518
|
+
letter-spacing: 0.02em;
|
|
519
|
+
}
|
|
520
|
+
.np-figcaption {
|
|
521
|
+
font-family: var(--serif);
|
|
522
|
+
font-size: 11px;
|
|
523
|
+
font-style: italic;
|
|
524
|
+
color: var(--ink-mid);
|
|
525
|
+
text-align: right;
|
|
526
|
+
margin-top: 4px;
|
|
527
|
+
letter-spacing: 0.02em;
|
|
528
|
+
}
|
|
529
|
+
.np-figcaption::before {
|
|
530
|
+
content: "Fig. ";
|
|
531
|
+
font-style: normal;
|
|
532
|
+
color: var(--ink);
|
|
533
|
+
font-weight: 700;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* ─── More headings sidebar list ────────────────────────────── */
|
|
537
|
+
.np-list {
|
|
538
|
+
list-style: none;
|
|
539
|
+
margin: 0;
|
|
540
|
+
padding: 0;
|
|
541
|
+
display: flex;
|
|
542
|
+
flex-direction: column;
|
|
543
|
+
gap: 10px;
|
|
544
|
+
}
|
|
545
|
+
.np-list li {
|
|
546
|
+
font-family: var(--serif);
|
|
547
|
+
font-size: 12.5px;
|
|
548
|
+
line-height: 1.5;
|
|
549
|
+
color: var(--ink-soft);
|
|
550
|
+
padding-bottom: 10px;
|
|
551
|
+
border-bottom: 1px solid var(--rule);
|
|
552
|
+
}
|
|
553
|
+
.np-list li:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
554
|
+
.np-list li b {
|
|
555
|
+
font-family: var(--serif-display);
|
|
556
|
+
font-size: 13px;
|
|
557
|
+
font-weight: 700;
|
|
558
|
+
color: var(--ink);
|
|
559
|
+
text-transform: uppercase;
|
|
560
|
+
letter-spacing: 0.02em;
|
|
561
|
+
margin-right: 4px;
|
|
562
|
+
}
|
|
563
|
+
.np-list li b::after {
|
|
564
|
+
content: ":";
|
|
565
|
+
color: var(--ink);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* ─── Pull quote · used in the bottom-middle column ─────────── */
|
|
569
|
+
.np-pull {
|
|
570
|
+
font-family: var(--serif-display);
|
|
571
|
+
font-size: 18px;
|
|
572
|
+
font-weight: 600;
|
|
573
|
+
font-style: italic;
|
|
574
|
+
line-height: 1.3;
|
|
575
|
+
color: var(--ink);
|
|
576
|
+
text-align: center;
|
|
577
|
+
padding: 10px 0;
|
|
578
|
+
border-top: 1px solid var(--ink);
|
|
579
|
+
border-bottom: 1px solid var(--ink);
|
|
580
|
+
margin: 8px 0;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/* ─── Footer ────────────────────────────────────────────────── */
|
|
584
|
+
.np-footer {
|
|
585
|
+
display: flex;
|
|
586
|
+
justify-content: space-between;
|
|
587
|
+
align-items: baseline;
|
|
588
|
+
gap: 14px;
|
|
589
|
+
margin-top: 18px;
|
|
590
|
+
padding-top: 12px;
|
|
591
|
+
border-top: 1px solid var(--ink);
|
|
592
|
+
font-family: var(--serif);
|
|
593
|
+
font-size: 11px;
|
|
594
|
+
font-style: italic;
|
|
595
|
+
color: var(--ink-mid);
|
|
596
|
+
flex-wrap: wrap;
|
|
597
|
+
}
|
|
598
|
+
.np-footer-stamp {
|
|
599
|
+
font-family: var(--mono);
|
|
600
|
+
font-size: 10.5px;
|
|
601
|
+
font-style: normal;
|
|
602
|
+
color: var(--ink);
|
|
603
|
+
letter-spacing: 0.04em;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/* ─── States · loading / error / empty ─────────────────────── */
|
|
607
|
+
.np-state {
|
|
608
|
+
max-width: 560px;
|
|
609
|
+
margin: 80px auto;
|
|
610
|
+
padding: 48px 36px;
|
|
611
|
+
text-align: center;
|
|
612
|
+
background: var(--paper);
|
|
613
|
+
border: 1px solid var(--ink);
|
|
614
|
+
box-shadow: var(--shadow-doc);
|
|
615
|
+
}
|
|
616
|
+
.np-state-mark {
|
|
617
|
+
font-family: var(--mono);
|
|
618
|
+
font-size: 10px;
|
|
619
|
+
letter-spacing: 0.18em;
|
|
620
|
+
text-transform: uppercase;
|
|
621
|
+
color: var(--ink);
|
|
622
|
+
border: 1px solid var(--ink);
|
|
623
|
+
padding: 5px 14px;
|
|
624
|
+
display: inline-block;
|
|
625
|
+
margin-bottom: 16px;
|
|
626
|
+
font-weight: 700;
|
|
627
|
+
}
|
|
628
|
+
.np-state-title {
|
|
629
|
+
font-family: var(--serif-display);
|
|
630
|
+
font-size: 24px;
|
|
631
|
+
font-weight: 700;
|
|
632
|
+
color: var(--ink);
|
|
633
|
+
margin-bottom: 10px;
|
|
634
|
+
line-height: 1.2;
|
|
635
|
+
text-transform: uppercase;
|
|
636
|
+
letter-spacing: 0.02em;
|
|
637
|
+
}
|
|
638
|
+
.np-state-body {
|
|
639
|
+
font-family: var(--serif);
|
|
640
|
+
font-size: 13.5px;
|
|
641
|
+
color: var(--ink-soft);
|
|
642
|
+
line-height: 1.6;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
646
|
+
LAYOUT-SPECIFIC GRIDS · variant 2 + variant 3 ship distinct
|
|
647
|
+
structures, not just paint changes. Variant 1 keeps the original
|
|
648
|
+
symmetric 3+3 broadsheet grid.
|
|
649
|
+
═════════════════════════════════════════════════════════════════ */
|
|
650
|
+
|
|
651
|
+
/* Layout 2 · wide hero band · lead story 2/3 LEFT (drop-cap +
|
|
652
|
+
bottom-line callout below) · sidebar 1/3 RIGHT (figure stacked
|
|
653
|
+
above date-stack so the column never reads as short). The
|
|
654
|
+
`:first-child` column gets the vertical column rule so the rule
|
|
655
|
+
paints between the two regardless of which child sits first. */
|
|
656
|
+
.np-hero-band {
|
|
657
|
+
display: grid;
|
|
658
|
+
grid-template-columns: 2fr 1fr;
|
|
659
|
+
gap: 24px;
|
|
660
|
+
margin: 8px 0 12px;
|
|
661
|
+
align-items: stretch;
|
|
662
|
+
}
|
|
663
|
+
.np-hero-band > :first-child {
|
|
664
|
+
padding-right: 24px;
|
|
665
|
+
border-right: 1px solid var(--rule);
|
|
666
|
+
}
|
|
667
|
+
.np-hero-band > .np-hero-figure {
|
|
668
|
+
display: flex;
|
|
669
|
+
flex-direction: column;
|
|
670
|
+
gap: 12px;
|
|
671
|
+
}
|
|
672
|
+
.np-hero-band > .np-hero-lead {
|
|
673
|
+
display: flex;
|
|
674
|
+
flex-direction: column;
|
|
675
|
+
gap: 12px;
|
|
676
|
+
}
|
|
677
|
+
@media (max-width: 720px) {
|
|
678
|
+
.np-hero-band { grid-template-columns: 1fr; gap: 18px; }
|
|
679
|
+
.np-hero-band > :first-child {
|
|
680
|
+
border-right: 0;
|
|
681
|
+
padding-right: 0;
|
|
682
|
+
border-bottom: 1px solid var(--rule);
|
|
683
|
+
padding-bottom: 18px;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.np-grid-4 {
|
|
688
|
+
display: grid;
|
|
689
|
+
grid-template-columns: 1fr 1fr 1fr 1fr;
|
|
690
|
+
gap: 18px;
|
|
691
|
+
margin: 12px 0;
|
|
692
|
+
}
|
|
693
|
+
.np-grid-4 > .np-col {
|
|
694
|
+
padding-right: 18px;
|
|
695
|
+
border-right: 1px solid var(--rule);
|
|
696
|
+
}
|
|
697
|
+
.np-grid-4 > .np-col:last-child {
|
|
698
|
+
padding-right: 0;
|
|
699
|
+
border-right: 0;
|
|
700
|
+
}
|
|
701
|
+
@media (max-width: 720px) {
|
|
702
|
+
.np-grid-4 { grid-template-columns: 1fr; }
|
|
703
|
+
.np-grid-4 > .np-col {
|
|
704
|
+
border-right: 0;
|
|
705
|
+
padding-right: 0;
|
|
706
|
+
padding-bottom: 18px;
|
|
707
|
+
border-bottom: 1px solid var(--rule);
|
|
708
|
+
}
|
|
709
|
+
.np-grid-4 > .np-col:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/* Centered pull-quote band · used by layout 2 between the
|
|
713
|
+
narrow-column body and the talking-points tail. */
|
|
714
|
+
.np-pull-band {
|
|
715
|
+
text-align: center;
|
|
716
|
+
font-family: var(--serif-display);
|
|
717
|
+
font-size: 22px;
|
|
718
|
+
font-style: italic;
|
|
719
|
+
line-height: 1.35;
|
|
720
|
+
color: var(--ink);
|
|
721
|
+
padding: 16px 36px;
|
|
722
|
+
letter-spacing: -0.005em;
|
|
723
|
+
margin: 10px auto;
|
|
724
|
+
max-width: 720px;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/* 2-col grid · used by layout 2 + layout 3 for the talking-points
|
|
728
|
+
tail (split into halves). */
|
|
729
|
+
.np-grid-2 {
|
|
730
|
+
display: grid;
|
|
731
|
+
grid-template-columns: 1fr 1fr;
|
|
732
|
+
gap: 22px;
|
|
733
|
+
margin: 12px 0;
|
|
734
|
+
}
|
|
735
|
+
.np-grid-2 > .np-col {
|
|
736
|
+
padding-right: 22px;
|
|
737
|
+
border-right: 1px solid var(--rule);
|
|
738
|
+
}
|
|
739
|
+
.np-grid-2 > .np-col:last-child {
|
|
740
|
+
padding-right: 0;
|
|
741
|
+
border-right: 0;
|
|
742
|
+
}
|
|
743
|
+
@media (max-width: 720px) {
|
|
744
|
+
.np-grid-2 { grid-template-columns: 1fr; gap: 18px; }
|
|
745
|
+
.np-grid-2 > .np-col {
|
|
746
|
+
border-right: 0;
|
|
747
|
+
padding-right: 0;
|
|
748
|
+
border-bottom: 1px solid var(--rule);
|
|
749
|
+
padding-bottom: 18px;
|
|
750
|
+
}
|
|
751
|
+
.np-grid-2 > .np-col:last-child { border-bottom: 0; padding-bottom: 0; }
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/* Layout 3 · asymmetric 2/3 main + 1/3 sidebar. The sidebar
|
|
755
|
+
stacks all the non-prose elements (callouts + figure/headings)
|
|
756
|
+
while the main column carries the lead + secondary stories. */
|
|
757
|
+
.np-hero-asym {
|
|
758
|
+
display: grid;
|
|
759
|
+
grid-template-columns: 2fr 1fr;
|
|
760
|
+
gap: 28px;
|
|
761
|
+
margin: 10px 0;
|
|
762
|
+
align-items: start;
|
|
763
|
+
}
|
|
764
|
+
.np-hero-asym > .np-hero-main {
|
|
765
|
+
padding-right: 28px;
|
|
766
|
+
border-right: 1px solid var(--rule);
|
|
767
|
+
display: flex;
|
|
768
|
+
flex-direction: column;
|
|
769
|
+
gap: 12px;
|
|
770
|
+
}
|
|
771
|
+
.np-hero-asym > .np-hero-side {
|
|
772
|
+
display: flex;
|
|
773
|
+
flex-direction: column;
|
|
774
|
+
gap: 14px;
|
|
775
|
+
}
|
|
776
|
+
@media (max-width: 720px) {
|
|
777
|
+
.np-hero-asym { grid-template-columns: 1fr; gap: 18px; }
|
|
778
|
+
.np-hero-asym > .np-hero-main {
|
|
779
|
+
padding-right: 0;
|
|
780
|
+
border-right: 0;
|
|
781
|
+
padding-bottom: 18px;
|
|
782
|
+
border-bottom: 1px solid var(--rule);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/* Framed pull-quote · used by layout 3 · double-rule top + bottom
|
|
787
|
+
for an antique broadsheet pull-quote feel. */
|
|
788
|
+
.np-pull-framed {
|
|
789
|
+
text-align: center;
|
|
790
|
+
font-family: var(--serif-display);
|
|
791
|
+
font-size: 22px;
|
|
792
|
+
font-style: italic;
|
|
793
|
+
line-height: 1.4;
|
|
794
|
+
color: var(--ink);
|
|
795
|
+
padding: 24px 36px;
|
|
796
|
+
border-top: 2px double var(--ink);
|
|
797
|
+
border-bottom: 2px double var(--ink);
|
|
798
|
+
margin: 12px auto;
|
|
799
|
+
max-width: 760px;
|
|
800
|
+
letter-spacing: -0.005em;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/* Centered figure · used by layout 3 at the bottom of the page
|
|
804
|
+
(the chart sits as the closing visual element rather than tucked
|
|
805
|
+
into a column). */
|
|
806
|
+
.np-figure-centered {
|
|
807
|
+
max-width: 480px;
|
|
808
|
+
margin: 12px auto;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
812
|
+
VARIANT 2 · Modern monochrome (Le Monde / Berliner register)
|
|
813
|
+
Cooler off-white paper, near-black ink, deep red spot accent.
|
|
814
|
+
Masthead nameplate flips to a CONDENSED SANS for the visual
|
|
815
|
+
identity shift; the body stays serif for editorial readability.
|
|
816
|
+
The dark callout fills become solid black with a red kicker.
|
|
817
|
+
═════════════════════════════════════════════════════════════════ */
|
|
818
|
+
body[data-np-variant="2"] {
|
|
819
|
+
--bg: #ECE7DA;
|
|
820
|
+
--bg-radial: rgba(20, 20, 15, 0.04);
|
|
821
|
+
--paper: #F8F4EA;
|
|
822
|
+
--paper-soft: #F2EDE0;
|
|
823
|
+
--paper-edge: #E8E2D2;
|
|
824
|
+
--ink: #14140F;
|
|
825
|
+
--ink-soft: #2C2A24;
|
|
826
|
+
--ink-mid: #5A574F;
|
|
827
|
+
--ink-faint: #8A867D;
|
|
828
|
+
--ink-muted: #B0ABA0;
|
|
829
|
+
--inv-bg: #14140F;
|
|
830
|
+
--inv-bg-soft: #1F1E18;
|
|
831
|
+
--inv-ink: #FFFFFF;
|
|
832
|
+
--inv-ink-soft:#C4C0B6;
|
|
833
|
+
--rule: #B0ABA0;
|
|
834
|
+
--rule-soft: #C8C4B8;
|
|
835
|
+
--rule-strong: #75716A;
|
|
836
|
+
--accent: #B12B2B;
|
|
837
|
+
--top-bar-bg: rgba(236, 231, 218, 0.92);
|
|
838
|
+
}
|
|
839
|
+
body[data-np-variant="2"] .np-mast-name {
|
|
840
|
+
font-family: "Inter", "Helvetica Neue", -apple-system,
|
|
841
|
+
"PingFang SC", "Hiragino Sans GB", sans-serif;
|
|
842
|
+
font-weight: 900;
|
|
843
|
+
font-stretch: 80%;
|
|
844
|
+
letter-spacing: -0.04em;
|
|
845
|
+
font-size: 70px;
|
|
846
|
+
}
|
|
847
|
+
@media (max-width: 720px) {
|
|
848
|
+
body[data-np-variant="2"] .np-mast-name { font-size: 46px; }
|
|
849
|
+
}
|
|
850
|
+
body[data-np-variant="2"] .np-frontpage-title { letter-spacing: -0.005em; }
|
|
851
|
+
body[data-np-variant="2"] .np-rule-double {
|
|
852
|
+
height: 2px;
|
|
853
|
+
background: var(--ink);
|
|
854
|
+
}
|
|
855
|
+
body[data-np-variant="2"] .np-callout-label {
|
|
856
|
+
color: var(--accent);
|
|
857
|
+
letter-spacing: 0.22em;
|
|
858
|
+
}
|
|
859
|
+
body[data-np-variant="2"] .np-callout-date .np-date-day {
|
|
860
|
+
color: var(--accent);
|
|
861
|
+
}
|
|
862
|
+
body[data-np-variant="2"] .np-conclusion-mark { color: var(--accent); }
|
|
863
|
+
body[data-np-variant="2"] .np-flow-mark { color: var(--accent); }
|
|
864
|
+
body[data-np-variant="2"] .np-prose-lead::first-letter {
|
|
865
|
+
color: var(--accent);
|
|
866
|
+
}
|
|
867
|
+
body[data-np-variant="2"] .np-bar-fill { background: var(--ink); }
|
|
868
|
+
body[data-np-variant="2"] .np-bar-row {
|
|
869
|
+
background: repeating-linear-gradient(
|
|
870
|
+
45deg,
|
|
871
|
+
#E8E2D2 0,
|
|
872
|
+
#E8E2D2 4px,
|
|
873
|
+
#F8F4EA 4px,
|
|
874
|
+
#F8F4EA 8px
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
body[data-np-variant="2"] .np-grid > .np-col {
|
|
878
|
+
border-right-color: var(--accent);
|
|
879
|
+
}
|
|
880
|
+
body[data-np-variant="2"] .np-list li b {
|
|
881
|
+
color: var(--accent);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
885
|
+
VARIANT 3 · Vintage / Sepia (penny-press · turn-of-the-century)
|
|
886
|
+
Deeper aged-paper, warm sepia ink, amber accent. Masthead flips
|
|
887
|
+
to an italic high-contrast Didot/Bodoni-style display serif.
|
|
888
|
+
Section dividers become ornamental ◆◆◆ dingbat rows. Callouts
|
|
889
|
+
pick up a 2px double border for a framed, antique feel.
|
|
890
|
+
═════════════════════════════════════════════════════════════════ */
|
|
891
|
+
body[data-np-variant="3"] {
|
|
892
|
+
--bg: #C9B286;
|
|
893
|
+
--bg-radial: rgba(155, 100, 50, 0.10);
|
|
894
|
+
--paper: #E5D2A4;
|
|
895
|
+
--paper-soft: #EBDAB0;
|
|
896
|
+
--paper-edge: #DCC68F;
|
|
897
|
+
--ink: #3D2818;
|
|
898
|
+
--ink-soft: #5C3F22;
|
|
899
|
+
--ink-mid: #7A593A;
|
|
900
|
+
--ink-faint: #9A7E5A;
|
|
901
|
+
--ink-muted: #B89D7A;
|
|
902
|
+
--inv-bg: #3D2818;
|
|
903
|
+
--inv-bg-soft: #4F3622;
|
|
904
|
+
--inv-ink: #F2DBB4;
|
|
905
|
+
--inv-ink-soft:#C7AC7E;
|
|
906
|
+
--rule: #A88D5F;
|
|
907
|
+
--rule-soft: #BFA677;
|
|
908
|
+
--rule-strong: #6B4A1F;
|
|
909
|
+
--accent: #B5722E;
|
|
910
|
+
--top-bar-bg: rgba(201, 178, 134, 0.92);
|
|
911
|
+
}
|
|
912
|
+
body[data-np-variant="3"] .np-mast-name {
|
|
913
|
+
font-family: "Didot", "Bodoni 72", "Bodoni Moda",
|
|
914
|
+
"Tiempos Headline", "Playfair Display", Georgia, serif;
|
|
915
|
+
font-weight: 900;
|
|
916
|
+
font-style: italic;
|
|
917
|
+
letter-spacing: 0.02em;
|
|
918
|
+
font-size: 60px;
|
|
919
|
+
}
|
|
920
|
+
@media (max-width: 720px) {
|
|
921
|
+
body[data-np-variant="3"] .np-mast-name { font-size: 42px; }
|
|
922
|
+
}
|
|
923
|
+
/* Replace the double-rule horizontal divider with a dingbat row.
|
|
924
|
+
Keeps the visual rhythm but reads as engraved/antique. */
|
|
925
|
+
body[data-np-variant="3"] .np-rule-double {
|
|
926
|
+
height: auto;
|
|
927
|
+
background: transparent;
|
|
928
|
+
margin: 18px 0;
|
|
929
|
+
text-align: center;
|
|
930
|
+
font-family: "Didot", "Bodoni 72", serif;
|
|
931
|
+
color: var(--ink);
|
|
932
|
+
font-size: 14px;
|
|
933
|
+
letter-spacing: 0.6em;
|
|
934
|
+
line-height: 1;
|
|
935
|
+
}
|
|
936
|
+
body[data-np-variant="3"] .np-rule-double::before {
|
|
937
|
+
content: "◆ ◆ ◆";
|
|
938
|
+
}
|
|
939
|
+
body[data-np-variant="3"] .np-callout {
|
|
940
|
+
border: 2px double var(--accent);
|
|
941
|
+
}
|
|
942
|
+
body[data-np-variant="3"] .np-callout-label,
|
|
943
|
+
body[data-np-variant="3"] .np-callout-date .np-date-day {
|
|
944
|
+
color: var(--accent);
|
|
945
|
+
}
|
|
946
|
+
body[data-np-variant="3"] .np-conclusion-mark::before {
|
|
947
|
+
color: var(--accent);
|
|
948
|
+
}
|
|
949
|
+
body[data-np-variant="3"] .np-flow-mark::before { color: var(--accent); }
|
|
950
|
+
body[data-np-variant="3"] .np-prose-lead::first-letter {
|
|
951
|
+
color: var(--accent);
|
|
952
|
+
font-family: "Didot", "Bodoni 72", "Tiempos Headline", Georgia, serif;
|
|
953
|
+
font-style: italic;
|
|
954
|
+
font-size: 48px;
|
|
955
|
+
}
|
|
956
|
+
body[data-np-variant="3"] .np-bar-fill { background: var(--accent); }
|
|
957
|
+
body[data-np-variant="3"] .np-figure { border: 2px double var(--ink); }
|
|
958
|
+
body[data-np-variant="3"] .np-list li b {
|
|
959
|
+
font-family: "Didot", "Bodoni 72", "Tiempos Headline", Georgia, serif;
|
|
960
|
+
font-style: italic;
|
|
961
|
+
}
|
|
962
|
+
body[data-np-variant="3"] .np-frontpage-title {
|
|
963
|
+
font-style: italic;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/* ─── Print · drop chrome ───────────────────────────────────── */
|
|
967
|
+
@media print {
|
|
968
|
+
.np-top-bar { display: none; }
|
|
969
|
+
body, html { background: white; }
|
|
970
|
+
.np-doc {
|
|
971
|
+
max-width: none;
|
|
972
|
+
margin: 0;
|
|
973
|
+
box-shadow: none;
|
|
974
|
+
}
|
|
975
|
+
.np-col, .np-callout, .np-figure,
|
|
976
|
+
.np-hero-band > .np-hero-figure,
|
|
977
|
+
.np-hero-asym > .np-hero-main,
|
|
978
|
+
.np-pull-band, .np-pull-framed {
|
|
979
|
+
break-inside: avoid;
|
|
980
|
+
page-break-inside: avoid;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
</style>
|
|
984
|
+
</head>
|
|
985
|
+
<body>
|
|
986
|
+
|
|
987
|
+
<header class="np-top-bar" data-np-chrome>
|
|
988
|
+
<a href="/" class="np-crumb">PrivateBoard <span class="np-crumb-accent">· newspaper</span></a>
|
|
989
|
+
<div class="np-actions">
|
|
990
|
+
<button type="button" class="np-btn" data-np-png>
|
|
991
|
+
<span class="glyph">↓</span>PNG
|
|
992
|
+
</button>
|
|
993
|
+
<button type="button" class="np-btn" data-np-print>
|
|
994
|
+
<span class="glyph">↓</span>PDF
|
|
995
|
+
</button>
|
|
996
|
+
</div>
|
|
997
|
+
</header>
|
|
998
|
+
|
|
999
|
+
<main data-np-root>
|
|
1000
|
+
<div class="np-state">
|
|
1001
|
+
<div class="np-state-mark">Loading</div>
|
|
1002
|
+
<div class="np-state-title">Loading newspaper…</div>
|
|
1003
|
+
<div class="np-state-body">Fetching the broadsheet for this brief.</div>
|
|
1004
|
+
</div>
|
|
1005
|
+
</main>
|
|
1006
|
+
|
|
1007
|
+
<script>
|
|
1008
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
1009
|
+
Newspaper renderer · reads the same BentoScaffold JSON that bento /
|
|
1010
|
+
magazine modes produce, but maps the slots into a broadsheet layout.
|
|
1011
|
+
|
|
1012
|
+
Slot mapping:
|
|
1013
|
+
title → front-page banner headline
|
|
1014
|
+
kicker → subdeck under the headline
|
|
1015
|
+
source → masthead byline (used in flank decoration)
|
|
1016
|
+
footerTag → meta-strip date / date-stack callout
|
|
1017
|
+
milestones[3] → 3 main column-stories
|
|
1018
|
+
· LEFT: heading + BOTTOM-LINE callout + body
|
|
1019
|
+
· MIDDLE: lead drop-cap body + date callout +
|
|
1020
|
+
continuation prose
|
|
1021
|
+
· RIGHT: figure (rankedBars chart) + heading + body
|
|
1022
|
+
rankedBars → figure card in the right column's "image slot"
|
|
1023
|
+
verification → "MORE HEADINGS" stacked sidebar in bottom-left
|
|
1024
|
+
talkingPoints → bottom-middle / bottom-right editorial paragraphs
|
|
1025
|
+
(with one card promoted to a centered pull-quote)
|
|
1026
|
+
conclusion → BOTTOM-LINE inverted callout in the LEFT column
|
|
1027
|
+
flow → bottom-right "FROM HERE" closing prose
|
|
1028
|
+
|
|
1029
|
+
The renderer is defensive — it skips any slot that's missing/empty
|
|
1030
|
+
instead of leaving placeholder boxes, so partial data still
|
|
1031
|
+
produces a clean page.
|
|
1032
|
+
────────────────────────────────────────────────────────────── */
|
|
1033
|
+
(function () {
|
|
1034
|
+
const params = new URLSearchParams(location.search);
|
|
1035
|
+
const briefId = (params.get("b") || "").trim();
|
|
1036
|
+
const roomId = (params.get("r") || "").trim();
|
|
1037
|
+
const root = document.querySelector("[data-np-root]");
|
|
1038
|
+
|
|
1039
|
+
/** Pick one of three newspaper variants deterministically from
|
|
1040
|
+
* the brief id, so a refresh always shows the same look for the
|
|
1041
|
+
* same brief. The `?v=1|2|3` URL parameter overrides the hash
|
|
1042
|
+
* for previewing / debugging.
|
|
1043
|
+
*
|
|
1044
|
+
* · 1 · Broadsheet · the default WSJ / FT register · cream paper
|
|
1045
|
+
* + deep brown ink + heavy serif nameplate
|
|
1046
|
+
* · 2 · Modern monochrome · Le Monde register · cooler off-white
|
|
1047
|
+
* + near-black + deep red accent · CONDENSED SANS nameplate
|
|
1048
|
+
* · 3 · Vintage sepia · penny-press register · aged sepia paper
|
|
1049
|
+
* + amber accent · italic Didot/Bodoni nameplate · ◆◆◆
|
|
1050
|
+
* ornamental dingbats replace the double-rule dividers
|
|
1051
|
+
*/
|
|
1052
|
+
function pickVariant(id) {
|
|
1053
|
+
const force = (params.get("v") || "").trim();
|
|
1054
|
+
if (force === "1" || force === "2" || force === "3") return parseInt(force, 10);
|
|
1055
|
+
const s = String(id || "");
|
|
1056
|
+
if (!s) return 1;
|
|
1057
|
+
let h = 0;
|
|
1058
|
+
for (let i = 0; i < s.length; i++) {
|
|
1059
|
+
h = ((h << 5) - h) + s.charCodeAt(i);
|
|
1060
|
+
h |= 0;
|
|
1061
|
+
}
|
|
1062
|
+
return (Math.abs(h) % 3) + 1;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function escape(s) {
|
|
1066
|
+
return String(s == null ? "" : s)
|
|
1067
|
+
.replace(/&/g, "&")
|
|
1068
|
+
.replace(/</g, "<")
|
|
1069
|
+
.replace(/>/g, ">")
|
|
1070
|
+
.replace(/"/g, """)
|
|
1071
|
+
.replace(/'/g, "'");
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function showState(mark, title, body) {
|
|
1075
|
+
root.innerHTML = `
|
|
1076
|
+
<div class="np-state">
|
|
1077
|
+
<div class="np-state-mark">${escape(mark)}</div>
|
|
1078
|
+
<div class="np-state-title">${escape(title)}</div>
|
|
1079
|
+
<div class="np-state-body">${escape(body)}</div>
|
|
1080
|
+
</div>`;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
async function loadBrief() {
|
|
1084
|
+
let url;
|
|
1085
|
+
if (briefId) {
|
|
1086
|
+
url = `/api/briefs/${encodeURIComponent(briefId)}`;
|
|
1087
|
+
} else if (roomId) {
|
|
1088
|
+
url = `/api/rooms/${encodeURIComponent(roomId)}/brief`;
|
|
1089
|
+
} else {
|
|
1090
|
+
showState("Missing query", "No brief specified",
|
|
1091
|
+
"Add ?b=<briefId> or ?r=<roomId> to the URL.");
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
const res = await fetch(url);
|
|
1095
|
+
if (!res.ok) {
|
|
1096
|
+
const e = await res.json().catch(() => ({}));
|
|
1097
|
+
showState("Not found", "Brief not found",
|
|
1098
|
+
e.error || "The requested brief doesn't exist or is no longer available.");
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
return await res.json();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** Split a "Heading: body." style verification bullet on the
|
|
1105
|
+
* first colon. Falls back to em-dash / middle-dot, then to
|
|
1106
|
+
* whole-bullet-as-heading. The newspaper prompt asks for colon
|
|
1107
|
+
* separators specifically. */
|
|
1108
|
+
function splitHeading(raw) {
|
|
1109
|
+
const s = String(raw || "").trim();
|
|
1110
|
+
if (!s) return { heading: "", body: "" };
|
|
1111
|
+
const seps = [": ", " · ", " — ", " - "];
|
|
1112
|
+
for (const sep of seps) {
|
|
1113
|
+
const idx = s.indexOf(sep);
|
|
1114
|
+
if (idx > 0 && idx < 60) {
|
|
1115
|
+
return {
|
|
1116
|
+
heading: s.slice(0, idx).trim(),
|
|
1117
|
+
body: s.slice(idx + sep.length).trim(),
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return { heading: s, body: "" };
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/** Render the rankedBars chart inside the right column's figure
|
|
1125
|
+
* slot · diagonal-hatched track + solid ink fill, mono labels,
|
|
1126
|
+
* newspaper-graphic register. Skipped when the brief has no
|
|
1127
|
+
* ranked-numeric material. */
|
|
1128
|
+
function renderFigure(rankedBars, fallbackCaption) {
|
|
1129
|
+
if (!rankedBars || !Array.isArray(rankedBars.entries) || rankedBars.entries.length === 0) {
|
|
1130
|
+
return "";
|
|
1131
|
+
}
|
|
1132
|
+
const rows = rankedBars.entries.slice(0, 5).map((e) => {
|
|
1133
|
+
const ratio = Math.max(0, Math.min(1, Number(e.ratio) || 0));
|
|
1134
|
+
const pct = (ratio * 100).toFixed(1);
|
|
1135
|
+
return `
|
|
1136
|
+
<div class="np-bar">
|
|
1137
|
+
<span>${escape(e.label || "")}</span>
|
|
1138
|
+
<span class="np-bar-value">${escape(e.value || "")}</span>
|
|
1139
|
+
<div class="np-bar-row"><span class="np-bar-fill" style="width: ${pct}%"></span></div>
|
|
1140
|
+
</div>`;
|
|
1141
|
+
}).join("");
|
|
1142
|
+
const caption = rankedBars.title || fallbackCaption || "Reading the room";
|
|
1143
|
+
return `
|
|
1144
|
+
<figure class="np-figure">
|
|
1145
|
+
<div class="np-figure-bars">${rows}</div>
|
|
1146
|
+
<figcaption class="np-figcaption">${escape(caption)}</figcaption>
|
|
1147
|
+
</figure>`;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function renderBottomLineCallout(conclusion) {
|
|
1151
|
+
if (!conclusion) return "";
|
|
1152
|
+
return `
|
|
1153
|
+
<aside class="np-callout">
|
|
1154
|
+
<div class="np-callout-label">Bottom line</div>
|
|
1155
|
+
<div class="np-callout-text">${escape(conclusion)}</div>
|
|
1156
|
+
</aside>`;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
/** Render the inverted date-stack callout. Pulls a date out of the
|
|
1160
|
+
* footerTag if one is present (matching `\d{4}` or `YYYY-MM-DD`);
|
|
1161
|
+
* otherwise uses today's date. */
|
|
1162
|
+
function renderDateCallout(footerTag) {
|
|
1163
|
+
const today = new Date();
|
|
1164
|
+
const months = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"];
|
|
1165
|
+
const days = ["SUNDAY","MONDAY","TUESDAY","WEDNESDAY","THURSDAY","FRIDAY","SATURDAY"];
|
|
1166
|
+
// Try to parse a date out of footerTag — supports YYYY-MM-DD anywhere in the string.
|
|
1167
|
+
let target = today;
|
|
1168
|
+
const m = String(footerTag || "").match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
1169
|
+
if (m) {
|
|
1170
|
+
const parsed = new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00`);
|
|
1171
|
+
if (!isNaN(parsed.getTime())) target = parsed;
|
|
1172
|
+
}
|
|
1173
|
+
const day = days[target.getDay()];
|
|
1174
|
+
const month = months[target.getMonth()];
|
|
1175
|
+
const num = String(target.getDate());
|
|
1176
|
+
const year = String(target.getFullYear());
|
|
1177
|
+
return `
|
|
1178
|
+
<aside class="np-callout np-callout-date">
|
|
1179
|
+
<div class="np-date-day">${day}</div>
|
|
1180
|
+
<div class="np-date-month">${month}</div>
|
|
1181
|
+
<div class="np-date-num">${num}</div>
|
|
1182
|
+
<div class="np-date-year">${year}</div>
|
|
1183
|
+
<div class="np-date-source">privateboard.app</div>
|
|
1184
|
+
</aside>`;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function renderColumnHeading(period, title) {
|
|
1188
|
+
// Use period as the small section banner; promote title to a
|
|
1189
|
+
// larger column heading when both are present and meaningfully
|
|
1190
|
+
// different.
|
|
1191
|
+
const p = String(period || "").trim();
|
|
1192
|
+
const t = String(title || "").trim();
|
|
1193
|
+
if (!p && !t) return "";
|
|
1194
|
+
if (p && t && p.toLowerCase() !== t.toLowerCase()) {
|
|
1195
|
+
return `
|
|
1196
|
+
<h4 class="np-col-heading">${escape(p)}</h4>
|
|
1197
|
+
<h3 class="np-col-heading np-col-heading-large">${escape(t)}</h3>`;
|
|
1198
|
+
}
|
|
1199
|
+
return `<h4 class="np-col-heading np-col-heading-large">${escape(t || p)}</h4>`;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function renderProse(text, options) {
|
|
1203
|
+
const opts = options || {};
|
|
1204
|
+
const t = String(text || "").trim();
|
|
1205
|
+
if (!t) return "";
|
|
1206
|
+
const klass = opts.lead ? "np-prose np-prose-lead" : "np-prose";
|
|
1207
|
+
return `<div class="${klass}">${escape(t)}</div>`;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function renderBullets(verification) {
|
|
1211
|
+
if (!verification || !Array.isArray(verification.bullets) || verification.bullets.length === 0) {
|
|
1212
|
+
return "";
|
|
1213
|
+
}
|
|
1214
|
+
const items = verification.bullets.slice(0, 5).map((b) => {
|
|
1215
|
+
const parts = splitHeading(b);
|
|
1216
|
+
const inner = parts.body
|
|
1217
|
+
? `<b>${escape(parts.heading)}</b>${escape(parts.body)}`
|
|
1218
|
+
: escape(parts.heading);
|
|
1219
|
+
return `<li>${inner}</li>`;
|
|
1220
|
+
}).join("");
|
|
1221
|
+
return `<ul class="np-list">${items}</ul>`;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
/** Render talking points · first one becomes a centered pull-quote
|
|
1225
|
+
* (the bottom-middle column's signature element); the rest become
|
|
1226
|
+
* paragraph prose for the right column. */
|
|
1227
|
+
function renderTalkingMiddle(talking) {
|
|
1228
|
+
if (!talking || !Array.isArray(talking.bullets) || talking.bullets.length === 0) {
|
|
1229
|
+
return "";
|
|
1230
|
+
}
|
|
1231
|
+
const first = talking.bullets[0];
|
|
1232
|
+
const rest = talking.bullets.slice(1, 3).map((b) => `<p>${escape(b)}</p>`).join("");
|
|
1233
|
+
return `
|
|
1234
|
+
<p class="np-pull">"${escape(first)}"</p>
|
|
1235
|
+
${rest ? `<div class="np-prose">${rest}</div>` : ""}`;
|
|
1236
|
+
}
|
|
1237
|
+
function renderTalkingTail(talking) {
|
|
1238
|
+
if (!talking || !Array.isArray(talking.bullets) || talking.bullets.length === 0) {
|
|
1239
|
+
return "";
|
|
1240
|
+
}
|
|
1241
|
+
const tail = talking.bullets.slice(3, 6).map((b) => `<p>${escape(b)}</p>`).join("");
|
|
1242
|
+
if (!tail) return "";
|
|
1243
|
+
return `<div class="np-prose">${tail}</div>`;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function render(brief) {
|
|
1247
|
+
if (brief.mode !== "newspaper") {
|
|
1248
|
+
showState("Wrong mode", "This brief isn't a newspaper",
|
|
1249
|
+
"Open it in the matching viewer instead. Newspaper, magazine, bento, and research-note are separate output modes.");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
const m = brief.bodyJson;
|
|
1253
|
+
if (!m || typeof m !== "object") {
|
|
1254
|
+
if (brief.isGenerating) {
|
|
1255
|
+
showState("Generating",
|
|
1256
|
+
"Newspaper is still being prepared",
|
|
1257
|
+
"The chair is currently composing the front page. Refresh in a few seconds.");
|
|
1258
|
+
} else {
|
|
1259
|
+
showState("Empty",
|
|
1260
|
+
"This brief has no newspaper data",
|
|
1261
|
+
"It may have failed to generate. Try regenerating from the room view.");
|
|
1262
|
+
}
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
if (m.title) document.title = `${m.title} · Newspaper`;
|
|
1267
|
+
|
|
1268
|
+
// Apply the variant attribute · drives the broadsheet / modern
|
|
1269
|
+
// / vintage skin via CSS. Setting on body so the page bg and
|
|
1270
|
+
// top-bar bg also retint per variant. Cleared / overwritten on
|
|
1271
|
+
// every render so re-renders within the same page (e.g. retry
|
|
1272
|
+
// path) pick up the right look.
|
|
1273
|
+
const variant = pickVariant(brief.id);
|
|
1274
|
+
document.body.setAttribute("data-np-variant", String(variant));
|
|
1275
|
+
|
|
1276
|
+
// Masthead flanks · use the source byline as the left flank's
|
|
1277
|
+
// "advertorial" decoration and a stable static line on the right
|
|
1278
|
+
// so the masthead reads as balanced regardless of brief length.
|
|
1279
|
+
const sourceTxt = m.source || "From the editor's desk";
|
|
1280
|
+
const leftFlankBody = m.kicker || sourceTxt;
|
|
1281
|
+
const flankLeft = `<b>${escape((sourceTxt || "Boardroom").toUpperCase())}</b>${escape(leftFlankBody.slice(0, 180))}`;
|
|
1282
|
+
const flankRight = `<b>EDITORIAL NOTE</b>An issue from the room · pour a coffee, take it column by column, and trust the numbers more than the slogans.`;
|
|
1283
|
+
|
|
1284
|
+
// Meta strip · prefer footerTag for the date format; fall back
|
|
1285
|
+
// to today.
|
|
1286
|
+
const today = new Date();
|
|
1287
|
+
const monthsShort = ["JAN","FEB","MAR","APR","MAY","JUN","JUL","AUG","SEP","OCT","NOV","DEC"];
|
|
1288
|
+
const daysShort = ["SUN","MON","TUE","WED","THU","FRI","SAT"];
|
|
1289
|
+
const dateMatch = String(m.footerTag || "").match(/(\d{4})-(\d{2})-(\d{2})/);
|
|
1290
|
+
const dispDate = (() => {
|
|
1291
|
+
const d = dateMatch ? new Date(`${dateMatch[1]}-${dateMatch[2]}-${dateMatch[3]}T00:00:00`) : today;
|
|
1292
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
1293
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
1294
|
+
const yyyy = d.getFullYear();
|
|
1295
|
+
const wk = daysShort[d.getDay()];
|
|
1296
|
+
return `${dd}.${mm}.${yyyy} / ${wk}`;
|
|
1297
|
+
})();
|
|
1298
|
+
const briefIdShort = brief.id ? `#${String(brief.id).slice(0, 10).toUpperCase()}` : "";
|
|
1299
|
+
|
|
1300
|
+
const milestones = (m.milestones || []).slice(0, 3);
|
|
1301
|
+
const ms0 = milestones[0] || {};
|
|
1302
|
+
const ms1 = milestones[1] || {};
|
|
1303
|
+
const ms2 = milestones[2] || {};
|
|
1304
|
+
|
|
1305
|
+
const figure = renderFigure(m.rankedBars, ms2.title);
|
|
1306
|
+
const bottomLine = renderBottomLineCallout(m.conclusion);
|
|
1307
|
+
const dateStack = renderDateCallout(m.footerTag);
|
|
1308
|
+
const moreHeadings = renderBullets(m.verification);
|
|
1309
|
+
|
|
1310
|
+
// Bottom band · 3 cols · MORE HEADINGS / pull quote + tail / closing
|
|
1311
|
+
const flowText = (m.flow && Array.isArray(m.flow.nodes) && m.flow.nodes.length >= 2)
|
|
1312
|
+
? `<div class="np-prose">${escape(m.flow.nodes.join(" → "))}${m.flow.caption ? `<p style="font-style:italic;margin-top:6px;">${escape(m.flow.caption)}</p>` : ""}</div>`
|
|
1313
|
+
: "";
|
|
1314
|
+
|
|
1315
|
+
const parts = {
|
|
1316
|
+
m, ms0, ms1, ms2,
|
|
1317
|
+
flankLeft, flankRight, dispDate, briefIdShort,
|
|
1318
|
+
bottomLine, dateStack, figure, moreHeadings, flowText,
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
// Dispatch to the layout matching the picked variant. Each
|
|
1322
|
+
// layout owns its full <article> tree · they share helpers
|
|
1323
|
+
// (renderProse, renderFigure, renderColumnHeading, etc.) but
|
|
1324
|
+
// arrange the blocks differently.
|
|
1325
|
+
let html;
|
|
1326
|
+
if (variant === 2) html = layoutVariant2(parts);
|
|
1327
|
+
else if (variant === 3) html = layoutVariant3(parts);
|
|
1328
|
+
else html = layoutVariant1(parts);
|
|
1329
|
+
root.innerHTML = html;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/* ─────────────────────── Layout dispatch ───────────────────────
|
|
1333
|
+
* Three structurally distinct broadsheet layouts. Each takes the
|
|
1334
|
+
* pre-rendered field parts and composes them into its own <article>
|
|
1335
|
+
* tree. Variant 1 keeps the symmetric 3+3 broadsheet feel · variant
|
|
1336
|
+
* 2 leans modular with a 2:1 hero band, 4-narrow-column body, and
|
|
1337
|
+
* a centered pull-quote band · variant 3 goes asymmetric (2/3 main
|
|
1338
|
+
* + 1/3 sidebar), framed pull-quote, 2-col tail, and a centered
|
|
1339
|
+
* closing figure.
|
|
1340
|
+
* ─────────────────────────────────────────────────────────────── */
|
|
1341
|
+
|
|
1342
|
+
/** Layout 1 · classic 3+3 broadsheet (WSJ / FT register).
|
|
1343
|
+
* TOP grid · heading+callout+body / heading+lead-drop+date / figure+heading+body
|
|
1344
|
+
* BOTTOM grid · MORE HEADINGS / pull-quote + tail / closing prose */
|
|
1345
|
+
function layoutVariant1(p) {
|
|
1346
|
+
const { m, ms0, ms1, ms2, flankLeft, flankRight, dispDate, briefIdShort,
|
|
1347
|
+
bottomLine, dateStack, figure, moreHeadings, flowText } = p;
|
|
1348
|
+
const middlePull = renderTalkingMiddle(m.talkingPoints);
|
|
1349
|
+
const tailProse = renderTalkingTail(m.talkingPoints);
|
|
1350
|
+
const tailFallback = tailProse || flowText
|
|
1351
|
+
|| (m.conclusion ? `<h4 class="np-col-heading">From here</h4><div class="np-prose">${escape(m.conclusion)}</div>` : "");
|
|
1352
|
+
|
|
1353
|
+
return `
|
|
1354
|
+
<article class="np-doc np-layout-1" data-np-paper>
|
|
1355
|
+
<div class="np-rule"></div>
|
|
1356
|
+
|
|
1357
|
+
<header class="np-masthead">
|
|
1358
|
+
<div class="np-mast-flank np-mast-flank-left">${flankLeft}</div>
|
|
1359
|
+
<h1 class="np-mast-name">PrivateBoard</h1>
|
|
1360
|
+
<div class="np-mast-flank np-mast-flank-right">${flankRight}</div>
|
|
1361
|
+
</header>
|
|
1362
|
+
|
|
1363
|
+
<div class="np-rule"></div>
|
|
1364
|
+
|
|
1365
|
+
<div class="np-meta-strip">
|
|
1366
|
+
<span class="np-meta-left">${escape(dispDate)}</span>
|
|
1367
|
+
<span class="np-meta-center">www.privateboard.app</span>
|
|
1368
|
+
<span class="np-meta-right">${escape(briefIdShort)}</span>
|
|
1369
|
+
</div>
|
|
1370
|
+
|
|
1371
|
+
<div class="np-rule-double"></div>
|
|
1372
|
+
|
|
1373
|
+
<section class="np-frontpage">
|
|
1374
|
+
<h2 class="np-frontpage-title">${escape(m.title || "")}</h2>
|
|
1375
|
+
${m.kicker ? `<div class="np-frontpage-deck">${escape(m.kicker)}</div>` : ""}
|
|
1376
|
+
</section>
|
|
1377
|
+
|
|
1378
|
+
<div class="np-rule"></div>
|
|
1379
|
+
|
|
1380
|
+
<section class="np-grid">
|
|
1381
|
+
<div class="np-col">
|
|
1382
|
+
${renderColumnHeading(ms0.period, ms0.title)}
|
|
1383
|
+
${bottomLine}
|
|
1384
|
+
${renderProse(ms0.body)}
|
|
1385
|
+
${m.talkingPoints && m.talkingPoints.title ? `<div class="np-pagehint">See ${escape(m.talkingPoints.title)}</div>` : ""}
|
|
1386
|
+
</div>
|
|
1387
|
+
<div class="np-col">
|
|
1388
|
+
${renderColumnHeading(ms1.period, ms1.title)}
|
|
1389
|
+
${renderProse(ms1.body, { lead: true })}
|
|
1390
|
+
${dateStack}
|
|
1391
|
+
</div>
|
|
1392
|
+
<div class="np-col">
|
|
1393
|
+
${figure || ""}
|
|
1394
|
+
${renderColumnHeading(ms2.period, ms2.title)}
|
|
1395
|
+
${renderProse(ms2.body)}
|
|
1396
|
+
</div>
|
|
1397
|
+
</section>
|
|
1398
|
+
|
|
1399
|
+
<div class="np-rule"></div>
|
|
1400
|
+
|
|
1401
|
+
<section class="np-grid">
|
|
1402
|
+
<div class="np-col">
|
|
1403
|
+
<h4 class="np-col-heading">${escape((m.verification && m.verification.title) || "More headings")}</h4>
|
|
1404
|
+
${moreHeadings}
|
|
1405
|
+
</div>
|
|
1406
|
+
<div class="np-col">${middlePull}</div>
|
|
1407
|
+
<div class="np-col">${tailFallback}</div>
|
|
1408
|
+
</section>
|
|
1409
|
+
|
|
1410
|
+
<footer class="np-footer">
|
|
1411
|
+
<span>${escape(m.footerTag || "")}</span>
|
|
1412
|
+
<span class="np-footer-stamp">PrivateBoard · ${escape(briefIdShort)}</span>
|
|
1413
|
+
</footer>
|
|
1414
|
+
</article>`;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/** Layout 2 · modular Le Monde feel.
|
|
1418
|
+
* · LEAD STORY 2/3 wide on the LEFT (drop-cap body + bottom-
|
|
1419
|
+
* line callout below) · SIDEBAR 1/3 on the RIGHT stacking
|
|
1420
|
+
* figure ABOVE date-stack so the column always packs 2 items
|
|
1421
|
+
* (no short-figure-leaves-whitespace problem)
|
|
1422
|
+
* · Centered pull-quote band drawn from talking[0]
|
|
1423
|
+
* · 3-col tail · ms1 / ms2 / "more from the room" (verification
|
|
1424
|
+
* bullets + tail talking points + flow stacked dense in the
|
|
1425
|
+
* third column so it always reads full)
|
|
1426
|
+
*/
|
|
1427
|
+
function layoutVariant2(p) {
|
|
1428
|
+
const { m, ms0, ms1, ms2, flankLeft, flankRight, dispDate, briefIdShort,
|
|
1429
|
+
bottomLine, dateStack, figure, moreHeadings, flowText } = p;
|
|
1430
|
+
|
|
1431
|
+
const tp = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
|
|
1432
|
+
const pullText = tp[0] || "";
|
|
1433
|
+
const tail = tp.slice(1, 5).map((b) => `<p>${escape(b)}</p>`).join("");
|
|
1434
|
+
|
|
1435
|
+
// Sidebar always packs 2 stacked elements so the 1fr column
|
|
1436
|
+
// matches the lead story's height. When rankedBars is missing
|
|
1437
|
+
// we substitute a "From the desk" callout so the slot still
|
|
1438
|
+
// carries weight rather than going blank.
|
|
1439
|
+
const sidebarTop = figure
|
|
1440
|
+
|| `<aside class="np-callout"><div class="np-callout-label">From the desk</div><div class="np-callout-text">${escape((m.source || "Boardroom").toUpperCase())}</div></aside>`;
|
|
1441
|
+
|
|
1442
|
+
return `
|
|
1443
|
+
<article class="np-doc np-layout-2" data-np-paper>
|
|
1444
|
+
<div class="np-rule"></div>
|
|
1445
|
+
|
|
1446
|
+
<header class="np-masthead">
|
|
1447
|
+
<div class="np-mast-flank np-mast-flank-left">${flankLeft}</div>
|
|
1448
|
+
<h1 class="np-mast-name">PrivateBoard</h1>
|
|
1449
|
+
<div class="np-mast-flank np-mast-flank-right">${flankRight}</div>
|
|
1450
|
+
</header>
|
|
1451
|
+
|
|
1452
|
+
<div class="np-rule-double"></div>
|
|
1453
|
+
|
|
1454
|
+
<div class="np-meta-strip">
|
|
1455
|
+
<span class="np-meta-left">${escape(dispDate)}</span>
|
|
1456
|
+
<span class="np-meta-center">www.privateboard.app</span>
|
|
1457
|
+
<span class="np-meta-right">${escape(briefIdShort)}</span>
|
|
1458
|
+
</div>
|
|
1459
|
+
|
|
1460
|
+
<div class="np-rule"></div>
|
|
1461
|
+
|
|
1462
|
+
<section class="np-frontpage">
|
|
1463
|
+
<h2 class="np-frontpage-title">${escape(m.title || "")}</h2>
|
|
1464
|
+
${m.kicker ? `<div class="np-frontpage-deck">${escape(m.kicker)}</div>` : ""}
|
|
1465
|
+
</section>
|
|
1466
|
+
|
|
1467
|
+
<div class="np-rule"></div>
|
|
1468
|
+
|
|
1469
|
+
<!-- Lead story 2fr LEFT, dense sidebar 1fr RIGHT -->
|
|
1470
|
+
<section class="np-hero-band">
|
|
1471
|
+
<div class="np-hero-lead">
|
|
1472
|
+
${renderColumnHeading(ms0.period, ms0.title)}
|
|
1473
|
+
${renderProse(ms0.body, { lead: true })}
|
|
1474
|
+
${bottomLine}
|
|
1475
|
+
</div>
|
|
1476
|
+
<div class="np-hero-figure">
|
|
1477
|
+
${sidebarTop}
|
|
1478
|
+
${dateStack}
|
|
1479
|
+
</div>
|
|
1480
|
+
</section>
|
|
1481
|
+
|
|
1482
|
+
${pullText ? `
|
|
1483
|
+
<div class="np-rule"></div>
|
|
1484
|
+
<section class="np-pull-band">"${escape(pullText)}"</section>
|
|
1485
|
+
` : ""}
|
|
1486
|
+
|
|
1487
|
+
<div class="np-rule"></div>
|
|
1488
|
+
|
|
1489
|
+
<!-- 3-col tail · column 3 packs verification + talking +
|
|
1490
|
+
flow dense so it never reads as a short empty cell. -->
|
|
1491
|
+
<section class="np-grid">
|
|
1492
|
+
<div class="np-col">
|
|
1493
|
+
${renderColumnHeading(ms1.period, ms1.title)}
|
|
1494
|
+
${renderProse(ms1.body)}
|
|
1495
|
+
</div>
|
|
1496
|
+
<div class="np-col">
|
|
1497
|
+
${renderColumnHeading(ms2.period, ms2.title)}
|
|
1498
|
+
${renderProse(ms2.body)}
|
|
1499
|
+
</div>
|
|
1500
|
+
<div class="np-col">
|
|
1501
|
+
<h4 class="np-col-heading">${escape((m.verification && m.verification.title) || "More headings")}</h4>
|
|
1502
|
+
${moreHeadings}
|
|
1503
|
+
${tail ? `<div class="np-prose" style="margin-top:14px;">${tail}</div>` : ""}
|
|
1504
|
+
${flowText}
|
|
1505
|
+
</div>
|
|
1506
|
+
</section>
|
|
1507
|
+
|
|
1508
|
+
<footer class="np-footer">
|
|
1509
|
+
<span>${escape(m.footerTag || "")}</span>
|
|
1510
|
+
<span class="np-footer-stamp">PrivateBoard · ${escape(briefIdShort)}</span>
|
|
1511
|
+
</footer>
|
|
1512
|
+
</article>`;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
/** Layout 3 · 2x2 framed-quote spread (penny-press feel).
|
|
1516
|
+
* Each quadrant carries a milestone body + a callout/figure so
|
|
1517
|
+
* the four cells are roughly the same height — the prior 2/3 +
|
|
1518
|
+
* 1/3 asymmetric layout left the sidebar short relative to the
|
|
1519
|
+
* main column's two stacked bodies.
|
|
1520
|
+
*
|
|
1521
|
+
* · TOP 2-col · LEFT: ms0 lead-drop + bottom-line · RIGHT: ms1
|
|
1522
|
+
* + figure (chart inline)
|
|
1523
|
+
* · CENTER · framed full-width pull-quote drawn from talking[0]
|
|
1524
|
+
* · BOTTOM 2-col · LEFT: ms2 + date-stack · RIGHT: more-headings
|
|
1525
|
+
* + talking tail + flow
|
|
1526
|
+
*
|
|
1527
|
+
* Result: 4 quadrants bookending a centered framed quote ·
|
|
1528
|
+
* reads as a magazine spread on broadsheet paper, not a
|
|
1529
|
+
* half-empty asymmetric grid.
|
|
1530
|
+
*/
|
|
1531
|
+
function layoutVariant3(p) {
|
|
1532
|
+
const { m, ms0, ms1, ms2, flankLeft, flankRight, dispDate, briefIdShort,
|
|
1533
|
+
bottomLine, dateStack, figure, moreHeadings, flowText } = p;
|
|
1534
|
+
|
|
1535
|
+
const tp = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
|
|
1536
|
+
const pullText = tp[0] || "";
|
|
1537
|
+
const tail = tp.slice(1, 5).map((b) => `<p>${escape(b)}</p>`).join("");
|
|
1538
|
+
|
|
1539
|
+
// Quadrant fillers · right-top carries the figure when
|
|
1540
|
+
// present, otherwise the date-stack steps in so that cell
|
|
1541
|
+
// never reads as prose-only. Left-bottom carries the
|
|
1542
|
+
// date-stack ONLY when right-top got the figure (avoids
|
|
1543
|
+
// double-rendering); when figure is missing the date-stack
|
|
1544
|
+
// already sat in right-top, so left-bottom stays bare prose.
|
|
1545
|
+
const rightTopAside = figure || dateStack;
|
|
1546
|
+
const leftBottomAside = figure ? dateStack : "";
|
|
1547
|
+
|
|
1548
|
+
return `
|
|
1549
|
+
<article class="np-doc np-layout-3" data-np-paper>
|
|
1550
|
+
<div class="np-rule"></div>
|
|
1551
|
+
|
|
1552
|
+
<header class="np-masthead">
|
|
1553
|
+
<div class="np-mast-flank np-mast-flank-left">${flankLeft}</div>
|
|
1554
|
+
<h1 class="np-mast-name">PrivateBoard</h1>
|
|
1555
|
+
<div class="np-mast-flank np-mast-flank-right">${flankRight}</div>
|
|
1556
|
+
</header>
|
|
1557
|
+
|
|
1558
|
+
<div class="np-rule-double"></div>
|
|
1559
|
+
|
|
1560
|
+
<div class="np-meta-strip">
|
|
1561
|
+
<span class="np-meta-left">${escape(dispDate)}</span>
|
|
1562
|
+
<span class="np-meta-center">www.privateboard.app</span>
|
|
1563
|
+
<span class="np-meta-right">${escape(briefIdShort)}</span>
|
|
1564
|
+
</div>
|
|
1565
|
+
|
|
1566
|
+
<div class="np-rule-double"></div>
|
|
1567
|
+
|
|
1568
|
+
<section class="np-frontpage">
|
|
1569
|
+
<h2 class="np-frontpage-title">${escape(m.title || "")}</h2>
|
|
1570
|
+
${m.kicker ? `<div class="np-frontpage-deck">${escape(m.kicker)}</div>` : ""}
|
|
1571
|
+
</section>
|
|
1572
|
+
|
|
1573
|
+
<div class="np-rule-double"></div>
|
|
1574
|
+
|
|
1575
|
+
<!-- TOP quadrants · ms0+bottomLine | ms1+figure -->
|
|
1576
|
+
<section class="np-grid-2">
|
|
1577
|
+
<div class="np-col">
|
|
1578
|
+
${renderColumnHeading(ms0.period, ms0.title)}
|
|
1579
|
+
${renderProse(ms0.body, { lead: true })}
|
|
1580
|
+
${bottomLine}
|
|
1581
|
+
</div>
|
|
1582
|
+
<div class="np-col">
|
|
1583
|
+
${renderColumnHeading(ms1.period, ms1.title)}
|
|
1584
|
+
${renderProse(ms1.body)}
|
|
1585
|
+
${rightTopAside}
|
|
1586
|
+
</div>
|
|
1587
|
+
</section>
|
|
1588
|
+
|
|
1589
|
+
${pullText ? `
|
|
1590
|
+
<div class="np-rule-double"></div>
|
|
1591
|
+
<section class="np-pull-framed">"${escape(pullText)}"</section>
|
|
1592
|
+
` : ""}
|
|
1593
|
+
|
|
1594
|
+
<div class="np-rule-double"></div>
|
|
1595
|
+
|
|
1596
|
+
<!-- BOTTOM quadrants · ms2+dateStack | more-headings + tail + flow -->
|
|
1597
|
+
<section class="np-grid-2">
|
|
1598
|
+
<div class="np-col">
|
|
1599
|
+
${renderColumnHeading(ms2.period, ms2.title)}
|
|
1600
|
+
${renderProse(ms2.body)}
|
|
1601
|
+
${leftBottomAside}
|
|
1602
|
+
</div>
|
|
1603
|
+
<div class="np-col">
|
|
1604
|
+
<h4 class="np-col-heading">${escape((m.verification && m.verification.title) || "More headings")}</h4>
|
|
1605
|
+
${moreHeadings}
|
|
1606
|
+
${tail ? `<div class="np-prose" style="margin-top:14px;">${tail}</div>` : ""}
|
|
1607
|
+
${flowText}
|
|
1608
|
+
</div>
|
|
1609
|
+
</section>
|
|
1610
|
+
|
|
1611
|
+
<div class="np-rule"></div>
|
|
1612
|
+
|
|
1613
|
+
<footer class="np-footer">
|
|
1614
|
+
<span>${escape(m.footerTag || "")}</span>
|
|
1615
|
+
<span class="np-footer-stamp">PrivateBoard · ${escape(briefIdShort)}</span>
|
|
1616
|
+
</footer>
|
|
1617
|
+
</article>`;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/* ─── Export wiring · same PNG-as-PDF strategy as bento/magazine ── */
|
|
1621
|
+
let _h2iLoaded = null;
|
|
1622
|
+
async function ensureHtmlToImage() {
|
|
1623
|
+
if (window.htmlToImage) return;
|
|
1624
|
+
if (!_h2iLoaded) {
|
|
1625
|
+
_h2iLoaded = new Promise((res, rej) => {
|
|
1626
|
+
const s = document.createElement("script");
|
|
1627
|
+
s.src = "https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js";
|
|
1628
|
+
s.onload = res;
|
|
1629
|
+
s.onerror = rej;
|
|
1630
|
+
document.head.appendChild(s);
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
await _h2iLoaded;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
async function captureNpPng() {
|
|
1637
|
+
const el = document.querySelector("[data-np-paper]");
|
|
1638
|
+
if (!el) throw new Error("newspaper doc not found");
|
|
1639
|
+
await ensureHtmlToImage();
|
|
1640
|
+
if (document.fonts && document.fonts.ready) {
|
|
1641
|
+
try { await document.fonts.ready; } catch { /* best-effort */ }
|
|
1642
|
+
}
|
|
1643
|
+
const width = Math.max(el.scrollWidth, el.offsetWidth, el.clientWidth);
|
|
1644
|
+
const height = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight);
|
|
1645
|
+
return window.htmlToImage.toPng(el, {
|
|
1646
|
+
pixelRatio: 2,
|
|
1647
|
+
backgroundColor: "#EEE2C6",
|
|
1648
|
+
cacheBust: true,
|
|
1649
|
+
width,
|
|
1650
|
+
height,
|
|
1651
|
+
canvasWidth: width,
|
|
1652
|
+
canvasHeight: height,
|
|
1653
|
+
style: {
|
|
1654
|
+
margin: "0",
|
|
1655
|
+
width: `${width}px`,
|
|
1656
|
+
height: `${height}px`,
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
function slugTitle() {
|
|
1662
|
+
return (document.title || "newspaper").replace(/[^a-z0-9]+/gi, "-").slice(0, 60) || "newspaper";
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
async function exportPng() {
|
|
1666
|
+
try {
|
|
1667
|
+
const dataUrl = await captureNpPng();
|
|
1668
|
+
const a = document.createElement("a");
|
|
1669
|
+
a.download = `${slugTitle()}.png`;
|
|
1670
|
+
a.href = dataUrl;
|
|
1671
|
+
a.click();
|
|
1672
|
+
} catch (e) {
|
|
1673
|
+
console.warn("[newspaper] PNG export failed:", e);
|
|
1674
|
+
alert("PNG export failed · see browser console.");
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
async function exportPdf() {
|
|
1679
|
+
try {
|
|
1680
|
+
const dataUrl = await captureNpPng();
|
|
1681
|
+
const win = window.open("", "_blank", "width=1024,height=720");
|
|
1682
|
+
if (!win) {
|
|
1683
|
+
alert("PDF export needs to open a new window — please allow popups for this site.");
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const slug = slugTitle();
|
|
1687
|
+
win.document.open();
|
|
1688
|
+
win.document.write(`<!doctype html><html lang="en"><head>
|
|
1689
|
+
<meta charset="utf-8">
|
|
1690
|
+
<title>${slug}</title>
|
|
1691
|
+
<style>
|
|
1692
|
+
@page { size: auto; margin: 10mm; }
|
|
1693
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1694
|
+
html, body { background: #FFFFFF; }
|
|
1695
|
+
body {
|
|
1696
|
+
display: flex;
|
|
1697
|
+
align-items: flex-start;
|
|
1698
|
+
justify-content: center;
|
|
1699
|
+
padding: 20px;
|
|
1700
|
+
min-height: 100vh;
|
|
1701
|
+
}
|
|
1702
|
+
img {
|
|
1703
|
+
display: block;
|
|
1704
|
+
width: 100%;
|
|
1705
|
+
max-width: 880px;
|
|
1706
|
+
height: auto;
|
|
1707
|
+
box-shadow: 0 0 24px rgba(0, 0, 0, 0.08);
|
|
1708
|
+
}
|
|
1709
|
+
@media print {
|
|
1710
|
+
body { padding: 0; }
|
|
1711
|
+
img { box-shadow: none; max-width: none; width: 100%; }
|
|
1712
|
+
}
|
|
1713
|
+
.hint {
|
|
1714
|
+
position: fixed;
|
|
1715
|
+
top: 12px;
|
|
1716
|
+
left: 12px;
|
|
1717
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
1718
|
+
font-size: 11px;
|
|
1719
|
+
color: #8E8B83;
|
|
1720
|
+
background: rgba(255,255,255,0.9);
|
|
1721
|
+
padding: 6px 10px;
|
|
1722
|
+
border: 1px solid #E5E2DA;
|
|
1723
|
+
}
|
|
1724
|
+
@media print { .hint { display: none; } }
|
|
1725
|
+
</style>
|
|
1726
|
+
</head><body>
|
|
1727
|
+
<div class="hint">// press <kbd>⌘P</kbd> / <kbd>Ctrl+P</kbd> · save as PDF</div>
|
|
1728
|
+
<img alt="${slug}" src="${dataUrl}">
|
|
1729
|
+
<script>
|
|
1730
|
+
(function () {
|
|
1731
|
+
var img = document.querySelector("img");
|
|
1732
|
+
function go() { setTimeout(function () { window.print(); }, 200); }
|
|
1733
|
+
if (img && img.complete) { go(); }
|
|
1734
|
+
else if (img) { img.addEventListener("load", go, { once: true }); }
|
|
1735
|
+
})();
|
|
1736
|
+
<\/script>
|
|
1737
|
+
</body></html>`);
|
|
1738
|
+
win.document.close();
|
|
1739
|
+
} catch (e) {
|
|
1740
|
+
console.warn("[newspaper] PDF export failed:", e);
|
|
1741
|
+
alert("PDF export failed · see browser console.");
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
document.addEventListener("click", (e) => {
|
|
1746
|
+
if (e.target.closest("[data-np-png]")) {
|
|
1747
|
+
e.preventDefault();
|
|
1748
|
+
exportPng();
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (e.target.closest("[data-np-print]")) {
|
|
1752
|
+
e.preventDefault();
|
|
1753
|
+
exportPdf();
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
/* ─── Boot ──────────────────────────────────────────────────── */
|
|
1759
|
+
loadBrief().then((brief) => {
|
|
1760
|
+
if (brief) render(brief);
|
|
1761
|
+
}).catch((e) => {
|
|
1762
|
+
console.error("[newspaper] load failed:", e);
|
|
1763
|
+
showState("Error", "Couldn't load this brief",
|
|
1764
|
+
e instanceof Error ? e.message : String(e));
|
|
1765
|
+
});
|
|
1766
|
+
})();
|
|
1767
|
+
</script>
|
|
1768
|
+
|
|
1769
|
+
</body>
|
|
1770
|
+
</html>
|