/* ---------------------------------------------------------------------------
   The home page as a deck.

   Every slide's content is in the HTML. CSS shows one at a time. Nothing is
   built by script and nothing is fetched, which is what keeps the two usual
   objections to a slide-shaped site from landing: a search engine reads the
   whole page, and every slide has its own address in the bar.

   Hidden slides use visibility and position rather than display:none, so the
   text stays in the accessibility tree for a screen reader that walks the
   document rather than the slides.

   ON THE MOTION
   The built-in easings are too weak to read as deliberate, so the curves here
   are named and shared. Slides come in under 300ms: a deck is stepped through
   dozens of times in a sitting, and anything slower starts to feel like waiting
   rather than moving. Content inside a slide is staggered by 45ms, which is
   enough to read as a sequence and not enough to make anyone wait for it.
--------------------------------------------------------------------------- */

:root {
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);
  --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);
}

.deck {
  position: fixed;
  inset: var(--bar-h, 64px) 0 0 0;
  overflow: hidden;
}

.slide {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: clamp(24px, 5vh, 64px) 0 clamp(84px, 11vh, 116px);
  overflow-y: auto;
  overscroll-behavior: contain;

  visibility: hidden;
  opacity: 0;
  transition: opacity 240ms var(--ease-out), visibility 0s linear 240ms;
}
.slide[data-here] {
  visibility: visible;
  opacity: 1;
  transition: opacity 240ms var(--ease-out), visibility 0s linear 0s;
}

/* A slide that scrolls is a slide that did not fit, and on a deck that reads
   as a fault rather than a feature. Scrolling stays available for a short
   window or a long slide, but nothing should need it at a normal size. */
.slide-in {
  width: var(--shell);
  margin-inline: auto;
  max-width: 100%;
}

/* Content arrives in order rather than all at once. Transform and opacity
   only, so it stays off the main thread. */
.slide[data-here] .slide-in > * {
  animation: riseIn 460ms var(--ease-out) both;
}
.slide[data-here] .slide-in > *:nth-child(1) { animation-delay: 40ms; }
.slide[data-here] .slide-in > *:nth-child(2) { animation-delay: 85ms; }
.slide[data-here] .slide-in > *:nth-child(3) { animation-delay: 130ms; }
.slide[data-here] .slide-in > *:nth-child(4) { animation-delay: 175ms; }
.slide[data-here] .slide-in > *:nth-child(5) { animation-delay: 220ms; }
.slide[data-here] .slide-in > *:nth-child(6) { animation-delay: 265ms; }
/* Six is the opening slide with the name field on it. Past that the stagger
   stops earning its keep: anything arriving later than about a third of a
   second after the slide turns is just a wait. */

@keyframes riseIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .slide { transition: opacity 1ms linear, visibility 0s linear 1ms; }
  .slide[data-here] { transition: opacity 1ms linear, visibility 0s linear 0s; }
  .slide[data-here] .slide-in > * { animation: none; }
}

/* ---- what a slide is made of ------------------------------------------- */

.s-kicker {
  font: 700 0.72rem/1 var(--display);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--quiet);
  margin: 0 0 20px;
}

/* One idea per slide, so the type can be large enough to read across a room.
   This is the whole difference between a deck and a page. */
.s-title {
  font-size: clamp(2.4rem, 1.3rem + 5vw, 5rem);
  line-height: 0.98;
  letter-spacing: -0.04em;
  margin: 0;
  max-width: 15ch;
}
.s-title + .s-line { margin-top: 26px; }

.s-line {
  font-size: clamp(1.05rem, 1rem + 0.55vw, 1.35rem);
  color: var(--quiet);
  max-width: 46ch;
  margin: 0;
}

.s-cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 34px; }
/* Security card 22: the legal pages are one tap away from the home page too. */
.s-legal { display: flex; flex-wrap: wrap; gap: 6px 18px; margin-top: 28px; font-size: 14px; }
.s-legal a { color: inherit; opacity: 0.72; text-decoration: underline; text-underline-offset: 3px; }
.s-legal a:hover, .s-legal a:focus-visible { opacity: 1; }
.s-foot { margin: 18px 0 0; font-size: 0.86rem; color: var(--quiet); opacity: 0.9; }

/* ---- the controls ------------------------------------------------------ */

.deck-bar {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 16px clamp(16px, 4vw, 40px);
  background: linear-gradient(to top, var(--ink-deep) 58%, transparent);
  pointer-events: none;
}
.deck-bar > * { pointer-events: auto; }

.dots { display: flex; gap: 7px; margin-right: auto; }
.dot {
  width: 8px; height: 8px; padding: 0;
  border-radius: 999px;
  border: 0;
  background: color-mix(in oklab, var(--paper) 30%, transparent);
  cursor: pointer;
  transition: background 200ms var(--ease-out), width 200ms var(--ease-out);
}
.dot:hover { background: color-mix(in oklab, var(--paper) 60%, transparent); }
.dot[aria-current="true"] { background: var(--paper); width: 24px; }

.deck-count { font: 400 0.8rem/1 var(--mono); color: var(--quiet); }

.deck-move { display: flex; gap: 8px; }
.deck-btn {
  display: grid; place-items: center;
  width: 40px; height: 40px; padding: 0;
  border: 1px solid var(--edge);
  border-radius: 999px;
  background: color-mix(in oklab, var(--ink-deep) 82%, var(--paper));
  color: var(--paper);
  cursor: pointer;
  transition: border-color 160ms var(--ease-out), transform 160ms var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .deck-btn:hover:not(:disabled) { border-color: var(--paper); }
}
/* Instant feedback that the press was heard. */
.deck-btn:active:not(:disabled) { transform: scale(0.94); }
.deck-btn:disabled { opacity: 0.28; cursor: default; }

/* ---- slide layouts ------------------------------------------------------ */

/* On the opening slide the wordmark is the title, so it is the largest thing
   on it and the line underneath supports rather than competes. Everywhere
   else the headline leads, because everywhere else the brand has already been
   established by the bar. */
.s-open .wordmark--big { height: clamp(74px, 13vw, 156px); margin-bottom: 26px; }
.s-open .s-title {
  font-size: clamp(1.5rem, 1.1rem + 1.9vw, 2.5rem);
  line-height: 1.05;
  letter-spacing: -0.03em;
  max-width: 20ch;
}
.s-open .s-title + .s-line { margin-top: 14px; }

/* ---- the command sky ----------------------------------------------------
   Twenty real commands drifting across the slide. They are the picture here,
   not a background texture, so they stay legible: the movement is slow enough
   to read through and small enough that nothing ever travels far from where
   it started.

   Each one floats between two points and back rather than looping round,
   because a loop has to jump home at the end and the jump is the only thing
   anyone would notice. Durations and delays are all different so the field
   never falls into step with itself.

   There is nothing else on the slide, so they use all of it. */
.cmd-sky {
  position: absolute;
  /* Clear of the bar along the bottom, and inset a little at the top so a word
     never sits against the header. */
  inset: clamp(12px, 3vh, 34px) clamp(14px, 2vw, 40px) clamp(84px, 11vh, 116px);
  overflow: hidden;

  /* The words are sized off THIS box rather than off the window.

     Sized off the window they grew and shrank at a different rate from the
     field they sit in, so how crowded the field was changed with every width,
     and a set of positions solved at 1600 had fourteen collisions at 1120.
     Tied to the container, the whole arrangement is the same shape at every
     width and one solution holds. */
  container-type: size;
  --fs: 2.05cqw;
}
/* Fewer words to fit once the field narrows, so they can afford to be bigger
   relative to it. */
@media (max-width: 759px) { .cmd-sky { --fs: 3.4cqw; } }

.cmd-sky code {
  position: absolute;
  top: var(--y);
  transform: translateY(-50%);
  white-space: nowrap;
  opacity: 0;
  pointer-events: auto;
  cursor: default;
}
/* Held by the edge it is nearest, so a long word can only grow inwards and
   nothing hangs off the side of the slide. */
.cmd-sky code[data-side="left"]  { left: var(--x); }
.cmd-sky code[data-side="right"] { right: var(--x); }
.cmd-sky code span {
  display: inline-block;
  /* Set in the display face, big and tight, rather than as small monospaced
     pills. The pills read as form fields sitting still; the words read as
     words, and the slash in front is enough to say what they are.

     Poppins because it is the wordmark's own typeface, so twenty of these
     across a slide look like the brand rather than like a code listing. */
  font: 700 calc(var(--fs) * var(--s, 1))/1.1 var(--display);
  letter-spacing: -0.035em;
  /* Coloured along the wordmark's own gradient, cyan on the left through
     violet to magenta on the right, so the field picks up the brand without
     anyone having to draw it. */
  color: var(--c, var(--paper));
  opacity: var(--o, 0.8);
  transition: transform 260ms var(--ease-out), opacity 260ms var(--ease-out);
}

/* Only while the slide is on screen: twenty animations running behind a slide
   nobody is looking at is work for nothing. */
.slide[data-here] .cmd-sky code {
  animation:
    skyIn 620ms var(--ease-out) both,
    drift var(--t) ease-in-out var(--d) infinite alternate;
}
@keyframes skyIn {
  from { opacity: 0; transform: translateY(-50%) scale(0.96); }
  to   { opacity: 1; transform: translateY(-50%); }
}
/* Travel is in container-width units, so a word's journey scales with the word
   and with the field it is crossing. */
@keyframes drift {
  from { transform: translateY(-50%) translate3d(0, 0, 0); }
  to   { transform: translateY(-50%) translate3d(var(--dx), var(--dy), 0); }
}

/* Under the pointer a word stops drifting, comes fully forward and grows a
   little: near enough to touch, so it behaves as though it were. */
@media (hover: hover) and (pointer: fine) {
  .cmd-sky code:hover { animation-play-state: paused; z-index: 2; }
  .cmd-sky code:hover span { transform: scale(1.16); opacity: 1; }
}

/* Nearer ones are bigger and brighter, which is the whole of the depth in
   this: no blur, no shadow, no perspective. --s scales the type, --o how far
   away it looks. */
.cmd-sky code[data-tier="near"] { --s: 1.18; --o: 1; }
.cmd-sky code[data-tier="mid"]  { --s: 0.86; --o: 0.66; }
.cmd-sky code[data-tier="far"]  { --s: 0.62; --o: 0.42; }

/* Nearer ones are bigger, brighter and travel further, which is the whole of
   the depth in this: no blur, no shadow, no perspective. */
/* The accents as they are used on a dark ground. */
.cmd-sky {
  --sky-cyan: var(--cyan);
  --sky-violet: var(--violet);
  --sky-magenta: var(--magenta);
}
:root[data-theme="light"] .cmd-sky,
:root:not([data-theme="dark"]) .cmd-sky {
  --sky-cyan: oklch(0.46 0.14 232);
  --sky-violet: oklch(0.42 0.22 292);
  --sky-magenta: oklch(0.45 0.23 348);
}
/* Only apply the light palette when light is actually in force. */
:root[data-theme="dark"] .cmd-sky {
  --sky-cyan: var(--cyan);
  --sky-violet: var(--violet);
  --sky-magenta: var(--magenta);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .cmd-sky {
    --sky-cyan: var(--cyan);
    --sky-violet: var(--violet);
    --sky-magenta: var(--magenta);
  }
}

/* The wash. Three pools of the brand's own colours, weak enough that nothing
   reads as a shape and strong enough that the words are standing on something.
   Dark mode already has the aurora doing this job, so it takes none.

   On the slide rather than on the field of words: the field is inset from the
   edges to keep words off the header and the bar, and a wash inset with it
   left a white margin all the way round that read as a border. */
.s-typing::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(46% 52% at 14% 20%, color-mix(in oklab, var(--cyan) 34%, transparent), transparent 72%),
    radial-gradient(50% 54% at 60% 70%, color-mix(in oklab, var(--violet) 30%, transparent), transparent 74%),
    radial-gradient(42% 46% at 90% 30%, color-mix(in oklab, var(--magenta) 28%, transparent), transparent 72%);
}
:root[data-theme="light"] .s-typing::before,
:root:not([data-theme="dark"]) .s-typing::before { opacity: 0.55; }
:root[data-theme="dark"] .s-typing::before { opacity: 0; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .s-typing::before { opacity: 0; }
}
.s-typing .cmd-sky { z-index: 1; }

/* Less of a drop between the tiers on white: what reads as distance against
   black reads as a printing fault against paper. */
:root[data-theme="light"] .cmd-sky code[data-tier="mid"],
:root:not([data-theme="dark"]) .cmd-sky code[data-tier="mid"] { --o: 0.84; }
:root[data-theme="light"] .cmd-sky code[data-tier="far"],
:root:not([data-theme="dark"]) .cmd-sky code[data-tier="far"] { --o: 0.66; }
:root[data-theme="dark"] .cmd-sky code[data-tier="mid"] { --o: 0.66; }
:root[data-theme="dark"] .cmd-sky code[data-tier="far"] { --o: 0.42; }

/* Twenty words need a wide slide to sit in without crowding, so the field
   thins as the window does: the furthest tier goes first, then the middle one,
   leaving the five nearest to carry the idea on a phone. */
@media (max-width: 1099px) {
  .cmd-sky code[data-tier="far"] { display: none; }
}
@media (max-width: 759px) {
  .cmd-sky code[data-tier="mid"] { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .slide[data-here] .cmd-sky code { animation: skyIn 1ms linear both; }
}

/* The screenshot is the slide. Nothing competes with it.

   It has to fit the height, not just the width. Sized on width alone it came
   out 737 tall in a slide with about 650 to give, so the one slide made of a
   single picture was the one slide you had to scroll. Bounded by both, and the
   picture keeps its shape. */
.s-shot .slide-in {
  width: min(1560px, 96vw);
  display: flex; flex-direction: column; align-items: center;
  max-height: 100%;
}

/* This slide is a picture, so it gets the room a picture wants: the padding
   every other slide uses is there to keep type off the edges, and type is not
   the problem here. Only the controls at the foot need clearing. */
.slide.s-shot { padding: clamp(14px, 2.5vh, 30px) 0 84px; }
.s-shot figure { min-height: 0; display: flex; flex-direction: column; align-items: center; }
.s-shot img {
  display: block;
  width: auto; max-width: 100%;
  /* Only what is genuinely in the way: the bar above, the controls below, and
     the caption. Two hundred and fifty was a guess and it cost half the
     picture. */
  height: auto; max-height: calc(100dvh - var(--bar-h, 64px) - 132px);
  border-radius: 12px;
  border: 1px solid color-mix(in oklab, var(--edge) 60%, transparent);
  box-shadow: 0 30px 70px -34px oklch(0 0 0 / var(--shadow, 0.9));
}
.s-shot figcaption { margin-top: 14px; font-size: 0.9rem; color: var(--quiet); text-align: center; }

.s-three { display: grid; gap: clamp(22px, 3vw, 40px); grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); margin-top: 12px; }
.s-three > div { padding-top: 18px; border-top: 2px solid var(--cyan); }
.s-three h3 { font-size: 1.15rem; margin: 0 0 7px; }
.s-three p { margin: 0; color: var(--quiet); font-size: 0.95rem; }

.s-where { display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); margin-top: 12px; }
.s-where .place h3 { font-size: 1.05rem; margin: 0 0 6px; }
.s-where .place p { margin: 0 0 14px; font-size: 0.92rem; }

/* Where it stands: two words and a phrase, not a paragraph in a cell. */
.s-state { display: grid; gap: 1px; margin-top: 14px; background: color-mix(in oklab, var(--edge) 34%, transparent); border-radius: 12px; overflow: hidden; }
.s-state div {
  display: grid; grid-template-columns: minmax(110px, 20%) 1fr; gap: 18px;
  padding: 16px 18px; background: var(--ink-deep);
}
.s-state dt { font: 700 0.82rem/1.4 var(--display); letter-spacing: 0.04em; text-transform: uppercase; color: var(--cyan); margin: 0; }
.s-state dd { margin: 0; color: var(--quiet); font-size: 0.96rem; }
.s-state div:nth-child(3) dt, .s-state div:nth-child(4) dt { color: var(--quiet); }

.s-price { font: 700 clamp(4rem, 2.5rem + 8vw, 9rem)/0.9 var(--display); letter-spacing: -0.05em; margin: 0; }

@media (max-height: 640px) {
  .slide { place-items: start center; }
  .s-title { font-size: clamp(1.8rem, 1.2rem + 3vw, 2.6rem); }
  .s-price { font-size: clamp(3rem, 2rem + 5vw, 5rem); }
}

.deck-page .foot { display: none; }
.deck-page { overflow: hidden; }


/* ---- the slide being built ----------------------------------------------
   Six photographs of the same slide, one after each command, stacked and cross
   faded. The first is in the flow and gives the stack its size; the rest sit on
   top of it and fade in over their turn, so there is never a moment where
   nothing is showing.

   They fade IN and stay, rather than in and out: with each one visible only
   during its own slot the opacities dip between slots and the page shows
   through the gap. Staying means the picture only ever builds up, which is
   also what the slide is about. At the end they all clear at once and the
   empty slide underneath is back. */
.build-stack {
  position: relative;
  display: block;
  /* Sized by its own aspect ratio rather than by the picture inside it.

     The typing is placed in fractions of this box so it stays on the chat box
     at any size, and fractions of a box need `container-type: size` to work.
     But size containment means the contents can no longer size the box, so
     with the picture in the flow the whole stack collapsed to nothing. Giving
     it the picture's shape and letting the width decide settles both. */
  aspect-ratio: 1800 / 1013;
  /* The figure is a centred column, so a flex item's width shrinks to its
     content: with the box now sized FROM its width that is circular, and it
     collapsed. Stretching gives the width something real to resolve against. */
  align-self: stretch;
  width: min(100%, calc((100dvh - var(--bar-h, 64px) - 196px) * 1800 / 1013));
  margin-inline: auto;
  container-type: size;
  border-radius: 12px;
  border: 1px solid color-mix(in oklab, var(--edge) 60%, transparent);
  box-shadow: 0 30px 70px -34px oklch(0 0 0 / var(--shadow, 0.9));
  overflow: hidden;
}
.build-stack .bf {
  position: absolute; inset: 0;
  display: block;
  width: 100%; height: 100%;
}
.build-stack .bf + .bf { opacity: 0; }
.slide[data-here] .build-stack .bf + .bf { animation: bfStep 18s linear infinite; }
.slide[data-here] .build-stack .bf:nth-child(2) { animation-name: bf1; }
.slide[data-here] .build-stack .bf:nth-child(3) { animation-name: bf2; }
.slide[data-here] .build-stack .bf:nth-child(4) { animation-name: bf3; }
.slide[data-here] .build-stack .bf:nth-child(5) { animation-name: bf4; }
.slide[data-here] .build-stack .bf:nth-child(6) { animation-name: bf5; }
@keyframes bf1 { 0%,15% { opacity: 0 } 18%,97% { opacity: 1 } 100% { opacity: 0 } }
@keyframes bf2 { 0%,33% { opacity: 0 } 36%,97% { opacity: 1 } 100% { opacity: 0 } }
@keyframes bf3 { 0%,51% { opacity: 0 } 54%,97% { opacity: 1 } 100% { opacity: 0 } }
@keyframes bf4 { 0%,69% { opacity: 0 } 72%,97% { opacity: 1 } 100% { opacity: 0 } }
@keyframes bf5 { 0%,87% { opacity: 0 } 90%,97% { opacity: 1 } 100% { opacity: 0 } }

/* The chat box, drawn over the one in the picture. Everything here is in
   fractions of the stack, taken from where the shoot measured the real box, so
   it sits on it at any size instead of only at the one it was authored at. */
.build-type {
  position: absolute;
  left: 82.9502%; top: 94.2460%;
  width: 7.9567%; height: 4.6958%;
  overflow: hidden;
  border-radius: 0.7cqh;
  /* Covers the placeholder the screenshot has in the box. */
  background: #fff;
}
.build-type span {
  position: absolute;
  inset: 0;
  display: flex; align-items: center;
  padding-left: 1.1cqh;
  font: 1.444cqh/1 "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif;
  color: #14161d;
  white-space: nowrap;
  /* Revealed by clipping rather than by width, because the app's box is set in
     a proportional face and a width in ch would not land on the letters. */
  clip-path: inset(0 100% 0 0);
}
/* One step per character, taken from --n on the span. steps(1) was a mistake:
   it holds the start value until the end of the interval, so the reveal never
   happened at all. */
.slide[data-here] .build-type span {
  animation: bfType 18s infinite;
  animation-timing-function: steps(var(--n, 20));
}
.slide[data-here] .build-type span:nth-child(1) { animation-name: type1; }
.slide[data-here] .build-type span:nth-child(2) { animation-name: type2; }
.slide[data-here] .build-type span:nth-child(3) { animation-name: type3; }
.slide[data-here] .build-type span:nth-child(4) { animation-name: type4; }
.slide[data-here] .build-type span:nth-child(5) { animation-name: type5; }
/* Typed over the run up to its own frame, then gone the moment it lands. */
@keyframes type1 { 0%,2% { clip-path: inset(0 100% 0 0) } 15% { clip-path: inset(0 0 0 0) } 15.01%,100% { clip-path: inset(0 100% 0 0) } }
@keyframes type2 { 0%,20% { clip-path: inset(0 100% 0 0) } 33% { clip-path: inset(0 0 0 0) } 33.01%,100% { clip-path: inset(0 100% 0 0) } }
@keyframes type3 { 0%,38% { clip-path: inset(0 100% 0 0) } 51% { clip-path: inset(0 0 0 0) } 51.01%,100% { clip-path: inset(0 100% 0 0) } }
@keyframes type4 { 0%,56% { clip-path: inset(0 100% 0 0) } 69% { clip-path: inset(0 0 0 0) } 69.01%,100% { clip-path: inset(0 100% 0 0) } }
@keyframes type5 { 0%,74% { clip-path: inset(0 100% 0 0) } 87% { clip-path: inset(0 0 0 0) } 87.01%,100% { clip-path: inset(0 100% 0 0) } }

/* The commands underneath, lighting up as each one lands. */
.build-cmds { display: flex; gap: 13px; margin-left: 16px; flex-wrap: wrap; }
.build-cmds b { font: 400 0.94em/1 var(--mono); color: var(--quiet); opacity: 0.45; }
.slide[data-here] .build-cmds b { animation: bfStep 18s linear infinite; }
.slide[data-here] .build-cmds b:nth-child(1) { animation-name: on1; }
.slide[data-here] .build-cmds b:nth-child(2) { animation-name: on2; }
.slide[data-here] .build-cmds b:nth-child(3) { animation-name: on3; }
.slide[data-here] .build-cmds b:nth-child(4) { animation-name: on4; }
.slide[data-here] .build-cmds b:nth-child(5) { animation-name: on5; }
@keyframes on1 { 0%,15% { opacity: 0.45 } 18%,97% { opacity: 1; color: var(--cyan) } 100% { opacity: 0.45 } }
@keyframes on2 { 0%,33% { opacity: 0.45 } 36%,97% { opacity: 1; color: var(--cyan) } 100% { opacity: 0.45 } }
@keyframes on3 { 0%,51% { opacity: 0.45 } 54%,97% { opacity: 1; color: var(--violet) } 100% { opacity: 0.45 } }
@keyframes on4 { 0%,69% { opacity: 0.45 } 72%,97% { opacity: 1; color: var(--violet) } 100% { opacity: 0.45 } }
@keyframes on5 { 0%,87% { opacity: 0.45 } 90%,97% { opacity: 1; color: var(--magenta) } 100% { opacity: 0.45 } }

/* The slide is a centred column and the figure is the item in it, so the
   figure shrinks to its content for the same reason the stack did. */
.s-build .build { align-self: stretch; width: 100%; }
.s-build figcaption { display: flex; align-items: center; flex-wrap: wrap; justify-content: center; }

/* Nothing moving: the finished slide, which is the one frame that says the most
   on its own, every command lit, and no typing. */
@media (prefers-reduced-motion: reduce) {
  .slide[data-here] .build-stack .bf + .bf { animation: none; }
  .build-stack .bf:nth-child(6) { opacity: 1; }
  .build-type { display: none; }
  .slide[data-here] .build-cmds b { animation: none; opacity: 1; }
}
