privateboard 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1685 @@
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
+ <!-- Playfair Display for the Vogue-style high-contrast Didot
9
+ logotype + serif headlines. Free Google font · no fallback
10
+ drama since the system Tiempos / Bodoni stack picks up. -->
11
+ <link rel="preconnect" href="https://fonts.googleapis.com">
12
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
13
+ <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400;1,700&display=swap" rel="stylesheet">
14
+ <style>
15
+ /* ═══════════════════════════════════════════════════════════════════
16
+ Magazine · two distinct templates picked deterministically from
17
+ the brief id (so refresh always renders the same look). Both
18
+ ship at LEAST 2 pages (Cover + Inside Feature) so the report
19
+ reads as a magazine rather than a single sheet.
20
+
21
+ · GQ · The Boardroom · GQ register · bold condensed sans
22
+ logo · black + red accent · cover lines / featured-quote
23
+ portrait · inside spread with red drop cap + vertical-bar
24
+ pull quote.
25
+ · VOGUE · BOARDROOM · Vogue register · spaced-out high-
26
+ contrast Didot logo · cream paper + deep red accent ·
27
+ italic register throughout · ★ ornaments between sections.
28
+ ═══════════════════════════════════════════════════════════════════ */
29
+ :root {
30
+ /* Default · GQ register */
31
+ --bg: #1A1A1A;
32
+ --paper: #F5F0E8;
33
+ --paper-soft: #FAF6EE;
34
+ --paper-edge: #E8E0CE;
35
+ --ink: #0F0F0F;
36
+ --ink-soft: #2C2C2C;
37
+ --ink-mid: #5C5C5C;
38
+ --ink-faint: #8A8A8A;
39
+ --ink-muted: #B0B0B0;
40
+ --inv-bg: #0F0F0F;
41
+ --inv-ink: #F5F0E8;
42
+ --inv-ink-soft: #C0BBB0;
43
+ --rule: #C8C2B6;
44
+ --rule-soft: #D8D2C6;
45
+ --rule-strong: #6E665A;
46
+ --accent: #D71920;
47
+ --accent-deep: #A41218;
48
+ --accent-soft: #E96B70;
49
+
50
+ --shadow-page: 0 2px 8px rgba(0, 0, 0, 0.18),
51
+ 0 16px 48px rgba(0, 0, 0, 0.22);
52
+
53
+ --serif-display: "Playfair Display", "Tiempos Headline",
54
+ "Bodoni 72", "Didot", "Source Serif Pro",
55
+ "Charter", Georgia, "Source Han Serif SC",
56
+ "Songti SC", "STSong", serif;
57
+ --serif: "Charter", "Source Serif Pro", "Iowan Old Style",
58
+ "Source Han Serif SC", Georgia, "Songti SC", "STSong", serif;
59
+ --sans: "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont,
60
+ "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
61
+ "Source Han Sans CN", "Noto Sans CJK SC", sans-serif;
62
+ --mono: "SF Mono", "JetBrains Mono", "Menlo",
63
+ "PingFang SC", "Source Han Sans CN", monospace;
64
+ }
65
+
66
+ /* Vogue variant · cream paper · deep red · serif throughout */
67
+ body[data-mag-variant="vogue"] {
68
+ --paper: #FAF5E6;
69
+ --paper-soft: #FCF8EE;
70
+ --paper-edge: #ECE4CD;
71
+ --ink: #1A1A1A;
72
+ --rule: #BDB39E;
73
+ --rule-soft: #CFC6B2;
74
+ --rule-strong: #6E654A;
75
+ --accent: #8C1922;
76
+ --accent-deep: #6B1018;
77
+ --accent-soft: #C9909A;
78
+ }
79
+
80
+ * { box-sizing: border-box; margin: 0; padding: 0; }
81
+ html, body {
82
+ background: var(--bg);
83
+ color: var(--ink);
84
+ font-family: var(--serif);
85
+ font-size: 14px;
86
+ line-height: 1.55;
87
+ -webkit-font-smoothing: antialiased;
88
+ text-rendering: optimizeLegibility;
89
+ min-height: 100vh;
90
+ }
91
+
92
+ /* ─── Top chrome ───────────────────────────────────────────────── */
93
+ .mag-top-bar {
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: space-between;
97
+ gap: 14px;
98
+ padding: 14px 28px;
99
+ background: rgba(26, 26, 26, 0.92);
100
+ backdrop-filter: blur(10px);
101
+ -webkit-backdrop-filter: blur(10px);
102
+ border-bottom: 1px solid rgba(245, 240, 232, 0.12);
103
+ flex-wrap: wrap;
104
+ position: sticky;
105
+ top: 0;
106
+ z-index: 10;
107
+ color: var(--paper);
108
+ }
109
+ .mag-crumb {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ gap: 12px;
113
+ font-family: var(--serif-display);
114
+ font-size: 16px;
115
+ font-weight: 700;
116
+ color: var(--paper);
117
+ text-decoration: none;
118
+ }
119
+ .mag-crumb::before {
120
+ content: "";
121
+ width: 9px;
122
+ height: 9px;
123
+ background: var(--accent);
124
+ flex: 0 0 auto;
125
+ border-radius: 50%;
126
+ }
127
+ .mag-crumb-accent {
128
+ color: var(--ink-faint);
129
+ font-style: italic;
130
+ font-weight: 400;
131
+ }
132
+ .mag-actions { display: flex; gap: 8px; align-items: center; }
133
+ .mag-page-nav {
134
+ display: inline-flex;
135
+ gap: 4px;
136
+ margin-right: 12px;
137
+ font-family: var(--mono);
138
+ font-size: 10px;
139
+ letter-spacing: 0.12em;
140
+ text-transform: uppercase;
141
+ }
142
+ .mag-page-nav a {
143
+ color: var(--ink-muted);
144
+ padding: 4px 8px;
145
+ border: 1px solid rgba(245, 240, 232, 0.16);
146
+ text-decoration: none;
147
+ transition: all 0.12s;
148
+ }
149
+ .mag-page-nav a:hover {
150
+ background: rgba(245, 240, 232, 0.10);
151
+ color: var(--paper);
152
+ }
153
+ .mag-variant-badge {
154
+ font-family: var(--mono);
155
+ font-size: 10px;
156
+ letter-spacing: 0.16em;
157
+ text-transform: uppercase;
158
+ color: var(--accent-soft);
159
+ margin-right: 12px;
160
+ padding: 4px 10px;
161
+ background: rgba(245, 240, 232, 0.06);
162
+ border: 1px solid rgba(245, 240, 232, 0.16);
163
+ }
164
+ .mag-btn {
165
+ font-family: var(--mono);
166
+ font-size: 10.5px;
167
+ letter-spacing: 0.04em;
168
+ padding: 7px 12px;
169
+ background: transparent;
170
+ border: 1px solid rgba(245, 240, 232, 0.30);
171
+ color: var(--paper);
172
+ cursor: pointer;
173
+ text-decoration: none;
174
+ text-transform: uppercase;
175
+ font-weight: 600;
176
+ transition: background 0.15s, color 0.15s;
177
+ }
178
+ .mag-btn:hover { background: var(--paper); color: var(--bg); }
179
+ .mag-btn .glyph { margin-right: 4px; }
180
+
181
+ /* ─── Doc · stack of magazine pages ─────────────────────────── */
182
+ .mag-doc {
183
+ max-width: 920px;
184
+ margin: 0 auto;
185
+ padding: 28px 12px 56px;
186
+ }
187
+ .mag-page {
188
+ background: var(--paper);
189
+ padding: 32px 38px 36px;
190
+ margin-bottom: 24px;
191
+ box-shadow: var(--shadow-page);
192
+ position: relative;
193
+ }
194
+ .mag-page:last-child { margin-bottom: 0; }
195
+
196
+ /* ═══════════════════════════════════════════════════════════════
197
+ GQ variant · The Boardroom · bold modern editorial
198
+ ═══════════════════════════════════════════════════════════════ */
199
+
200
+ /* Cover · top issue strip + huge logo + 2-col below */
201
+ .gq-cover-top {
202
+ display: flex;
203
+ justify-content: space-between;
204
+ align-items: baseline;
205
+ padding-bottom: 12px;
206
+ border-bottom: 1px solid var(--ink);
207
+ font-family: var(--sans);
208
+ font-size: 10px;
209
+ font-weight: 700;
210
+ letter-spacing: 0.18em;
211
+ text-transform: uppercase;
212
+ color: var(--ink);
213
+ }
214
+ .gq-cover-logo {
215
+ font-family: var(--sans);
216
+ font-weight: 900;
217
+ font-size: clamp(48px, 11vw, 92px);
218
+ line-height: 0.94;
219
+ letter-spacing: -0.045em;
220
+ text-transform: uppercase;
221
+ color: var(--ink);
222
+ text-align: center;
223
+ margin: 14px 0 12px;
224
+ word-break: break-word;
225
+ overflow-wrap: anywhere;
226
+ }
227
+ .gq-cover-strip {
228
+ display: flex;
229
+ justify-content: space-between;
230
+ padding: 8px 0;
231
+ border-top: 4px solid var(--accent);
232
+ border-bottom: 1px solid var(--ink);
233
+ font-family: var(--sans);
234
+ font-size: 11px;
235
+ font-weight: 700;
236
+ letter-spacing: 0.14em;
237
+ text-transform: uppercase;
238
+ color: var(--ink);
239
+ margin-bottom: 18px;
240
+ }
241
+ .gq-cover-strip em { color: var(--accent); font-style: normal; }
242
+
243
+ .gq-cover-grid {
244
+ display: grid;
245
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
246
+ gap: 28px;
247
+ align-items: start;
248
+ }
249
+ .gq-cover-grid > * { min-width: 0; }
250
+ @media (max-width: 720px) {
251
+ .gq-cover-grid { grid-template-columns: 1fr; gap: 22px; }
252
+ }
253
+
254
+ /* GQ "portrait" · solid dark colored panel with editorial quote
255
+ (replaces a fake-photo image · honest typographic feature). */
256
+ .gq-cover-portrait {
257
+ background: var(--inv-bg);
258
+ color: var(--paper);
259
+ padding: 26px 22px 24px;
260
+ min-height: 360px;
261
+ display: flex;
262
+ flex-direction: column;
263
+ justify-content: space-between;
264
+ gap: 14px;
265
+ }
266
+ .gq-cover-portrait-eye {
267
+ font-family: var(--sans);
268
+ font-size: 10px;
269
+ font-weight: 800;
270
+ letter-spacing: 0.22em;
271
+ text-transform: uppercase;
272
+ color: var(--accent-soft);
273
+ }
274
+ .gq-cover-portrait-mark {
275
+ font-family: var(--serif-display);
276
+ font-style: italic;
277
+ font-size: 56px;
278
+ line-height: 0.6;
279
+ color: var(--accent-soft);
280
+ margin-bottom: 4px;
281
+ }
282
+ .gq-cover-portrait-text {
283
+ font-family: var(--sans);
284
+ font-size: 18px;
285
+ font-weight: 600;
286
+ line-height: 1.3;
287
+ letter-spacing: -0.005em;
288
+ color: var(--paper);
289
+ }
290
+ .gq-cover-portrait-cite {
291
+ font-family: var(--sans);
292
+ font-size: 10px;
293
+ font-weight: 700;
294
+ letter-spacing: 0.18em;
295
+ text-transform: uppercase;
296
+ color: var(--inv-ink-soft);
297
+ }
298
+
299
+ /* GQ cover lines · stacked headlines + featured points */
300
+ .gq-cover-lines {
301
+ display: flex;
302
+ flex-direction: column;
303
+ gap: 12px;
304
+ }
305
+ .gq-cover-flag {
306
+ display: inline-block;
307
+ background: var(--accent);
308
+ color: var(--paper);
309
+ font-family: var(--sans);
310
+ font-weight: 800;
311
+ font-size: 11px;
312
+ letter-spacing: 0.22em;
313
+ text-transform: uppercase;
314
+ padding: 4px 12px;
315
+ align-self: flex-start;
316
+ }
317
+ .gq-cover-headline {
318
+ font-family: var(--sans);
319
+ font-weight: 900;
320
+ font-size: clamp(28px, 5.2vw, 38px);
321
+ line-height: 1.04;
322
+ letter-spacing: -0.022em;
323
+ text-transform: uppercase;
324
+ color: var(--ink);
325
+ word-break: break-word;
326
+ overflow-wrap: anywhere;
327
+ }
328
+ .gq-cover-headline em {
329
+ color: var(--accent);
330
+ font-style: normal;
331
+ }
332
+ .gq-cover-deck {
333
+ font-family: var(--serif);
334
+ font-style: italic;
335
+ font-size: 16px;
336
+ line-height: 1.45;
337
+ color: var(--ink-soft);
338
+ }
339
+ .gq-cover-points {
340
+ list-style: none;
341
+ margin: 4px 0 0;
342
+ padding: 0;
343
+ display: flex;
344
+ flex-direction: column;
345
+ gap: 8px;
346
+ border-top: 2px solid var(--ink);
347
+ padding-top: 12px;
348
+ }
349
+ .gq-cover-points li {
350
+ display: grid;
351
+ grid-template-columns: auto 1fr auto;
352
+ gap: 12px;
353
+ align-items: baseline;
354
+ padding-bottom: 8px;
355
+ border-bottom: 1px dotted var(--rule-strong);
356
+ }
357
+ .gq-cover-points li:last-child { border-bottom: 0; }
358
+ .gq-cover-points li b {
359
+ font-family: var(--sans);
360
+ font-weight: 800;
361
+ font-size: 14px;
362
+ color: var(--accent);
363
+ letter-spacing: 0.04em;
364
+ }
365
+ .gq-cover-points li span:nth-child(2) {
366
+ font-family: var(--serif-display);
367
+ font-size: 16px;
368
+ font-weight: 700;
369
+ line-height: 1.25;
370
+ color: var(--ink);
371
+ }
372
+ .gq-cover-points li .pg {
373
+ font-family: var(--sans);
374
+ font-size: 10px;
375
+ letter-spacing: 0.14em;
376
+ text-transform: uppercase;
377
+ color: var(--ink-mid);
378
+ font-weight: 700;
379
+ }
380
+
381
+ /* GQ inside · running header + feature opener + body */
382
+ .gq-running {
383
+ display: grid;
384
+ grid-template-columns: auto 1fr auto;
385
+ gap: 16px;
386
+ align-items: center;
387
+ padding-bottom: 10px;
388
+ border-bottom: 2px solid var(--ink);
389
+ font-family: var(--sans);
390
+ font-size: 11px;
391
+ letter-spacing: 0.18em;
392
+ text-transform: uppercase;
393
+ color: var(--ink);
394
+ font-weight: 800;
395
+ }
396
+ .gq-running-brand {
397
+ display: inline-flex;
398
+ align-items: center;
399
+ gap: 8px;
400
+ }
401
+ .gq-running-brand::before {
402
+ content: "";
403
+ width: 9px;
404
+ height: 9px;
405
+ background: var(--accent);
406
+ border-radius: 50%;
407
+ }
408
+ .gq-running-meta {
409
+ text-align: center;
410
+ font-weight: 600;
411
+ color: var(--ink-mid);
412
+ letter-spacing: 0.14em;
413
+ }
414
+ .gq-running-page {
415
+ font-family: var(--sans);
416
+ font-weight: 800;
417
+ color: var(--accent);
418
+ }
419
+
420
+ .gq-feature-dept {
421
+ display: inline-block;
422
+ background: var(--ink);
423
+ color: var(--paper);
424
+ font-family: var(--sans);
425
+ font-weight: 800;
426
+ font-size: 11px;
427
+ letter-spacing: 0.20em;
428
+ text-transform: uppercase;
429
+ padding: 5px 12px;
430
+ margin: 18px 0 10px;
431
+ }
432
+ .gq-feature-title {
433
+ font-family: var(--sans);
434
+ font-weight: 900;
435
+ font-size: 50px;
436
+ line-height: 0.98;
437
+ letter-spacing: -0.022em;
438
+ color: var(--ink);
439
+ margin-bottom: 12px;
440
+ max-width: 760px;
441
+ }
442
+ @media (max-width: 720px) {
443
+ .gq-feature-title { font-size: 34px; }
444
+ }
445
+ .gq-feature-deck {
446
+ font-family: var(--serif);
447
+ font-style: italic;
448
+ font-size: 17px;
449
+ line-height: 1.45;
450
+ color: var(--ink-soft);
451
+ margin-bottom: 12px;
452
+ max-width: 720px;
453
+ }
454
+ .gq-feature-byline {
455
+ font-family: var(--sans);
456
+ font-size: 11px;
457
+ letter-spacing: 0.1em;
458
+ text-transform: uppercase;
459
+ color: var(--ink-mid);
460
+ font-weight: 700;
461
+ margin-bottom: 14px;
462
+ padding-bottom: 12px;
463
+ border-bottom: 1px solid var(--rule);
464
+ }
465
+ .gq-feature-byline em {
466
+ font-style: italic;
467
+ color: var(--ink-soft);
468
+ text-transform: none;
469
+ letter-spacing: 0;
470
+ }
471
+
472
+ .gq-feature-body {
473
+ column-count: 2;
474
+ column-gap: 28px;
475
+ column-rule: 1px solid var(--rule);
476
+ font-family: var(--serif);
477
+ font-size: 13.5px;
478
+ line-height: 1.62;
479
+ color: var(--ink-soft);
480
+ text-align: justify;
481
+ hyphens: auto;
482
+ -webkit-hyphens: auto;
483
+ }
484
+ @media (max-width: 540px) {
485
+ .gq-feature-body { column-count: 1; }
486
+ }
487
+ .gq-feature-body p + p { margin-top: 8px; }
488
+ .gq-feature-body.has-drop::first-letter {
489
+ font-family: var(--sans);
490
+ font-weight: 900;
491
+ font-size: 64px;
492
+ line-height: 0.85;
493
+ color: var(--accent);
494
+ float: left;
495
+ margin: 6px 8px 0 0;
496
+ }
497
+
498
+ /* GQ pull-quote · vertical red bar + bold sans */
499
+ .gq-pull {
500
+ border-left: 6px solid var(--accent);
501
+ padding: 8px 22px;
502
+ margin: 22px 0;
503
+ }
504
+ .gq-pull-text {
505
+ font-family: var(--sans);
506
+ font-weight: 700;
507
+ font-size: 22px;
508
+ line-height: 1.28;
509
+ letter-spacing: -0.012em;
510
+ color: var(--ink);
511
+ }
512
+ .gq-pull-cite {
513
+ font-family: var(--sans);
514
+ font-size: 10px;
515
+ letter-spacing: 0.18em;
516
+ text-transform: uppercase;
517
+ color: var(--accent);
518
+ font-weight: 800;
519
+ margin-top: 8px;
520
+ }
521
+
522
+ /* GQ stat tile · red top bar */
523
+ .gq-stat {
524
+ background: var(--paper-soft);
525
+ padding: 16px 18px;
526
+ border-top: 4px solid var(--accent);
527
+ margin: 14px 0;
528
+ display: flex;
529
+ flex-direction: column;
530
+ gap: 6px;
531
+ break-inside: avoid;
532
+ }
533
+ .gq-stat-eye {
534
+ font-family: var(--sans);
535
+ font-size: 10px;
536
+ font-weight: 800;
537
+ letter-spacing: 0.18em;
538
+ text-transform: uppercase;
539
+ color: var(--accent);
540
+ }
541
+ .gq-stat-num {
542
+ font-family: var(--sans);
543
+ font-weight: 900;
544
+ font-size: 36px;
545
+ line-height: 1.0;
546
+ letter-spacing: -0.025em;
547
+ color: var(--ink);
548
+ word-break: break-word;
549
+ overflow-wrap: anywhere;
550
+ }
551
+ .gq-stat-cap {
552
+ font-family: var(--serif);
553
+ font-style: italic;
554
+ font-size: 12.5px;
555
+ line-height: 1.4;
556
+ color: var(--ink-mid);
557
+ }
558
+
559
+ /* GQ inside grid · article + sidebar */
560
+ .gq-inside-grid {
561
+ display: grid;
562
+ grid-template-columns: minmax(0, 2.2fr) minmax(180px, 1fr);
563
+ gap: 28px;
564
+ margin-top: 4px;
565
+ align-items: start;
566
+ }
567
+ @media (max-width: 720px) {
568
+ .gq-inside-grid { grid-template-columns: 1fr; gap: 22px; }
569
+ }
570
+ .gq-side {
571
+ display: flex;
572
+ flex-direction: column;
573
+ gap: 14px;
574
+ padding-left: 24px;
575
+ border-left: 1px solid var(--rule);
576
+ }
577
+ @media (max-width: 720px) {
578
+ .gq-side { border-left: 0; padding-left: 0; padding-top: 18px; border-top: 1px solid var(--rule); }
579
+ }
580
+ .gq-side-flag {
581
+ font-family: var(--sans);
582
+ font-size: 10px;
583
+ font-weight: 800;
584
+ letter-spacing: 0.22em;
585
+ text-transform: uppercase;
586
+ color: var(--accent);
587
+ padding-bottom: 8px;
588
+ border-bottom: 2px solid var(--accent);
589
+ align-self: flex-start;
590
+ }
591
+ .gq-side-list {
592
+ list-style: none;
593
+ margin: 0;
594
+ padding: 0;
595
+ display: flex;
596
+ flex-direction: column;
597
+ }
598
+ .gq-side-list li {
599
+ padding: 10px 0;
600
+ border-bottom: 1px solid var(--rule);
601
+ font-family: var(--serif);
602
+ font-size: 12.5px;
603
+ line-height: 1.55;
604
+ color: var(--ink-soft);
605
+ }
606
+ .gq-side-list li:last-child { border-bottom: 0; }
607
+ .gq-side-list li b {
608
+ display: block;
609
+ font-family: var(--sans);
610
+ font-weight: 800;
611
+ font-size: 12.5px;
612
+ color: var(--ink);
613
+ letter-spacing: 0.02em;
614
+ text-transform: uppercase;
615
+ margin-bottom: 4px;
616
+ }
617
+
618
+ /* ═══════════════════════════════════════════════════════════════
619
+ VOGUE variant · BOARDROOM · refined Didot-elegance
620
+ ═══════════════════════════════════════════════════════════════ */
621
+
622
+ body[data-mag-variant="vogue"] .mag-page {
623
+ padding: 36px 44px 38px;
624
+ }
625
+
626
+ .vogue-cover-tagline {
627
+ font-family: var(--serif);
628
+ font-style: italic;
629
+ font-size: 12px;
630
+ color: var(--ink-mid);
631
+ text-align: center;
632
+ padding: 4px 0 10px;
633
+ letter-spacing: 0.04em;
634
+ }
635
+ .vogue-cover-logo {
636
+ font-family: "Playfair Display", "Bodoni 72", "Didot", serif;
637
+ font-weight: 900;
638
+ font-size: clamp(54px, 12vw, 100px);
639
+ line-height: 0.94;
640
+ letter-spacing: 0.06em;
641
+ text-transform: uppercase;
642
+ color: var(--ink);
643
+ text-align: center;
644
+ margin: 4px 0 10px;
645
+ word-break: break-word;
646
+ overflow-wrap: anywhere;
647
+ /* Wide tracking + Playfair Display's high-stroke contrast
648
+ give the spaced-out Didot wordmark feel. Clamp keeps the
649
+ logo from blowing past the page edges on narrower viewports. */
650
+ }
651
+ .vogue-cover-strip {
652
+ display: flex;
653
+ justify-content: center;
654
+ gap: 22px;
655
+ padding: 8px 0;
656
+ border-top: 1px solid var(--ink);
657
+ border-bottom: 1px solid var(--ink);
658
+ font-family: "Playfair Display", serif;
659
+ font-style: italic;
660
+ font-size: 12px;
661
+ color: var(--ink);
662
+ margin-bottom: 18px;
663
+ flex-wrap: wrap;
664
+ }
665
+ .vogue-cover-strip .star { color: var(--accent); }
666
+
667
+ .vogue-cover-content {
668
+ display: grid;
669
+ grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
670
+ gap: 34px;
671
+ align-items: start;
672
+ }
673
+ .vogue-cover-content > * { min-width: 0; }
674
+ @media (max-width: 720px) {
675
+ .vogue-cover-content { grid-template-columns: 1fr; gap: 22px; }
676
+ }
677
+
678
+ .vogue-cover-left {
679
+ display: flex;
680
+ flex-direction: column;
681
+ gap: 14px;
682
+ }
683
+ .vogue-cover-eye {
684
+ font-family: "Playfair Display", serif;
685
+ font-style: italic;
686
+ font-size: 13px;
687
+ color: var(--accent);
688
+ text-align: left;
689
+ letter-spacing: 0.04em;
690
+ }
691
+ .vogue-cover-eye::before { content: "★ "; }
692
+ .vogue-cover-eye::after { content: " ★"; }
693
+ .vogue-cover-title {
694
+ font-family: "Playfair Display", "Tiempos Headline", serif;
695
+ font-weight: 700;
696
+ font-size: clamp(28px, 5.6vw, 42px);
697
+ line-height: 1.06;
698
+ letter-spacing: -0.012em;
699
+ color: var(--ink);
700
+ margin-bottom: 8px;
701
+ word-break: break-word;
702
+ overflow-wrap: anywhere;
703
+ }
704
+ .vogue-cover-title em {
705
+ font-style: italic;
706
+ color: var(--accent);
707
+ }
708
+ .vogue-cover-deck {
709
+ font-family: "Playfair Display", serif;
710
+ font-style: italic;
711
+ font-size: 18px;
712
+ line-height: 1.45;
713
+ color: var(--ink-soft);
714
+ letter-spacing: -0.005em;
715
+ }
716
+ .vogue-cover-byline {
717
+ font-family: "Playfair Display", serif;
718
+ font-style: italic;
719
+ font-size: 12px;
720
+ letter-spacing: 0.16em;
721
+ text-transform: uppercase;
722
+ color: var(--ink-mid);
723
+ padding: 8px 0;
724
+ border-bottom: 1px solid var(--rule);
725
+ }
726
+ .vogue-cover-byline em {
727
+ color: var(--ink);
728
+ font-style: italic;
729
+ }
730
+ .vogue-cover-lines {
731
+ list-style: none;
732
+ margin: 4px 0 0;
733
+ padding: 0;
734
+ display: flex;
735
+ flex-direction: column;
736
+ gap: 0;
737
+ }
738
+ .vogue-cover-lines li {
739
+ padding: 12px 0;
740
+ border-bottom: 1px solid var(--rule);
741
+ font-family: "Playfair Display", serif;
742
+ font-size: 17px;
743
+ line-height: 1.32;
744
+ color: var(--ink);
745
+ display: grid;
746
+ grid-template-columns: 1fr auto;
747
+ gap: 12px;
748
+ align-items: baseline;
749
+ }
750
+ .vogue-cover-lines li:last-child { border-bottom: 0; }
751
+ .vogue-cover-lines li em {
752
+ font-style: italic;
753
+ color: var(--accent);
754
+ font-weight: 700;
755
+ }
756
+ .vogue-cover-lines li .pg {
757
+ font-family: "Playfair Display", serif;
758
+ font-style: italic;
759
+ font-size: 11px;
760
+ letter-spacing: 0.12em;
761
+ color: var(--ink-mid);
762
+ text-transform: uppercase;
763
+ }
764
+
765
+ /* Vogue typographic "portrait" · cream block with elegant quote */
766
+ .vogue-cover-portrait {
767
+ background: var(--paper-soft);
768
+ border: 1px solid var(--ink);
769
+ padding: 26px 24px;
770
+ display: flex;
771
+ flex-direction: column;
772
+ gap: 12px;
773
+ text-align: center;
774
+ min-height: 320px;
775
+ }
776
+ .vogue-cover-portrait-eye {
777
+ font-family: "Playfair Display", serif;
778
+ font-style: italic;
779
+ font-size: 11px;
780
+ letter-spacing: 0.18em;
781
+ text-transform: uppercase;
782
+ color: var(--accent);
783
+ font-weight: 700;
784
+ }
785
+ .vogue-cover-portrait-eye::before { content: "★ "; }
786
+ .vogue-cover-portrait-eye::after { content: " ★"; }
787
+ .vogue-cover-portrait-mark {
788
+ font-family: "Playfair Display", serif;
789
+ font-style: italic;
790
+ font-weight: 900;
791
+ font-size: 78px;
792
+ line-height: 0.5;
793
+ color: var(--accent);
794
+ }
795
+ .vogue-cover-portrait-text {
796
+ font-family: "Playfair Display", serif;
797
+ font-style: italic;
798
+ font-size: 19px;
799
+ line-height: 1.42;
800
+ color: var(--ink);
801
+ letter-spacing: -0.003em;
802
+ flex: 1;
803
+ display: flex;
804
+ align-items: center;
805
+ justify-content: center;
806
+ }
807
+ .vogue-cover-portrait-cite {
808
+ font-family: "Playfair Display", serif;
809
+ font-style: italic;
810
+ font-size: 11px;
811
+ letter-spacing: 0.18em;
812
+ text-transform: uppercase;
813
+ color: var(--ink-mid);
814
+ }
815
+
816
+ /* Vogue inside · refined feature spread */
817
+ .vogue-running {
818
+ display: grid;
819
+ grid-template-columns: 1fr auto 1fr;
820
+ align-items: center;
821
+ gap: 18px;
822
+ padding-bottom: 10px;
823
+ border-bottom: 1px solid var(--ink);
824
+ font-family: "Playfair Display", serif;
825
+ font-style: italic;
826
+ font-size: 12px;
827
+ color: var(--ink-mid);
828
+ }
829
+ .vogue-running-l {
830
+ font-family: "Playfair Display", serif;
831
+ font-weight: 700;
832
+ font-style: normal;
833
+ letter-spacing: 0.12em;
834
+ text-transform: uppercase;
835
+ color: var(--ink);
836
+ font-size: 13px;
837
+ }
838
+ .vogue-running-c { text-align: center; }
839
+ .vogue-running-c::before { content: "★ "; color: var(--accent); }
840
+ .vogue-running-c::after { content: " ★"; color: var(--accent); }
841
+ .vogue-running-r {
842
+ text-align: right;
843
+ font-family: "Playfair Display", serif;
844
+ font-style: italic;
845
+ font-size: 13px;
846
+ color: var(--ink);
847
+ }
848
+
849
+ .vogue-feature-eye {
850
+ font-family: "Playfair Display", serif;
851
+ font-style: italic;
852
+ font-size: 13px;
853
+ letter-spacing: 0.06em;
854
+ color: var(--accent);
855
+ text-align: center;
856
+ margin: 22px 0 8px;
857
+ }
858
+ .vogue-feature-eye::before { content: "★ "; }
859
+ .vogue-feature-eye::after { content: " ★"; }
860
+ .vogue-feature-title {
861
+ font-family: "Playfair Display", "Tiempos Headline", serif;
862
+ font-weight: 900;
863
+ font-size: 56px;
864
+ line-height: 0.98;
865
+ letter-spacing: -0.018em;
866
+ color: var(--ink);
867
+ text-align: center;
868
+ margin-bottom: 12px;
869
+ }
870
+ @media (max-width: 720px) {
871
+ .vogue-feature-title { font-size: 36px; }
872
+ }
873
+ .vogue-feature-title em {
874
+ font-style: italic;
875
+ font-weight: 700;
876
+ color: var(--accent);
877
+ }
878
+ .vogue-feature-deck {
879
+ font-family: "Playfair Display", serif;
880
+ font-style: italic;
881
+ font-size: 19px;
882
+ line-height: 1.45;
883
+ color: var(--ink-soft);
884
+ text-align: center;
885
+ margin-bottom: 12px;
886
+ max-width: 720px;
887
+ margin-left: auto;
888
+ margin-right: auto;
889
+ }
890
+ .vogue-feature-byline {
891
+ font-family: "Playfair Display", serif;
892
+ font-style: italic;
893
+ font-size: 12px;
894
+ letter-spacing: 0.16em;
895
+ text-transform: uppercase;
896
+ color: var(--ink-mid);
897
+ text-align: center;
898
+ margin-bottom: 18px;
899
+ padding-bottom: 14px;
900
+ border-bottom: 1px solid var(--rule);
901
+ }
902
+ .vogue-feature-byline em {
903
+ font-style: italic;
904
+ color: var(--ink);
905
+ }
906
+
907
+ .vogue-feature-body {
908
+ column-count: 2;
909
+ column-gap: 28px;
910
+ column-rule: 1px solid var(--rule);
911
+ font-family: var(--serif);
912
+ font-size: 13.5px;
913
+ line-height: 1.65;
914
+ color: var(--ink-soft);
915
+ text-align: justify;
916
+ hyphens: auto;
917
+ -webkit-hyphens: auto;
918
+ }
919
+ @media (max-width: 540px) {
920
+ .vogue-feature-body { column-count: 1; }
921
+ }
922
+ .vogue-feature-body p + p { margin-top: 6px; text-indent: 1.2em; }
923
+ .vogue-feature-body.has-drop::first-letter {
924
+ font-family: "Playfair Display", "Tiempos Headline", serif;
925
+ font-weight: 900;
926
+ font-size: 72px;
927
+ line-height: 0.82;
928
+ color: var(--accent);
929
+ float: left;
930
+ margin: 6px 8px 0 0;
931
+ }
932
+
933
+ /* Vogue pull · centered italic with star ornaments */
934
+ .vogue-pull {
935
+ text-align: center;
936
+ margin: 28px auto;
937
+ padding: 14px 0;
938
+ border-top: 1px solid var(--ink);
939
+ border-bottom: 1px solid var(--ink);
940
+ max-width: 720px;
941
+ }
942
+ .vogue-pull-text {
943
+ font-family: "Playfair Display", serif;
944
+ font-style: italic;
945
+ font-weight: 700;
946
+ font-size: 24px;
947
+ line-height: 1.32;
948
+ color: var(--ink);
949
+ letter-spacing: -0.005em;
950
+ }
951
+ .vogue-pull-cite {
952
+ font-family: "Playfair Display", serif;
953
+ font-style: italic;
954
+ font-size: 11px;
955
+ letter-spacing: 0.18em;
956
+ text-transform: uppercase;
957
+ color: var(--ink-mid);
958
+ margin-top: 8px;
959
+ }
960
+ .vogue-pull-cite::before { content: "★ "; color: var(--accent); }
961
+ .vogue-pull-cite::after { content: " ★"; color: var(--accent); }
962
+
963
+ /* Vogue stat / sidebar list */
964
+ .vogue-stat {
965
+ background: var(--paper-soft);
966
+ border: 1px solid var(--ink);
967
+ padding: 18px 18px;
968
+ text-align: center;
969
+ margin: 14px 0;
970
+ }
971
+ .vogue-stat-eye {
972
+ font-family: "Playfair Display", serif;
973
+ font-style: italic;
974
+ font-size: 11px;
975
+ letter-spacing: 0.18em;
976
+ text-transform: uppercase;
977
+ color: var(--ink-mid);
978
+ font-weight: 700;
979
+ padding-bottom: 8px;
980
+ border-bottom: 1px solid var(--rule);
981
+ margin-bottom: 10px;
982
+ }
983
+ .vogue-stat-eye::before { content: "★ "; color: var(--ink); }
984
+ .vogue-stat-eye::after { content: " ★"; color: var(--ink); }
985
+ .vogue-stat-num {
986
+ font-family: "Playfair Display", serif;
987
+ font-weight: 900;
988
+ font-size: 38px;
989
+ line-height: 1.0;
990
+ letter-spacing: -0.018em;
991
+ color: var(--ink);
992
+ word-break: break-word;
993
+ overflow-wrap: anywhere;
994
+ }
995
+ .vogue-stat-cap {
996
+ font-family: "Playfair Display", serif;
997
+ font-style: italic;
998
+ font-size: 12px;
999
+ line-height: 1.4;
1000
+ color: var(--ink-mid);
1001
+ margin-top: 6px;
1002
+ }
1003
+
1004
+ .vogue-inside-grid {
1005
+ display: grid;
1006
+ grid-template-columns: minmax(0, 2.4fr) minmax(180px, 1fr);
1007
+ gap: 30px;
1008
+ margin-top: 4px;
1009
+ align-items: start;
1010
+ }
1011
+ @media (max-width: 720px) {
1012
+ .vogue-inside-grid { grid-template-columns: 1fr; gap: 22px; }
1013
+ }
1014
+ .vogue-side {
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ gap: 14px;
1018
+ padding-left: 24px;
1019
+ border-left: 1px solid var(--rule);
1020
+ }
1021
+ @media (max-width: 720px) {
1022
+ .vogue-side { border-left: 0; padding-left: 0; padding-top: 18px; border-top: 1px solid var(--rule); }
1023
+ }
1024
+ .vogue-side-flag {
1025
+ font-family: "Playfair Display", serif;
1026
+ font-weight: 700;
1027
+ font-size: 14px;
1028
+ letter-spacing: 0.12em;
1029
+ text-transform: uppercase;
1030
+ color: var(--ink);
1031
+ text-align: center;
1032
+ padding: 6px 0;
1033
+ border-top: 2px solid var(--ink);
1034
+ border-bottom: 1px solid var(--ink);
1035
+ }
1036
+ .vogue-side-list {
1037
+ list-style: none;
1038
+ margin: 0;
1039
+ padding: 0;
1040
+ display: flex;
1041
+ flex-direction: column;
1042
+ }
1043
+ .vogue-side-list li {
1044
+ padding: 10px 0;
1045
+ border-bottom: 1px solid var(--rule);
1046
+ font-family: var(--serif);
1047
+ font-size: 12.5px;
1048
+ line-height: 1.55;
1049
+ color: var(--ink-soft);
1050
+ }
1051
+ .vogue-side-list li:last-child { border-bottom: 0; }
1052
+ .vogue-side-list li b {
1053
+ display: block;
1054
+ font-family: "Playfair Display", serif;
1055
+ font-style: italic;
1056
+ font-size: 13.5px;
1057
+ font-weight: 700;
1058
+ color: var(--ink);
1059
+ margin-bottom: 4px;
1060
+ }
1061
+
1062
+ /* ═══════════════════════════════════════════════════════════════
1063
+ Shared · page footer · states · print
1064
+ ═══════════════════════════════════════════════════════════════ */
1065
+
1066
+ .mag-page-foot {
1067
+ display: grid;
1068
+ grid-template-columns: 1fr auto 1fr;
1069
+ align-items: baseline;
1070
+ gap: 14px;
1071
+ padding-top: 14px;
1072
+ margin-top: 18px;
1073
+ border-top: 1px solid var(--rule);
1074
+ font-family: var(--mono);
1075
+ font-size: 10px;
1076
+ letter-spacing: 0.08em;
1077
+ text-transform: uppercase;
1078
+ color: var(--ink-faint);
1079
+ }
1080
+ .mag-page-foot-l { text-align: left; }
1081
+ .mag-page-foot-c { text-align: center; color: var(--ink-mid); font-weight: 700; }
1082
+ .mag-page-foot-r { text-align: right; font-style: italic; text-transform: none; }
1083
+
1084
+ .mag-state {
1085
+ max-width: 560px;
1086
+ margin: 80px auto;
1087
+ padding: 48px 36px;
1088
+ text-align: center;
1089
+ background: var(--paper);
1090
+ box-shadow: var(--shadow-page);
1091
+ }
1092
+ .mag-state-mark {
1093
+ font-family: var(--mono);
1094
+ font-size: 10px;
1095
+ letter-spacing: 0.18em;
1096
+ text-transform: uppercase;
1097
+ color: var(--ink);
1098
+ border: 1px solid var(--ink);
1099
+ padding: 5px 14px;
1100
+ display: inline-block;
1101
+ margin-bottom: 16px;
1102
+ font-weight: 700;
1103
+ }
1104
+ .mag-state-title {
1105
+ font-family: var(--serif-display);
1106
+ font-size: 24px;
1107
+ font-weight: 700;
1108
+ color: var(--ink);
1109
+ margin-bottom: 10px;
1110
+ line-height: 1.2;
1111
+ text-transform: uppercase;
1112
+ letter-spacing: 0.02em;
1113
+ }
1114
+ .mag-state-body {
1115
+ font-family: var(--serif);
1116
+ font-size: 13.5px;
1117
+ color: var(--ink-soft);
1118
+ line-height: 1.6;
1119
+ }
1120
+
1121
+ @media print {
1122
+ .mag-top-bar { display: none; }
1123
+ body, html { background: white; }
1124
+ .mag-doc { max-width: none; padding: 0; margin: 0; }
1125
+ .mag-page {
1126
+ box-shadow: none;
1127
+ margin: 0;
1128
+ padding: 16mm 18mm;
1129
+ page-break-after: always;
1130
+ }
1131
+ .mag-page:last-child { page-break-after: auto; }
1132
+ }
1133
+ </style>
1134
+ </head>
1135
+ <body>
1136
+
1137
+ <header class="mag-top-bar" data-mag-chrome>
1138
+ <a href="/" class="mag-crumb">PrivateBoard <span class="mag-crumb-accent">· magazine</span></a>
1139
+ <div class="mag-actions">
1140
+ <span class="mag-variant-badge" data-mag-variant-badge>Loading…</span>
1141
+ <nav class="mag-page-nav" aria-label="Pages">
1142
+ <a href="#mag-page-1">Cover</a>
1143
+ <a href="#mag-page-2">Inside</a>
1144
+ </nav>
1145
+ <button type="button" class="mag-btn" data-mag-png>
1146
+ <span class="glyph">↓</span>PNG
1147
+ </button>
1148
+ <button type="button" class="mag-btn" data-mag-print>
1149
+ <span class="glyph">↓</span>PDF
1150
+ </button>
1151
+ </div>
1152
+ </header>
1153
+
1154
+ <main data-mag-root>
1155
+ <div class="mag-state">
1156
+ <div class="mag-state-mark">Loading</div>
1157
+ <div class="mag-state-title">Loading magazine…</div>
1158
+ <div class="mag-state-body">Fetching the editorial spread for this brief.</div>
1159
+ </div>
1160
+ </main>
1161
+
1162
+ <script>
1163
+ /* ──────────────────────────────────────────────────────────────────
1164
+ Magazine renderer · two distinct templates (GQ / Vogue) picked
1165
+ deterministically per brief id. Each renders 2 pages (Cover +
1166
+ Inside Feature) so the report reads as a magazine, not a
1167
+ single sheet.
1168
+ ────────────────────────────────────────────────────────────── */
1169
+ (function () {
1170
+ const params = new URLSearchParams(location.search);
1171
+ const briefId = (params.get("b") || "").trim();
1172
+ const roomId = (params.get("r") || "").trim();
1173
+ const root = document.querySelector("[data-mag-root]");
1174
+ const variantBadge = document.querySelector("[data-mag-variant-badge]");
1175
+
1176
+ function escape(s) {
1177
+ return String(s == null ? "" : s)
1178
+ .replace(/&/g, "&amp;")
1179
+ .replace(/</g, "&lt;")
1180
+ .replace(/>/g, "&gt;")
1181
+ .replace(/"/g, "&quot;")
1182
+ .replace(/'/g, "&#39;");
1183
+ }
1184
+
1185
+ function showState(mark, title, body) {
1186
+ root.innerHTML = `
1187
+ <div class="mag-state">
1188
+ <div class="mag-state-mark">${escape(mark)}</div>
1189
+ <div class="mag-state-title">${escape(title)}</div>
1190
+ <div class="mag-state-body">${escape(body)}</div>
1191
+ </div>`;
1192
+ }
1193
+
1194
+ async function loadBrief() {
1195
+ let url;
1196
+ if (briefId) url = `/api/briefs/${encodeURIComponent(briefId)}`;
1197
+ else if (roomId) url = `/api/rooms/${encodeURIComponent(roomId)}/brief`;
1198
+ else { showState("Missing query", "No brief specified", "Add ?b=<briefId> or ?r=<roomId> to the URL."); return null; }
1199
+ const res = await fetch(url);
1200
+ if (!res.ok) {
1201
+ const e = await res.json().catch(() => ({}));
1202
+ showState("Not found", "Brief not found", e.error || "The requested brief doesn't exist or is no longer available.");
1203
+ return null;
1204
+ }
1205
+ return await res.json();
1206
+ }
1207
+
1208
+ /** Pick "gq" or "vogue" deterministically from the brief id ·
1209
+ * same brief id always renders the same template. The
1210
+ * `?v=gq|vogue` URL parameter forces a specific template. */
1211
+ function pickVariant(id) {
1212
+ const force = (params.get("v") || "").trim().toLowerCase();
1213
+ if (force === "gq" || force === "vogue") return force;
1214
+ const s = String(id || "");
1215
+ if (!s) return "gq";
1216
+ let h = 0;
1217
+ for (let i = 0; i < s.length; i++) {
1218
+ h = ((h << 5) - h) + s.charCodeAt(i);
1219
+ h |= 0;
1220
+ }
1221
+ return (Math.abs(h) % 2) === 0 ? "gq" : "vogue";
1222
+ }
1223
+
1224
+ function formatDate(footerTag) {
1225
+ const months = ["January","February","March","April","May","June","July","August","September","October","November","December"];
1226
+ const days = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
1227
+ const m = String(footerTag || "").match(/(\d{4})-(\d{2})-(\d{2})/);
1228
+ const target = m ? new Date(`${m[1]}-${m[2]}-${m[3]}T00:00:00`) : new Date();
1229
+ const d = isNaN(target.getTime()) ? new Date() : target;
1230
+ return {
1231
+ weekday: days[d.getDay()],
1232
+ long: `${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`,
1233
+ monthYear: `${months[d.getMonth()]} ${d.getFullYear()}`,
1234
+ year: String(d.getFullYear()),
1235
+ };
1236
+ }
1237
+
1238
+ /** Split a "Heading: body." style verification bullet on the
1239
+ * first colon. Falls back to em-dash / middle-dot, then to
1240
+ * whole-bullet-as-heading. */
1241
+ function splitHeading(raw) {
1242
+ const s = String(raw || "").trim();
1243
+ if (!s) return { heading: "", body: "" };
1244
+ const seps = [": ", " · ", " — ", " - "];
1245
+ for (const sep of seps) {
1246
+ const idx = s.indexOf(sep);
1247
+ if (idx > 0 && idx < 60) {
1248
+ return { heading: s.slice(0, idx).trim(), body: s.slice(idx + sep.length).trim() };
1249
+ }
1250
+ }
1251
+ return { heading: s, body: "" };
1252
+ }
1253
+
1254
+ /** Strict stat picker · only short numeric callouts. */
1255
+ function pickStat(m) {
1256
+ const ms = m.milestones || [];
1257
+ const hit = ms.find((x) => x && x.callout && String(x.callout).trim().length > 0 && String(x.callout).trim().length <= 14);
1258
+ if (hit) {
1259
+ return { num: String(hit.callout).trim(), cap: hit.title || hit.period || "" };
1260
+ }
1261
+ return null;
1262
+ }
1263
+
1264
+ function pageFoot(pageNum, totalPages, briefIdShort) {
1265
+ return `
1266
+ <div class="mag-page-foot">
1267
+ <span class="mag-page-foot-l">${escape(briefIdShort)}</span>
1268
+ <span class="mag-page-foot-c">Page ${pageNum} of ${totalPages}</span>
1269
+ <span class="mag-page-foot-r">privateboard.ai</span>
1270
+ </div>`;
1271
+ }
1272
+
1273
+ /* ═════════════════════════════════════════════════════════════
1274
+ GQ · The Boardroom · bold modern editorial
1275
+ ═════════════════════════════════════════════════════════════ */
1276
+
1277
+ function renderGqCover(brief, parts) {
1278
+ const { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
1279
+
1280
+ const coverPoints = [
1281
+ { num: "01", title: ms0.title || "Top story", page: "P. 02" },
1282
+ { num: "02", title: ms1.title || "On the markets", page: "P. 02" },
1283
+ { num: "03", title: ms2.title || "Late dispatches", page: "P. 02" },
1284
+ ].filter((p) => p.title).map((p) => `
1285
+ <li>
1286
+ <b>${escape(p.num)}</b>
1287
+ <span>${escape(p.title)}</span>
1288
+ <span class="pg">${escape(p.page)}</span>
1289
+ </li>
1290
+ `).join("");
1291
+
1292
+ return `
1293
+ <section class="mag-page" id="mag-page-1">
1294
+ <div class="gq-cover-top">
1295
+ <span>The Boardroom · Issue ${escape(briefIdShort.replace(/^#/, ""))}</span>
1296
+ <span>${escape(dateInfo.monthYear)}</span>
1297
+ </div>
1298
+
1299
+ <h1 class="gq-cover-logo">Boardroom</h1>
1300
+
1301
+ <div class="gq-cover-strip">
1302
+ <span><em>★</em> Exclusive</span>
1303
+ <span>Reported, written &amp; edited by the chair</span>
1304
+ <span><em>★</em> ${escape(dateInfo.year)} edition</span>
1305
+ </div>
1306
+
1307
+ <div class="gq-cover-grid">
1308
+ <!-- Typographic "portrait" panel · solid dark, editorial quote -->
1309
+ <aside class="gq-cover-portrait">
1310
+ <div class="gq-cover-portrait-eye">— Featured</div>
1311
+ <div>
1312
+ <div class="gq-cover-portrait-mark">"</div>
1313
+ <div class="gq-cover-portrait-text">${escape(m.conclusion || m.kicker || m.title || "")}</div>
1314
+ </div>
1315
+ <div class="gq-cover-portrait-cite">— By the chair</div>
1316
+ </aside>
1317
+
1318
+ <!-- Cover lines stacked · main headline + featured items -->
1319
+ <div class="gq-cover-lines">
1320
+ <span class="gq-cover-flag">The big read</span>
1321
+ <h2 class="gq-cover-headline">${escape(m.title || "")}</h2>
1322
+ ${m.kicker ? `<p class="gq-cover-deck">${escape(m.kicker)}</p>` : ""}
1323
+ ${coverPoints ? `<ul class="gq-cover-points">${coverPoints}</ul>` : ""}
1324
+ </div>
1325
+ </div>
1326
+
1327
+ ${pageFoot(1, totalPages, briefIdShort)}
1328
+ </section>`;
1329
+ }
1330
+
1331
+ function renderGqInside(brief, parts) {
1332
+ const { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
1333
+ const tpAll = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
1334
+ const stat = pickStat(m);
1335
+ const verifs = (m.verification && Array.isArray(m.verification.bullets)) ? m.verification.bullets : [];
1336
+
1337
+ // Body cell · pack with kicker lead + 3 milestone bodies +
1338
+ // mid pull-quote + closing conclusion line so the body cell
1339
+ // matches the sidebar's height instead of trailing off short.
1340
+ const bodyHead = m.kicker ? `<p style="font-style:italic;color:var(--ink-mid);font-size:14.5px;margin-bottom:10px;">${escape(m.kicker)}</p>` : "";
1341
+ const bodyParas = [];
1342
+ if (ms0.body) bodyParas.push(`<p>${escape(ms0.body)}</p>`);
1343
+ if (ms1.body) bodyParas.push(`<p>${escape(ms1.body)}</p>`);
1344
+ if (ms2.body) bodyParas.push(`<p>${escape(ms2.body)}</p>`);
1345
+ const bodyClose = m.conclusion ? `<p style="font-weight:700;color:var(--ink);margin-top:6px;">${escape(m.conclusion)}</p>` : "";
1346
+
1347
+ const verifsHtml = verifs.slice(0, 5).map((b) => {
1348
+ const p = splitHeading(b);
1349
+ return `<li>${p.body
1350
+ ? `<b>${escape(p.heading)}</b>${escape(p.body)}`
1351
+ : escape(p.heading)}</li>`;
1352
+ }).join("");
1353
+
1354
+ return `
1355
+ <section class="mag-page" id="mag-page-2">
1356
+ <div class="gq-running">
1357
+ <span class="gq-running-brand">Boardroom</span>
1358
+ <span class="gq-running-meta">${escape(dateInfo.monthYear)} · The big read</span>
1359
+ <span class="gq-running-page">P. 02</span>
1360
+ </div>
1361
+
1362
+ <span class="gq-feature-dept">Feature · ${escape(ms0.period || "Strategy")}</span>
1363
+ <h2 class="gq-feature-title">${escape(m.title || "")}</h2>
1364
+ ${m.kicker ? `<p class="gq-feature-deck">${escape(m.kicker)}</p>` : ""}
1365
+ <div class="gq-feature-byline">By <em>The Chair</em> · Edited from the boardroom</div>
1366
+
1367
+ <div class="gq-inside-grid">
1368
+ <div>
1369
+ ${bodyParas.length ? `<div class="gq-feature-body has-drop">${bodyParas.join("")}${bodyClose}</div>` : ""}
1370
+ ${tpAll[0] ? `
1371
+ <blockquote class="gq-pull">
1372
+ <div class="gq-pull-text">"${escape(tpAll[0])}"</div>
1373
+ <div class="gq-pull-cite">— From the editorial</div>
1374
+ </blockquote>` : ""}
1375
+ ${tpAll[2] ? `
1376
+ <p style="font-family:var(--serif);font-size:13.5px;line-height:1.62;color:var(--ink-soft);margin-top:10px;text-align:justify;">${escape(tpAll[2])}</p>` : ""}
1377
+ </div>
1378
+
1379
+ <aside class="gq-side">
1380
+ <span class="gq-side-flag">More from the room</span>
1381
+ ${verifsHtml ? `<ul class="gq-side-list">${verifsHtml}</ul>` : ""}
1382
+ ${stat ? `
1383
+ <div class="gq-stat">
1384
+ <div class="gq-stat-eye">By the numbers</div>
1385
+ <div class="gq-stat-num">${escape(stat.num)}</div>
1386
+ <div class="gq-stat-cap">${escape(stat.cap)}</div>
1387
+ </div>` : ""}
1388
+ ${tpAll[1] ? `
1389
+ <blockquote class="gq-pull" style="margin: 0;">
1390
+ <div class="gq-pull-text">"${escape(tpAll[1])}"</div>
1391
+ <div class="gq-pull-cite">— Boardroom note</div>
1392
+ </blockquote>` : ""}
1393
+ </aside>
1394
+ </div>
1395
+
1396
+ ${pageFoot(2, totalPages, briefIdShort)}
1397
+ </section>`;
1398
+ }
1399
+
1400
+ /* ═════════════════════════════════════════════════════════════
1401
+ VOGUE · BOARDROOM · refined Didot-elegance
1402
+ ═════════════════════════════════════════════════════════════ */
1403
+
1404
+ function renderVogueCover(brief, parts) {
1405
+ const { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
1406
+
1407
+ const coverLines = [
1408
+ { text: ms0.title, page: "P. II" },
1409
+ { text: ms1.title, page: "P. II" },
1410
+ { text: ms2.title, page: "P. II" },
1411
+ ].filter((it) => it.text).map((it, i) => `
1412
+ <li>
1413
+ <span>${i === 0 ? `<em>${escape(it.text)}</em>` : escape(it.text)}</span>
1414
+ <span class="pg">${escape(it.page)}</span>
1415
+ </li>`).join("");
1416
+
1417
+ return `
1418
+ <section class="mag-page" id="mag-page-1">
1419
+ <div class="vogue-cover-tagline">"Intelligence from the boardroom"</div>
1420
+ <h1 class="vogue-cover-logo">Boardroom</h1>
1421
+ <div class="vogue-cover-strip">
1422
+ <span>VOL. XII</span>
1423
+ <span class="star">★</span>
1424
+ <span><em>Issue</em> ${escape(briefIdShort.replace(/^#/, ""))}</span>
1425
+ <span class="star">★</span>
1426
+ <span>${escape(dateInfo.monthYear)}</span>
1427
+ <span class="star">★</span>
1428
+ <span>$14.00</span>
1429
+ </div>
1430
+
1431
+ <div class="vogue-cover-content">
1432
+ <div class="vogue-cover-left">
1433
+ <div class="vogue-cover-eye">Featured in this issue</div>
1434
+ <h2 class="vogue-cover-title">${escape(m.title || "")}</h2>
1435
+ ${m.kicker ? `<p class="vogue-cover-deck">${escape(m.kicker)}</p>` : ""}
1436
+ <div class="vogue-cover-byline">Reported by <em>The Chair</em></div>
1437
+ ${coverLines ? `<ul class="vogue-cover-lines">${coverLines}</ul>` : ""}
1438
+ </div>
1439
+
1440
+ <aside class="vogue-cover-portrait">
1441
+ <div class="vogue-cover-portrait-eye">The Editorial</div>
1442
+ <div class="vogue-cover-portrait-mark">"</div>
1443
+ <div class="vogue-cover-portrait-text">${escape(m.conclusion || m.kicker || m.title || "")}</div>
1444
+ <div class="vogue-cover-portrait-cite">— Bottom line</div>
1445
+ </aside>
1446
+ </div>
1447
+
1448
+ ${pageFoot(1, totalPages, briefIdShort)}
1449
+ </section>`;
1450
+ }
1451
+
1452
+ function renderVogueInside(brief, parts) {
1453
+ const { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages } = parts;
1454
+ const tpAll = (m.talkingPoints && Array.isArray(m.talkingPoints.bullets)) ? m.talkingPoints.bullets : [];
1455
+ const stat = pickStat(m);
1456
+ const verifs = (m.verification && Array.isArray(m.verification.bullets)) ? m.verification.bullets : [];
1457
+
1458
+ // Body cell · pack with milestone bodies + closing conclusion
1459
+ // line so the body matches the sidebar's height. The kicker
1460
+ // already shows above the body as the deck · so don't repeat
1461
+ // it inside (would feel like a stutter).
1462
+ const bodyParas = [];
1463
+ if (ms0.body) bodyParas.push(`<p>${escape(ms0.body)}</p>`);
1464
+ if (ms1.body) bodyParas.push(`<p>${escape(ms1.body)}</p>`);
1465
+ if (ms2.body) bodyParas.push(`<p>${escape(ms2.body)}</p>`);
1466
+ const bodyClose = m.conclusion ? `<p style="font-style:italic;font-weight:700;color:var(--ink);margin-top:8px;">${escape(m.conclusion)}</p>` : "";
1467
+
1468
+ const verifsHtml = verifs.slice(0, 5).map((b) => {
1469
+ const p = splitHeading(b);
1470
+ return `<li>${p.body
1471
+ ? `<b>${escape(p.heading)}</b>${escape(p.body)}`
1472
+ : escape(p.heading)}</li>`;
1473
+ }).join("");
1474
+
1475
+ return `
1476
+ <section class="mag-page" id="mag-page-2">
1477
+ <div class="vogue-running">
1478
+ <span class="vogue-running-l">Boardroom</span>
1479
+ <span class="vogue-running-c">${escape(dateInfo.monthYear)}</span>
1480
+ <span class="vogue-running-r">Page II</span>
1481
+ </div>
1482
+
1483
+ <div class="vogue-feature-eye">The Feature</div>
1484
+ <h2 class="vogue-feature-title">${escape(m.title || "")}</h2>
1485
+ ${m.kicker ? `<p class="vogue-feature-deck">${escape(m.kicker)}</p>` : ""}
1486
+ <div class="vogue-feature-byline">Reported by <em>The Chair</em> · Edited from the boardroom</div>
1487
+
1488
+ <div class="vogue-inside-grid">
1489
+ <div>
1490
+ ${bodyParas.length ? `<div class="vogue-feature-body has-drop">${bodyParas.join("")}${bodyClose}</div>` : ""}
1491
+ ${tpAll[0] ? `
1492
+ <div class="vogue-pull">
1493
+ <div class="vogue-pull-text">"${escape(tpAll[0])}"</div>
1494
+ <div class="vogue-pull-cite">From the editorial</div>
1495
+ </div>` : ""}
1496
+ ${tpAll[2] ? `
1497
+ <p style="font-family:var(--serif);font-size:13.5px;line-height:1.65;color:var(--ink-soft);margin-top:8px;text-align:justify;text-indent:1.2em;">${escape(tpAll[2])}</p>` : ""}
1498
+ </div>
1499
+
1500
+ <aside class="vogue-side">
1501
+ <span class="vogue-side-flag">More from the room</span>
1502
+ ${verifsHtml ? `<ul class="vogue-side-list">${verifsHtml}</ul>` : ""}
1503
+ ${stat ? `
1504
+ <div class="vogue-stat">
1505
+ <div class="vogue-stat-eye">By the numbers</div>
1506
+ <div class="vogue-stat-num">${escape(stat.num)}</div>
1507
+ <div class="vogue-stat-cap">${escape(stat.cap)}</div>
1508
+ </div>` : ""}
1509
+ ${tpAll[1] ? `
1510
+ <div class="vogue-pull" style="margin: 0; max-width: none;">
1511
+ <div class="vogue-pull-text" style="font-size: 18px;">"${escape(tpAll[1])}"</div>
1512
+ <div class="vogue-pull-cite">Boardroom note</div>
1513
+ </div>` : ""}
1514
+ </aside>
1515
+ </div>
1516
+
1517
+ ${pageFoot(2, totalPages, briefIdShort)}
1518
+ </section>`;
1519
+ }
1520
+
1521
+ /* ═════════════════════════════════════════════════════════════ */
1522
+
1523
+ function render(brief) {
1524
+ if (brief.mode !== "magazine") {
1525
+ showState("Wrong mode", "This brief isn't a magazine",
1526
+ "Open it in the matching viewer instead. Magazine, newspaper, and research-note are separate output modes.");
1527
+ return;
1528
+ }
1529
+ const m = brief.bodyJson;
1530
+ if (!m || typeof m !== "object") {
1531
+ if (brief.isGenerating) {
1532
+ showState("Generating", "Magazine is still being prepared",
1533
+ "The chair is currently composing the issue. Refresh in a few seconds.");
1534
+ } else {
1535
+ showState("Empty", "This brief has no magazine data",
1536
+ "It may have failed to generate. Try regenerating from the room view.");
1537
+ }
1538
+ return;
1539
+ }
1540
+
1541
+ if (m.title) document.title = `${m.title} · Magazine`;
1542
+
1543
+ const variant = pickVariant(brief.id);
1544
+ document.body.setAttribute("data-mag-variant", variant);
1545
+ if (variantBadge) {
1546
+ variantBadge.textContent = variant === "vogue" ? "Vogue Edition" : "GQ Edition";
1547
+ }
1548
+
1549
+ const milestones = (m.milestones || []).slice(0, 3);
1550
+ const ms0 = milestones[0] || {};
1551
+ const ms1 = milestones[1] || {};
1552
+ const ms2 = milestones[2] || {};
1553
+ const dateInfo = formatDate(m.footerTag);
1554
+ const briefIdShort = brief.id ? `#${String(brief.id).slice(0, 10).toUpperCase()}` : "";
1555
+ const totalPages = 2;
1556
+
1557
+ const parts = { m, ms0, ms1, ms2, dateInfo, briefIdShort, totalPages };
1558
+
1559
+ const pages = variant === "vogue"
1560
+ ? [renderVogueCover(brief, parts), renderVogueInside(brief, parts)]
1561
+ : [renderGqCover(brief, parts), renderGqInside(brief, parts)];
1562
+
1563
+ root.innerHTML = `<article class="mag-doc" data-mag-paper>${pages.join("")}</article>`;
1564
+ }
1565
+
1566
+ /* ─── Export wiring · same PNG-as-PDF strategy ─────────── */
1567
+ let _h2iLoaded = null;
1568
+ async function ensureHtmlToImage() {
1569
+ if (window.htmlToImage) return;
1570
+ if (!_h2iLoaded) {
1571
+ _h2iLoaded = new Promise((res, rej) => {
1572
+ const s = document.createElement("script");
1573
+ s.src = "https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js";
1574
+ s.onload = res;
1575
+ s.onerror = rej;
1576
+ document.head.appendChild(s);
1577
+ });
1578
+ }
1579
+ await _h2iLoaded;
1580
+ }
1581
+
1582
+ async function captureMagPng() {
1583
+ const el = document.querySelector("[data-mag-paper]");
1584
+ if (!el) throw new Error("magazine doc not found");
1585
+ await ensureHtmlToImage();
1586
+ if (document.fonts && document.fonts.ready) {
1587
+ try { await document.fonts.ready; } catch { /* best-effort */ }
1588
+ }
1589
+ const width = Math.max(el.scrollWidth, el.offsetWidth, el.clientWidth);
1590
+ const height = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight);
1591
+ return window.htmlToImage.toPng(el, {
1592
+ pixelRatio: 2,
1593
+ backgroundColor: "#1A1A1A",
1594
+ cacheBust: true,
1595
+ width, height,
1596
+ canvasWidth: width, canvasHeight: height,
1597
+ style: { margin: "0", width: `${width}px`, height: `${height}px` },
1598
+ });
1599
+ }
1600
+
1601
+ function slugTitle() {
1602
+ return (document.title || "magazine").replace(/[^a-z0-9]+/gi, "-").slice(0, 60) || "magazine";
1603
+ }
1604
+
1605
+ async function exportPng() {
1606
+ try {
1607
+ const dataUrl = await captureMagPng();
1608
+ const a = document.createElement("a");
1609
+ a.download = `${slugTitle()}.png`;
1610
+ a.href = dataUrl;
1611
+ a.click();
1612
+ } catch (e) {
1613
+ console.warn("[magazine] PNG export failed:", e);
1614
+ alert("PNG export failed · see browser console.");
1615
+ }
1616
+ }
1617
+
1618
+ async function exportPdf() {
1619
+ try {
1620
+ const dataUrl = await captureMagPng();
1621
+ const win = window.open("", "_blank", "width=1024,height=720");
1622
+ if (!win) {
1623
+ alert("PDF export needs to open a new window — please allow popups for this site.");
1624
+ return;
1625
+ }
1626
+ const slug = slugTitle();
1627
+ win.document.open();
1628
+ win.document.write(`<!doctype html><html lang="en"><head>
1629
+ <meta charset="utf-8">
1630
+ <title>${slug}</title>
1631
+ <style>
1632
+ @page { size: auto; margin: 10mm; }
1633
+ * { box-sizing: border-box; margin: 0; padding: 0; }
1634
+ html, body { background: #FFFFFF; }
1635
+ body { display: flex; align-items: flex-start; justify-content: center; padding: 20px; min-height: 100vh; }
1636
+ img { display: block; width: 100%; max-width: 880px; height: auto; box-shadow: 0 0 24px rgba(0, 0, 0, 0.08); }
1637
+ @media print {
1638
+ body { padding: 0; }
1639
+ img { box-shadow: none; max-width: none; width: 100%; }
1640
+ }
1641
+ .hint {
1642
+ position: fixed; top: 12px; left: 12px;
1643
+ font-family: ui-monospace, "SF Mono", Menlo, monospace;
1644
+ font-size: 11px; color: #8E8B83;
1645
+ background: rgba(255,255,255,0.9); padding: 6px 10px;
1646
+ border: 1px solid #E5E2DA;
1647
+ }
1648
+ @media print { .hint { display: none; } }
1649
+ </style>
1650
+ </head><body>
1651
+ <div class="hint">// press <kbd>⌘P</kbd> / <kbd>Ctrl+P</kbd> · save as PDF</div>
1652
+ <img alt="${slug}" src="${dataUrl}">
1653
+ <script>
1654
+ (function () {
1655
+ var img = document.querySelector("img");
1656
+ function go() { setTimeout(function () { window.print(); }, 200); }
1657
+ if (img && img.complete) { go(); }
1658
+ else if (img) { img.addEventListener("load", go, { once: true }); }
1659
+ })();
1660
+ <\/script>
1661
+ </body></html>`);
1662
+ win.document.close();
1663
+ } catch (e) {
1664
+ console.warn("[magazine] PDF export failed:", e);
1665
+ alert("PDF export failed · see browser console.");
1666
+ }
1667
+ }
1668
+
1669
+ document.addEventListener("click", (e) => {
1670
+ if (e.target.closest("[data-mag-png]")) { e.preventDefault(); exportPng(); return; }
1671
+ if (e.target.closest("[data-mag-print]")) { e.preventDefault(); exportPdf(); return; }
1672
+ });
1673
+
1674
+ /* ─── Boot ──────────────────────────────────────────────────── */
1675
+ loadBrief().then((brief) => {
1676
+ if (brief) render(brief);
1677
+ }).catch((e) => {
1678
+ console.error("[magazine] load failed:", e);
1679
+ showState("Error", "Couldn't load this brief", e instanceof Error ? e.message : String(e));
1680
+ });
1681
+ })();
1682
+ </script>
1683
+
1684
+ </body>
1685
+ </html>