/* The room. White, empty, and mostly nothing. The restraint is the design, so
   think twice before adding anything that moves on its own.

   Type, colour and spacing follow the Figma comp. Innovator Grotesk is the
   designed face; the woff2 files aren't in the repo yet, so the stack falls
   back to Helvetica Neue until they are. */

:root {
  --ink: #000;
  --mid: #9a9a9a;
  --hair: #e6e7ea;
  --paper: #fff;
  --placeholder: #e3ebee;

  /* The height and width the whole room is worked out from, written by
     `relayout` in src/main.js and measured off `#vh` rather than assumed.

     Everything below that used to say `vh` says a share of this instead. On a
     phone `vh` is the viewport with the browser's toolbar *hidden* while
     `innerHeight` is the one you can see, so the stylesheet and the engine were
     laying out two different rooms about seventy pixels apart — the floor, the
     eye line and every object's scale among them. One number, measured once,
     and they can't disagree.

     The fallbacks are what the room is before the first layout, and `svh` for
     the same reason the probe uses it. */
  --vh: 100vh;
  --vh: 100svh;
  --vw: 100vw;

  /* The floor line: where the character's feet land, as a share of the window.
     835 of 982 in the comp. */
  --ground: calc(.85 * var(--vh));
  /* Where objects and their stories are centred vertically. */
  --eye: calc(.52 * var(--vh));
  /* The caption's place at the top of the room, and the space it always takes
     whether or not it's saying anything. Both of these are read back by the
     engine — `ROOM` in src/engine/config.js — when it works out how big an
     object is allowed to be, so the two have to agree. */
  --caption-top: calc(.16 * var(--vh));
  --caption-h: 78px;
  /* The column the reading route lays out in. Matches READING_W in
     src/engine/config.js, which is what scales the objects to fit it. */
  --reading-w: 680px;

  /* How long the room takes to become somewhere else, and back. Everything that
     changes in that moment is on this one clock: the page's own colour, the ink
     and every piece of chrome painted in it, the backdrop, and the floor.

     They used to run at .7s, .7s, .8s and .4s, and four clocks on one moment is
     what read as a flash — the floor arriving a third of a second before the
     wall behind it, with the ink somewhere in between. One number, one easing,
     one move. */
  --room-fade: .7s;
  --room-ease: ease;

  /* How far off the middle of the screen the character stands. Zero everywhere
     the object has room to be beside him; see `body.touch.compact` below, where
     it doesn't. The camera is untouched by this — it still centres the *world*
     on him, so standing at a beat still means the beat is in front of you. All
     that moves is where he's drawn. */
  --char-x: 0px;

  /* What a phone's bars are told to be, which is deliberately not `--paper`.

     Three rooms change the paper; only one of them should change the bars. The
     blackout genuinely is the page going dark and the bars should follow it —
     they always have, and it is the one beat that has never misbehaved. The
     lobby is a photograph, and a photograph has no single colour for a bar to
     be: its own top is light stonework and its own bottom is dark floor, which
     is how one screen ended up with two different greys. The grid room is a pale
     wireframe on white and is already white.

     So the bars are white everywhere except the two dark rooms, which take the
     paper's own near-black. The lobby's colour was never the thing being
     protected — what mattered was that it stopped sticking — and a dark bar
     over a castle at dusk is the better of the two answers anyway. */
  --bar: #fff;

  --gutter: 32px;
  --font: "Innovator Grotesk", "Helvetica Neue", Helvetica, Inter, Arial, sans-serif;
}

* { box-sizing: border-box; margin: 0; padding: 0; }
html, body { height: 100%; overflow: hidden; }

/* The paper is painted on the root, not only on the body.

   A phone's status bar and toolbar are drawn over the page, and what shows
   through them is whatever paints the canvas. A background on `body` is
   normally propagated to the canvas for you — but only while the root has none
   of its own, and only as a colour: it doesn't reach the strips behind the
   bars in every state, which is why this came out right in some places and grey
   in others. Painting the root paints the canvas outright, and `enterRoom` puts
   the room's class here as well as on the body so the dark rooms come with it. */
html {
  background: var(--paper);
  transition: background var(--room-fade) var(--room-ease);
  /* What kind of page this is, which is a separate question from what colour it
     is and is answered separately. A phone in dark mode paints its own furniture
     dark over a page it assumes is dark too, and a white room underneath doesn't
     change its mind — `theme-color` sets the tint, this sets which end of the
     scale the browser thinks it's on. Without it the bars stay the system's grey
     over a white room, which is the strip of somebody else's colour.

     And it never changes. `color-scheme` on the *root* is the one thing on this
     page a browser reads as being about itself rather than about the document:
     it re-renders its own furniture when it changes, which on a desktop is the
     toolbar flashing as you walk into the lobby and out of the blackout, and on
     a phone is worse — the bars go to the system's neutral grey on the way into
     the RuneScape room and stay there for the rest of the visit, with
     `theme-color` no longer getting a say. Those are exactly the two rooms that
     used to flip this, and the grid room, which changes everything else about
     the page and doesn't touch this, never had either symptom.

     A dark room is this page turning its lights off, not this page becoming a
     dark-mode document. What colour the bars are is `theme-color`'s to say, and
     it says it on its own. */
  color-scheme: light;
}
html.room-dark,
html.room-stone { background: #08090b; }

/* The scheme the room's own widgets are drawn for, which is a different
   question from the one above and is why this is on the body rather than on the
   root. Here it reaches the scrollbar down the side of a story and the fields in
   the contact sheet — anything the browser draws *inside* the page — and reaches
   nothing the browser draws around it. */
body.room-dark,
body.room-stone { color-scheme: dark; }

/* The rooms the bars follow. See `--bar` at the top of this file.

   The lobby is in this list for a different reason from the blackout. Its own
   colour was never the point — what mattered was that it stopped sticking — but
   the band the bottom bar reads shows through around the floating toolbar, and
   a white strip along the foot of a castle at dusk is worse than a dark one. */
body.room-dark,
body.room-stone { --bar: #08090b; }

/* Two hairlines, one at the very top of the viewport and one at the very
   bottom, painted the room's own colour. They are what a phone's bars read.

   Safari derives the tint of its status bar and its toolbar from the page
   itself, and what it looks at is the `background-color` of the body *or* of a
   `position: fixed` element touching the exact edge of the viewport — within
   three or four pixels of it. This room puts a fixed element on both of those
   edges and gives it no background at all: `#stage` is the whole walk and it is
   transparent. So the answer Safari got was "nothing", and a browser that finds
   nothing falls back to its own system grey — which is the grey belonging to no
   beat in the walk, arriving on some loads and not others depending on what
   else happened to paint there first.

   So: give it something to find. Four pixels is what it reads and four pixels
   is what this is, at each end, carrying `--bar` — see the top of this file for
   why that is its own colour rather than `--paper`. It changes as a plain
   `background-color` rather than through opacity, so it repaints.

   Deliberately not transitioned. The room may take three-quarters of a second
   to change colour; this must not, because a sample taken part-way through a
   fade is a blend of two rooms and that is the other half of the same bug. Four
   pixels at the extreme edge of the glass, under the bar, is not somewhere a
   hard change can be seen.

   Only on a touch screen. A desktop has no glass over the page, and four pixels
   of paper across the top of the lobby photograph there would be a cost paid
   for nothing. */
#tint { display: none; }
body.touch #tint::before,
body.touch #tint::after {
  content: '';
  position: fixed;
  left: 0;
  right: 0;
  height: 4px;
  z-index: 70;
  pointer-events: none;
  background-color: var(--bar);
}
body.touch #tint::before { top: 0; }

/* The bottom one covers *both* candidate edges, because there are two of them
   and choosing between them is what has kept this broken.

   The top strip sits at `top: 0`, which is unambiguous: there is exactly one top
   edge and it works. The bottom has two. `100svh` is where the visible page
   ends, and `100lvh` is where the page really ends — some forty pixels apart on
   a phone with a toolbar. Anchoring to one left `#room`'s own edge sitting
   precisely on the other, and a transparent fixed element on the edge is the
   thing that started all of this.

   So this stops picking. It runs from six pixels above the small viewport to the
   bottom of the large one, which puts an opaque, deliberate colour across both,
   and `#room` is held clear of both in turn. Everything below `100svh` is behind
   the toolbar and cannot be seen; the six pixels above it are the same six the
   top edge already gives up.

   `max()` because the two viewports are the same number on anything without a
   collapsing toolbar, and a band of no height samples nothing. */
body.touch #tint::after {
  top: calc(100svh - 6px);
  height: max(10px, calc(100lvh - 100svh + 6px));
}

/* Nothing to look at: the strip the engine measures the room's height from.
   Zero-width so it can't affect anything, visible to layout so it has a height
   to report. See the comment on `--vh` above and `roomHeight` in src/main.js. */
#vh {
  position: fixed;
  top: 0;
  left: 0;
  width: 0;
  height: 100vh;
  height: 100svh;
  pointer-events: none;
  visibility: hidden;
}

/* The room doesn't scroll, so nothing should offer to. Without this a phone
   answers a downward drag with pull-to-refresh and an edge drag with the
   browser's own back gesture, both of which arrive as a `pointercancel` that
   drops the room mid-walk. */
body { overscroll-behavior: none; }

/* Nothing in the room is text you select. It's a place you walk around, and
   every gesture in it is a drag — so a drag that begins on a word selects the
   word instead of moving the room, and on a phone it goes further and calls up
   the magnifier and the copy bubble. Neither is an interaction this site has.

   The one exception is a field you're meant to type into, which has to stay
   selectable or you can't correct what you typed. */
body {
  -webkit-user-select: none;
  user-select: none;
  /* The long-press callout on iOS: a picture is not a thing to save from here. */
  -webkit-touch-callout: none;
  /* The grey flash a phone paints over anything it thinks you tapped. */
  -webkit-tap-highlight-color: transparent;
}
input, textarea {
  -webkit-user-select: text;
  user-select: text;
}

body {
  background: var(--paper);
  color: var(--ink);
  font: 400 16px/1.4 var(--font);
  -webkit-font-smoothing: antialiased;
  transition: background var(--room-fade) var(--room-ease),
              color var(--room-fade) var(--room-ease);
}

/* A beat can take the whole room somewhere else: `room:` in content/exhibits.md
   puts the name on the body while that beat is open. Three rooms exist and that
   should stay roughly true: it's the strongest move the site has, and it stops
   meaning anything if it happens often.

   Two of them are dark enough that black type stops working, so they flip the
   ink. Everything that isn't a beat — the name, the footer, the caption, the
   chapter chip, the contact sheet — is keyed off these five, so flipping them
   here is the whole change and nothing downstream has to know which room it is
   in. `grid` is deliberately not in this list: it's a pale wireframe on white,
   and black still reads on it. */
body.room-dark,
body.room-stone {
  --paper: #08090b;
  --ink: #fff;
  --hair: #22252b;
  --mid: #6f757f;
  --placeholder: #1b1f24;
  color: #fff;
}

/* Only the blackout paints the page itself. The lobby has a picture doing that,
   and painting under it would just be a colour nobody sees. */
body.room-dark { background: #08090b; }

/* Everything painted in those five has to ease at the same rate the page does,
   or the room fades over most of a second while the chrome changes in a single
   frame — which is what the flash going into the blackout was. A custom
   property can't transition, so the transition goes on the things that use it
   rather than on the property. */
header,
footer,
#caption,
#chapters .chip,
#chapters .menu,
#sound .speaker,
.cta,
kbd {
  transition: background-color var(--room-fade) var(--room-ease),
              color var(--room-fade) var(--room-ease),
              border-color var(--room-fade) var(--room-ease);
}

/* Chrome ------------------------------------------------------------- */

/* Same shape as the footer below, and the same treatment: a full-width fixed
   bar whose empty middle would otherwise sit over the room and catch pointers
   meant for it. Nothing interactive reaches up here today; this is so nothing
   has to remember not to. */
header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;
  display: flex;
  gap: 13px;
  align-items: center;
  /* The safe areas, because `viewport-fit=cover` lets the page reach under the
     notch and the home indicator. Reaching under them is the point — the room's
     paper should run to the edges of the glass — but the words in the room
     mustn't. Zero on everything that hasn't got any. */
  padding: calc(21px + env(safe-area-inset-top, 0px))
           calc(var(--gutter) + env(safe-area-inset-right, 0px))
           21px
           calc(var(--gutter) + env(safe-area-inset-left, 0px));
  pointer-events: none;
}
header .cta { pointer-events: auto; }

.name, .role { font-size: 16px; font-weight: 600; }
/* Not grey: the same ink, stepped back. It reads as one lockup that way. */
.role { opacity: .2; }

/* Rule 7 used to live here as "See the work". The comp puts a contact button in
   its place; the work still needs a permanent route of its own. */
/* A button rather than a link: it opens the contact form, and the address it
   reaches isn't in this page to link to. */
.cta {
  margin-left: auto;
  padding: 12px 16px;
  border: 0;
  border-radius: 80px;
  background: var(--ink);
  color: var(--paper);
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
}
.cta:hover { opacity: .85; }

/* Full width so the links sit centred on the room, which means its box runs
   straight across the chapter selector in the corner. Only the links should
   catch a pointer: without this the footer silently eats clicks on the chip,
   and the middle band of it stops working while the top and bottom edges
   carry on, which is a horrible thing to debug. */
footer {
  position: fixed;
  bottom: calc(31px + env(safe-area-inset-bottom, 0px)); left: 0; right: 0;
  z-index: 60;
  display: flex;
  gap: 24px;
  justify-content: center;
  font-size: 14px;
  font-weight: 500;
  color: var(--mid);
  pointer-events: none;
}
footer a { color: inherit; text-decoration: none; pointer-events: auto; }
footer a:hover { color: var(--ink); }
a:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }

/* Stage -------------------------------------------------------------- */

/* The room is `--vh` tall rather than `inset: 0`, so that "the bottom" means the
   same thing to the stylesheet, to the engine and to a phone with a toolbar
   sliding in and out over the page. Anything measured up from the bottom inside
   here is measured from the bottom of the room that was actually laid out.

   `touch-action: none` because the room owns the horizontal drag: without it a
   browser starts its own pan or back-swipe on the same finger, and the takeover
   arrives as a `pointercancel` that drops the walk halfway through. Nothing in
   here scrolls — the one thing that does is `.story`, which asks for its axis
   back in styles/exhibits.css. */
#stage {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--vh);
  overflow: hidden;
  touch-action: none;
}
#world { position: absolute; inset: 0; will-change: transform; }

/* The floor: a soft band beneath the character, faded out at both ends so the
   room still reads as empty rather than as a room with a floor in it. */
#floor {
  position: fixed;
  left: 0; right: 0;
  top: calc(var(--ground) - 35px);
  height: 111px;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(207, 214, 217, .24) 0%, rgba(207, 214, 217, 0) 81%);
  /* The ends fade out with a mask rather than by painting the paper colour over
     them. They used to be two stops of var(--paper), and a custom property
     can't transition: going into the blackout, the ends snapped to near-black
     while the page behind them was still easing, which read as a flash at both
     edges of the floor. A mask has no colour to be wrong. */
  -webkit-mask-image: linear-gradient(90deg, transparent 0%, #000 50%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0%, #000 50%, transparent 100%);
  /* Its opacity fade is set with the rest of the boot fade, at the bottom. */
}
body.room-dark #floor { opacity: .35; }

/* The grid room: the accelerator beat, where the walls of an empty white room
   turn out to have been there all along. Drawn once in Figma and exported whole
   (art/tunnel.svg) rather than rebuilt out of CSS planes, so the vanishing point
   stays exactly where it was put.

   It's a still image that fades in, not an effect that plays. Nothing here
   animates beyond the fade, which is why reduced motion needs no special case
   past shortening it. */
/* The full window, not the room the walk was measured against. `--vh` is the
   viewport with a phone's toolbar showing, which is what everything laid out in
   the room takes its share of — but a backdrop is not laid out in the room, it
   *is* the room, and it should reach behind the bars rather than stopping in a
   line where they begin. `lvh` is the viewport with them out of the way. */
#room {
  position: fixed;
  inset: 0;
  height: 100vh;
  height: 100lvh;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--room-fade) var(--room-ease);
  background: center / cover no-repeat;
}

/* And on a phone it stops six pixels short of both edges, which is the whole of
   what it costs to keep it out of this argument.

   A browser reading the page to tint its own bars looks at fixed elements
   touching the exact edge — within three or four pixels. This one touches both,
   and it has no `background-color` at all: a photograph is a `background-image`
   over nothing. So what it offers at the edge is "transparent", which is the
   answer that sends a browser back to its own system grey; and while it fades,
   what it offers is a fraction of a photograph over whatever is behind, which
   is a blend. Both are readings that stick.

   Six pixels clears the four it reads, and what shows in them is the hairline
   above, which is opaque and deliberate. On a full-bleed photograph under the
   glass it is not a thing anybody can see. Touch only: a desktop has no bars to
   sample it, and the backdrop is the strongest move the site has. */
body.touch #room {
  top: 6px;
  height: calc(100svh - 12px);
}
body.room-grid #room {
  opacity: 1;
  /* Drawn at 1512x982. `cover` keeps the perspective true on other windows and
     crops the edges rather than stretching the vanishing point off-centre. */
  background-image: url('../art/tunnel.svg');
}
body.room-grid #floor { opacity: 0; }

/* The lobby: the runescape beat. The room he was actually looking at, so it's
   the game's own login backdrop rather than anything drawn for this site. Same
   treatment as the grid — a still image that fades in, cropped rather than
   stretched so the arch stays centred behind him.

   The ink flips with it, above. The comp draws the chrome black over the stone,
   but at full size the name and the caption are unreadable there, and the walk
   already teaches that a dark room means white type. */
body.room-stone #room { opacity: 1; background-image: url('../art/runescape-room.png'); }

}
body.room-stone #floor { opacity: 0; }

/* Both backdrops are drawn 1512 wide and 982 tall, and both stay on `cover` in
   portrait, which fills the screen top to bottom and crops the sides.

   `contain` was tried and is wrong. It keeps the whole drawing, which sounds
   like the careful answer and isn't: a tunnel letterboxed into a band across
   the middle of the screen stops being a room you're standing in and becomes a
   picture of one, hung. The grid is the strongest move the site has and it only
   works if it *is* the room — so it reaches the top and the bottom, and losing
   the outer walls to the crop costs nothing, because the vanishing point is
   centred and the vanishing point is the whole idea.

   The lobby wants the same thing for the same reason. It's still a landscape
   photograph on a portrait screen, and what it would really like is a portrait
   framing — a decision for the person whose picture it is, not a formula. */

@media (prefers-reduced-motion: reduce) {
  #room { transition-duration: .2s; }
}


/* Caption ------------------------------------------------------------ */

/* One line at the top of the room, and the keys under it. This is the only
   prompt on screen. There is no second hint at the bottom. */
#caption {
  position: fixed;
  top: var(--caption-top);
  left: 50%;
  transform: translateX(-50%);
  width: 556px;
  max-width: calc(var(--vw) - 2 * var(--gutter));
  /* A line, the gap under it and the row of keycaps, reserved whether or not
     they're all there — the line goes when a beat is open. The room's geometry
     is worked out against this number (`ROOM` in engine/config.js), so it has
     to be a space the caption always occupies rather than one it sometimes
     does, or every object would resize as you opened a beat. */
  min-height: var(--caption-h);
  z-index: 50;
  text-align: center;
  pointer-events: none;
}
#caption .line,
#caption .keys {
  opacity: 0;
  transition: opacity .4s ease;
}
/* Comes down with the room, like the stories do — see `--type` in
   styles/exhibits.css. Exactly 20px on any window the design fits on. */
#caption .line { font-size: calc(20px * var(--type, 1)); line-height: 1.2; }
#caption .keys {
  margin-top: 24px;
  display: flex;
  gap: 48px;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  /* Half-strength: it's an instruction, not part of the story. */
  opacity: 0;
}
#caption .line.on { opacity: 1; }
#caption .keys.on { opacity: .5; }

/* One instruction. 48px separates phrases (the flex gap above), 16px separates
   a key from the words that explain it, 8px separates keys pressed together. */
.phrase {
  display: flex;
  gap: 16px;
  align-items: center;
  white-space: nowrap;
}
.phrase kbd + kbd { margin-left: -8px; }

kbd {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 32px;
  height: 32px;
  border: 1px solid var(--ink);
  border-radius: 4px;
  font: inherit;
  font-size: 16px;
  color: var(--ink);
}

/* Introduction ------------------------------------------------------- */

/* Anchored to the character, not to the room: the greeting on one side of him,
   the keys on the other, and a line between the words and the person they're
   about. All of it goes the moment he starts to run. */
/* No transition: the opacity is driven by how far he's walked, so it already
   moves with him. A transition on top would lag behind his feet. */
#intro {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: var(--vh);
  z-index: 40;
  pointer-events: none;
  /* Faded by how far he has walked, in step with the instructions. */
  opacity: var(--fade, 1);
}
#intro[hidden] { display: none; }

/* Both of these hang off the character rather than off the room — the greeting
   is beside his head and the arrow points at him — so they're measured in his
   units and follow him down when a short window shrinks him. `--char-scale` is
   written by the sprite; it's 1 on any window 900px tall or more. */
#intro .greeting {
  position: absolute;
  left: calc(50% - 141.5px * var(--char-scale, 1));
  bottom: calc(var(--vh) - var(--ground) + 329px * var(--char-scale, 1));
  transform: translate(-50%, 50%);
  font-size: 20px;
  white-space: nowrap;
}
#intro .greeting p + p { display: none; } /* the greeting is its first line */

#intro .pointer {
  position: absolute;
  left: calc(50% - 114px * var(--char-scale, 1));
  bottom: calc(var(--vh) - var(--ground) + 197px * var(--char-scale, 1));
  color: var(--ink);
  scale: var(--char-scale, 1);
}

/* Chapters ----------------------------------------------------------- */

/* The speaker and the chapter selector stand on one line in the bottom left
   corner, 32px tall each with 16px between them. The speaker is first because
   it's the smaller and the less important of the two: the chapter selector is
   the route through the whole site (rule 7) and shouldn't be the thing your eye
   has to get past. */
#sound {
  position: fixed;
  left: calc(var(--gutter) + env(safe-area-inset-left, 0px));
  bottom: calc(24px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
}
#sound .speaker {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}
/* Muted is the resting state and shouldn't look like a warning, so it's the
   same button stepped back rather than a struck-through one shouting at you. */
#sound .speaker[aria-pressed="false"] { opacity: .45; }
#sound .speaker:hover { opacity: 1; }
#sound .speaker svg { display: block; }

#chapters {
  position: fixed;
  left: calc(var(--gutter) + 48px + env(safe-area-inset-left, 0px));
  bottom: calc(24px + env(safe-area-inset-bottom, 0px));
  z-index: 60;
}
#chapters .chip {
  display: flex;
  gap: 8px;
  align-items: center;
  height: 32px;
  padding: 0 12px;
  border: 1px solid var(--ink);
  border-radius: 80px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
#chapters .caret { transition: transform .2s ease; }
#chapters.open .caret { transform: rotate(180deg); }

#chapters .menu {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  min-width: 100%;
  list-style: none;
  padding: 6px;
  border: 1px solid var(--ink);
  border-radius: 16px;
  background: var(--paper);
}
#chapters .menu button {
  display: block;
  width: 100%;
  padding: 7px 12px;
  border: 0;
  border-radius: 80px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 14px;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}
#chapters .menu button:hover,
#chapters .menu button:focus-visible { background: rgba(128, 128, 128, .14); }
#chapters .menu button.here { font-weight: 600; }

/* Get in touch ------------------------------------------------------- */

/* The one panel that sits over the room rather than in it, so it borrows the
   room's parts rather than inventing any: paper, ink, the hairline, and the
   same pill the header button is cut from. */
#contact {
  width: min(440px, calc(var(--vw) - 2 * var(--gutter)));
  /* A modal dialog centres itself on `margin: auto`, and the reset at the top
     of this file sets every margin to 0. Put it back. */
  margin: auto;
  padding: 0;
  border: 1px solid var(--ink);
  border-radius: 16px;
  background: var(--paper);
  color: var(--ink);
}
#contact::backdrop { background: rgba(255, 255, 255, .72); backdrop-filter: blur(3px); }
body.room-dark #contact::backdrop,
body.room-stone #contact::backdrop { background: rgba(8, 9, 11, .72); }

#contact .sheet { display: flex; flex-direction: column; gap: 8px; padding: 28px; }
#contact h2 { font-size: 20px; font-weight: 500; }
#contact .blurb { font-size: 14px; color: var(--mid); margin-bottom: 12px; }

#contact label { font-size: 13px; color: var(--mid); margin-top: 6px; }
#contact input,
#contact textarea {
  width: 100%;
  padding: 10px 12px;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: var(--paper);
  color: inherit;
  font: inherit;
  font-size: 15px;
  resize: vertical;
}
#contact input:focus-visible,
#contact textarea:focus-visible { outline: none; border-color: var(--ink); }

/* The honeypot. Off-screen rather than display:none, because some bots skip
   what is obviously hidden, and out of the tab order either way. */
#contact .trap {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

#contact .says { font-size: 13px; color: var(--mid); min-height: 1.4em; margin-top: 10px; }
#contact[data-state="sent"] .says { color: var(--ink); }

#contact .row { display: flex; gap: 10px; justify-content: flex-end; margin-top: 6px; }
#contact .quiet {
  padding: 12px 16px;
  border: 0;
  border-radius: 80px;
  background: none;
  color: var(--mid);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
#contact .quiet:hover { color: var(--ink); }
#contact .cta { border: 0; cursor: pointer; }
#contact .cta[disabled] { opacity: .5; cursor: default; }

/* Loading and failure ------------------------------------------------ */

/* The copy and artwork are fetched, so there is a moment with nothing in it.
   Hold the room empty rather than showing it half-built. */
body.loading #stage, body.loading #char, body.loading #floor, body.loading #intro { opacity: 0; }
#stage, #char { transition: opacity .4s ease; }
/* The floor changes for two reasons — the room going dark, and the boot fade —
   and it's one property, so it runs at the room's rate. */
#floor { transition: opacity var(--room-fade) var(--room-ease); }

#error {
  position: fixed;
  inset: 0;
  display: none;
  place-items: center;
  padding: 40px;
  text-align: center;
  color: var(--mid);
  font-size: 14px;
  z-index: 70;
}
body.broken #error { display: grid; }

#dev {
  position: fixed;
  left: var(--gutter);
  bottom: 60px;
  z-index: 60;
  font-size: 11px;
  color: var(--mid);
  max-width: 220px;
  line-height: 1.6;
  display: none;
}
body.dev #dev { display: block; }

/* On a short window the caption is the same two lines but takes less room for
   them: the keycaps come in closer under the label and are drawn smaller. The
   line and the prompt stay together at the top, where they belong — this is
   about the pair costing less height, not about splitting it up.

   `--caption-h` is what the engine reserves above the object (`ROOM` in
   src/engine/config.js reads the same 60), so shrinking it here is what hands
   the difference back to the artwork. */
@media (max-height: 760px) {
  :root { --caption-h: 60px; }
  #caption .keys { margin-top: 10px; }
  #caption .line { font-size: 18px; }
  kbd {
    width: 26px;
    height: 26px;
    font-size: 14px;
  }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition-duration: .01ms !important; }
}

/* The way past the walk ----------------------------------------------

   A skip link, and the standard shape of one: out of the way until a keyboard
   reaches it, which it does first, before the header or the room. Once you're
   reading it stays on screen, because then it's the way back. */
.skip {
  position: fixed;
  top: 12px;
  left: 50%;
  z-index: 90;
  transform: translate(-50%, -200%);
  padding: 10px 16px;
  border: 0;
  border-radius: 80px;
  background: var(--ink);
  color: var(--paper);
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  cursor: pointer;
  transition: transform .25s ease;
}
.skip:focus-visible { transform: translate(-50%, 0); }

/* Once you're reading it moves to the corner the chapter selector had, which is
   where this site keeps the way out of wherever you are. Centred at the top it
   was landing on his name at anything under about 600px, and it isn't the most
   important thing on the page — it's the way off it. */
body.reading .skip {
  top: auto;
  bottom: 24px;
  left: var(--gutter);
  transform: none;
}

/* A beat is a real section you can tab to, and arriving at one walks you there.
   The ring is drawn round the whole beat rather than round the object, because
   the beat is what you've arrived at.

   Only ever for somebody who got there with Tab — `body.keys`, set in
   src/main.js. A beat is a big target with a `tabindex` on it, so clicking
   anywhere near one focuses it, and `:focus-visible` doesn't reliably tell that
   apart for an element that isn't a control. A ring drawn round half the screen
   because you clicked the paper reads as breakage. */
.ex:focus { outline: none; }
body.keys .ex:focus {
  outline: 2px solid currentColor;
  outline-offset: 12px;
  border-radius: 4px;
}

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

   Everything the walk needs to be a walk, undone: the world stops being a
   sideways strip behind a camera and becomes a column down the page, the room
   itself goes, and every beat is simply open. No content moves and nothing is
   duplicated — these are the same elements the walk uses. */
html.reading,
body.reading { height: auto; overflow: visible; }

body.reading #stage {
  position: static;
  overflow: visible;
  padding: 96px 0 40px;
}
body.reading #world {
  position: static;
  transform: none !important;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 88px;
}

/* The room's own furniture. All of it is about being somewhere, and you aren't. */
body.reading #char,
body.reading #floor,
body.reading #room,
body.reading #caption,
body.reading #intro,
body.reading #dev { display: none; }

/* The chapter selector jumps between places in a walk; here everything is on
   one page and the browser's own find does that better. The speaker goes with
   it: nothing plays on this route, so it would be a switch with nothing on the
   other end of it. */
body.reading #chapters,
body.reading #sound { display: none; }

/* The footer stops floating over the room and becomes the end of the page,
   which is where a footer goes when there is a page for it to be the end of. */
body.reading footer {
  position: static;
  padding: 40px 0 56px;
}

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

   Under 900px wide the object and its story stop fitting side by side, and no
   amount of shrinking fixes that: a 340px column next to a 300px object is two
   things squeezed, not one thing smaller. So the room changes shape instead.
   The story goes underneath as a sheet, the floor comes up to leave space for
   it, and the object has the whole width to itself.

   `body.compact` is set by `relayout`, off the same `COMPACT` the engine uses
   to work out how big an object may be. The two must agree; see `ROOM` in
   src/engine/config.js, which carries the matching numbers.

   The walk itself is unchanged: he still walks, and beats still arrive one at a
   time. How you do either without a keyboard is answered separately, by the
   pointer — see `body.touch` below. */
body.compact {
  --caption-top: calc(.10 * var(--vh));
  /* One line. There are no keycaps down here: they'd be telling you to press
     keys this device hasn't got. */
  --caption-h: 44px;
  /* Both higher than the wide room's. The story sits under his feet rather than
     beside him, so the floor has to leave it somewhere to be.

     They were 38 and 58, which left the story 272px on a 390x844 phone — less
     than most of the stories in the walk need, in a box with no way to tell it
     scrolled, while the object above kept 338. That is the wide room's
     priorities on a screen that doesn't share them. On a phone the story isn't
     a caption to the object, it's the thing you came for, and on touch it's
     open from the moment you stop rather than only when you ask. So the floor
     comes up: the object takes 280 and the story 340, about 46:54 of the room
     between the caption and the chrome.

     Said again in `ROOM.compact` in src/engine/config.js, which is what decides
     how big an object may be. The two have to agree. */
  --eye: calc(.32 * var(--vh));
  --ground: calc(.50 * var(--vh));
  --gutter: 20px;
}

body.compact #caption .keys { margin-top: 10px; font-size: 13px; }
body.compact #caption .line { font-size: 17px; }

/* The room goes in front of its own floor.

   `#floor` is a sibling of `#stage` with a z-index of its own, so it paints over
   everything in the room rather than under it. In the wide room that is right
   and is the comp: the band is a wash, the only thing that reaches into it is
   the kitchen standing on it, and a wash over the feet of something standing on
   the ground is what a wash is for.

   The compact room isn't shaped like that. The floor comes up to make space for
   the story (`--ground` above), the object above it reaches down to the floor
   line, and the story starts 26px *below* it — so the band lands across the top
   of the words and the bottom of the object, greying both. It's the ground, so
   it goes behind the things standing on it. */
body.compact:not(.reading) #stage { z-index: 2; }


/* The name alone. "Designer" is the second half of a lockup that needs room to
   read as one, and at 375px it wraps into a second line under his name. */
@media (max-width: 480px) {
  body.compact .role { display: none; }
}

body.compact header {
  padding: calc(14px + env(safe-area-inset-top, 0px))
           calc(var(--gutter) + env(safe-area-inset-right, 0px))
           14px
           calc(var(--gutter) + env(safe-area-inset-left, 0px));
}
body.compact .cta { padding: 10px 14px; }

/* The footer's links move up out of the sheet's way and share the line with the
   speaker and the chapter selector, which is the only row of chrome left. */
body.compact footer {
  bottom: 14px;
  justify-content: flex-end;
  padding-right: var(--gutter);
  gap: 16px;
  font-size: 13px;
}
body.compact #sound,
body.compact #chapters { bottom: 12px; }

/* Which of the two the room asks you to use is a question about the *device*,
   not about the window. A browser window dragged narrow on a laptop is still a
   laptop: it has A, D and E, and they are the better way to walk. Only a device
   whose main pointer is a finger gets told about its thumb — and there the
   keycaps go, because drawing keys this thing hasn't got would make the site's
   first instruction a lie.

   Everything above this line is about the window. Everything in here is about
   what the visitor is holding. Conflating the two is what hid the E key on a
   small desktop window, which is the one instruction the site can't do without.

   `body.touch` rather than `@media (pointer: coarse)`, which is what this used
   to be. The engine has to know the same fact — it decides whether beats open
   themselves — and a media query it can only ask about in JavaScript is two
   copies of one answer. The class is written by `createPointer` in
   engine/touch.js, which starts from that same query and then believes what the
   visitor actually does over what the hardware claims. */
/* The caption has nothing to say on a touch screen. Its line moved into the
   story, where it scrolls with the words it introduces (`.story .title`), and
   there is no prompt to put in its place — the beat opened itself. So the band
   goes back to the object, which on a phone is the thing with least room to
   spare. Matched by `ROOM.touch` in src/engine/config.js, which is what decides
   how big an object may be. */
body.touch #caption { display: none; }
body.touch.compact { --caption-h: 0px; }

/* The two rows of instructions on the first beat: one names keys, one names
   what a thumb does, and exactly one of them is true at a time. The touch row
   used to be missing rather than swapped, so a phone arrived at the opening
   screen with nothing telling it how to move at all.

   Written at the same weight as `.ex.always-open .controls` in
   styles/exhibits.css, which is the rule that places them. Said any shorter it
   loses to that one and both rows show at once. */
.ex.always-open .controls.by-touch { display: none; }
body.touch .ex.always-open .controls.by-key { display: none; }
body.touch .ex.always-open .controls.by-touch { display: flex; }

/* The arrow drawn from the greeting across to the keycaps. It points at one
   spot, and on a touch screen there's nothing at that spot. */
body.touch #intro .pointer { display: none; }

/* He steps aside. In the wide room the object is beside him and he stands in
   the gap between it and the story; in the compact room both of those are above
   and below him, and dead centre is the one place the object can't afford to
   lose — it's where the magazine's spread is, and the video, and the face in
   the screen recording.

   A quarter of the width, to the left, which puts him clear of the middle
   without putting him off the edge. He's still in the room, still in front of
   the object, still the only drawn thing on screen — rule 6 is that he stays
   visible, not that he stays central.

   Only while a beat is open, which on touch means only while there is something
   for him to be standing out of the way of. The first and last beats are him in
   an empty room with a line beside him, and the middle of an empty room is
   where somebody standing in one belongs. `beat-open` is put on the body by
   `openBeat`; the always-open beats never set it. */
body.touch.compact.beat-open { --char-x: calc(-.25 * var(--vw)); }

/* He steps aside rather than cutting there, because the beat he's making room
   for arrives on a transition too, and one of the two moving is worse than
   both. Same easing as the story sliding up under him. */
body.touch #char { transition: left .45s cubic-bezier(.22, 1, .36, 1); }

/* The greeting hangs off him, so it goes where he goes. */
body.touch.compact #intro .greeting { left: calc(50% + var(--char-x)); }

/* A beat's own prev/next, drawn on its object. Only on touch: everywhere else
   the arrow keys do this and nothing needs drawing. See `steps` in ui/build.js
   for why this isn't the d-pad rule 4 forbids. */
.ex .steps { display: none; }
body.touch .ex.open .steps { display: flex; }

/* The greeting goes over his head instead of beside it: there isn't room at
   this width for anything to stand next to him. The arrow that pointed across
   at the keys goes with it — it's drawn as one diagonal to one spot, and that
   spot has moved. */
body.compact #intro .greeting {
  left: 50%;
  bottom: calc(var(--vh) - var(--ground) + 232px * var(--char-scale, 1));
  font-size: 18px;
  text-align: center;
  white-space: normal;
  width: calc(var(--vw) - 2 * var(--gutter));
}
body.compact #intro .pointer { display: none; }
