/* One warm neutral, one accent. The only colour on the page is what the machines draw. */
:root {
  --bg: #0c0b0a;
  --bg-2: #131211;
  --line: #262320;
  --line-2: #3a3631;
  --ink: #ece7dd;
  --dim: #8d877c;
  --faint: #5f5a52;
  --accent: #ffb454;
  --radius: 10px;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, "Helvetica Neue", Arial, sans-serif;
}
* { box-sizing: border-box; }
body {
  overflow-x: clip;
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
}
::selection { background: var(--accent); color: var(--bg); }
a { color: inherit; }
code { font-family: var(--mono); }
button { font: inherit; color: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 2px; }
.skip { position: absolute; left: -9999px; }
.skip:focus { left: 1rem; top: 1rem; background: var(--bg-2); padding: .6rem 1rem; z-index: 10; }
/* `display` on a class outranks the browser's own [hidden] rule, so say it here. */
[hidden] { display: none !important; }

/* Small print: labels, the GitHub link, the footer. */
.gh, .tabs button, .copy, body > footer {
  font-family: var(--mono); font-size: .75rem; letter-spacing: .04em;
}

/* Docs and GitHub, top right */
.corner {
  position: absolute; top: clamp(1.1rem, 2.5vw, 1.8rem); right: clamp(1rem, 4vw, 2.5rem); z-index: 5;
  display: flex; align-items: center; gap: 1.4rem;
}
.gh {
  display: inline-flex; align-items: center; gap: .5rem;
  color: var(--dim); text-decoration: none;
  transition: color .15s ease;
}
.gh:hover { color: var(--ink); }
.gh b { font-weight: 400; color: var(--accent); font-variant-numeric: tabular-nums; }

main { max-width: 1140px; margin: 0 auto; padding: 0 clamp(1rem, 4vw, 2.5rem); }

/* Title: the name, and what it is said once, quietly */
.title { padding: clamp(3.6rem, 7vw, 5rem) 0 clamp(1.4rem, 3vw, 2.2rem); }
.title h1 {
  margin: 0; font-size: clamp(1.7rem, 3.6vw, 2.5rem); font-weight: 650;
  line-height: 1.1; letter-spacing: -.035em;
}
.title p { margin: .45rem 0 0; color: var(--dim); font-size: .95rem; }

/* The slideshow: a strip of machines as wide as the window. The one in the middle is
   running and takes input; its neighbours wait, dimmed, at the edges. */
.stage {
  position: relative;
  --w-desk: min(1080px, 84vw, (100vh - 21rem) * 1.6);
  --h-desk: calc(var(--w-desk) * .625);
  --h-phone: var(--h-desk);
  /* The room a whole slide has. A team of machines is allowed to run wider than one of
     them, but not so much wider that it dwarfs its neighbours on the strip; app.js reads
     these three back in pixels and sizes every tile inside them. */
  --w-group: min(94vw, calc(var(--w-desk) * 1.34));
  --gut: clamp(.45rem, 1.1vw, 1rem);
  margin-inline: calc(50% - 50vw); overflow: hidden;
  padding: 1rem 0 0;
}
.track {
  position: relative; column-gap: clamp(.75rem, 3vw, 2.5rem);
  margin-bottom: 5.5rem;   /* room for the shadow, which `overflow` would otherwise cut */
  transition: height .55s cubic-bezier(.22, .8, .24, 1);
}
.slide {
  position: absolute; left: 50%; top: 50%;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--gut);
  opacity: .28; cursor: pointer;
  transform: translate(calc(-50% + var(--x, 0px)), -50%) scale(.94);
  transition: opacity .55s ease, transform .55s cubic-bezier(.22, .8, .24, 1);
}
.slide:hover { opacity: .5; }
.slide.active { opacity: 1; cursor: default; transform: translate(calc(-50% + var(--x, 0px)), -50%); }
.slide.far { visibility: hidden; transition: none; }
.track.still, .track.still .slide { transition: none; }

/* A shallow arc. The strip lies on the face of a very wide cylinder: a slide that stands
   off centre is set back into the page (`--z`) and turned away from the viewer (`--rot`),
   both written by app.js beside `--x`, so the running machine is square on at the front of
   the curve and its neighbours lean away at the edges. The setting-back is what keeps the
   turn from swinging them out of the window: on a curve they sit a little closer together
   than they would in a straight line, exactly as much as the turn takes off their width.
   It rides the transform transition above, so a slide turns as it slides. A browser with
   no 3D transforms keeps the flat row, and so does anyone who asked for less motion. */
@supports (perspective: 1px) {
  .track { perspective: clamp(700px, 130vw, 2200px); }
  .slide {
    transform: translate(calc(-50% + var(--x, 0px)), -50%) translateZ(var(--z, 0px))
               rotateY(var(--rot, 0deg)) scale(.94);
  }
  .slide.active {
    transform: translate(calc(-50% + var(--x, 0px)), -50%) translateZ(var(--z, 0px))
               rotateY(var(--rot, 0deg));
  }
}
/* Equal share: a slide is one rectangle cut in two, and each half cut in two again, down
   to the machines — `.row` for a cut side by side, `.col` for one above the other — so the
   halves fill what they were given and the composition has no holes in it. app.js writes
   the heights, the corner radii and the shadow depth, since it is the one that knows how
   the slide was cut and how much room there is; the sizes here are what one machine on its
   own gets, and what a slide falls back to before the first measurement. */
.slide .row, .slide .col { display: flex; align-items: center; justify-content: center; gap: var(--gut); }
.slide .col { flex-direction: column; }
.tile {
  position: relative; flex: none; height: var(--h-desk); aspect-ratio: 1280 / 800;
  border-radius: var(--radius); overflow: hidden; background: #000;
  box-shadow: 0 0 0 1px var(--line), 0 1px 0 1px rgba(255, 255, 255, .03);
  transition: box-shadow .55s ease;
}
.tile.phone { height: var(--h-phone); border-radius: clamp(16px, 2.2vw, 28px); }
/* A machine whose still has not been rendered yet: an empty screen, not a broken image. */
.tile.blank { background: repeating-linear-gradient(-45deg, #131211 0 10px, #0f0e0e 10px 20px); }
.tile.blank img { display: none; }
/* One shadow rule for every tile, lifted by its own size: app.js writes `--lift` from the
   height it gave the tile, so seven machines on one slide do not each drop the shadow of a
   laptop, and one machine on its own still does. */
.slide.active .tile {
  box-shadow: 0 0 0 1px var(--line), 0 1px 0 1px rgba(255, 255, 255, .03),
              0 var(--lift, 40px) calc(var(--lift, 40px) * 2.25) calc(var(--lift, 40px) * -.5) rgba(0, 0, 0, .8);
}
.tile img, .tile canvas { width: 100%; height: 100%; display: block; }
.live-canvas { outline: none; touch-action: none; cursor: default; }
.slide.active .tile:has(.live-canvas:focus-visible) {
  box-shadow: 0 0 0 1px var(--accent),
              0 var(--lift, 40px) calc(var(--lift, 40px) * 2.25) calc(var(--lift, 40px) * -.5) rgba(0, 0, 0, .8);
}

/* WHILE THE SIMULATOR IS COMING DOWN. The still darkened, a spinner, and the download's
   own percentage — nothing else. `data-boot` on the document says which of the four it is,
   because there is one download for the whole page: `ask` is a Save-Data browser waiting
   to be told to go and shows the one control that tells it; `load` is the download, and
   the label is empty whenever the bytes cannot honestly be counted (see live.js), leaving
   the spinner on its own; `work` is a world being built out of what came down. A machine
   that is running has no overlay at all, and neither has a slide nobody is looking at. */
.boot {
  position: absolute; inset: 0; display: none; place-content: center; justify-items: center;
  gap: .55rem; padding: .5rem; text-align: center;
  background: rgba(8, 7, 6, .62);
}
html[data-boot] .slide.active .tile:not(.live) > .boot { display: grid; }
.spin {
  width: 26px; aspect-ratio: 1; border-radius: 50%;
  border: 2px solid rgba(236, 231, 221, .2); border-top-color: var(--accent);
  animation: spin .85s linear infinite;
}
@keyframes spin { to { transform: rotate(1turn); } }
.pct {
  margin: 0; color: var(--ink); font-family: var(--mono); font-size: .78rem;
  letter-spacing: .04em; font-variant-numeric: tabular-nums; white-space: nowrap;
  text-shadow: 0 1px 12px rgba(0, 0, 0, .9);
}
.pct:empty { display: none; }
html[data-boot="ask"] .spin, html[data-boot="ask"] .pct { display: none; }
.get {
  display: none; place-items: center; width: 2.6rem; height: 2.6rem; padding: 0;
  border: 1px solid var(--line-2); border-radius: 50%; background: rgba(12, 11, 10, .75);
  color: var(--ink); cursor: pointer; transition: border-color .15s ease, color .15s ease;
}
html[data-boot="ask"] .get { display: grid; }
.get:hover { border-color: var(--accent); color: var(--accent); }
/* In a team every tile is small, and one the size of a stamp has room for the spinner and
   nothing beside it. app.js marks both, since it is the one that knows how a tile came out. */
.tile.small .spin { width: 18px; border-width: 1.5px; }
.tile.small .pct { font-size: .62rem; }
.tile.small .get { width: 2rem; height: 2rem; }
.tile.tight .pct { display: none; }
.tile.tight .spin { width: 13px; border-width: 1.5px; }
.tile.tight .get { width: 1.5rem; height: 1.5rem; }

/* ONE MACHINE, THE WHOLE SCREEN. The control sits in the tile's top right and is there
   only while that machine is live — over a still there is nothing to be full screen with,
   and `display: none` keeps it out of the tab order as well as out of the picture. */
.grow {
  position: absolute; top: .4rem; right: .4rem; z-index: 3;
  display: none; place-items: center; width: 1.9rem; height: 1.9rem; padding: 0;
  border: 0; border-radius: 7px; background: rgba(8, 7, 6, .55); color: rgba(236, 231, 221, .8);
  cursor: pointer; opacity: .55; transition: opacity .15s ease, color .15s ease, background .15s ease;
}
.tile.live > .grow { display: grid; }
.tile:hover > .grow, .grow:hover, .grow:focus-visible { opacity: 1; }
.grow:hover { background: rgba(8, 7, 6, .8); color: var(--accent); }
.grow svg + svg, .tile.full > .grow svg:first-child { display: none; }
.tile.full > .grow svg + svg { display: block; }
/* A phone's corners are rounded far harder than a desktop's, so the control comes in off
   them rather than sitting in the curve. */
.tile.phone > .grow { top: .75rem; right: .75rem; }
.tile.small > .grow { width: 1.5rem; height: 1.5rem; border-radius: 5px; top: .3rem; right: .3rem; }
.tile.phone.small > .grow { top: .5rem; right: .5rem; }
.tile.tight > .grow { width: 1.2rem; height: 1.2rem; border-radius: 4px; top: .2rem; right: .2rem; }
.tile.small > .grow svg { width: 12px; height: 12px; }
.tile.tight > .grow svg { width: 10px; height: 10px; }

/* The tile that has the screen, whether the Fullscreen API put it in the top layer or
   `.fs-host` is holding it instead. Both give the tile the whole viewport; the canvas then
   takes as much of it as ITS OWN SHAPE allows, so the machine is never stretched and the
   box the pointer is measured against is the box the picture is drawn in. app.js writes
   `--ar`, the machine's width over its height. */
.fs-host { position: fixed; inset: 0; z-index: 60; background: #000; display: grid; place-items: center; }
.tile.full {
  width: 100vw !important; height: 100vh !important; aspect-ratio: auto !important;
  border-radius: 0 !important; box-shadow: none !important;
  display: grid; place-items: center; background: #000;
}
.tile.full > canvas, .tile.full > img {
  width: min(100vw, calc(100vh * var(--ar, 1.6)));
  height: min(100vh, calc(100vw / var(--ar, 1.6)));
}
.tile.full::backdrop { background: #000; }
.tile.full > .grow { width: 2.4rem; height: 2.4rem; top: .9rem; right: .9rem; }
.tile.full > .grow svg { width: 17px; height: 17px; }

/* What the machines on this slide are doing, and the two arrows. The caption is the
   indicator: it says where in the cast this is, so there is no strip of dots to read. */
.pager {
  display: flex; align-items: center; justify-content: center; gap: .4rem;
  margin-top: -2.6rem; padding: 0 1rem; position: relative;
}
.pager > button {
  flex: none; padding: .5rem .7rem; border: 0; background: none; cursor: pointer;
  color: var(--dim); font-size: 1rem; line-height: 1; transition: color .15s ease;
}
.pager > button:hover { color: var(--ink); }
/* The caption is two things, and they are not the same kind of thing. The title is what
   this slide IS, and it is the dominant element on the line: set in the page's own face,
   a half again the size of anything beside it, in the full ink. The summary is the
   technical detail under that name — the part designator, the file, the ticket, the
   branch — so it stays in the machines' monospace, two thirds the title's size and a shade
   dimmer, and reads as the caption's small print rather than as a second title. The count
   is fainter still. */
.caption {
  flex: 0 1 76rem; min-width: 0; margin: 0;
  /* The room the longest caption in the cast needs at this width, kept back whatever is
     on screen, so the page under the strip does not step up and down as one caption runs
     a word longer than the last. Wide enough and that is a title's line and a summary's;
     the ladder below is where the summary starts wrapping under it, measured over all
     forty-four scenes rather than guessed at. */
  min-height: 2.95rem;
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center;
  column-gap: .8rem; row-gap: .1rem; text-align: center;
  font-family: var(--sans); line-height: 1.35;
}
#caption-title {
  font-size: clamp(1.02rem, 1.35vw, 1.2rem); font-weight: 600; letter-spacing: -.015em;
  color: var(--ink);
}
#caption-summary {
  min-width: 0; font-family: var(--mono); font-size: .72rem; line-height: 1.5;
  letter-spacing: .01em; color: var(--dim);
}
#caption-where {
  font-family: var(--mono); font-size: .68rem; letter-spacing: .04em; color: var(--faint);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* Where the summary no longer sits beside the title, and then where it wraps again. */
@media (max-width: 919px) { .caption { min-height: 4.8rem; } }
.stage noscript img { display: block; width: var(--w-desk); height: auto; margin: 0 auto; border-radius: var(--radius); }

/* Install and snippet: one object, with the language as its tabs */
.install { padding: clamp(3.5rem, 9vw, 6.5rem) 0 clamp(3rem, 7vw, 5rem); }
.codebox { border: 1px solid var(--line); border-radius: var(--radius); background: var(--bg-2); }
.bar {
  display: flex; align-items: stretch; justify-content: space-between;
  padding: 0 .5rem 0 .6rem; border-bottom: 1px solid var(--line);
}
.tabs { display: flex; }
.tabs button, .copy {
  padding: .85rem .8rem; border: 0; background: none; cursor: pointer;
  color: var(--dim); text-transform: uppercase;
  transition: color .15s ease;
}
.tabs button:hover, .copy:hover { color: var(--ink); }
.tabs button[aria-selected="true"] { color: var(--ink); box-shadow: inset 0 -1px 0 var(--ink); margin-bottom: -1px; }
.tabs button:focus-visible, .copy:focus-visible { outline-offset: -4px; }
.codebox pre { margin: 0; padding: 1.4rem 1.4rem; overflow-x: auto; }
.codebox code { color: #c9c3b7; line-height: 1.7; font-size: .85rem; }
#install {
  padding-block: 1rem; border-bottom: 1px solid var(--line); cursor: copy;
}
#install code { color: var(--ink); }
#install code::before { content: "$ "; color: var(--faint); }
#install code::after { content: attr(data-said); color: var(--accent); margin-left: 1.5em; }

/* Syntax colours, written into the markup rather than fetched from a highlighter. */
.k { color: #a69f92; font-style: italic; }   /* keywords */
.s { color: #a9b88f; }                       /* strings */
.n { color: var(--accent); }                 /* numbers */
.f { color: var(--ink); }                    /* calls and macros */
.t { color: #e0c9a0; }                       /* types */
.c { color: var(--faint); font-style: italic; }

body > footer {
  max-width: 1140px; margin: 0 auto; padding: 1.4rem clamp(1rem, 4vw, 2.5rem) 3rem;
  color: var(--faint); text-transform: uppercase;
}
body > footer span { display: block; padding-top: 1.4rem; border-top: 1px solid var(--line); }

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
  /* Nothing spins for a reader who asked for less motion, so the ring stays as a plain
     mark that something is on its way and the percentage beside it is what moves. */
  .spin { border-color: rgba(255, 180, 84, .38); }
  /* Flat again, to the pixel: no depth to the strip, and the plain transform back, so
     what is drawn is the straight row above and not a curve flattened to nothing. */
  .track { perspective: none; }
  .slide { transform: translate(calc(-50% + var(--x, 0px)), -50%) scale(.94) !important; }
  .slide.active { transform: translate(calc(-50% + var(--x, 0px)), -50%) !important; }
}
@media (max-width: 720px) {
  .stage { --w-desk: 82vw; --h-phone: min(640px, 150vw, 100vh - 12rem); }
  .track { margin-bottom: 4.5rem; }
  /* The caption wraps here rather than being cut, so it is given the lines to wrap into
     and the arrows sit at the top of them, beside its first line. */
  .pager { margin-top: -1.6rem; align-items: flex-start; padding-inline: .5rem; }
  .pager > button { padding: .1rem .5rem; }
  /* The title holds its size here — it is the thing being read — and the summary wraps
     under it into the lines kept back for it. */
  .caption { min-height: 6rem; column-gap: .55rem; }
  #caption-summary { font-size: .68rem; }
  .gh span { display: none; }
  .codebox pre { padding-inline: 1rem; }
  .codebox code { font-size: .78rem; }
}

/* A title that wraps too, and then one that wraps twice. */
@media (max-width: 479px) { .caption { min-height: 7rem; } }
@media (max-width: 359px) { .caption { min-height: 8.4rem; } }
