@cyber-dash-tech/revela 0.1.9 → 0.1.11

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,2392 @@
1
+ ---
2
+ name: summit
3
+ description: genneral clean template for presentation
4
+ author: cyber-dash
5
+ version: 1.0.0
6
+ preview:
7
+ ---
8
+
9
+ ## Visual Style — Summit 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
+ ```css
18
+ :root {
19
+ --bg-frame: #050505;
20
+ --bg-page: #f7f4ee;
21
+ --bg-page-alt: #efe9df;
22
+ --text-primary: #171411;
23
+ --text-secondary: #5e554c;
24
+ --text-muted: #8a7f73;
25
+ --line: rgba(23, 20, 17, 0.14);
26
+ --line-strong: rgba(23, 20, 17, 0.28);
27
+ --accent-earth: #8d6a49;
28
+ --accent-olive: #6f7562;
29
+ --accent-stone: #b9afa1;
30
+ --accent-gold: #c9992a;
31
+ --accent-danger: #b94a3c;
32
+ --accent-sage: #9eb0a6;
33
+ --shadow-soft: rgba(0, 0, 0, 0.18);
34
+ }
35
+ ```
36
+
37
+ Accent usage guidance:
38
+ - `--accent-gold` — primary emphasis, TOC dividers, key data callouts
39
+ - `--accent-earth` — warm secondary accent, image captions, secondary labels
40
+ - `--accent-olive` — muted structural accent, chart fills, subtle dividers
41
+ - `--accent-stone` — lightest accent, disabled states, faint decorative lines
42
+ - `--accent-sage` — desaturated cool green; use for environmental, sustainability, or positive-signal content (e.g. quote decorations, positive indicators, nature-themed slides)
43
+ - `--accent-danger` — negative indicators, alerts, down-trend markers only
44
+
45
+ ### Typography
46
+
47
+ - **Display / heading font**: `IBM Plex Sans Condensed` — used for all headings (`h1`–`h4`), eyebrows, and display text across every layout
48
+ - **Body font**: `Inter` — used for body copy, labels, captions, and UI text
49
+ - Font link tag:
50
+ ```html
51
+ <link rel="preconnect" href="https://fonts.googleapis.com">
52
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
53
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
54
+ ```
55
+ - cover h1: `108px` to `124px`, weight `600` to `700`, line-height `0.88` to `0.94`, uppercase
56
+ - inner-layout h2: `30px` to `36px`, weight `600` to `700`, line-height `1.06` to `1.12`
57
+ - inner-layout h3: `20px` to `24px`, weight `600`, line-height `1.12` to `1.18`
58
+ - Body: `17px`, line-height `1.6`
59
+ - Eyebrow / caption: `11px` to `13px`, uppercase, letter-spacing `0.16em` to `0.2em`
60
+ - Stat number: `72px` to `88px`, weight `500`, line-height `0.95`
61
+ - Never use text shadows or glow.
62
+ - Never switch to a serif typeface; Summit is strictly sans-serif.
63
+
64
+ All sizes are fixed `px` for the 1920x1080 canvas. JS `transform: scale()` handles viewport adaptation. Never use `clamp()` or viewport-relative units.
65
+
66
+ ### Page Framing
67
+
68
+ - The browser viewport is the black frame: `#050505`.
69
+ - The presentation canvas is a warm paper page inside that frame.
70
+ - Use the page edge as a visible compositional device. The paper should feel placed inside the viewport, not full-bleed by default.
71
+ - Default page inset: `40px` from viewport edges, with subtle inner shadow only if needed.
72
+
73
+ ### Grid System
74
+
75
+ - **1920x1080 fixed canvas** with a paper page around `1760px` wide inside the frame.
76
+ - Main content width: `1480px`.
77
+ - Preferred editorial splits: `5 / 7`, `4 / 8`, `3 / 9`.
78
+ - Text column target: about `480px` max.
79
+ - Image column target: about `880px` max.
80
+ - Headings align to the text column, not to the full canvas.
81
+ - Let images carry visual weight; text should remain narrow, calm, and readable.
82
+
83
+ ### Image Treatment
84
+
85
+ - Use real photography or realistic documentary imagery.
86
+ - Default to outdoor, alpine, mountain, ridge, snowline, expedition, and field photography.
87
+ - Prefer mountain and landscape imagery over indoor workshop scenes or generic product close-ups.
88
+ - Favor hard crops, edge-to-edge image blocks, and cinematic aspect ratios.
89
+ - Use `object-fit: cover` and accept aggressive cropping when composition improves.
90
+ - Avoid heavy dark overlays, neon gradients, frosted glass, or artificial glows.
91
+ - Captions should be small, quiet, and aligned to an image edge.
92
+ - When a layout places copy over a dominant visual field, create a charcoal-toned reading field that begins dense at the text side and fades naturally across the visual. Keep this as a broad structural transition, not a small boxed panel.
93
+
94
+ ### Decorative Language
95
+
96
+ - Ornament is restrained: thin rules, small chevron dividers, subtle page blocks.
97
+ - Dense slides should rely on structure, not decoration.
98
+ - Sparse slides can use a single oversize photo or one quiet rule to hold composition.
99
+ - Never use blobs, glow halos, glass cards, or dashboard chrome.
100
+
101
+ ### Slide Layout
102
+
103
+ - Every slide uses `.slide-canvas` sized to `1920px x 1080px`, scaled by JS.
104
+ - Every `<section class="slide">` must include `slide-qa="true"` or `slide-qa="false"`.
105
+ - Use `slide-qa="true"` for dense content layouts and `slide-qa="false"` for structural or intentionally sparse layouts.
106
+ - Default canvas padding: `72px 80px`.
107
+ - The paper page should usually sit inside the canvas with `padding: 56px 64px 64px`.
108
+ - Target strong fill on content-heavy slides while preserving editorial whitespace.
109
+
110
+ ### HTML Structure
111
+
112
+ Every generated presentation must use this exact HTML skeleton:
113
+
114
+ ```html
115
+ <!DOCTYPE html>
116
+ <html lang="{language}">
117
+ <head>
118
+ <meta charset="UTF-8">
119
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
120
+ <title>{Presentation Title}</title>
121
+ <link rel="preconnect" href="https://fonts.googleapis.com">
122
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
123
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:wght@500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
124
+ <!-- <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> only if charts are needed -->
125
+ <style>/* all CSS here */</style>
126
+ </head>
127
+ <body>
128
+ <section class="slide cover-slide" slide-qa="false" data-index="0">
129
+ <div class="slide-canvas"> ... </div>
130
+ </section>
131
+ <section class="slide" slide-qa="true" data-index="1">
132
+ <div class="slide-canvas"> ... </div>
133
+ </section>
134
+ <script>/* all JS here */</script>
135
+ </body>
136
+ </html>
137
+ ```
138
+
139
+ ### Core CSS
140
+
141
+ ```css
142
+ * { box-sizing: border-box; margin: 0; padding: 0; }
143
+
144
+ html {
145
+ scroll-snap-type: y mandatory;
146
+ overflow-y: scroll;
147
+ height: 100%;
148
+ }
149
+
150
+ body {
151
+ background: var(--bg-frame);
152
+ color: var(--text-primary);
153
+ font-family: 'Inter', ui-sans-serif, sans-serif;
154
+ -webkit-font-smoothing: antialiased;
155
+ height: 100%;
156
+ }
157
+
158
+ .slide {
159
+ min-height: 100dvh;
160
+ scroll-snap-align: start;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ overflow: hidden;
165
+ background: var(--bg-frame);
166
+ }
167
+
168
+ .slide-canvas {
169
+ width: 1920px;
170
+ height: 1080px;
171
+ flex-shrink: 0;
172
+ transform-origin: center center;
173
+ position: relative;
174
+ overflow: hidden;
175
+ padding: 72px 80px;
176
+ }
177
+
178
+ .page {
179
+ position: relative;
180
+ width: 100%;
181
+ height: 100%;
182
+ background: var(--bg-page);
183
+ color: var(--text-primary);
184
+ padding: 0;
185
+ box-shadow: 0 24px 80px var(--shadow-soft);
186
+ display: flex;
187
+ flex-direction: column;
188
+ }
189
+
190
+ .page.alt {
191
+ background: var(--bg-page-alt);
192
+ }
193
+
194
+ .eyebrow,
195
+ .caption,
196
+ .meta-label {
197
+ font-size: 12px;
198
+ line-height: 1.4;
199
+ letter-spacing: 0.18em;
200
+ text-transform: uppercase;
201
+ color: var(--text-muted);
202
+ }
203
+
204
+ h1, h2, h3, h4 {
205
+ font-family: 'IBM Plex Sans Condensed', 'Inter', ui-sans-serif, sans-serif;
206
+ font-weight: 600;
207
+ letter-spacing: -0.02em;
208
+ color: var(--text-primary);
209
+ }
210
+
211
+ h1 { font-size: 88px; line-height: 0.96; }
212
+ h2 { font-size: 34px; line-height: 1.08; }
213
+ h3 { font-size: 24px; line-height: 1.14; }
214
+
215
+ p, li {
216
+ font-size: 17px;
217
+ line-height: 1.6;
218
+ color: var(--text-secondary);
219
+ }
220
+
221
+ .rule {
222
+ width: 100%;
223
+ height: 1px;
224
+ background: var(--line);
225
+ }
226
+
227
+ .rule.strong {
228
+ background: var(--line-strong);
229
+ }
230
+
231
+ .chevron-divider {
232
+ display: inline-flex;
233
+ align-items: center;
234
+ gap: 10px;
235
+ font-size: 11px;
236
+ letter-spacing: 0.18em;
237
+ text-transform: uppercase;
238
+ color: var(--text-muted);
239
+ }
240
+
241
+ .chevron-divider::before,
242
+ .chevron-divider::after {
243
+ content: '';
244
+ width: 18px;
245
+ height: 1px;
246
+ background: var(--line-strong);
247
+ }
248
+
249
+ .media-frame {
250
+ position: relative;
251
+ overflow: hidden;
252
+ }
253
+
254
+ .media-frame img {
255
+ width: 100%;
256
+ height: 100%;
257
+ display: block;
258
+ object-fit: cover;
259
+ }
260
+
261
+ .media-caption {
262
+ margin-top: 12px;
263
+ font-size: 12px;
264
+ line-height: 1.5;
265
+ letter-spacing: 0.14em;
266
+ text-transform: uppercase;
267
+ color: var(--text-muted);
268
+ }
269
+
270
+ /* editorial-list: square bullet + optional <strong> lead phrase per item.
271
+ Usage: <li><strong>Lead phrase.</strong> Supporting copy.</li>
272
+ Dark bg override: set --accent-earth to rgba(247,244,238,0.72) on the list wrapper. */
273
+ .editorial-list {
274
+ list-style: none;
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 14px;
278
+ }
279
+
280
+ .editorial-list li {
281
+ position: relative;
282
+ padding-left: 20px;
283
+ font-size: 14px;
284
+ line-height: 1.58;
285
+ color: var(--text-secondary);
286
+ }
287
+
288
+ .editorial-list li::before {
289
+ content: '';
290
+ position: absolute;
291
+ left: 0;
292
+ top: 8px;
293
+ width: 6px;
294
+ height: 6px;
295
+ background: var(--accent-earth);
296
+ }
297
+
298
+ .reveal {
299
+ opacity: 0;
300
+ transform: translateY(18px);
301
+ transition: opacity 0.55s cubic-bezier(0.22, 1, 0.36, 1),
302
+ transform 0.55s cubic-bezier(0.22, 1, 0.36, 1);
303
+ }
304
+
305
+ .reveal.visible {
306
+ opacity: 1;
307
+ transform: translateY(0);
308
+ }
309
+ ```
310
+
311
+ ### SlidePresentation Class (Complete JavaScript)
312
+
313
+ All presentations must include this complete `SlidePresentation` class.
314
+
315
+ ```javascript
316
+ class SlidePresentation {
317
+ constructor() {
318
+ this.slides = document.querySelectorAll('.slide');
319
+ this.currentSlide = 0;
320
+ this.setupScaling();
321
+ this.setupIntersectionObserver();
322
+ this.setupKeyboardNav();
323
+ this.setupTouchNav();
324
+ this.setupMouseWheel();
325
+ }
326
+
327
+ setupScaling() {
328
+ const canvases = document.querySelectorAll('.slide-canvas');
329
+ const BASE_W = 1920;
330
+ const BASE_H = 1080;
331
+ const update = () => {
332
+ const vw = window.innerWidth;
333
+ const vh = window.innerHeight;
334
+ const scale = Math.min(vw / BASE_W, vh / BASE_H);
335
+ canvases.forEach((canvas) => {
336
+ canvas.style.transform = `scale(${scale})`;
337
+ });
338
+ };
339
+ window.addEventListener('resize', update);
340
+ update();
341
+ }
342
+
343
+ setupIntersectionObserver() {
344
+ const observer = new IntersectionObserver((entries) => {
345
+ entries.forEach((entry) => {
346
+ if (entry.isIntersecting) {
347
+ entry.target.querySelectorAll('.reveal').forEach((el) => el.classList.add('visible'));
348
+ }
349
+ });
350
+ }, { threshold: 0.2 });
351
+ this.slides.forEach((slide) => observer.observe(slide));
352
+ }
353
+
354
+ setupKeyboardNav() {
355
+ document.addEventListener('keydown', (event) => {
356
+ if (['ArrowDown', 'ArrowRight', ' ', 'PageDown'].includes(event.key)) {
357
+ event.preventDefault();
358
+ this.goTo(this.currentSlide + 1);
359
+ } else if (['ArrowUp', 'ArrowLeft', 'PageUp'].includes(event.key)) {
360
+ event.preventDefault();
361
+ this.goTo(this.currentSlide - 1);
362
+ }
363
+ });
364
+ }
365
+
366
+ setupTouchNav() {
367
+ let startY = 0;
368
+ document.addEventListener('touchstart', (event) => {
369
+ startY = event.touches[0].clientY;
370
+ }, { passive: true });
371
+ document.addEventListener('touchend', (event) => {
372
+ const deltaY = startY - event.changedTouches[0].clientY;
373
+ if (Math.abs(deltaY) > 40) {
374
+ this.goTo(this.currentSlide + (deltaY > 0 ? 1 : -1));
375
+ }
376
+ }, { passive: true });
377
+ }
378
+
379
+ setupMouseWheel() {
380
+ let last = 0;
381
+ document.addEventListener('wheel', (event) => {
382
+ const now = Date.now();
383
+ if (now - last < 800) return;
384
+ last = now;
385
+ this.goTo(this.currentSlide + (event.deltaY > 0 ? 1 : -1));
386
+ }, { passive: true });
387
+ }
388
+
389
+ goTo(index) {
390
+ const clamped = Math.max(0, Math.min(this.slides.length - 1, index));
391
+ this.slides[clamped].scrollIntoView({ behavior: 'smooth' });
392
+ this.currentSlide = clamped;
393
+ }
394
+ }
395
+
396
+ new SlidePresentation();
397
+ ```
398
+
399
+ <!-- @design:foundation:end -->
400
+
401
+ <!-- @design:rules:start -->
402
+
403
+ ### Composition Rules
404
+
405
+ These rules are mandatory for Summit.
406
+
407
+ - **Image over decoration.** If a slide has strong photography, reduce ornamental elements to one rule or one caption.
408
+ - **Text column stays narrow.** Do not let narrative text stretch across the page just because space exists.
409
+ - **One visual center per slide.** Either the image, the heading block, or the metrics dominate. Never all three at equal weight.
410
+ - **Metrics should read like a report, not a dashboard.** Use calm typography, generous spacing, and minimal UI framing.
411
+ - **Dense slides need structure, not decoration.** Use rules, columns, and alignment. Avoid extra accents.
412
+ - **Sparse slides depend on image weight.** If content is light, the photo or page framing must hold the composition.
413
+ - **No glass cards, neon KPI styling, or startup-product chrome.** Summit is editorial and print-adjacent.
414
+ - **Visual hierarchy is strict:** eyebrow -> heading -> body -> caption.
415
+
416
+ ### Common Mistakes
417
+
418
+ - Using equally wide text and image columns instead of an asymmetric editorial split.
419
+ - Filling empty space with decorative shapes instead of using a stronger crop or larger photograph.
420
+ - Treating metric blocks like SaaS dashboard cards.
421
+ - Setting long paragraphs directly on top of busy imagery.
422
+ - Using more than one dominant photo on a single slide without a clear hierarchy.
423
+ - Letting captions become body-copy sized.
424
+
425
+ ### Do & Don't
426
+
427
+ - **Do** let the paper page feel physical inside the dark frame.
428
+ - **Do** use thin rules and quiet labels to create structure.
429
+ - **Do** crop imagery boldly when it improves the composition.
430
+ - **Do** keep headings hard-edged, restrained, and report-like.
431
+ - **Don't** use glow, glass, blob shapes, or neon gradients.
432
+ - **Don't** center everything by default; asymmetry is part of the design language.
433
+ - **Don't** turn highlights into generic cards with rounded corners and shadows.
434
+ - **Don't** overload a slide with both many metrics and many images.
435
+
436
+ <!-- @design:rules:end -->
437
+
438
+ <!-- @design:layouts:start -->
439
+
440
+ ### Layout Types
441
+
442
+ 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>"`).
443
+
444
+ <!-- @layout:fullbleed:start qa=false -->
445
+ #### Fullbleed
446
+
447
+ 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.
448
+
449
+ Structural intent:
450
+ - 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.
451
+
452
+ ```html
453
+ <section class="slide" slide-qa="false" data-index="N">
454
+ <div class="slide-canvas">
455
+ <div class="page" style="padding:0;">
456
+
457
+ <!-- [slot: content] — use image-title component; set --left for cover, --right for closing -->
458
+ <!-- image-title handles all internal z-index layering (image → blur → overlay → text) -->
459
+
460
+ </div>
461
+ </div>
462
+ </section>
463
+ ```
464
+
465
+ ##### Tips
466
+ - **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.
467
+ - **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.
468
+ - **Page number.** Use `.page-number--light` — position it inside `.page` at `z-index:10` so it sits above the `image-title` stacking context.
469
+ - **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.
470
+ <!-- @layout:fullbleed:end -->
471
+
472
+ <!-- @layout:narrative:start qa=true -->
473
+ #### Narrative
474
+
475
+ 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.
476
+
477
+ Structural intent:
478
+ - left slot: wider zone (1.618fr) — can hold any component(s)
479
+ - right slot: narrower zone (1fr) — can hold any component(s)
480
+
481
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — there is no text/visual semantic preset.
482
+
483
+
484
+ ```html
485
+ <section class="slide" slide-qa="true" data-index="N">
486
+ <div class="slide-canvas">
487
+ <div class="page" style="padding:0;overflow:hidden;">
488
+ <div class="narrative-grid">
489
+
490
+ <!-- [slot: left] — 1+ components; suggested: image-title, echart-panel, text-panel -->
491
+ <div>
492
+ </div>
493
+
494
+ <!-- [slot: right] — 1+ components; suggested: text-panel, toc, flow-vertical, data-table -->
495
+ <div>
496
+ </div>
497
+
498
+ </div>
499
+ </div>
500
+ </div>
501
+ </section>
502
+ ```
503
+
504
+ ```css
505
+ .narrative-grid {
506
+ display: grid;
507
+ grid-template-columns: minmax(0, 1.618fr) minmax(0, 1fr);
508
+ grid-template-rows: minmax(0, 1fr);
509
+ height: 100%;
510
+ overflow: hidden;
511
+ align-items: start;
512
+ }
513
+
514
+ .narrative-grid--reverse {
515
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.618fr);
516
+ }
517
+
518
+ .narrative-grid > * {
519
+ overflow: hidden;
520
+ min-height: 0;
521
+ min-width: 0;
522
+ }
523
+ ```
524
+
525
+ ##### Tips
526
+ - **Grid container uses `.narrative-grid` class.** Applies `minmax(0, Nfr)` tracks and `overflow:hidden` on all children. Do not add inline `height:100%` or `flex:1` — the class handles containment.
527
+ - **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.
528
+ - **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`.
529
+ - **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`.
530
+ <!-- @layout:narrative:end -->
531
+
532
+ <!-- @layout:narrative-reverse:start qa=true -->
533
+ #### Narrative Reverse
534
+
535
+ 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.
536
+
537
+ Structural intent:
538
+ - left slot: narrower zone (1fr) — can hold any component(s)
539
+ - right slot: wider zone (1.618fr) — can hold any component(s)
540
+
541
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — there is no text/visual semantic preset.
542
+
543
+
544
+ ```html
545
+ <section class="slide" slide-qa="true" data-index="N">
546
+ <div class="slide-canvas">
547
+ <div class="page" style="padding:0;overflow:hidden;">
548
+ <div class="narrative-grid narrative-grid--reverse">
549
+
550
+ <!-- [slot: left] — 1+ components; suggested: text-panel, toc, flow-vertical, echart-panel -->
551
+ <div>
552
+ </div>
553
+
554
+ <!-- [slot: right] — 1+ components; suggested: image-title, echart-panel, data-table -->
555
+ <div>
556
+ </div>
557
+
558
+ </div>
559
+ </div>
560
+ </div>
561
+ </section>
562
+ ```
563
+
564
+ ##### Tips
565
+ - **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`.
566
+ - **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.
567
+ - **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.
568
+ <!-- @layout:narrative-reverse:end -->
569
+
570
+ <!-- @layout:highlight-cols:start qa=true -->
571
+ #### Highlight Cols
572
+
573
+ 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.
574
+
575
+ Structural intent:
576
+ - each slot: 1fr column — any component(s)
577
+ - column count: determined by the number of direct child divs in the grid container; `auto-fit` distributes space equally
578
+
579
+ 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.
580
+
581
+ ```html
582
+ <section class="slide" slide-qa="true" data-index="N">
583
+ <div class="slide-canvas">
584
+ <div class="page">
585
+ <div class="highlight-cols-grid" style="flex:1;min-height:0;">
586
+
587
+ <!-- [slot: 1] — 1+ components; suggested: editorial-image-top, editorial-text-top, echart-panel -->
588
+ <div>
589
+ </div>
590
+
591
+ <!-- [slot: 2] — 1+ components; suggested: editorial-text-top, echart-panel, flow-vertical -->
592
+ <div>
593
+ </div>
594
+
595
+ <!-- [slot: 3] — 1+ components; suggested: editorial-image-top, editorial-text-top, echart-panel -->
596
+ <div>
597
+ </div>
598
+
599
+ </div>
600
+ </div>
601
+ </div>
602
+ </section>
603
+ ```
604
+
605
+ ```css
606
+ .highlight-cols-grid {
607
+ display: grid;
608
+ grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
609
+ gap: 32px;
610
+ overflow: hidden;
611
+ align-items: start;
612
+ }
613
+
614
+ .highlight-cols-grid > * {
615
+ overflow: hidden;
616
+ min-height: 0;
617
+ }
618
+ ```
619
+
620
+ ##### Tips
621
+ - **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.
622
+ - **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.
623
+ - **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.
624
+ - **Do not set fixed heights on editorial components.** Let components fill height via flexbox stretch.
625
+ <!-- @layout:highlight-cols:end -->
626
+
627
+ <!-- @layout:halves:start qa=true -->
628
+ #### Halves
629
+
630
+ 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.
631
+
632
+ Structural intent:
633
+ - left slot: 1fr column — any component(s)
634
+ - right slot: 1fr column — any component(s)
635
+
636
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — both columns are fully equal with no hierarchy preset.
637
+
638
+
639
+ ```html
640
+ <section class="slide" slide-qa="true" data-index="N">
641
+ <div class="slide-canvas">
642
+ <div class="page" style="overflow:hidden;">
643
+ <div class="halves-grid" style="flex:1;min-height:0;">
644
+
645
+ <!-- [slot: left] — 1+ components; suggested: echart-panel, data-table, editorial-image-top -->
646
+ <div>
647
+ </div>
648
+
649
+ <!-- [slot: right] — 1+ components; suggested: echart-panel, data-table, text-panel -->
650
+ <div>
651
+ </div>
652
+
653
+ </div>
654
+ </div>
655
+ </div>
656
+ </section>
657
+ ```
658
+
659
+ ```css
660
+ .halves-grid {
661
+ display: grid;
662
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
663
+ gap: 0px;
664
+ overflow: hidden;
665
+ align-items: stretch;
666
+ }
667
+
668
+ .halves-grid > * {
669
+ overflow: hidden;
670
+ min-height: 0;
671
+ min-width: 0;
672
+ }
673
+ ```
674
+
675
+ ##### Tips
676
+ - **Grid container needs `flex:1;min-height:0` inline** when inside `.page`. The class handles column sizing.
677
+ - **Equal columns — no hierarchy.** Both slots carry the same weight. Choose components based on content, not a fixed text/visual assignment.
678
+ - **Gap `40px` is intentional.** The slightly wider gap than `three-col` (32px) compensates for the larger individual column width.
679
+ <!-- @layout:halves:end -->
680
+
681
+ <!-- @layout:stacked:start qa=true -->
682
+ #### Stacked
683
+
684
+ 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.
685
+
686
+ Structural intent:
687
+ - top slot: `1fr` height — upper zone in golden-ratio proportion
688
+ - bottom slot: `1.618fr` height — larger lower zone fills remaining space
689
+
690
+ Every slot accepts 1 or more components. The LLM decides what each slot contains — there is no semantic preset for either row.
691
+
692
+
693
+ ```html
694
+ <section class="slide" slide-qa="true" data-index="N">
695
+ <div class="slide-canvas">
696
+ <div class="page" style="padding:0;">
697
+ <div class="stacked-grid">
698
+
699
+ <!-- [slot: top] — 1+ components; suggested: flow-horizontal, stat-row -->
700
+ <div class="stacked-top">
701
+ </div>
702
+
703
+ <!-- [slot: bottom] — 1+ components; suggested: echart-panel, data-table -->
704
+ <div class="stacked-bottom">
705
+ </div>
706
+
707
+ </div>
708
+ </div>
709
+ </div>
710
+ </section>
711
+ ```
712
+
713
+ ```css
714
+ .stacked-grid {
715
+ display: grid;
716
+ grid-template-rows: minmax(0, 1fr) minmax(0, 1.618fr);
717
+ height: 100%;
718
+ width: 100%;
719
+ overflow: hidden;
720
+ }
721
+
722
+ .stacked-top {
723
+ overflow: hidden;
724
+ min-height: 0;
725
+ }
726
+
727
+ .stacked-bottom {
728
+ overflow: hidden;
729
+ min-height: 0;
730
+ }
731
+ ```
732
+
733
+ ##### Tips
734
+ - **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.
735
+ - **Both slots clip overflow.** `min-height: 0` on both `.stacked-top` and `.stacked-bottom` ensures content cannot break out of its row.
736
+ - **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.
737
+ - **Dark background variant.** Set CSS variable overrides (`--text-primary` etc.) on `.stacked-grid` to cascade into both slots automatically.
738
+ <!-- @layout:stacked:end -->
739
+
740
+ <!-- @design:layouts:end -->
741
+
742
+
743
+ <!-- @design:components:start -->
744
+
745
+ ### Component Library
746
+
747
+ 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.
748
+
749
+
750
+
751
+ <!-- @component:text-panel:start -->
752
+ #### Text Panel
753
+
754
+ <!-- renamed from report-text-panel -->
755
+
756
+ 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.
757
+
758
+ ```html
759
+ <!-- variant A: prose only (--dark) -->
760
+ <div class="text-panel text-panel--dark">
761
+ <div style="max-width:420px;">
762
+ <p class="eyebrow" style="color:rgba(243,238,230,0.72);">Section label / annual review</p>
763
+ <h2 style="margin-top:16px;font-size:60px;line-height:0.92;letter-spacing:-0.03em;text-transform:uppercase;color:#f7f4ee;max-width:360px;">Narrative heading</h2>
764
+ <div class="text-panel-body" style="margin-top:20px;">
765
+ <p style="font-size:13px;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>
766
+ </div>
767
+ </div>
768
+ <div class="text-panel-footer" style="color:rgba(243,238,230,0.68);">
769
+ <p class="caption">Summit / Climate Report 2026</p>
770
+ <p class="caption">03</p>
771
+ </div>
772
+ </div>
773
+
774
+ <!-- variant B: bullet list only (--light) -->
775
+ <div class="text-panel text-panel--light">
776
+ <div style="max-width:420px;">
777
+ <p class="eyebrow">Key findings</p>
778
+ <h3 style="margin-top:12px;">Three priorities for 2026</h3>
779
+ <div class="text-panel-body" style="margin-top:16px;">
780
+ <ul class="editorial-list">
781
+ <li><strong>Lead phrase.</strong> Supporting explanation for this point.</li>
782
+ <li><strong>Second point.</strong> One sentence of context or evidence.</li>
783
+ <li><strong>Third point.</strong> Keep each item roughly equal in length.</li>
784
+ </ul>
785
+ </div>
786
+ </div>
787
+ <div class="text-panel-footer">
788
+ <p class="caption">Summit / Climate Report 2026</p>
789
+ <p class="caption">03</p>
790
+ </div>
791
+ </div>
792
+
793
+ <!-- variant C: prose + bullets mixed (--dark) -->
794
+ <div class="text-panel text-panel--dark">
795
+ <div style="max-width:420px;">
796
+ <p class="eyebrow" style="color:rgba(243,238,230,0.72);">Context</p>
797
+ <h3 style="margin-top:12px;color:#f7f4ee;">Heading here</h3>
798
+ <div class="text-panel-body" style="margin-top:16px;">
799
+ <p style="font-size:13px;line-height:1.58;color:rgba(243,238,230,0.84);">Introductory sentence that frames what follows. Then the list expands the argument:</p>
800
+ <ul class="editorial-list" style="margin-top:12px;">
801
+ <li><strong>Lead phrase.</strong> Supporting explanation.</li>
802
+ <li><strong>Second point.</strong> One sentence of evidence.</li>
803
+ </ul>
804
+ </div>
805
+ </div>
806
+ <div class="text-panel-footer" style="color:rgba(243,238,230,0.68);">
807
+ <p class="caption">Summit / Climate Report 2026</p>
808
+ <p class="caption">03</p>
809
+ </div>
810
+ </div>
811
+ ```
812
+
813
+ ```css
814
+ /* renamed from .report-text-panel */
815
+ .text-panel {
816
+ height: 100%;
817
+ padding: 56px 48px 34px;
818
+ display: flex;
819
+ flex-direction: column;
820
+ justify-content: space-between;
821
+ }
822
+
823
+ .text-panel--dark {
824
+ background: #2c2828;
825
+ color: #f3eee6;
826
+ }
827
+
828
+ .text-panel--light {
829
+ background: var(--bg-page-alt);
830
+ color: var(--text-primary);
831
+ }
832
+
833
+ /* body zone: flex column so prose <p> and <ul> stack naturally with consistent gap */
834
+ .text-panel-body {
835
+ display: flex;
836
+ flex-direction: column;
837
+ gap: 12px;
838
+ }
839
+
840
+ /* renamed from .report-panel-footer */
841
+ .text-panel-footer {
842
+ display: flex;
843
+ justify-content: space-between;
844
+ align-items: end;
845
+ gap: 18px;
846
+ }
847
+ ```
848
+
849
+ Rules:
850
+ - `.text-panel-body` is the only required structural child. Place `<p>` elements, an `<ul class="editorial-list">`, or both inside it.
851
+ - Eyebrow, heading, and footer are all optional — include them only when the content calls for them.
852
+ - Choose `--dark` or `--light` to match the slide's tone. Do not mix variants within a single panel.
853
+ - Pair with a visually dominant neighbor (image, chart) when the layout needs strong contrast against the text zone.
854
+
855
+ ##### Tips
856
+ - **`--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.
857
+ - **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.
858
+ - **`--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.
859
+ - **`--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.
860
+ - **`editorial-list` inside `--dark`.** Add `style="--accent-earth:rgba(247,244,238,0.72)"` on the `<ul>` wrapper so the bullet squares read against the dark background.
861
+ <!-- @component:text-panel:end -->
862
+
863
+ <!-- @component:editorial-image-top:start -->
864
+ #### Editorial Image Top
865
+
866
+ 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.
867
+
868
+ The copy zone uses `text-panel-body` for consistent prose/bullet handling across all editorial modules.
869
+
870
+ ```html
871
+ <div class="editorial-image-top">
872
+ <div class="media-frame editorial-media">
873
+ <img src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?q=80&w=1200&auto=format&fit=crop" alt="Climber on alpine ridge">
874
+ </div>
875
+ <div class="editorial-module-body">
876
+ <div class="module-kicker-row">
877
+ <i data-lucide="leaf" class="module-icon"></i>
878
+ <p class="caption">Alpine materials</p>
879
+ </div>
880
+ <h3>Use the image to set tone before the copy explains the point.</h3>
881
+ <!-- text-panel-body: place <p>, <ul class="editorial-list">, or both -->
882
+ <div class="text-panel-body">
883
+ <p>Choose this component when the visual should establish texture, materiality, or field context before the audience reads the narrative.</p>
884
+ <!-- optional bullets:
885
+ <ul class="editorial-list">
886
+ <li><strong>Lead phrase.</strong> Supporting point.</li>
887
+ <li><strong>Second point.</strong> One sentence.</li>
888
+ </ul>
889
+ -->
890
+ </div>
891
+ </div>
892
+ </div>
893
+ ```
894
+
895
+ ```css
896
+ .editorial-media {
897
+ width: 100%;
898
+ }
899
+
900
+ .editorial-module-body {
901
+ display: flex;
902
+ flex-direction: column;
903
+ gap: 12px;
904
+ }
905
+
906
+ .module-kicker-row {
907
+ display: inline-flex;
908
+ align-items: center;
909
+ gap: 8px;
910
+ }
911
+
912
+ .module-icon {
913
+ width: 15px;
914
+ height: 15px;
915
+ stroke-width: 1.6;
916
+ color: var(--accent-earth);
917
+ flex-shrink: 0;
918
+ }
919
+
920
+ .editorial-image-top {
921
+ display: flex;
922
+ flex-direction: column;
923
+ gap: 16px;
924
+ }
925
+
926
+ .editorial-image-top .editorial-media {
927
+ height: 240px;
928
+ }
929
+
930
+ .editorial-image-top .editorial-module-body h3 {
931
+ margin-top: 2px;
932
+ }
933
+ ```
934
+
935
+ Rules:
936
+ - Use when the image should lead and the text should read as a follow-on explanation.
937
+ - The image should usually occupy more visual weight than the text.
938
+ - Icon is optional. If used, keep it small, single-color, and subordinate to the caption.
939
+ - The `text-panel-body` inside `.editorial-module-body` accepts prose `<p>`, an `<ul class="editorial-list">`, or both. Use whichever form suits the content.
940
+ - When used inside multi-column layouts, keep this component's copy shorter than the primary reading column unless the page hierarchy explicitly promotes it.
941
+
942
+ ##### Tips
943
+ - **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.
944
+ - **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.
945
+ - **Kicker icon size.** Keep Lucide SVG icons at 16–20px. Larger icons shift visual weight from the image to the label zone.
946
+ - **`editorial-list` font-size.** Override to `font-size:13px;gap:10px` inline — the base `editorial-list` is `14px/gap:14px`, which is slightly large for the narrow copy zone here.
947
+ <!-- @component:editorial-image-top:end -->
948
+
949
+ <!-- @component:editorial-text-top:start -->
950
+ #### Editorial Text Top
951
+
952
+ 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.
953
+
954
+ The copy zone uses `text-panel-body` for consistent prose/bullet handling across all editorial modules.
955
+
956
+ ```html
957
+ <div class="editorial-text-top">
958
+ <div class="editorial-module-body">
959
+ <div class="module-kicker-row">
960
+ <i data-lucide="gauge" class="module-icon"></i>
961
+ <p class="caption">Mountain operations</p>
962
+ </div>
963
+ <h3>Lead with the argument, then let the image confirm it.</h3>
964
+ <!-- text-panel-body: place <p>, <ul class="editorial-list">, or both -->
965
+ <div class="text-panel-body">
966
+ <p>Use this component when the audience should understand the point first and only then read the image as supporting evidence or context.</p>
967
+ <!-- optional bullets:
968
+ <ul class="editorial-list">
969
+ <li><strong>Lead phrase.</strong> Supporting point.</li>
970
+ <li><strong>Second point.</strong> One sentence.</li>
971
+ </ul>
972
+ -->
973
+ </div>
974
+ </div>
975
+ <div class="media-frame editorial-media">
976
+ <img src="https://images.unsplash.com/photo-1519904981063-b0cf448d479e?q=80&w=1200&auto=format&fit=crop" alt="Hiker in alpine basin">
977
+ </div>
978
+ <p class="media-caption">Optional caption / field site / program name</p>
979
+ </div>
980
+ ```
981
+
982
+ ```css
983
+ .editorial-text-top {
984
+ display: flex;
985
+ flex-direction: column;
986
+ gap: 16px;
987
+ }
988
+
989
+ .editorial-text-top .editorial-module-body {
990
+ padding-bottom: 2px;
991
+ }
992
+
993
+ .editorial-text-top .editorial-media {
994
+ height: 240px;
995
+ }
996
+
997
+ .editorial-text-top .media-caption {
998
+ margin-top: -4px;
999
+ }
1000
+ ```
1001
+
1002
+ Rules:
1003
+ - Use when the text must establish the idea before the image appears.
1004
+ - The `text-panel-body` inside `.editorial-module-body` accepts prose `<p>`, an `<ul class="editorial-list">`, or both. Use whichever form suits the content.
1005
+ - Keep the lower image block quieter than a hero image; it is supporting evidence, not the whole slide's focal point.
1006
+ - Icon is optional. Prefer it only when it helps distinguish categories across repeated modules.
1007
+ - In multi-column layouts, this component can carry more reading weight than neighboring support modules when the page hierarchy needs a denser narrative block.
1008
+
1009
+ ##### Tips
1010
+ - **Same height/stretch rule as `editorial-image-top`.** Do not set fixed heights; let parent grid stretch control the column.
1011
+ - **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.
1012
+ - **`editorial-list` font-size.** Override to `font-size:13px;gap:10px` inline — the base `editorial-list` is `14px/gap:14px`, which is slightly large for the narrow copy zone here.
1013
+ <!-- @component:editorial-text-top:end -->
1014
+
1015
+ <!-- @component:editorial-text-left:start -->
1016
+ #### Editorial Text Left
1017
+
1018
+ 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.
1019
+
1020
+ Structure:
1021
+ - **header zone** (full width): holds the `h3` module title — independent of the copy below
1022
+ - **left: `.editorial-text-left-copy`** — kicker, then `text-panel-body` (prose, bullets, or both)
1023
+ - **right: `.editorial-text-left-visual`** — accepts any of: `media-frame img`, `echart-container`, or `image-title`
1024
+
1025
+ ```html
1026
+ <div class="editorial-text-left">
1027
+
1028
+ <!-- header: module title spans full width -->
1029
+ <div class="editorial-text-left-header">
1030
+ <h3 style="font-size:20px;line-height:1.08;">Module title — a single standalone heading above both columns</h3>
1031
+ </div>
1032
+
1033
+ <div class="editorial-text-left-content">
1034
+
1035
+ <!-- left: editorial copy -->
1036
+ <div class="editorial-text-left-copy">
1037
+ <div class="module-kicker-row">
1038
+ <i data-lucide="zap" class="module-icon"></i>
1039
+ <p class="caption">Category label</p>
1040
+ </div>
1041
+ <!-- text-panel-body: place <p>, <ul class="editorial-list">, or both — choose based on content -->
1042
+ <div class="text-panel-body" style="margin-top:12px;">
1043
+ <!-- prose variant -->
1044
+ <p style="font-size:13px;line-height:1.5;color:var(--text-secondary);">Supporting description. One or two sentences that position this card within the broader page argument.</p>
1045
+ <!-- bullet variant (use instead of or after prose): -->
1046
+ <!-- <ul class="editorial-list" style="font-size:13px;gap:10px;">
1047
+ <li><strong>Lead phrase.</strong> Supporting explanation for this point.</li>
1048
+ <li><strong>Second point.</strong> One sentence of context or evidence.</li>
1049
+ <li><strong>Third point.</strong> Keep each item roughly equal in length.</li>
1050
+ </ul> -->
1051
+ </div>
1052
+ </div>
1053
+
1054
+ <!-- right: visual slot — choose one -->
1055
+ <div class="editorial-text-left-visual">
1056
+
1057
+ <!-- option A: plain image -->
1058
+ <div class="media-frame" style="width:100%;height:100%;">
1059
+ <img src="https://images.unsplash.com/photo-1519904981063-b0cf448d479e?q=80&w=800&auto=format&fit=crop" alt="Supporting visual">
1060
+ </div>
1061
+
1062
+ <!-- option B: echart -->
1063
+ <!-- <div class="echart-container" id="chart-unique-id" style="width:100%;height:100%;"></div> -->
1064
+
1065
+ <!-- option C: image-title (self-contained, handles its own overlay and text layers) -->
1066
+ <!-- <div class="image-title image-title--right"> ... </div> -->
1067
+
1068
+ </div>
1069
+
1070
+ </div>
1071
+ </div>
1072
+ ```
1073
+
1074
+ ```css
1075
+ .editorial-text-left {
1076
+ display: flex;
1077
+ flex-direction: column;
1078
+ gap: 0;
1079
+ height: 100%;
1080
+ overflow: hidden;
1081
+ }
1082
+
1083
+ .editorial-text-left-header {
1084
+ flex-shrink: 0;
1085
+ padding: 20px 24px 16px;
1086
+ }
1087
+
1088
+ .editorial-text-left-content {
1089
+ display: flex;
1090
+ flex: 1;
1091
+ min-height: 0;
1092
+ }
1093
+
1094
+ .editorial-text-left-copy {
1095
+ flex: 1.1;
1096
+ min-width: 0;
1097
+ padding: 16px 20px 20px 24px;
1098
+ display: flex;
1099
+ flex-direction: column;
1100
+ justify-content: flex-start;
1101
+ }
1102
+
1103
+ .editorial-text-left-visual {
1104
+ flex: 1;
1105
+ min-width: 0;
1106
+ min-height: 0;
1107
+ align-self: stretch;
1108
+ overflow: hidden;
1109
+ position: relative;
1110
+ }
1111
+ ```
1112
+
1113
+ Rules:
1114
+ - The `h3` in `.editorial-text-left-header` is the module's top-level title; it must not be repeated inside the copy zone.
1115
+ - 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.
1116
+ - 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.
1117
+ - 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.
1118
+ - 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: IBM Plex Sans Condensed; font-weight: 700; color: var(--accent-gold); line-height: 1;`).
1119
+
1120
+ ##### Tips
1121
+ - **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`).
1122
+ - **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.
1123
+ - **`editorial-list` font-size inside copy zone.** Override to `font-size:13px` and `gap:10px` inline — the base `editorial-list` uses `14px / gap:14px`, which is slightly large for the narrow copy column.
1124
+ - **`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.
1125
+ - **`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.
1126
+ - **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.
1127
+ <!-- @component:editorial-text-left:end -->
1128
+
1129
+ <!-- @component:echart-panel:start -->
1130
+ #### EChart Panel
1131
+
1132
+ 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.
1133
+
1134
+ ```html
1135
+ <div class="echart-panel">
1136
+ <div class="echart-panel-header">
1137
+ <p class="eyebrow">SECTION LABEL</p>
1138
+ <h3>Chart Title</h3>
1139
+ <p class="chart-subtitle">Optional context or unit note</p>
1140
+ </div>
1141
+ <div class="echart-container" id="chart-01"></div>
1142
+ <p class="chart-caption">Source: Organisation / Year</p>
1143
+ </div>
1144
+
1145
+ <script>
1146
+ // Include ECharts in the HTML head only when charts are present:
1147
+ // <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>
1148
+ // Initialise after SlidePresentation is instantiated:
1149
+ const chart = echarts.init(document.getElementById('chart-01'));
1150
+ chart.setOption({ /* LLM selects type and config */ });
1151
+ </script>
1152
+ ```
1153
+
1154
+ ```css
1155
+ .echart-panel {
1156
+ display: flex;
1157
+ flex-direction: column;
1158
+ height: 100%;
1159
+ gap: 0;
1160
+ }
1161
+
1162
+ .echart-panel-header {
1163
+ flex-shrink: 0;
1164
+ padding-bottom: 16px;
1165
+ border-bottom: 1px solid var(--line);
1166
+ margin-bottom: 20px;
1167
+ }
1168
+
1169
+ .echart-panel-header h3 {
1170
+ margin-top: 6px;
1171
+ }
1172
+
1173
+ .chart-subtitle {
1174
+ margin-top: 4px;
1175
+ font-size: 13px;
1176
+ color: var(--text-muted);
1177
+ line-height: 1.4;
1178
+ }
1179
+
1180
+ .echart-container {
1181
+ flex: 1;
1182
+ min-height: 0; /* required inside flex to let ECharts fill correctly */
1183
+ }
1184
+
1185
+ .chart-caption {
1186
+ flex-shrink: 0;
1187
+ margin-top: 12px;
1188
+ font-size: 11px;
1189
+ letter-spacing: 0.12em;
1190
+ text-transform: uppercase;
1191
+ color: var(--text-muted);
1192
+ }
1193
+ ```
1194
+
1195
+ Rules:
1196
+ - Always include `min-height: 0` on `.echart-container`; without it ECharts overflows flex containers.
1197
+ - Set `id` per chart to avoid collision when multiple charts appear in one slide.
1198
+ - Choose chart type, colour palette, and axis config based on the data. Summit palette suggestion: use `--accent-earth` (`#8d6a49`), `--accent-olive` (`#6f7562`), and `--text-muted` (`#8a7f73`) as the primary series colours.
1199
+ - Add ECharts script tag to `<head>` only when charts are present in the slide deck; do not include it unconditionally.
1200
+ - `echarts.init()` must run after the DOM is ready and after `SlidePresentation` is instantiated.
1201
+
1202
+ ##### Tips
1203
+ - **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.
1204
+ - **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(247,244,238,0.65)` for labels, `rgba(247,244,238,0.2)` for grid lines.
1205
+ - **Candlestick on dark background.** Use `--accent-olive` (`#6f7562`) for up candles and `--accent-danger` (`#b94a3c`) for down candles. These read clearly against dark backgrounds and stay within the Summit palette.
1206
+ - **`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.
1207
+ - **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.
1208
+ <!-- @component:echart-panel:end -->
1209
+
1210
+ <!-- @component:flow-horizontal:start -->
1211
+ #### Flow Horizontal
1212
+
1213
+ 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.
1214
+
1215
+ ```html
1216
+ <div class="flow-horizontal">
1217
+ <div class="flow-item">
1218
+ <div class="flow-number">01</div>
1219
+ <div class="flow-body">
1220
+ <h4>Step Title</h4>
1221
+ <p>Brief description of this step or phase.</p>
1222
+ </div>
1223
+ </div>
1224
+ <div class="flow-item">
1225
+ <div class="flow-number">02</div>
1226
+ <div class="flow-body">
1227
+ <h4>Step Title</h4>
1228
+ <p>Brief description of this step or phase.</p>
1229
+ </div>
1230
+ </div>
1231
+ <div class="flow-item">
1232
+ <div class="flow-number">03</div>
1233
+ <div class="flow-body">
1234
+ <h4>Step Title</h4>
1235
+ <p>Brief description of this step or phase.</p>
1236
+ </div>
1237
+ </div>
1238
+ </div>
1239
+ ```
1240
+
1241
+ ```css
1242
+ .flow-horizontal {
1243
+ position: relative;
1244
+ display: flex;
1245
+ align-items: flex-start;
1246
+ width: 100%;
1247
+ }
1248
+
1249
+ .flow-horizontal::before {
1250
+ content: '';
1251
+ position: absolute;
1252
+ top: 17px;
1253
+ left: 0;
1254
+ right: 0;
1255
+ height: 1px;
1256
+ background: var(--line-strong);
1257
+ z-index: 0;
1258
+ }
1259
+
1260
+ .flow-horizontal .flow-item {
1261
+ flex: 1;
1262
+ display: flex;
1263
+ flex-direction: column;
1264
+ gap: 18px;
1265
+ padding-right: 40px;
1266
+ }
1267
+
1268
+ .flow-horizontal .flow-item:last-child {
1269
+ padding-right: 0;
1270
+ }
1271
+
1272
+ .flow-horizontal .flow-number {
1273
+ position: relative;
1274
+ z-index: 1;
1275
+ background: var(--bg-page);
1276
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
1277
+ font-size: 13px;
1278
+ font-weight: 700;
1279
+ letter-spacing: 0.12em;
1280
+ color: var(--text-muted);
1281
+ border: 1px solid var(--line-strong);
1282
+ width: 34px;
1283
+ height: 34px;
1284
+ display: flex;
1285
+ align-items: center;
1286
+ justify-content: center;
1287
+ flex-shrink: 0;
1288
+ }
1289
+
1290
+ .flow-horizontal .flow-body h4 {
1291
+ font-size: 20px;
1292
+ font-weight: 600;
1293
+ line-height: 1.14;
1294
+ }
1295
+
1296
+ .flow-horizontal .flow-body p {
1297
+ font-size: 14px;
1298
+ line-height: 1.6;
1299
+ color: var(--text-secondary);
1300
+ }
1301
+ ```
1302
+
1303
+ Rules:
1304
+ - Prefer 3–4 items for balanced layout; 5 items work when copy is very short.
1305
+ - Do not use arrowheads or chevrons between items; the horizontal rule threading through the numbers is the only connector.
1306
+ - Number labels are report-style (`01`, `02`, `03`), not circles or bullets.
1307
+ - Keep each item's body copy short — this is a reference summary, not a detailed explanation.
1308
+
1309
+ ##### Tips
1310
+ - **Dark background color overrides.** Flow-number: `border-color:rgba(247,244,238,0.3); color:rgba(247,244,238,0.6); background:<dark-bg-color>`. Heading h4: `color:#f7f4ee`. Body p: `color:rgba(247,244,238,0.7)`. Apply inline on each element — CSS cascade does not automatically inherit from the slide background.
1311
+ - **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).
1312
+ - **Horizontal rule connector.** The `::before` pseudo-element on `.flow-horizontal` draws a full-width line at `top: 17px` (vertical centre of the 34px number box). `.flow-number` sits above it via `z-index: 1` and `background: var(--bg-page)`, creating the effect of the line threading through the numbers. On dark backgrounds, override `background` on `.flow-number` to match the slide background color, and set `.flow-horizontal::before { background: rgba(247,244,238,0.15); }`.
1313
+ <!-- @component:flow-horizontal:end -->
1314
+
1315
+ <!-- @component:flow-vertical:start -->
1316
+ #### Flow Vertical
1317
+
1318
+ 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.
1319
+
1320
+ ```html
1321
+ <div class="flow-vertical">
1322
+ <div class="flow-item">
1323
+ <div class="flow-marker">
1324
+ <div class="flow-number">01</div>
1325
+ <div class="flow-line"></div>
1326
+ </div>
1327
+ <div class="flow-body">
1328
+ <h4>Phase Title</h4>
1329
+ <p>Description of this stage or milestone.</p>
1330
+ </div>
1331
+ </div>
1332
+ <div class="flow-item">
1333
+ <div class="flow-marker">
1334
+ <div class="flow-number">02</div>
1335
+ <div class="flow-line"></div>
1336
+ </div>
1337
+ <div class="flow-body">
1338
+ <h4>Phase Title</h4>
1339
+ <p>Description of this stage or milestone.</p>
1340
+ </div>
1341
+ </div>
1342
+ <div class="flow-item last">
1343
+ <div class="flow-marker">
1344
+ <div class="flow-number">03</div>
1345
+ <!-- no flow-line on last item -->
1346
+ </div>
1347
+ <div class="flow-body">
1348
+ <h4>Phase Title</h4>
1349
+ <p>Description of this stage or milestone.</p>
1350
+ </div>
1351
+ </div>
1352
+ </div>
1353
+ ```
1354
+
1355
+ ```css
1356
+ .flow-vertical {
1357
+ display: flex;
1358
+ flex-direction: column;
1359
+ width: 100%;
1360
+ }
1361
+
1362
+ .flow-vertical .flow-item {
1363
+ display: flex;
1364
+ gap: 28px;
1365
+ align-items: flex-start;
1366
+ }
1367
+
1368
+ .flow-vertical .flow-marker {
1369
+ display: flex;
1370
+ flex-direction: column;
1371
+ align-items: center;
1372
+ flex-shrink: 0;
1373
+ }
1374
+
1375
+ .flow-vertical .flow-number {
1376
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
1377
+ font-size: 13px;
1378
+ font-weight: 700;
1379
+ letter-spacing: 0.12em;
1380
+ color: var(--text-muted);
1381
+ border: 1px solid var(--line-strong);
1382
+ width: 34px;
1383
+ height: 34px;
1384
+ display: flex;
1385
+ align-items: center;
1386
+ justify-content: center;
1387
+ flex-shrink: 0;
1388
+ }
1389
+
1390
+ .flow-vertical .flow-line {
1391
+ width: 1px;
1392
+ flex: 1;
1393
+ min-height: 28px;
1394
+ background: var(--line-strong);
1395
+ margin: 6px 0;
1396
+ }
1397
+
1398
+ .flow-vertical .flow-body {
1399
+ padding-bottom: 32px;
1400
+ }
1401
+
1402
+ .flow-vertical .flow-item.last .flow-body {
1403
+ padding-bottom: 0;
1404
+ }
1405
+
1406
+ .flow-vertical .flow-body h4 {
1407
+ font-size: 20px;
1408
+ font-weight: 600;
1409
+ line-height: 1.14;
1410
+ margin-top: 4px;
1411
+ }
1412
+
1413
+ .flow-vertical .flow-body p {
1414
+ margin-top: 8px;
1415
+ font-size: 14px;
1416
+ line-height: 1.6;
1417
+ color: var(--text-secondary);
1418
+ }
1419
+ ```
1420
+
1421
+ Rules:
1422
+ - Add class `last` to the final `.flow-item` and omit the `.flow-line` div inside it.
1423
+ - The connecting line grows to fill the vertical space between items via `flex: 1`.
1424
+ - Number boxes share the same border-only square style as `flow-horizontal` for visual consistency.
1425
+ - Copy per item can be slightly longer than horizontal flow since vertical reading allows more density.
1426
+ - Combine with `text-panel` or `echart-panel` on the opposing side of a layout when needed.
1427
+
1428
+ ##### Tips
1429
+ - **`.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`.
1430
+ - **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`.
1431
+ - **Dark text overrides (same as flow-horizontal).** Flow-number border `rgba(247,244,238,0.3)`, color `rgba(247,244,238,0.6)`; h4 `#f7f4ee`; p `rgba(247,244,238,0.7)`. Also override the connecting line color: `background:rgba(247,244,238,0.2)`.
1432
+ - **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.
1433
+ <!-- @component:flow-vertical:end -->
1434
+
1435
+ <!-- @component:data-table:start -->
1436
+ #### Data Table
1437
+
1438
+ 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.
1439
+
1440
+ ```html
1441
+ <div class="data-table-wrap">
1442
+ <p class="data-table-label">Key Figures — Income Statement</p>
1443
+ <table class="data-table">
1444
+ <thead>
1445
+ <tr>
1446
+ <th>Scope</th>
1447
+ <th>2021</th>
1448
+ <th>2022</th>
1449
+ <th class="col-highlight">2023</th>
1450
+ <th class="col-highlight">2024</th>
1451
+ <th>YoY</th>
1452
+ </tr>
1453
+ </thead>
1454
+ <tbody>
1455
+ <tr class="section-header">
1456
+ <td colspan="6">Direct Emissions</td>
1457
+ </tr>
1458
+ <tr>
1459
+ <td>Scope 1</td>
1460
+ <td>1,329.2</td>
1461
+ <td>1,273.4</td>
1462
+ <td class="col-highlight">1,156.4</td>
1463
+ <td class="col-highlight">1,042.0</td>
1464
+ <td class="delta positive">−10%</td>
1465
+ </tr>
1466
+ <tr>
1467
+ <td>Scope 2</td>
1468
+ <td>1,617.8</td>
1469
+ <td>1,432.9</td>
1470
+ <td class="col-highlight">820.0</td>
1471
+ <td class="col-highlight">0.0</td>
1472
+ <td class="delta positive">−100%</td>
1473
+ </tr>
1474
+ <tr class="subtotal">
1475
+ <td>1+2 (net)</td>
1476
+ <td>2,905.5</td>
1477
+ <td>3,286.3</td>
1478
+ <td class="col-highlight">1,976.4</td>
1479
+ <td class="col-highlight">1,042.0</td>
1480
+ <td class="delta positive">−47%</td>
1481
+ </tr>
1482
+ </tbody>
1483
+ </table>
1484
+ <p class="table-caption">Thousands of tonnes CO₂e · Source: Company Disclosures 2024</p>
1485
+ </div>
1486
+ ```
1487
+
1488
+ ```css
1489
+ .data-table-wrap {
1490
+ width: 100%;
1491
+ }
1492
+
1493
+ .data-table-label {
1494
+ font-size: 10px;
1495
+ font-weight: 700;
1496
+ letter-spacing: 0.14em;
1497
+ text-transform: uppercase;
1498
+ color: var(--text-muted);
1499
+ margin-bottom: 8px;
1500
+ }
1501
+
1502
+ .data-table {
1503
+ width: 100%;
1504
+ border-collapse: collapse;
1505
+ font-family: 'Inter', sans-serif;
1506
+ font-size: 13px;
1507
+ font-variant-numeric: tabular-nums;
1508
+ color: var(--text-primary);
1509
+ }
1510
+
1511
+ .data-table thead tr {
1512
+ border-bottom: 1.5px solid var(--line-strong);
1513
+ }
1514
+
1515
+ .data-table th {
1516
+ padding: 0 12px 10px 0;
1517
+ text-align: left;
1518
+ font-size: 11px;
1519
+ font-weight: 600;
1520
+ letter-spacing: 0.1em;
1521
+ text-transform: uppercase;
1522
+ color: var(--text-muted);
1523
+ white-space: nowrap;
1524
+ }
1525
+
1526
+ .data-table th:not(:first-child),
1527
+ .data-table td:not(:first-child) {
1528
+ text-align: right;
1529
+ }
1530
+
1531
+ .data-table th.col-highlight,
1532
+ .data-table td.col-highlight {
1533
+ color: var(--text-primary);
1534
+ background: rgba(23, 20, 17, 0.05);
1535
+ padding-left: 6px;
1536
+ padding-right: 8px;
1537
+ }
1538
+
1539
+ .data-table th.col-highlight {
1540
+ color: var(--text-secondary);
1541
+ }
1542
+
1543
+ .data-table tbody tr {
1544
+ border-bottom: 1px solid var(--line);
1545
+ }
1546
+
1547
+ .data-table tbody tr:last-child {
1548
+ border-bottom: none;
1549
+ }
1550
+
1551
+ .data-table td {
1552
+ padding: 9px 12px 9px 0;
1553
+ line-height: 1.4;
1554
+ color: var(--text-secondary);
1555
+ }
1556
+
1557
+ .data-table tr.subtotal td {
1558
+ font-weight: 600;
1559
+ color: var(--text-primary);
1560
+ border-top: 1px solid var(--line-strong);
1561
+ border-bottom: 1px solid var(--line-strong);
1562
+ }
1563
+
1564
+ .data-table tr.section-header td {
1565
+ font-size: 10px;
1566
+ font-weight: 700;
1567
+ letter-spacing: 0.10em;
1568
+ text-transform: uppercase;
1569
+ color: var(--text-muted);
1570
+ padding-top: 14px;
1571
+ padding-bottom: 4px;
1572
+ border-bottom: none;
1573
+ }
1574
+
1575
+ .data-table .delta {
1576
+ font-weight: 600;
1577
+ white-space: nowrap;
1578
+ }
1579
+
1580
+ .data-table .delta.positive {
1581
+ color: var(--accent-olive);
1582
+ }
1583
+
1584
+ .data-table .delta.negative {
1585
+ color: var(--accent-danger);
1586
+ }
1587
+
1588
+ .data-table .delta.neutral {
1589
+ color: var(--text-muted);
1590
+ }
1591
+
1592
+ .table-caption {
1593
+ margin-top: 12px;
1594
+ font-size: 11px;
1595
+ letter-spacing: 0.1em;
1596
+ text-transform: uppercase;
1597
+ color: var(--text-muted);
1598
+ }
1599
+ ```
1600
+
1601
+ Rules:
1602
+ - No outer border. Separation is by `border-bottom` on rows only.
1603
+ - Numeric columns are right-aligned; the label column is left-aligned.
1604
+ - Use `tabular-nums` for number columns so decimals align vertically.
1605
+ - Use `.subtotal` on summary rows (totals, net figures) — heavier weight and double-rule border.
1606
+ - Use `.section-header` rows to group rows into labelled categories within a single table (no data, just a label spanning all columns).
1607
+ - Use `.col-highlight` on both `th` and `td` in a column to spotlight the current or most important period.
1608
+ - `.delta.positive` and `.delta.negative` use Summit accent colours, not generic green/red. `.delta.neutral` for flat movement.
1609
+ - Use `data-table-label` as a heading above the table when multiple tables are stacked.
1610
+ - Include `.table-caption` below with the data source and unit.
1611
+
1612
+ ##### Tips
1613
+ - **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.
1614
+ - **Dark background: override CSS variables on `.data-table-wrap`.** Set `--text-primary:#f7f4ee`, `--text-secondary:rgba(247,244,238,0.7)`, `--text-muted:rgba(247,244,238,0.45)`, `--line:rgba(247,244,238,0.12)`, `--line-strong:rgba(247,244,238,0.28)`. All child elements inherit automatically via `var()`.
1615
+ - **`col-highlight` on dark.** Override background on `.data-table-wrap`: `.data-table th.col-highlight, .data-table td.col-highlight { background: rgba(247,244,238,0.06); }`. Also override `--accent-earth` → `var(--accent-gold)` so highlight header color remains visible.
1616
+ - **Delta positive on dark.** Override `--accent-olive` → `#8faf7e` on `.data-table-wrap`. The default `--accent-olive` (#6f7562) is nearly invisible on dark backgrounds.
1617
+ - **`.table-caption` on dark.** Set explicitly: `color:rgba(247,244,238,0.45)`. It does not inherit from the CSS variable override on the wrapper.
1618
+ - **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.
1619
+ <!-- @component:data-table:end -->
1620
+
1621
+
1622
+
1623
+
1624
+
1625
+
1626
+
1627
+ <!-- @component:image-title:start -->
1628
+ #### Image Title
1629
+
1630
+ 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.
1631
+
1632
+ The LLM controls three key variables via inline style or modifier class:
1633
+ - **Alignment**: `image-title--left` (cover default), `image-title--right` (closing default), `image-title--center`
1634
+ - **Overlay opacity / direction**: set the `background` gradient on `.image-title-overlay` inline
1635
+ - **Text opacity**: set `opacity` on `.image-title` itself (range `0.7`–`1.0`; default `1.0`)
1636
+
1637
+ ```html
1638
+ <div class="image-title image-title--left">
1639
+
1640
+ <!-- Layer 0: background image -->
1641
+ <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">
1642
+
1643
+ <!-- Layer 1: directional blur — mask gradient concentrates blur on the text side -->
1644
+ <!-- cover (--left): mask to-left → blur left, sharp right -->
1645
+ <!-- closing (--right): mask to-right → blur right, sharp left -->
1646
+ <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>
1647
+
1648
+ <!-- Layer 2: gradient overlay — adjust direction and opacity to suit slide purpose -->
1649
+ <!-- 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%) -->
1650
+ <!-- closing default: linear-gradient(180deg, rgba(0,0,0,0.30) 0%, rgba(0,0,0,0.72) 100%) -->
1651
+ <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>
1652
+
1653
+ <!-- Layer 3: foreground text -->
1654
+ <div class="image-title-fg">
1655
+ <div class="image-title-brand reveal">
1656
+ <span class="chevron-divider" style="color:rgba(247,244,238,0.55);">Organisation Name</span>
1657
+ </div>
1658
+ <div class="image-title-body">
1659
+ <p class="image-title-eyebrow reveal">Report Title · Year</p>
1660
+ <h1 class="reveal">Opening<br>Statement<br>Here.</h1>
1661
+ <p class="image-title-subtitle reveal">One or two lines of supporting copy. Keep it short — the image does the visual work.</p>
1662
+ </div>
1663
+ <div class="image-title-footer">
1664
+ <p class="caption reveal" style="color:rgba(247,244,238,0.5);">website or location</p>
1665
+ <p class="caption reveal" style="color:rgba(247,244,238,0.5);">Organisation · Year</p>
1666
+ </div>
1667
+ </div>
1668
+
1669
+ </div>
1670
+ ```
1671
+
1672
+ ```css
1673
+ /* Container — clips all layers, holds stacking context */
1674
+ .image-title {
1675
+ position: relative;
1676
+ width: 100%;
1677
+ height: 100%;
1678
+ overflow: hidden;
1679
+ color: #f7f4ee;
1680
+ }
1681
+
1682
+ /* Layer 0: background image */
1683
+ .image-title-media {
1684
+ position: absolute;
1685
+ inset: 0;
1686
+ width: 100%;
1687
+ height: 100%;
1688
+ object-fit: cover;
1689
+ display: block;
1690
+ z-index: 0;
1691
+ }
1692
+
1693
+ /* Layer 1: directional blur */
1694
+ .image-title-blur {
1695
+ position: absolute;
1696
+ inset: 0;
1697
+ z-index: 1;
1698
+ backdrop-filter: blur(50px);
1699
+ -webkit-backdrop-filter: blur(50px);
1700
+ }
1701
+
1702
+ /* Layer 2: gradient overlay — background set inline by LLM */
1703
+ .image-title-overlay {
1704
+ position: absolute;
1705
+ inset: 0;
1706
+ z-index: 2;
1707
+ }
1708
+
1709
+ /* Layer 3: foreground text */
1710
+ .image-title-fg {
1711
+ position: relative;
1712
+ z-index: 3;
1713
+ height: 100%;
1714
+ display: flex;
1715
+ flex-direction: column;
1716
+ justify-content: space-between;
1717
+ padding: 72px 84px;
1718
+ }
1719
+
1720
+ /* Alignment variants */
1721
+ .image-title--left .image-title-body {
1722
+ max-width: 680px;
1723
+ }
1724
+
1725
+ .image-title--right .image-title-fg {
1726
+ text-align: right;
1727
+ }
1728
+ .image-title--right .image-title-body {
1729
+ max-width: 860px;
1730
+ margin-left: auto;
1731
+ }
1732
+
1733
+ .image-title--center .image-title-fg {
1734
+ text-align: center;
1735
+ align-items: center;
1736
+ }
1737
+ .image-title--center .image-title-body {
1738
+ max-width: 900px;
1739
+ }
1740
+
1741
+ /* Text elements */
1742
+ .image-title-eyebrow {
1743
+ font-size: 11px;
1744
+ font-weight: 700;
1745
+ letter-spacing: 0.18em;
1746
+ text-transform: uppercase;
1747
+ color: rgba(247, 244, 238, 0.55);
1748
+ margin-bottom: 20px;
1749
+ }
1750
+
1751
+ .image-title h1 {
1752
+ color: #f7f4ee;
1753
+ font-size: 96px;
1754
+ line-height: 0.92;
1755
+ letter-spacing: -0.03em;
1756
+ text-transform: uppercase;
1757
+ }
1758
+
1759
+ .image-title-subtitle {
1760
+ margin-top: 24px;
1761
+ font-size: 15px;
1762
+ line-height: 1.56;
1763
+ color: rgba(247, 244, 238, 0.72);
1764
+ max-width: 480px;
1765
+ }
1766
+
1767
+ .image-title-footer {
1768
+ display: flex;
1769
+ justify-content: space-between;
1770
+ align-items: flex-end;
1771
+ gap: 24px;
1772
+ }
1773
+ ```
1774
+
1775
+ Rules:
1776
+ - Always use one of the three modifier classes: `image-title--left`, `image-title--right`, or `image-title--center`. Never omit the modifier.
1777
+ - 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.
1778
+ - 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.
1779
+ - All text inside `.image-title-fg` must be white-family: headings `#f7f4ee`, body `rgba(247,244,238,0.72)`, captions and eyebrows `rgba(247,244,238,0.50–0.55)`.
1780
+ - Use `.page-number--light` on any slide using this component.
1781
+ - When used inside `fullbleed` layout, place this component directly as the sole child of the `.page` div — the layout provides no additional framing.
1782
+
1783
+ ##### Tips
1784
+ - **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`.
1785
+ - **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.
1786
+ - **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`.
1787
+ - **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`.
1788
+ - **`--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.
1789
+ - **`--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%)`.
1790
+ - **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;"`.
1791
+ <!-- @component:image-title:end -->
1792
+
1793
+ <!-- @component:toc:start -->
1794
+ #### TOC Panel
1795
+
1796
+ Narrow editorial panel for table-of-contents slides. A 3px accent-gold vertical rule on the left anchors the panel; the right body holds a title, short intro note, chapter list, and a footer block. The `justify-content:space-between` flex column pins the footer to the bottom of the panel.
1797
+
1798
+ ```html
1799
+ <div class="toc-panel">
1800
+ <div style="width:3px;background:var(--accent-gold);flex:0 0 3px;"></div>
1801
+ <div style="padding-left:22px;display:flex;flex-direction:column;justify-content:space-between;flex:1;">
1802
+ <div>
1803
+ <h2 style="font-size:34px;line-height:0.94;letter-spacing:-0.03em;text-transform:uppercase;max-width:220px;">Table of Contents</h2>
1804
+ <p style="margin-top:18px;font-size:11px;line-height:1.6;letter-spacing:0.06em;color:var(--text-secondary);max-width:255px;">Short introductory note describing the scope of the sections that follow.</p>
1805
+ <ol style="list-style:none;display:flex;flex-direction:column;gap:10px;margin-top:26px;">
1806
+ <li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;font-size:11px;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">01</span><span>Chapter title or section theme</span></li>
1807
+ <li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;font-size:11px;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">02</span><span>Chapter title or section theme</span></li>
1808
+ <li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;font-size:11px;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">03</span><span>Chapter title or section theme</span></li>
1809
+ <li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;font-size:11px;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">04</span><span>Chapter title or section theme</span></li>
1810
+ <li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;font-size:11px;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;border-bottom:1px solid var(--line);padding-bottom:8px;"><span style="font-weight:700;">05</span><span>Chapter title or section theme</span></li>
1811
+ <li style="display:grid;grid-template-columns:26px 1fr;gap:12px;align-items:center;font-size:11px;line-height:1.45;text-transform:uppercase;letter-spacing:0.06em;"><span style="font-weight:700;">06</span><span>Chapter title or section theme</span></li>
1812
+ </ol>
1813
+ </div>
1814
+ <div style="display:flex;flex-direction:column;gap:14px;">
1815
+ <div class="rule"></div>
1816
+ <div>
1817
+ <p class="caption">Scope of report</p>
1818
+ <p style="margin-top:10px;font-size:11px;line-height:1.6;color:var(--text-secondary);max-width:255px;">Optional scope note, data coverage period, or brief methodology reference.</p>
1819
+ </div>
1820
+ <div style="display:flex;justify-content:space-between;align-items:end;">
1821
+ <p class="caption">Organisation · Year</p>
1822
+ </div>
1823
+ </div>
1824
+ </div>
1825
+ </div>
1826
+ ```
1827
+
1828
+ ```css
1829
+ .toc-panel {
1830
+ background: var(--bg-page);
1831
+ height: 100%;
1832
+ padding: 42px 38px 30px;
1833
+ display: flex;
1834
+ }
1835
+ ```
1836
+
1837
+ ##### Tips
1838
+ - **Chapter numbers must be `font-weight:700`.** Without bold, the numbers dissolve visually into the lighter chapter title text.
1839
+ - **Last `li` has no `border-bottom`.** Every item except the last carries `border-bottom:1px solid var(--line)`. Remove it from the final entry to avoid a floating rule at the bottom of the list.
1840
+ - **accent-gold vertical rule.** The 3px left rule uses `var(--accent-gold)`. Do not substitute another color — it is the primary editorial accent in Summit.
1841
+ - **`justify-content:space-between` requires a defined height on the parent.** The panel must sit inside a container with a known height (grid cell, absolute position, or `height:100%` chain) or the footer will not pin to the bottom.
1842
+ <!-- @component:toc:end -->
1843
+
1844
+ <!-- @component:quote:start -->
1845
+ #### Quote (.quote-block)
1846
+
1847
+ 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).
1848
+
1849
+ ```html
1850
+ <div class="quote-block">
1851
+ <div class="quote-mark" aria-hidden="true">“</div>
1852
+ <p class="quote-text">The mountains teach us that progress is measured not in speed, but in the ground gained against resistance.</p>
1853
+ <div class="quote-attribution">
1854
+ <div class="quote-avatar">JD</div><!-- or <img src="avatar.jpg" alt="Jane Doe"> -->
1855
+ <div class="quote-meta">
1856
+ <p class="quote-name">Jane Doe</p>
1857
+ <p class="caption">CEO, Acme Corporation</p>
1858
+ </div>
1859
+ </div>
1860
+ </div>
1861
+ ```
1862
+
1863
+ ```css
1864
+ .quote-block {
1865
+ position: relative;
1866
+ padding: 36px 44px 32px;
1867
+ overflow: hidden;
1868
+ }
1869
+
1870
+ .quote-mark {
1871
+ position: absolute;
1872
+ top: -18px;
1873
+ left: 28px;
1874
+ font-family: Baskerville, Georgia, serif;
1875
+ font-size: 140px;
1876
+ font-weight: 700;
1877
+ line-height: 1;
1878
+ color: var(--accent-sage);
1879
+ opacity: 0.42;
1880
+ pointer-events: none;
1881
+ user-select: none;
1882
+ }
1883
+
1884
+ .quote-text {
1885
+ position: relative;
1886
+ font-size: 20px;
1887
+ font-style: italic;
1888
+ line-height: 1.5;
1889
+ color: var(--text-primary);
1890
+ max-width: 860px;
1891
+ padding-top: 48px; /* clears the decorative mark */
1892
+ }
1893
+
1894
+ .quote-attribution {
1895
+ display: flex;
1896
+ align-items: center;
1897
+ gap: 14px;
1898
+ margin-top: 24px;
1899
+ }
1900
+
1901
+ .quote-avatar {
1902
+ width: 48px;
1903
+ height: 48px;
1904
+ border-radius: 50%;
1905
+ background: var(--bg-page-alt);
1906
+ border: 1px solid var(--line-strong);
1907
+ display: flex;
1908
+ align-items: center;
1909
+ justify-content: center;
1910
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
1911
+ font-size: 14px;
1912
+ font-weight: 700;
1913
+ color: var(--text-muted);
1914
+ flex-shrink: 0;
1915
+ overflow: hidden;
1916
+ }
1917
+
1918
+ .quote-avatar img {
1919
+ width: 100%;
1920
+ height: 100%;
1921
+ object-fit: cover;
1922
+ }
1923
+
1924
+ .quote-meta {
1925
+ display: flex;
1926
+ flex-direction: column;
1927
+ gap: 2px;
1928
+ }
1929
+
1930
+ .quote-name {
1931
+ font-size: 14px;
1932
+ font-weight: 600;
1933
+ color: var(--text-primary);
1934
+ line-height: 1.3;
1935
+ }
1936
+ ```
1937
+
1938
+ **Tips:**
1939
+
1940
+ - **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).
1941
+ - **Avatar with photo**: replace `<div class="quote-avatar">JD</div>` with `<div class="quote-avatar"><img src="path/to/photo.jpg" alt="Jane Doe"></div>`. The `overflow: hidden` + `object-fit: cover` handles any image aspect ratio.
1942
+ - **Quote text length**: adjust `font-size` between `18px` (longer quotes, 3+ lines) and `24px` (short punchy quotes, 1 line). Keep `line-height: 1.5`.
1943
+ - **Opacity guidance**: on `--bg-page` (warm paper), `.quote-mark` opacity `0.25` works well. On dark `--bg-frame` backgrounds, reduce to `0.15`.
1944
+ - **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).
1945
+
1946
+ <!-- @component:quote:end -->
1947
+
1948
+ <!-- @component:brand-watermark:start -->
1949
+ #### Brand Watermark
1950
+
1951
+ 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.
1952
+
1953
+ ```html
1954
+ <div class="brand-watermark" aria-hidden="true">
1955
+ <img src="assets/brand-watermark-dark.png" alt="">
1956
+ </div>
1957
+ ```
1958
+
1959
+ ```css
1960
+ .brand-watermark {
1961
+ position: absolute;
1962
+ top: 46px;
1963
+ right: 54px;
1964
+ height: 28px;
1965
+ max-width: 360px;
1966
+ opacity: 0.10;
1967
+ pointer-events: none;
1968
+ z-index: 1;
1969
+ }
1970
+
1971
+ .brand-watermark img {
1972
+ height: 100%;
1973
+ width: auto;
1974
+ display: block;
1975
+ object-fit: contain;
1976
+ }
1977
+
1978
+ .brand-watermark--light {
1979
+ opacity: 0.14;
1980
+ }
1981
+ ```
1982
+
1983
+ Rules:
1984
+ - Use a transparent-background, monochrome image. Prefer a simplified brand mark rather than a full logo lockup.
1985
+ - Default placement is the top-right corner. Keep it inside the paper page, not floating in the outer black frame.
1986
+ - Treat it as decorative. It must remain weaker than the slide title, image, and main narrative content.
1987
+ - Use a dark watermark image on light pages and a light watermark image on dark or fullbleed pages.
1988
+ - Do not pair it with another top-corner brand label, heading, or caption in the same region.
1989
+ - Omit it when the top-right area contains important photography detail or dense content.
1990
+
1991
+ ##### Tips
1992
+ - **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 Summit.
1993
+ - **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.
1994
+ - **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.
1995
+ - **Opacity range.** On light pages, keep opacity around `0.08` to `0.12`. On dark pages, `0.12` to `0.18` is usually enough.
1996
+ - **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.
1997
+ - **Overflow guard.** Keep a `max-width` on the wrapper for unusually long horizontal wordmarks so they do not intrude into the title field.
1998
+ - **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.
1999
+ <!-- @component:brand-watermark:end -->
2000
+
2001
+ <!-- @component:page-number:start -->
2002
+ #### Page Number (.page-number)
2003
+
2004
+ Absolute-positioned slide counter, bottom-right corner. Always present on content slides.
2005
+ Use `.page-number--light` when the slide background is dark (which is most slides in Summit).
2006
+
2007
+ ```html
2008
+ <div class="page-number page-number--light">01 / 12</div>
2009
+ ```
2010
+
2011
+ Omit `--light` only on slides with a white/light background.
2012
+
2013
+ ```css
2014
+ .page-number {
2015
+ position: absolute;
2016
+ bottom: 36px;
2017
+ right: 52px;
2018
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
2019
+ font-size: 11px;
2020
+ font-weight: 700;
2021
+ letter-spacing: 0.18em;
2022
+ color: var(--text-muted);
2023
+ z-index: 10;
2024
+ pointer-events: none;
2025
+ }
2026
+ .page-number--light {
2027
+ color: rgba(247, 244, 238, 0.45);
2028
+ }
2029
+ ```
2030
+
2031
+ <!-- @component:page-number:end -->
2032
+
2033
+ <!-- @component:timeline-journey-horizontal:start -->
2034
+ #### Timeline Journey Horizontal
2035
+
2036
+ 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.
2037
+
2038
+ ```html
2039
+ <div class="tjh">
2040
+ <div class="tjh-axis"></div>
2041
+
2042
+ <!-- UP node: item bottom edge sits on axis, content grows upward.
2043
+ DOM order (top→bottom): label, tip-dot, stem, axis-dot -->
2044
+ <div class="tjh-item tjh-item--up" style="left:7%; --tjh-item-color:var(--accent-earth);">
2045
+ <div class="tjh-label">
2046
+ <span class="tjh-date">Mar 2019</span>
2047
+ <span class="tjh-title">Programme Launch</span>
2048
+ <span class="tjh-text">Cross-regional baseline mapping and legacy risk exposure formally catalogued.</span>
2049
+ </div>
2050
+ <div class="tjh-tip-dot"></div>
2051
+ <div class="tjh-stem"></div>
2052
+ <div class="tjh-axis-dot"></div>
2053
+ </div>
2054
+
2055
+ <!-- DOWN node: item top edge sits on axis, content grows downward.
2056
+ DOM order (top→bottom): axis-dot, stem, tip-dot, label -->
2057
+ <div class="tjh-item tjh-item--down" style="left:21%; --tjh-item-color:var(--accent-gold);">
2058
+ <div class="tjh-axis-dot"></div>
2059
+ <div class="tjh-stem"></div>
2060
+ <div class="tjh-tip-dot"></div>
2061
+ <div class="tjh-label">
2062
+ <span class="tjh-date">Nov 2019</span>
2063
+ <span class="tjh-title">Supplier Audit</span>
2064
+ <span class="tjh-text">Sprint completed across all strategic mills.</span>
2065
+ </div>
2066
+ </div>
2067
+
2068
+ <!-- Add more nodes following the same up/down pattern -->
2069
+ </div>
2070
+ ```
2071
+
2072
+ ```css
2073
+ .tjh {
2074
+ --tjh-node: 12px;
2075
+ --tjh-stem-h: 80px;
2076
+ --tjh-col: calc(100% / 7); /* adjust denominator to match node count */
2077
+
2078
+ position: relative;
2079
+ width: 100%;
2080
+ height: 360px;
2081
+ }
2082
+
2083
+ /* Axis line */
2084
+ .tjh-axis {
2085
+ position: absolute;
2086
+ top: 50%;
2087
+ left: 0;
2088
+ right: 0;
2089
+ height: 1px;
2090
+ background: var(--line-strong);
2091
+ transform: translateY(-50%);
2092
+ }
2093
+
2094
+ /* Item base */
2095
+ .tjh-item {
2096
+ position: absolute;
2097
+ display: flex;
2098
+ flex-direction: column;
2099
+ align-items: center;
2100
+ width: var(--tjh-col);
2101
+ transform: translateX(-50%);
2102
+ }
2103
+
2104
+ /* --up: bottom edge on axis, content grows upward */
2105
+ .tjh-item--up { bottom: 50%; }
2106
+
2107
+ /* --down: top edge on axis, content grows downward */
2108
+ .tjh-item--down { top: 50%; }
2109
+
2110
+ /* Dots */
2111
+ .tjh-axis-dot,
2112
+ .tjh-tip-dot {
2113
+ width: var(--tjh-node);
2114
+ height: var(--tjh-node);
2115
+ border-radius: 50%;
2116
+ background: var(--tjh-item-color);
2117
+ flex-shrink: 0;
2118
+ }
2119
+
2120
+ /* Straddle the axis line */
2121
+ .tjh-item--up .tjh-axis-dot { margin-bottom: calc(-1 * var(--tjh-node) / 2); }
2122
+ .tjh-item--down .tjh-axis-dot { margin-top: calc(-1 * var(--tjh-node) / 2); }
2123
+
2124
+ /* Dashed stem */
2125
+ .tjh-stem {
2126
+ width: 1px;
2127
+ height: var(--tjh-stem-h);
2128
+ background-image: repeating-linear-gradient(
2129
+ to bottom,
2130
+ var(--line-strong) 0px,
2131
+ var(--line-strong) 4px,
2132
+ transparent 4px,
2133
+ transparent 8px
2134
+ );
2135
+ flex-shrink: 0;
2136
+ }
2137
+
2138
+ /* Label */
2139
+ .tjh-label {
2140
+ display: flex;
2141
+ flex-direction: column;
2142
+ gap: 4px;
2143
+ width: 100%;
2144
+ padding: 0 6px;
2145
+ }
2146
+
2147
+ .tjh-item--up .tjh-label { margin-bottom: 6px; }
2148
+ .tjh-item--down .tjh-label { margin-top: 6px; }
2149
+
2150
+ /* Date: inherits node colour via --tjh-item-color */
2151
+ .tjh-date {
2152
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
2153
+ font-size: 11px;
2154
+ font-weight: 700;
2155
+ letter-spacing: 0.16em;
2156
+ text-transform: uppercase;
2157
+ color: var(--tjh-item-color);
2158
+ line-height: 1.3;
2159
+ white-space: nowrap;
2160
+ }
2161
+
2162
+ .tjh-title {
2163
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
2164
+ font-size: 16px;
2165
+ font-weight: 600;
2166
+ letter-spacing: -0.01em;
2167
+ color: var(--text-primary);
2168
+ line-height: 1.15;
2169
+ }
2170
+
2171
+ .tjh-text {
2172
+ font-size: 13px;
2173
+ line-height: 1.5;
2174
+ color: var(--text-secondary);
2175
+ }
2176
+ ```
2177
+
2178
+ Rules:
2179
+ - Position nodes with `left: X%` inline style. For N nodes, space them at `(100/(N+1)) * k %` or manually distribute to reflect time proportions.
2180
+ - Each node requires `--tjh-item-color` set inline (use any summit accent colour).
2181
+ - **`--up` DOM order**: label → tip-dot → stem → axis-dot (label at top, axis-dot at bottom touching axis).
2182
+ - **`--down` DOM order**: axis-dot → stem → tip-dot → label (axis-dot at top touching axis, label at bottom).
2183
+ - Label order within `.tjh-label` is always: date → title → text (top to bottom).
2184
+ - Keep `.tjh-text` short (1–2 lines). The column width (`--tjh-col`) limits wrapping naturally.
2185
+ - Alternate up/down across nodes for visual rhythm. Do not stack multiple up or multiple down nodes consecutively unless intentional.
2186
+ - `--tjh-col` denominator should match the total number of nodes so each item gets equal horizontal space.
2187
+
2188
+ ##### Tips
2189
+ - **Adjust height**: Increase `.tjh { height }` if label text is tall or stems feel cramped.
2190
+ - **Adjust stem length**: Change `--tjh-stem-h` to lengthen or shorten the dashed connector.
2191
+ - **Dark background overrides**: Set `--line-strong: rgba(247,244,238,0.25)` on `.tjh`, override `.tjh-axis { background }`, set `.tjh-title { color: #f7f4ee }`, `.tjh-text { color: rgba(247,244,238,0.7) }`. The `--tjh-item-color` accent colours work on dark backgrounds without change.
2192
+ - **Fewer nodes**: For 4–5 nodes, widen `--tjh-col` by using a smaller denominator (e.g. `calc(100% / 5)`), and space `left` values accordingly.
2193
+ <!-- @component:timeline-journey-horizontal:end -->
2194
+
2195
+ <!-- @component:timeline-journey-vertical:start -->
2196
+ #### Timeline Journey Vertical
2197
+
2198
+ 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.
2199
+
2200
+ 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.
2201
+
2202
+ ```html
2203
+ <div class="tjv">
2204
+ <div class="tjv-axis"></div>
2205
+
2206
+ <!-- LEFT node: DOM order axis-dot → stem → tip-dot → label.
2207
+ flex-direction: row-reverse flips visual order to: label | tip-dot | stem | axis-dot
2208
+ → axis-dot ends up on the right touching the axis; label on the far left, right-aligned. -->
2209
+ <div class="tjv-item tjv-item--left" style="top:14%;--tjv-item-color:var(--accent-earth);">
2210
+ <div class="tjv-axis-dot"></div>
2211
+ <div class="tjv-stem"></div>
2212
+ <div class="tjv-tip-dot"></div>
2213
+ <div class="tjv-label">
2214
+ <span class="tjv-date">Mar 2019</span>
2215
+ <span class="tjv-title">Programme Launch</span>
2216
+ <span class="tjv-text">Cross-regional baseline mapping and legacy risk exposure formally catalogued across all operating units.</span>
2217
+ </div>
2218
+ </div>
2219
+
2220
+ <!-- RIGHT node: DOM order axis-dot → stem → tip-dot → label.
2221
+ flex-direction: row renders as: axis-dot | stem | tip-dot | label
2222
+ → axis-dot on the left touching the axis; label on the far right, left-aligned. -->
2223
+ <div class="tjv-item tjv-item--right" style="top:30%;--tjv-item-color:var(--accent-gold);">
2224
+ <div class="tjv-axis-dot"></div>
2225
+ <div class="tjv-stem"></div>
2226
+ <div class="tjv-tip-dot"></div>
2227
+ <div class="tjv-label">
2228
+ <span class="tjv-date">Nov 2019</span>
2229
+ <span class="tjv-title">Supplier Audit Completed</span>
2230
+ <span class="tjv-text">Full sprint completed across all strategic mills. 94% of Tier 1 suppliers assessed against new emissions criteria.</span>
2231
+ </div>
2232
+ </div>
2233
+
2234
+ <!-- Add more nodes following the same left/right pattern -->
2235
+ </div>
2236
+ ```
2237
+
2238
+ ```css
2239
+ .tjv {
2240
+ --tjv-node: 12px; /* axis-dot diameter */
2241
+ --tjv-stem-w: 80px; /* horizontal dashed stem width */
2242
+
2243
+ position: relative;
2244
+ width: 100%;
2245
+ height: 100%;
2246
+ }
2247
+
2248
+ /* Vertical axis — horizontally centered, full height */
2249
+ .tjv-axis {
2250
+ position: absolute;
2251
+ left: 50%;
2252
+ top: 0;
2253
+ bottom: 0;
2254
+ width: 1px;
2255
+ background: var(--line-strong);
2256
+ transform: translateX(-50%);
2257
+ }
2258
+
2259
+ /* Item base */
2260
+ .tjv-item {
2261
+ position: absolute;
2262
+ display: flex;
2263
+ align-items: center;
2264
+ height: 80px; /* vertical size of the clickable/hover zone */
2265
+ transform: translateY(-50%); /* center the row on the top: Y% point */
2266
+ }
2267
+
2268
+ /* LEFT: row-reverse flips DOM order so axis-dot appears on the right (on the axis) */
2269
+ .tjv-item--left {
2270
+ right: 50%;
2271
+ flex-direction: row-reverse;
2272
+ }
2273
+
2274
+ /* RIGHT: standard row; axis-dot appears on the left (on the axis) */
2275
+ .tjv-item--right {
2276
+ left: 50%;
2277
+ flex-direction: row;
2278
+ }
2279
+
2280
+ /* Axis dot — straddles the axis line */
2281
+ .tjv-axis-dot {
2282
+ width: var(--tjv-node);
2283
+ height: var(--tjv-node);
2284
+ border-radius: 50%;
2285
+ background: var(--tjv-item-color);
2286
+ flex-shrink: 0;
2287
+ position: relative;
2288
+ z-index: 1;
2289
+ }
2290
+
2291
+ /* LEFT: axis-dot is visually rightmost (row-reverse); push right to straddle axis */
2292
+ .tjv-item--left .tjv-axis-dot {
2293
+ margin-right: calc(-1 * var(--tjv-node) / 2);
2294
+ }
2295
+
2296
+ /* RIGHT: axis-dot is visually leftmost; push left to straddle axis */
2297
+ .tjv-item--right .tjv-axis-dot {
2298
+ margin-left: calc(-1 * var(--tjv-node) / 2);
2299
+ }
2300
+
2301
+ /* Tip dot — smaller circle at the stem end near the label */
2302
+ .tjv-tip-dot {
2303
+ width: 8px;
2304
+ height: 8px;
2305
+ border-radius: 50%;
2306
+ background: var(--tjv-item-color);
2307
+ flex-shrink: 0;
2308
+ }
2309
+
2310
+ /* Horizontal dashed stem */
2311
+ .tjv-stem {
2312
+ width: var(--tjv-stem-w);
2313
+ height: 1px;
2314
+ background-image: repeating-linear-gradient(
2315
+ to right,
2316
+ var(--line-strong) 0px,
2317
+ var(--line-strong) 4px,
2318
+ transparent 4px,
2319
+ transparent 8px
2320
+ );
2321
+ flex-shrink: 0;
2322
+ }
2323
+
2324
+ /* Label block */
2325
+ .tjv-label {
2326
+ display: flex;
2327
+ flex-direction: column;
2328
+ gap: 4px;
2329
+ }
2330
+
2331
+ .tjv-item--left .tjv-label {
2332
+ text-align: right;
2333
+ align-items: flex-end;
2334
+ padding-right: 20px;
2335
+ max-width: 560px;
2336
+ }
2337
+
2338
+ .tjv-item--right .tjv-label {
2339
+ text-align: left;
2340
+ align-items: flex-start;
2341
+ padding-left: 20px;
2342
+ max-width: 560px;
2343
+ }
2344
+
2345
+ /* Date — colored per node via --tjv-item-color */
2346
+ .tjv-date {
2347
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
2348
+ font-size: 11px;
2349
+ font-weight: 700;
2350
+ letter-spacing: 0.16em;
2351
+ text-transform: uppercase;
2352
+ color: var(--tjv-item-color);
2353
+ line-height: 1.3;
2354
+ white-space: nowrap;
2355
+ }
2356
+
2357
+ .tjv-title {
2358
+ font-family: 'IBM Plex Sans Condensed', sans-serif;
2359
+ font-size: 18px;
2360
+ font-weight: 600;
2361
+ letter-spacing: -0.01em;
2362
+ color: var(--text-primary);
2363
+ line-height: 1.15;
2364
+ }
2365
+
2366
+ .tjv-text {
2367
+ font-size: 13px;
2368
+ line-height: 1.5;
2369
+ color: var(--text-secondary);
2370
+ max-width: 380px;
2371
+ }
2372
+ ```
2373
+
2374
+ Rules:
2375
+ - **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.
2376
+ - Position each node with `top: Y%` inline style. For N nodes, distribute evenly: `(100 / (N + 1)) * k %` or manually to reflect actual time proportions.
2377
+ - Every node must set `--tjv-item-color` inline (use any Summit accent: `--accent-earth`, `--accent-gold`, `--accent-olive`, `--accent-sage`).
2378
+ - Alternate `--left` and `--right` across nodes for visual rhythm. Do not place consecutive same-side nodes unless intentional.
2379
+ - 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.
2380
+ - Keep `.tjv-text` to 2–4 lines. Longer text shifts the effective visual centre of the item away from the `axis-dot`.
2381
+
2382
+ ##### Tips
2383
+ - **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`.
2384
+ - **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.
2385
+ - **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.
2386
+ - **Standalone full-page use**: set an explicit height on the `.tjv` wrapper (e.g. `height: 720px`) when used outside a height-constrained layout.
2387
+ - **Dark background overrides**: set on the `.tjv` wrapper — `--line-strong: rgba(247,244,238,0.25)` (axis + stem), `.tjv-title { color: #f7f4ee }`, `.tjv-text { color: rgba(247,244,238,0.7) }`. The `--tjv-item-color` accent colours work on dark backgrounds without change.
2388
+ - **Fewer nodes (3–4)**: increase spacing — use `top` values like `15%, 35%, 55%, 75%` to prevent the timeline from clustering at the top.
2389
+ - **More nodes (6–8)**: reduce `.tjv-text` to 1–2 lines and consider reducing `font-size` to `12px` to avoid label collisions.
2390
+ <!-- @component:timeline-journey-vertical:end -->
2391
+
2392
+ <!-- @design:components:end -->