/* Exhibits: the shared frame first, then per-beat open behaviour.

   Two tiers of text, and only two. Walking near shows the beat's line in the
   caption at the top of the room; pressing E shows the object opened *and* the
   story beside it. Never put the story behind a second interaction.

   Layout follows the Figma comp. An exhibit is an anchor at its world x. Closed,
   the object is centred on it. Walking up to a beat puts it in front of you.
   Open, the object and its story are centred on the viewport as one group, so
   the object slides left by half of what the story takes up and the character
   ends up standing between the two. */

/* These three are the comp's, and they're what a 1512-wide window gets. On a
   narrower one the first two are rewritten in pixels by `relayout` in
   src/main.js, off `storyFor` in the engine's config — the gaps close before
   any artwork is scaled, because they're empty space and nobody misses them.
   The values here are the ones that hold at 1270 and above, and the ones that
   apply for the moment before the first layout runs. */
:root {
  /* The column of words. `--story-w` is what a beat's story lays out at, which
     is the column unless the beat wraps it in something bigger. */
  --story-col: 400px;
  --story-w: var(--story-col);
  --story-gap: 120px;
}

.ex {
  position: absolute;
  top: var(--eye);
  display: flex;
  justify-content: center;
  transform: translate(-50%, -50%);
  transition: transform .6s cubic-bezier(.22, 1, .36, 1);
  /* How far this beat's object is scaled to fit the window. 1 at 1512x982 and
     on anything roomier; set per beat by `relayout`, because it depends on how
     big that object is and only the ones that don't fit should shrink.

     `--art-w`/`--art-h` are the object's natural size, measured once at boot;
     `--art-box` is the width the beat lays out in, which is the scaled size or
     the floor that keeps the story off the character, whichever is larger.
     All three are zero until the first layout, and the room is hidden until
     the first layout. */
  --art-scale: 1;
  --art-w: 0px;
  --art-h: 0px;
  --art-box: 0px;
}
/* `telling` is the story arriving, `open` is the object opening, and on a
   keyboard they are the same instant. They come apart on a touch screen, where
   the beat opens itself and nobody asked for it: the words go first and the
   object follows a second later, or you watch the thing you walked up to leave
   before you've read why it mattered. See `openBeat` in src/main.js.

   Everything about where the beat *stands* belongs to `telling`, because the
   story is what moves it: the object slides left by half of what the words take
   up, and it should do that as they arrive rather than a second afterwards. */
.ex.open:not(.always-open),
.ex.telling:not(.always-open) {
  transform: translate(calc(-50% - (var(--story-w) + var(--story-gap)) / 2), -50%);
}

/* The object, in two boxes rather than one, because scaling it has to satisfy
   two things that pull against each other.

   `.art` is the *layout* box, and it's the scaled size. Everything outside the
   object measures off it: the story hangs on its right edge, and the shift a
   beat makes on opening is worked out from its width. That has to shrink or
   nothing else follows.

   `.art-in` is the *drawing* box, and it stays the natural size with a
   transform on it. That's what keeps the inside of a beat honest: every
   hand-placed pixel in the walk — the kitchen's cards on their photograph, the
   RuneScape plaque, the flanking logos, the readouts, `#now`'s notice hanging
   off `100%` — is measured against a box that hasn't moved. Scaling the layout
   box instead would resolve all of those against a box that had, and every
   percentage in the walk would quietly mean something else.

   A separate element rather than a transform on `.art` itself for the same
   reason it isn't on `.art`'s children: half the beats already put a transform
   on their outermost element, and an id beats a class every time. */
.art {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  /* Not the drawn size: it also carries the floor that keeps the story off the
     character. See `artBox` in the engine's config. */
  width: var(--art-box);
  height: calc(var(--art-h) * var(--art-scale));
  /* Nothing, for a beat that hangs at eye level, which is most of them. A beat
     that stands on the floor is moved down to it from here, outside the scaling,
     so that shrinking it takes the top down rather than lifting the feet. */
  transform: translateY(var(--art-drop, 0px));
}
.art-in {
  position: relative;
  flex: none;
  display: flex;
  justify-content: center;
  align-items: center;
  width: var(--art-w);
  height: var(--art-h);
  transform: scale(var(--art-scale));
  transform-origin: center;
}
.art svg { display: block; }

/* Tier 2, the story, on the same line as the object it belongs to. */
.story {
  position: absolute;
  left: calc(100% + var(--story-gap));
  top: 50%;
  width: var(--story-w);
  display: flex;
  flex-direction: column;
  /* No gap. The spacing between these is per-pair and set below, because a flex
     gap can't tell an eyebrow from a paragraph break: it added 12px on top of
     the blank line between paragraphs, which came out at a line and a half and
     read as double-spaced. */
  opacity: 0;
  transform: translateY(-50%) translateX(-18px);
  transition: opacity .5s ease, transform .6s cubic-bezier(.22, 1, .36, 1);
  pointer-events: none;
  /* Capped at the room between the caption and the floor, and scrolls inside it
     rather than sliding up under the caption. The words come down with the room
     (`--type`, below) but only so far, so a long story on a short window still
     runs out of somewhere to be.

     `--story-max-h` is that room, written by `relayout`. It's taller than any
     story in the walk on any window over about 720px, so this does nothing at
     all on most screens — including the one the design was drawn at. */
  max-height: var(--story-max-h, none);
  overflow-y: auto;
  /* `none` rather than `contain`: neither of them lets the scroll chain out to
     the page, but `none` also refuses the bounce at either end. Nothing in this
     site is a panel with an elastic edge, and on a phone that bounce is the
     gesture a browser reads as "the page moved", which is what sets its own
     toolbars sliding — and a toolbar sliding takes the fixed chrome with it. */
  overscroll-behavior: none;
  scrollbar-width: thin;
  scrollbar-color: var(--hair) transparent;
  /* The room owns the horizontal drag and this owns the vertical one, and both
     are true on the same words: `#stage` takes `touch-action: none` and this
     asks *only* the vertical axis back. So a thumb moving down the story
     scrolls it, and a thumb moving across the story walks — the story isn't a
     hole in the room you have to drag around. */
  touch-action: pan-y;
}

/* A story with more to it than fits says so, at whichever end the rest of it is.
   There's no scrollbar to see on a phone and nothing else in this design is a
   panel with an edge, so without this the words simply stop — which reads as
   the end of the story rather than as the end of the box.

   Both ends, because only fading the bottom is worse than fading neither: the
   words appear out of a soft edge going down and are guillotined going up, and
   the mismatch draws attention to the box you were trying not to draw.

   The two lengths are written by `markScroll` in src/main.js, because whether
   the words overflow — and which way — is a fact about the rendered text that
   only the page knows. Both are 0 when there's nothing hidden, and a gradient
   that goes opaque at 0 is the same as no gradient at all, so this costs
   nothing on the stories that fit. */
.story {
  --fade-up: 0px;
  --fade-down: 0px;
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--fade-up),
    #000 calc(100% - var(--fade-down)), transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0, #000 var(--fade-up),
    #000 calc(100% - var(--fade-down)), transparent 100%);
}

/* A beat's own prev/next, for touch.

   Either side of the object rather than under it. Under it is where they
   naturally want to go and it's the one place they can't be: the character
   stands between the object and the story, so a centred row lands on his shoes.
   Out at the edges they flank the thing they step, and they're where a thumb
   already is.

   Level with the middle of the object, because that's the thing they work on.
   They sat a quarter of its height lower, which put them nearer the thumb and
   made them look like they belonged to the gap under the picture rather than to
   the picture.

   Shown only under `body.touch` — see styles/base.css, which is where the
   question of what the visitor is holding is answered. */
.steps {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(var(--vw) - 2 * var(--gutter));
  max-width: 420px;
  transform: translate(-50%, -50%);
  justify-content: space-between;
  z-index: 25;
  /* Only the two circles catch a finger; the space between them is the room's,
     so a drag that starts there still walks. */
  pointer-events: none;
}
.steps .step { pointer-events: auto; }
.steps .step {
  display: grid;
  place-items: center;
  width: 44px;              /* a thumb's worth, which is the whole point */
  height: 44px;
  border: 1px solid var(--hair);
  border-radius: 50%;
  background: var(--paper);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
  /* The browser must not decide this is the start of a pan. */
  touch-action: none;
  -webkit-tap-highlight-color: transparent;
}
.steps .step:active { background: var(--hair); }
/* One drawing, used twice. The chevron points back the way it was drawn. */
.steps .step[data-step="next"] .chevron { transform: scaleX(-1); }
.ex.open .story,
.ex.telling .story {
  opacity: 1;
  transform: translateY(-50%);
  pointer-events: auto;
}

/* The comp's sizes, times how much the room has had to come down — `--type`,
   written by `relayout`, and exactly 1 on any window the design fits on. The
   objects scale to fit the window, and the words scale with them: a full-size
   column beside an object at half size doesn't read as generous, it reads as
   two things drawn at two different scales. There's a floor on it in the
   engine's config, so this bottoms out at a size you can still read. */
.story h3 {
  font-size: calc(16px * var(--type, 1));
  font-weight: 500;
  /* Stepped back rather than greyed, so it stays the same ink as the story. */
  opacity: .3;
  /* Off the comp: the eyebrow's baseline sits 12px above the story's first. */
  margin-bottom: calc(12px * var(--type, 1));
}
/* The beat's line, as the title of the story rather than as a caption over it.
   Only where the caption isn't saying it: on a keyboard it belongs at the top
   of the room, where you read it on the way in, before you press anything. */
.story .title { display: none; }
body.touch .story .title,
body.reading .story .title {
  display: block;
  font-size: calc(19px * var(--type, 1));
  font-weight: 500;
  line-height: 1.25;
  /* The same distance off the paragraph below it as the eyebrow is off this. */
  margin-bottom: calc(14px * var(--type, 1));
}
/* A size down in the compact room. 19px is the wide room's title standing in
   for a caption at the top of the screen; down here it's the first line of a
   sheet of words on a phone, with the object above it and the chrome below, and
   at 19 it took a third line as often as not and pushed the story it was
   introducing off the bottom. */
body.touch.compact .story .title { font-size: calc(17px * var(--type, 1)); }

/* The eyebrow breaks into even lines rather than a full one and an orphan.

   These are job titles and course titles — "Graphic Media Degree, University
   Centre Somerset" — two phrases with a comma between them, and left to itself
   the line fills up and drops "Somerset" underneath on its own. Balancing puts
   the break where the sense already is, at the comma, and it does the same for
   the ones that are still fitting today on a phone narrower than the one this
   was noticed on, rather than waiting to be told about each of them.

   The eyebrow only. The title under it is a sentence rather than a label, and a
   sentence should fill its measure and wrap where it runs out — balancing it
   sets the beat's own line in a narrower column than the story it introduces,
   which reads as two different widths of text rather than one.

   And only in the compact room. The wide room's column is the comp's and its
   breaks are the ones the walk was drawn with. */
body.compact:not(.reading) .story h3 { text-wrap: balance; }
/* On the page it's a heading in a document rather than a title over an object,
   and a document that goes eyebrow, heading, heading reads as a mistake. */
body.reading .story h3 { margin-bottom: calc(6px * var(--type, 1)); }
/* The eyebrow stops being the thing the story opens with, so the gap under it
   belongs to the pair rather than to the paragraph that used to follow. */
body.touch .story h3 { margin-bottom: calc(6px * var(--type, 1)); }

.story p { font-size: calc(16px * var(--type, 1)); line-height: 1.4; }
/* Exactly one blank line between paragraphs — 1.4em is the line-height, so this
   is the same space a return would give you, and nothing more. */
.story p + p { margin-top: 1.4em; }
.story .media { margin-bottom: 12px; }
.story a { color: inherit; text-decoration: none; border-bottom: 1px solid var(--hair); }
.story a:hover { border-bottom-color: currentColor; }

/* Notes to self in the copy file. These should not survive to launch. */
.story .todo {
  color: var(--mid);
  font-size: calc(14px * var(--type, 1));
  border-left: 2px solid var(--hair);
  padding-left: 10px;
}

.media {
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #101216;
  display: grid;
  place-items: center;
  color: #6f757f;
  font-size: 12px;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-align: center;
  padding: 12px;
}

/* The first and last beats: no object, so nothing hangs and nothing opens.
   Their box is genuinely nothing — `artBox` gives a beat with no object no
   width at all, so there's no minimum here to undo.

   The last beat's words sit to the left of the character and read left to right.
   Ranged right pulled the two lines apart from each other in the comp. */
.ex.always-open .story {
  left: auto;
  right: 88px;
  width: 300px;
  text-align: left;
  opacity: 1;
  transform: translateY(-50%);
}
.ex.always-open .story p { font-size: calc(20px * var(--type, 1)); }
.ex.always-open .story .todo { font-size: 14px; border: 0; padding: 0; }

/* The first beat's greeting is rendered on the character instead (ui/intro.js).
   Its keys stay here, in the room, so they slide away as he runs off. */
.ex.is-intro .story { visibility: hidden; }

.ex.always-open .controls {
  position: absolute;
  /* Beside him, so it's measured in his units — see `--char-scale` in base.css. */
  left: calc(376.5px * var(--char-scale, 1));
  top: 50%;
  transform: translate(-50%, calc(-50% - 42px));
  display: flex;
  gap: 48px;
  align-items: center;
  white-space: nowrap;
  font-size: 16px;
  /* Stepped back to begin with, and faded out by how far he has walked. */
  opacity: calc(.5 * var(--fade, 1));
}

/* The compact room --------------------------------------------------

   The story stops being a column beside the object and becomes a sheet across
   the bottom of the room. The object keeps the middle to itself, the character
   stands in front of it, and the sheet slides up over the floor when you open
   the beat.

   The sheet is `position: absolute` rather than `fixed` because `#world` is
   transformed, and a transform makes its element the containing block for any
   fixed descendant — `fixed` in here would anchor to the world, which slides.
   So it's placed against the beat instead, which comes to the same thing: a
   beat is only ever open while you're standing at it, and standing at a beat
   puts it in the middle of the screen. */
/* All of this is `:not(.reading)`, because the two are answers to different
   questions and only one of them applies at a time. Compact is the walk in a
   narrow window; reading is no walk at all. Without it the compact rules win on
   the page — every beat carries `.open` there, which makes `body.compact
   .ex.open .story` more specific than anything the reading route says — and the
   words end up dragged half their own width off the left of the screen. */
body.compact:not(.reading) .ex.open:not(.always-open),
body.compact:not(.reading) .ex.telling:not(.always-open) { transform: translate(-50%, -50%); }

/* Just under his feet, not pinned to the bottom of the window. It was a sheet
   across the bottom edge with a rule along the top of it, and that was wrong
   twice over: nothing else in this site is a panel, and a column of words at
   the far end of the screen from the thing it's about reads as a separate
   document rather than as the caption to what you're looking at. Here it's what
   every other beat's story is — words in the room, near the object, on the
   paper — and the only difference is that it's under him instead of beside him,
   because there's no beside at this width. */
body.compact:not(.reading) .story {
  /* Down from the middle of the beat to the floor line, and a breath past it. */
  top: calc(50% + (var(--ground) - var(--eye)) + 26px);
  left: 50%;
  right: auto;
  /* As wide as the window allows, up to a line you can still read the end of.
     It used to take the whole width, which on an 800px window is a 760px line
     of 13px type — about 120 characters, or twice what anybody tracks back
     from comfortably. The cap is the same measure the wide room's column is,
     so a paragraph reads the same either side of the breakpoint. */
  width: min(calc(var(--vw) - 2 * var(--gutter)), calc(460px * var(--type, 1)));
  max-width: none;
  /* Down to just above the row of chrome in the corner. The floor coming up
     (`body.compact` in styles/base.css) is what makes this a reading area
     rather than a slot: 340px on a 390x844 phone rather than 272. */
  max-height: calc(var(--vh) - var(--ground) - 26px - 56px);
  padding: 0;
  transform: translateX(-50%) translateY(10px);
  transition: opacity .35s ease, transform .45s cubic-bezier(.22, 1, .36, 1);
}
body.compact:not(.reading) .ex.open .story,
body.compact:not(.reading) .ex.telling .story { transform: translateX(-50%); }

/* The stone tablet stops being a tablet down here. It's a picture painted
   behind a column that scrolls, so on a window where the words don't fit inside
   it they slide off the bottom of the stone and carry on over the floor.

   What it gets instead is a panel that *can* stretch: the lobby darkened and
   blurred behind the words, rather than the stone stretched to fit them. The
   drop shadow alone wasn't enough — this is white text on a photograph of a
   castle at dusk, and the parts of it that land on stonework are as light as
   the words are. A wash of the room's own dark, blurred so the picture still
   reads through it as the place you're standing in.

   Softly rounded rather than square-cornered, because a hard rectangle of dark
   over a photograph reads as a dialog that has opened on top of the room, and
   this is meant to be part of it.

   `backdrop-filter` is the nicety, not the fix: the wash underneath it is what
   makes the words legible, so a browser without the filter loses some of the
   depth and none of the reading. */
body.compact:not(.reading) #runescape .story {
  background: rgba(8, 9, 11, .58);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  padding: 16px 18px;
  /* The mask that fades the scroll would cut the panel off square with the
     text; letting it run a little past both ends keeps the wash reading as
     light rather than as an edge. */
  border-radius: 14px;
}

/* The last beat's words go back to being ordinary: there's no room beside him
   for a column at this width. */
body.compact:not(.reading) .ex.always-open .story {
  top: calc(50% + (var(--ground) - var(--eye)) + 26px);
  right: auto;
  text-align: center;
}
body.compact:not(.reading) .ex.always-open .story p { font-size: calc(18px * var(--type, 1)); }

/* The row of keycaps that stands beside him on the first beat comes down under
   him instead — there's nothing like enough room at his shoulder at this width.
   Moved rather than hidden: a narrow window on a laptop still walks with A and
   D, and this is where it's told so. The one place they *are* hidden is a
   device with no keys to press, which is a question about the pointer and is
   answered in styles/base.css. */
body.compact:not(.reading) .ex.always-open .controls {
  left: 50%;
  top: calc(50% + (var(--ground) - var(--eye)) + 26px);
  transform: translate(-50%, 0);
  gap: 28px;
  font-size: 14px;
}

/* The reading route -------------------------------------------------

   A beat stops being an anchor in a world and becomes a section of a page: the
   object, then the words, one after another down a column. Every beat carries
   `.open`, because opening is what shows the story and there's nothing here to
   press. What that costs is the beats that hide something until they're opened
   — the fork's two ends, the Kandinsky tracing, the kitchen's cards — showing
   it from the start, which is the right way round when nobody is going to
   interact: this route is for reading, so it opens the book. */
body.reading .ex {
  position: static;
  transform: none !important;
  display: block;
  width: min(var(--reading-w), calc(var(--vw) - 2 * var(--gutter)));
  transition: none;
}

body.reading .art {
  /* Nothing stands on a floor in a page. */
  transform: none;
  margin: 0 auto 28px;
}

body.reading .ex .story {
  position: static;
  /* Narrower than the column the objects get. 680px of 16px type is nearly a
     hundred characters a line; this is about seventy, which is what the walk's
     own column is and what a paragraph wants either way. */
  width: 100%;
  max-width: 560px;
  margin: 0 auto;
  max-height: none;
  overflow: visible;
  opacity: 1;
  transform: none;
  pointer-events: auto;
  padding: 0;
  background: none;
  /* The stone tablet and the black room go with the walk; on the page every
     story is the same words on the same paper. */
  color: inherit;
  text-shadow: none;
}
body.reading .ex .story h3 { color: inherit; opacity: .3; }

/* The one story with a picture behind it. Its id beats the rule above, so it
   has to be said again here: on the page it's words like all the others. A
   stone tablet is the beat's costume, and this route isn't the beat — the room
   it belonged to has gone too. */
body.reading #runescape .story {
  background: none;
  padding: 0;
  color: inherit;
  text-shadow: none;
}
body.reading #runescape .story h3 { color: inherit; opacity: .3; }

/* Its own line above the story, rather than tucked beside the character. */
body.reading .ex.always-open .story { text-align: left; }
body.reading .ex.always-open .controls { display: none; }
body.reading .ex.is-intro .story { visibility: visible; }

/* Per-beat ----------------------------------------------------------- */

/* two: a photograph of him as a child, and nothing that moves. Opening the beat
   brings the story and nothing else. */
#two .photo { display: block; border-radius: 16px; }

/* kandinsky: the painting falls back and the geometry traced over it comes up */
#kandinsky .painting { position: relative; }
#kandinsky .scan {
  display: block;
  width: 100%; height: 100%;
  object-fit: cover;
  transition: opacity .6s ease;
}
#kandinsky .geo {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* Delay on the open rule only. On the base rule it applies to closing too,
     and every beat that did that felt slow to walk out of. */
  transition: opacity .3s ease;
}
#kandinsky.open .scan { opacity: .2; }
#kandinsky.open .geo { opacity: 1; transition: opacity .5s ease .25s; }

/* ipod: the device closed, the launch clip open. Both live in a box the size of
   the larger, centred on each other, so swapping one for the other doesn't move
   the beat. */
#ipod .device { position: relative; display: grid; place-items: center; }
#ipod .shut { position: relative; overflow: hidden; }
#ipod .shut img { position: absolute; top: 0; height: 100%; max-width: none; }
#ipod .clip {
  position: absolute;
  display: block;
  max-width: none;
  opacity: 0;
  transition: opacity .3s ease;
}
#ipod.open .shut { opacity: 0; transition: opacity .3s ease; }
#ipod.open .clip { opacity: 1; transition: opacity .5s ease .15s; }

/* runescape: the login screen becomes the signature, and the room becomes the
   place he was looking at while he drew it (`room: stone`, in base.css).

   The monitor is a cutout with a black panel where the screen is. The screen
   content sits over it in a box measured off the comp, in src/exhibits/
   runescape.js, so the two images swap without touching the monitor. */
#runescape .monitor { position: relative; }
#runescape .set { display: block; width: 100%; height: 100%; }
#runescape .screen {
  position: absolute;
  overflow: hidden;
  display: grid;
  place-items: center;
  /* Behind both images: the login shot is a photograph of a lit room and the
     signature is a cutout, so what's underneath it has to be a switched-off
     screen rather than the page. */
  background: #000;
}
/* `cover` rather than `fill` because the comp crops the login shot by a pixel
   or two rather than squashing it. */
#runescape .login {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The monitor and its screen leave together, and the signature's plaque arrives
   behind them: two moves, not a crossfade in place, so it reads as the set
   being struck rather than one picture becoming another. */
#runescape .set,
#runescape .screen { transition: opacity .45s ease; }
#runescape.open .set,
#runescape.open .screen { opacity: 0; }

#runescape .plaque {
  position: absolute;
  left: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  border-radius: 24px;
  background: rgba(0, 0, 0, .88);
  opacity: 0;
  /* The delay lives on the open rule only. On the base rule it applies both
     ways, and walking away leaves the beat hanging about behind you. */
  transition: opacity .3s ease;
}
#runescape.open .plaque { opacity: 1; transition: opacity .5s ease .25s; }

/* The story sits where every other beat's story sits, on the middle of the
   object it belongs to. It used to carry an offset, because the monitor stood
   on the floor and the plaque hung on the old screen's centre line; both of
   those are gone (see src/exhibits/runescape.js) and so is the offset. */
#runescape .sig { display: block; }

/* The story is a stone tablet here rather than words in the air, because by
   this point the room is the game and text floating in it would be the one
   thing that isn't. Measured off the comp: a 558x328 tablet with the same
   400px column of words every other beat has, inset 79 and 63.

   Widening --story-w to the tablet rather than the column is what keeps it the
   right shape; where it sits is the rule below, not the usual arithmetic. */
/* The tablet is the column of words plus the stone either side of it, so the
   words keep the same measure every other beat's story has. On a window without
   room for the comp's own numbers the stone gets thinner rather than the words
   getting narrower. Matches `story` in src/exhibits/runescape.js, which is how
   the room knows how much width this beat is taking. */
#runescape { --story-w: var(--story-col); }

/* The one beat whose open position is placed rather than worked out. Everywhere
   else the object and its story are centred on the viewport as a group, and the
   shift falls out of the story's width and gap. Here the comp hangs the tablet
   and the signature off the two plinths in the background plate — the plaque
   lands on the left one, the tablet on the right — and those aren't symmetrical
   about the middle of the room, so the group isn't either. Off the comp: the
   plaque's centre 249px left of him, the tablet's left edge 24px right of him,
   which is the 480px object box plus the 33px gap above.

   Only where the comp's own sizes still hold. Below that these are numbers
   measured against a room that no longer exists, and the beat is better off
   with the arrangement every other one uses, which is worked out rather than
   placed and so fits whatever room there is. The tight gap and the full stone
   inset go with the placed position and come back with it — the media query is
   below, after the rules it has to beat. */
#runescape .story {
  /* The thinner stone, for a window without room for the comp's. */
  padding: 32px 28px;
  /* Stretched, not cropped. It's a 558x328 tablet with a carved border, and the
     box it has to fill is whatever the words come out at — `cover` kept the
     picture's proportions and threw the edges away instead, which cut the
     border off the sides and left the panel looking broken in half. Stone is
     the one texture that doesn't mind being pulled about. */
  background: center / 100% 100% no-repeat url('../art/runescape-panel.png');
  color: #fff;
  /* The game's own drop shadow. It's what makes the words read as part of the
     tablet rather than as HTML laid over a picture of one. */
  text-shadow: 1px 1px 0 #000;
}
/* Yellow, at full strength: in RuneScape this line is a heading, not an aside,
   so the stepped-back treatment the other beats use would be wrong here. */
#runescape .story h3 { color: #ff0; opacity: 1; }

/* The comp's own numbers, at the widths the comp's own numbers fit. See the
   note above `#runescape .story`; this is last so it beats the rules it
   replaces rather than relying on being more specific than them. */
@media (min-width: 1270px) {
  #runescape {
    --story-w: calc(var(--story-col) + 158px);
    --story-gap: 33px;
  }
  #runescape .story { padding: 63px 79px; }
  #runescape.open { transform: translate(calc(-50% - 249px), -50%); }
}

/* Picked up, and the room goes dark. The lift is on the device alone: the clip
   is a screen you watch, not an object being handled. */
#ipod .shut { transition: transform .6s cubic-bezier(.22, 1, .36, 1), opacity .3s ease; }
#ipod.open .shut { transform: translateY(-20px) rotate(-4deg) scale(1.04); }

/* fork: closed, one black arrow that splits and tells you nothing. Open, the
   route he didn't take drops back and both ends grow the thing they led to.

   The greying is opacity rather than a grey fill: the drawings are solid black,
   and 20% of black on the paper is the #ccc the comp uses. That also means it
   stays right in a room that isn't white. */
#fork .fork { position: relative; }
#fork .fork > div { position: absolute; }
#fork .fork svg { display: block; width: 100%; height: 100%; }
#fork .other { transition: opacity .6s ease; }
#fork.open .other { opacity: .2; }

/* The ends arrive after the branch has faded, so you read the fork first and
   what it meant second. */
#fork .mark { opacity: 0; transition: opacity .3s ease; }
#fork.open .mark { opacity: 1; transition: opacity .45s ease .3s; }

/* typeface: arrow keys change the weight, and it stops being right */
#typeface .readout {
  position: absolute;
  left: 0; right: 0;
  bottom: -40px;
  text-align: center;
  font-size: 14px;
  color: var(--mid);
  opacity: 0;
  transition: opacity .4s;
}
#typeface.open .readout { opacity: 1; }

/* magazine: the degree, summed up in eighty pages. Two pages wide at all
   times, so opening it never reflows the beat. Closed, the whole object is
   shifted half a page left to put the cover on the anchor, and opening just
   takes that shift away. The only 3D in the room is the leaf swinging on the
   spine.

   Opening is two moves and they do not overlap:

     1. the cover turns, where the magazine already is;
     2. only then does the beat slide over and the story arrive.

   Running them together is what read as janky. The object was travelling
   before the cover had visibly moved, and the spine it was turning about was
   sliding sideways underneath it at the same time. */
#magazine {
  --turn: .72s;     /* one leaf swinging. Riffling shortens this. */
  --settle: .72s;   /* how long the beat waits before taking its place */
}

#magazine .magazine {
  position: relative;
  width: calc(var(--page-w) * 2);
  height: var(--page-h);
  perspective: 1800px;
  /* Closed, the object sits half a page left so the cover lands on the anchor.
     That shift is undone in move two, not during the turn, which is what keeps
     the spine still while the cover swings about it. */
  transform: translateX(calc(var(--page-w) / -2));
  transition: transform .6s cubic-bezier(.22, 1, .36, 1);
}

/* Move two, once the cover is down: the beat takes its place beside the story,
   and the story arrives with it. Delays live on the `.open` rules, so closing
   is immediate. Walking away should never wait for anything. */
#magazine.open,
#magazine.open .magazine,
#magazine.open .story { transition-delay: var(--settle); }
#magazine.open .magazine { transform: none; }

/* Shutting it runs the same two moves the other way: the pages come over, and
   only once they have does the beat go back to where it hangs. Without this the
   object is already travelling while the magazine is still open. */
#magazine.closing,
#magazine.closing .magazine,
#magazine.closing .story { transition-delay: var(--turn); }

/* Both waits go in the compact room.

   The stagger exists because in the wide room opening this beat is two moves —
   the cover turns where the magazine already is, and only then does the object
   travel across to make room for the story beside it. Running them together was
   the janky version: the spine slid sideways underneath the leaf swinging about
   it.

   Down here there is no second move. The story goes underneath rather than
   beside, so the beat never travels, and all `--settle` does is hold everything
   still for three-quarters of a second after the cover has landed. On touch,
   where the beat opened itself and nobody pressed anything, that reads as a
   magazine that hasn't heard you — which is exactly what the wait was invented
   to prevent. */
body.compact:not(.reading) #magazine.open,
body.compact:not(.reading) #magazine.open .magazine,
body.compact:not(.reading) #magazine.open .story,
body.compact:not(.reading) #magazine.closing,
body.compact:not(.reading) #magazine.closing .magazine,
body.compact:not(.reading) #magazine.closing .story { transition-delay: 0s; }

/* The leaves live in the right-hand page and swing about their left edge, which
   is the spine. */
#magazine .leaves {
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--page-w);
  height: var(--page-h);
  transform-style: preserve-3d;
}
#magazine .leaf {
  position: absolute;
  inset: 0;
  transform-origin: left center;
  transform-style: preserve-3d;
  transition: transform var(--turn) cubic-bezier(.4, .1, .3, 1);
}
#magazine .leaf.turned { transform: rotateY(-180deg); }

#magazine .face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  background: #fff;
}
#magazine .face.back { transform: rotateY(180deg); }

/* A leaf too far from the current spread to be seen: it's behind the topmost
   one on its side of the stack, and every leaf occupies the same rectangle, so
   there is nothing of it to look at. Taken out of the picture entirely so the
   compositor stops keeping a layer for each of its two faces — see `show` in
   src/exhibits/magazine.js for why that mattered. `visibility` rather than
   `display`, so the leaf keeps its place in the stack and nothing reflows. */
#magazine .leaf.far { visibility: hidden; }
#magazine .face img { display: block; width: 100%; height: 100%; }

/* Held down, it riffles rather than turning one page every three-quarters of a
   second, forty leaves is half a minute of holding a key otherwise. This lands
   on the leaves rather than on `--turn`, so riffling can't leak into how long
   the magazine takes to shut afterwards. */
#magazine .magazine.riffling .leaf { transition-duration: .24s; }
/* Snapping back to the cover on close shouldn't flap forty leaves on the way. */
#magazine .magazine.resetting .leaf { transition: none; }

/* The body of the magazine, behind the leaves. It carries the shadow that lifts
   the object off a white room, and it's a single element rather than a shadow
   on every page so the silhouette is always exactly right: one page wide while
   the magazine is shut, two once it's open. */
#magazine .paper {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  right: 0;
  background: #fff;
  box-shadow: 0 22px 55px rgba(0, 0, 0, .13), 0 3px 10px rgba(0, 0, 0, .05);
  /* Part of move one: the footprint opens out to the left as the cover comes
     over, right edge and spine both staying put. Held back a quarter of the
     turn so the cover leads and they land together. */
  transition: left calc(var(--turn) * .75) cubic-bezier(.4, .1, .3, 1);
}
#magazine.open .paper {
  left: 0;
  transition-delay: calc(var(--turn) * .25);
}

/* The shading that makes two flat images read as a bound object rather than a
   pair of pictures — and it belongs to the pages, not to the gap between them.

   It used to be one gradient pinned to the middle of the object, which is what
   a gutter looks like only while nothing is moving. The moment a leaf swung, the
   shadow stayed behind: the page turned out from underneath its own shading and
   the whole thing stopped reading as paper.

   So every face carries its own, darkest at the edge nearest the binding and
   gone within a sixth of the page. Each one is drawn on the leaf's own left
   edge, which is the edge it pivots about — and the backs are mirrored by their
   `rotateY(180deg)`, so the same rule puts the shading on the spine side of the
   left-hand page and the spine side of the right-hand one. It turns with the
   paper because it is on the paper. */
#magazine .face::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(90deg,
    rgba(0, 0, 0, .17) 0%, rgba(0, 0, 0, .09) 4%,
    rgba(0, 0, 0, .03) 9%, rgba(0, 0, 0, 0) 16%);
}

/* The seam itself: a hairline where the two pages meet, so the middle isn't a
   hard join between two whites. Nothing like the old band — that was doing the
   shading's job as well, which is why losing it doesn't leave a gap. */
#magazine .spine {
  position: absolute;
  left: 50%;
  top: 0;
  z-index: 2;
  width: 1px;
  height: 100%;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, .12);
  opacity: 0;
  transition: opacity .5s ease;
  pointer-events: none;
}
#magazine.open .spine { opacity: 1; }

/* Where you are in eighty pages. Same readout as the typeface beat. */
#magazine .readout {
  position: absolute;
  left: 0; right: 0;
  bottom: -40px;
  text-align: center;
  font-size: 14px;
  color: var(--mid);
  opacity: 0;
  transition: opacity .4s;
}
#magazine.open .readout { opacity: 1; }

/* Reduced motion: the page still goes over, it just doesn't take time doing it.
   Fading between spreads instead would leave the left-hand page nowhere to come
   from. The turn is what puts it there. */
@media (prefers-reduced-motion: reduce) {
  #magazine .magazine, #magazine .leaf { transition: none; }
}

/* The job beats: a carousel of work on the arrow keys. One rule for all three,
   because they are the same beat with a different list in it.

   Every piece stacks in one stage the size of the biggest thing the comp draws,
   and is `contain`ed rather than cropped. A portrait screen recording fills the
   height, a landscape clip fills the width, and neither moves the beat when you
   step from one to the other. */
/* The cover and the carousel share one box the size of the larger, centred on
   each other, so opening the beat swaps what's in it without moving it. */
.job { position: relative; }
.job > .cover,
.job > .reel {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
.job > .cover {
  position: absolute;
  transition: opacity .4s ease;
}
.job > .cover > img { display: block; width: 100%; height: 100%; object-fit: cover; }
.job > .cover .mark { position: absolute; object-fit: fill; }
.ex.open .job > .cover { opacity: 0; }
/* The work arrives after the cover has gone, not across it — but only on the
   way in. With the delay on the base rule it applied to closing too, so walking
   away left the work sitting on top of the cover coming back. */
.job > .reel { opacity: 0; transition: opacity .25s ease; }
.ex.open .job > .reel { opacity: 1; transition: opacity .45s ease .2s; }
.ex.open .job > .cover { opacity: 0; transition: opacity .3s ease; }

.reel { position: relative; }
/* The picture is centred on the stage, and the caption hangs off the bottom of
   it without being part of what gets centred. It used to be a flex column with
   the two of them centred together, which pushed the picture up by half the
   caption — about 21px — and left every carousel sitting that much higher than
   the story beside it. The comp lines the two up; so does this. */
.reel .piece {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  /* Off the pointer and out of the tree until it's the one you're looking at. */
  visibility: hidden;
  transition: opacity .35s ease, visibility .35s;
}
.reel .piece.on { opacity: 1; visibility: visible; }

/* The picture's own box, the shape the build measured it at. Everything that
   has to line up with the edge of the picture rather than the edge of the
   stage — the shadow, the caption — hangs off this. */
.reel .frame {
  position: relative;
  flex: none;
  overflow: hidden;
  border-radius: 4px;
  /* Sequences change the box as they change frame, and it eases rather than
     jumping — the pictures are different shapes and the reshape is part of the
     cross-fade. */
  transition: width .5s ease, height .5s ease;
  box-shadow: 0 14px 34px rgba(0, 0, 0, .16), 0 2px 6px rgba(0, 0, 0, .06);
}
.reel .frame-in {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .5s ease;
}
.reel .frame-in.on { opacity: 1; }
.reel .frame-in > * {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* The frame is already the picture's shape, so cover and contain agree —
     cover just guarantees no hairline of background at a rounded corner. */
  object-fit: cover;
}

/* A page render is framed as a screen and panned down it, rather than shrunk
   until the copy on it is a grey texture. `--pan` is how many screens tall the
   render is, worked out in the module from its real shape, so the pan always
   ends at the bottom of the page and never on white. */
.reel .piece[data-kind="scroll"] .frame-in > * {
  height: calc(var(--pan) * 100%);
  inset: 0 0 auto 0;
  object-fit: cover;
  object-position: top center;
}
.reel .piece[data-kind="scroll"].on .frame-in > * {
  animation: work-pan calc(var(--pan) * 7s) ease-in-out infinite alternate;
}
/* A percentage in `translateY` is a share of the element's *own* height, not
   the frame's, and the element here is the whole page render. What wants
   travelling is the render minus one frame, which as a share of the render is
   `1 - 1/--pan`. Using `1 - --pan` panned nearly three times too far and left
   the bottom half of the frame on blank paper. */
@keyframes work-pan {
  from { transform: translateY(0); }
  to { transform: translateY(calc((1 / var(--pan) - 1) * 100%)); }
}

/* 24px under the picture, off the comp. `--frame-h` is the showing frame's own
   height, set by the module — a sequence changes shape as it changes frame, and
   the caption has to follow it down rather than hang under the tallest one. */
.reel figcaption {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(50% + var(--frame-h, 0px) / 2 + 24px);
  font-size: 16px;
  color: var(--mid);
  text-align: center;
  transition: top .5s ease;
}

/* Amdaris and Pearson, on the beat where he worked for both. Two positions and
   one move between them, all measured off the comp against the 546px box the
   headset stands in.

   Closed, they hang either side of the headset on its own centre line, like the
   two labels beside a case. Open, they come together above the work: the shift
   the beat makes on opening is `(--story-w + --story-gap) / 2`, so adding it
   back is what lands the pair on the character rather than on the object.

   `left` is where the mark's *centre* goes, which is what makes the two states
   one transition instead of an anchor swapping sides halfway through. */
#accelerator .flank {
  position: absolute;
  top: 50%;
  height: auto;
  transform: translate(-50%, -50%);
  transition: left .6s cubic-bezier(.22, 1, .36, 1),
              top .6s cubic-bezier(.22, 1, .36, 1),
              width .6s cubic-bezier(.22, 1, .36, 1);
}
#accelerator .flank-left { left: -105px; width: 161px; }
#accelerator .flank-right { left: 669px; width: 222px; }

/* The open position reaches out of the object to land on the character, so it's
   the one measurement on the beat that isn't in the object's own coordinates —
   and the object is inside `.art-in`, which is scaled. Dividing by the scale
   converts the shift back into the box's own units, so the pair still lands on
   him on a window where the headset has been scaled down. */
#accelerator.open .flank { top: 34px; }
#accelerator.open .flank-left {
  left: calc(50% + (var(--story-w) + var(--story-gap)) / 2 / var(--art-scale) - 87px);
  width: 97px;
}
#accelerator.open .flank-right {
  left: calc(50% + (var(--story-w) + var(--story-gap)) / 2 / var(--art-scale) + 60px);
  width: 149px;
}

/* And in the compact room they land back where they started, because the shift
   they were compensating for isn't happening: `body.compact .ex.open` cancels
   it, since the story goes underneath rather than beside. Left in, that
   correction was still adding half a story column divided by the scale — about
   352px inside a 546px box on a phone — so both marks flew off the headset and
   off the screen entirely. */
/* Two companies, and at this width they read better as a pair than as two
   things pushed to opposite edges of a box.

   `left` is the *centre* of each mark, not its left edge — `.flank` carries a
   `translate(-50%, -50%)`. So: the box is 546 wide and centred on 273, the two
   marks are 161 and 222, and 60 between them makes the pair 443 across. That
   starts at 51.5, which puts the first centre on 132 and the second on 383.

   Both open and closed, `top` included. The compact room never shifts the beat
   sideways, so there is nothing for them to compensate for and no reason to
   move at all — and on touch a beat is open from the moment you stop, so any
   difference between the two states is a jump you watch rather than a
   transition you cause. They were still starting at `top: 50%`, on the headset,
   and sliding up to 34 as the beat opened under them. */
body.compact:not(.reading) #accelerator .flank { top: 34px; }
body.compact:not(.reading) #accelerator .flank-left { left: 132px; width: 161px; }
body.compact:not(.reading) #accelerator .flank-right { left: 383px; width: 222px; }

@media (prefers-reduced-motion: reduce) {
  #accelerator .flank { transition-duration: .01s; }
}

/* What a beat isn't showing you. Set like a work caption rather than like the
   story, because that's what it's standing in for: it belongs to the object's
   half of the screen, not to the words. On a beat with no object at all it sits
   in the middle of that half; on `now` it hangs under the object, below. */
.art .notice {
  max-width: 330px;
  text-align: center;
  font-size: 16px;
  line-height: 1.45;
  color: var(--mid);
}

/* now: the site he's building, on a loop, because it's the one object in the
   walk that hasn't happened yet — everything else is a photograph of something
   already over, and a still of this would file it with them.

   Two sizes, one box. The comp draws it at 720 wide with the room to itself and
   556 once the story is beside it, so the box is the 556 and the extra size is
   a transform: laid out at the larger it would carry 80px of empty box either
   side all the way through the open state, and the beat's shift is worked out
   from the box. */
#now .site {
  display: block;
  transform: scale(1.295);
  transition: transform .6s cubic-bezier(.22, 1, .36, 1);
}
#now.open .site { transform: none; }

/* Under the object rather than beside the words: it's about the work, and the
   work is what's missing from this half of the screen. It arrives with the
   story — closed, nobody has asked, and an apology hanging in an empty room is
   a strange thing to walk past. */
#now .notice {
  position: absolute;
  top: calc(100% + 32px);
  left: 50%;
  width: 352px;
  max-width: none;
  transform: translateX(-50%);
  opacity: 0;
  transition: opacity .3s ease;
}
#now.open .notice { opacity: 1; transition: opacity .5s ease .25s; }

@media (prefers-reduced-motion: reduce) {
  #now .site { transition-duration: .01s; }
}

/* Who he was working for. Sizes are per-mark rather than one height for all:
   the comp scales each to look the same weight, and a droplet and a wordmark
   don't do that at the same number. */
/* 29px to the eyebrow below, off the comp, times how much the marks have had to
   come down — `--logo`, which is 1 everywhere the comp's own room exists.

   The comp draws these beside a column of words on a 1512px window, where a
   51px mark is a modest thing in the corner of the story. In the compact room
   the same 51px sits at the top of a sheet of words on a 390px screen, above a
   title and an eyebrow and three paragraphs, and it stops being a credit and
   starts being the headline. It's a mark saying who he worked for; it should be
   the smallest thing in the story rather than the largest.

   One multiplier rather than a second set of heights, because the sizes are
   per-mark on purpose: a droplet and a wordmark don't read at the same weight
   at the same height, and that relationship should survive being shrunk. The
   marks that hang *above* the object rather than in the story — Amdaris and
   Pearson on the accelerator — aren't these and don't move; see `.flank`. */
:root { --logo: 1; }
.story .logos {
  display: flex;
  align-items: center;
  gap: calc(22px * var(--logo));
  margin-bottom: calc(29px * var(--logo));
}
.story .logos img { display: block; width: auto; }
#betting .logos img:first-child { height: calc(51px * var(--logo)); }
#betting .logos img:last-child { height: calc(38px * var(--logo)); }
#startup .logos img { height: calc(63px * var(--logo)); }
#accelerator .logos img { height: calc(30px * var(--logo)); }
#now .logos img { height: calc(30px * var(--logo)); }

body.compact:not(.reading) { --logo: .74; }

/* kitchen: the dishes, fanned over a photograph of the kitchen itself. One card
   at a time on the arrow keys; the other two stay in the fan behind it, because
   a stack you can see the depth of reads as a stack.

   These cards are the only colour in the room besides the character. That's
   deliberate and it's from the comp, a kitchen is the one place in this story
   that wasn't quiet. */
/* A kitchen is a room, not a thing hung on a wall, so it stands on the floor
   rather than floating at eye level like the other objects. The story stays
   where every other story is: only the object moves, which is why the shift is
   on the inner box and not on `.art`. */
/* Centred in its box like every other object. What puts it on the floor is
   `stands` in src/exhibits/kitchen.js, which moves the whole box down from
   outside the scaling — see `artDrop` in the engine's config. It used to be a
   transform in here, which scaled along with the photograph and took it off the
   floor on every window except the one it was drawn for. */
#kitchen .kitchen {
  position: relative;
  width: 580px;
  height: 426px;
}
/* Exported at the size it is drawn, so it needs no cropping here. */
#kitchen .kitchen .room { display: block; width: 100%; height: 100%; }

/* The fan. Places measured off the comp rather than computed, so three cards
   land where they were drawn instead of approximately where a formula puts
   them. Positions are relative to the photograph's box; the cards deliberately
   overhang its top edge, which is what stops the beat reading as a picture with
   things neatly inside it. */
/* The cards are what interacting gets you, so walking past is just the kitchen.
   They come in on E and go again when you leave. The container only handles
   whether they're there at all; the arriving is on the cards themselves, so the
   fan and the entrance don't have to share one transform. */
#kitchen .cards { position: absolute; inset: 0; pointer-events: none; }
#kitchen .slot {
  position: absolute;
  /* All three exactly 229x274, which is what the comp draws and what makes them
     read as a deck. This used to be a `min-height`, so a card grew to fit its
     note and the three came out three different heights — a stack of cards that
     were never the same card. The note is set to fit inside the height instead;
     see `.card p` below. */
  width: 229px;
  height: 274px;
  /* Full strength, all three. They used to be dimmed except the one you were
     on, and against the comp that read as miserable — these are the only colour
     in the walk and holding two of them back wasted it. The card you're on is
     picked out by lifting instead, below. */
  transition: transform .5s cubic-bezier(.22, 1, .36, 1), box-shadow .4s ease;
}
/* The heights are the comp's: the fan sits 74px higher than it used to and
   leans harder, so the cards clear the top of the photograph rather than
   resting inside it. That's what stops the beat reading as a picture with
   things neatly arranged on it.

   The spacing across is not the comp's. It drew them 235px apart, which on a
   229px card leaves the third one clear of the second by a few pixels — three
   cards standing side by side, and nothing about that says there's an order to
   go through. They're 129px apart now: every card covers 100px of the one
   under it, so the deck reads as a deck and ←/→ reads as the way through it.
   Anchored on the middle card, so the fan stays centred on the photograph.

   `top` and `left` place a card unturned — CSS rotates about the centre — so
   these are the corners the comp would have if you straightened everything up.
   Relative to the photograph's box, which is why two of them are negative. */
#kitchen .slot-0 { left: 46px;  top: -26px;  z-index: 3; transform: rotate(-19.6deg); }
#kitchen .slot-1 { left: 175px; top: -127px; z-index: 2; transform: rotate(0deg); }
#kitchen .slot-2 { left: 304px; top: -102px; z-index: 1; transform: rotate(13.5deg); }

/* Whichever you're on comes forward, straightens up a little and takes full
   strength. The other two stay in the fan, a stack you can see the depth of is
   what makes ←/→ an obvious thing to try. */
/* Straightening up and lifting, but no scale: these are printed cards and a
   card doesn't get bigger when you pick it up, it comes off the pile. Scaling
   also dragged the type with it, so the same sentence was set at three sizes
   depending on which card you were on. The shadow does the rest of the work. */
/* The stacking order is set by the module, on every step, because it depends on
   which card you're looking at — see `show` in src/exhibits/kitchen.js. The
   numbers on the three slots below are only the starting fan. */
#kitchen .slot.on .card { box-shadow: 0 26px 50px rgba(0, 0, 0, .22); }
#kitchen .slot-0.on { transform: rotate(-14deg) translateY(-10px); }
#kitchen .slot-1.on { transform: rotate(0deg) translateY(-10px); }
#kitchen .slot-2.on { transform: rotate(9.5deg) translateY(-10px); }

/* They pop in one after another rather than fading in as a block: three cards
   put down on a pass, not a panel appearing. The overshoot in the curve is the
   pop; the delays are what stops it reading as one movement.

   The entrance lives here rather than on `.slot` because the slot's transform
   is already carrying the fan, and one element can only have one of those. */
#kitchen .card {
  width: 100%;
  height: 100%;
  /* The comp clips, and so does this. It shouldn't ever have to: the note is
     set small enough that the longest of them lands inside the card. This is
     here so that a note somebody adds later spoils its own card rather than the
     shape of the deck. */
  overflow: hidden;
  border-radius: 16px;
  padding: 24px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  background: var(--placeholder);
  box-shadow: 0 12px 30px rgba(0, 0, 0, .12);
  opacity: 0;
  transform: scale(.82);
  transition: transform .42s cubic-bezier(.2, 1.35, .4, 1), opacity .26s ease;
}
#kitchen.open .card { opacity: 1; transform: none; }
#kitchen.open .slot-0 .card { transition-delay: .05s; }
#kitchen.open .slot-1 .card { transition-delay: .13s; }
#kitchen.open .slot-2 .card { transition-delay: .21s; }
/* Straight off the comp. These were eyeballed before and every one was a
   little muddy; they are the loudest thing in the walk and being a shade out
   showed. */
#kitchen .slot-0 .card { background: #51ff9d; }
#kitchen .slot-1 .card { background: #56cfff; }
#kitchen .slot-2 .card { background: #ffc551; }

/* The dish. A fixed height rather than an aspect ratio: three photographs of
   different shapes have to line their titles up, and the crop is the comp's. */
#kitchen .card img {
  display: block;
  width: 100%;
  height: 127px;
  object-fit: cover;
  /* 23px between the picture and the title in the comp, less the flex gap. */
  margin-bottom: 15px;
}
/* Both off the comp, and both load-bearing: 20/24 puts the title's baseline
   where the comp has it, and 12/1.2 is what gets the longest of the three notes
   into three lines and inside the card. */
#kitchen .card h4 { font-size: 20px; line-height: 1.2; font-weight: 500; color: #101214; }
#kitchen .card p { font-size: 12px; line-height: 1.2; color: rgba(16, 18, 20, .5); }

@media (prefers-reduced-motion: reduce) {
  #kitchen .slot { transition-duration: .01s; }
  /* They still arrive, they just don't perform it: no pop, no stagger. */
  #kitchen .card { transition-duration: .01s; transform: none; }
  #kitchen.open .card { transition-delay: 0s; }
}
