privateboard 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,75 +5,57 @@
5
5
  <meta name="viewport" content="width=900, initial-scale=1">
6
6
  <title>Magazine · PrivateBoard</title>
7
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">
8
14
  <style>
9
15
  /* ═══════════════════════════════════════════════════════════════════
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
- ─────────────────────────────────────────────────────────────────── */
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
+ ═══════════════════════════════════════════════════════════════════ */
24
29
  :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);
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);
73
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;
74
57
  --serif: "Charter", "Source Serif Pro", "Iowan Old Style",
75
- "Tiempos Headline", "Source Han Serif SC", Georgia,
76
- "Songti SC", "STSong", serif;
58
+ "Source Han Serif SC", Georgia, "Songti SC", "STSong", serif;
77
59
  --sans: "Inter", "Helvetica Neue", -apple-system, BlinkMacSystemFont,
78
60
  "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
79
61
  "Source Han Sans CN", "Noto Sans CJK SC", sans-serif;
@@ -81,11 +63,25 @@
81
63
  "PingFang SC", "Source Han Sans CN", monospace;
82
64
  }
83
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
+
84
80
  * { box-sizing: border-box; margin: 0; padding: 0; }
85
81
  html, body {
86
- background: #ECE6D5;
82
+ background: var(--bg);
87
83
  color: var(--ink);
88
- font-family: var(--sans);
84
+ font-family: var(--serif);
89
85
  font-size: 14px;
90
86
  line-height: 1.55;
91
87
  -webkit-font-smoothing: antialiased;
@@ -93,695 +89,1046 @@
93
89
  min-height: 100vh;
94
90
  }
95
91
 
96
- /* ─── Top chrome · brand crumb + actions ────────────────────────── */
92
+ /* ─── Top chrome ───────────────────────────────────────────────── */
97
93
  .mag-top-bar {
98
94
  display: flex;
99
95
  align-items: center;
100
96
  justify-content: space-between;
101
97
  gap: 14px;
102
- padding: 18px 32px;
103
- background: rgba(236, 230, 213, 0.85);
98
+ padding: 14px 28px;
99
+ background: rgba(26, 26, 26, 0.92);
104
100
  backdrop-filter: blur(10px);
105
101
  -webkit-backdrop-filter: blur(10px);
106
- border-bottom: 1px solid var(--rule);
102
+ border-bottom: 1px solid rgba(245, 240, 232, 0.12);
107
103
  flex-wrap: wrap;
108
104
  position: sticky;
109
105
  top: 0;
110
106
  z-index: 10;
107
+ color: var(--paper);
111
108
  }
112
109
  .mag-crumb {
113
110
  display: inline-flex;
114
111
  align-items: center;
115
112
  gap: 12px;
116
- font-family: var(--serif);
113
+ font-family: var(--serif-display);
117
114
  font-size: 16px;
118
- font-weight: 500;
119
- color: var(--ink);
115
+ font-weight: 700;
116
+ color: var(--paper);
120
117
  text-decoration: none;
121
118
  }
122
119
  .mag-crumb::before {
123
120
  content: "";
124
- width: 10px;
125
- height: 10px;
126
- background: linear-gradient(135deg, var(--accent) 0%, var(--accent-deep) 100%);
127
- border-radius: 50%;
121
+ width: 9px;
122
+ height: 9px;
123
+ background: var(--accent);
128
124
  flex: 0 0 auto;
129
- box-shadow: 0 0 0 3px rgba(111, 181, 114, 0.22);
125
+ border-radius: 50%;
130
126
  }
131
127
  .mag-crumb-accent {
132
128
  color: var(--ink-faint);
133
129
  font-style: italic;
134
130
  font-weight: 400;
135
131
  }
136
- .mag-actions { display: flex; gap: 8px; }
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
+ }
137
164
  .mag-btn {
138
165
  font-family: var(--mono);
139
- font-size: 11px;
166
+ font-size: 10.5px;
140
167
  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);
168
+ padding: 7px 12px;
169
+ background: transparent;
170
+ border: 1px solid rgba(245, 240, 232, 0.30);
171
+ color: var(--paper);
146
172
  cursor: pointer;
147
173
  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);
174
+ text-transform: uppercase;
175
+ font-weight: 600;
176
+ transition: background 0.15s, color 0.15s;
157
177
  }
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); }
178
+ .mag-btn:hover { background: var(--paper); color: var(--bg); }
179
+ .mag-btn .glyph { margin-right: 4px; }
161
180
 
162
- /* ─── Doc · the magazine sheet ─────────────────────────────────── */
181
+ /* ─── Doc · stack of magazine pages ─────────────────────────── */
163
182
  .mag-doc {
164
- max-width: 880px;
165
- margin: 28px auto;
166
- background: var(--paper);
167
- box-shadow: var(--shadow-card);
168
- overflow: hidden;
183
+ max-width: 920px;
184
+ margin: 0 auto;
185
+ padding: 28px 12px 56px;
169
186
  }
170
-
171
- /* ─── Masthead band · creator + display title + deck + issue ──── */
172
- .mag-masthead {
173
- padding: 32px 44px 24px;
187
+ .mag-page {
174
188
  background: var(--paper);
189
+ padding: 32px 38px 36px;
190
+ margin-bottom: 24px;
191
+ box-shadow: var(--shadow-page);
175
192
  position: relative;
176
193
  }
177
- .mag-masthead-top {
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 {
178
202
  display: flex;
179
- align-items: baseline;
180
203
  justify-content: space-between;
181
- gap: 24px;
182
- margin-bottom: 14px;
183
- }
184
- .mag-byline {
185
- font-family: var(--mono);
186
- font-size: 11px;
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;
187
210
  letter-spacing: 0.18em;
188
211
  text-transform: uppercase;
189
- color: var(--ink-mid);
190
- font-weight: 600;
212
+ color: var(--ink);
191
213
  }
192
- .mag-issue {
193
- font-family: var(--mono);
194
- font-size: 10.5px;
195
- letter-spacing: 0.16em;
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;
196
220
  text-transform: uppercase;
197
- color: var(--ink-faint);
198
- text-align: right;
199
- line-height: 1.5;
200
- white-space: pre-line;
221
+ color: var(--ink);
222
+ text-align: center;
223
+ margin: 14px 0 12px;
224
+ word-break: break-word;
225
+ overflow-wrap: anywhere;
201
226
  }
202
- .mag-title {
203
- font-family: var(--serif);
204
- font-size: 48px;
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;
205
235
  font-weight: 700;
206
- line-height: 1.05;
207
- letter-spacing: -0.022em;
236
+ letter-spacing: 0.14em;
237
+ text-transform: uppercase;
208
238
  color: var(--ink);
209
- margin: 0 0 12px;
210
- /* Magazine cover lines need to feel printed · slight optical
211
- tightening keeps the gravity. */
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;
212
265
  }
213
- .mag-deck {
266
+ .gq-cover-portrait-eye {
214
267
  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;
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;
220
287
  letter-spacing: -0.005em;
288
+ color: var(--paper);
221
289
  }
222
- .mag-masthead-rule {
223
- height: 1px;
224
- background: var(--rule-strong);
225
- margin: 22px 44px 0;
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);
226
297
  }
227
- .mag-masthead + .mag-masthead-rule { /* selector parity in case of placement */ }
228
298
 
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);
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;
236
327
  }
237
- @media (max-width: 760px) {
238
- .mag-hero { grid-template-columns: 1fr; gap: 24px; padding: 28px 28px 32px; }
328
+ .gq-cover-headline em {
329
+ color: var(--accent);
330
+ font-style: normal;
239
331
  }
240
-
241
- /* Left feature · outline numeral + section title + body */
242
- .mag-feature {
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;
243
343
  display: flex;
244
344
  flex-direction: column;
245
- align-items: flex-start;
246
345
  gap: 8px;
247
- padding-top: 6px;
346
+ border-top: 2px solid var(--ink);
347
+ padding-top: 12px;
248
348
  }
249
- .mag-feature-kicker {
250
- font-family: var(--serif);
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;
251
361
  font-size: 14px;
252
- color: var(--ink-soft);
253
- margin-bottom: 4px;
362
+ color: var(--accent);
363
+ letter-spacing: 0.04em;
254
364
  }
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;
365
+ .gq-cover-points li span:nth-child(2) {
366
+ font-family: var(--serif-display);
367
+ font-size: 16px;
270
368
  font-weight: 700;
271
- line-height: 1.15;
369
+ line-height: 1.25;
272
370
  color: var(--ink);
273
- letter-spacing: -0.012em;
274
371
  }
275
- .mag-feature-sub {
276
- font-family: var(--mono);
277
- font-size: 11px;
372
+ .gq-cover-points li .pg {
373
+ font-family: var(--sans);
374
+ font-size: 10px;
278
375
  letter-spacing: 0.14em;
279
376
  text-transform: uppercase;
280
377
  color: var(--ink-mid);
281
- margin-bottom: 12px;
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;
282
410
  font-weight: 600;
411
+ color: var(--ink-mid);
412
+ letter-spacing: 0.14em;
283
413
  }
284
- .mag-feature-body {
414
+ .gq-running-page {
285
415
  font-family: var(--sans);
286
- font-size: 13px;
287
- line-height: 1.65;
288
- color: var(--ink-soft);
289
- max-width: 240px;
416
+ font-weight: 800;
417
+ color: var(--accent);
290
418
  }
291
419
 
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;
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;
301
431
  }
302
- @media (max-width: 760px) {
303
- .mag-cards { grid-template-columns: 1fr; }
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;
304
441
  }
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;
442
+ @media (max-width: 720px) {
443
+ .gq-feature-title { font-size: 34px; }
314
444
  }
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 {
445
+ .gq-feature-deck {
320
446
  font-family: var(--serif);
321
- font-size: 26px;
322
- font-weight: 700;
323
447
  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
448
  font-size: 17px;
331
- font-weight: 700;
332
- line-height: 1.25;
333
- color: var(--ink);
334
- letter-spacing: -0.01em;
449
+ line-height: 1.45;
450
+ color: var(--ink-soft);
451
+ margin-bottom: 12px;
452
+ max-width: 720px;
335
453
  }
336
- .mag-card-sub {
454
+ .gq-feature-byline {
337
455
  font-family: var(--sans);
338
- font-size: 12px;
339
- line-height: 1.35;
456
+ font-size: 11px;
457
+ letter-spacing: 0.1em;
458
+ text-transform: uppercase;
340
459
  color: var(--ink-mid);
341
- font-weight: 500;
342
- letter-spacing: -0.005em;
343
- margin-bottom: 4px;
460
+ font-weight: 700;
461
+ margin-bottom: 14px;
462
+ padding-bottom: 12px;
463
+ border-bottom: 1px solid var(--rule);
344
464
  }
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;
465
+ .gq-feature-byline em {
466
+ font-style: italic;
359
467
  color: var(--ink-soft);
360
- pointer-events: none;
468
+ text-transform: none;
469
+ letter-spacing: 0;
361
470
  }
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
471
 
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);
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;
373
483
  }
374
- .mag-setup-head {
375
- text-align: center;
376
- margin-bottom: 28px;
484
+ @media (max-width: 540px) {
485
+ .gq-feature-body { column-count: 1; }
377
486
  }
378
- .mag-setup-title {
379
- font-family: var(--serif);
380
- font-size: 26px;
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);
381
506
  font-weight: 700;
382
- line-height: 1.2;
507
+ font-size: 22px;
508
+ line-height: 1.28;
509
+ letter-spacing: -0.012em;
383
510
  color: var(--ink);
384
- letter-spacing: -0.014em;
385
- margin-bottom: 6px;
386
511
  }
387
- .mag-setup-sub {
388
- font-family: var(--mono);
389
- font-size: 11px;
512
+ .gq-pull-cite {
513
+ font-family: var(--sans);
514
+ font-size: 10px;
390
515
  letter-spacing: 0.18em;
391
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;
392
556
  color: var(--ink-mid);
393
- font-weight: 600;
394
557
  }
395
- .mag-setup-grid {
558
+
559
+ /* GQ inside grid · article + sidebar */
560
+ .gq-inside-grid {
396
561
  display: grid;
397
- grid-template-columns: 1fr 1fr 1fr;
562
+ grid-template-columns: minmax(0, 2.2fr) minmax(180px, 1fr);
398
563
  gap: 28px;
564
+ margin-top: 4px;
565
+ align-items: start;
399
566
  }
400
567
  @media (max-width: 720px) {
401
- .mag-setup-grid { grid-template-columns: 1fr; gap: 20px; }
568
+ .gq-inside-grid { grid-template-columns: 1fr; gap: 22px; }
402
569
  }
403
- .mag-step {
570
+ .gq-side {
404
571
  display: flex;
405
572
  flex-direction: column;
406
- gap: 8px;
573
+ gap: 14px;
574
+ padding-left: 24px;
575
+ border-left: 1px solid var(--rule);
407
576
  }
408
- .mag-step-glyph {
409
- width: 28px;
410
- height: 28px;
411
- color: var(--accent-deep);
412
- margin-bottom: 6px;
577
+ @media (max-width: 720px) {
578
+ .gq-side { border-left: 0; padding-left: 0; padding-top: 18px; border-top: 1px solid var(--rule); }
413
579
  }
414
- .mag-step-title {
580
+ .gq-side-flag {
415
581
  font-family: var(--sans);
416
- font-size: 14px;
417
- font-weight: 700;
418
- color: var(--ink);
419
- letter-spacing: -0.005em;
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;
420
590
  }
421
- .mag-step-body {
422
- font-family: var(--sans);
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);
423
602
  font-size: 12.5px;
424
- line-height: 1.6;
603
+ line-height: 1.55;
425
604
  color: var(--ink-soft);
426
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
+ }
427
617
 
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;
618
+ /* ═══════════════════════════════════════════════════════════════
619
+ VOGUE variant · BOARDROOM · refined Didot-elegance
620
+ ═══════════════════════════════════════════════════════════════ */
621
+
622
+ body[data-mag-variant="vogue"] .mag-page {
623
+ padding: 36px 44px 38px;
434
624
  }
435
- .mag-closer-head {
625
+
626
+ .vogue-cover-tagline {
627
+ font-family: var(--serif);
628
+ font-style: italic;
629
+ font-size: 12px;
630
+ color: var(--ink-mid);
436
631
  text-align: center;
437
- margin-bottom: 24px;
632
+ padding: 4px 0 10px;
633
+ letter-spacing: 0.04em;
438
634
  }
439
- .mag-closer-title {
440
- font-family: var(--serif);
441
- font-size: 30px;
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;
442
695
  font-weight: 700;
443
- color: var(--ink-inv);
444
- letter-spacing: -0.014em;
445
- margin-bottom: 6px;
446
- line-height: 1.2;
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;
447
703
  }
448
- .mag-closer-sub {
449
- font-family: var(--mono);
450
- font-size: 11px;
451
- letter-spacing: 0.18em;
452
- text-transform: uppercase;
704
+ .vogue-cover-title em {
705
+ font-style: italic;
453
706
  color: var(--accent);
454
- font-weight: 600;
455
707
  }
456
- .mag-closer-list {
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 {
457
731
  list-style: none;
458
- margin: 0 auto;
732
+ margin: 4px 0 0;
459
733
  padding: 0;
460
- max-width: 600px;
734
+ display: flex;
735
+ flex-direction: column;
736
+ gap: 0;
461
737
  }
462
- .mag-closer-list li {
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);
463
745
  display: grid;
464
- grid-template-columns: 22px 1fr;
465
- gap: 14px;
746
+ grid-template-columns: 1fr auto;
747
+ gap: 12px;
466
748
  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
749
  }
474
- .mag-closer-list li:last-child { border-bottom: 0; }
475
- .mag-closer-list li .mag-closer-glyph {
476
- width: 20px;
477
- height: 20px;
750
+ .vogue-cover-lines li:last-child { border-bottom: 0; }
751
+ .vogue-cover-lines li em {
752
+ font-style: italic;
478
753
  color: var(--accent);
479
- align-self: center;
754
+ font-weight: 700;
480
755
  }
481
- .mag-closer-list li .mag-closer-glyph svg {
482
- display: block;
483
- width: 100%;
484
- height: 100%;
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;
485
763
  }
486
- .mag-closer-list li b {
487
- color: var(--ink-inv);
488
- font-weight: 600;
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;
489
775
  }
490
- .mag-closer-list li .mag-closer-sep {
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;
491
782
  color: var(--accent);
492
- margin: 0 6px;
493
- font-weight: 500;
783
+ font-weight: 700;
494
784
  }
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;
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);
506
794
  }
507
- .mag-closer-stamp-pill {
508
- display: inline-flex;
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;
509
804
  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);
805
+ justify-content: center;
806
+ }
807
+ .vogue-cover-portrait-cite {
808
+ font-family: "Playfair Display", serif;
809
+ font-style: italic;
515
810
  font-size: 11px;
516
- font-weight: 600;
517
- letter-spacing: -0.005em;
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;
518
869
  }
519
-
520
- /* ─── States · loading / error / empty ─────────────────────────── */
521
- .mag-state {
522
- max-width: 560px;
523
- margin: 80px auto;
524
- padding: 48px 36px;
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);
525
884
  text-align: center;
526
- background: var(--surface);
527
- border-radius: var(--r-lg);
528
- box-shadow: var(--shadow-card);
885
+ margin-bottom: 12px;
886
+ max-width: 720px;
887
+ margin-left: auto;
888
+ margin-right: auto;
529
889
  }
530
- .mag-state-mark {
531
- font-family: var(--mono);
532
- font-size: 10px;
533
- letter-spacing: 0.18em;
890
+ .vogue-feature-byline {
891
+ font-family: "Playfair Display", serif;
892
+ font-style: italic;
893
+ font-size: 12px;
894
+ letter-spacing: 0.16em;
534
895
  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;
896
+ color: var(--ink-mid);
897
+ text-align: center;
898
+ margin-bottom: 18px;
899
+ padding-bottom: 14px;
900
+ border-bottom: 1px solid var(--rule);
542
901
  }
543
- .mag-state-title {
544
- font-family: var(--serif);
545
- font-size: 24px;
546
- font-weight: 600;
902
+ .vogue-feature-byline em {
903
+ font-style: italic;
547
904
  color: var(--ink);
548
- margin-bottom: 10px;
549
- line-height: 1.3;
550
- letter-spacing: -0.012em;
551
905
  }
552
- .mag-state-body {
553
- font-family: var(--sans);
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);
554
912
  font-size: 13.5px;
913
+ line-height: 1.65;
555
914
  color: var(--ink-soft);
556
- line-height: 1.6;
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;
557
931
  }
558
932
 
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
- ═════════════════════════════════════════════════════════════════ */
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); }
564
962
 
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;
963
+ /* Vogue stat / sidebar list */
964
+ .vogue-stat {
965
+ background: var(--paper-soft);
966
+ border: 1px solid var(--ink);
967
+ padding: 18px 18px;
571
968
  text-align: center;
572
- border-bottom: 1px solid var(--rule);
969
+ margin: 14px 0;
573
970
  }
574
- .mag-cover-quote-mark {
575
- font-family: var(--mono);
971
+ .vogue-stat-eye {
972
+ font-family: "Playfair Display", serif;
973
+ font-style: italic;
576
974
  font-size: 11px;
577
975
  letter-spacing: 0.18em;
578
976
  text-transform: uppercase;
579
- color: var(--accent-deep);
977
+ color: var(--ink-mid);
580
978
  font-weight: 700;
581
- margin-bottom: 14px;
979
+ padding-bottom: 8px;
980
+ border-bottom: 1px solid var(--rule);
981
+ margin-bottom: 10px;
582
982
  }
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;
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;
589
991
  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;
992
+ word-break: break-word;
993
+ overflow-wrap: anywhere;
602
994
  }
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; }
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;
607
1002
  }
608
1003
 
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 {
1004
+ .vogue-inside-grid {
613
1005
  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;
1006
+ grid-template-columns: minmax(0, 2.4fr) minmax(180px, 1fr);
1007
+ gap: 30px;
1008
+ margin-top: 4px;
1009
+ align-items: start;
619
1010
  }
620
- .mag-cards-mosaic > .mag-card-feature {
621
- grid-column: 1 / span 2;
622
- grid-row: 1 / span 2;
623
- padding: 28px 30px 30px;
1011
+ @media (max-width: 720px) {
1012
+ .vogue-inside-grid { grid-template-columns: 1fr; gap: 22px; }
624
1013
  }
625
- .mag-cards-mosaic > .mag-card-feature .mag-card-numeral {
626
- font-size: 60px;
627
- margin-bottom: 8px;
1014
+ .vogue-side {
1015
+ display: flex;
1016
+ flex-direction: column;
1017
+ gap: 14px;
1018
+ padding-left: 24px;
1019
+ border-left: 1px solid var(--rule);
628
1020
  }
629
- .mag-cards-mosaic > .mag-card-feature .mag-card-title {
630
- font-size: 26px;
631
- line-height: 1.2;
632
- margin-bottom: 6px;
1021
+ @media (max-width: 720px) {
1022
+ .vogue-side { border-left: 0; padding-left: 0; padding-top: 18px; border-top: 1px solid var(--rule); }
633
1023
  }
634
- .mag-cards-mosaic > .mag-card-feature .mag-card-body {
1024
+ .vogue-side-flag {
1025
+ font-family: "Playfair Display", serif;
1026
+ font-weight: 700;
635
1027
  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;
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);
647
1035
  }
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
- }
1036
+ .vogue-side-list {
1037
+ list-style: none;
1038
+ margin: 0;
1039
+ padding: 0;
1040
+ display: flex;
1041
+ flex-direction: column;
659
1042
  }
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;
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);
664
1050
  }
665
- @media (max-width: 720px) {
666
- .mag-setup-grid-2 { grid-template-columns: 1fr; gap: 20px; }
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;
667
1060
  }
668
1061
 
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
- ═════════════════════════════════════════════════════════════════ */
1062
+ /* ═══════════════════════════════════════════════════════════════
1063
+ Shared · page footer · states · print
1064
+ ═══════════════════════════════════════════════════════════════ */
674
1065
 
675
- /* Edition stamp · sits in the masthead corner instead of the
676
- issue date. Reads as a serial number stamp. */
677
- .mag-edition-stamp {
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);
678
1074
  font-family: var(--mono);
679
1075
  font-size: 10px;
680
- letter-spacing: 0.18em;
1076
+ letter-spacing: 0.08em;
681
1077
  text-transform: uppercase;
682
1078
  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
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; }
689
1083
 
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;
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);
704
1091
  }
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;
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;
710
1102
  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
1103
  }
721
- .mag-zine-title {
722
- font-family: var(--serif);
723
- font-size: 22px;
1104
+ .mag-state-title {
1105
+ font-family: var(--serif-display);
1106
+ font-size: 24px;
724
1107
  font-weight: 700;
725
- line-height: 1.2;
726
- letter-spacing: -0.012em;
727
1108
  color: var(--ink);
728
- }
729
- .mag-zine-sub {
730
- font-family: var(--sans);
731
- font-size: 12px;
732
- letter-spacing: 0.04em;
1109
+ margin-bottom: 10px;
1110
+ line-height: 1.2;
733
1111
  text-transform: uppercase;
734
- color: var(--ink-mid);
735
- font-weight: 600;
1112
+ letter-spacing: 0.02em;
736
1113
  }
737
- .mag-zine-body {
738
- font-family: var(--sans);
1114
+ .mag-state-body {
1115
+ font-family: var(--serif);
739
1116
  font-size: 13.5px;
740
- line-height: 1.6;
741
1117
  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; }
1118
+ line-height: 1.6;
766
1119
  }
767
1120
 
768
- /* ─── Print · drop top chrome, fit one page ───────────────────── */
769
1121
  @media print {
770
1122
  .mag-top-bar { display: none; }
771
1123
  body, html { background: white; }
772
- .mag-doc {
773
- max-width: none;
774
- margin: 0;
1124
+ .mag-doc { max-width: none; padding: 0; margin: 0; }
1125
+ .mag-page {
775
1126
  box-shadow: none;
1127
+ margin: 0;
1128
+ padding: 16mm 18mm;
1129
+ page-break-after: always;
776
1130
  }
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
- }
1131
+ .mag-page:last-child { page-break-after: auto; }
785
1132
  }
786
1133
  </style>
787
1134
  </head>
@@ -790,6 +1137,11 @@
790
1137
  <header class="mag-top-bar" data-mag-chrome>
791
1138
  <a href="/" class="mag-crumb">PrivateBoard <span class="mag-crumb-accent">· magazine</span></a>
792
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>
793
1145
  <button type="button" class="mag-btn" data-mag-png>
794
1146
  <span class="glyph">↓</span>PNG
795
1147
  </button>
@@ -809,31 +1161,17 @@
809
1161
 
810
1162
  <script>
811
1163
  /* ──────────────────────────────────────────────────────────────────
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.
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.
831
1168
  ────────────────────────────────────────────────────────────── */
832
1169
  (function () {
833
1170
  const params = new URLSearchParams(location.search);
834
1171
  const briefId = (params.get("b") || "").trim();
835
1172
  const roomId = (params.get("r") || "").trim();
836
1173
  const root = document.querySelector("[data-mag-root]");
1174
+ const variantBadge = document.querySelector("[data-mag-variant-badge]");
837
1175
 
838
1176
  function escape(s) {
839
1177
  return String(s == null ? "" : s)
@@ -855,186 +1193,346 @@
855
1193
 
856
1194
  async function loadBrief() {
857
1195
  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
- }
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; }
867
1199
  const res = await fetch(url);
868
1200
  if (!res.ok) {
869
1201
  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.");
1202
+ showState("Not found", "Brief not found", e.error || "The requested brief doesn't exist or is no longer available.");
872
1203
  return null;
873
1204
  }
874
1205
  return await res.json();
875
1206
  }
876
1207
 
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) {
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) {
884
1242
  const s = String(raw || "").trim();
885
- if (!s) return { title: "", body: "" };
886
- const seps = [" · ", " ", " - ", ": "];
1243
+ if (!s) return { heading: "", body: "" };
1244
+ const seps = [": ", " · ", " ", " - "];
887
1245
  for (const sep of seps) {
888
1246
  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
- };
1247
+ if (idx > 0 && idx < 60) {
1248
+ return { heading: s.slice(0, idx).trim(), body: s.slice(idx + sep.length).trim() };
894
1249
  }
895
1250
  }
896
- return { title: s, body: "" };
1251
+ return { heading: s, body: "" };
897
1252
  }
898
1253
 
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];
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;
918
1262
  }
919
1263
 
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];
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>`;
934
1271
  }
935
1272
 
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];
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>`;
952
1329
  }
953
1330
 
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>`;
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>`;
979
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>`;
980
1398
  }
981
1399
 
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;
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>`;
998
1450
  }
999
1451
 
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>`;
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>`;
1019
1473
  }).join("");
1020
- return items;
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>`;
1021
1519
  }
1022
1520
 
1521
+ /* ═════════════════════════════════════════════════════════════ */
1522
+
1023
1523
  function render(brief) {
1024
1524
  if (brief.mode !== "magazine") {
1025
1525
  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.");
1526
+ "Open it in the matching viewer instead. Magazine, newspaper, and research-note are separate output modes.");
1027
1527
  return;
1028
1528
  }
1029
1529
  const m = brief.bodyJson;
1030
1530
  if (!m || typeof m !== "object") {
1031
1531
  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.");
1532
+ showState("Generating", "Magazine is still being prepared",
1533
+ "The chair is currently composing the issue. Refresh in a few seconds.");
1035
1534
  } else {
1036
- showState("Empty",
1037
- "This brief has no magazine data",
1535
+ showState("Empty", "This brief has no magazine data",
1038
1536
  "It may have failed to generate. Try regenerating from the room view.");
1039
1537
  }
1040
1538
  return;
@@ -1042,78 +1540,30 @@
1042
1540
 
1043
1541
  if (m.title) document.title = `${m.title} · Magazine`;
1044
1542
 
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";
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
+ }
1069
1548
 
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>`;
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>`;
1114
1564
  }
1115
1565
 
1116
- /* ─── Export wiring · same PNG-as-PDF strategy as bento ──────── */
1566
+ /* ─── Export wiring · same PNG-as-PDF strategy ─────────── */
1117
1567
  let _h2iLoaded = null;
1118
1568
  async function ensureHtmlToImage() {
1119
1569
  if (window.htmlToImage) return;
@@ -1140,17 +1590,11 @@
1140
1590
  const height = Math.max(el.scrollHeight, el.offsetHeight, el.clientHeight);
1141
1591
  return window.htmlToImage.toPng(el, {
1142
1592
  pixelRatio: 2,
1143
- backgroundColor: "#FBF8EE",
1593
+ backgroundColor: "#1A1A1A",
1144
1594
  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
- },
1595
+ width, height,
1596
+ canvasWidth: width, canvasHeight: height,
1597
+ style: { margin: "0", width: `${width}px`, height: `${height}px` },
1154
1598
  });
1155
1599
  }
1156
1600
 
@@ -1188,33 +1632,17 @@
1188
1632
  @page { size: auto; margin: 10mm; }
1189
1633
  * { box-sizing: border-box; margin: 0; padding: 0; }
1190
1634
  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
- }
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); }
1205
1637
  @media print {
1206
1638
  body { padding: 0; }
1207
1639
  img { box-shadow: none; max-width: none; width: 100%; }
1208
1640
  }
1209
1641
  .hint {
1210
- position: fixed;
1211
- top: 12px;
1212
- left: 12px;
1642
+ position: fixed; top: 12px; left: 12px;
1213
1643
  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;
1644
+ font-size: 11px; color: #8E8B83;
1645
+ background: rgba(255,255,255,0.9); padding: 6px 10px;
1218
1646
  border: 1px solid #E5E2DA;
1219
1647
  }
1220
1648
  @media print { .hint { display: none; } }
@@ -1239,16 +1667,8 @@
1239
1667
  }
1240
1668
 
1241
1669
  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
- }
1670
+ if (e.target.closest("[data-mag-png]")) { e.preventDefault(); exportPng(); return; }
1671
+ if (e.target.closest("[data-mag-print]")) { e.preventDefault(); exportPdf(); return; }
1252
1672
  });
1253
1673
 
1254
1674
  /* ─── Boot ──────────────────────────────────────────────────── */
@@ -1256,8 +1676,7 @@
1256
1676
  if (brief) render(brief);
1257
1677
  }).catch((e) => {
1258
1678
  console.error("[magazine] load failed:", e);
1259
- showState("Error", "Couldn't load this brief",
1260
- e instanceof Error ? e.message : String(e));
1679
+ showState("Error", "Couldn't load this brief", e instanceof Error ? e.message : String(e));
1261
1680
  });
1262
1681
  })();
1263
1682
  </script>