Studio
Build a prompt for any AI model. Pick a format and a look, choose what goes on the slides, then paste the prompt into Claude, ChatGPT or any other model and get one HTML file back.
Format
How the slides are laid out and how you move between them.
The deck is one continuous space and every slide change is a move inside it.
Selected: Focus Stack. About this format
Look
Keep the format's own palette, or override it. The preview updates as you choose.
Theme
Accent
Type
Corners
- Background
- #0a0a0a
- Text
- #ffffff
- Accent
- #f05a1e
- Type
- Inter Tight
- Corners
- 18px
Slide objects
Building blocks the model may use on content slides. Pick the ones your material needs.
Text and layout
Numbers
Structure and time
5 of 30 selected
See every object liveContent
Optional. Blank fields make the prompt ask the model to propose them.
If you list titles, their count sets the number of slides.
Behaviour
What the finished deck must do.
Prompt
50,041 characters, about 13k tokens
# Build an HTML presentation in the "Focus Stack" format
You are a senior front-end designer and engineer. Produce one self-contained HTML file (inline CSS and JavaScript, no frameworks, no build step) that presents the content below as a keyboard-driven slide deck. The format specification is the point of this task: match its layout, motion and navigation exactly instead of improvising your own.
## Deck
- Topic: not given; infer it from the material below, or ask if there is none
- Audience: not specified; write for a smart, busy reader
- Slides: 7 (one cover, 5 content slides, one closing slide)
- Language of all copy: English
- Tone: Direct and confident
## Format: Focus Stack
A vertical wheel of titles; the active one snaps into focus, the rest fall into blur.
Layout
- Full-bleed dark stage (#0a0a0a), no chrome, nothing centered. Two zones: a title stack pinned to the left (7vw from the edge, vertically centered on the viewport) and a content zone on the right half (from 50vw to 94vw, vertically centered).
- Every slide title lives permanently in the stack as one line of bold display type: 4.4vw, weight 700, letter-spacing -0.025em, line-height 1. A short horizontal dash (3.4vw wide, 3px tall, 2px radius) sits 1.6vw to the left of each title; the dash is the pivot.
- The active title is crisp and full contrast (#ffffff) at scale 1 and its dash is the accent (#f05a1e). Every other title is styled purely by its signed distance d from the active one (…, -2, -1, +1, +2, …), with a = |d|:
transform: translateY(d × 15vh) rotate(d × 6deg) scale(1 - min(a,3) × 0.16), transform-origin left center;
filter: blur(min(a,4) × 2.2px); opacity: 1 - a × 0.34; color: dim gray (#5d5d5d), dash the same gray.
Read together the titles form a vertical wheel seen from the side: the title above tilts up to the right, the title below tilts down to the right, and everything else falls off into depth-of-field blur until it disappears.
- Moving one slide rotates the whole wheel one notch: every title animates transform, blur and opacity over 700ms with cubic-bezier(.22,1,.36,1). Nothing else on the page moves during that.
- The active slide's body content then fades and rises into the right zone (opacity 0→1, translateY 30px→0, 600ms, delayed 300ms so the wheel settles first). Content is a column with 1.6rem gaps, starting with a muted lede line. The cover slide leaves the right zone almost empty (one lede line at most); that near-empty frame with the wheel alone is the signature image of this format.
- HUD: slide counter "02 / 07" bottom-right in muted 0.9rem tabular figures; a one-line hint ("Scroll or press ↓ to move") bottom-left that disappears after the first slide.
Navigation: ↓ → Space PageDown = next; ↑ ← PageUp = prev; mouse wheel / trackpad moves one slide per gesture (debounced 750ms); clicking any title in the stack jumps to it; Home/End; number keys 1-9 jump.
## Look
Define these as CSS custom properties on :root and use them everywhere. No other colors except where the format names one.
| Token | Value | Use |
|---|---|---|
| --bg | #0a0a0a | page background |
| --fg | #ffffff | primary text |
| --muted | #7a7a7a | secondary text, counters, labels |
| --accent | #f05a1e | the one highlight color: dashes, active states, chart fills |
| --on-accent | #ffffff | text on accent fills |
| --card | rgba(255,255,255,.05) | surface of boxes and tiles |
| --line | rgba(255,255,255,.12) | hairlines, tracks, borders |
| --radius | 18px | corner radius of boxes |
Typography: Inter Tight. Load it from Google Fonts (https://fonts.googleapis.com/css2?family=Inter+Tight:wght@500;600;700;800&display=swap) and always keep the system fallback so the deck still works offline. Body face: 'Inter Tight', system-ui, sans-serif. Display face: 'Inter Tight', system-ui, sans-serif. Sizes use vw and clamp() so slides scale with the viewport. Line length under 70 characters; body copy under 25 words per block.
## Slide objects
Lay out content slides using only these objects. Each inherits the tokens above. Choose the object that fits each slide's content; do not use every object on every slide, and never more than two objects on one slide.
### Rounded cards
Rounded cards: a responsive grid (auto-fit, minimum 13rem per column, gap clamp(.8rem,1.2vw,1.25rem)) of boxes with 18px radius, a 1px hairline border (rgba(255,255,255,.12)), surface fill (rgba(255,255,255,.05)) and clamp(1rem,1.6vw,1.6rem) padding. Each card is a column: optional small accent kicker (0.8rem, weight 600), a heading (clamp(1rem,1.3vw,1.3rem), weight 650), then muted body text (line-height 1.45). One card per group may be solid accent (#f05a1e) with on-accent text. Use for 3–4 parallel points; never more than 6 cards on a slide; keep each body under 20 words.
### Carousel
Carousel: a horizontal snap-scrolling track of rounded boxes styled like cards (18px radius, hairline border, surface fill, clamp(1.2rem,2vw,2rem) padding, minimum height 11rem, content vertically centered). It shows one item at a time by default (--per:1) or two to three on wide slides (--per:2 / 3, gap 1rem). Under the track, a row with dots on the left (0.5rem circles in the hairline color; the active dot stretches into a 1.4rem accent pill) and two round 2.4rem ← → buttons on the right. Arrow keys move the carousel only while it is focused (click or tab into it); otherwise arrow keys move slides. Touch swipe scrolls it. It resets to the first item when its slide is left. Use for testimonials, examples, a series of screenshots, or one idea per panel.
### Ring charts
Ring charts: one SVG donut per metric, laid out in a row with clamp(1rem,2.5vw,2.5rem) gaps. Each ring is clamp(7rem,11vw,10rem) wide: a 120×120 viewBox, radius 52, 10px stroke; a track in the hairline color and an accent (#f05a1e) arc with round caps, rotated -90° so it starts at 12 o'clock. The arc is drawn from 0 to the value with stroke-dashoffset over 1.2s cubic-bezier(.22,1,.36,1) when the slide becomes active (300ms after), and the number in the center counts up in step (bold, 22% of the ring size, tabular figures, followed by the unit). A muted label (0.9rem, max 12ch, centered) sits under the ring. Markup: <div class="ring" data-value="92" data-label="client retention" data-unit="%"></div>; add data-max for non-percent scales. Two to four rings per slide.
### Stat tiles
Stat tiles: a row (auto-fit, minimum 9rem per tile, 1rem gap) where each tile is a 2px top rule in the hairline color, then a big number (clamp(2.2rem,4vw,4rem), weight 700, letter-spacing -0.03em, tabular figures), then a muted label (0.95rem). The key stat's rule is accent (#f05a1e). Numbers count up from 0 over 1.2s when the slide becomes active: <span class="value count" data-to="38" data-suffix="%"></span>. Two to four tiles; the label says what the number means in plain words, not a heading.
### Bullet reveal
Bullet reveal: an unstyled list (0.7rem gap, clamp(1.05rem,1.5vw,1.5rem) text) where each item is a two-column grid: a 0.55rem accent dot (#f05a1e) and the text. Items carry class "reveal" so they fade and rise 14px one after another, 90ms apart, once the slide is active. Three to five items, each a short verb phrase, no full stops.
## Behavior
- Keyboard: → ↓ Space PageDown next; ← ↑ PageUp previous; Home / End; number keys 1–9 jump to a slide. Ignore keys while an input is focused.
- Mouse wheel and trackpad move one slide per gesture, debounced 750ms; scrolling inside a carousel scrolls the carousel only.
- Keep the slide index in the URL hash (#3) so a slide can be linked and a reload returns to it.
- Show progress the way the format defines it (counter and/or progress line) with two-digit tabular numbers.
- Pressing F toggles fullscreen.
- On touch devices a swipe of at least 50px moves one slide; a swipe inside a carousel scrolls the carousel only.
- Every slide element gets data-state="past" | "active" | "future" plus --d (signed distance from the active slide) and --ad (absolute distance) as inline custom properties; transitions are driven from those in CSS, not from JavaScript timers, except where the format says otherwise.
- Respect prefers-reduced-motion: cut transitions and animations to near zero and skip typing or count-up effects.
- Visible keyboard focus on every control; aria-hidden on inactive slides; clickable things are real <button> elements.
## Content
Propose the 7 slide titles yourself: one idea per slide, two to five words each, sentence case, no punctuation, no numbering inside the title text. The cover states the theme; the closing slide says what happens next.
No raw material was supplied. Write plausible placeholder copy that fits the topic, and mark every number that must be replaced as [xx] so it is obviously a placeholder.
Writing rules: one idea per slide; titles are claims, not labels; a muted lede under the title only when it adds something; no bullet longer than one line; every number gets a unit and a plain-words label; quotes get a name, role and company.
## Technical requirements
- One .html file with inline <style> and <script>. No external scripts or stylesheets other than the Google Fonts link.
- Designed for 16:9. Must look right at 1280×720 and 1920×1080 with nothing scrolling inside a slide, and degrade sensibly at phone width.
- Markup contract: <main class="stage"> holds one <section class="slide" data-title="…"> per slide; objects that should animate in carry class="reveal".
- No lorem ipsum, no emoji, no drop shadows or gradients unless the format names them.
- Valid, accessible HTML: lang attribute, headings in a sensible order, labelled buttons, text contrast of at least 4.5:1.
## Reference implementation
These snippets are the format's own working code. Reuse them as they are, fill in the content and keep the tokens. Do not rewrite the mechanics a different way.
### Format markup, styles and script
```html
<!-- Focus Stack format -->
<style>
:root{--bg:#0a0a0a;--fg:#ffffff;--muted:#7a7a7a;--accent:#f05a1e;--on-accent:#ffffff;--card:rgba(255,255,255,.05);--line:rgba(255,255,255,.12);--radius:18px;--font:'Inter Tight', system-ui, sans-serif;--font-display:'Inter Tight', system-ui, sans-serif}
:root{--dim:#5d5d5d;--gap:15vh;--tilt:6deg;--ease:cubic-bezier(.22,1,.36,1)}
*{box-sizing:border-box}html,body{height:100%;margin:0}
body{background:var(--bg);color:var(--fg);font-family:var(--font);overflow:hidden}
.stack{position:fixed;left:7vw;top:50%;z-index:2}
.stack-item{position:absolute;left:0;top:0;display:flex;align-items:center;gap:1.6vw;white-space:nowrap;margin:0;padding:0;border:0;background:none;cursor:pointer;font:inherit;color:var(--dim);font-size:4.4vw;font-weight:700;letter-spacing:-.025em;line-height:1;transform-origin:0 50%;
transform:translateY(calc(var(--d)*var(--gap))) translateY(-50%) rotate(calc(var(--d)*var(--tilt))) scale(calc(1 - min(var(--ad),3)*.16));
filter:blur(calc(min(var(--ad),4)*2.2px));opacity:calc(1 - var(--ad)*.34);
transition:transform .7s var(--ease),filter .7s var(--ease),opacity .7s var(--ease),color .5s}
.stack-item .dash{flex:none;width:3.4vw;height:max(3px,.22vw);border-radius:2px;background:var(--dim);transition:background .5s}
.stack-item.active{color:var(--fg)}.stack-item.active .dash{background:var(--accent)}
.stage{position:fixed;top:0;bottom:0;left:50vw;right:6vw;display:grid;align-items:center}
.slide{grid-area:1/1;display:flex;flex-direction:column;gap:1.6rem;pointer-events:none;opacity:0;transform:translateY(30px);transition:opacity .6s ease,transform .6s var(--ease)}
.slide[data-state="active"]{opacity:1;transform:none;pointer-events:auto;transition-delay:.3s}
.lede{font-size:clamp(1.05rem,1.4vw,1.4rem);color:var(--muted);max-width:34em;line-height:1.5}
.hud{position:fixed;right:3vw;bottom:3.5vh;color:var(--muted);font-size:.9rem;font-variant-numeric:tabular-nums}
.hint{position:fixed;left:7vw;bottom:3.5vh;color:var(--muted);font-size:.85rem;transition:opacity .8s}
html:not([data-slide="0"]) .hint{opacity:0}
</style>
<body data-wheel>
<nav class="stack" aria-label="Slides"></nav>
<main class="stage">
<section class="slide" data-title="One step ahead"><p class="lede">One line, or nothing.</p></section>
<section class="slide" data-title="Broke the mold"><p class="lede reveal">Lede line.</p><!-- objects with class="reveal" --></section>
</main>
<div class="hud"><span data-idx>01</span> / <span data-total>07</span></div>
<div class="hint">Scroll or press ↓ to move</div>
<script>
// Build the wheel from the slides' titles and keep each item's distance in sync with the engine.
const stack = document.querySelector('.stack');
document.querySelectorAll('.slide').forEach((s, i) => { const b = document.createElement('button'); b.className = 'stack-item'; b.type = 'button'; b.dataset.goto = i; b.innerHTML = '<span class="dash"></span><span class="label">' + s.dataset.title + '</span>'; stack.append(b); });
document.addEventListener('deck:change', e => stack.querySelectorAll('.stack-item').forEach((b, i) => { const d = i - e.detail.index; b.style.setProperty('--d', d); b.style.setProperty('--ad', Math.abs(d)); }));
</script>
```
### Object styles
```css
.slide h1,.slide h2{font-family:var(--font-display,var(--font));letter-spacing:-.02em;line-height:1.05;margin:0}.slide p{margin:0}
.lede{font-size:clamp(1.05rem,1.4vw,1.4rem);color:var(--muted);max-width:34em;line-height:1.5}
.kicker{font-size:.95rem;color:var(--accent);font-weight:600}
.reveal{opacity:0;transform:translateY(14px);transition:opacity .55s ease,transform .55s cubic-bezier(.22,1,.36,1)}.reveal.in{opacity:1;transform:none}
@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition-duration:.01ms!important;animation-duration:.01ms!important;animation-delay:0ms!important;transition-delay:0ms!important}}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,13rem),1fr));gap:clamp(.8rem,1.2vw,1.25rem)}
.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:clamp(1rem,1.6vw,1.6rem);display:flex;flex-direction:column;gap:.5rem;min-height:8rem}
.card h3{margin:0;font-size:clamp(1rem,1.3vw,1.3rem);font-weight:650;letter-spacing:-.01em}.card p{color:var(--muted);line-height:1.45;font-size:clamp(.9rem,1.05vw,1.05rem)}.card .kicker{font-size:.8rem}
.card.accent{background:var(--accent);color:var(--on-accent);border-color:transparent}.card.accent p,.card.accent .kicker{color:inherit;opacity:.85}
.carousel{position:relative;--per:1;--gap:1rem;outline:none}.carousel:focus-visible{box-shadow:0 0 0 3px color-mix(in oklab,var(--accent) 40%,transparent);border-radius:var(--radius)}
.carousel-track{display:flex;gap:var(--gap);overflow-x:auto;scroll-snap-type:x mandatory;scrollbar-width:none;padding:2px}.carousel-track::-webkit-scrollbar{display:none}
.carousel-item{flex:0 0 calc((100% - (var(--per) - 1)*var(--gap))/var(--per));scroll-snap-align:start;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:clamp(1.2rem,2vw,2rem);min-height:11rem;display:flex;flex-direction:column;justify-content:center;gap:.6rem}
.carousel-ui{display:flex;align-items:center;justify-content:space-between;margin-top:1rem}.carousel-dots{display:flex;gap:.4rem}
.carousel-dots button{width:.5rem;height:.5rem;border-radius:999px;border:0;background:var(--line);padding:0;cursor:pointer;transition:.3s}.carousel-dots button.active{background:var(--accent);width:1.4rem}
.carousel-btns{display:flex;gap:.5rem}.carousel-btns button{width:2.4rem;height:2.4rem;border-radius:50%;border:1px solid var(--line);background:transparent;color:var(--fg);cursor:pointer;font-size:1.05rem;display:grid;place-items:center;font-family:inherit;transition:.2s}.carousel-btns button:hover{border-color:var(--accent);color:var(--accent)}
.rings{display:flex;flex-wrap:wrap;gap:clamp(1rem,2.5vw,2.5rem)}
.ring{--size:clamp(7rem,11vw,10rem);position:relative;width:var(--size);display:flex;flex-direction:column;align-items:center;gap:.6rem;text-align:center}
.ring svg{width:var(--size);height:var(--size);transform:rotate(-90deg);overflow:visible}.ring .track{fill:none;stroke:var(--line);stroke-width:10}
.ring .fill{fill:none;stroke:var(--accent);stroke-width:10;stroke-linecap:round;transition:stroke-dashoffset 1.2s cubic-bezier(.22,1,.36,1)}
.ring .center{position:absolute;top:calc(var(--size)/2);left:50%;transform:translate(-50%,-50%);font-family:var(--font-display,var(--font));font-weight:700;font-size:calc(var(--size)*.22);letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.ring .label{color:var(--muted);font-size:.9rem;max-width:12ch;line-height:1.3}
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,9rem),1fr));gap:1rem}
.stat{display:flex;flex-direction:column;gap:.3rem;padding-top:1rem;border-top:2px solid var(--line)}
.stat .value{font-family:var(--font-display,var(--font));font-size:clamp(2.2rem,4vw,4rem);font-weight:700;letter-spacing:-.03em;line-height:1;font-variant-numeric:tabular-nums}
.stat .label{color:var(--muted);font-size:.95rem;line-height:1.3}.stat.accent{border-top-color:var(--accent)}
.bullets{list-style:none;margin:0;padding:0;display:grid;gap:.7rem;font-size:clamp(1.05rem,1.5vw,1.5rem)}.bullets li{display:grid;grid-template-columns:1.4rem 1fr;gap:.4rem;align-items:baseline}
.bullets li::before{content:"";width:.55rem;height:.55rem;border-radius:50%;background:var(--accent);display:inline-block;transform:translateY(-.15em)}
```
### Object markup
```html
<!-- Rounded cards -->
<div class="cards">
<div class="card reveal"><span class="kicker">Roster</span><h3>Fewer, longer projects</h3><p>Body copy under twenty words.</p></div>
<div class="card accent reveal"><span class="kicker">Time</span><h3>Fridays off the clock</h3><p>The one you want remembered.</p></div>
</div>
<!-- Carousel -->
<div class="carousel reveal" aria-label="Client quotes" style="--per:1">
<div class="carousel-track">
<div class="carousel-item quote"><blockquote>“They asked better questions than our own team did.”</blockquote><cite><b>Nadia R.</b> · Head of Product, Kite</cite></div>
<div class="carousel-item quote"><blockquote>“…”</blockquote><cite><b>Name</b> · Role, Company</cite></div>
</div>
</div>
<!-- Ring charts -->
<div class="rings reveal">
<div class="ring" data-value="92" data-label="client retention"></div>
<div class="ring" data-value="71" data-label="revenue from returning clients"></div>
<div class="ring" data-value="38" data-label="shorter delivery time"></div>
</div>
<!-- Stat tiles -->
<div class="stats reveal">
<div class="stat accent"><span class="value count" data-to="6"></span><span class="label">projects shipped</span></div>
<div class="stat"><span class="value count" data-to="38" data-suffix="%"></span><span class="label">shorter delivery time</span></div>
</div>
<!-- Bullet reveal -->
<ul class="bullets">
<li class="reveal">Open a second room in Lisbon</li>
<li class="reveal">Publish the process as a playbook</li>
</ul>
```
### Object runtime (load before the deck engine)
```html
<script>
// Slide objects runtime, builds ring charts, bars and carousels, and animates them when their slide becomes active. Load before the deck engine.
(() => {
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches, animateDelay = +document.body.dataset.animateDelay || 300;
document.querySelectorAll('.ring').forEach(el => {
const r = 52, C = 2 * Math.PI * r, unit = el.dataset.unit ?? '%';
el.innerHTML = '<svg viewBox="0 0 120 120" aria-hidden="true"><circle class="track" cx="60" cy="60" r="' + r + '"/><circle class="fill" cx="60" cy="60" r="' + r + '" stroke-dasharray="' + C + '" stroke-dashoffset="' + C + '"/></svg><div class="center"><span class="num">0</span>' + unit + '</div><div class="label">' + (el.dataset.label || '') + '</div>';
el._target = +el.dataset.value || 0; el._max = +el.dataset.max || 100; el._C = C;
});
document.querySelectorAll('.bar').forEach(el => {
const unit = el.dataset.unit ?? '%';
el.innerHTML = '<span class="name">' + (el.dataset.label || '') + '</span><span class="track"><span class="fill"></span></span><span class="num">' + (el.dataset.value || 0) + unit + '</span>';
el._target = +el.dataset.value || 0; el._max = +el.dataset.max || 100;
});
document.querySelectorAll('.carousel').forEach(car => {
const track = car.querySelector('.carousel-track'), items = [...track.children];
const ui = document.createElement('div'); ui.className = 'carousel-ui';
const dots = document.createElement('div'); dots.className = 'carousel-dots';
const btns = document.createElement('div'); btns.className = 'carousel-btns';
const mk = (label, html, fn) => { const b = document.createElement('button'); b.type = 'button'; b.setAttribute('aria-label', label); b.innerHTML = html; b.addEventListener('click', e => { e.stopPropagation(); fn(); }); return b; };
const step = () => items[0].getBoundingClientRect().width + parseFloat(getComputedStyle(track).columnGap || 0);
const idx = () => Math.round(track.scrollLeft / step());
const goTo = i => { i = (i + items.length) % items.length; track.scrollTo({ left: i * step(), behavior: reduced ? 'auto' : 'smooth' }); };
items.forEach((_, i) => dots.append(mk('Item ' + (i + 1), '', () => goTo(i))));
btns.append(mk('Previous', '←', () => goTo(idx() - 1)), mk('Next', '→', () => goTo(idx() + 1)));
ui.append(dots, btns); car.append(ui); car.tabIndex = 0;
const paint = () => { const i = idx(); dots.querySelectorAll('button').forEach((b, k) => b.classList.toggle('active', k === i)); };
track.addEventListener('scroll', () => requestAnimationFrame(paint), { passive: true }); paint();
car.addEventListener('keydown', e => { if (e.key === 'ArrowLeft') { goTo(idx() - 1); e.preventDefault(); e.stopPropagation(); } if (e.key === 'ArrowRight') { goTo(idx() + 1); e.preventDefault(); e.stopPropagation(); } });
car._reset = () => track.scrollTo({ left: 0, behavior: 'auto' });
});
const ease = t => 1 - Math.pow(1 - t, 3);
function countTo(el, to, dur, pre, suf) {
pre = pre || ''; suf = suf || '';
if (reduced || dur === 0) { el.textContent = pre + to + suf; return; }
const from = parseFloat(el.textContent) || 0, t0 = performance.now(), dec = String(to).includes('.') ? String(to).split('.')[1].length : 0;
cancelAnimationFrame(el._raf);
const tick = now => { const p = Math.max(0, Math.min(1, (now - t0) / dur)); el.textContent = pre + (from + (to - from) * ease(p)).toFixed(dec) + suf; if (p < 1) el._raf = requestAnimationFrame(tick); };
el._raf = requestAnimationFrame(tick);
}
function animate(slide, on) {
slide.querySelectorAll('.ring').forEach(el => { el.querySelector('.fill').style.strokeDashoffset = on ? el._C * (1 - el._target / el._max) : el._C; countTo(el.querySelector('.num'), on ? el._target : 0, on ? 1200 : 0); });
slide.querySelectorAll('.bar').forEach(el => { el.querySelector('.fill').style.width = on ? (el._target / el._max * 100) + '%' : '0'; });
slide.querySelectorAll('.count').forEach(el => countTo(el, on ? +el.dataset.to : 0, on ? 1200 : 0, el.dataset.prefix, el.dataset.suffix));
if (!on) slide.querySelectorAll('.carousel').forEach(c => c._reset && c._reset());
}
document.addEventListener('deck:change', e => document.querySelectorAll('.slide').forEach(s => { if (s === e.detail.slide) setTimeout(() => animate(s, true), animateDelay); else animate(s, false); }));
})();
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
/* ======================================================================
infographics, builds the drawn objects (journey, arc, funnel, columns,
line chart, waffle, matrix, hub, cycle, slope, roadmap, diagram cards)
and adds class "on" to them when their slide becomes active, which is
what every transition in components.css keys off. Re-measures on resize.
====================================================================== */
(() => {
const NS = 'http://www.w3.org/2000/svg';
const animateDelay = +document.body.dataset.animateDelay || 300;
const DRAWN = '.journey, .arc, .diagram, .funnel, .columns, .linechart, .waffle, .matrix, .hub, .cycle, .slope, .roadmap';
const el = (tag, attrs) => { const n = document.createElementNS(NS, tag); for (const k in attrs) n.setAttribute(k, attrs[k]); return n; };
const svg = (host, w, h) => { const s = el('svg', { viewBox: '0 0 ' + w + ' ' + h, 'aria-hidden': 'true' }); host.prepend(s); return s; };
const nums = s => (s || '').split(',').map(v => parseFloat(v.trim())).filter(v => !isNaN(v));
const list = s => (s || '').split(/\s*[,|]\s*/).filter(Boolean);
const fmt = v => v >= 10000 ? v.toLocaleString('en-US') : String(v);
const idx = (parent, sel) => parent.querySelectorAll(sel).forEach((c, i) => c.style.setProperty('--i', i));
/* untransformed offset of el inside host, safe on slides a format has scaled or rotated */
const rel = (node, host) => {
let x = 0, y = 0, n = node;
while (n && n !== host) { x += n.offsetLeft; y += n.offsetTop; n = n.offsetParent; }
return { x, y, w: node.offsetWidth, h: node.offsetHeight, cx: x + node.offsetWidth / 2, cy: y + node.offsetHeight / 2 };
};
/* polyline through points with rounded corners */
const rounded = (p, r) => {
let d = 'M' + p[0].x + ' ' + p[0].y;
for (let i = 1; i < p.length - 1; i++) {
const a = p[i - 1], b = p[i], c = p[i + 1];
const l1 = Math.hypot(b.x - a.x, b.y - a.y), l2 = Math.hypot(c.x - b.x, c.y - b.y);
const k = Math.min(r, l1 / 2, l2 / 2);
d += ' L' + (b.x - (b.x - a.x) / l1 * k) + ' ' + (b.y - (b.y - a.y) / l1 * k);
d += ' Q' + b.x + ' ' + b.y + ' ' + (b.x + (c.x - b.x) / l2 * k) + ' ' + (b.y + (c.y - b.y) / l2 * k);
}
const z = p[p.length - 1];
return d + ' L' + z.x + ' ' + z.y;
};
const draw = n => { n.setAttribute('pathLength', '1'); return n; };
/* ---- build (markup only, runs once) ------------------------------ */
document.querySelectorAll('.columns').forEach(c => {
const cols = [...c.querySelectorAll('.column')];
const max = +c.dataset.max || Math.max(...cols.map(x => +x.dataset.value || 0));
const unit = c.dataset.unit ?? '';
cols.forEach((col, i) => {
const v = +col.dataset.value || 0;
col.style.setProperty('--i', i);
col.style.setProperty('--h', max ? v / max : 0);
col.innerHTML = '<span class="num">' + fmt(v) + unit + '</span><span class="pillar"></span><span class="lab">' + (col.dataset.label || '') + '</span>';
});
});
document.querySelectorAll('.funnel').forEach(f => {
const tiers = [...f.querySelectorAll('.tier')];
const max = +f.dataset.max || Math.max(...tiers.map(t => +t.dataset.value || 0));
const unit = f.dataset.unit ?? '';
tiers.forEach((t, i) => {
const v = +t.dataset.value || 0, prev = i ? +tiers[i - 1].dataset.value || 0 : 0;
const rate = t.dataset.rate ?? (i && prev ? Math.round(v / prev * 100) + '% of previous' : '');
t.style.setProperty('--i', i);
t.innerHTML = '<span class="name">' + (t.dataset.label || '') + '</span>'
+ '<span class="band"><span class="fill" style="--w:' + Math.max(16, max ? v / max * 100 : 0).toFixed(1) + '%"></span></span>'
+ '<span class="num">' + fmt(v) + unit + (rate ? '<span class="rate">' + rate + '</span>' : '') + '</span>';
});
});
document.querySelectorAll('.waffle').forEach(w => {
const total = +w.dataset.dots || 100, on = Math.round(+w.dataset.value || 0), cols = +w.dataset.cols || 10;
const cells = el('div', {}); // placeholder, replaced below
const box = document.createElement('div'); box.className = 'cells'; box.style.setProperty('--cols', cols);
for (let i = 0; i < total; i++) {
const c = document.createElement('span');
c.className = 'cell' + (i < on ? ' f' : '');
c.style.setProperty('--i', i);
box.append(c);
}
const side = document.createElement('div');
side.innerHTML = '<span class="value count" data-to="' + on + '" data-suffix="' + (w.dataset.unit ?? '') + '"></span>'
+ '<span class="label">' + (w.dataset.label || '') + '</span>';
w.setAttribute('role', 'img');
w.setAttribute('aria-label', on + ' of ' + total + ' ' + (w.dataset.label || ''));
w.append(box, side);
void cells;
});
document.querySelectorAll('.roadmap').forEach(r => {
const cols = list(r.dataset.cols), rows = [...r.querySelectorAll('.row')];
r.style.setProperty('--cols', cols.length || 4);
const head = document.createElement('div'); head.className = 'rhead';
head.innerHTML = cols.map(c => '<span>' + c + '</span>').join('');
r.prepend(head);
rows.forEach((row, i) => {
const name = document.createElement('div'); name.className = 'name'; name.textContent = row.dataset.label || '';
const lanes = document.createElement('div'); lanes.className = 'lanes';
const lane = document.createElement('span');
lane.className = 'lane' + (row.classList.contains('accent') ? ' accent' : '') + (row.dataset.start === row.dataset.end && row.classList.contains('milestone') ? ' milestone' : '');
lane.style.setProperty('--s', +row.dataset.start || 1);
lane.style.setProperty('--e', +row.dataset.end || +row.dataset.start || 1);
lane.textContent = row.dataset.note || '';
lanes.append(lane);
lanes.style.setProperty('--i', i);
lane.style.setProperty('--i', i);
row.replaceWith(name, lanes);
});
if (r.dataset.now) { const n = document.createElement('div'); n.className = 'now'; n.style.setProperty('--at', r.dataset.now); r.append(n); }
});
document.querySelectorAll('.matrix').forEach(m => {
const q = list(m.dataset.quadrants), hot = +m.dataset.hot || 0;
const plane = document.createElement('div'); plane.className = 'plane';
q.slice(0, 4).forEach((t, i) => {
const s = document.createElement('span');
s.className = 'q' + (hot === i + 1 ? ' hot' : '');
s.textContent = t;
plane.append(s);
});
[...m.querySelectorAll('.pt')].forEach((p, i) => {
p.style.left = (+p.dataset.x || 0) + '%';
p.style.bottom = (+p.dataset.y || 0) + '%';
p.style.setProperty('--i', i);
if ((+p.dataset.x || 0) > 64) p.classList.add('flip');
plane.append(p);
});
const y = document.createElement('span'); y.className = 'ylab'; y.textContent = m.dataset.y || '';
const x = document.createElement('span'); x.className = 'xlab'; x.textContent = m.dataset.x || '';
const pad = document.createElement('span');
m.innerHTML = ''; m.append(y, plane, pad, x);
});
document.querySelectorAll('.diagram').forEach(d => {
idx(d, '.dcard');
d.querySelectorAll('.art :is(path,line,circle,ellipse,rect,polyline,polygon)').forEach(s => { if (!s.classList.contains('af')) draw(s); });
});
document.querySelectorAll('.hub').forEach(h => {
const core = document.createElement('div'); core.className = 'core'; core.textContent = h.dataset.core || '';
h.append(core);
idx(h, '.spoke');
});
document.querySelectorAll('.cycle').forEach(c => {
[...c.querySelectorAll('.node')].forEach((n, i) => {
n.style.setProperty('--i', i);
const text = document.createElement('div'); text.className = 'text';
while (n.firstChild) text.append(n.firstChild);
const badge = document.createElement('span'); badge.className = 'idx'; badge.textContent = i + 1;
n.append(badge, text);
});
if (c.dataset.core) { const k = document.createElement('div'); k.className = 'core'; k.textContent = c.dataset.core; c.append(k); }
});
document.querySelectorAll('.journey').forEach(j => {
const stops = [...j.querySelectorAll('.stop')];
j.style.setProperty('--cols', +j.dataset.cols || 3);
let step = j.classList.contains('zero') ? 0 : 1;
stops.forEach((s, i) => {
s.style.setProperty('--i', i);
const m = document.createElement('span');
m.className = 'marker';
m.innerHTML = s.classList.contains('mark')
? '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 0l2.6 7.2L22 5l-4 6 4 6-7.4-2.2L12 22l-2.6-7.2L2 17l4-6-4-6 7.4 2.2z"/></svg>'
: String(step++);
s.prepend(m);
});
});
document.querySelectorAll('.slope').forEach(s => {
const rows = [...s.querySelectorAll('.row')], suf = s.dataset.unit ?? '';
const all = rows.flatMap(r => [+r.dataset.from || 0, +r.dataset.to || 0]);
const lo = Math.min(...all), hi = Math.max(...all), span = hi - lo || 1;
s._rows = rows.map((r, i) => {
const a = +r.dataset.from || 0, b = +r.dataset.to || 0, up = b > a;
const la = document.createElement('span'), lb = document.createElement('span');
la.className = 'lbl a' + (up ? ' up' : ''); lb.className = 'lbl b' + (up ? ' up' : '');
la.innerHTML = '<span>' + (r.dataset.label || '') + '</span><b>' + a + suf + '</b>';
lb.innerHTML = '<b>' + b + suf + '</b><span>' + (r.dataset.label || '') + '</span>';
[la, lb].forEach(l => l.style.setProperty('--i', i));
s.append(la, lb);
r.remove();
return { a: 1 - (a - lo) / span, b: 1 - (b - lo) / span, up, i, la, lb };
});
const ax = document.createElement('div'); ax.className = 'axis a'; ax.innerHTML = '<b>' + (s.dataset.a || '') + '</b>';
const bx = document.createElement('div'); bx.className = 'axis b'; bx.innerHTML = '<b>' + (s.dataset.b || '') + '</b>';
s.append(ax, bx);
});
/* ---- measure and paint (runs on load and on resize) -------------- */
function paint() {
document.querySelectorAll('.journey').forEach(j => {
const stops = [...j.querySelectorAll('.stop')];
/* the stylesheet owns the column count, a media query may have overridden data-cols */
const cols = Math.max(1, getComputedStyle(j).gridTemplateColumns.split(/\s+/).filter(Boolean).length);
stops.forEach((stop, i) => {
const row = Math.floor(i / cols), col = i % cols;
stop.style.gridRow = row + 1;
stop.style.gridColumn = (row % 2 ? cols - col : col + 1);
stop.classList.toggle('flip', row % 2 === 1 && cols > 1);
});
const w = j.offsetWidth, h = j.offsetHeight;
if (!w || !h) return;
j.querySelector(':scope > svg')?.remove();
const s = svg(j, w, h);
const pts = [];
stops.forEach((stop, i) => {
const m = rel(stop.querySelector('.marker'), j);
const row = Math.floor(i / cols);
if (i && row !== Math.floor((i - 1) / cols)) {
const p = pts[pts.length - 1];
const turn = row % 2 ? w - 8 : 8;
pts.push({ x: turn, y: p.y }, { x: turn, y: m.cy });
}
pts.push({ x: m.cx, y: m.cy });
});
if (pts.length > 1) s.append(draw(el('path', { d: rounded(pts, 26) })));
});
document.querySelectorAll('.arc').forEach(a => {
const side = a.querySelector('.side'), items = [...a.querySelectorAll('.item')];
if (!side || !items.length) return;
const w = side.offsetWidth, h = side.offsetHeight;
if (!w || !h) return;
side.querySelector('svg')?.remove();
const s = svg(side, w, h);
const oy = rel(side, a).y; /* item centres live in .arc space; shift them into .side space */
const yOf = it => rel(it, a).cy - oy;
const cy = h / 2, R = h * .62, cx = w - R;
const xAt = y => cx + Math.sqrt(Math.max(0, R * R - (y - cy) * (y - cy)));
const top = yOf(items[0]), bot = yOf(items[items.length - 1]);
let d = '';
for (let y = top; y <= bot; y += 4) d += (d ? 'L' : 'M') + xAt(y).toFixed(1) + ' ' + y.toFixed(1);
d += 'L' + xAt(bot).toFixed(1) + ' ' + bot.toFixed(1);
s.append(draw(el('path', { class: 'curve', d })));
const dr = Math.min(w * .42, h * .32), dx = w * .34; /* thick half-ring behind the heading */
s.append(draw(el('path', {
class: 'dial', 'stroke-width': dr * .52,
d: 'M' + dx + ' ' + (cy - dr) + ' A' + dr + ' ' + dr + ' 0 0 0 ' + dx + ' ' + (cy + dr)
})));
items.forEach((it, i) => {
it.style.setProperty('--i', i);
const y = yOf(it), x = xAt(y);
const tick = draw(el('line', { class: 'tick', x1: x, y1: y, x2: w, y2: y })); tick.style.setProperty('--i', i);
const node = el('circle', { class: 'node', cx: x, cy: y, r: 6 }); node.style.setProperty('--i', i);
s.append(tick, node);
if (!it.querySelector('.idx')) { const b = document.createElement('span'); b.className = 'idx'; b.textContent = String(i + 1).padStart(2, '0'); it.prepend(b); }
});
});
document.querySelectorAll('.hub').forEach(h => {
const w = h.offsetWidth, ht = h.offsetHeight, spokes = [...h.querySelectorAll('.spoke')];
if (!w || !ht || !spokes.length) return;
h.querySelector(':scope > svg')?.remove();
const s = svg(h, w, ht);
const cx = w / 2, cy = ht / 2, rx = w * .34, ry = ht * .36;
const core = h.querySelector('.core'), cr = (core?.offsetWidth || 90) / 2;
spokes.forEach((sp, i) => {
const t = -Math.PI / 2 + i / spokes.length * Math.PI * 2;
const x = cx + Math.cos(t) * rx, y = cy + Math.sin(t) * ry;
sp.style.left = x + 'px'; sp.style.top = y + 'px';
const line = draw(el('line', { x1: cx + Math.cos(t) * (cr + 4), y1: cy + Math.sin(t) * (cr + 4), x2: x, y2: y }));
line.style.setProperty('--i', i);
s.append(line);
});
});
document.querySelectorAll('.cycle').forEach(c => {
const w = c.offsetWidth, h = c.offsetHeight, nodes = [...c.querySelectorAll('.node')];
if (!w || !h || !nodes.length) return;
c.querySelector(':scope > svg')?.remove();
const s = svg(c, w, h);
const cx = w / 2, cy = h / 2, rx = Math.min(w * .3, 260), ry = Math.min(h * .34, 220);
const at = i => { const t = -Math.PI / 2 + i / nodes.length * Math.PI * 2; return { t, x: cx + Math.cos(t) * rx, y: cy + Math.sin(t) * ry }; };
nodes.forEach((n, i) => {
const p = at(i);
n.style.left = p.x + 'px'; n.style.top = p.y + 'px';
const text = n.querySelector('.text'), right = Math.cos(p.t) > -.2;
text.style.left = right ? '1.8rem' : 'auto';
text.style.right = right ? 'auto' : '1.8rem';
text.style.top = '50%';
text.style.transform = 'translateY(-50%)';
text.style.textAlign = right ? 'left' : 'right';
});
const pad = .16 * Math.PI * 2 / nodes.length;
nodes.forEach((n, i) => {
const a = at(i), b = at((i + 1) % nodes.length);
const t0 = a.t + pad, t1 = (i + 1 === nodes.length ? b.t + Math.PI * 2 : b.t) - pad;
const p0 = { x: cx + Math.cos(t0) * rx, y: cy + Math.sin(t0) * ry };
const p1 = { x: cx + Math.cos(t1) * rx, y: cy + Math.sin(t1) * ry };
const seg = draw(el('path', { class: 'seg', d: 'M' + p0.x + ' ' + p0.y + ' A' + rx + ' ' + ry + ' 0 0 1 ' + p1.x + ' ' + p1.y }));
const dir = t1 + Math.PI / 2, k = 9;
const head = draw(el('path', {
class: 'head',
d: 'M' + (p1.x - Math.cos(dir - .45) * k) + ' ' + (p1.y - Math.sin(dir - .45) * k)
+ 'L' + p1.x + ' ' + p1.y
+ 'L' + (p1.x - Math.cos(dir + .45) * k) + ' ' + (p1.y - Math.sin(dir + .45) * k)
}));
[seg, head].forEach(x => x.style.setProperty('--i', i));
s.append(seg, head);
});
});
document.querySelectorAll('.slope').forEach(sl => {
const w = sl.offsetWidth, h = sl.offsetHeight;
if (!w || !h || !sl._rows) return;
sl.querySelector(':scope > svg')?.remove();
const s = svg(sl, w, h);
const xa = w * .3, xb = w * .7, top = 10, span = h - 20;
sl._rows.forEach(r => {
const ya = top + r.a * span, yb = top + r.b * span;
const line = draw(el('line', { class: r.up ? 'up' : '', x1: xa, y1: ya, x2: xb, y2: yb }));
const da = el('circle', { class: 'dot' + (r.up ? ' up' : ''), cx: xa, cy: ya, r: 5 });
const db = el('circle', { class: 'dot' + (r.up ? ' up' : ''), cx: xb, cy: yb, r: 5 });
[line, da, db].forEach(x => x.style.setProperty('--i', r.i));
s.append(line, da, db);
r.la.style.top = ya + 'px';
r.lb.style.top = yb + 'px';
});
});
document.querySelectorAll('.linechart').forEach(lc => {
const v = nums(lc.dataset.values);
if (v.length < 2) return;
let plot = lc.querySelector('.plot');
if (!plot) {
plot = document.createElement('div'); plot.className = 'plot';
const xs = document.createElement('div'); xs.className = 'xlabels';
xs.innerHTML = list(lc.dataset.labels).map(l => '<span>' + l + '</span>').join('');
lc.append(plot, xs);
}
const w = plot.offsetWidth, h = plot.offsetHeight;
if (!w || !h) return;
plot.querySelector('svg')?.remove();
const s = svg(plot, w, h);
const lo = lc.dataset.min !== undefined ? +lc.dataset.min : Math.min(...v, 0);
const hi = lc.dataset.max !== undefined ? +lc.dataset.max : Math.max(...v);
const span = hi - lo || 1, pad = 8;
const pt = i => ({ x: i / (v.length - 1) * w, y: pad + (1 - (v[i] - lo) / span) * (h - pad * 2) });
for (let g = 0; g <= 3; g++) s.append(el('line', { class: 'grid', x1: 0, x2: w, y1: pad + g / 3 * (h - pad * 2), y2: pad + g / 3 * (h - pad * 2) }));
const d = v.map((_, i) => (i ? 'L' : 'M') + pt(i).x.toFixed(1) + ' ' + pt(i).y.toFixed(1)).join('');
s.append(el('path', { class: 'area', d: d + 'L' + w + ' ' + h + 'L0 ' + h + 'Z' }));
s.append(draw(el('path', { class: 'curve', d })));
const last = pt(v.length - 1);
s.append(el('circle', { class: 'end', cx: last.x, cy: last.y, r: 5.5 }));
let lab = plot.querySelector('.endlabel');
if (!lab) { lab = document.createElement('span'); lab.className = 'endlabel'; plot.append(lab); }
lab.textContent = fmt(v[v.length - 1]) + (lc.dataset.unit ?? '');
lab.style.left = last.x + 'px'; lab.style.top = last.y + 'px';
});
}
/* ---- switch on with the slide ------------------------------------ */
document.addEventListener('deck:change', e => {
document.querySelectorAll('.slide').forEach(s => {
const on = s === e.detail.slide;
s.querySelectorAll(DRAWN).forEach(o => {
if (on) setTimeout(() => { if (s.dataset.state === 'active') o.classList.add('on'); }, animateDelay);
else o.classList.remove('on');
});
});
});
const ready = () => { try { paint(); } catch (e) { console.error('infographics paint', e); } requestAnimationFrame(() => { try { paint(); } catch (e) { void e; } }); };
if (document.readyState === 'complete') ready(); else addEventListener('load', ready);
ready();
let t; addEventListener('resize', () => { clearTimeout(t); t = setTimeout(paint, 150); });
})();
</script>
```
### Deck engine (last script in <body>)
```html
<script>
// Deck engine, put at the end of <body>. Sets data-state=past|active|future and --d / --ad (distance from the active slide) on every .slide,
// --i and --progress on <html>, fills [data-idx] / [data-total], toggles .active on [data-goto], and emits 'deck:change'.
(() => {
const root = document.documentElement, slides = [...document.querySelectorAll('.slide')];
let i = -1;
const pad = n => String(n).padStart(2, '0'), revealBase = +document.body.dataset.revealDelay || 220;
function render() {
slides.forEach((s, k) => {
const d = k - i;
s.style.setProperty('--d', d); s.style.setProperty('--ad', Math.abs(d));
s.dataset.state = d < 0 ? 'past' : d > 0 ? 'future' : 'active';
s.setAttribute('aria-hidden', d !== 0);
s.querySelectorAll('.reveal').forEach((el, j) => { el.style.transitionDelay = d === 0 ? (revealBase + j * 90) + 'ms' : '0ms'; el.classList.toggle('in', d === 0); });
});
root.style.setProperty('--i', i); root.style.setProperty('--progress', slides.length > 1 ? i / (slides.length - 1) : 1); root.dataset.slide = i;
document.querySelectorAll('[data-idx]').forEach(e => e.textContent = pad(i + 1));
document.querySelectorAll('[data-total]').forEach(e => e.textContent = pad(slides.length));
document.querySelectorAll('[data-goto]').forEach(e => { e.classList.toggle('active', +e.dataset.goto === i); e.classList.toggle('done', +e.dataset.goto < i); });
history.replaceState(null, '', '#' + (i + 1));
document.dispatchEvent(new CustomEvent('deck:change', { detail: { index: i, slide: slides[i] } }));
}
function go(k) { k = Math.max(0, Math.min(slides.length - 1, k)); if (k !== i) { i = k; render(); } }
document.addEventListener('keydown', e => {
if (e.metaKey || e.ctrlKey || e.altKey || /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName)) return;
if (['ArrowRight', 'ArrowDown', 'PageDown', ' ', 'Enter'].includes(e.key)) { e.preventDefault(); go(i + 1); }
else if (['ArrowLeft', 'ArrowUp', 'PageUp', 'Backspace'].includes(e.key)) { e.preventDefault(); go(i - 1); }
else if (e.key === 'Home') go(0); else if (e.key === 'End') go(slides.length - 1);
else if (e.key === 'f' || e.key === 'F') document.fullscreenElement ? document.exitFullscreen() : root.requestFullscreen();
else if (/^[1-9]$/.test(e.key)) go(+e.key - 1);
});
document.addEventListener('click', e => { const t = e.target.closest('[data-goto]'); if (t) go(+t.dataset.goto); });
if (document.body.hasAttribute('data-wheel')) { let acc = 0, lock = 0; addEventListener('wheel', e => { if (e.target.closest('.carousel-track')) return; const now = Date.now(); if (now < lock) return; acc += e.deltaY; if (Math.abs(acc) > 60) { go(acc > 0 ? i + 1 : i - 1); acc = 0; lock = now + 750; } }, { passive: true }); }
let tx = 0, ty = 0;
addEventListener('touchstart', e => { tx = e.touches[0].clientX; ty = e.touches[0].clientY; }, { passive: true });
addEventListener('touchend', e => { if (e.target.closest('.carousel-track')) return; const dx = e.changedTouches[0].clientX - tx, dy = e.changedTouches[0].clientY - ty; if (Math.max(Math.abs(dx), Math.abs(dy)) < 50) return; go((Math.abs(dx) > Math.abs(dy) ? dx : dy) < 0 ? i + 1 : i - 1); }, { passive: true });
addEventListener('hashchange', () => go((parseInt(location.hash.slice(1)) || 1) - 1));
window.deck = { go, next: () => go(i + 1), prev: () => go(i - 1), get index() { return i; }, slides };
requestAnimationFrame(() => go((parseInt(location.hash.slice(1)) || 1) - 1));
})();
</script>
```
## Before you answer
- Walk through every slide at 1280×720 and check nothing overflows or wraps awkwardly.
- Check the first and the last slide look intentional, not empty.
- Check the accent appears on every slide at least once and never more than three times.
- Return only the complete HTML file in one code block, with no commentary before or after.Shortcut: Cmd or Ctrl + Shift + C copies the prompt.