@cyber-dash-tech/revela 0.1.15 → 0.1.16

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,2736 @@
1
+ ---
2
+ name: monet
3
+ description: genneral clean template for presentation
4
+ author: cyber-dash
5
+ version: 1.0.0
6
+ preview:
7
+ ---
8
+
9
+ ## Visual Style — Monet Theme
10
+
11
+ Apply this visual style when generating all slides in this session.
12
+
13
+ <!-- @design:foundation:start -->
14
+
15
+ ### Color Palette
16
+
17
+ Inspired by Claude Monet's *Le Bassin aux Nymphéas* — mist, water, and soft light.
18
+
19
+ ```css
20
+ :root {
21
+ --bg-frame: #0d1a24;
22
+ --bg-page: #f0f4f7;
23
+ --bg-page-alt: #e4ecf2;
24
+ --text-primary: #0f1e2a;
25
+ --text-secondary: #3a5068;
26
+ --text-muted: #7a96aa;
27
+ --line: rgba(15, 30, 42, 0.12);
28
+ --line-strong: rgba(15, 30, 42, 0.24);
29
+ --accent-earth: #2980AF;
30
+ --accent-olive: #00A2B0;
31
+ --accent-stone: #83A5BA;
32
+ --accent-gold: #738DC2;
33
+ --accent-danger: #E593B4;
34
+ --accent-sage: #7CBDC3;
35
+ --shadow-soft: rgba(0, 55, 102, 0.18);
36
+ --font-display: 'EB Garamond', 'Georgia', ui-serif, serif;
37
+ --font-body: 'EB Garamond', 'Georgia', ui-serif, serif;
38
+ }
39
+ ```
40
+
41
+ Accent usage guidance:
42
+ - `--accent-gold` (`#738DC2`, Cornflower Blue) — primary emphasis, TOC dividers, key data callouts
43
+ - `--accent-earth` (`#2980AF`, Hanada-iro) — strong structural accent, headers, primary chart series
44
+ - `--accent-olive` (`#00A2B0`, Asagi-iro) — vivid teal accent, highlights, positive-signal content
45
+ - `--accent-stone` (`#83A5BA`, Mist Gray) — lightest accent, disabled states, faint decorative lines
46
+ - `--accent-sage` (`#7CBDC3`, Water Lily) — soft teal; use for environmental, calm, or reflective content
47
+ - `--accent-danger` — negative indicators, alerts, down-trend markers only
48
+
49
+ ### Typography
50
+
51
+ - **Display / heading font**: `EB Garamond` — used for all headings (`h1`–`h4`), eyebrows, and display text across every layout
52
+ - **Body font**: `EB Garamond` — used for body copy, labels, captions, and UI text; Monet is a unified serif design
53
+ - Font link tag:
54
+ ```html
55
+ <link rel="preconnect" href="https://fonts.googleapis.com">
56
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
57
+ <link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap" rel="stylesheet">
58
+ ```
59
+ - cover h1: `108px` to `124px`, weight `500` to `600`, line-height `0.90` to `0.96`
60
+ - inner-layout h2: `30px` to `36px`, weight `500` to `600`, line-height `1.10` to `1.16`
61
+ - inner-layout h3: `20px` to `24px`, weight `500`, line-height `1.16` to `1.22`
62
+ - Body: `17px`, line-height `1.65`
63
+ - Quote text: `20px`, italic, line-height `1.5`
64
+ - Eyebrow / caption: `12px` to `13px`, uppercase, letter-spacing `0.14em` to `0.18em`
65
+ - Stat number: `72px` to `88px`, weight `400` to `500`, line-height `0.95`
66
+ - Never use text shadows or glow.
67
+ - Monet is a serif design; do not substitute sans-serif typefaces.
68
+
69
+ All sizes are fixed `px` for the 1920x1080 canvas. JS `transform: scale()` handles viewport adaptation. Never use `clamp()` or viewport-relative units.
70
+
71
+ ### Page Framing
72
+
73
+ - The browser viewport is the deep ink frame: `#0d1a24`.
74
+ - The presentation canvas is a cool mist page inside that frame — soft blue-white, like morning light on water.
75
+ - Use the page edge as a visible compositional device. The page should feel placed inside the viewport, not full-bleed by default.
76
+ - Default page inset: `40px` from viewport edges, with subtle inner shadow only if needed.
77
+
78
+ ### Grid System
79
+
80
+ - **1920x1080 fixed canvas** with a page around `1760px` wide inside the frame.
81
+ - Main content width: `1480px`.
82
+ - Preferred editorial splits: `5 / 7`, `4 / 8`, `3 / 9`.
83
+ - Text column target: about `480px` max.
84
+ - Image column target: about `880px` max.
85
+ - Headings align to the text column, not to the full canvas.
86
+ - Let images carry visual weight; text should remain narrow, calm, and readable.
87
+
88
+ ### Image Treatment
89
+
90
+ - Use impressionist-inspired photography: water surfaces, gardens, soft natural light, reflections, and botanical subjects.
91
+ - Prefer misty, soft-focus, and atmospheric imagery over sharp industrial or urban photography.
92
+ - Favor gentle crops and painterly aspect ratios. Allow blurred edges and tonal transitions.
93
+ - Use `object-fit: cover` and accept cropping when composition improves.
94
+ - Avoid heavy dark overlays, neon gradients, frosted glass, or artificial glows.
95
+ - Captions should be small, quiet, and aligned to an image edge.
96
+ - When a layout places copy over a visual field, create a soft blue-toned reading field that begins dense at the text side and fades naturally across the visual.
97
+
98
+ ### Decorative Language
99
+
100
+ - Ornament is restrained: thin rules, delicate horizontal dividers, subtle tonal page blocks.
101
+ - Dense slides should rely on structure, not decoration.
102
+ - Sparse slides can use a single atmospheric image or one quiet rule to hold composition.
103
+ - Never use blobs, glow halos, glass cards, or dashboard chrome.
104
+
105
+ ### Editorial Index Motifs
106
+
107
+ - Monet may use a **catalogue-style index language** for contents, section menus, and chapter navigators: compact rows, quiet page references, and airy spacing.
108
+ - When numbering chapters or sections, prefer a **tilt plaque** over a bare utility numeral. The effect should feel like lightly layered paper, not a sticker or badge system.
109
+ - Tilt plaques should stay restrained: usually two layers only, light border, soft shadow, and small rotation deltas. Keep the front/back rotation within about `3deg` to `9deg`.
110
+ - Monet may use an **offset display heading** for short editorial titles. If a heading breaks across two lines, the second line may shift horizontally to create a composed art-book rhythm.
111
+ - Offset headings are only for short titles. Do not use this treatment for dense multi-line copy blocks or long explanatory headlines.
112
+ - For index-like structures, the information hierarchy should read **chapter label -> title -> page reference**. The label stays small and quiet; the title carries the main emphasis; the page reference stays right-aligned and understated.
113
+ - TOC and index rows should avoid long summaries by default. Prefer short chapter titles plus page references. Add only a very short intro/dek when orientation is genuinely needed.
114
+
115
+ Recommended reusable primitives:
116
+
117
+ ```css
118
+ .tilt-plaque {
119
+ position: relative;
120
+ width: 68px;
121
+ height: 68px;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ }
126
+
127
+ .tilt-plaque::before,
128
+ .tilt-plaque::after {
129
+ content: '';
130
+ position: absolute;
131
+ inset: 0;
132
+ border: 1px solid rgba(15, 30, 42, 0.10);
133
+ box-shadow: 0 16px 36px rgba(41, 80, 104, 0.10);
134
+ }
135
+
136
+ .tilt-plaque::before {
137
+ transform: rotate(var(--plaque-back-rot, -7deg)) translate(-6px, 5px);
138
+ background: var(--plaque-back, rgba(123, 189, 195, 0.22));
139
+ }
140
+
141
+ .tilt-plaque::after {
142
+ transform: rotate(var(--plaque-front-rot, 4deg));
143
+ background: linear-gradient(145deg, rgba(255,255,255,0.92), rgba(255,255,255,0.66));
144
+ }
145
+
146
+ .tilt-plaque-value {
147
+ position: relative;
148
+ z-index: 1;
149
+ font-family: var(--font-display);
150
+ font-size: 31px;
151
+ line-height: 1;
152
+ font-weight: 600;
153
+ letter-spacing: -0.04em;
154
+ transform: rotate(var(--plaque-value-rot, -3deg));
155
+ color: var(--plaque-ink, var(--text-primary));
156
+ }
157
+
158
+ .offset-title {
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 6px;
162
+ }
163
+
164
+ .offset-title-line--offset {
165
+ padding-left: 84px;
166
+ }
167
+ ```
168
+
169
+ ### Slide Layout
170
+
171
+ - Every slide uses `.slide-canvas` sized to `1920px x 1080px`, scaled by JS.
172
+ - Every `<section class="slide">` must include `slide-qa="true"` or `slide-qa="false"`.
173
+ - Use `slide-qa="true"` for dense content layouts and `slide-qa="false"` for structural or intentionally sparse layouts.
174
+ - Default canvas padding: `10px` (minimal frame; the page nearly fills the canvas).
175
+ - The paper page should usually sit inside the canvas with `padding: 56px 64px 64px`.
176
+ - Target strong fill on content-heavy slides while preserving editorial whitespace.
177
+
178
+ ### HTML Structure
179
+
180
+ Every generated presentation must use this exact HTML skeleton:
181
+
182
+ ```html
183
+ <!DOCTYPE html>
184
+ <html lang="{language}">
185
+ <head>
186
+ <meta charset="UTF-8">
187
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
188
+ <title>{Presentation Title}</title>
189
+ <link rel="preconnect" href="https://fonts.googleapis.com">
190
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
191
+ <link href="https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500&display=swap" rel="stylesheet">
192
+ <!-- <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> only if charts are needed -->
193
+ <style>/* all CSS here */</style>
194
+ </head>
195
+ <body>
196
+ <section class="slide cover-slide" slide-qa="false" data-index="0">
197
+ <div class="slide-canvas"> ... </div>
198
+ </section>
199
+ <section class="slide" slide-qa="true" data-index="1">
200
+ <div class="slide-canvas"> ... </div>
201
+ </section>
202
+ <script>/* all JS here */</script>
203
+ </body>
204
+ </html>
205
+ ```
206
+
207
+ ### Core CSS
208
+
209
+ ```css
210
+ * { box-sizing: border-box; margin: 0; padding: 0; }
211
+
212
+ html {
213
+ scroll-snap-type: y mandatory;
214
+ overflow-y: scroll;
215
+ height: 100%;
216
+ }
217
+
218
+ body {
219
+ background: var(--bg-frame);
220
+ color: var(--text-primary);
221
+ font-family: var(--font-body);
222
+ -webkit-font-smoothing: antialiased;
223
+ height: 100%;
224
+ }
225
+
226
+ .slide {
227
+ min-height: 100dvh;
228
+ scroll-snap-align: start;
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: center;
232
+ overflow: hidden;
233
+ background: var(--bg-frame);
234
+ }
235
+
236
+ .slide-canvas {
237
+ width: 1920px;
238
+ height: 1080px;
239
+ flex-shrink: 0;
240
+ transform-origin: center center;
241
+ position: relative;
242
+ overflow: hidden;
243
+ padding: 10px;
244
+ }
245
+
246
+ .page {
247
+ position: relative;
248
+ width: 100%;
249
+ height: 100%;
250
+ background: var(--bg-page);
251
+ color: var(--text-primary);
252
+ padding: 0;
253
+ box-shadow: 0 24px 80px var(--shadow-soft);
254
+ display: flex;
255
+ flex-direction: column;
256
+ }
257
+
258
+ .page.alt {
259
+ background: var(--bg-page-alt);
260
+ }
261
+
262
+ .eyebrow,
263
+ .caption,
264
+ .meta-label {
265
+ font-size: 12px;
266
+ line-height: 1.4;
267
+ letter-spacing: 0.18em;
268
+ text-transform: uppercase;
269
+ color: var(--text-muted);
270
+ }
271
+
272
+ h1, h2, h3, h4 {
273
+ font-family: var(--font-display);
274
+ font-weight: 500;
275
+ letter-spacing: 0em;
276
+ color: var(--text-primary);
277
+ }
278
+
279
+ h1 { font-size: 88px; line-height: 0.96; }
280
+ h2 { font-size: 34px; line-height: 1.10; }
281
+ h3 { font-size: 24px; line-height: 1.16; }
282
+
283
+ p, li {
284
+ font-size: 17px;
285
+ line-height: 1.65;
286
+ color: var(--text-secondary);
287
+ }
288
+
289
+ .rule {
290
+ width: 100%;
291
+ height: 1px;
292
+ background: var(--line);
293
+ }
294
+
295
+ .rule.strong {
296
+ background: var(--line-strong);
297
+ }
298
+
299
+ .chevron-divider {
300
+ display: inline-flex;
301
+ align-items: center;
302
+ gap: 10px;
303
+ font-size: 11px;
304
+ letter-spacing: 0.18em;
305
+ text-transform: uppercase;
306
+ color: var(--text-muted);
307
+ }
308
+
309
+ .chevron-divider::before,
310
+ .chevron-divider::after {
311
+ content: '';
312
+ width: 18px;
313
+ height: 1px;
314
+ background: var(--line-strong);
315
+ }
316
+
317
+ .media-frame {
318
+ position: relative;
319
+ overflow: hidden;
320
+ }
321
+
322
+ .media-frame img {
323
+ width: 100%;
324
+ height: 100%;
325
+ display: block;
326
+ object-fit: cover;
327
+ }
328
+
329
+ .media-caption {
330
+ margin-top: 12px;
331
+ font-size: 12px;
332
+ line-height: 1.5;
333
+ letter-spacing: 0.14em;
334
+ text-transform: uppercase;
335
+ color: var(--text-muted);
336
+ }
337
+
338
+ /* editorial-list: square bullet + optional <strong> lead phrase per item.
339
+ Usage: <li><strong>Lead phrase.</strong> Supporting copy.</li>
340
+ Dark bg override: set --accent-earth to rgba(240,244,247,0.72) on the list wrapper. */
341
+ .editorial-list {
342
+ list-style: none;
343
+ display: flex;
344
+ flex-direction: column;
345
+ gap: 14px;
346
+ }
347
+
348
+ .editorial-list li {
349
+ position: relative;
350
+ padding-left: 20px;
351
+ font-size: 17px;
352
+ line-height: 1.58;
353
+ color: var(--text-secondary);
354
+ }
355
+
356
+ .editorial-list li::before {
357
+ content: '';
358
+ position: absolute;
359
+ left: 0;
360
+ top: 8px;
361
+ width: 6px;
362
+ height: 6px;
363
+ background: var(--accent-earth);
364
+ }
365
+
366
+ .reveal {
367
+ opacity: 0;
368
+ transform: translateY(18px);
369
+ transition: opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1),
370
+ transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
371
+ }
372
+
373
+ .reveal.visible {
374
+ opacity: 1;
375
+ transform: translateY(0);
376
+ }
377
+ ```
378
+
379
+ ### SlidePresentation Class (Complete JavaScript)
380
+
381
+ All presentations must include this complete `SlidePresentation` class.
382
+
383
+ ```javascript
384
+ class SlidePresentation {
385
+ constructor() {
386
+ this.slides = document.querySelectorAll('.slide');
387
+ this.currentSlide = 0;
388
+ this.setupScaling();
389
+ this.setupIntersectionObserver();
390
+ this.setupKeyboardNav();
391
+ this.setupTouchNav();
392
+ this.setupMouseWheel();
393
+ }
394
+
395
+ setupScaling() {
396
+ const canvases = document.querySelectorAll('.slide-canvas');
397
+ const BASE_W = 1920;
398
+ const BASE_H = 1080;
399
+ const update = () => {
400
+ const vw = window.innerWidth;
401
+ const vh = window.innerHeight;
402
+ const scale = Math.min(vw / BASE_W, vh / BASE_H);
403
+ canvases.forEach((canvas) => {
404
+ canvas.style.transform = `scale(${scale})`;
405
+ });
406
+ };
407
+ window.addEventListener('resize', update);
408
+ update();
409
+ }
410
+
411
+ setupIntersectionObserver() {
412
+ const observer = new IntersectionObserver((entries) => {
413
+ entries.forEach((entry) => {
414
+ if (entry.isIntersecting) {
415
+ entry.target.querySelectorAll('.reveal').forEach((el) => el.classList.add('visible'));
416
+ }
417
+ });
418
+ }, { threshold: 0.2 });
419
+ this.slides.forEach((slide) => observer.observe(slide));
420
+ }
421
+
422
+ setupKeyboardNav() {
423
+ document.addEventListener('keydown', (event) => {
424
+ if (['ArrowDown', 'ArrowRight', ' ', 'PageDown'].includes(event.key)) {
425
+ event.preventDefault();
426
+ this.goTo(this.currentSlide + 1);
427
+ } else if (['ArrowUp', 'ArrowLeft', 'PageUp'].includes(event.key)) {
428
+ event.preventDefault();
429
+ this.goTo(this.currentSlide - 1);
430
+ }
431
+ });
432
+ }
433
+
434
+ setupTouchNav() {
435
+ let startY = 0;
436
+ document.addEventListener('touchstart', (event) => {
437
+ startY = event.touches[0].clientY;
438
+ }, { passive: true });
439
+ document.addEventListener('touchend', (event) => {
440
+ const deltaY = startY - event.changedTouches[0].clientY;
441
+ if (Math.abs(deltaY) > 40) {
442
+ this.goTo(this.currentSlide + (deltaY > 0 ? 1 : -1));
443
+ }
444
+ }, { passive: true });
445
+ }
446
+
447
+ setupMouseWheel() {
448
+ let last = 0;
449
+ document.addEventListener('wheel', (event) => {
450
+ const now = Date.now();
451
+ if (now - last < 800) return;
452
+ last = now;
453
+ this.goTo(this.currentSlide + (event.deltaY > 0 ? 1 : -1));
454
+ }, { passive: true });
455
+ }
456
+
457
+ goTo(index) {
458
+ const clamped = Math.max(0, Math.min(this.slides.length - 1, index));
459
+ this.slides[clamped].scrollIntoView({ behavior: 'smooth' });
460
+ this.currentSlide = clamped;
461
+ }
462
+ }
463
+
464
+ new SlidePresentation();
465
+ ```
466
+
467
+ <!-- @design:foundation:end -->
468
+
469
+ <!-- @design:rules:start -->
470
+
471
+ ### Composition Rules
472
+
473
+ These rules are mandatory for Monet.
474
+
475
+ - **Image over decoration.** If a slide has strong photography, reduce ornamental elements to one rule or one caption.
476
+ - **Text column stays narrow.** Do not let narrative text stretch across the page just because space exists.
477
+ - **One visual center per slide.** Either the image, the heading block, or the metrics dominate. Never all three at equal weight.
478
+ - **Metrics should read like a report, not a dashboard.** Use calm typography, generous spacing, and minimal UI framing.
479
+ - **Dense slides need structure, not decoration.** Use rules, columns, and alignment. Avoid extra accents.
480
+ - **Sparse slides depend on image weight.** If content is light, the photo or page framing must hold the composition.
481
+ - **No glass cards, neon KPI styling, or startup-product chrome.** Monet is editorial and print-adjacent.
482
+ - **Visual hierarchy is strict:** eyebrow -> heading -> body -> caption.
483
+
484
+ ### Common Mistakes
485
+
486
+ - Using equally wide text and image columns instead of an asymmetric editorial split.
487
+ - Filling empty space with decorative shapes instead of using a stronger crop or larger photograph.
488
+ - Treating metric blocks like SaaS dashboard cards.
489
+ - Setting long paragraphs directly on top of busy imagery.
490
+ - Using more than one dominant photo on a single slide without a clear hierarchy.
491
+ - Letting captions become body-copy sized.
492
+
493
+ ### Do & Don't
494
+
495
+ - **Do** let the paper page feel physical inside the dark frame.
496
+ - **Do** use thin rules and quiet labels to create structure.
497
+ - **Do** crop imagery boldly when it improves the composition.
498
+ - **Do** keep headings hard-edged, restrained, and report-like.
499
+ - **Don't** use glow, glass, blob shapes, or neon gradients.
500
+ - **Don't** center everything by default; asymmetry is part of the design language.
501
+ - **Don't** turn highlights into generic cards with rounded corners and shadows.
502
+ - **Don't** overload a slide with both many metrics and many images.
503
+
504
+ <!-- @design:rules:end -->
505
+
506
+ <!-- @design:layouts:start -->
507
+
508
+ ### Layout Types
509
+
510
+ Each `<section class="slide">` must set `slide-qa="true"` or `slide-qa="false"`. Use the QA column to decide which value to write. Fetch any layout with the `revela-designs` tool (`action: "read"`, `layout: "<name>"`).
511
+
512
+ <!-- @layout:fullbleed:start qa=false -->
513
+ #### Fullbleed
514
+
515
+ Full-canvas layout for slides where a single image dominates the entire canvas with text composited over it. Use for opening (cover) and closing slides, or atmospheric section dividers.
516
+
517
+ Structural intent:
518
+ - Single slot: place one `image-title` component directly inside `.page`. The component is self-contained — it manages its own image, blur, overlay, and text layers internally.
519
+
520
+ ```html
521
+ <section class="slide" slide-qa="false" data-index="N">
522
+ <div class="slide-canvas">
523
+ <div class="page" style="padding:0;">
524
+
525
+ <!-- [slot: content] — use image-title component; set --left for cover, --right for closing -->
526
+ <!-- image-title handles all internal z-index layering (image → blur → overlay → text) -->
527
+
528
+ </div>
529
+ </div>
530
+ </section>
531
+ ```
532
+
533
+ ##### Tips
534
+ - **Use `image-title` as the sole child.** The component is self-contained and fills `width:100%; height:100%` automatically. Do not add extra wrapper divs around it.
535
+ - **Cover vs closing.** Cover: `image-title--left` with a diagonal overlay (`105deg`, left-dark to right-transparent) and left-biased blur mask. Closing: `image-title--right` with a bottom-heavy overlay (`180deg`) and right-biased blur mask.
536
+ - **Page number.** Use `.page-number--light` — position it inside `.page` at `z-index:10` so it sits above the `image-title` stacking context.
537
+ - **Text opacity.** For atmospheric section dividers where content is minimal, add `style="opacity:0.85"` on the `.image-title` container to soften the foreground text layer against the image.
538
+ <!-- @layout:fullbleed:end -->
539
+
540
+ <!-- @layout:narrative:start qa=true -->
541
+ #### Narrative
542
+
543
+ Asymmetric two-column layout with the left column wider (1.618fr) and the right column narrower (1fr). Use when one side needs more visual or reading weight than the other.
544
+
545
+ Structural intent:
546
+ - left slot: wider zone (1.618fr) — can hold any component(s)
547
+ - right slot: narrower zone (1fr) — can hold any component(s)
548
+
549
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — there is no text/visual semantic preset.
550
+
551
+
552
+ ```html
553
+ <section class="slide" slide-qa="true" data-index="N">
554
+ <div class="slide-canvas">
555
+ <div class="page" style="padding:0;overflow:hidden;">
556
+ <div class="narrative-grid">
557
+
558
+ <!-- [slot: left] — 1+ components; suggested: image-title, echart-panel, text-panel -->
559
+ <div>
560
+ </div>
561
+
562
+ <!-- [slot: right] — 1+ components; suggested: text-panel, toc, flow-vertical, data-table -->
563
+ <div>
564
+ </div>
565
+
566
+ </div>
567
+ </div>
568
+ </div>
569
+ </section>
570
+ ```
571
+
572
+ ```css
573
+ .narrative-grid {
574
+ display: grid;
575
+ grid-template-columns: minmax(0, 1.618fr) minmax(0, 1fr);
576
+ grid-template-rows: minmax(0, 1fr);
577
+ height: 100%;
578
+ overflow: hidden;
579
+ align-items: stretch;
580
+ }
581
+
582
+ .narrative-grid--reverse {
583
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.618fr);
584
+ }
585
+
586
+ .narrative-grid > * {
587
+ overflow: hidden;
588
+ min-height: 0;
589
+ min-width: 0;
590
+ }
591
+ ```
592
+
593
+ ##### Tips
594
+ - **Grid container uses `.narrative-grid` class.** Applies `minmax(0, Nfr)` tracks, `overflow:hidden` on all children, and `align-items:stretch` so both columns fill the full row height. Do not override with `align-items:start` — that collapses columns to content height and exposes the page background.
595
+ - **No semantic preset.** Either slot can hold any component. The wider left column naturally suits visually dominant content (full-bleed media, wide charts), but this is not a hard rule.
596
+ - **Dark panel variant.** When a slot uses a dark background, override CSS variables on that container: `--text-primary`, `--text-secondary`, `--text-muted`, `--line`, `--line-strong` — all set to white-family values. Use `.page-number--light`.
597
+ - **Background image inside a slot.** Use the three-layer z-index pattern: background `z-index:0`, dark overlay `z-index:1`, content `z-index:2`.
598
+ <!-- @layout:narrative:end -->
599
+
600
+ <!-- @layout:narrative-reverse:start qa=true -->
601
+ #### Narrative Reverse
602
+
603
+ Asymmetric two-column layout with the left column narrower (1fr) and the right column wider (1.618fr). Mirror of `narrative` — same grid class with `--reverse` modifier.
604
+
605
+ Structural intent:
606
+ - left slot: narrower zone (1fr) — can hold any component(s)
607
+ - right slot: wider zone (1.618fr) — can hold any component(s)
608
+
609
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — there is no text/visual semantic preset.
610
+
611
+
612
+ ```html
613
+ <section class="slide" slide-qa="true" data-index="N">
614
+ <div class="slide-canvas">
615
+ <div class="page" style="padding:0;overflow:hidden;">
616
+ <div class="narrative-grid narrative-grid--reverse">
617
+
618
+ <!-- [slot: left] — 1+ components; suggested: text-panel, toc, flow-vertical, echart-panel -->
619
+ <div>
620
+ </div>
621
+
622
+ <!-- [slot: right] — 1+ components; suggested: image-title, echart-panel, data-table -->
623
+ <div>
624
+ </div>
625
+
626
+ </div>
627
+ </div>
628
+ </div>
629
+ </section>
630
+ ```
631
+
632
+ ##### Tips
633
+ - **Same `.narrative-grid` class as `narrative`, with `--reverse` modifier.** Add both `narrative-grid` and `narrative-grid--reverse` to the grid container. The modifier swaps column proportions to `1fr left / 1.618fr right`.
634
+ - **No semantic preset.** Either slot can hold any component — visual on the right, text on the left, or any other combination based on content needs.
635
+ - **Dark panel variant.** Same CSS variable override pattern as `narrative`: set `--text-primary` etc. to white-family values on the panel container, all child components inherit automatically.
636
+ <!-- @layout:narrative-reverse:end -->
637
+
638
+ <!-- @layout:highlight-cols:start qa=true -->
639
+ #### Highlight Cols
640
+
641
+ Equal N-column layout. Use when 3 or more parallel items of roughly equal visual weight should appear side by side — proof blocks, highlights, feature comparisons, stat groups, or any multi-column editorial spread.
642
+
643
+ A short section header is optional but recommended. In Monet, that header should stay lean: eyebrow plus title only, with no intro paragraph competing with the columns below.
644
+
645
+ Structural intent:
646
+ - each slot: 1fr column — any component(s)
647
+ - column count: determined by the number of direct child divs in the grid container; `auto-fit` distributes space equally
648
+
649
+ Every slot accepts 1 or more components. Add or remove child divs to control column count — 3 is the default, but 4 or 5 columns work equally well.
650
+
651
+ ```html
652
+ <section class="slide" slide-qa="true" data-index="N">
653
+ <div class="slide-canvas">
654
+ <div class="page">
655
+ <div style="display:flex;flex-direction:column;gap:10px;margin-bottom:28px;max-width:520px;">
656
+ <p class="eyebrow">Section Label</p>
657
+ <h2 style="font-size:52px;line-height:0.94;text-transform:uppercase;">Short framing title for the parallel columns</h2>
658
+ </div>
659
+
660
+ <div class="highlight-cols-grid" style="flex:1;min-height:0;">
661
+
662
+ <!-- [slot: 1] — 1+ components; suggested: editorial-image-top, editorial-text-top, echart-panel -->
663
+ <div>
664
+ </div>
665
+
666
+ <!-- [slot: 2] — 1+ components; suggested: editorial-text-top, echart-panel, flow-vertical -->
667
+ <div>
668
+ </div>
669
+
670
+ <!-- [slot: 3] — 1+ components; suggested: editorial-image-top, editorial-text-top, echart-panel -->
671
+ <div>
672
+ </div>
673
+
674
+ </div>
675
+ </div>
676
+ </div>
677
+ </section>
678
+ ```
679
+
680
+ ```css
681
+ .highlight-cols-grid {
682
+ display: grid;
683
+ grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
684
+ gap: 32px;
685
+ overflow: hidden;
686
+ align-items: start;
687
+ }
688
+
689
+ .highlight-cols-grid > * {
690
+ overflow: hidden;
691
+ min-height: 0;
692
+ }
693
+ ```
694
+
695
+ ##### Tips
696
+ - **Grid container needs `flex:1;min-height:0` inline** when inside `.page` (which is flex-column). The class handles column sizing; the inline style handles row stretch.
697
+ - **Header stays lean.** If you add a section header above the grid, use only `eyebrow + title`. Do not add an intro paragraph; the columns themselves should carry the explanation.
698
+ - **Column count = number of direct child divs.** `repeat(auto-fit, minmax(0, 1fr))` distributes available width equally across however many children exist. Add a 4th or 5th div to get 4 or 5 columns — no CSS change needed.
699
+ - **Equal columns — no hierarchy.** All slots carry the same visual weight. Adjust content density to suit the slide purpose; do not artificially inflate one column to create false hierarchy.
700
+ - **When using 4-5 columns, compress the header.** Keep the title to one or two short lines so the grid retains most of the slide height.
701
+ - **Do not set fixed heights on editorial components.** Let components fill height via flexbox stretch.
702
+ <!-- @layout:highlight-cols:end -->
703
+
704
+ <!-- @layout:halves:start qa=true -->
705
+ #### Halves
706
+
707
+ Equal two-column layout. Use when two items of equal visual weight should appear side by side — paired charts, dual evidence blocks, before/after comparisons, or any two-column editorial spread.
708
+
709
+ Structural intent:
710
+ - left slot: 1fr column — any component(s)
711
+ - right slot: 1fr column — any component(s)
712
+
713
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — both columns are fully equal with no hierarchy preset.
714
+
715
+
716
+ ```html
717
+ <section class="slide" slide-qa="true" data-index="N">
718
+ <div class="slide-canvas">
719
+ <div class="page" style="overflow:hidden;">
720
+ <div class="halves-grid" style="flex:1;min-height:0;">
721
+
722
+ <!-- [slot: left] — 1+ components; suggested: echart-panel, data-table, editorial-image-top -->
723
+ <div>
724
+ </div>
725
+
726
+ <!-- [slot: right] — 1+ components; suggested: echart-panel, data-table, text-panel -->
727
+ <div>
728
+ </div>
729
+
730
+ </div>
731
+ </div>
732
+ </div>
733
+ </section>
734
+ ```
735
+
736
+ ```css
737
+ .halves-grid {
738
+ display: grid;
739
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
740
+ gap: 0px;
741
+ overflow: hidden;
742
+ align-items: stretch;
743
+ }
744
+
745
+ .halves-grid > * {
746
+ overflow: hidden;
747
+ min-height: 0;
748
+ min-width: 0;
749
+ }
750
+ ```
751
+
752
+ ##### Tips
753
+ - **Grid container needs `flex:1;min-height:0` inline** when inside `.page`. The class handles column sizing.
754
+ - **Equal columns — no hierarchy.** Both slots carry the same weight. Choose components based on content, not a fixed text/visual assignment.
755
+ - **Gap `40px` is intentional.** The slightly wider gap than `three-col` (32px) compensates for the larger individual column width.
756
+ <!-- @layout:halves:end -->
757
+
758
+ <!-- @layout:stacked:start qa=true -->
759
+ #### Stacked
760
+
761
+ Two-row vertical layout in a fixed golden-ratio proportion: top row takes 1fr and bottom row takes 1.618fr. Use when a horizontal component (process flow, stat row, header band) should anchor the top, with a taller content zone below.
762
+
763
+ Structural intent:
764
+ - top slot: `1fr` height — upper zone in golden-ratio proportion
765
+ - bottom slot: `1.618fr` height — larger lower zone fills remaining space
766
+
767
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — there is no semantic preset for either row.
768
+
769
+
770
+ ```html
771
+ <section class="slide" slide-qa="true" data-index="N">
772
+ <div class="slide-canvas">
773
+ <div class="page" style="padding:0;">
774
+ <div class="stacked-grid">
775
+
776
+ <!-- [slot: top] — 1+ components; suggested: flow-horizontal, stat-row -->
777
+ <div class="stacked-top">
778
+ </div>
779
+
780
+ <!-- [slot: bottom] — 1+ components; suggested: echart-panel, data-table -->
781
+ <div class="stacked-bottom">
782
+ </div>
783
+
784
+ </div>
785
+ </div>
786
+ </div>
787
+ </section>
788
+ ```
789
+
790
+ ```css
791
+ .stacked-grid {
792
+ display: grid;
793
+ grid-template-rows: minmax(0, 1fr) minmax(0, 1.618fr);
794
+ height: 100%;
795
+ width: 100%;
796
+ overflow: hidden;
797
+ }
798
+
799
+ .stacked-top {
800
+ overflow: hidden;
801
+ min-height: 0;
802
+ }
803
+
804
+ .stacked-bottom {
805
+ overflow: hidden;
806
+ min-height: 0;
807
+ }
808
+ ```
809
+
810
+ ##### Tips
811
+ - **Top and bottom rows follow a fixed 1 : 1.618 golden-ratio proportion.** The top slot takes 1fr and the bottom takes 1.618fr — both rows are sized relative to the total canvas height, not by their content.
812
+ - **Both slots clip overflow.** `min-height: 0` on both `.stacked-top` and `.stacked-bottom` ensures content cannot break out of its row.
813
+ - **Both slots are fully equal in kind.** There is no preset for which slot holds "process" vs "data" — place any combination of components that fits the slide narrative.
814
+ - **Dark background variant.** Set CSS variable overrides (`--text-primary` etc.) on `.stacked-grid` to cascade into both slots automatically.
815
+ <!-- @layout:stacked:end -->
816
+
817
+ <!-- @design:layouts:end -->
818
+
819
+
820
+ <!-- @design:components:start -->
821
+
822
+ ### Component Library
823
+
824
+ Use these components when a page needs repeatable editorial modules inside a larger layout. Components define the block itself, not the page grid around it.
825
+
826
+
827
+
828
+ <!-- @component:text-panel:start -->
829
+ #### Text Panel
830
+
831
+ <!-- renamed from report-text-panel -->
832
+
833
+ Unified narrative text container. Use inside any layout slot that needs a self-contained reading surface with heading, body copy, and optional footer metadata. The body zone accepts prose, a bullet list, or both — choose based on content, not convention.
834
+
835
+ ```html
836
+ <!-- variant A: prose only (--dark) -->
837
+ <div class="text-panel text-panel--dark">
838
+ <div style="max-width:420px;">
839
+ <p class="eyebrow" style="color:rgba(243,238,230,0.72);">Section label / annual review</p>
840
+ <h2 style="margin-top:16px;font-size:60px;line-height:0.92;letter-spacing:-0.03em;text-transform:uppercase;color:#f0f4f7;max-width:360px;">Narrative heading</h2>
841
+ <div class="text-panel-body" style="margin-top:20px;">
842
+ <p style="font-size:17px;line-height:1.58;color:rgba(243,238,230,0.84);max-width:390px;">Use one or two compact paragraphs when continuous prose fits the content better than a list.</p>
843
+ </div>
844
+ </div>
845
+ <div class="text-panel-footer" style="color:rgba(243,238,230,0.68);">
846
+ <p class="caption">Monet / Climate Report 2026</p>
847
+ <p class="caption">03</p>
848
+ </div>
849
+ </div>
850
+
851
+ <!-- variant B: bullet list only (--light) -->
852
+ <div class="text-panel text-panel--light">
853
+ <div style="max-width:420px;">
854
+ <p class="eyebrow">Key findings</p>
855
+ <h3 style="margin-top:12px;">Three priorities for 2026</h3>
856
+ <div class="text-panel-body" style="margin-top:16px;">
857
+ <ul class="editorial-list">
858
+ <li><strong>Lead phrase.</strong> Supporting explanation for this point.</li>
859
+ <li><strong>Second point.</strong> One sentence of context or evidence.</li>
860
+ <li><strong>Third point.</strong> Keep each item roughly equal in length.</li>
861
+ </ul>
862
+ </div>
863
+ </div>
864
+ <div class="text-panel-footer">
865
+ <p class="caption">Monet / Climate Report 2026</p>
866
+ <p class="caption">03</p>
867
+ </div>
868
+ </div>
869
+
870
+ <!-- variant C: prose + bullets mixed (--dark) -->
871
+ <div class="text-panel text-panel--dark">
872
+ <div style="max-width:420px;">
873
+ <p class="eyebrow" style="color:rgba(243,238,230,0.72);">Context</p>
874
+ <h3 style="margin-top:12px;color:#f0f4f7;">Heading here</h3>
875
+ <div class="text-panel-body" style="margin-top:16px;">
876
+ <p style="font-size:17px;line-height:1.58;color:rgba(243,238,230,0.84);">Introductory sentence that frames what follows. Then the list expands the argument:</p>
877
+ <ul class="editorial-list" style="margin-top:12px;">
878
+ <li><strong>Lead phrase.</strong> Supporting explanation.</li>
879
+ <li><strong>Second point.</strong> One sentence of evidence.</li>
880
+ </ul>
881
+ </div>
882
+ </div>
883
+ <div class="text-panel-footer" style="color:rgba(243,238,230,0.68);">
884
+ <p class="caption">Monet / Climate Report 2026</p>
885
+ <p class="caption">03</p>
886
+ </div>
887
+ </div>
888
+ ```
889
+
890
+ ```css
891
+ /* renamed from .report-text-panel */
892
+ .text-panel {
893
+ height: 100%;
894
+ padding: 56px 48px 34px;
895
+ display: flex;
896
+ flex-direction: column;
897
+ justify-content: space-between;
898
+ }
899
+
900
+ .text-panel--dark {
901
+ background: #2c2828;
902
+ color: #f3eee6;
903
+ }
904
+
905
+ .text-panel--light {
906
+ background: var(--bg-page-alt);
907
+ color: var(--text-primary);
908
+ }
909
+
910
+ /* body zone: flex column so prose <p> and <ul> stack naturally with consistent gap */
911
+ .text-panel-body {
912
+ display: flex;
913
+ flex-direction: column;
914
+ gap: 12px;
915
+ }
916
+
917
+ /* renamed from .report-panel-footer */
918
+ .text-panel-footer {
919
+ display: flex;
920
+ justify-content: space-between;
921
+ align-items: end;
922
+ gap: 18px;
923
+ }
924
+ ```
925
+
926
+ Rules:
927
+ - `.text-panel-body` is the only required structural child. Place `<p>` elements, an `<ul class="editorial-list">`, or both inside it.
928
+ - Eyebrow, heading, and footer are all optional — include them only when the content calls for them.
929
+ - Choose `--dark` or `--light` to match the slide's tone. Do not mix variants within a single panel.
930
+ - Pair with a visually dominant neighbor (image, chart) when the layout needs strong contrast against the text zone.
931
+
932
+ ##### Tips
933
+ - **`--dark` with background image: three-layer z-index required.** Place the `<img>` absolutely at `z-index:0`, the dark scrim at `z-index:1`, and all text content divs at `z-index:2`. Without explicit z-index, the image may render above the overlay in some stacking contexts.
934
+ - **Dark scrim opacity.** Use `rgba(23,18,14, 0.78→0.70)` for the gradient direction (top darker, bottom slightly lighter) or a flat `rgba(14,12,10,0.75)` for uniform depth. Avoid going below 0.65 — text legibility degrades on busy photography.
935
+ - **`--dark` text is already white-family in the built-in CSS.** Do not add inline color overrides to individual text nodes unless you are modifying the base variant — it creates maintenance debt.
936
+ - **`--light` on dark-background slides.** If a light panel sits on a slide with a dark background overlay, ensure the panel has an explicit background (e.g., `var(--bg-page)`) and is above the overlay in z-index.
937
+ - **`editorial-list` inside `--dark`.** Add `style="--accent-earth:rgba(240,244,247,0.72)"` on the `<ul>` wrapper so the bullet squares read against the dark background.
938
+ <!-- @component:text-panel:end -->
939
+
940
+ <!-- @component:stat-card:start -->
941
+ #### Stat Card
942
+
943
+ Lightweight editorial metric module for a single highlighted number. Use when one data point should carry the visual emphasis while the metric name and guiding sentence stay quiet and compact.
944
+
945
+ This is **not** a dashboard card. It has no border, no background fill, no shadow, and no corner treatment. Hierarchy comes from number scale, spacing, and restrained typography.
946
+
947
+ ```html
948
+ <!-- variant A: default vertical composition -->
949
+ <div class="stat-card">
950
+ <p class="caption" style="color:var(--text-muted);">Performance signal</p>
951
+ <div class="stat-card-value" style="color: var(--accent-gold);">27%</div>
952
+ <div class="text-panel-body" style="gap:10px;max-width:330px;">
953
+ <h3 style="font-size:24px;line-height:1.04;text-transform:uppercase;">EBIT Margin</h3>
954
+ <p style="font-size:17px;line-height:1.56;color:var(--text-secondary);">Expanded for the third consecutive quarter as premium mix offset freight pressure and held pricing discipline through softer volume.</p>
955
+ </div>
956
+ </div>
957
+
958
+ <!-- variant B: horizontal composition -->
959
+ <div class="stat-card stat-card--horizontal">
960
+ <div style="display:flex;flex-direction:column;gap:12px;flex:0 0 220px;min-width:0;">
961
+ <p class="caption" style="color:var(--text-muted);">Operational baseline</p>
962
+ <div class="stat-card-value" style="color: var(--accent-olive);">4.8x</div>
963
+ </div>
964
+ <div class="text-panel-body" style="gap:10px;max-width:330px;">
965
+ <h3 style="font-size:24px;line-height:1.04;text-transform:uppercase;">Inventory Turnover</h3>
966
+ <p style="font-size:17px;line-height:1.56;color:var(--text-secondary);">Higher cycle efficiency reduced working-capital drag without adding new capacity, leaving more headroom for seasonal demand swings.</p>
967
+ </div>
968
+ </div>
969
+ ```
970
+
971
+ ```css
972
+ .stat-card {
973
+ height: 100%;
974
+ display: flex;
975
+ min-height: 0;
976
+ flex-direction: column;
977
+ justify-content: flex-start;
978
+ gap: 16px;
979
+ padding-top: 8px;
980
+ }
981
+
982
+ .stat-card--horizontal {
983
+ flex-direction: row;
984
+ align-items: flex-start;
985
+ gap: 30px;
986
+ }
987
+
988
+ .stat-card-value {
989
+ font-family: var(--font-display);
990
+ font-size: 88px;
991
+ line-height: 0.9;
992
+ letter-spacing: -0.04em;
993
+ font-weight: 600;
994
+ font-variant-numeric: tabular-nums;
995
+ color: inherit;
996
+ }
997
+ ```
998
+
999
+ Rules:
1000
+ - Use this component only for a **single** highlighted metric. If the slide needs several peer metrics, switch to a multi-item layout instead of stacking several stat cards together without hierarchy.
1001
+ - The number color is chosen semantically from the Monet palette via inline `style="color: var(--accent-xxx)"`; do not hardcode a fixed color in the component CSS.
1002
+ - Do not add background fills, borders, shadows, or rounded corners. The module must remain typographic and editorial.
1003
+ - Use `h3` for the metric name and a single compact `p` for the guiding sentence. Keep the copy to one or two sentences.
1004
+ - Default `stat-card` is vertical. Add `stat-card--horizontal` only when the number and explanation need a side-by-side read.
1005
+
1006
+ ##### Tips
1007
+ - **Horizontal width discipline.** In the horizontal variant, keep the number column narrow (`~220px`) so the prose still reads as the explanatory side, not as a second visual block.
1008
+ - **Long numbers.** For values such as `12,450` or `83.6%`, reduce the inline font size slightly before tightening letter-spacing further.
1009
+ - **Semantic color choice.** `--accent-gold` (`Cornflower Blue`) works for primary emphasis, `--accent-earth` (`Hanada-iro`) for structural or data-heavy metrics, `--accent-danger` for negative indicators, and `--accent-sage` (`Water Lily`) for calm or environmental signals.
1010
+ - **Do not over-explain.** If the description starts to become paragraph-length, switch to `text-panel` or pair the stat card with a narrative component in the neighboring slot.
1011
+ <!-- @component:stat-card:end -->
1012
+
1013
+ <!-- @component:editorial-image-top:start -->
1014
+ #### Editorial Image Top
1015
+
1016
+ Image-first editorial module: image on top, text below. Best for highlight grids, product/material stories, and any module where the picture should lead before the reader enters the copy.
1017
+
1018
+ The copy zone uses `text-panel-body` for consistent prose/bullet handling across all editorial modules.
1019
+
1020
+ ```html
1021
+ <div class="editorial-image-top">
1022
+ <div class="media-frame editorial-media">
1023
+ <img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=1200&auto=format&fit=crop" alt="Climber on alpine ridge">
1024
+ </div>
1025
+ <div class="editorial-module-body">
1026
+ <div class="module-kicker-row">
1027
+ <i data-lucide="leaf" class="module-icon"></i>
1028
+ <p class="caption">Alpine materials</p>
1029
+ </div>
1030
+ <h3>Use the image to set tone before the copy explains the point.</h3>
1031
+ <!-- text-panel-body: place <p>, <ul class="editorial-list">, or both -->
1032
+ <div class="text-panel-body">
1033
+ <p>Choose this component when the visual should establish texture, materiality, or field context before the audience reads the narrative.</p>
1034
+ <!-- optional bullets:
1035
+ <ul class="editorial-list">
1036
+ <li><strong>Lead phrase.</strong> Supporting point.</li>
1037
+ <li><strong>Second point.</strong> One sentence.</li>
1038
+ </ul>
1039
+ -->
1040
+ </div>
1041
+ </div>
1042
+ </div>
1043
+ ```
1044
+
1045
+ ```css
1046
+ .editorial-media {
1047
+ width: 100%;
1048
+ }
1049
+
1050
+ .editorial-module-body {
1051
+ display: flex;
1052
+ flex-direction: column;
1053
+ gap: 12px;
1054
+ }
1055
+
1056
+ .module-kicker-row {
1057
+ display: inline-flex;
1058
+ align-items: center;
1059
+ gap: 8px;
1060
+ }
1061
+
1062
+ .module-icon {
1063
+ width: 15px;
1064
+ height: 15px;
1065
+ stroke-width: 1.6;
1066
+ color: var(--accent-earth);
1067
+ flex-shrink: 0;
1068
+ }
1069
+
1070
+ .editorial-image-top {
1071
+ display: flex;
1072
+ flex-direction: column;
1073
+ gap: 16px;
1074
+ }
1075
+
1076
+ .editorial-image-top .editorial-media {
1077
+ height: 240px;
1078
+ }
1079
+
1080
+ .editorial-image-top .editorial-module-body h3 {
1081
+ margin-top: 2px;
1082
+ }
1083
+ ```
1084
+
1085
+ Rules:
1086
+ - Use when the image should lead and the text should read as a follow-on explanation.
1087
+ - The image should usually occupy more visual weight than the text.
1088
+ - Icon is optional. If used, keep it small, single-color, and subordinate to the caption.
1089
+ - The `text-panel-body` inside `.editorial-module-body` accepts prose `<p>`, an `<ul class="editorial-list">`, or both. Use whichever form suits the content.
1090
+ - When used inside multi-column layouts, keep this component's copy shorter than the primary reading column unless the page hierarchy explicitly promotes it.
1091
+
1092
+ ##### Tips
1093
+ - **Do not set a fixed height on this component when used inside `highlight-cols`.** Let the parent grid's `align-items:stretch` control the column height. Fixed heights fight against the stretch and create misaligned baselines.
1094
+ - **Image aspect ratio.** Aim for 16:9 or 3:2 crops for the image block. Portrait crops create tall image zones that push text down and unbalance the composition.
1095
+ - **Kicker icon size.** Keep Lucide SVG icons at 16–20px. Larger icons shift visual weight from the image to the label zone.
1096
+ - **`editorial-list` font-size.** Override to `font-size:13px;gap:10px` inline when used inside a very narrow copy zone — the base `editorial-list` is `17px/gap:14px`, which may be too large for tight columns.
1097
+ <!-- @component:editorial-image-top:end -->
1098
+
1099
+ <!-- @component:editorial-text-top:start -->
1100
+ #### Editorial Text Top
1101
+
1102
+ Text-first editorial module: text on top, image below. Best for narrative snippets, report-style explanations, or blocks where the image serves as evidence rather than the primary hook.
1103
+
1104
+ The copy zone uses `text-panel-body` for consistent prose/bullet handling across all editorial modules.
1105
+
1106
+ ```html
1107
+ <div class="editorial-text-top">
1108
+ <div class="editorial-module-body">
1109
+ <div class="module-kicker-row">
1110
+ <i data-lucide="gauge" class="module-icon"></i>
1111
+ <p class="caption">Mountain operations</p>
1112
+ </div>
1113
+ <h3>Lead with the argument, then let the image confirm it.</h3>
1114
+ <!-- text-panel-body: place <p>, <ul class="editorial-list">, or both -->
1115
+ <div class="text-panel-body">
1116
+ <p>Use this component when the audience should understand the point first and only then read the image as supporting evidence or context.</p>
1117
+ <!-- optional bullets:
1118
+ <ul class="editorial-list">
1119
+ <li><strong>Lead phrase.</strong> Supporting point.</li>
1120
+ <li><strong>Second point.</strong> One sentence.</li>
1121
+ </ul>
1122
+ -->
1123
+ </div>
1124
+ </div>
1125
+ <div class="media-frame editorial-media">
1126
+ <img src="https://images.unsplash.com/photo-1519904981063-b0cf448d479e?q=80&w=1200&auto=format&fit=crop" alt="Hiker in alpine basin">
1127
+ </div>
1128
+ <p class="media-caption">Optional caption / field site / program name</p>
1129
+ </div>
1130
+ ```
1131
+
1132
+ ```css
1133
+ .editorial-text-top {
1134
+ display: flex;
1135
+ flex-direction: column;
1136
+ gap: 16px;
1137
+ }
1138
+
1139
+ .editorial-text-top .editorial-module-body {
1140
+ padding-bottom: 2px;
1141
+ }
1142
+
1143
+ .editorial-text-top .editorial-media {
1144
+ height: 240px;
1145
+ }
1146
+
1147
+ .editorial-text-top .media-caption {
1148
+ margin-top: -4px;
1149
+ }
1150
+ ```
1151
+
1152
+ Rules:
1153
+ - Use when the text must establish the idea before the image appears.
1154
+ - The `text-panel-body` inside `.editorial-module-body` accepts prose `<p>`, an `<ul class="editorial-list">`, or both. Use whichever form suits the content.
1155
+ - Keep the lower image block quieter than a hero image; it is supporting evidence, not the whole slide's focal point.
1156
+ - Icon is optional. Prefer it only when it helps distinguish categories across repeated modules.
1157
+ - In multi-column layouts, this component can carry more reading weight than neighboring support modules when the page hierarchy needs a denser narrative block.
1158
+
1159
+ ##### Tips
1160
+ - **Same height/stretch rule as `editorial-image-top`.** Do not set fixed heights; let parent grid stretch control the column.
1161
+ - **When used as a center spine in `highlight-cols`,** this is the one component that may legitimately be taller than its neighbors. That density imbalance is intentional — do not try to equalize it with padding or extra content in the outer columns.
1162
+ - **`editorial-list` font-size.** Override to `font-size:13px;gap:10px` inline when used inside a very narrow copy zone — the base `editorial-list` is `17px/gap:14px`, which may be too large for tight columns.
1163
+ <!-- @component:editorial-text-top:end -->
1164
+
1165
+ <!-- @component:editorial-text-left:start -->
1166
+ #### Editorial Text Left
1167
+
1168
+ Horizontal editorial module: a full-width title band on top, with text on the left and a visual slot on the right below. Best for compact feature rows or any slot where a wide-but-short frame suits a side-by-side composition with a clear heading above.
1169
+
1170
+ Structure:
1171
+ - **header zone** (full width): holds the `h3` module title — independent of the copy below
1172
+ - **left: `.editorial-text-left-copy`** — kicker, then `text-panel-body` (prose, bullets, or both)
1173
+ - **right: `.editorial-text-left-visual`** — accepts any of: `media-frame img`, `echart-container`, or `image-title`
1174
+
1175
+ ```html
1176
+ <div class="editorial-text-left">
1177
+
1178
+ <!-- header: module title spans full width -->
1179
+ <div class="editorial-text-left-header">
1180
+ <h3 style="font-size:20px;line-height:1.08;">Module title — a single standalone heading above both columns</h3>
1181
+ </div>
1182
+
1183
+ <div class="editorial-text-left-content">
1184
+
1185
+ <!-- left: editorial copy -->
1186
+ <div class="editorial-text-left-copy">
1187
+ <div class="module-kicker-row">
1188
+ <i data-lucide="zap" class="module-icon"></i>
1189
+ <p class="caption">Category label</p>
1190
+ </div>
1191
+ <!-- text-panel-body: place <p>, <ul class="editorial-list">, or both — choose based on content -->
1192
+ <div class="text-panel-body" style="margin-top:12px;">
1193
+ <!-- prose variant -->
1194
+ <p style="font-size:17px;line-height:1.5;color:var(--text-secondary);">Supporting description. One or two sentences that position this card within the broader page argument.</p>
1195
+ <!-- bullet variant (use instead of or after prose): -->
1196
+ <!-- <ul class="editorial-list" style="font-size:13px;gap:10px;">
1197
+ <li><strong>Lead phrase.</strong> Supporting explanation for this point.</li>
1198
+ <li><strong>Second point.</strong> One sentence of context or evidence.</li>
1199
+ <li><strong>Third point.</strong> Keep each item roughly equal in length.</li>
1200
+ </ul> -->
1201
+ </div>
1202
+ </div>
1203
+
1204
+ <!-- right: visual slot — choose one -->
1205
+ <div class="editorial-text-left-visual">
1206
+
1207
+ <!-- option A: plain image -->
1208
+ <div class="media-frame" style="width:100%;height:100%;">
1209
+ <img src="https://images.unsplash.com/photo-1519904981063-b0cf448d479e?q=80&w=800&auto=format&fit=crop" alt="Supporting visual">
1210
+ </div>
1211
+
1212
+ <!-- option B: echart -->
1213
+ <!-- <div class="echart-container" id="chart-unique-id" style="width:100%;height:100%;"></div> -->
1214
+
1215
+ <!-- option C: image-title (self-contained, handles its own overlay and text layers) -->
1216
+ <!-- <div class="image-title image-title--right"> ... </div> -->
1217
+
1218
+ </div>
1219
+
1220
+ </div>
1221
+ </div>
1222
+ ```
1223
+
1224
+ ```css
1225
+ .editorial-text-left {
1226
+ display: flex;
1227
+ flex-direction: column;
1228
+ gap: 0;
1229
+ height: 100%;
1230
+ overflow: hidden;
1231
+ }
1232
+
1233
+ .editorial-text-left-header {
1234
+ flex-shrink: 0;
1235
+ padding: 20px 24px 16px;
1236
+ }
1237
+
1238
+ .editorial-text-left-content {
1239
+ display: flex;
1240
+ flex: 1;
1241
+ min-height: 0;
1242
+ }
1243
+
1244
+ .editorial-text-left-copy {
1245
+ flex: 1.1;
1246
+ min-width: 0;
1247
+ padding: 16px 20px 20px 24px;
1248
+ display: flex;
1249
+ flex-direction: column;
1250
+ justify-content: flex-start;
1251
+ }
1252
+
1253
+ .editorial-text-left-visual {
1254
+ flex: 1;
1255
+ min-width: 0;
1256
+ min-height: 0;
1257
+ align-self: stretch;
1258
+ overflow: hidden;
1259
+ position: relative;
1260
+ }
1261
+ ```
1262
+
1263
+ Rules:
1264
+ - The `h3` in `.editorial-text-left-header` is the module's top-level title; it must not be repeated inside the copy zone.
1265
+ - The left copy zone holds a kicker row followed by a `text-panel-body`. The body accepts prose `<p>`, an `<ul class="editorial-list">`, or both — choose based on content.
1266
+ - The right visual slot is open: use a plain `media-frame img`, an `echart-container`, or a full `image-title` component. Choose based on content — there is no default.
1267
+ - When using `editorial-list` inside `text-panel-body`, add `<strong>` around the first 2–5 words of each `<li>` to create a bold lead phrase for scannable hierarchy.
1268
+ - When the card carries a large statistic or callout number, place it between the header and the copy zone using an inline style (`font-size: 48px; font-family: var(--font-display); font-weight: 700; color: var(--accent-gold); line-height: 1;`).
1269
+
1270
+ ##### Tips
1271
+ - **Parent must supply height.** `.editorial-text-left` uses `height: 100%` and `flex: 1` internally. The parent slot must have a defined height (grid cell, `height:100%` chain, or `flex:1;min-height:0`).
1272
+ - **Text-to-visual flex ratio.** Default is `1.1 : 1` (copy slightly wider). For more copy, try `1.3 : 1`. For a visually dominant right panel, try `1 : 1.2`. Do not go below `0.8` on the copy side.
1273
+ - **`editorial-list` font-size inside copy zone.** Override to `font-size:13px` and `gap:10px` inline when the copy column is very narrow — the base `editorial-list` uses `17px / gap:14px`.
1274
+ - **`echart-container` in visual slot.** Set `width:100%;height:100%` on the container and call `echarts.init()` after `SlidePresentation` is instantiated. The `position:relative;overflow:hidden` on `.editorial-text-left-visual` contains the canvas correctly.
1275
+ - **`image-title` in visual slot.** The component is self-contained and fills `width:100%;height:100%` automatically. Use `image-title--right` modifier with a bottom-heavy overlay and right-biased blur mask for the most common editorial orientation.
1276
+ - **Dark background.** Override CSS variables on `.editorial-text-left` to cascade into both the copy and visual zones: `--text-primary`, `--text-secondary`, `--text-muted`, `--line`, `--line-strong` — all set to white-family values.
1277
+ <!-- @component:editorial-text-left:end -->
1278
+
1279
+ <!-- @component:echart-panel:start -->
1280
+ #### EChart Panel
1281
+
1282
+ Chart layout frame for data visualisation. Defines the structural container and header zone; the chart type (bar, line, donut, scatter, heatmap, treemap, etc.) is chosen by the LLM based on the data.
1283
+
1284
+ ```html
1285
+ <div class="echart-panel">
1286
+ <div class="echart-panel-header">
1287
+ <p class="eyebrow">SECTION LABEL</p>
1288
+ <h3>Chart Title</h3>
1289
+ <p class="chart-subtitle">Optional context or unit note</p>
1290
+ </div>
1291
+ <div class="echart-container" id="chart-01"></div>
1292
+ <p class="chart-caption">Source: Organisation / Year</p>
1293
+ </div>
1294
+
1295
+ <script>
1296
+ // Include ECharts in the HTML head only when charts are present:
1297
+ // <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
1298
+ // Initialise after SlidePresentation is instantiated:
1299
+ const chart = echarts.init(document.getElementById('chart-01'));
1300
+ chart.setOption({ /* LLM selects type and config */ });
1301
+ </script>
1302
+ ```
1303
+
1304
+ ```css
1305
+ .echart-panel {
1306
+ display: flex;
1307
+ flex-direction: column;
1308
+ height: 100%;
1309
+ gap: 0;
1310
+ }
1311
+
1312
+ .echart-panel-header {
1313
+ flex-shrink: 0;
1314
+ padding-bottom: 16px;
1315
+ border-bottom: 1px solid var(--line);
1316
+ margin-bottom: 20px;
1317
+ }
1318
+
1319
+ .echart-panel-header h3 {
1320
+ margin-top: 6px;
1321
+ }
1322
+
1323
+ .chart-subtitle {
1324
+ margin-top: 4px;
1325
+ font-size: 13px;
1326
+ color: var(--text-muted);
1327
+ line-height: 1.4;
1328
+ }
1329
+
1330
+ .echart-container {
1331
+ flex: 1;
1332
+ min-height: 0; /* required inside flex to let ECharts fill correctly */
1333
+ }
1334
+
1335
+ .chart-caption {
1336
+ flex-shrink: 0;
1337
+ margin-top: 12px;
1338
+ font-size: 11px;
1339
+ letter-spacing: 0.12em;
1340
+ text-transform: uppercase;
1341
+ color: var(--text-muted);
1342
+ }
1343
+ ```
1344
+
1345
+ Rules:
1346
+ - Always include `min-height: 0` on `.echart-container`; without it ECharts overflows flex containers.
1347
+ - Set `id` per chart to avoid collision when multiple charts appear in one slide.
1348
+ - Choose chart type, colour palette, and axis config based on the data. Monet palette suggestion: use `--accent-earth` (`#2980AF`), `--accent-olive` (`#00A2B0`), and `--accent-stone` (`#83A5BA`) as the primary series colours.
1349
+ - Add ECharts script tag to `<head>` only when charts are present in the slide deck; do not include it unconditionally.
1350
+ - `echarts.init()` must run after the DOM is ready and after `SlidePresentation` is instantiated.
1351
+
1352
+ ##### Tips
1353
+ - **Dark background: always use `backgroundColor:'transparent'` in `setOption`.** Do not rely on CSS background — ECharts renders to a canvas element and ignores inherited CSS background.
1354
+ - **Dark background: override ALL text colors in the ECharts option.** Axis labels, legend text, axis line colors, and tooltip styles do not inherit from CSS. Set them explicitly: `rgba(240,244,247,0.65)` for labels, `rgba(240,244,247,0.2)` for grid lines.
1355
+ - **Candlestick on dark background.** Use `--accent-olive` (`#00A2B0`) for up candles and `--accent-danger` (`#E593B4`) for down candles. These read clearly against dark backgrounds and stay within the Monet palette.
1356
+ - **`height:100%` vs fixed pixel height.** Use `flex:1;min-height:0` when the chart should fill available space automatically (e.g., inside `split-dashboard`). Use a fixed pixel height (e.g., `height:380px`) only when you need to cap the chart to a specific proportion of the slide.
1357
+ - **Chart sizing for `narrative-hero-left-dark`.** The left 7.8fr column is wide. A donut or candlestick chart works best centered with some breathing room. Add `padding: 24px 32px` to `.echart-container` to prevent the chart from touching the column edges.
1358
+ <!-- @component:echart-panel:end -->
1359
+
1360
+ <!-- @component:flow-horizontal:start -->
1361
+ #### Flow Horizontal
1362
+
1363
+ Horizontal step or phase sequence. Use for process stages, numbered definitions, or parallel concepts that should be read left to right. Suitable for 2–5 items.
1364
+
1365
+ ```html
1366
+ <div class="flow-horizontal">
1367
+ <div class="flow-item">
1368
+ <div class="flow-number" data-n="01" style="--fn-rot:-9deg;"></div>
1369
+ <div class="flow-body">
1370
+ <h4>Step Title</h4>
1371
+ <p>Brief description of this step or phase.</p>
1372
+ </div>
1373
+ </div>
1374
+ <div class="flow-item">
1375
+ <div class="flow-number" data-n="02" style="--fn-rot:7deg;"></div>
1376
+ <div class="flow-body">
1377
+ <h4>Step Title</h4>
1378
+ <p>Brief description of this step or phase.</p>
1379
+ </div>
1380
+ </div>
1381
+ <div class="flow-item">
1382
+ <div class="flow-number" data-n="03" style="--fn-rot:-6deg;"></div>
1383
+ <div class="flow-body">
1384
+ <h4>Step Title</h4>
1385
+ <p>Brief description of this step or phase.</p>
1386
+ </div>
1387
+ </div>
1388
+ </div>
1389
+ ```
1390
+
1391
+ ```css
1392
+ /* Shared by flow-horizontal and flow-vertical */
1393
+ .flow-number {
1394
+ position: relative;
1395
+ width: 36px;
1396
+ height: 36px;
1397
+ flex-shrink: 0;
1398
+ }
1399
+
1400
+ /* bottom square — white/neutral base */
1401
+ .flow-number::before {
1402
+ content: '';
1403
+ position: absolute;
1404
+ inset: 0;
1405
+ background: #fff;
1406
+ border: 1px solid var(--line-strong);
1407
+ }
1408
+
1409
+ /* top square — accent color, rotated via --fn-rot CSS variable */
1410
+ .flow-number::after {
1411
+ content: attr(data-n);
1412
+ position: absolute;
1413
+ inset: 0;
1414
+ background: var(--accent-earth);
1415
+ display: flex;
1416
+ align-items: center;
1417
+ justify-content: center;
1418
+ font-family: var(--font-display);
1419
+ font-size: 13px;
1420
+ font-weight: 700;
1421
+ letter-spacing: 0.1em;
1422
+ color: #fff;
1423
+ transform: rotate(var(--fn-rot, -8deg));
1424
+ transform-origin: center;
1425
+ }
1426
+
1427
+ .flow-body h4 {
1428
+ font-size: 20px;
1429
+ font-weight: 600;
1430
+ line-height: 1.14;
1431
+ }
1432
+
1433
+ .flow-body p {
1434
+ font-size: 17px;
1435
+ line-height: 1.6;
1436
+ color: var(--text-secondary);
1437
+ }
1438
+
1439
+ .flow-horizontal {
1440
+ position: relative;
1441
+ display: flex;
1442
+ align-items: flex-start;
1443
+ width: 100%;
1444
+ }
1445
+
1446
+ .flow-horizontal::before {
1447
+ content: '';
1448
+ position: absolute;
1449
+ top: 18px;
1450
+ left: 0;
1451
+ right: 0;
1452
+ height: 1px;
1453
+ background: var(--line-strong);
1454
+ z-index: 0;
1455
+ }
1456
+
1457
+ .flow-horizontal .flow-item {
1458
+ flex: 1;
1459
+ display: flex;
1460
+ flex-direction: column;
1461
+ gap: 18px;
1462
+ padding-right: 40px;
1463
+ }
1464
+
1465
+ .flow-horizontal .flow-item:last-child {
1466
+ padding-right: 0;
1467
+ }
1468
+
1469
+ .flow-horizontal .flow-number {
1470
+ position: relative;
1471
+ z-index: 1;
1472
+ }
1473
+
1474
+ .flow-horizontal .flow-number::before {
1475
+ background: var(--bg-page);
1476
+ }
1477
+ ```
1478
+
1479
+ Rules:
1480
+ - Prefer 3–4 items for balanced layout; 5 items work when copy is very short.
1481
+ - Do not use arrowheads or chevrons between items; the horizontal rule threading through the numbers is the only connector.
1482
+ - Number labels are report-style (`01`, `02`, `03`), not circles or bullets.
1483
+ - Keep each item's body copy short — this is a reference summary, not a detailed explanation.
1484
+
1485
+ ##### Tips
1486
+ - **Dark background color overrides.** Flow-number: `border-color:rgba(240,244,247,0.3); color:rgba(240,244,247,0.6); background:<dark-bg-color>`. Heading h4: `color:#f0f4f7`. Body p: `color:rgba(240,244,247,0.7)`. Apply inline on each element — CSS cascade does not automatically inherit from the slide background.
1487
+ - **Step copy length directly affects column balance.** One step with a long paragraph will push its column taller than the others and break the horizontal rhythm. Trim all steps to roughly equal length (2–4 lines each).
1488
+ - **Number card design.** Each `.flow-number` uses two overlapping squares: a white `::before` base square (with border) and a blue `::after` top square (`--accent-earth`) rotated by `--fn-rot`. Set `data-n="01"` on the element (the `::after` reads it via `content: attr(data-n)`). Vary `--fn-rot` per item (e.g. `-9deg`, `7deg`, `-6deg`, `10deg`) for a hand-placed impression.
1489
+ - **Horizontal rule connector.** The `::before` pseudo-element on `.flow-horizontal` draws a full-width line at `top: 18px` (vertical centre of the 36px number box). `.flow-number` sits above it via `z-index: 1`; its `::before` background is set to `var(--bg-page)` to mask the line behind the white square. On dark backgrounds, override `.flow-horizontal .flow-number::before { background: <dark-bg-color>; }` and set `.flow-horizontal::before { background: rgba(240,244,247,0.15); }`.
1490
+ <!-- @component:flow-horizontal:end -->
1491
+
1492
+ <!-- @component:flow-vertical:start -->
1493
+ #### Flow Vertical
1494
+
1495
+ Vertical step or timeline sequence. Use for chronological phases, execution stages, or progress narratives that should be read top to bottom. Suitable for 2–6 items.
1496
+
1497
+ ```html
1498
+ <div class="flow-vertical">
1499
+ <div class="flow-item">
1500
+ <div class="flow-marker">
1501
+ <div class="flow-number" data-n="01" style="--fn-rot:-8deg;"></div>
1502
+ <div class="flow-line"></div>
1503
+ </div>
1504
+ <div class="flow-body">
1505
+ <h4>Phase Title</h4>
1506
+ <p>Description of this stage or milestone.</p>
1507
+ </div>
1508
+ </div>
1509
+ <div class="flow-item">
1510
+ <div class="flow-marker">
1511
+ <div class="flow-number" data-n="02" style="--fn-rot:11deg;"></div>
1512
+ <div class="flow-line"></div>
1513
+ </div>
1514
+ <div class="flow-body">
1515
+ <h4>Phase Title</h4>
1516
+ <p>Description of this stage or milestone.</p>
1517
+ </div>
1518
+ </div>
1519
+ <div class="flow-item last">
1520
+ <div class="flow-marker">
1521
+ <div class="flow-number" data-n="03" style="--fn-rot:-6deg;"></div>
1522
+ <!-- no flow-line on last item -->
1523
+ </div>
1524
+ <div class="flow-body">
1525
+ <h4>Phase Title</h4>
1526
+ <p>Description of this stage or milestone.</p>
1527
+ </div>
1528
+ </div>
1529
+ </div>
1530
+ ```
1531
+
1532
+ ```css
1533
+ .flow-vertical {
1534
+ display: flex;
1535
+ flex-direction: column;
1536
+ width: 100%;
1537
+ }
1538
+
1539
+ .flow-vertical .flow-item {
1540
+ display: flex;
1541
+ gap: 28px;
1542
+ align-items: flex-start;
1543
+ }
1544
+
1545
+ .flow-vertical .flow-marker {
1546
+ display: flex;
1547
+ flex-direction: column;
1548
+ align-items: center;
1549
+ flex-shrink: 0;
1550
+ }
1551
+
1552
+ .flow-vertical .flow-line {
1553
+ width: 1px;
1554
+ flex: 1;
1555
+ min-height: 28px;
1556
+ background: var(--line-strong);
1557
+ margin: 6px 0;
1558
+ }
1559
+
1560
+ .flow-vertical .flow-body {
1561
+ padding-bottom: 32px;
1562
+ }
1563
+
1564
+ .flow-vertical .flow-item.last .flow-body {
1565
+ padding-bottom: 0;
1566
+ }
1567
+
1568
+ .flow-vertical .flow-body h4 {
1569
+ margin-top: 4px;
1570
+ }
1571
+
1572
+ .flow-vertical .flow-body p {
1573
+ margin-top: 8px;
1574
+ }
1575
+ ```
1576
+
1577
+ Rules:
1578
+ - Add class `last` to the final `.flow-item` and omit the `.flow-line` div inside it.
1579
+ - The connecting line grows to fill the vertical space between items via `flex: 1`.
1580
+ - Number boxes share the same border-only square style as `flow-horizontal` for visual consistency.
1581
+ - Copy per item can be slightly longer than horizontal flow since vertical reading allows more density.
1582
+ - Combine with `text-panel` or `echart-panel` on the opposing side of a layout when needed.
1583
+
1584
+ ##### Tips
1585
+ - **`.last` class on final item is mandatory.** Without it, the connecting line extends past the last item and exits the component boundary. Always add `.last` to the final `div.flow-item`.
1586
+ - **Dark background with background image.** When the column containing `flow-vertical` has a background image, use the same three-layer z-index pattern: background `z-index:0`, dark scrim `z-index:1`, component content `z-index:2`. Set the parent column to `position:relative;overflow:hidden`.
1587
+ - **Number card design (same as flow-horizontal).** Use `data-n="01"` and `style="--fn-rot:-8deg;"` on each `.flow-number`. Vary the rotation angle per item.
1588
+ - **Dark text overrides.** Flow-number `::before` border: `rgba(240,244,247,0.3)`; `::after` background: a darker accent or `rgba(41,128,175,0.85)`. h4: `color:#f0f4f7`; p: `color:rgba(240,244,247,0.7)`. Also override the connecting line: `background:rgba(240,244,247,0.2)`.
1589
+ - **Column height constraint.** `flow-vertical` expands naturally with content. In a two-column layout, ensure the opposing column (`text-panel` or `echart-panel`) has enough content to avoid a large height mismatch.
1590
+ <!-- @component:flow-vertical:end -->
1591
+
1592
+ <!-- @component:data-table:start -->
1593
+ #### Data Table
1594
+
1595
+ Annual-report format data table. Use for year-on-year comparisons, emissions data, supply chain figures, and any structured numeric dataset that requires legible column alignment. Adjust `font-size` and cell `padding` to suit density needs — no separate component required for compact variants.
1596
+
1597
+ ```html
1598
+ <div class="data-table-wrap">
1599
+ <p class="data-table-label">Key Figures — Income Statement</p>
1600
+ <table class="data-table">
1601
+ <thead>
1602
+ <tr>
1603
+ <th>Scope</th>
1604
+ <th>2021</th>
1605
+ <th>2022</th>
1606
+ <th class="col-highlight">2023</th>
1607
+ <th class="col-highlight">2024</th>
1608
+ <th>YoY</th>
1609
+ </tr>
1610
+ </thead>
1611
+ <tbody>
1612
+ <tr class="section-header">
1613
+ <td colspan="6">Direct Emissions</td>
1614
+ </tr>
1615
+ <tr>
1616
+ <td>Scope 1</td>
1617
+ <td>1,329.2</td>
1618
+ <td>1,273.4</td>
1619
+ <td class="col-highlight">1,156.4</td>
1620
+ <td class="col-highlight">1,042.0</td>
1621
+ <td class="delta positive">−10%</td>
1622
+ </tr>
1623
+ <tr>
1624
+ <td>Scope 2</td>
1625
+ <td>1,617.8</td>
1626
+ <td>1,432.9</td>
1627
+ <td class="col-highlight">820.0</td>
1628
+ <td class="col-highlight">0.0</td>
1629
+ <td class="delta positive">−100%</td>
1630
+ </tr>
1631
+ <tr class="subtotal">
1632
+ <td>1+2 (net)</td>
1633
+ <td>2,905.5</td>
1634
+ <td>3,286.3</td>
1635
+ <td class="col-highlight">1,976.4</td>
1636
+ <td class="col-highlight">1,042.0</td>
1637
+ <td class="delta positive">−47%</td>
1638
+ </tr>
1639
+ </tbody>
1640
+ </table>
1641
+ <p class="table-caption">Thousands of tonnes CO₂e · Source: Company Disclosures 2024</p>
1642
+ </div>
1643
+ ```
1644
+
1645
+ ```css
1646
+ .data-table-wrap {
1647
+ width: 100%;
1648
+ }
1649
+
1650
+ .data-table-label {
1651
+ font-size: 10px;
1652
+ font-weight: 700;
1653
+ letter-spacing: 0.14em;
1654
+ text-transform: uppercase;
1655
+ color: var(--text-muted);
1656
+ margin-bottom: 8px;
1657
+ }
1658
+
1659
+ .data-table {
1660
+ width: 100%;
1661
+ border-collapse: collapse;
1662
+ font-family: var(--font-body);
1663
+ font-size: 17px;
1664
+ font-variant-numeric: tabular-nums;
1665
+ color: var(--text-primary);
1666
+ }
1667
+
1668
+ .data-table thead tr {
1669
+ border-bottom: 1.5px solid var(--line-strong);
1670
+ }
1671
+
1672
+ .data-table th {
1673
+ padding: 0 12px 10px 0;
1674
+ text-align: left;
1675
+ font-size: 13px;
1676
+ font-weight: 600;
1677
+ letter-spacing: 0.1em;
1678
+ text-transform: uppercase;
1679
+ color: var(--text-muted);
1680
+ white-space: nowrap;
1681
+ }
1682
+
1683
+ .data-table th:not(:first-child),
1684
+ .data-table td:not(:first-child) {
1685
+ text-align: right;
1686
+ }
1687
+
1688
+ .data-table th.col-highlight,
1689
+ .data-table td.col-highlight {
1690
+ color: var(--text-primary);
1691
+ background: rgba(23, 20, 17, 0.05);
1692
+ padding-left: 6px;
1693
+ padding-right: 8px;
1694
+ }
1695
+
1696
+ .data-table th.col-highlight {
1697
+ color: var(--text-secondary);
1698
+ }
1699
+
1700
+ .data-table tbody tr {
1701
+ border-bottom: 1px solid var(--line);
1702
+ }
1703
+
1704
+ .data-table tbody tr:last-child {
1705
+ border-bottom: none;
1706
+ }
1707
+
1708
+ .data-table td {
1709
+ padding: 9px 12px 9px 0;
1710
+ line-height: 1.4;
1711
+ color: var(--text-secondary);
1712
+ }
1713
+
1714
+ .data-table tr.subtotal td {
1715
+ font-weight: 600;
1716
+ color: var(--text-primary);
1717
+ border-top: 1px solid var(--line-strong);
1718
+ border-bottom: 1px solid var(--line-strong);
1719
+ }
1720
+
1721
+ .data-table tr.section-header td {
1722
+ font-size: 12px;
1723
+ font-weight: 700;
1724
+ letter-spacing: 0.10em;
1725
+ text-transform: uppercase;
1726
+ color: var(--text-muted);
1727
+ padding-top: 14px;
1728
+ padding-bottom: 4px;
1729
+ border-bottom: none;
1730
+ }
1731
+
1732
+ .data-table .delta {
1733
+ font-weight: 600;
1734
+ white-space: nowrap;
1735
+ }
1736
+
1737
+ .data-table .delta.positive {
1738
+ color: var(--accent-olive);
1739
+ }
1740
+
1741
+ .data-table .delta.negative {
1742
+ color: var(--accent-danger);
1743
+ }
1744
+
1745
+ .data-table .delta.neutral {
1746
+ color: var(--text-muted);
1747
+ }
1748
+
1749
+ .table-caption {
1750
+ margin-top: 12px;
1751
+ font-size: 11px;
1752
+ letter-spacing: 0.1em;
1753
+ text-transform: uppercase;
1754
+ color: var(--text-muted);
1755
+ }
1756
+ ```
1757
+
1758
+ Rules:
1759
+ - No outer border. Separation is by `border-bottom` on rows only.
1760
+ - Numeric columns are right-aligned; the label column is left-aligned.
1761
+ - Use `tabular-nums` for number columns so decimals align vertically.
1762
+ - Use `.subtotal` on summary rows (totals, net figures) — heavier weight and double-rule border.
1763
+ - Use `.section-header` rows to group rows into labelled categories within a single table (no data, just a label spanning all columns).
1764
+ - Use `.col-highlight` on both `th` and `td` in a column to spotlight the current or most important period.
1765
+ - `.delta.positive` and `.delta.negative` use Monet accent colours, not generic green/red. `.delta.neutral` for flat movement.
1766
+ - Use `data-table-label` as a heading above the table when multiple tables are stacked.
1767
+ - Include `.table-caption` below with the data source and unit.
1768
+
1769
+ ##### Tips
1770
+ - **Compact variant.** For high-density datasets, reduce `.data-table` to `font-size:11px` and `.data-table td` padding to `6px 8px 6px 0`. No separate component needed.
1771
+ - **Dark background: override CSS variables on `.data-table-wrap`.** Set `--text-primary:#f0f4f7`, `--text-secondary:rgba(240,244,247,0.7)`, `--text-muted:rgba(240,244,247,0.45)`, `--line:rgba(240,244,247,0.12)`, `--line-strong:rgba(240,244,247,0.28)`. All child elements inherit automatically via `var()`.
1772
+ - **`col-highlight` on dark.** Override background on `.data-table-wrap`: `.data-table th.col-highlight, .data-table td.col-highlight { background: rgba(240,244,247,0.06); }`. Also override `--accent-earth` → `var(--accent-gold)` so highlight header color remains visible.
1773
+ - **Delta positive on dark.** Override `--accent-olive` → `#7CBDC3` on `.data-table-wrap`. The default `--accent-olive` (#00A2B0) may be too vivid against some dark backgrounds.
1774
+ - **`.table-caption` on dark.** Set explicitly: `color:rgba(240,244,247,0.45)`. It does not inherit from the CSS variable override on the wrapper.
1775
+ - **Two stacked tables.** Add `margin-top:18px` between `.data-table-wrap` blocks and use `data-table-label` on each. Do not add a horizontal rule between them — the label serves as the visual separator.
1776
+ <!-- @component:data-table:end -->
1777
+
1778
+
1779
+
1780
+
1781
+
1782
+
1783
+
1784
+ <!-- @component:image-title:start -->
1785
+ #### Image Title
1786
+
1787
+ Self-contained full-canvas component: a dominant photograph with a directional blur layer, a gradient overlay, and a foreground text stack — all composited inside one element. Use for cover slides, closing slides, atmospheric section dividers, or any full-bleed spread where a single image should dominate the entire canvas.
1788
+
1789
+ The LLM controls three key variables via inline style or modifier class:
1790
+ - **Alignment**: `image-title--left` (cover default), `image-title--right` (closing default), `image-title--center`
1791
+ - **Overlay opacity / direction**: set the `background` gradient on `.image-title-overlay` inline
1792
+ - **Text opacity**: set `opacity` on `.image-title` itself (range `0.7`–`1.0`; default `1.0`)
1793
+
1794
+ ```html
1795
+ <div class="image-title image-title--left">
1796
+
1797
+ <!-- Layer 0: background image -->
1798
+ <img class="image-title-media" src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=1920&auto=format&fit=crop" alt="Alpine ridge at dawn">
1799
+
1800
+ <!-- Layer 1: directional blur — mask gradient concentrates blur on the text side -->
1801
+ <!-- cover (--left): mask to-left → blur left, sharp right -->
1802
+ <!-- closing (--right): mask to-right → blur right, sharp left -->
1803
+ <div class="image-title-blur" style="-webkit-mask-image:linear-gradient(to left, transparent 0%, transparent 20%, black 100%);mask-image:linear-gradient(to left, transparent 0%, transparent 20%, black 100%);"></div>
1804
+
1805
+ <!-- Layer 2: gradient overlay — adjust direction and opacity to suit slide purpose -->
1806
+ <!-- cover default: linear-gradient(105deg, rgba(5,5,5,0.95) 0%, rgba(5,5,5,0.72) 55%, rgba(5,5,5,0.10) 100%) -->
1807
+ <!-- closing default: linear-gradient(180deg, rgba(0,0,0,0.30) 0%, rgba(0,0,0,0.72) 100%) -->
1808
+ <div class="image-title-overlay" style="background:linear-gradient(105deg, rgba(5,5,5,0.95) 0%, rgba(5,5,5,0.72) 55%, rgba(5,5,5,0.10) 100%);"></div>
1809
+
1810
+ <!-- Layer 3: foreground text -->
1811
+ <div class="image-title-fg">
1812
+ <div class="image-title-brand reveal">
1813
+ <span class="chevron-divider" style="color:rgba(240,244,247,0.55);">Organisation Name</span>
1814
+ </div>
1815
+ <div class="image-title-body">
1816
+ <p class="image-title-eyebrow reveal">Report Title · Year</p>
1817
+ <h1 class="reveal">Opening<br>Statement<br>Here.</h1>
1818
+ <p class="image-title-subtitle reveal">One or two lines of supporting copy. Keep it short — the image does the visual work.</p>
1819
+ </div>
1820
+ <div class="image-title-footer">
1821
+ <p class="caption reveal" style="color:rgba(240,244,247,0.5);">website or location</p>
1822
+ <p class="caption reveal" style="color:rgba(240,244,247,0.5);">Organisation · Year</p>
1823
+ </div>
1824
+ </div>
1825
+
1826
+ </div>
1827
+ ```
1828
+
1829
+ ```css
1830
+ /* Container — clips all layers, holds stacking context */
1831
+ .image-title {
1832
+ position: relative;
1833
+ width: 100%;
1834
+ height: 100%;
1835
+ overflow: hidden;
1836
+ color: #f0f4f7;
1837
+ }
1838
+
1839
+ /* Layer 0: background image */
1840
+ .image-title-media {
1841
+ position: absolute;
1842
+ inset: 0;
1843
+ width: 100%;
1844
+ height: 100%;
1845
+ object-fit: cover;
1846
+ display: block;
1847
+ z-index: 0;
1848
+ }
1849
+
1850
+ /* Layer 1: directional blur */
1851
+ .image-title-blur {
1852
+ position: absolute;
1853
+ inset: 0;
1854
+ z-index: 1;
1855
+ backdrop-filter: blur(50px);
1856
+ -webkit-backdrop-filter: blur(50px);
1857
+ }
1858
+
1859
+ /* Layer 2: gradient overlay — background set inline by LLM */
1860
+ .image-title-overlay {
1861
+ position: absolute;
1862
+ inset: 0;
1863
+ z-index: 2;
1864
+ }
1865
+
1866
+ /* Layer 3: foreground text */
1867
+ .image-title-fg {
1868
+ position: relative;
1869
+ z-index: 3;
1870
+ height: 100%;
1871
+ display: flex;
1872
+ flex-direction: column;
1873
+ justify-content: space-between;
1874
+ padding: 72px 84px;
1875
+ }
1876
+
1877
+ /* Alignment variants */
1878
+ .image-title--left .image-title-body {
1879
+ max-width: 680px;
1880
+ }
1881
+
1882
+ .image-title--right .image-title-fg {
1883
+ text-align: right;
1884
+ }
1885
+ .image-title--right .image-title-body {
1886
+ max-width: 860px;
1887
+ margin-left: auto;
1888
+ }
1889
+
1890
+ .image-title--center .image-title-fg {
1891
+ text-align: center;
1892
+ align-items: center;
1893
+ }
1894
+ .image-title--center .image-title-body {
1895
+ max-width: 900px;
1896
+ }
1897
+
1898
+ /* Text elements */
1899
+ .image-title-eyebrow {
1900
+ font-size: 11px;
1901
+ font-weight: 700;
1902
+ letter-spacing: 0.18em;
1903
+ text-transform: uppercase;
1904
+ color: rgba(247, 244, 238, 0.55);
1905
+ margin-bottom: 20px;
1906
+ }
1907
+
1908
+ .image-title h1 {
1909
+ color: #f0f4f7;
1910
+ font-size: 96px;
1911
+ line-height: 0.92;
1912
+ letter-spacing: -0.03em;
1913
+ text-transform: uppercase;
1914
+ }
1915
+
1916
+ .image-title-subtitle {
1917
+ margin-top: 24px;
1918
+ font-size: 15px;
1919
+ line-height: 1.56;
1920
+ color: rgba(247, 244, 238, 0.72);
1921
+ max-width: 480px;
1922
+ }
1923
+
1924
+ .image-title-footer {
1925
+ display: flex;
1926
+ justify-content: space-between;
1927
+ align-items: flex-end;
1928
+ gap: 24px;
1929
+ }
1930
+ ```
1931
+
1932
+ Rules:
1933
+ - Always use one of the three modifier classes: `image-title--left`, `image-title--right`, or `image-title--center`. Never omit the modifier.
1934
+ - Set the `background` gradient on `.image-title-overlay` inline — never use a static class value. The direction and opacity must match the text position: dense dark on the text side, fading toward the open image side.
1935
+ - The blur mask direction must mirror the text alignment: `--left` uses `to left`, `--right` uses `to right`. Both `-webkit-mask-image` and `mask-image` are required for cross-browser support.
1936
+ - All text inside `.image-title-fg` must be white-family: headings `#f0f4f7`, body `rgba(240,244,247,0.72)`, captions and eyebrows `rgba(240,244,247,0.50–0.55)`.
1937
+ - Use `.page-number--light` on any slide using this component.
1938
+ - When used inside `fullbleed` layout, place this component directly as the sole child of the `.page` div — the layout provides no additional framing.
1939
+
1940
+ ##### Tips
1941
+ - **h1 size range.** Scale between `88px` and `120px` depending on title length. Three short lines at `96px` is the default for cover; `100px` works well for short closing statements. Longer titles should reduce to `80–88px`.
1942
+ - **Cover overlay (--left).** Use `linear-gradient(105deg, rgba(5,5,5,0.95) 0%, rgba(5,5,5,0.72) 55%, rgba(5,5,5,0.10) 100%)`. Left stop must stay near `0.95` — the title needs a near-opaque dark backing on all hero imagery. Only the right tail fades to near-transparent so the image breathes.
1943
+ - **Closing overlay (--right).** Use `linear-gradient(180deg, rgba(0,0,0,0.30) 0%, rgba(0,0,0,0.72) 100%)`. Bottom-heavy dark zone backs the right-aligned text. Do not drop the bottom stop below `0.60`.
1944
+ - **Text opacity.** Add `style="opacity:0.85"` (or similar) to the `.image-title` container itself to softly blend the entire foreground text layer into the image — useful for atmospheric or section-divider slides where editorial restraint is the goal. Do not go below `0.70`.
1945
+ - **`--right` closing variant.** Mirror the blur mask: use `mask-image:linear-gradient(to right, transparent 0%, transparent 20%, black 100%)`. The right side stays blurred (text zone); the left side of the image stays sharp.
1946
+ - **`--center` variant.** Use a radial or symmetric overlay: `radial-gradient(ellipse at center, rgba(5,5,5,0.72) 0%, rgba(5,5,5,0.20) 100%)` or a flat `rgba(5,5,5,0.55)`. Center blur mask: `mask-image:radial-gradient(ellipse at center, black 0%, transparent 80%)`.
1947
+ - **Subtitle width on `--right`.** `.image-title-subtitle` inherits `max-width:480px` which is set for left-aligned text. On `--right`, override to match the `.image-title-body` width: `style="max-width:520px;margin-left:auto;"`.
1948
+ <!-- @component:image-title:end -->
1949
+
1950
+ <!-- @component:toc:start -->
1951
+ #### TOC Panel
1952
+
1953
+ Catalogue-style table-of-contents panel for chaptered presentations. Use a composed title block, compact TOC rows, tilt chapter plaques, and quiet right-aligned page references. The panel should feel like an editorial contents spread, not a consulting report index.
1954
+
1955
+ ```html
1956
+ <div class="toc-panel">
1957
+ <div class="toc-panel-inner">
1958
+ <div>
1959
+ <div class="toc-header">
1960
+ <p class="toc-kicker">Contents</p>
1961
+ <h2 class="toc-title offset-title">
1962
+ <span>A quieter way</span>
1963
+ <span class="offset-title-line--offset">into the story.</span>
1964
+ </h2>
1965
+ <p class="toc-dek">Optional short orientation line. Keep this brief and remove it entirely when the chapter list can stand on its own.</p>
1966
+ </div>
1967
+ <ol class="toc-list">
1968
+ <li class="toc-item">
1969
+ <div class="tilt-plaque" style="--plaque-back:rgba(115, 141, 194, 0.24);--plaque-back-rot:-9deg;--plaque-front-rot:4deg;--plaque-value-rot:-3deg;--plaque-ink:#5879a8;">
1970
+ <span class="tilt-plaque-value">01</span>
1971
+ </div>
1972
+ <div class="toc-item-body">
1973
+ <div class="toc-item-meta">
1974
+ <p class="toc-chapter-label">Chapter 1</p>
1975
+ <p class="toc-page-number">06</p>
1976
+ </div>
1977
+ <p class="toc-item-title">Prelude &amp; Intent</p>
1978
+ </div>
1979
+ </li>
1980
+ <li class="toc-item">
1981
+ <div class="tilt-plaque" style="--plaque-back:rgba(123, 189, 195, 0.24);--plaque-back-rot:6deg;--plaque-front-rot:-5deg;--plaque-value-rot:2deg;--plaque-ink:#5f8f92;">
1982
+ <span class="tilt-plaque-value">02</span>
1983
+ </div>
1984
+ <div class="toc-item-body">
1985
+ <div class="toc-item-meta">
1986
+ <p class="toc-chapter-label">Chapter 2</p>
1987
+ <p class="toc-page-number">14</p>
1988
+ </div>
1989
+ <p class="toc-item-title">Landscape in Motion</p>
1990
+ </div>
1991
+ </li>
1992
+ <li class="toc-item">
1993
+ <div class="tilt-plaque" style="--plaque-back:rgba(41, 128, 175, 0.20);--plaque-back-rot:-5deg;--plaque-front-rot:3deg;--plaque-value-rot:-1deg;--plaque-ink:#3f6f8a;">
1994
+ <span class="tilt-plaque-value">03</span>
1995
+ </div>
1996
+ <div class="toc-item-body">
1997
+ <div class="toc-item-meta">
1998
+ <p class="toc-chapter-label">Chapter 3</p>
1999
+ <p class="toc-page-number">22</p>
2000
+ </div>
2001
+ <p class="toc-item-title">Findings &amp; Texture</p>
2002
+ </div>
2003
+ </li>
2004
+ <li class="toc-item">
2005
+ <div class="tilt-plaque" style="--plaque-back:rgba(229, 147, 180, 0.24);--plaque-back-rot:8deg;--plaque-front-rot:-4deg;--plaque-value-rot:3deg;--plaque-ink:#9c6482;">
2006
+ <span class="tilt-plaque-value">04</span>
2007
+ </div>
2008
+ <div class="toc-item-body">
2009
+ <div class="toc-item-meta">
2010
+ <p class="toc-chapter-label">Chapter 4</p>
2011
+ <p class="toc-page-number">31</p>
2012
+ </div>
2013
+ <p class="toc-item-title">Priorities for Action</p>
2014
+ </div>
2015
+ </li>
2016
+ </ol>
2017
+ </div>
2018
+ <div class="toc-footer">
2019
+ <div>
2020
+ <p class="caption">Curatorial note</p>
2021
+ <p class="toc-footer-note">Optional note. Keep it quiet and short; this footer should support the TOC, not compete with it.</p>
2022
+ </div>
2023
+ <p class="caption">Organisation · Year</p>
2024
+ </div>
2025
+ </div>
2026
+ </div>
2027
+ ```
2028
+
2029
+ ```css
2030
+ .toc-panel {
2031
+ position: relative;
2032
+ background:
2033
+ radial-gradient(circle at 18% 14%, rgba(123, 189, 195, 0.18), transparent 34%),
2034
+ radial-gradient(circle at 76% 78%, rgba(229, 147, 180, 0.16), transparent 30%),
2035
+ linear-gradient(135deg, rgba(255,255,255,0.34), rgba(255,255,255,0) 36%),
2036
+ var(--bg-page);
2037
+ height: 100%;
2038
+ padding: 54px 52px 42px;
2039
+ display: flex;
2040
+ overflow: hidden;
2041
+ }
2042
+
2043
+ .toc-panel-inner {
2044
+ position: relative;
2045
+ z-index: 1;
2046
+ display: flex;
2047
+ flex-direction: column;
2048
+ justify-content: space-between;
2049
+ width: 100%;
2050
+ gap: 32px;
2051
+ }
2052
+
2053
+ .toc-header {
2054
+ max-width: 560px;
2055
+ display: flex;
2056
+ flex-direction: column;
2057
+ gap: 18px;
2058
+ }
2059
+
2060
+ .toc-kicker {
2061
+ font-size: 12px;
2062
+ line-height: 1.4;
2063
+ letter-spacing: 0.24em;
2064
+ text-transform: uppercase;
2065
+ color: var(--text-muted);
2066
+ }
2067
+
2068
+ .toc-title {
2069
+ font-size: 54px;
2070
+ line-height: 0.9;
2071
+ letter-spacing: -0.035em;
2072
+ max-width: 500px;
2073
+ }
2074
+
2075
+ .toc-dek {
2076
+ max-width: 460px;
2077
+ font-size: 17px;
2078
+ line-height: 1.62;
2079
+ color: var(--text-secondary);
2080
+ }
2081
+
2082
+ .toc-list {
2083
+ list-style: none;
2084
+ display: flex;
2085
+ flex-direction: column;
2086
+ gap: 16px;
2087
+ margin-top: 10px;
2088
+ max-width: 640px;
2089
+ }
2090
+
2091
+ .toc-item {
2092
+ min-height: 84px;
2093
+ display: flex;
2094
+ align-items: flex-start;
2095
+ gap: 16px;
2096
+ }
2097
+
2098
+ .toc-item-body {
2099
+ display: flex;
2100
+ flex-direction: column;
2101
+ gap: 4px;
2102
+ padding-top: 6px;
2103
+ border-top: 1px solid rgba(15, 30, 42, 0.09);
2104
+ flex: 1;
2105
+ }
2106
+
2107
+ .toc-item-meta {
2108
+ display: flex;
2109
+ justify-content: space-between;
2110
+ align-items: baseline;
2111
+ gap: 18px;
2112
+ }
2113
+
2114
+ .toc-chapter-label {
2115
+ font-size: 10px;
2116
+ line-height: 1.4;
2117
+ letter-spacing: 0.20em;
2118
+ text-transform: uppercase;
2119
+ color: var(--text-muted);
2120
+ }
2121
+
2122
+ .toc-page-number {
2123
+ font-family: var(--font-display);
2124
+ font-size: 18px;
2125
+ line-height: 1;
2126
+ letter-spacing: -0.03em;
2127
+ color: rgba(15, 30, 42, 0.40);
2128
+ }
2129
+
2130
+ .toc-item-title {
2131
+ font-family: var(--font-display);
2132
+ font-size: 22px;
2133
+ line-height: 1.08;
2134
+ color: var(--text-primary);
2135
+ }
2136
+
2137
+ .toc-footer {
2138
+ display: flex;
2139
+ justify-content: space-between;
2140
+ align-items: flex-end;
2141
+ gap: 24px;
2142
+ padding-top: 18px;
2143
+ border-top: 1px solid rgba(15, 30, 42, 0.09);
2144
+ }
2145
+
2146
+ .toc-footer-note {
2147
+ max-width: 320px;
2148
+ font-size: 13px;
2149
+ line-height: 1.55;
2150
+ color: var(--text-muted);
2151
+ }
2152
+ ```
2153
+
2154
+ ##### Tips
2155
+ - **TOC semantics come first.** Each row should read clearly as chapter label, title, and page number before any decorative treatment is added.
2156
+ - **Keep the rows compact.** Monet TOC rows should feel refined and quiet. Avoid oversized chapter plaques, wide gutters, or tall row heights.
2157
+ - **Tilt plaques must stay restrained.** Keep the rotation subtle and the palette drift soft; the plaque is an editorial accent, not the main subject.
2158
+ - **Skip long summaries by default.** If a TOC needs explanation, use one short dek under the main title rather than per-row body copy.
2159
+ - **Page references stay understated.** Right-align page numbers and keep them visually secondary to the chapter title.
2160
+ <!-- @component:toc:end -->
2161
+
2162
+ <!-- @component:quote:start -->
2163
+ #### Quote (.quote-block)
2164
+
2165
+ Flat editorial quote block. Wide and short (width > height). Transparent background — place it inside any layout slot. The large decorative quotation mark is CSS-rendered (no icon dependency).
2166
+
2167
+ ```html
2168
+ <div class="quote-block">
2169
+ <div class="quote-mark" aria-hidden="true">"</div>
2170
+ <p class="quote-text">The mountains teach us that progress is measured not in speed, but in the ground gained against resistance.</p>
2171
+ <div class="quote-attribution">
2172
+ <!-- initials variant -->
2173
+ <div class="quote-avatar" data-initials="JD" style="--qa-rot:-11deg;"></div>
2174
+ <!-- photo variant: <div class="quote-avatar" style="--qa-rot:8deg;"><img src="avatar.jpg" alt="Jane Doe"></div> -->
2175
+ <div class="quote-meta">
2176
+ <p class="quote-name">Jane Doe</p>
2177
+ <p class="caption">CEO, Acme Corporation</p>
2178
+ </div>
2179
+ </div>
2180
+ </div>
2181
+ ```
2182
+
2183
+ ```css
2184
+ .quote-block {
2185
+ position: relative;
2186
+ padding: 36px 44px 32px;
2187
+ overflow: hidden;
2188
+ }
2189
+
2190
+ .quote-mark {
2191
+ position: absolute;
2192
+ top: -18px;
2193
+ left: 28px;
2194
+ font-family: Baskerville, Georgia, serif;
2195
+ font-size: 140px;
2196
+ font-weight: 700;
2197
+ line-height: 1;
2198
+ color: var(--accent-sage);
2199
+ opacity: 0.42;
2200
+ pointer-events: none;
2201
+ user-select: none;
2202
+ }
2203
+
2204
+ .quote-text {
2205
+ position: relative;
2206
+ font-size: 20px;
2207
+ font-style: italic;
2208
+ line-height: 1.5;
2209
+ color: var(--text-primary);
2210
+ max-width: 860px;
2211
+ padding-top: 48px; /* clears the decorative mark */
2212
+ }
2213
+
2214
+ .quote-attribution {
2215
+ display: flex;
2216
+ align-items: center;
2217
+ gap: 14px;
2218
+ margin-top: 24px;
2219
+ }
2220
+
2221
+ .quote-avatar {
2222
+ position: relative;
2223
+ width: 52px;
2224
+ height: 52px;
2225
+ flex-shrink: 0;
2226
+ }
2227
+
2228
+ .quote-avatar::before {
2229
+ content: '';
2230
+ position: absolute;
2231
+ inset: 0;
2232
+ background: #fff;
2233
+ border: 1px solid var(--line-strong);
2234
+ }
2235
+
2236
+ .quote-avatar::after {
2237
+ content: attr(data-initials);
2238
+ position: absolute;
2239
+ inset: 0;
2240
+ background: var(--accent-earth);
2241
+ display: flex;
2242
+ align-items: center;
2243
+ justify-content: center;
2244
+ font-family: var(--font-display);
2245
+ font-size: 14px;
2246
+ font-weight: 700;
2247
+ color: #fff;
2248
+ transform: rotate(var(--qa-rot, -8deg));
2249
+ }
2250
+
2251
+ .quote-avatar img {
2252
+ position: absolute;
2253
+ inset: 0;
2254
+ width: 52px;
2255
+ height: 52px;
2256
+ object-fit: cover;
2257
+ transform: rotate(var(--qa-rot, -8deg));
2258
+ z-index: 1;
2259
+ }
2260
+
2261
+ .quote-meta {
2262
+ display: flex;
2263
+ flex-direction: column;
2264
+ gap: 2px;
2265
+ }
2266
+
2267
+ .quote-name {
2268
+ font-size: 14px;
2269
+ font-weight: 600;
2270
+ color: var(--text-primary);
2271
+ line-height: 1.3;
2272
+ }
2273
+ ```
2274
+
2275
+ **Tips:**
2276
+
2277
+ - **Dark background**: override text colors on the parent slot — `color: var(--bg-page)` for `.quote-text` and `.quote-name`; increase `.quote-mark` opacity to `0.15` (the sage hue reads better against dark at lower opacity).
2278
+ - **Avatar initials variant**: use `data-initials="JD"` and `style="--qa-rot:-11deg;"` on `.quote-avatar` (no child elements). The `::after` pseudo-element reads the initials and renders them on a rotated `--accent-earth` square over a white base square.
2279
+ - **Avatar photo variant**: add `<img>` inside `.quote-avatar` with `style="--qa-rot:8deg;"` on the parent. The image is fixed at `52×52px`, rotated by `--qa-rot`, and clips to the container via `overflow:hidden`. The white `::before` base square shows as corner peeking behind the rotated photo.
2280
+ - **Quote text length**: adjust `font-size` between `17px` (longer quotes, 3+ lines) and `24px` (short punchy quotes, 1 line). Keep `line-height: 1.5`.
2281
+ - **Opacity guidance**: on `--bg-page` (warm paper), `.quote-mark` opacity `0.25` works well. On dark `--bg-frame` backgrounds, reduce to `0.15`.
2282
+ - **Source-only attribution** (no person): omit `.quote-avatar` entirely and use `.quote-name` for the source text (e.g. a report title or publication name).
2283
+
2284
+ <!-- @component:quote:end -->
2285
+
2286
+ <!-- @component:brand-watermark:start -->
2287
+ #### Brand Watermark
2288
+
2289
+ Decorative brand watermark for selected slides. Use it as a quiet print-style brand trace in the top-right corner, not as a header logo. The image is typically a user-provided transparent PNG.
2290
+
2291
+ ```html
2292
+ <div class="brand-watermark" aria-hidden="true">
2293
+ <img src="assets/brand-watermark-dark.png" alt="">
2294
+ </div>
2295
+ ```
2296
+
2297
+ ```css
2298
+ .brand-watermark {
2299
+ position: absolute;
2300
+ top: 46px;
2301
+ right: 54px;
2302
+ height: 28px;
2303
+ max-width: 360px;
2304
+ opacity: 0.10;
2305
+ pointer-events: none;
2306
+ z-index: 1;
2307
+ }
2308
+
2309
+ .brand-watermark img {
2310
+ height: 100%;
2311
+ width: auto;
2312
+ display: block;
2313
+ object-fit: contain;
2314
+ }
2315
+
2316
+ .brand-watermark--light {
2317
+ opacity: 0.14;
2318
+ }
2319
+ ```
2320
+
2321
+ Rules:
2322
+ - Use a transparent-background, monochrome image. Prefer a simplified brand mark rather than a full logo lockup.
2323
+ - Default placement is the top-right corner. Keep it inside the paper page, not floating in the outer black frame.
2324
+ - Treat it as decorative. It must remain weaker than the slide title, image, and main narrative content.
2325
+ - Use a dark watermark image on light pages and a light watermark image on dark or fullbleed pages.
2326
+ - Do not pair it with another top-corner brand label, heading, or caption in the same region.
2327
+ - Omit it when the top-right area contains important photography detail or dense content.
2328
+
2329
+ ##### Tips
2330
+ - **Preferred asset format.** Use a user-provided transparent PNG with no background box. WebP or SVG can also work, but transparent PNG is the default assumption for Monet.
2331
+ - **Light vs dark variants.** Prepare two assets when possible: `brand-watermark-dark.png` for light pages and `brand-watermark-light.png` for dark pages. PNG cannot reliably recolor like inline SVG, so separate assets are safer.
2332
+ - **Size control.** Default to controlling the watermark by `height`, not fixed `width`. This keeps user-provided marks with very different aspect ratios visually consistent while preserving their natural proportions.
2333
+ - **Opacity range.** On light pages, keep opacity around `0.08` to `0.12`. On dark pages, `0.12` to `0.18` is usually enough.
2334
+ - **Aspect ratio.** Do not force the mark into a square crop if the supplied artwork is wide or tall. Use `height: 100%` and `width: auto` on the image so the artwork keeps its natural proportions.
2335
+ - **Overflow guard.** Keep a `max-width` on the wrapper for unusually long horizontal wordmarks so they do not intrude into the title field.
2336
+ - **Best usage.** Works best on section openers, TOC pages, and selected content slides with enough negative space. Cover and closing slides usually rely on their existing brand labels instead.
2337
+ <!-- @component:brand-watermark:end -->
2338
+
2339
+ <!-- @component:page-number:start -->
2340
+ #### Page Number (.page-number)
2341
+
2342
+ Absolute-positioned slide counter, bottom-right corner. Always present on content slides.
2343
+ Use `.page-number--light` when the slide background is dark (which is most slides in Monet).
2344
+
2345
+ ```html
2346
+ <div class="page-number page-number--light">01 / 12</div>
2347
+ ```
2348
+
2349
+ Omit `--light` only on slides with a white/light background.
2350
+
2351
+ ```css
2352
+ .page-number {
2353
+ position: absolute;
2354
+ bottom: 36px;
2355
+ right: 52px;
2356
+ font-family: var(--font-display);
2357
+ font-size: 11px;
2358
+ font-weight: 700;
2359
+ letter-spacing: 0.18em;
2360
+ color: var(--text-muted);
2361
+ z-index: 10;
2362
+ pointer-events: none;
2363
+ }
2364
+ .page-number--light {
2365
+ color: rgba(247, 244, 238, 0.45);
2366
+ }
2367
+ ```
2368
+
2369
+ <!-- @component:page-number:end -->
2370
+
2371
+ <!-- @component:timeline-journey-horizontal:start -->
2372
+ #### Timeline Journey Horizontal
2373
+
2374
+ A horizontal milestone timeline with a central axis line. Nodes sit on the axis; a dashed vertical stem leads to a tip node, with date, title, and description text alongside. Alternate nodes above and below the axis for rhythm. Suitable for 4–8 milestones across a chronological arc, transformation story, or multi-year programme recap.
2375
+
2376
+ ```html
2377
+ <div class="tjh">
2378
+ <div class="tjh-axis"></div>
2379
+
2380
+ <!-- UP node: item bottom edge sits on axis, content grows upward.
2381
+ DOM order (top→bottom): label, tip-dot, stem, axis-dot -->
2382
+ <div class="tjh-item tjh-item--up" style="left:7%; --tjh-item-color:var(--accent-earth);">
2383
+ <div class="tjh-label">
2384
+ <span class="tjh-date">Mar 2019</span>
2385
+ <span class="tjh-title">Programme Launch</span>
2386
+ <span class="tjh-text">Baseline mapping launched.</span>
2387
+ </div>
2388
+ <div class="tjh-tip-dot"></div>
2389
+ <div class="tjh-stem"></div>
2390
+ <div class="tjh-axis-dot"></div>
2391
+ </div>
2392
+
2393
+ <!-- DOWN node: item top edge sits on axis, content grows downward.
2394
+ DOM order (top→bottom): axis-dot, stem, tip-dot, label -->
2395
+ <div class="tjh-item tjh-item--down" style="left:21%; --tjh-item-color:var(--accent-gold);">
2396
+ <div class="tjh-axis-dot"></div>
2397
+ <div class="tjh-stem"></div>
2398
+ <div class="tjh-tip-dot"></div>
2399
+ <div class="tjh-label">
2400
+ <span class="tjh-date">Nov 2019</span>
2401
+ <span class="tjh-title">Supplier Audit</span>
2402
+ <span class="tjh-text">Supplier sprint completed.</span>
2403
+ </div>
2404
+ </div>
2405
+
2406
+ <!-- Add more nodes following the same up/down pattern -->
2407
+ </div>
2408
+ ```
2409
+
2410
+ ```css
2411
+ .tjh {
2412
+ --tjh-node: 12px;
2413
+ --tjh-stem-h: 80px;
2414
+ --tjh-col: calc(100% / 7); /* adjust denominator to match node count */
2415
+
2416
+ position: relative;
2417
+ width: 100%;
2418
+ height: 360px;
2419
+ }
2420
+
2421
+ /* Axis line */
2422
+ .tjh-axis {
2423
+ position: absolute;
2424
+ top: 50%;
2425
+ left: 0;
2426
+ right: 0;
2427
+ height: 1px;
2428
+ background: var(--line-strong);
2429
+ transform: translateY(-50%);
2430
+ }
2431
+
2432
+ /* Item base */
2433
+ .tjh-item {
2434
+ position: absolute;
2435
+ display: flex;
2436
+ flex-direction: column;
2437
+ align-items: center;
2438
+ width: var(--tjh-col);
2439
+ transform: translateX(-50%);
2440
+ }
2441
+
2442
+ /* --up: bottom edge on axis, content grows upward */
2443
+ .tjh-item--up { bottom: 50%; }
2444
+
2445
+ /* --down: top edge on axis, content grows downward */
2446
+ .tjh-item--down { top: 50%; }
2447
+
2448
+ /* Dots */
2449
+ .tjh-axis-dot {
2450
+ width: var(--tjh-node);
2451
+ height: var(--tjh-node);
2452
+ background: var(--tjh-item-color);
2453
+ flex-shrink: 0;
2454
+ transform: rotate(var(--nd-rot, -8deg));
2455
+ }
2456
+ .tjh-tip-dot {
2457
+ width: var(--tjh-node);
2458
+ height: var(--tjh-node);
2459
+ background: var(--tjh-item-color);
2460
+ flex-shrink: 0;
2461
+ transform: rotate(var(--td-rot, 6deg));
2462
+ }
2463
+
2464
+ /* Straddle the axis line */
2465
+ .tjh-item--up .tjh-axis-dot { margin-bottom: calc(-1 * var(--tjh-node) / 2); }
2466
+ .tjh-item--down .tjh-axis-dot { margin-top: calc(-1 * var(--tjh-node) / 2); }
2467
+
2468
+ /* Dashed stem */
2469
+ .tjh-stem {
2470
+ width: 1px;
2471
+ height: var(--tjh-stem-h);
2472
+ background-image: repeating-linear-gradient(
2473
+ to bottom,
2474
+ var(--line-strong) 0px,
2475
+ var(--line-strong) 4px,
2476
+ transparent 4px,
2477
+ transparent 8px
2478
+ );
2479
+ flex-shrink: 0;
2480
+ }
2481
+
2482
+ /* Label */
2483
+ .tjh-label {
2484
+ display: flex;
2485
+ flex-direction: column;
2486
+ gap: 4px;
2487
+ width: 100%;
2488
+ padding: 0 6px;
2489
+ }
2490
+
2491
+ .tjh-item--up .tjh-label { margin-bottom: 6px; }
2492
+ .tjh-item--down .tjh-label { margin-top: 6px; }
2493
+
2494
+ /* Date: inherits node colour via --tjh-item-color */
2495
+ .tjh-date {
2496
+ font-family: var(--font-display);
2497
+ font-size: 11px;
2498
+ font-weight: 700;
2499
+ letter-spacing: 0.16em;
2500
+ text-transform: uppercase;
2501
+ color: var(--tjh-item-color);
2502
+ line-height: 1.3;
2503
+ white-space: nowrap;
2504
+ }
2505
+
2506
+ .tjh-title {
2507
+ font-family: var(--font-display);
2508
+ font-size: 16px;
2509
+ font-weight: 600;
2510
+ letter-spacing: -0.01em;
2511
+ color: var(--text-primary);
2512
+ line-height: 1.15;
2513
+ }
2514
+
2515
+ .tjh-text {
2516
+ font-size: 17px;
2517
+ line-height: 1.5;
2518
+ color: var(--text-secondary);
2519
+ }
2520
+ ```
2521
+
2522
+ Rules:
2523
+ - Position nodes with `left: X%` inline style. For N nodes, space them at `(100/(N+1)) * k %` or manually distribute to reflect time proportions.
2524
+ - Each node requires `--tjh-item-color` set inline (use any summit accent colour).
2525
+ - **`--up` DOM order**: label → tip-dot → stem → axis-dot (label at top, axis-dot at bottom touching axis).
2526
+ - **`--down` DOM order**: axis-dot → stem → tip-dot → label (axis-dot at top touching axis, label at bottom).
2527
+ - Label order within `.tjh-label` is always: date → title → text (top to bottom).
2528
+ - Keep `.tjh-text` concise enough to stay within 1-2 lines at the default `17px`. If a node gets too dense, shorten the copy before changing the type scale.
2529
+ - Alternate up/down across nodes for visual rhythm. Do not stack multiple up or multiple down nodes consecutively unless intentional.
2530
+ - `--tjh-col` denominator should match the total number of nodes so each item gets equal horizontal space.
2531
+
2532
+ ##### Tips
2533
+ - **Adjust height**: Increase `.tjh { height }` if label text is tall or stems feel cramped.
2534
+ - **Adjust stem length**: Change `--tjh-stem-h` to lengthen or shorten the dashed connector.
2535
+ - **Dark background overrides**: Set `--line-strong: rgba(240,244,247,0.25)` on `.tjh`, override `.tjh-axis { background }`, set `.tjh-title { color: #f0f4f7 }`, `.tjh-text { color: rgba(240,244,247,0.7) }`. The `--tjh-item-color` accent colours work on dark backgrounds without change.
2536
+ - **Fewer nodes**: For 4–5 nodes, widen `--tjh-col` by using a smaller denominator (e.g. `calc(100% / 5)`), and space `left` values accordingly.
2537
+ <!-- @component:timeline-journey-horizontal:end -->
2538
+
2539
+ <!-- @component:timeline-journey-vertical:start -->
2540
+ #### Timeline Journey Vertical
2541
+
2542
+ A vertical milestone timeline with a central axis line. Nodes sit on the axis; a horizontal dashed stem leads to a tip dot, with date, title, and description text alongside. Alternate nodes left and right of the axis for rhythm. Suitable for 3–8 milestones across a chronological arc, transformation story, or multi-year programme recap.
2543
+
2544
+ Can be placed inside any layout slot that provides a defined height (`narrative`, `halves`, `highlight-cols`, `stacked`, or a full-page content zone). The component fills `width: 100%; height: 100%` of its parent.
2545
+
2546
+ ```html
2547
+ <div class="tjv">
2548
+ <div class="tjv-axis"></div>
2549
+
2550
+ <!-- LEFT node: DOM order axis-dot → stem → tip-dot → label.
2551
+ flex-direction: row-reverse flips visual order to: label | tip-dot | stem | axis-dot
2552
+ → axis-dot ends up on the right touching the axis; label on the far left, right-aligned. -->
2553
+ <div class="tjv-item tjv-item--left" style="top:14%;--tjv-item-color:var(--accent-earth);">
2554
+ <div class="tjv-axis-dot"></div>
2555
+ <div class="tjv-stem"></div>
2556
+ <div class="tjv-tip-dot"></div>
2557
+ <div class="tjv-label">
2558
+ <span class="tjv-date">Mar 2019</span>
2559
+ <span class="tjv-title">Programme Launch</span>
2560
+ <span class="tjv-text">Baseline mapping launched across all units.</span>
2561
+ </div>
2562
+ </div>
2563
+
2564
+ <!-- RIGHT node: DOM order axis-dot → stem → tip-dot → label.
2565
+ flex-direction: row renders as: axis-dot | stem | tip-dot | label
2566
+ → axis-dot on the left touching the axis; label on the far right, left-aligned. -->
2567
+ <div class="tjv-item tjv-item--right" style="top:30%;--tjv-item-color:var(--accent-gold);">
2568
+ <div class="tjv-axis-dot"></div>
2569
+ <div class="tjv-stem"></div>
2570
+ <div class="tjv-tip-dot"></div>
2571
+ <div class="tjv-label">
2572
+ <span class="tjv-date">Nov 2019</span>
2573
+ <span class="tjv-title">Supplier Audit Completed</span>
2574
+ <span class="tjv-text">94% of Tier 1 suppliers assessed.</span>
2575
+ </div>
2576
+ </div>
2577
+
2578
+ <!-- Add more nodes following the same left/right pattern -->
2579
+ </div>
2580
+ ```
2581
+
2582
+ ```css
2583
+ .tjv {
2584
+ --tjv-node: 12px; /* axis-dot diameter */
2585
+ --tjv-stem-w: 80px; /* horizontal dashed stem width */
2586
+
2587
+ position: relative;
2588
+ width: 100%;
2589
+ height: 100%;
2590
+ }
2591
+
2592
+ /* Vertical axis — horizontally centered, full height */
2593
+ .tjv-axis {
2594
+ position: absolute;
2595
+ left: 50%;
2596
+ top: 0;
2597
+ bottom: 0;
2598
+ width: 1px;
2599
+ background: var(--line-strong);
2600
+ transform: translateX(-50%);
2601
+ }
2602
+
2603
+ /* Item base */
2604
+ .tjv-item {
2605
+ position: absolute;
2606
+ display: flex;
2607
+ align-items: center;
2608
+ height: 80px; /* vertical size of the clickable/hover zone */
2609
+ transform: translateY(-50%); /* center the row on the top: Y% point */
2610
+ }
2611
+
2612
+ /* LEFT: row-reverse flips DOM order so axis-dot appears on the right (on the axis) */
2613
+ .tjv-item--left {
2614
+ right: 50%;
2615
+ flex-direction: row-reverse;
2616
+ }
2617
+
2618
+ /* RIGHT: standard row; axis-dot appears on the left (on the axis) */
2619
+ .tjv-item--right {
2620
+ left: 50%;
2621
+ flex-direction: row;
2622
+ }
2623
+
2624
+ /* Axis dot — straddles the axis line */
2625
+ .tjv-axis-dot {
2626
+ width: var(--tjv-node);
2627
+ height: var(--tjv-node);
2628
+ background: var(--tjv-item-color);
2629
+ flex-shrink: 0;
2630
+ position: relative;
2631
+ z-index: 1;
2632
+ transform: rotate(var(--nd-rot, -8deg));
2633
+ }
2634
+
2635
+ /* LEFT: axis-dot is visually rightmost (row-reverse); push right to straddle axis */
2636
+ .tjv-item--left .tjv-axis-dot {
2637
+ margin-right: calc(-1 * var(--tjv-node) / 2);
2638
+ }
2639
+
2640
+ /* RIGHT: axis-dot is visually leftmost; push left to straddle axis */
2641
+ .tjv-item--right .tjv-axis-dot {
2642
+ margin-left: calc(-1 * var(--tjv-node) / 2);
2643
+ }
2644
+
2645
+ /* Tip dot — small square at the stem end near the label */
2646
+ .tjv-tip-dot {
2647
+ width: var(--tjv-node);
2648
+ height: var(--tjv-node);
2649
+ background: var(--tjv-item-color);
2650
+ flex-shrink: 0;
2651
+ transform: rotate(var(--td-rot, 6deg));
2652
+ }
2653
+
2654
+ /* Horizontal dashed stem */
2655
+ .tjv-stem {
2656
+ width: var(--tjv-stem-w);
2657
+ height: 1px;
2658
+ background-image: repeating-linear-gradient(
2659
+ to right,
2660
+ var(--line-strong) 0px,
2661
+ var(--line-strong) 4px,
2662
+ transparent 4px,
2663
+ transparent 8px
2664
+ );
2665
+ flex-shrink: 0;
2666
+ }
2667
+
2668
+ /* Label block */
2669
+ .tjv-label {
2670
+ display: flex;
2671
+ flex-direction: column;
2672
+ gap: 4px;
2673
+ }
2674
+
2675
+ .tjv-item--left .tjv-label {
2676
+ text-align: right;
2677
+ align-items: flex-end;
2678
+ padding-right: 20px;
2679
+ max-width: 560px;
2680
+ }
2681
+
2682
+ .tjv-item--right .tjv-label {
2683
+ text-align: left;
2684
+ align-items: flex-start;
2685
+ padding-left: 20px;
2686
+ max-width: 560px;
2687
+ }
2688
+
2689
+ /* Date — colored per node via --tjv-item-color */
2690
+ .tjv-date {
2691
+ font-family: var(--font-display);
2692
+ font-size: 11px;
2693
+ font-weight: 700;
2694
+ letter-spacing: 0.16em;
2695
+ text-transform: uppercase;
2696
+ color: var(--tjv-item-color);
2697
+ line-height: 1.3;
2698
+ white-space: nowrap;
2699
+ }
2700
+
2701
+ .tjv-title {
2702
+ font-family: var(--font-display);
2703
+ font-size: 18px;
2704
+ font-weight: 600;
2705
+ letter-spacing: -0.01em;
2706
+ color: var(--text-primary);
2707
+ line-height: 1.15;
2708
+ }
2709
+
2710
+ .tjv-text {
2711
+ font-size: 17px;
2712
+ line-height: 1.5;
2713
+ color: var(--text-secondary);
2714
+ max-width: 380px;
2715
+ }
2716
+ ```
2717
+
2718
+ Rules:
2719
+ - **DOM order is identical for left and right nodes**: `axis-dot → stem → tip-dot → label`. The visual direction is controlled by CSS (`row-reverse` for left, `row` for right) — never by changing the DOM order.
2720
+ - Position each node with `top: Y%` inline style. For N nodes, distribute evenly: `(100 / (N + 1)) * k %` or manually to reflect actual time proportions.
2721
+ - Every node must set `--tjv-item-color` inline (use any Monet accent: `--accent-earth`, `--accent-gold`, `--accent-olive`, `--accent-sage`).
2722
+ - Alternate `--left` and `--right` across nodes for visual rhythm. Do not place consecutive same-side nodes unless intentional.
2723
+ - The parent container must have a defined height. Use `height: 100%` when inside a layout slot, or set an explicit `px` height when used standalone.
2724
+ - Keep `.tjv-text` to roughly 2-3 lines at the default `17px`. Longer text shifts the effective visual centre of the item away from the `axis-dot`.
2725
+
2726
+ ##### Tips
2727
+ - **Adjust stem length**: Change `--tjv-stem-w` on `.tjv` (or inline on a single item) to lengthen or shorten the dashed connector. Wider columns benefit from a longer stem (`120px`); narrow slots look cleaner at `60px`.
2728
+ - **Adjust node slot height**: The `height: 80px` on `.tjv-item` sets the vertical click/hover zone. It does not clip label text — labels overflow naturally below the slot. If labels are tall, increase `top` spacing between nodes to avoid overlap.
2729
+ - **In a layout slot** (`narrative`, `halves`, `highlight-cols`): wrap `.tjv` in a `div` with `height: 100%` and `position: relative` so the absolute positioning resolves correctly.
2730
+ - **Standalone full-page use**: set an explicit height on the `.tjv` wrapper (e.g. `height: 720px`) when used outside a height-constrained layout.
2731
+ - **Dark background overrides**: set on the `.tjv` wrapper — `--line-strong: rgba(240,244,247,0.25)` (axis + stem), `.tjv-title { color: #f0f4f7 }`, `.tjv-text { color: rgba(240,244,247,0.7) }`. The `--tjv-item-color` accent colours work on dark backgrounds without change.
2732
+ - **Fewer nodes (3–4)**: increase spacing — use `top` values like `15%, 35%, 55%, 75%` to prevent the timeline from clustering at the top.
2733
+ - **More nodes (6–8)**: keep `.tjv-text` to 1-2 lines and increase vertical spacing between nodes if needed. Treat `17px` as the default body size; only use a smaller local override in exceptional dense layouts.
2734
+ <!-- @component:timeline-journey-vertical:end -->
2735
+
2736
+ <!-- @design:components:end -->