feat: the deck studio is a deck being made, not three pictures of slides

Three improvements and one real bug.

The bug: `.lp-sl-wash` is absolutely positioned and ends in an opaque white, so
it painted *over* its static siblings — the title slide laid its rule, heading
and picture out correctly and then covered them with its own background. An
explicit z-index on the wash and its siblings fixes it.

The improvements: the slide now sits inside editor chrome, with an export
control that lights up as each slide lands, because three slides in a row are
three pictures and the same three inside a window are a deck being made; a
pointer moves across the canvas, places two things and presses export, which is
what makes it read as being *built*; and the artwork is worth looking at — a
figure on the title slide, a line chart with gridlines, a shaded band and
plotted points, a two-segment ring and a legend. Each slide carries its own
accent, so the three do not look like one slide shown three times.

The warm accent was half theme and half amber, which against a blue primary is
the colour of a wet pavement. It is mostly amber now.

Also: "Pediatric clinical tools" is "Clinical tools", for the same reason the
hero no longer says pediatric.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
Daniel 2026-09-12 19:57:06 +02:00
parent 831cb01650
commit 5670ebbdda
2 changed files with 236 additions and 98 deletions

View file

@ -542,21 +542,22 @@
/* The deck studio
A slide building itself, and the deck beside it filling up. No prose: the
third thing this site does is make a talk out of the same evidence, and a
picture of that lands before a sentence about it would have been read.
A slide building itself inside an editor, and the deck beside it filling up.
No prose: the third thing this site does is make a talk out of the same
evidence, and a picture of that lands before a sentence about it is read.
The colours here are a deliberate exception to the tokens-only rule at the
top of this file, in the same way the mimed question screens are: these are
pictures of slides, and a slide with no colour of its own is not a picture
of a slide. They are declared once, as local properties, and every one of
them is mixed out of --primary so the warm theme stays warm.
of a slide. They are declared once, as local properties, and mixed out of
--primary so the warm theme stays warm with one accent per slide, so three
slides in a row do not look like one slide shown three times.
Timing. One 21s loop shared by everything. Each slide is a third of a turn
behind the last, done with a negative delay (`--i * -14s`), so the three
take the stage in order with no clock in JavaScript. Every part inside a
slide carries the same delay plus its own `--d`, and that not a separate
keyframe per part is what staggers the build. */
behind the last, done with a negative delay (`--i * -14s`), so the three take
the stage in order with no clock in JavaScript. Every part inside a slide
carries the same delay plus its own `--d`, and that not a keyframe per
part is what staggers the build. */
.lp-studio {
--sl-ink: #1e293b;
--sl-paper: #ffffff;
@ -564,75 +565,132 @@
--sl-tint: color-mix(in srgb, var(--primary) 12%, #ffffff);
--sl-tint-2: color-mix(in srgb, var(--primary) 26%, #ffffff);
--sl-quiet: color-mix(in srgb, var(--primary) 16%, #94a3b8);
--sl-warm: color-mix(in srgb, var(--primary) 55%, #f0a04b);
/* A little of the theme, mostly the hue itself. Half and half turned amber
into mud against a blue primary, which is how the highlight bar came out
the colour of a wet pavement. */
--sl-warm: color-mix(in srgb, var(--primary) 12%, #f0a53d);
--sl-cool: color-mix(in srgb, var(--primary) 35%, #14b8a6);
display: grid; grid-template-columns: minmax(0, 1fr) 168px; gap: 22px;
align-items: stretch; max-width: 880px; margin: 0 auto;
}
.lp-stage { margin: 0; min-width: 0; }
/* 16:9, because that is the shape people recognise before they read
anything on it. */
/* The editor around the slide. Three slides in a row are three pictures; the
same three inside a window are a deck being made. */
.lp-deck {
position: relative; aspect-ratio: 16 / 9; overflow: hidden;
position: relative; overflow: hidden;
border: 1px solid var(--border); border-radius: 14px; background: var(--sl-paper);
box-shadow: 0 20px 48px rgba(15, 23, 42, .16);
}
.lp-chrome {
display: flex; align-items: center; gap: 12px;
padding: 8px 12px; border-bottom: 1px solid var(--border); background: var(--bg);
}
.lp-chrome-dots, .lp-chrome-tools { display: inline-flex; gap: 5px; }
.lp-chrome-dots i { width: 8px; height: 8px; border-radius: 50%; background: var(--border); }
.lp-chrome-tools i { width: 22px; height: 7px; border-radius: 3px; background: var(--sl-tint-2); }
.lp-chrome-tools i:nth-child(2) { width: 14px; }
.lp-chrome-tools i:nth-child(3) { width: 30px; }
/* The export control, which flashes as each slide lands: a downward arrow, the
one shape that means "out of here and onto your disk" without a word. */
.lp-chrome-export {
position: relative; margin-left: auto; width: 20px; height: 20px;
border-radius: 6px; background: color-mix(in srgb, var(--primary) 12%, transparent);
}
.lp-chrome-export::before {
content: ''; position: absolute; left: 9px; top: 4px;
width: 2px; height: 9px; border-radius: 1px; background: var(--sl-accent);
}
.lp-chrome-export::after {
content: ''; position: absolute; left: 6px; top: 8px;
width: 7px; height: 7px; transform: rotate(45deg);
border-right: 2px solid var(--sl-accent); border-bottom: 2px solid var(--sl-accent);
}
/* 16:9, because that is the shape people recognise before they read anything. */
.lp-canvas { position: relative; aspect-ratio: 16 / 9; overflow: hidden; }
.lp-slide {
position: absolute; inset: 0; padding: 7% 8%;
display: flex; flex-direction: column; gap: 2.4%;
position: absolute; inset: 0; padding: 6% 7%;
display: flex; flex-direction: column; gap: 2.2%;
background: var(--sl-paper); opacity: 0;
}
/* Calm motion, or no motion preference: the three sit side by side, finished.
Nothing is lost a deck is a thing you look at, not a thing you watch. */
.lp-studio.is-calm .lp-deck { aspect-ratio: auto; display: grid; gap: 10px; padding: 10px; }
/* One accent each, so the deck reads as three different slides. */
.lp-slide.is-figure { --sl-accent: var(--sl-cool); }
.lp-slide.is-data { --sl-accent: var(--primary); }
/* Calm motion: the three lie side by side, finished. Nothing is lost a deck
is a thing you look at, not a thing you watch. */
.lp-studio.is-calm .lp-canvas { aspect-ratio: auto; display: grid; gap: 10px; padding: 10px; }
.lp-studio.is-calm .lp-slide { position: static; opacity: 1; aspect-ratio: 16 / 9; border: 1px solid var(--border); border-radius: 8px; }
.lp-studio.is-calm .lp-sl-cursor, .lp-studio.is-calm .lp-sl-sheen { display: none; }
/* Slide 1 — the opener. */
.lp-slide.is-title { justify-content: center; padding-left: 11%; }
.lp-slide.is-title { display: grid; grid-template-columns: 1.5fr 1fr; align-items: center; gap: 6%; }
/* A fixed gap: the block is only as tall as its three bars, so a percentage
gap resolves to three pixels and the title crowds its own rule. */
.lp-sl-titleblock { display: flex; flex-direction: column; gap: 11px; }
/* Behind the slide's content, explicitly. An absolutely positioned element
paints above its static siblings, and this one ends in an opaque white so
the title, the rule and the picture were being laid out correctly and then
covered by their own background. */
.lp-slide > * { position: relative; z-index: 1; }
.lp-sl-wash {
position: absolute; inset: 0; opacity: 0;
position: absolute; inset: 0; opacity: 0; z-index: 0;
background:
radial-gradient(120% 90% at 8% 110%, var(--sl-tint-2), transparent 62%),
radial-gradient(90% 80% at 100% 0%, color-mix(in srgb, var(--sl-warm) 34%, #ffffff), transparent 58%),
radial-gradient(120% 90% at 6% 110%, var(--sl-tint-2), transparent 60%),
radial-gradient(90% 80% at 100% 0%, color-mix(in srgb, var(--sl-warm) 30%, #ffffff), transparent 55%),
var(--sl-paper);
}
.lp-sl-mark {
position: absolute; top: 9%; right: 8%; width: 26px; height: 26px; border-radius: 50%;
position: absolute; top: 7%; right: 6%; width: 22px; height: 22px; border-radius: 50%;
background: var(--sl-accent); opacity: 0;
box-shadow: 0 0 0 6px color-mix(in srgb, var(--sl-accent) 16%, transparent);
box-shadow: 0 0 0 5px color-mix(in srgb, var(--sl-accent) 16%, transparent);
}
.lp-sl-rule { width: 52px; height: 5px; border-radius: 3px; background: var(--sl-accent); opacity: 0; }
.lp-sl-h1 { width: 74%; height: 15%; border-radius: 6px; background: var(--sl-ink); opacity: 0; }
.lp-sl-h2 { width: 46%; height: 8%; border-radius: 5px; background: var(--sl-quiet); opacity: 0; }
.lp-sl-foot { position: absolute; left: 11%; bottom: 8%; width: 30%; height: 5px; border-radius: 3px; background: var(--sl-tint-2); opacity: 0; }
.lp-sl-rule { width: 46px; height: 5px; border-radius: 3px; background: var(--sl-accent); opacity: 0; }
.lp-sl-h1 { width: 92%; height: 17px; border-radius: 5px; background: var(--sl-ink); opacity: 0; }
.lp-sl-h2 { width: 58%; height: 9px; border-radius: 5px; background: var(--sl-quiet); opacity: 0; }
.lp-sl-foot { position: absolute; left: 7%; bottom: 7%; width: 26%; height: 5px; border-radius: 3px; background: var(--sl-tint-2); opacity: 0; }
/* The picture on a title slide. Abstract on purpose a figure, not a diagram
of anything in particular. */
.lp-sl-hero {
align-self: stretch; margin: 6% 0; border-radius: 10px; overflow: hidden; opacity: 0;
background: linear-gradient(150deg, var(--sl-tint), color-mix(in srgb, var(--sl-warm) 22%, #ffffff));
}
.lp-sl-hero svg { display: block; width: 100%; height: 100%; }
.lp-hero-a { fill: var(--sl-accent); opacity: .28; }
.lp-hero-b { fill: var(--sl-accent); opacity: .18; }
/* Shared furniture on slides 2 and 3. */
.lp-sl-eyebrow { width: 16%; height: 5px; border-radius: 3px; background: var(--sl-accent); opacity: 0; }
.lp-sl-title { width: 58%; height: 9%; border-radius: 5px; background: var(--sl-ink); opacity: 0; }
.lp-sl-cite { margin-top: auto; width: 34%; height: 4px; border-radius: 2px; background: var(--sl-tint-2); opacity: 0; }
.lp-sl-eyebrow { width: 14%; height: 5px; border-radius: 3px; background: var(--sl-accent); opacity: 0; }
.lp-sl-title { width: 54%; height: 11px; border-radius: 5px; background: var(--sl-ink); opacity: 0; }
.lp-sl-cite { margin-top: auto; width: 30%; height: 4px; border-radius: 2px; background: var(--sl-tint-2); opacity: 0; }
/* Slide 2 — the figure. */
.lp-sl-cols { display: grid; grid-template-columns: 1fr 1.15fr; gap: 5%; flex: 1; min-height: 0; margin-top: 2%; }
.lp-sl-copy { display: flex; flex-direction: column; justify-content: center; gap: 11%; }
.lp-sl-cols { display: grid; grid-template-columns: 1fr 1.2fr; gap: 5%; flex: 1; min-height: 0; margin-top: 2%; }
.lp-sl-copy { display: flex; flex-direction: column; justify-content: center; gap: 10%; }
.lp-sl-copy span { display: block; height: 5px; border-radius: 3px; background: var(--sl-quiet); opacity: 0; }
.lp-sl-copy span:nth-child(1) { width: 100%; }
.lp-sl-copy span:nth-child(2) { width: 92%; }
.lp-sl-copy span:nth-child(2) { width: 90%; }
.lp-sl-copy span:nth-child(3) { width: 96%; }
.lp-sl-copy span:nth-child(4) { width: 64%; }
.lp-sl-copy span:nth-child(4) { width: 62%; }
/* The line an author adds last, in the accent: the point of the slide. */
.lp-sl-copy .lp-sl-bullet { width: 76%; height: 6px; background: var(--sl-accent); opacity: 0; }
.lp-sl-figure {
border-radius: 8px; overflow: hidden; opacity: 0;
background: linear-gradient(150deg, var(--sl-tint), color-mix(in srgb, var(--sl-warm) 20%, #ffffff));
background: linear-gradient(150deg, var(--sl-tint), color-mix(in srgb, var(--sl-cool) 14%, #ffffff));
}
.lp-sl-figure svg { display: block; width: 100%; height: 100%; }
.lp-sl-band { fill: var(--sl-accent); opacity: .16; }
.lp-sl-grid line { stroke: var(--sl-accent); stroke-width: .6; opacity: .18; }
.lp-sl-band { fill: var(--sl-accent); opacity: .14; }
.lp-sl-curve { fill: none; stroke: var(--sl-accent); stroke-width: 3; stroke-linecap: round;
stroke-dasharray: 100; stroke-dashoffset: 100; }
.lp-sl-dot { fill: var(--sl-accent); opacity: 0; }
.lp-sl-dot.is-last { fill: var(--sl-warm); }
/* Slide 3 — the numbers. */
.lp-sl-data { display: grid; grid-template-columns: 1fr 86px; gap: 6%; align-items: end; flex: 1; min-height: 0; margin-top: 3%; }
.lp-sl-data { display: grid; grid-template-columns: 1fr 80px; gap: 6%; align-items: end; flex: 1; min-height: 0; margin-top: 3%; }
.lp-sl-bars { list-style: none; margin: 0; padding: 0 0 6px; height: 100%;
display: flex; align-items: flex-end; gap: 7%; border-bottom: 2px solid var(--sl-tint-2); }
.lp-sl-bars li {
@ -640,15 +698,30 @@
background: linear-gradient(180deg, var(--sl-accent), color-mix(in srgb, var(--sl-accent) 55%, #ffffff));
}
.lp-sl-bars li:nth-child(4) { background: linear-gradient(180deg, var(--sl-warm), color-mix(in srgb, var(--sl-warm) 50%, #ffffff)); }
.lp-sl-ring { align-self: center; justify-self: end; width: 86px; opacity: 0; }
.lp-sl-ring { align-self: center; justify-self: end; width: 80px; opacity: 0; }
.lp-sl-ring svg { display: block; width: 100%; height: auto; transform: rotate(-90deg); }
.lp-sl-ring-track { fill: none; stroke: var(--sl-tint); stroke-width: 7; }
.lp-sl-ring-fill { fill: none; stroke: var(--sl-accent); stroke-width: 7; stroke-linecap: round;
stroke-dasharray: 100; stroke-dashoffset: 100; }
/* A second segment, so the ring is a proportion of something rather than a
progress bar bent into a circle. */
.lp-sl-ring-second { fill: none; stroke: var(--sl-warm); stroke-width: 7; stroke-linecap: round;
stroke-dasharray: 18 82; stroke-dashoffset: -62; opacity: 0; }
.lp-sl-legend { list-style: none; margin: 3% 0 0; padding: 0; display: flex; gap: 10px; }
.lp-sl-legend li { width: 34px; height: 5px; border-radius: 3px; background: var(--sl-tint-2); opacity: 0; }
.lp-sl-legend li:nth-child(1) { background: var(--sl-accent); }
.lp-sl-legend li:nth-child(3) { background: var(--sl-warm); }
/* A single pass of light once a slide is standing. It is the only part of
this that is purely decorative, and it is why the thing reads as finished
rather than as still loading. */
/* The pointer that puts things where they go. It is what makes the panel read
as a deck being *made* rather than three slides being shown. */
.lp-sl-cursor {
position: absolute; top: 0; left: 0; width: 11px; height: 11px; border-radius: 50%;
background: var(--sl-ink); opacity: 0; pointer-events: none;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--sl-ink) 14%, transparent);
}
/* A single pass of light once a slide is standing. The only purely decorative
part, and the reason the thing reads as finished rather than still loading. */
.lp-sl-sheen {
position: absolute; inset: -40% -60%; pointer-events: none; opacity: 0;
background: linear-gradient(72deg, transparent 42%, rgba(255,255,255,.55) 50%, transparent 58%);
@ -669,7 +742,7 @@
.lp-th-split { display: grid; grid-template-columns: 1fr 1fr; gap: 8%; flex: 1; }
.lp-th-lines { border-radius: 3px;
background: repeating-linear-gradient(180deg, var(--sl-quiet) 0 3px, transparent 3px 9px); }
.lp-th-fig { border-radius: 4px; background: linear-gradient(150deg, var(--sl-tint-2), var(--sl-tint)); }
.lp-th-fig { border-radius: 4px; background: linear-gradient(150deg, var(--sl-cool), var(--sl-tint)); }
.lp-th-cols { display: flex; align-items: flex-end; gap: 8%; flex: 1; }
.lp-th-cols i { flex: 1; height: var(--h); border-radius: 2px 2px 0 0; background: var(--sl-accent); opacity: .75; }
.lp-th-cols i:last-child { background: var(--sl-warm); }
@ -682,10 +755,19 @@
/* Everything a slide is made of, on the one loop and the one delay, each
part late by its own --d. */
.lp-studio:not(.is-calm) .lp-slide > *,
.lp-studio:not(.is-calm) .lp-sl-copy span {
.lp-studio:not(.is-calm) .lp-sl-titleblock > *,
.lp-studio:not(.is-calm) .lp-sl-copy span,
.lp-studio:not(.is-calm) .lp-sl-legend li {
animation: lp-sl-part 21s cubic-bezier(.2, .7, .3, 1) infinite;
animation-delay: calc(var(--i) * -14s + var(--d, 0s));
}
/* The two wrappers are layout, not content: they must not fade as a block or
the parts inside them build twice over. */
.lp-studio:not(.is-calm) .lp-sl-titleblock,
.lp-studio:not(.is-calm) .lp-sl-copy,
.lp-studio:not(.is-calm) .lp-sl-cols,
.lp-studio:not(.is-calm) .lp-sl-data,
.lp-studio:not(.is-calm) .lp-sl-bars { animation: none; opacity: 1; }
.lp-studio:not(.is-calm) .lp-sl-wash { animation-name: lp-sl-wash-in; }
.lp-studio:not(.is-calm) .lp-sl-curve {
animation: lp-sl-draw 21s cubic-bezier(.4, 0, .2, 1) infinite;
@ -693,19 +775,23 @@
}
.lp-studio:not(.is-calm) .lp-sl-dot {
animation: lp-sl-part 21s ease-out infinite;
animation-delay: calc(var(--i) * -14s + 2.1s);
animation-delay: calc(var(--i) * -14s + var(--d, 1.9s));
}
.lp-studio:not(.is-calm) .lp-sl-ring-fill {
animation: lp-sl-draw 21s cubic-bezier(.4, 0, .2, 1) infinite;
animation-delay: calc(var(--i) * -14s + 1.2s);
animation-delay: calc(var(--i) * -14s + 1.1s);
}
.lp-studio:not(.is-calm) .lp-sl-ring-second {
animation: lp-sl-part 21s ease-out infinite;
animation-delay: calc(var(--i) * -14s + 1.8s);
}
.lp-studio:not(.is-calm) .lp-sl-bars li {
animation: lp-sl-grow 21s cubic-bezier(.2, .8, .25, 1) infinite;
animation-delay: calc(var(--i) * -14s + var(--d));
}
.lp-studio:not(.is-calm) .lp-sl-sheen {
animation: lp-sl-sheen 21s linear infinite;
}
.lp-studio:not(.is-calm) .lp-sl-sheen { animation: lp-sl-sheen 21s linear infinite; }
.lp-studio:not(.is-calm) .lp-sl-cursor { animation: lp-sl-point 7s cubic-bezier(.5, 0, .3, 1) infinite; }
.lp-studio:not(.is-calm) .lp-chrome-export { animation: lp-sl-export 7s ease-out infinite; }
.lp-studio:not(.is-calm) .lp-thumb {
animation: lp-sl-land 21s ease-in-out infinite;
animation-delay: calc(var(--i) * -14s);
@ -749,6 +835,27 @@
28% { opacity: 0; transform: translate3d(70%, 0, 0); }
29%, 100% { opacity: 0; transform: translate3d(-70%, 0, 0); }
}
/* The pointer runs on the per-slide beat rather than the whole loop: it places
two things, presses the export, and gets out of the way. */
@keyframes lp-sl-point {
0% { opacity: 0; transform: translate3d(18%, 74%, 0) scale(1); }
6% { opacity: .85; transform: translate3d(18%, 62%, 0) scale(1); }
14% { opacity: .85; transform: translate3d(24%, 34%, 0) scale(.8); }
18% { opacity: .85; transform: translate3d(24%, 34%, 0) scale(1); }
30% { opacity: .85; transform: translate3d(66%, 52%, 0) scale(1); }
34% { opacity: .85; transform: translate3d(66%, 52%, 0) scale(.8); }
38% { opacity: .85; transform: translate3d(66%, 52%, 0) scale(1); }
58% { opacity: .85; transform: translate3d(95%, 4%, 0) scale(1); }
62% { opacity: .85; transform: translate3d(95%, 4%, 0) scale(.75); }
70% { opacity: 0; transform: translate3d(95%, 4%, 0) scale(1); }
100% { opacity: 0; transform: translate3d(18%, 74%, 0) scale(1); }
}
/* And the export lights up as the pointer reaches it. */
@keyframes lp-sl-export {
0%, 60% { background: color-mix(in srgb, var(--primary) 12%, transparent); transform: none; }
64% { background: color-mix(in srgb, var(--primary) 34%, transparent); transform: scale(1.12); }
74%, 100% { background: color-mix(in srgb, var(--primary) 12%, transparent); transform: none; }
}
/* The thumbnail lifts while its slide is the one on the stage. */
@keyframes lp-sl-land {
0%, 2% { transform: none; border-color: var(--border); box-shadow: 0 4px 12px rgba(15, 23, 42, .07); }

View file

@ -621,66 +621,97 @@ function ModeShowcase({ calm }) {
function SlideStudio({ calm }) {
return (
<div className={`lp-studio${calm ? ' is-calm' : ''}`}>
{/* aria-hidden: this is a picture of a deck, not a deck. A screen reader
is told what it is by the figure's label and skips the mime. */}
<figure className="lp-stage" aria-label="A slide deck building itself: a title, a figure with a growth curve, and a slide of results">
{/* aria-hidden throughout: this is a picture of a deck being made, not a
deck. A screen reader is told what it is by the figure's label and
skips the mime. */}
<figure className="lp-stage" aria-label="A slide deck being built: a title, a figure with a growth curve, and a slide of results, then exported">
<div className="lp-deck" aria-hidden="true">
{/* 1 — the opener. A wash, a rule, a title. */}
<div className="lp-slide is-title" style={{ '--i': 0 }}>
<span className="lp-sl-wash" style={{ '--d': '0.15s' }} />
<span className="lp-sl-mark" style={{ '--d': '0.9s' }} />
<span className="lp-sl-rule" style={{ '--d': '0.5s' }} />
<span className="lp-sl-h1" style={{ '--d': '0.7s' }} />
<span className="lp-sl-h2" style={{ '--d': '0.9s' }} />
<span className="lp-sl-foot" style={{ '--d': '1.2s' }} />
{/* The app around the slide. Without it three slides in a row are
three pictures; with it they are a deck being made. */}
<div className="lp-chrome">
<span className="lp-chrome-dots"><i /><i /><i /></span>
<span className="lp-chrome-tools"><i /><i /><i /></span>
<span className="lp-chrome-export" />
</div>
{/* 2 — evidence and a figure. The curve draws itself. */}
<div className="lp-slide is-figure" style={{ '--i': 1 }}>
<span className="lp-sl-eyebrow" style={{ '--d': '0.3s' }} />
<span className="lp-sl-title" style={{ '--d': '0.45s' }} />
<div className="lp-sl-cols">
<div className="lp-sl-copy">
<span style={{ '--d': '0.7s' }} />
<span style={{ '--d': '0.85s' }} />
<span style={{ '--d': '1s' }} />
<span style={{ '--d': '1.15s' }} />
<div className="lp-canvas">
{/* 1 — the opener. */}
<div className="lp-slide is-title" style={{ '--i': 0 }}>
<span className="lp-sl-wash" style={{ '--d': '0.15s' }} />
<span className="lp-sl-mark" style={{ '--d': '1.0s' }} />
<div className="lp-sl-titleblock">
<span className="lp-sl-rule" style={{ '--d': '0.45s' }} />
<span className="lp-sl-h1" style={{ '--d': '0.65s' }} />
<span className="lp-sl-h2" style={{ '--d': '0.85s' }} />
</div>
<div className="lp-sl-figure" style={{ '--d': '0.75s' }}>
<svg viewBox="0 0 120 84" preserveAspectRatio="none" role="presentation">
<path className="lp-sl-band" d="M6 62 C 34 54, 66 34, 114 12 L114 30 C 66 50, 34 66, 6 72 Z" />
<path className="lp-sl-curve" d="M6 68 C 34 60, 66 40, 114 18" pathLength="100" />
<circle className="lp-sl-dot" cx="114" cy="18" r="3.6" />
<div className="lp-sl-hero" style={{ '--d': '0.55s' }}>
<svg viewBox="0 0 60 80" preserveAspectRatio="none" role="presentation">
<circle className="lp-hero-a" cx="30" cy="26" r="17" />
<path className="lp-hero-b" d="M8 78 C 10 52, 22 44, 30 44 C 38 44, 50 52, 52 78 Z" />
</svg>
</div>
<span className="lp-sl-foot" style={{ '--d': '1.25s' }} />
</div>
<span className="lp-sl-cite" style={{ '--d': '1.35s' }} />
</div>
{/* 3 — the numbers. Bars grow, the ring closes. */}
<div className="lp-slide is-data" style={{ '--i': 2 }}>
<span className="lp-sl-eyebrow" style={{ '--d': '0.3s' }} />
<span className="lp-sl-title" style={{ '--d': '0.45s' }} />
<div className="lp-sl-data">
<ul className="lp-sl-bars">
<li style={{ '--h': '46%', '--d': '0.7s' }} />
<li style={{ '--h': '72%', '--d': '0.82s' }} />
<li style={{ '--h': '58%', '--d': '0.94s' }} />
<li style={{ '--h': '92%', '--d': '1.06s' }} />
{/* 2 — evidence and a figure that draws itself. */}
<div className="lp-slide is-figure" style={{ '--i': 1 }}>
<span className="lp-sl-eyebrow" style={{ '--d': '0.25s' }} />
<span className="lp-sl-title" style={{ '--d': '0.4s' }} />
<div className="lp-sl-cols">
<div className="lp-sl-copy">
<span style={{ '--d': '0.65s' }} />
<span style={{ '--d': '0.78s' }} />
<span style={{ '--d': '0.91s' }} />
<span style={{ '--d': '1.04s' }} />
<span className="lp-sl-bullet" style={{ '--d': '1.2s' }} />
</div>
<div className="lp-sl-figure" style={{ '--d': '0.7s' }}>
<svg viewBox="0 0 120 84" preserveAspectRatio="none" role="presentation">
<g className="lp-sl-grid">
<line x1="6" y1="20" x2="114" y2="20" /><line x1="6" y1="42" x2="114" y2="42" />
<line x1="6" y1="64" x2="114" y2="64" />
</g>
<path className="lp-sl-band" d="M6 62 C 34 54, 66 34, 114 12 L114 32 C 66 52, 34 66, 6 74 Z" />
<path className="lp-sl-curve" d="M6 68 C 34 60, 66 40, 114 18" pathLength="100" />
<circle className="lp-sl-dot" cx="42" cy="57" r="2.8" style={{ '--d': '1.9s' }} />
<circle className="lp-sl-dot" cx="78" cy="39" r="2.8" style={{ '--d': '2.0s' }} />
<circle className="lp-sl-dot is-last" cx="114" cy="18" r="3.6" style={{ '--d': '2.1s' }} />
</svg>
</div>
</div>
<span className="lp-sl-cite" style={{ '--d': '1.35s' }} />
</div>
{/* 3 — the numbers. */}
<div className="lp-slide is-data" style={{ '--i': 2 }}>
<span className="lp-sl-eyebrow" style={{ '--d': '0.25s' }} />
<span className="lp-sl-title" style={{ '--d': '0.4s' }} />
<div className="lp-sl-data">
<ul className="lp-sl-bars">
<li style={{ '--h': '46%', '--d': '0.7s' }} />
<li style={{ '--h': '72%', '--d': '0.82s' }} />
<li style={{ '--h': '58%', '--d': '0.94s' }} />
<li style={{ '--h': '92%', '--d': '1.06s' }} />
</ul>
<div className="lp-sl-ring" style={{ '--d': '0.85s' }}>
<svg viewBox="0 0 44 44" role="presentation">
<circle className="lp-sl-ring-track" cx="22" cy="22" r="18" />
<circle className="lp-sl-ring-fill" cx="22" cy="22" r="18" pathLength="100" />
<circle className="lp-sl-ring-second" cx="22" cy="22" r="18" pathLength="100" />
</svg>
</div>
</div>
<ul className="lp-sl-legend">
<li style={{ '--d': '1.25s' }} /><li style={{ '--d': '1.33s' }} /><li style={{ '--d': '1.41s' }} />
</ul>
<div className="lp-sl-ring" style={{ '--d': '0.9s' }}>
<svg viewBox="0 0 44 44" role="presentation">
<circle className="lp-sl-ring-track" cx="22" cy="22" r="18" />
<circle className="lp-sl-ring-fill" cx="22" cy="22" r="18" pathLength="100" />
</svg>
</div>
</div>
<span className="lp-sl-cite" style={{ '--d': '1.35s' }} />
</div>
{/* The sheen that crosses a slide once it is finished. */}
<span className="lp-sl-sheen" aria-hidden="true" />
{/* The pointer that puts things where they go, and the sheen that
crosses a slide once it is standing. */}
<span className="lp-sl-cursor" aria-hidden="true" />
<span className="lp-sl-sheen" aria-hidden="true" />
</div>
</div>
</figure>
@ -800,7 +831,7 @@ export default function LandingPage() {
<div className="lp-inner lp-split">
<div>
<span className="lp-eyebrow">Also from PedsHub</span>
<h2>Pediatric clinical tools</h2>
<h2>Clinical tools</h2>
<p>
The same material, pointed at the ward instead of the exam: well visit
planning from newborn to adolescence, developmental milestones, the full