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,1266 @@
|
|
|
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>Magazine · PrivateBoard</title>
|
|
7
|
+
<link rel="icon" href="/avatars/chair.svg" type="image/svg+xml">
|
|
8
|
+
<style>
|
|
9
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
10
|
+
Magazine · editorial single-page spread.
|
|
11
|
+
· Masthead (creator byline · large display serif title · sub-deck
|
|
12
|
+
· issue/date stamp top-right · hairline rule below)
|
|
13
|
+
· Hero spread · LEFT outline numeral feature ("5" + section title
|
|
14
|
+
+ body paragraph) · RIGHT 5-card pastel grid (numbered, mint /
|
|
15
|
+
pale-blue alternating, body + sub-label)
|
|
16
|
+
· 3-step setup band on white · serif heading + small-caps subtitle
|
|
17
|
+
· 3-column numbered steps with monoline glyphs
|
|
18
|
+
· Dark closer band · "Why this matters" pull-list with 4 bullets
|
|
19
|
+
· accent-coloured small glyph per row · brand stamp bottom-right
|
|
20
|
+
Reads BentoScaffold JSON from body_json. The data shape is the same
|
|
21
|
+
as bento mode · the renderer just maps the same fields into a
|
|
22
|
+
magazine layout instead of an infographic.
|
|
23
|
+
─────────────────────────────────────────────────────────────────── */
|
|
24
|
+
:root {
|
|
25
|
+
/* Surfaces · cream paper for the spread, dark navy for closer */
|
|
26
|
+
--paper: #FBF8EE;
|
|
27
|
+
--paper-warm: #F6F1DF;
|
|
28
|
+
--surface: #FFFFFF;
|
|
29
|
+
--closer-bg: #1B202C;
|
|
30
|
+
--closer-soft: #232938;
|
|
31
|
+
|
|
32
|
+
/* Ink · paper register */
|
|
33
|
+
--ink: #14110B;
|
|
34
|
+
--ink-soft: #3D362C;
|
|
35
|
+
--ink-mid: #6B6359;
|
|
36
|
+
--ink-faint: #8E8B83;
|
|
37
|
+
--ink-muted: #B8B0A0;
|
|
38
|
+
|
|
39
|
+
/* Ink · closer register (light type on dark) */
|
|
40
|
+
--ink-inv: #FFFFFF;
|
|
41
|
+
--ink-inv-soft: #C5C0B5;
|
|
42
|
+
--ink-inv-faint:#7E8290;
|
|
43
|
+
|
|
44
|
+
/* Rules */
|
|
45
|
+
--rule: #E2DDD0;
|
|
46
|
+
--rule-strong: #C5BEAE;
|
|
47
|
+
--rule-inv: #2E3441;
|
|
48
|
+
|
|
49
|
+
/* Pastel card tints · alternating across the 5 hero cards */
|
|
50
|
+
--tint-mint: #DCE9D1;
|
|
51
|
+
--tint-mint-deep: #C0D6B1;
|
|
52
|
+
--tint-mint-grad: linear-gradient(135deg, #E2EDDA 0%, #C4D5B7 100%);
|
|
53
|
+
--tint-blue: #DCE3EC;
|
|
54
|
+
--tint-blue-deep: #BCC8D9;
|
|
55
|
+
--tint-blue-grad: linear-gradient(135deg, #E0E6EE 0%, #C0CCDE 100%);
|
|
56
|
+
|
|
57
|
+
/* Accent · forest green for the closer-band glyphs */
|
|
58
|
+
--accent: #6FB572;
|
|
59
|
+
--accent-deep: #4F8E54;
|
|
60
|
+
--accent-soft: #B8DDB6;
|
|
61
|
+
|
|
62
|
+
/* Radii */
|
|
63
|
+
--r-xs: 6px;
|
|
64
|
+
--r-sm: 10px;
|
|
65
|
+
--r-md: 14px;
|
|
66
|
+
--r-lg: 22px;
|
|
67
|
+
--r-pill: 999px;
|
|
68
|
+
|
|
69
|
+
/* Shadows · soft warm, used sparingly */
|
|
70
|
+
--shadow-card: 0 1px 2px rgba(60, 45, 20, 0.04),
|
|
71
|
+
0 4px 12px rgba(60, 45, 20, 0.05);
|
|
72
|
+
--shadow-stamp: 0 2px 6px rgba(0, 0, 0, 0.18);
|
|
73
|
+
|
|
74
|
+
--serif: "Charter", "Source Serif Pro", "Iowan Old Style",
|
|
75
|
+
"Tiempos Headline", "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
|
+
background: #ECE6D5;
|
|
87
|
+
color: var(--ink);
|
|
88
|
+
font-family: var(--sans);
|
|
89
|
+
font-size: 14px;
|
|
90
|
+
line-height: 1.55;
|
|
91
|
+
-webkit-font-smoothing: antialiased;
|
|
92
|
+
text-rendering: optimizeLegibility;
|
|
93
|
+
min-height: 100vh;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* ─── Top chrome · brand crumb + actions ────────────────────────── */
|
|
97
|
+
.mag-top-bar {
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
gap: 14px;
|
|
102
|
+
padding: 18px 32px;
|
|
103
|
+
background: rgba(236, 230, 213, 0.85);
|
|
104
|
+
backdrop-filter: blur(10px);
|
|
105
|
+
-webkit-backdrop-filter: blur(10px);
|
|
106
|
+
border-bottom: 1px solid var(--rule);
|
|
107
|
+
flex-wrap: wrap;
|
|
108
|
+
position: sticky;
|
|
109
|
+
top: 0;
|
|
110
|
+
z-index: 10;
|
|
111
|
+
}
|
|
112
|
+
.mag-crumb {
|
|
113
|
+
display: inline-flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
gap: 12px;
|
|
116
|
+
font-family: var(--serif);
|
|
117
|
+
font-size: 16px;
|
|
118
|
+
font-weight: 500;
|
|
119
|
+
color: var(--ink);
|
|
120
|
+
text-decoration: none;
|
|
121
|
+
}
|
|
122
|
+
.mag-crumb::before {
|
|
123
|
+
content: "";
|
|
124
|
+
width: 10px;
|
|
125
|
+
height: 10px;
|
|
126
|
+
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-deep) 100%);
|
|
127
|
+
border-radius: 50%;
|
|
128
|
+
flex: 0 0 auto;
|
|
129
|
+
box-shadow: 0 0 0 3px rgba(111, 181, 114, 0.22);
|
|
130
|
+
}
|
|
131
|
+
.mag-crumb-accent {
|
|
132
|
+
color: var(--ink-faint);
|
|
133
|
+
font-style: italic;
|
|
134
|
+
font-weight: 400;
|
|
135
|
+
}
|
|
136
|
+
.mag-actions { display: flex; gap: 8px; }
|
|
137
|
+
.mag-btn {
|
|
138
|
+
font-family: var(--mono);
|
|
139
|
+
font-size: 11px;
|
|
140
|
+
letter-spacing: 0.04em;
|
|
141
|
+
padding: 8px 14px;
|
|
142
|
+
background: var(--surface);
|
|
143
|
+
border: 1px solid var(--rule);
|
|
144
|
+
border-radius: var(--r-pill);
|
|
145
|
+
color: var(--ink-mid);
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
text-decoration: none;
|
|
148
|
+
box-shadow: 0 1px 2px rgba(60, 45, 20, 0.05);
|
|
149
|
+
transition: color 0.15s, border-color 0.15s, box-shadow 0.15s, transform 0.15s;
|
|
150
|
+
}
|
|
151
|
+
.mag-btn:hover {
|
|
152
|
+
color: var(--accent-deep);
|
|
153
|
+
border-color: var(--accent);
|
|
154
|
+
transform: translateY(-1px);
|
|
155
|
+
box-shadow: 0 1px 2px rgba(60, 45, 20, 0.05),
|
|
156
|
+
0 0 0 3px rgba(111, 181, 114, 0.22);
|
|
157
|
+
}
|
|
158
|
+
.mag-btn:active { transform: translateY(0); }
|
|
159
|
+
.mag-btn .glyph { color: var(--ink-faint); margin-right: 4px; transition: color 0.15s; }
|
|
160
|
+
.mag-btn:hover .glyph { color: var(--accent-deep); }
|
|
161
|
+
|
|
162
|
+
/* ─── Doc · the magazine sheet ─────────────────────────────────── */
|
|
163
|
+
.mag-doc {
|
|
164
|
+
max-width: 880px;
|
|
165
|
+
margin: 28px auto;
|
|
166
|
+
background: var(--paper);
|
|
167
|
+
box-shadow: var(--shadow-card);
|
|
168
|
+
overflow: hidden;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/* ─── Masthead band · creator + display title + deck + issue ──── */
|
|
172
|
+
.mag-masthead {
|
|
173
|
+
padding: 32px 44px 24px;
|
|
174
|
+
background: var(--paper);
|
|
175
|
+
position: relative;
|
|
176
|
+
}
|
|
177
|
+
.mag-masthead-top {
|
|
178
|
+
display: flex;
|
|
179
|
+
align-items: baseline;
|
|
180
|
+
justify-content: space-between;
|
|
181
|
+
gap: 24px;
|
|
182
|
+
margin-bottom: 14px;
|
|
183
|
+
}
|
|
184
|
+
.mag-byline {
|
|
185
|
+
font-family: var(--mono);
|
|
186
|
+
font-size: 11px;
|
|
187
|
+
letter-spacing: 0.18em;
|
|
188
|
+
text-transform: uppercase;
|
|
189
|
+
color: var(--ink-mid);
|
|
190
|
+
font-weight: 600;
|
|
191
|
+
}
|
|
192
|
+
.mag-issue {
|
|
193
|
+
font-family: var(--mono);
|
|
194
|
+
font-size: 10.5px;
|
|
195
|
+
letter-spacing: 0.16em;
|
|
196
|
+
text-transform: uppercase;
|
|
197
|
+
color: var(--ink-faint);
|
|
198
|
+
text-align: right;
|
|
199
|
+
line-height: 1.5;
|
|
200
|
+
white-space: pre-line;
|
|
201
|
+
}
|
|
202
|
+
.mag-title {
|
|
203
|
+
font-family: var(--serif);
|
|
204
|
+
font-size: 48px;
|
|
205
|
+
font-weight: 700;
|
|
206
|
+
line-height: 1.05;
|
|
207
|
+
letter-spacing: -0.022em;
|
|
208
|
+
color: var(--ink);
|
|
209
|
+
margin: 0 0 12px;
|
|
210
|
+
/* Magazine cover lines need to feel printed · slight optical
|
|
211
|
+
tightening keeps the gravity. */
|
|
212
|
+
}
|
|
213
|
+
.mag-deck {
|
|
214
|
+
font-family: var(--sans);
|
|
215
|
+
font-size: 16px;
|
|
216
|
+
line-height: 1.45;
|
|
217
|
+
color: var(--ink-soft);
|
|
218
|
+
font-weight: 400;
|
|
219
|
+
max-width: 720px;
|
|
220
|
+
letter-spacing: -0.005em;
|
|
221
|
+
}
|
|
222
|
+
.mag-masthead-rule {
|
|
223
|
+
height: 1px;
|
|
224
|
+
background: var(--rule-strong);
|
|
225
|
+
margin: 22px 44px 0;
|
|
226
|
+
}
|
|
227
|
+
.mag-masthead + .mag-masthead-rule { /* selector parity in case of placement */ }
|
|
228
|
+
|
|
229
|
+
/* ─── Hero spread · left feature + right card grid ────────────── */
|
|
230
|
+
.mag-hero {
|
|
231
|
+
display: grid;
|
|
232
|
+
grid-template-columns: minmax(220px, 280px) 1fr;
|
|
233
|
+
gap: 36px;
|
|
234
|
+
padding: 36px 44px 40px;
|
|
235
|
+
background: var(--paper);
|
|
236
|
+
}
|
|
237
|
+
@media (max-width: 760px) {
|
|
238
|
+
.mag-hero { grid-template-columns: 1fr; gap: 24px; padding: 28px 28px 32px; }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* Left feature · outline numeral + section title + body */
|
|
242
|
+
.mag-feature {
|
|
243
|
+
display: flex;
|
|
244
|
+
flex-direction: column;
|
|
245
|
+
align-items: flex-start;
|
|
246
|
+
gap: 8px;
|
|
247
|
+
padding-top: 6px;
|
|
248
|
+
}
|
|
249
|
+
.mag-feature-kicker {
|
|
250
|
+
font-family: var(--serif);
|
|
251
|
+
font-size: 14px;
|
|
252
|
+
color: var(--ink-soft);
|
|
253
|
+
margin-bottom: 4px;
|
|
254
|
+
}
|
|
255
|
+
.mag-feature-numeral {
|
|
256
|
+
font-family: var(--serif);
|
|
257
|
+
font-size: 168px;
|
|
258
|
+
line-height: 0.85;
|
|
259
|
+
font-weight: 700;
|
|
260
|
+
color: transparent;
|
|
261
|
+
-webkit-text-stroke: 1.4px var(--ink);
|
|
262
|
+
text-stroke: 1.4px var(--ink);
|
|
263
|
+
letter-spacing: -0.04em;
|
|
264
|
+
margin: 4px 0 8px;
|
|
265
|
+
user-select: none;
|
|
266
|
+
}
|
|
267
|
+
.mag-feature-title {
|
|
268
|
+
font-family: var(--serif);
|
|
269
|
+
font-size: 24px;
|
|
270
|
+
font-weight: 700;
|
|
271
|
+
line-height: 1.15;
|
|
272
|
+
color: var(--ink);
|
|
273
|
+
letter-spacing: -0.012em;
|
|
274
|
+
}
|
|
275
|
+
.mag-feature-sub {
|
|
276
|
+
font-family: var(--mono);
|
|
277
|
+
font-size: 11px;
|
|
278
|
+
letter-spacing: 0.14em;
|
|
279
|
+
text-transform: uppercase;
|
|
280
|
+
color: var(--ink-mid);
|
|
281
|
+
margin-bottom: 12px;
|
|
282
|
+
font-weight: 600;
|
|
283
|
+
}
|
|
284
|
+
.mag-feature-body {
|
|
285
|
+
font-family: var(--sans);
|
|
286
|
+
font-size: 13px;
|
|
287
|
+
line-height: 1.65;
|
|
288
|
+
color: var(--ink-soft);
|
|
289
|
+
max-width: 240px;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Right card grid · 5 numbered pastel cards. The 5th card spans
|
|
293
|
+
two columns to fill the bottom row cleanly. Even-positioned
|
|
294
|
+
cards get the blue tint, odd-positioned the mint tint — matches
|
|
295
|
+
the diagonal alternation in the reference. */
|
|
296
|
+
.mag-cards {
|
|
297
|
+
display: grid;
|
|
298
|
+
grid-template-columns: 1fr 1fr;
|
|
299
|
+
gap: 14px;
|
|
300
|
+
align-content: start;
|
|
301
|
+
}
|
|
302
|
+
@media (max-width: 760px) {
|
|
303
|
+
.mag-cards { grid-template-columns: 1fr; }
|
|
304
|
+
}
|
|
305
|
+
.mag-card {
|
|
306
|
+
position: relative;
|
|
307
|
+
padding: 16px 18px 18px;
|
|
308
|
+
border-radius: var(--r-md);
|
|
309
|
+
min-height: 140px;
|
|
310
|
+
display: flex;
|
|
311
|
+
flex-direction: column;
|
|
312
|
+
gap: 6px;
|
|
313
|
+
overflow: hidden;
|
|
314
|
+
}
|
|
315
|
+
.mag-card.tint-mint { background: var(--tint-mint-grad); }
|
|
316
|
+
.mag-card.tint-blue { background: var(--tint-blue-grad); }
|
|
317
|
+
.mag-card.tint-mint .mag-card-numeral { color: var(--tint-mint-deep); }
|
|
318
|
+
.mag-card.tint-blue .mag-card-numeral { color: var(--tint-blue-deep); }
|
|
319
|
+
.mag-card-numeral {
|
|
320
|
+
font-family: var(--serif);
|
|
321
|
+
font-size: 26px;
|
|
322
|
+
font-weight: 700;
|
|
323
|
+
font-style: italic;
|
|
324
|
+
line-height: 1;
|
|
325
|
+
margin-bottom: 2px;
|
|
326
|
+
letter-spacing: -0.02em;
|
|
327
|
+
}
|
|
328
|
+
.mag-card-title {
|
|
329
|
+
font-family: var(--serif);
|
|
330
|
+
font-size: 17px;
|
|
331
|
+
font-weight: 700;
|
|
332
|
+
line-height: 1.25;
|
|
333
|
+
color: var(--ink);
|
|
334
|
+
letter-spacing: -0.01em;
|
|
335
|
+
}
|
|
336
|
+
.mag-card-sub {
|
|
337
|
+
font-family: var(--sans);
|
|
338
|
+
font-size: 12px;
|
|
339
|
+
line-height: 1.35;
|
|
340
|
+
color: var(--ink-mid);
|
|
341
|
+
font-weight: 500;
|
|
342
|
+
letter-spacing: -0.005em;
|
|
343
|
+
margin-bottom: 4px;
|
|
344
|
+
}
|
|
345
|
+
.mag-card-body {
|
|
346
|
+
font-family: var(--sans);
|
|
347
|
+
font-size: 12.5px;
|
|
348
|
+
line-height: 1.55;
|
|
349
|
+
color: var(--ink-soft);
|
|
350
|
+
max-width: 240px;
|
|
351
|
+
}
|
|
352
|
+
.mag-card-glyph {
|
|
353
|
+
position: absolute;
|
|
354
|
+
bottom: 12px;
|
|
355
|
+
right: 14px;
|
|
356
|
+
width: 28px;
|
|
357
|
+
height: 28px;
|
|
358
|
+
opacity: 0.55;
|
|
359
|
+
color: var(--ink-soft);
|
|
360
|
+
pointer-events: none;
|
|
361
|
+
}
|
|
362
|
+
.mag-card.tint-mint .mag-card-glyph { color: #5C8A5F; }
|
|
363
|
+
.mag-card.tint-blue .mag-card-glyph { color: #5A6E92; }
|
|
364
|
+
/* The 5th card spans both columns to balance the bottom row. */
|
|
365
|
+
.mag-card.span-2 { grid-column: 1 / -1; }
|
|
366
|
+
|
|
367
|
+
/* ─── Setup band · 3-column step recipe on white ──────────────── */
|
|
368
|
+
.mag-setup {
|
|
369
|
+
background: var(--surface);
|
|
370
|
+
padding: 36px 44px 40px;
|
|
371
|
+
border-top: 1px solid var(--rule);
|
|
372
|
+
border-bottom: 1px solid var(--rule);
|
|
373
|
+
}
|
|
374
|
+
.mag-setup-head {
|
|
375
|
+
text-align: center;
|
|
376
|
+
margin-bottom: 28px;
|
|
377
|
+
}
|
|
378
|
+
.mag-setup-title {
|
|
379
|
+
font-family: var(--serif);
|
|
380
|
+
font-size: 26px;
|
|
381
|
+
font-weight: 700;
|
|
382
|
+
line-height: 1.2;
|
|
383
|
+
color: var(--ink);
|
|
384
|
+
letter-spacing: -0.014em;
|
|
385
|
+
margin-bottom: 6px;
|
|
386
|
+
}
|
|
387
|
+
.mag-setup-sub {
|
|
388
|
+
font-family: var(--mono);
|
|
389
|
+
font-size: 11px;
|
|
390
|
+
letter-spacing: 0.18em;
|
|
391
|
+
text-transform: uppercase;
|
|
392
|
+
color: var(--ink-mid);
|
|
393
|
+
font-weight: 600;
|
|
394
|
+
}
|
|
395
|
+
.mag-setup-grid {
|
|
396
|
+
display: grid;
|
|
397
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
398
|
+
gap: 28px;
|
|
399
|
+
}
|
|
400
|
+
@media (max-width: 720px) {
|
|
401
|
+
.mag-setup-grid { grid-template-columns: 1fr; gap: 20px; }
|
|
402
|
+
}
|
|
403
|
+
.mag-step {
|
|
404
|
+
display: flex;
|
|
405
|
+
flex-direction: column;
|
|
406
|
+
gap: 8px;
|
|
407
|
+
}
|
|
408
|
+
.mag-step-glyph {
|
|
409
|
+
width: 28px;
|
|
410
|
+
height: 28px;
|
|
411
|
+
color: var(--accent-deep);
|
|
412
|
+
margin-bottom: 6px;
|
|
413
|
+
}
|
|
414
|
+
.mag-step-title {
|
|
415
|
+
font-family: var(--sans);
|
|
416
|
+
font-size: 14px;
|
|
417
|
+
font-weight: 700;
|
|
418
|
+
color: var(--ink);
|
|
419
|
+
letter-spacing: -0.005em;
|
|
420
|
+
}
|
|
421
|
+
.mag-step-body {
|
|
422
|
+
font-family: var(--sans);
|
|
423
|
+
font-size: 12.5px;
|
|
424
|
+
line-height: 1.6;
|
|
425
|
+
color: var(--ink-soft);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* ─── Dark closer band · why this matters ─────────────────────── */
|
|
429
|
+
.mag-closer {
|
|
430
|
+
background: var(--closer-bg);
|
|
431
|
+
color: var(--ink-inv);
|
|
432
|
+
padding: 40px 44px 48px;
|
|
433
|
+
position: relative;
|
|
434
|
+
}
|
|
435
|
+
.mag-closer-head {
|
|
436
|
+
text-align: center;
|
|
437
|
+
margin-bottom: 24px;
|
|
438
|
+
}
|
|
439
|
+
.mag-closer-title {
|
|
440
|
+
font-family: var(--serif);
|
|
441
|
+
font-size: 30px;
|
|
442
|
+
font-weight: 700;
|
|
443
|
+
color: var(--ink-inv);
|
|
444
|
+
letter-spacing: -0.014em;
|
|
445
|
+
margin-bottom: 6px;
|
|
446
|
+
line-height: 1.2;
|
|
447
|
+
}
|
|
448
|
+
.mag-closer-sub {
|
|
449
|
+
font-family: var(--mono);
|
|
450
|
+
font-size: 11px;
|
|
451
|
+
letter-spacing: 0.18em;
|
|
452
|
+
text-transform: uppercase;
|
|
453
|
+
color: var(--accent);
|
|
454
|
+
font-weight: 600;
|
|
455
|
+
}
|
|
456
|
+
.mag-closer-list {
|
|
457
|
+
list-style: none;
|
|
458
|
+
margin: 0 auto;
|
|
459
|
+
padding: 0;
|
|
460
|
+
max-width: 600px;
|
|
461
|
+
}
|
|
462
|
+
.mag-closer-list li {
|
|
463
|
+
display: grid;
|
|
464
|
+
grid-template-columns: 22px 1fr;
|
|
465
|
+
gap: 14px;
|
|
466
|
+
align-items: baseline;
|
|
467
|
+
padding: 12px 0;
|
|
468
|
+
font-family: var(--sans);
|
|
469
|
+
font-size: 13.5px;
|
|
470
|
+
line-height: 1.55;
|
|
471
|
+
color: var(--ink-inv-soft);
|
|
472
|
+
border-bottom: 1px solid var(--rule-inv);
|
|
473
|
+
}
|
|
474
|
+
.mag-closer-list li:last-child { border-bottom: 0; }
|
|
475
|
+
.mag-closer-list li .mag-closer-glyph {
|
|
476
|
+
width: 20px;
|
|
477
|
+
height: 20px;
|
|
478
|
+
color: var(--accent);
|
|
479
|
+
align-self: center;
|
|
480
|
+
}
|
|
481
|
+
.mag-closer-list li .mag-closer-glyph svg {
|
|
482
|
+
display: block;
|
|
483
|
+
width: 100%;
|
|
484
|
+
height: 100%;
|
|
485
|
+
}
|
|
486
|
+
.mag-closer-list li b {
|
|
487
|
+
color: var(--ink-inv);
|
|
488
|
+
font-weight: 600;
|
|
489
|
+
}
|
|
490
|
+
.mag-closer-list li .mag-closer-sep {
|
|
491
|
+
color: var(--accent);
|
|
492
|
+
margin: 0 6px;
|
|
493
|
+
font-weight: 500;
|
|
494
|
+
}
|
|
495
|
+
.mag-closer-stamp {
|
|
496
|
+
position: absolute;
|
|
497
|
+
bottom: 16px;
|
|
498
|
+
right: 28px;
|
|
499
|
+
display: inline-flex;
|
|
500
|
+
align-items: center;
|
|
501
|
+
gap: 10px;
|
|
502
|
+
font-family: var(--mono);
|
|
503
|
+
font-size: 10.5px;
|
|
504
|
+
color: var(--ink-inv-faint);
|
|
505
|
+
letter-spacing: 0.06em;
|
|
506
|
+
}
|
|
507
|
+
.mag-closer-stamp-pill {
|
|
508
|
+
display: inline-flex;
|
|
509
|
+
align-items: center;
|
|
510
|
+
padding: 4px 10px;
|
|
511
|
+
border-radius: var(--r-pill);
|
|
512
|
+
background: var(--ink-inv);
|
|
513
|
+
color: var(--closer-bg);
|
|
514
|
+
font-family: var(--sans);
|
|
515
|
+
font-size: 11px;
|
|
516
|
+
font-weight: 600;
|
|
517
|
+
letter-spacing: -0.005em;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* ─── States · loading / error / empty ─────────────────────────── */
|
|
521
|
+
.mag-state {
|
|
522
|
+
max-width: 560px;
|
|
523
|
+
margin: 80px auto;
|
|
524
|
+
padding: 48px 36px;
|
|
525
|
+
text-align: center;
|
|
526
|
+
background: var(--surface);
|
|
527
|
+
border-radius: var(--r-lg);
|
|
528
|
+
box-shadow: var(--shadow-card);
|
|
529
|
+
}
|
|
530
|
+
.mag-state-mark {
|
|
531
|
+
font-family: var(--mono);
|
|
532
|
+
font-size: 10px;
|
|
533
|
+
letter-spacing: 0.18em;
|
|
534
|
+
text-transform: uppercase;
|
|
535
|
+
color: var(--accent-deep);
|
|
536
|
+
background: rgba(111, 181, 114, 0.14);
|
|
537
|
+
border-radius: var(--r-pill);
|
|
538
|
+
padding: 5px 14px;
|
|
539
|
+
display: inline-block;
|
|
540
|
+
margin-bottom: 16px;
|
|
541
|
+
font-weight: 600;
|
|
542
|
+
}
|
|
543
|
+
.mag-state-title {
|
|
544
|
+
font-family: var(--serif);
|
|
545
|
+
font-size: 24px;
|
|
546
|
+
font-weight: 600;
|
|
547
|
+
color: var(--ink);
|
|
548
|
+
margin-bottom: 10px;
|
|
549
|
+
line-height: 1.3;
|
|
550
|
+
letter-spacing: -0.012em;
|
|
551
|
+
}
|
|
552
|
+
.mag-state-body {
|
|
553
|
+
font-family: var(--sans);
|
|
554
|
+
font-size: 13.5px;
|
|
555
|
+
color: var(--ink-soft);
|
|
556
|
+
line-height: 1.6;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
560
|
+
LAYOUT 2 · Editorial mosaic · cover pull-quote + asymmetric
|
|
561
|
+
cards grid (1 big featured tile + 4 smaller). Used when the
|
|
562
|
+
brief id hashes to variant 2.
|
|
563
|
+
═════════════════════════════════════════════════════════════════ */
|
|
564
|
+
|
|
565
|
+
/* Cover pull-quote band · sits below the masthead. The
|
|
566
|
+
conclusion (or kicker fallback) becomes a centered editorial
|
|
567
|
+
quote · mono kicker above, italic serif quote, attribution
|
|
568
|
+
line below. Replaces the layout-1 numeral feature. */
|
|
569
|
+
.mag-cover-quote {
|
|
570
|
+
padding: 28px 56px 32px;
|
|
571
|
+
text-align: center;
|
|
572
|
+
border-bottom: 1px solid var(--rule);
|
|
573
|
+
}
|
|
574
|
+
.mag-cover-quote-mark {
|
|
575
|
+
font-family: var(--mono);
|
|
576
|
+
font-size: 11px;
|
|
577
|
+
letter-spacing: 0.18em;
|
|
578
|
+
text-transform: uppercase;
|
|
579
|
+
color: var(--accent-deep);
|
|
580
|
+
font-weight: 700;
|
|
581
|
+
margin-bottom: 14px;
|
|
582
|
+
}
|
|
583
|
+
.mag-cover-quote-text {
|
|
584
|
+
font-family: var(--serif);
|
|
585
|
+
font-style: italic;
|
|
586
|
+
font-size: 30px;
|
|
587
|
+
line-height: 1.22;
|
|
588
|
+
letter-spacing: -0.012em;
|
|
589
|
+
color: var(--ink);
|
|
590
|
+
max-width: 820px;
|
|
591
|
+
margin: 0 auto;
|
|
592
|
+
}
|
|
593
|
+
.mag-cover-quote-text::before { content: "“"; color: var(--accent-deep); margin-right: 2px; }
|
|
594
|
+
.mag-cover-quote-text::after { content: "”"; color: var(--accent-deep); margin-left: 2px; }
|
|
595
|
+
.mag-cover-quote-attr {
|
|
596
|
+
font-family: var(--mono);
|
|
597
|
+
font-size: 10px;
|
|
598
|
+
letter-spacing: 0.16em;
|
|
599
|
+
text-transform: uppercase;
|
|
600
|
+
color: var(--ink-faint);
|
|
601
|
+
margin-top: 16px;
|
|
602
|
+
}
|
|
603
|
+
.mag-cover-quote-attr::before { content: "— "; color: var(--accent-deep); }
|
|
604
|
+
@media (max-width: 720px) {
|
|
605
|
+
.mag-cover-quote { padding: 22px 22px 26px; }
|
|
606
|
+
.mag-cover-quote-text { font-size: 22px; }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/* Mosaic cards grid · 1 featured big tile (cols 1-2 spanning 2
|
|
610
|
+
rows) + 4 smaller tiles in cols 3 (rows 1+2) and cols 1+2 row
|
|
611
|
+
3. Asymmetric magazine feel. */
|
|
612
|
+
.mag-cards-mosaic {
|
|
613
|
+
display: grid;
|
|
614
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
615
|
+
grid-template-rows: 1fr 1fr;
|
|
616
|
+
gap: 14px;
|
|
617
|
+
padding: 32px 44px 36px;
|
|
618
|
+
min-height: 520px;
|
|
619
|
+
}
|
|
620
|
+
.mag-cards-mosaic > .mag-card-feature {
|
|
621
|
+
grid-column: 1 / span 2;
|
|
622
|
+
grid-row: 1 / span 2;
|
|
623
|
+
padding: 28px 30px 30px;
|
|
624
|
+
}
|
|
625
|
+
.mag-cards-mosaic > .mag-card-feature .mag-card-numeral {
|
|
626
|
+
font-size: 60px;
|
|
627
|
+
margin-bottom: 8px;
|
|
628
|
+
}
|
|
629
|
+
.mag-cards-mosaic > .mag-card-feature .mag-card-title {
|
|
630
|
+
font-size: 26px;
|
|
631
|
+
line-height: 1.2;
|
|
632
|
+
margin-bottom: 6px;
|
|
633
|
+
}
|
|
634
|
+
.mag-cards-mosaic > .mag-card-feature .mag-card-body {
|
|
635
|
+
font-size: 14px;
|
|
636
|
+
line-height: 1.5;
|
|
637
|
+
}
|
|
638
|
+
.mag-cards-mosaic > .mag-card-feature .mag-card-glyph {
|
|
639
|
+
width: 44px;
|
|
640
|
+
height: 44px;
|
|
641
|
+
bottom: 18px;
|
|
642
|
+
right: 20px;
|
|
643
|
+
opacity: 0.45;
|
|
644
|
+
}
|
|
645
|
+
.mag-cards-mosaic > .mag-card:not(.mag-card-feature) {
|
|
646
|
+
min-height: 0;
|
|
647
|
+
}
|
|
648
|
+
@media (max-width: 720px) {
|
|
649
|
+
.mag-cards-mosaic {
|
|
650
|
+
grid-template-columns: 1fr;
|
|
651
|
+
grid-template-rows: auto;
|
|
652
|
+
min-height: 0;
|
|
653
|
+
padding: 24px 22px 28px;
|
|
654
|
+
}
|
|
655
|
+
.mag-cards-mosaic > .mag-card-feature {
|
|
656
|
+
grid-column: 1;
|
|
657
|
+
grid-row: auto;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/* 2-col setup band · used by layout 2 in place of the 3-col grid. */
|
|
662
|
+
.mag-setup-grid-2 {
|
|
663
|
+
grid-template-columns: 1fr 1fr;
|
|
664
|
+
}
|
|
665
|
+
@media (max-width: 720px) {
|
|
666
|
+
.mag-setup-grid-2 { grid-template-columns: 1fr; gap: 20px; }
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
670
|
+
LAYOUT 3 · Vertical zine · stacked numbered entries with HUGE
|
|
671
|
+
numerals on the left. No grid · single column flow. Used when
|
|
672
|
+
the brief id hashes to variant 3.
|
|
673
|
+
═════════════════════════════════════════════════════════════════ */
|
|
674
|
+
|
|
675
|
+
/* Edition stamp · sits in the masthead corner instead of the
|
|
676
|
+
issue date. Reads as a serial number stamp. */
|
|
677
|
+
.mag-edition-stamp {
|
|
678
|
+
font-family: var(--mono);
|
|
679
|
+
font-size: 10px;
|
|
680
|
+
letter-spacing: 0.18em;
|
|
681
|
+
text-transform: uppercase;
|
|
682
|
+
color: var(--ink-faint);
|
|
683
|
+
border: 1px solid var(--ink);
|
|
684
|
+
padding: 5px 11px;
|
|
685
|
+
display: inline-block;
|
|
686
|
+
line-height: 1;
|
|
687
|
+
font-weight: 700;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* Vertical zine list · stacked entries with big numeral + content
|
|
691
|
+
row. Replaces the hero+cards split of layout 1. */
|
|
692
|
+
.mag-zine-list {
|
|
693
|
+
display: flex;
|
|
694
|
+
flex-direction: column;
|
|
695
|
+
padding: 24px 44px 36px;
|
|
696
|
+
}
|
|
697
|
+
.mag-zine-entry {
|
|
698
|
+
display: grid;
|
|
699
|
+
grid-template-columns: 110px 1fr;
|
|
700
|
+
gap: 24px;
|
|
701
|
+
padding: 22px 0;
|
|
702
|
+
border-bottom: 1px solid var(--rule);
|
|
703
|
+
align-items: start;
|
|
704
|
+
}
|
|
705
|
+
.mag-zine-entry:first-child { padding-top: 8px; }
|
|
706
|
+
.mag-zine-entry:last-child { border-bottom: 0; padding-bottom: 8px; }
|
|
707
|
+
.mag-zine-numeral {
|
|
708
|
+
font-family: var(--serif);
|
|
709
|
+
font-size: 80px;
|
|
710
|
+
font-weight: 700;
|
|
711
|
+
line-height: 0.9;
|
|
712
|
+
letter-spacing: -0.04em;
|
|
713
|
+
color: var(--accent-deep);
|
|
714
|
+
text-align: right;
|
|
715
|
+
}
|
|
716
|
+
.mag-zine-content {
|
|
717
|
+
display: flex;
|
|
718
|
+
flex-direction: column;
|
|
719
|
+
gap: 6px;
|
|
720
|
+
}
|
|
721
|
+
.mag-zine-title {
|
|
722
|
+
font-family: var(--serif);
|
|
723
|
+
font-size: 22px;
|
|
724
|
+
font-weight: 700;
|
|
725
|
+
line-height: 1.2;
|
|
726
|
+
letter-spacing: -0.012em;
|
|
727
|
+
color: var(--ink);
|
|
728
|
+
}
|
|
729
|
+
.mag-zine-sub {
|
|
730
|
+
font-family: var(--sans);
|
|
731
|
+
font-size: 12px;
|
|
732
|
+
letter-spacing: 0.04em;
|
|
733
|
+
text-transform: uppercase;
|
|
734
|
+
color: var(--ink-mid);
|
|
735
|
+
font-weight: 600;
|
|
736
|
+
}
|
|
737
|
+
.mag-zine-body {
|
|
738
|
+
font-family: var(--sans);
|
|
739
|
+
font-size: 13.5px;
|
|
740
|
+
line-height: 1.6;
|
|
741
|
+
color: var(--ink-soft);
|
|
742
|
+
margin-top: 4px;
|
|
743
|
+
}
|
|
744
|
+
/* Alternating accent · even-row numerals pick up a darker tone */
|
|
745
|
+
.mag-zine-entry:nth-child(even) .mag-zine-numeral {
|
|
746
|
+
color: var(--ink);
|
|
747
|
+
-webkit-text-stroke: 1px transparent;
|
|
748
|
+
}
|
|
749
|
+
@media (max-width: 720px) {
|
|
750
|
+
.mag-zine-list { padding: 16px 22px 24px; }
|
|
751
|
+
.mag-zine-entry { grid-template-columns: 64px 1fr; gap: 14px; }
|
|
752
|
+
.mag-zine-numeral { font-size: 52px; }
|
|
753
|
+
.mag-zine-title { font-size: 18px; }
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/* Compact zine masthead · title left-aligned at slightly smaller
|
|
757
|
+
scale, edition stamp on the right. Used by layout 3. */
|
|
758
|
+
.mag-zine-masthead {
|
|
759
|
+
text-align: left;
|
|
760
|
+
}
|
|
761
|
+
.mag-zine-masthead .mag-title {
|
|
762
|
+
font-size: 40px;
|
|
763
|
+
}
|
|
764
|
+
@media (max-width: 720px) {
|
|
765
|
+
.mag-zine-masthead .mag-title { font-size: 30px; }
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/* ─── Print · drop top chrome, fit one page ───────────────────── */
|
|
769
|
+
@media print {
|
|
770
|
+
.mag-top-bar { display: none; }
|
|
771
|
+
body, html { background: white; }
|
|
772
|
+
.mag-doc {
|
|
773
|
+
max-width: none;
|
|
774
|
+
margin: 0;
|
|
775
|
+
box-shadow: none;
|
|
776
|
+
}
|
|
777
|
+
.mag-card,
|
|
778
|
+
.mag-step,
|
|
779
|
+
.mag-closer-list li,
|
|
780
|
+
.mag-zine-entry,
|
|
781
|
+
.mag-cards-mosaic > .mag-card-feature {
|
|
782
|
+
break-inside: avoid;
|
|
783
|
+
page-break-inside: avoid;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
</style>
|
|
787
|
+
</head>
|
|
788
|
+
<body>
|
|
789
|
+
|
|
790
|
+
<header class="mag-top-bar" data-mag-chrome>
|
|
791
|
+
<a href="/" class="mag-crumb">PrivateBoard <span class="mag-crumb-accent">· magazine</span></a>
|
|
792
|
+
<div class="mag-actions">
|
|
793
|
+
<button type="button" class="mag-btn" data-mag-png>
|
|
794
|
+
<span class="glyph">↓</span>PNG
|
|
795
|
+
</button>
|
|
796
|
+
<button type="button" class="mag-btn" data-mag-print>
|
|
797
|
+
<span class="glyph">↓</span>PDF
|
|
798
|
+
</button>
|
|
799
|
+
</div>
|
|
800
|
+
</header>
|
|
801
|
+
|
|
802
|
+
<main data-mag-root>
|
|
803
|
+
<div class="mag-state">
|
|
804
|
+
<div class="mag-state-mark">Loading</div>
|
|
805
|
+
<div class="mag-state-title">Loading magazine…</div>
|
|
806
|
+
<div class="mag-state-body">Fetching the editorial spread for this brief.</div>
|
|
807
|
+
</div>
|
|
808
|
+
</main>
|
|
809
|
+
|
|
810
|
+
<script>
|
|
811
|
+
/* ──────────────────────────────────────────────────────────────────
|
|
812
|
+
Magazine renderer · reads the same BentoScaffold JSON that bento
|
|
813
|
+
mode produces, but maps the slots into a magazine-spread layout.
|
|
814
|
+
|
|
815
|
+
Slot mapping (BentoScaffold field → magazine slot):
|
|
816
|
+
title → masthead headline
|
|
817
|
+
kicker → masthead deck
|
|
818
|
+
source → masthead byline (top-left)
|
|
819
|
+
footerTag → masthead issue/date (top-right)
|
|
820
|
+
talkingPoints → 5 numbered pastel cards (split each bullet on
|
|
821
|
+
" · " into title + body)
|
|
822
|
+
milestones → 3-step setup band
|
|
823
|
+
verification → dark-closer "why this matters" pull-list
|
|
824
|
+
conclusion → setup-band sub-heading reinforcement
|
|
825
|
+
(decorative; the cover headline carries it too)
|
|
826
|
+
|
|
827
|
+
The 5 hero cards are derived from talkingPoints.bullets. Each
|
|
828
|
+
bullet is split on " · " (the magazine prompt asks for "Title · Body"
|
|
829
|
+
format); fall back to whole-bullet-as-title when no separator is
|
|
830
|
+
present.
|
|
831
|
+
────────────────────────────────────────────────────────────── */
|
|
832
|
+
(function () {
|
|
833
|
+
const params = new URLSearchParams(location.search);
|
|
834
|
+
const briefId = (params.get("b") || "").trim();
|
|
835
|
+
const roomId = (params.get("r") || "").trim();
|
|
836
|
+
const root = document.querySelector("[data-mag-root]");
|
|
837
|
+
|
|
838
|
+
function escape(s) {
|
|
839
|
+
return String(s == null ? "" : s)
|
|
840
|
+
.replace(/&/g, "&")
|
|
841
|
+
.replace(/</g, "<")
|
|
842
|
+
.replace(/>/g, ">")
|
|
843
|
+
.replace(/"/g, """)
|
|
844
|
+
.replace(/'/g, "'");
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function showState(mark, title, body) {
|
|
848
|
+
root.innerHTML = `
|
|
849
|
+
<div class="mag-state">
|
|
850
|
+
<div class="mag-state-mark">${escape(mark)}</div>
|
|
851
|
+
<div class="mag-state-title">${escape(title)}</div>
|
|
852
|
+
<div class="mag-state-body">${escape(body)}</div>
|
|
853
|
+
</div>`;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function loadBrief() {
|
|
857
|
+
let url;
|
|
858
|
+
if (briefId) {
|
|
859
|
+
url = `/api/briefs/${encodeURIComponent(briefId)}`;
|
|
860
|
+
} else if (roomId) {
|
|
861
|
+
url = `/api/rooms/${encodeURIComponent(roomId)}/brief`;
|
|
862
|
+
} else {
|
|
863
|
+
showState("Missing query", "No brief specified",
|
|
864
|
+
"Add ?b=<briefId> or ?r=<roomId> to the URL.");
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
const res = await fetch(url);
|
|
868
|
+
if (!res.ok) {
|
|
869
|
+
const e = await res.json().catch(() => ({}));
|
|
870
|
+
showState("Not found", "Brief not found",
|
|
871
|
+
e.error || "The requested brief doesn't exist or is no longer available.");
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
return await res.json();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
/** Split a talkingPoints bullet into title + body. The magazine
|
|
878
|
+
* prompt asks for "Title · Body" with a middle dot + spaces. We
|
|
879
|
+
* also accept em-dash ("Title — Body") and colon (": ") as
|
|
880
|
+
* separators since LLMs occasionally drift. If no separator is
|
|
881
|
+
* found the whole string becomes the title and the body is
|
|
882
|
+
* empty — the card still renders with just the headline. */
|
|
883
|
+
function splitCard(raw) {
|
|
884
|
+
const s = String(raw || "").trim();
|
|
885
|
+
if (!s) return { title: "", body: "" };
|
|
886
|
+
const seps = [" · ", " — ", " - ", ": "];
|
|
887
|
+
for (const sep of seps) {
|
|
888
|
+
const idx = s.indexOf(sep);
|
|
889
|
+
if (idx > 0 && idx < 50) {
|
|
890
|
+
return {
|
|
891
|
+
title: s.slice(0, idx).trim(),
|
|
892
|
+
body: s.slice(idx + sep.length).trim(),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
return { title: s, body: "" };
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/** Inline SVG glyph picker for the closer-band bullets. The
|
|
900
|
+
* reference uses 4 light monoline icons (hourglass / person /
|
|
901
|
+
* lightbulb / infinity). We pick by index (1st = hourglass, 2nd
|
|
902
|
+
* = person, etc.) so the visual rhythm matches the source even
|
|
903
|
+
* when the LLM-generated bullets are about different topics. */
|
|
904
|
+
function closerGlyph(i) {
|
|
905
|
+
const ICONS = [
|
|
906
|
+
// hourglass
|
|
907
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2h12M6 22h12M7 2v5l5 5-5 5v5M17 2v5l-5 5 5 5v5"/></svg>',
|
|
908
|
+
// person
|
|
909
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 4-7 8-7s8 3 8 7"/></svg>',
|
|
910
|
+
// lightbulb
|
|
911
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-3 11c.6.6 1 1.5 1 2.4V18h4v-1.6c0-.9.4-1.8 1-2.4a6 6 0 0 0-3-11Z"/></svg>',
|
|
912
|
+
// infinity
|
|
913
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12c0-2.5 2-4 4-4s3 1 5 4 3 4 5 4 4-1.5 4-4-2-4-4-4-3 1-5 4-3 4-5 4-4-1.5-4-4Z"/></svg>',
|
|
914
|
+
// bookmark (5th — fallback when verification gives ≥5 bullets)
|
|
915
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h12v18l-6-4-6 4z"/></svg>',
|
|
916
|
+
];
|
|
917
|
+
return ICONS[i % ICONS.length];
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
/** Generic step glyph for the setup band. The reference uses
|
|
921
|
+
* three different small icons (folder-plus, magic wand, gear);
|
|
922
|
+
* we ship a flat 3 with the same flavor so the band reads
|
|
923
|
+
* recognisably regardless of the room's actual step content. */
|
|
924
|
+
function stepGlyph(i) {
|
|
925
|
+
const ICONS = [
|
|
926
|
+
// folder-plus
|
|
927
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7Z"/><path d="M12 11v6M9 14h6"/></svg>',
|
|
928
|
+
// magic wand
|
|
929
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4l2 2M19 8l2 2M5 19l11-11M4 12l1.5-1.5M20 14l1.5-1.5M14 5l-1.5 1.5"/></svg>',
|
|
930
|
+
// gear
|
|
931
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>',
|
|
932
|
+
];
|
|
933
|
+
return ICONS[i % ICONS.length];
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Glyph for the hero cards · soft monoline icons that suggest
|
|
937
|
+
* the card's subject without committing to a literal match. */
|
|
938
|
+
function cardGlyph(i) {
|
|
939
|
+
const ICONS = [
|
|
940
|
+
// bar-chart (1st card · feels like a dashboard / metrics)
|
|
941
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h16M7 20V10M12 20V4M17 20v-7"/></svg>',
|
|
942
|
+
// notebook (2nd card · journal / capture)
|
|
943
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h12v18H6zM6 8h12M6 13h12M6 18h12"/></svg>',
|
|
944
|
+
// search (3rd card · research)
|
|
945
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.35-4.35"/></svg>',
|
|
946
|
+
// brain (4th card · analysis)
|
|
947
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6a3 3 0 0 1 3-3 3 3 0 0 1 3 3M6 9a3 3 0 0 0 3 3M18 9a3 3 0 0 1-3 3M9 12v3a3 3 0 0 0 3 3 3 3 0 0 0 3-3v-3M9 18a3 3 0 0 1-3-3M15 18a3 3 0 0 0 3-3"/></svg>',
|
|
948
|
+
// newspaper (5th card · daily brief)
|
|
949
|
+
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h14v16H4zM18 8h2v12h-2M8 8h6M8 12h6M8 16h6"/></svg>',
|
|
950
|
+
];
|
|
951
|
+
return ICONS[i % ICONS.length];
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function renderHeroCards(bullets) {
|
|
955
|
+
// Cap to 5 cards · the magazine layout is fixed at this density.
|
|
956
|
+
// Pad with empty slots when the LLM provided <5 so the grid
|
|
957
|
+
// doesn't collapse asymmetrically.
|
|
958
|
+
const cards = bullets.slice(0, 5).map(splitCard);
|
|
959
|
+
while (cards.length < 3) cards.push(null);
|
|
960
|
+
return cards.map((c, i) => {
|
|
961
|
+
const tint = i % 2 === 0 ? "tint-mint" : "tint-blue";
|
|
962
|
+
// 5th card spans both columns to balance the bottom row.
|
|
963
|
+
const span = i === 4 ? " span-2" : "";
|
|
964
|
+
if (!c) return "";
|
|
965
|
+
const subPart = c.body
|
|
966
|
+
? `<div class="mag-card-sub">${escape(c.title.slice(0, 60))}</div>`
|
|
967
|
+
: "";
|
|
968
|
+
return `
|
|
969
|
+
<article class="mag-card ${tint}${span}">
|
|
970
|
+
<div class="mag-card-numeral">${i + 1}</div>
|
|
971
|
+
${
|
|
972
|
+
c.body
|
|
973
|
+
? `<div class="mag-card-title">${escape(c.title)}</div>
|
|
974
|
+
<div class="mag-card-body">${escape(c.body)}</div>`
|
|
975
|
+
: `<div class="mag-card-title">${escape(c.title)}</div>`
|
|
976
|
+
}
|
|
977
|
+
<span class="mag-card-glyph" aria-hidden="true">${cardGlyph(i)}</span>
|
|
978
|
+
</article>`;
|
|
979
|
+
}).join("");
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function renderSetup(milestones) {
|
|
983
|
+
const arr = (milestones || []).slice(0, 3);
|
|
984
|
+
if (arr.length === 0) return "";
|
|
985
|
+
const cells = arr.map((m, i) => {
|
|
986
|
+
const period = m.period || `Step ${i + 1}`;
|
|
987
|
+
const title = m.title || period;
|
|
988
|
+
// Render "1. {title}" style heading to match the reference.
|
|
989
|
+
const heading = `${i + 1}. ${title}`;
|
|
990
|
+
return `
|
|
991
|
+
<div class="mag-step">
|
|
992
|
+
<span class="mag-step-glyph" aria-hidden="true">${stepGlyph(i)}</span>
|
|
993
|
+
<div class="mag-step-title">${escape(heading)}</div>
|
|
994
|
+
<div class="mag-step-body">${escape(m.body || "")}</div>
|
|
995
|
+
</div>`;
|
|
996
|
+
}).join("");
|
|
997
|
+
return cells;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function renderCloser(verification) {
|
|
1001
|
+
if (!verification || !Array.isArray(verification.bullets) || verification.bullets.length === 0) {
|
|
1002
|
+
return "";
|
|
1003
|
+
}
|
|
1004
|
+
const items = verification.bullets.slice(0, 5).map((b, i) => {
|
|
1005
|
+
// Bold the leading title-fragment when the bullet has a
|
|
1006
|
+
// separator. Mirrors the "**省时省力** · …" pattern in the
|
|
1007
|
+
// reference where each bullet leads with a tight label
|
|
1008
|
+
// followed by a middle-dot separator and the body.
|
|
1009
|
+
const parts = splitCard(b);
|
|
1010
|
+
const leading = parts.body
|
|
1011
|
+
? `<b>${escape(parts.title)}</b><span class="mag-closer-sep">·</span>${escape(parts.body)}`
|
|
1012
|
+
: escape(parts.title);
|
|
1013
|
+
const glyph = closerGlyph(i);
|
|
1014
|
+
return `
|
|
1015
|
+
<li>
|
|
1016
|
+
<span class="mag-closer-glyph" aria-hidden="true">${glyph}</span>
|
|
1017
|
+
<span>${leading}</span>
|
|
1018
|
+
</li>`;
|
|
1019
|
+
}).join("");
|
|
1020
|
+
return items;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function render(brief) {
|
|
1024
|
+
if (brief.mode !== "magazine") {
|
|
1025
|
+
showState("Wrong mode", "This brief isn't a magazine",
|
|
1026
|
+
"Open it in the matching viewer instead. Magazine, bento, and research-note are separate output modes.");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const m = brief.bodyJson;
|
|
1030
|
+
if (!m || typeof m !== "object") {
|
|
1031
|
+
if (brief.isGenerating) {
|
|
1032
|
+
showState("Generating",
|
|
1033
|
+
"Magazine is still being prepared",
|
|
1034
|
+
"The chair is currently composing the spread. Refresh in a few seconds.");
|
|
1035
|
+
} else {
|
|
1036
|
+
showState("Empty",
|
|
1037
|
+
"This brief has no magazine data",
|
|
1038
|
+
"It may have failed to generate. Try regenerating from the room view.");
|
|
1039
|
+
}
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (m.title) document.title = `${m.title} · Magazine`;
|
|
1044
|
+
|
|
1045
|
+
const heroCards = renderHeroCards((m.talkingPoints && m.talkingPoints.bullets) || []);
|
|
1046
|
+
const featureCount = Math.min(5, ((m.talkingPoints && m.talkingPoints.bullets) || []).length || 0);
|
|
1047
|
+
const featureTitle = (m.talkingPoints && m.talkingPoints.title) || (featureCount ? `${featureCount} tactics` : "Tactics");
|
|
1048
|
+
const setupCells = renderSetup(m.milestones);
|
|
1049
|
+
const setupHasItems = setupCells.length > 0;
|
|
1050
|
+
const closerItems = renderCloser(m.verification);
|
|
1051
|
+
const closerTitle = (m.verification && m.verification.title) || "Why this matters";
|
|
1052
|
+
|
|
1053
|
+
// Source text · masthead byline · prefer source, fall back to
|
|
1054
|
+
// footerTag, then "Boardroom".
|
|
1055
|
+
const byline = m.source || "Boardroom presents";
|
|
1056
|
+
// Issue / date · split the footerTag on a delimiter into 2
|
|
1057
|
+
// lines so the top-right reads like an issue stamp ("Issue 01
|
|
1058
|
+
// / July 2024" in the reference). When the tag has no obvious
|
|
1059
|
+
// split, just show it as one line.
|
|
1060
|
+
const issueParts = (m.footerTag || "").split(" · ").filter(Boolean);
|
|
1061
|
+
const issueLines = issueParts.length >= 2
|
|
1062
|
+
? `${issueParts[0]}\n${issueParts.slice(1).join(" · ")}`
|
|
1063
|
+
: (m.footerTag || "");
|
|
1064
|
+
|
|
1065
|
+
// Setup band heading · default text falls back to the brief's
|
|
1066
|
+
// conclusion when no specific subject heading exists, so the
|
|
1067
|
+
// band always has a real label.
|
|
1068
|
+
const setupHeading = m.conclusion || "Set up your system";
|
|
1069
|
+
|
|
1070
|
+
root.innerHTML = `
|
|
1071
|
+
<article class="mag-doc" data-mag-paper>
|
|
1072
|
+
<header class="mag-masthead">
|
|
1073
|
+
<div class="mag-masthead-top">
|
|
1074
|
+
<div class="mag-byline">${escape(byline.toUpperCase())}</div>
|
|
1075
|
+
${issueLines ? `<div class="mag-issue">${escape(issueLines)}</div>` : ""}
|
|
1076
|
+
</div>
|
|
1077
|
+
<h1 class="mag-title">${escape(m.title || "")}</h1>
|
|
1078
|
+
${m.kicker ? `<div class="mag-deck">${escape(m.kicker)}</div>` : ""}
|
|
1079
|
+
</header>
|
|
1080
|
+
<div class="mag-masthead-rule"></div>
|
|
1081
|
+
|
|
1082
|
+
<section class="mag-hero">
|
|
1083
|
+
<aside class="mag-feature">
|
|
1084
|
+
${featureCount ? `<div class="mag-feature-numeral">${featureCount}</div>` : ""}
|
|
1085
|
+
<div class="mag-feature-title">${escape(featureTitle)}</div>
|
|
1086
|
+
<div class="mag-feature-sub">${escape((featureTitle || "").toUpperCase())}</div>
|
|
1087
|
+
${m.kicker ? `<div class="mag-feature-body">${escape(m.kicker)}</div>` : ""}
|
|
1088
|
+
</aside>
|
|
1089
|
+
<div class="mag-cards">${heroCards}</div>
|
|
1090
|
+
</section>
|
|
1091
|
+
|
|
1092
|
+
${setupHasItems ? `
|
|
1093
|
+
<section class="mag-setup">
|
|
1094
|
+
<div class="mag-setup-head">
|
|
1095
|
+
<div class="mag-setup-title">${escape(setupHeading)}</div>
|
|
1096
|
+
<div class="mag-setup-sub">SETUP GUIDE</div>
|
|
1097
|
+
</div>
|
|
1098
|
+
<div class="mag-setup-grid">${setupCells}</div>
|
|
1099
|
+
</section>` : ""}
|
|
1100
|
+
|
|
1101
|
+
${closerItems ? `
|
|
1102
|
+
<section class="mag-closer">
|
|
1103
|
+
<div class="mag-closer-head">
|
|
1104
|
+
<div class="mag-closer-title">${escape(closerTitle)}</div>
|
|
1105
|
+
<div class="mag-closer-sub">${escape((closerTitle || "").toUpperCase())}</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
<ul class="mag-closer-list">${closerItems}</ul>
|
|
1108
|
+
<div class="mag-closer-stamp">
|
|
1109
|
+
<span class="mag-closer-stamp-pill">PrivateBoard</span>
|
|
1110
|
+
<span>boardroom · ${escape(brief.id || "")}</span>
|
|
1111
|
+
</div>
|
|
1112
|
+
</section>` : ""}
|
|
1113
|
+
</article>`;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/* ─── Export wiring · same PNG-as-PDF strategy as bento ──────── */
|
|
1117
|
+
let _h2iLoaded = null;
|
|
1118
|
+
async function ensureHtmlToImage() {
|
|
1119
|
+
if (window.htmlToImage) return;
|
|
1120
|
+
if (!_h2iLoaded) {
|
|
1121
|
+
_h2iLoaded = new Promise((res, rej) => {
|
|
1122
|
+
const s = document.createElement("script");
|
|
1123
|
+
s.src = "https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js";
|
|
1124
|
+
s.onload = res;
|
|
1125
|
+
s.onerror = rej;
|
|
1126
|
+
document.head.appendChild(s);
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
await _h2iLoaded;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
async function captureMagPng() {
|
|
1133
|
+
const el = document.querySelector("[data-mag-paper]");
|
|
1134
|
+
if (!el) throw new Error("magazine doc not found");
|
|
1135
|
+
await ensureHtmlToImage();
|
|
1136
|
+
if (document.fonts && document.fonts.ready) {
|
|
1137
|
+
try { await document.fonts.ready; } catch { /* best-effort */ }
|
|
1138
|
+
}
|
|
1139
|
+
const width = Math.max(el.scrollWidth, el.offsetWidth, el.clientWidth);
|
|
1140
|
+
const height = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight);
|
|
1141
|
+
return window.htmlToImage.toPng(el, {
|
|
1142
|
+
pixelRatio: 2,
|
|
1143
|
+
backgroundColor: "#FBF8EE",
|
|
1144
|
+
cacheBust: true,
|
|
1145
|
+
width,
|
|
1146
|
+
height,
|
|
1147
|
+
canvasWidth: width,
|
|
1148
|
+
canvasHeight: height,
|
|
1149
|
+
style: {
|
|
1150
|
+
margin: "0",
|
|
1151
|
+
width: `${width}px`,
|
|
1152
|
+
height: `${height}px`,
|
|
1153
|
+
},
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function slugTitle() {
|
|
1158
|
+
return (document.title || "magazine").replace(/[^a-z0-9]+/gi, "-").slice(0, 60) || "magazine";
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
async function exportPng() {
|
|
1162
|
+
try {
|
|
1163
|
+
const dataUrl = await captureMagPng();
|
|
1164
|
+
const a = document.createElement("a");
|
|
1165
|
+
a.download = `${slugTitle()}.png`;
|
|
1166
|
+
a.href = dataUrl;
|
|
1167
|
+
a.click();
|
|
1168
|
+
} catch (e) {
|
|
1169
|
+
console.warn("[magazine] PNG export failed:", e);
|
|
1170
|
+
alert("PNG export failed · see browser console.");
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
async function exportPdf() {
|
|
1175
|
+
try {
|
|
1176
|
+
const dataUrl = await captureMagPng();
|
|
1177
|
+
const win = window.open("", "_blank", "width=1024,height=720");
|
|
1178
|
+
if (!win) {
|
|
1179
|
+
alert("PDF export needs to open a new window — please allow popups for this site.");
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
const slug = slugTitle();
|
|
1183
|
+
win.document.open();
|
|
1184
|
+
win.document.write(`<!doctype html><html lang="en"><head>
|
|
1185
|
+
<meta charset="utf-8">
|
|
1186
|
+
<title>${slug}</title>
|
|
1187
|
+
<style>
|
|
1188
|
+
@page { size: auto; margin: 10mm; }
|
|
1189
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
1190
|
+
html, body { background: #FFFFFF; }
|
|
1191
|
+
body {
|
|
1192
|
+
display: flex;
|
|
1193
|
+
align-items: flex-start;
|
|
1194
|
+
justify-content: center;
|
|
1195
|
+
padding: 20px;
|
|
1196
|
+
min-height: 100vh;
|
|
1197
|
+
}
|
|
1198
|
+
img {
|
|
1199
|
+
display: block;
|
|
1200
|
+
width: 100%;
|
|
1201
|
+
max-width: 880px;
|
|
1202
|
+
height: auto;
|
|
1203
|
+
box-shadow: 0 0 24px rgba(0, 0, 0, 0.08);
|
|
1204
|
+
}
|
|
1205
|
+
@media print {
|
|
1206
|
+
body { padding: 0; }
|
|
1207
|
+
img { box-shadow: none; max-width: none; width: 100%; }
|
|
1208
|
+
}
|
|
1209
|
+
.hint {
|
|
1210
|
+
position: fixed;
|
|
1211
|
+
top: 12px;
|
|
1212
|
+
left: 12px;
|
|
1213
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
1214
|
+
font-size: 11px;
|
|
1215
|
+
color: #8E8B83;
|
|
1216
|
+
background: rgba(255,255,255,0.9);
|
|
1217
|
+
padding: 6px 10px;
|
|
1218
|
+
border: 1px solid #E5E2DA;
|
|
1219
|
+
}
|
|
1220
|
+
@media print { .hint { display: none; } }
|
|
1221
|
+
</style>
|
|
1222
|
+
</head><body>
|
|
1223
|
+
<div class="hint">// press <kbd>⌘P</kbd> / <kbd>Ctrl+P</kbd> · save as PDF</div>
|
|
1224
|
+
<img alt="${slug}" src="${dataUrl}">
|
|
1225
|
+
<script>
|
|
1226
|
+
(function () {
|
|
1227
|
+
var img = document.querySelector("img");
|
|
1228
|
+
function go() { setTimeout(function () { window.print(); }, 200); }
|
|
1229
|
+
if (img && img.complete) { go(); }
|
|
1230
|
+
else if (img) { img.addEventListener("load", go, { once: true }); }
|
|
1231
|
+
})();
|
|
1232
|
+
<\/script>
|
|
1233
|
+
</body></html>`);
|
|
1234
|
+
win.document.close();
|
|
1235
|
+
} catch (e) {
|
|
1236
|
+
console.warn("[magazine] PDF export failed:", e);
|
|
1237
|
+
alert("PDF export failed · see browser console.");
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
document.addEventListener("click", (e) => {
|
|
1242
|
+
if (e.target.closest("[data-mag-png]")) {
|
|
1243
|
+
e.preventDefault();
|
|
1244
|
+
exportPng();
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (e.target.closest("[data-mag-print]")) {
|
|
1248
|
+
e.preventDefault();
|
|
1249
|
+
exportPdf();
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
/* ─── Boot ──────────────────────────────────────────────────── */
|
|
1255
|
+
loadBrief().then((brief) => {
|
|
1256
|
+
if (brief) render(brief);
|
|
1257
|
+
}).catch((e) => {
|
|
1258
|
+
console.error("[magazine] load failed:", e);
|
|
1259
|
+
showState("Error", "Couldn't load this brief",
|
|
1260
|
+
e instanceof Error ? e.message : String(e));
|
|
1261
|
+
});
|
|
1262
|
+
})();
|
|
1263
|
+
</script>
|
|
1264
|
+
|
|
1265
|
+
</body>
|
|
1266
|
+
</html>
|