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 /* The deck studio
A slide building itself, and the deck beside it filling up. No prose: the A slide building itself inside an editor, and the deck beside it filling up.
third thing this site does is make a talk out of the same evidence, and a No prose: the third thing this site does is make a talk out of the same
picture of that lands before a sentence about it would have been read. 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 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 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 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 of a slide. They are declared once, as local properties, and mixed out of
them is mixed out of --primary so the warm theme stays warm. --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 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 behind the last, done with a negative delay (`--i * -14s`), so the three take
take the stage in order with no clock in JavaScript. Every part inside a the stage in order with no clock in JavaScript. Every part inside a slide
slide carries the same delay plus its own `--d`, and that not a separate carries the same delay plus its own `--d`, and that not a keyframe per
keyframe per part is what staggers the build. */ part is what staggers the build. */
.lp-studio { .lp-studio {
--sl-ink: #1e293b; --sl-ink: #1e293b;
--sl-paper: #ffffff; --sl-paper: #ffffff;
@ -564,75 +565,132 @@
--sl-tint: color-mix(in srgb, var(--primary) 12%, #ffffff); --sl-tint: color-mix(in srgb, var(--primary) 12%, #ffffff);
--sl-tint-2: color-mix(in srgb, var(--primary) 26%, #ffffff); --sl-tint-2: color-mix(in srgb, var(--primary) 26%, #ffffff);
--sl-quiet: color-mix(in srgb, var(--primary) 16%, #94a3b8); --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; display: grid; grid-template-columns: minmax(0, 1fr) 168px; gap: 22px;
align-items: stretch; max-width: 880px; margin: 0 auto; align-items: stretch; max-width: 880px; margin: 0 auto;
} }
.lp-stage { margin: 0; min-width: 0; } .lp-stage { margin: 0; min-width: 0; }
/* 16:9, because that is the shape people recognise before they read /* The editor around the slide. Three slides in a row are three pictures; the
anything on it. */ same three inside a window are a deck being made. */
.lp-deck { .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); border: 1px solid var(--border); border-radius: 14px; background: var(--sl-paper);
box-shadow: 0 20px 48px rgba(15, 23, 42, .16); 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 { .lp-slide {
position: absolute; inset: 0; padding: 7% 8%; position: absolute; inset: 0; padding: 6% 7%;
display: flex; flex-direction: column; gap: 2.4%; display: flex; flex-direction: column; gap: 2.2%;
background: var(--sl-paper); opacity: 0; background: var(--sl-paper); opacity: 0;
} }
/* Calm motion, or no motion preference: the three sit side by side, finished. /* One accent each, so the deck reads as three different slides. */
Nothing is lost a deck is a thing you look at, not a thing you watch. */ .lp-slide.is-figure { --sl-accent: var(--sl-cool); }
.lp-studio.is-calm .lp-deck { aspect-ratio: auto; display: grid; gap: 10px; padding: 10px; } .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-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. */ /* 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 { .lp-sl-wash {
position: absolute; inset: 0; opacity: 0; position: absolute; inset: 0; opacity: 0; z-index: 0;
background: background:
radial-gradient(120% 90% at 8% 110%, var(--sl-tint-2), transparent 62%), 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) 34%, #ffffff), transparent 58%), radial-gradient(90% 80% at 100% 0%, color-mix(in srgb, var(--sl-warm) 30%, #ffffff), transparent 55%),
var(--sl-paper); var(--sl-paper);
} }
.lp-sl-mark { .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; 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-rule { width: 46px; 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-h1 { width: 92%; height: 17px; border-radius: 5px; background: var(--sl-ink); opacity: 0; }
.lp-sl-h2 { width: 46%; height: 8%; border-radius: 5px; background: var(--sl-quiet); opacity: 0; } .lp-sl-h2 { width: 58%; height: 9px; 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-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. */ /* 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-eyebrow { width: 14%; 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-title { width: 54%; height: 11px; 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-cite { margin-top: auto; width: 30%; height: 4px; border-radius: 2px; background: var(--sl-tint-2); opacity: 0; }
/* Slide 2 — the figure. */ /* 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-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: 11%; } .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 { 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(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(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 { .lp-sl-figure {
border-radius: 8px; overflow: hidden; opacity: 0; 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-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; .lp-sl-curve { fill: none; stroke: var(--sl-accent); stroke-width: 3; stroke-linecap: round;
stroke-dasharray: 100; stroke-dashoffset: 100; } stroke-dasharray: 100; stroke-dashoffset: 100; }
.lp-sl-dot { fill: var(--sl-accent); opacity: 0; } .lp-sl-dot { fill: var(--sl-accent); opacity: 0; }
.lp-sl-dot.is-last { fill: var(--sl-warm); }
/* Slide 3 — the numbers. */ /* 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%; .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); } display: flex; align-items: flex-end; gap: 7%; border-bottom: 2px solid var(--sl-tint-2); }
.lp-sl-bars li { .lp-sl-bars li {
@ -640,15 +698,30 @@
background: linear-gradient(180deg, var(--sl-accent), color-mix(in srgb, var(--sl-accent) 55%, #ffffff)); 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-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 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-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; .lp-sl-ring-fill { fill: none; stroke: var(--sl-accent); stroke-width: 7; stroke-linecap: round;
stroke-dasharray: 100; stroke-dashoffset: 100; } 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 /* The pointer that puts things where they go. It is what makes the panel read
this that is purely decorative, and it is why the thing reads as finished as a deck being *made* rather than three slides being shown. */
rather than as still loading. */ .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 { .lp-sl-sheen {
position: absolute; inset: -40% -60%; pointer-events: none; opacity: 0; position: absolute; inset: -40% -60%; pointer-events: none; opacity: 0;
background: linear-gradient(72deg, transparent 42%, rgba(255,255,255,.55) 50%, transparent 58%); 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-split { display: grid; grid-template-columns: 1fr 1fr; gap: 8%; flex: 1; }
.lp-th-lines { border-radius: 3px; .lp-th-lines { border-radius: 3px;
background: repeating-linear-gradient(180deg, var(--sl-quiet) 0 3px, transparent 3px 9px); } 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 { 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 { 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); } .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 /* Everything a slide is made of, on the one loop and the one delay, each
part late by its own --d. */ part late by its own --d. */
.lp-studio:not(.is-calm) .lp-slide > *, .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: lp-sl-part 21s cubic-bezier(.2, .7, .3, 1) infinite;
animation-delay: calc(var(--i) * -14s + var(--d, 0s)); 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-wash { animation-name: lp-sl-wash-in; }
.lp-studio:not(.is-calm) .lp-sl-curve { .lp-studio:not(.is-calm) .lp-sl-curve {
animation: lp-sl-draw 21s cubic-bezier(.4, 0, .2, 1) infinite; animation: lp-sl-draw 21s cubic-bezier(.4, 0, .2, 1) infinite;
@ -693,19 +775,23 @@
} }
.lp-studio:not(.is-calm) .lp-sl-dot { .lp-studio:not(.is-calm) .lp-sl-dot {
animation: lp-sl-part 21s ease-out infinite; 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 { .lp-studio:not(.is-calm) .lp-sl-ring-fill {
animation: lp-sl-draw 21s cubic-bezier(.4, 0, .2, 1) infinite; 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 { .lp-studio:not(.is-calm) .lp-sl-bars li {
animation: lp-sl-grow 21s cubic-bezier(.2, .8, .25, 1) infinite; animation: lp-sl-grow 21s cubic-bezier(.2, .8, .25, 1) infinite;
animation-delay: calc(var(--i) * -14s + var(--d)); animation-delay: calc(var(--i) * -14s + var(--d));
} }
.lp-studio:not(.is-calm) .lp-sl-sheen { .lp-studio:not(.is-calm) .lp-sl-sheen { animation: lp-sl-sheen 21s linear infinite; }
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 { .lp-studio:not(.is-calm) .lp-thumb {
animation: lp-sl-land 21s ease-in-out infinite; animation: lp-sl-land 21s ease-in-out infinite;
animation-delay: calc(var(--i) * -14s); animation-delay: calc(var(--i) * -14s);
@ -749,6 +835,27 @@
28% { opacity: 0; transform: translate3d(70%, 0, 0); } 28% { opacity: 0; transform: translate3d(70%, 0, 0); }
29%, 100% { 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. */ /* The thumbnail lifts while its slide is the one on the stage. */
@keyframes lp-sl-land { @keyframes lp-sl-land {
0%, 2% { transform: none; border-color: var(--border); box-shadow: 0 4px 12px rgba(15, 23, 42, .07); } 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 }) { function SlideStudio({ calm }) {
return ( return (
<div className={`lp-studio${calm ? ' is-calm' : ''}`}> <div className={`lp-studio${calm ? ' is-calm' : ''}`}>
{/* aria-hidden: this is a picture of a deck, not a deck. A screen reader {/* aria-hidden throughout: this is a picture of a deck being made, not a
is told what it is by the figure's label and skips the mime. */} deck. A screen reader is told what it is by the figure's label and
<figure className="lp-stage" aria-label="A slide deck building itself: a title, a figure with a growth curve, and a slide of results"> 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"> <div className="lp-deck" aria-hidden="true">
{/* 1 — the opener. A wash, a rule, a title. */} {/* The app around the slide. Without it three slides in a row are
<div className="lp-slide is-title" style={{ '--i': 0 }}> three pictures; with it they are a deck being made. */}
<span className="lp-sl-wash" style={{ '--d': '0.15s' }} /> <div className="lp-chrome">
<span className="lp-sl-mark" style={{ '--d': '0.9s' }} /> <span className="lp-chrome-dots"><i /><i /><i /></span>
<span className="lp-sl-rule" style={{ '--d': '0.5s' }} /> <span className="lp-chrome-tools"><i /><i /><i /></span>
<span className="lp-sl-h1" style={{ '--d': '0.7s' }} /> <span className="lp-chrome-export" />
<span className="lp-sl-h2" style={{ '--d': '0.9s' }} />
<span className="lp-sl-foot" style={{ '--d': '1.2s' }} />
</div> </div>
{/* 2 — evidence and a figure. The curve draws itself. */} <div className="lp-canvas">
<div className="lp-slide is-figure" style={{ '--i': 1 }}> {/* 1 — the opener. */}
<span className="lp-sl-eyebrow" style={{ '--d': '0.3s' }} /> <div className="lp-slide is-title" style={{ '--i': 0 }}>
<span className="lp-sl-title" style={{ '--d': '0.45s' }} /> <span className="lp-sl-wash" style={{ '--d': '0.15s' }} />
<div className="lp-sl-cols"> <span className="lp-sl-mark" style={{ '--d': '1.0s' }} />
<div className="lp-sl-copy"> <div className="lp-sl-titleblock">
<span style={{ '--d': '0.7s' }} /> <span className="lp-sl-rule" style={{ '--d': '0.45s' }} />
<span style={{ '--d': '0.85s' }} /> <span className="lp-sl-h1" style={{ '--d': '0.65s' }} />
<span style={{ '--d': '1s' }} /> <span className="lp-sl-h2" style={{ '--d': '0.85s' }} />
<span style={{ '--d': '1.15s' }} />
</div> </div>
<div className="lp-sl-figure" style={{ '--d': '0.75s' }}> <div className="lp-sl-hero" style={{ '--d': '0.55s' }}>
<svg viewBox="0 0 120 84" preserveAspectRatio="none" role="presentation"> <svg viewBox="0 0 60 80" 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" /> <circle className="lp-hero-a" cx="30" cy="26" r="17" />
<path className="lp-sl-curve" d="M6 68 C 34 60, 66 40, 114 18" pathLength="100" /> <path className="lp-hero-b" d="M8 78 C 10 52, 22 44, 30 44 C 38 44, 50 52, 52 78 Z" />
<circle className="lp-sl-dot" cx="114" cy="18" r="3.6" />
</svg> </svg>
</div> </div>
<span className="lp-sl-foot" style={{ '--d': '1.25s' }} />
</div> </div>
<span className="lp-sl-cite" style={{ '--d': '1.35s' }} />
</div>
{/* 3 — the numbers. Bars grow, the ring closes. */} {/* 2 — evidence and a figure that draws itself. */}
<div className="lp-slide is-data" style={{ '--i': 2 }}> <div className="lp-slide is-figure" style={{ '--i': 1 }}>
<span className="lp-sl-eyebrow" style={{ '--d': '0.3s' }} /> <span className="lp-sl-eyebrow" style={{ '--d': '0.25s' }} />
<span className="lp-sl-title" style={{ '--d': '0.45s' }} /> <span className="lp-sl-title" style={{ '--d': '0.4s' }} />
<div className="lp-sl-data"> <div className="lp-sl-cols">
<ul className="lp-sl-bars"> <div className="lp-sl-copy">
<li style={{ '--h': '46%', '--d': '0.7s' }} /> <span style={{ '--d': '0.65s' }} />
<li style={{ '--h': '72%', '--d': '0.82s' }} /> <span style={{ '--d': '0.78s' }} />
<li style={{ '--h': '58%', '--d': '0.94s' }} /> <span style={{ '--d': '0.91s' }} />
<li style={{ '--h': '92%', '--d': '1.06s' }} /> <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> </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> </div>
<span className="lp-sl-cite" style={{ '--d': '1.35s' }} />
</div>
{/* The sheen that crosses a slide once it is finished. */} {/* The pointer that puts things where they go, and the sheen that
<span className="lp-sl-sheen" aria-hidden="true" /> 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> </div>
</figure> </figure>
@ -800,7 +831,7 @@ export default function LandingPage() {
<div className="lp-inner lp-split"> <div className="lp-inner lp-split">
<div> <div>
<span className="lp-eyebrow">Also from PedsHub</span> <span className="lp-eyebrow">Also from PedsHub</span>
<h2>Pediatric clinical tools</h2> <h2>Clinical tools</h2>
<p> <p>
The same material, pointed at the ward instead of the exam: well visit The same material, pointed at the ward instead of the exam: well visit
planning from newborn to adolescence, developmental milestones, the full planning from newborn to adolescence, developmental milestones, the full