/* The console's own look: the palette and the typeface the Kodi front end and
 * the player picker already use, so a guest's phone belongs to the same
 * machine as the television.
 *
 * Deliberately one theme. This is a games console, not a document -- there is
 * no light-mode version of it -- so every colour is painted explicitly rather
 * than inherited from whatever the browser would otherwise supply.
 */

@font-face {
  font-family: "PressStart";
  src: url("/static/fonts/PressStart2P.ttf") format("truetype");
  font-display: swap;           /* the page is usable before the font lands */
}

/* How much room the clock and battery need at the top of the screen.

   Added to the home screen, this page is drawn *under* the status bar on
   purpose, so the picture reaches the very top -- and the safe-area inset is
   supposed to say how much of that top is spoken for. On a phone with a notch
   it does. On one without, the inset is zero and the status bar is still there
   and twenty points tall, so everything anchored to the top ended up
   underneath the time and the battery.

   Only in portrait: standalone landscape hides the status bar altogether, and
   a floor there would push the chips down for no reason. */
:root { --top-safe: env(safe-area-inset-top, 0px); }
@media (display-mode: standalone) and (orientation: portrait) {
  :root { --top-safe: max(env(safe-area-inset-top, 0px), 20px); }
}
/* iOS answered display-mode late and inconsistently for home-screen apps, so
   the page sets this class from navigator.standalone as well. */
@media (orientation: portrait) {
  html.standalone { --top-safe: max(env(safe-area-inset-top, 0px), 20px); }
}

:root {
  /* Straight from ra_players.py and ra_holdbar.py. */
  --bg:      #100e28;
  --bg2:     #1c1642;
  --edge:    #ff3ea5;
  --accent:  #ffd447;
  --dim:     #7a6fc0;
  --ink:     #f0eeff;

  --ok:      #4bd6a0;
  --bad:     #ff5c7a;

  --pixel: "PressStart", ui-monospace, monospace;
  /* Prose stays in a normal face. Press Start 2P is a display type: a
     paragraph set in it is a wall somebody has to decode rather than read. */
  --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
}

* {
  box-sizing: border-box;
  /* Kills double-tap zoom without taking pinch away from anyone who needs it.
     touch-action does not inherit, so it has to be said here. */
  touch-action: manipulation;
}

/* Anything marked hidden stays hidden, whatever else claims a display value --
   a class selector in a media query beats the browser's own [hidden] rule, and
   that once dropped a full-screen panel over the join form. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  min-height: 100%;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

/* ---- the gate ---- */

.gate {
  display: flex;
  /* Exactly what is on screen, and never a pixel more.
   *
   * It used to be min-height, so the gate grew when its contents did -- and
   * automatically rejoining is when they are tallest, with the link row, its
   * note and an error all showing at once. On a 660-tall phone that made the
   * document 804 tall, and a document taller than the window is what a mobile
   * address bar reacts to: it hides, the viewport grows, the layout crosses
   * the threshold back, it shows again. That oscillation is the "aggressive
   * jitter" on the PIN page, and it only ever appeared while logging in
   * automatically because that is the state with the most in it.
   *
   * --vv-height is the visible height, kept current by fitStage(); the dvh is
   * for the moment before that runs and for anything without visualViewport.
   * Nothing overflows the document now: what is too tall scrolls inside. */
  height: var(--vv-height, 100dvh);
  padding: 1.5rem 1.2rem calc(1.5rem + env(safe-area-inset-bottom, 0px));
  background:
    radial-gradient(120% 90% at 50% 0%, var(--bg2) 0%, var(--bg) 70%);
  position: relative;
  overflow: hidden;
}

/* Scanlines, on the page only -- never over the game picture, which has enough
   to contend with. Two pixels of nothing, at low contrast. */
.gate::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom, rgba(0, 0, 0, .22) 0 1px, transparent 1px 3px);
  mix-blend-mode: multiply;
}

/* margin:auto rather than justify-content:center: centring a container that
   overflows clips the top and puts it out of reach. */
.gate-inner {
  display: flex;
  flex-direction: column;
  gap: 1.15rem;
  width: 100%;
  max-width: 27rem;
  margin: auto;
  /* Taller than the screen scrolls here rather than growing the page. The
     margin above still centres it while it is short, which is the ordinary
     case; overflow-y rather than clipping because the top of this is the
     heading and the PIN box, and clipping would put them out of reach. */
  max-height: 100%;
  overflow-y: auto;
  touch-action: pan-y;
  overscroll-behavior: contain;
  position: relative;
  z-index: 1;
}

/* The icon, above the heading. Sized in rem so it shrinks with everything
   else on a short screen -- a phone in landscape has very little height, and
   the PIN box is what has to stay visible. */
.gate-mark {
  display: block;
  width: clamp(4rem, 18vw, 7rem);
  height: auto;
  margin: 0 auto -.4rem;
  image-rendering: pixelated;      /* it is pixel art; do not smooth it */
}
@media (max-height: 32rem) {
  /* Landscape on a phone. The heading and the PIN are the job here. */
  .gate-mark { display: none; }
}

.gate h1 {
  font-family: var(--pixel);
  font-size: clamp(1rem, 4.6vw, 1.5rem);
  line-height: 1.5;
  margin: 0;
  color: var(--accent);
  /* Enough pink to read as the console's own title, not so much that it fights
     the yellow it sits on top of. */
  text-shadow: 3px 3px 0 rgba(255, 62, 165, .5);
  letter-spacing: .02em;
}

.lede { color: var(--dim); margin: 0; font-size: .95rem; }

form { display: flex; flex-direction: column; gap: .8rem; margin: .4rem 0 0; }

label {
  font-family: var(--pixel);
  font-size: .58rem;
  line-height: 1.7;
  letter-spacing: .04em;
  color: var(--dim);
  text-transform: uppercase;
}

input {
  font: 1.6rem/1 var(--pixel);
  letter-spacing: .3em;
  text-indent: .3em;              /* balances the trailing letter-space */
  text-align: center;
  padding: .95rem .5rem;
  border: 3px solid var(--edge);
  border-radius: 0;               /* a console has corners */
  background: var(--bg2);
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
input::placeholder { color: #4a4270; }
input:focus { outline: none; border-color: var(--accent); }
input:focus-visible { outline: none; }

button {
  font: .8rem/1 var(--pixel);
  letter-spacing: .04em;
  padding: 1rem .9rem;
  border: 3px solid var(--accent);
  border-radius: 0;
  background: var(--bg2);
  color: var(--accent);
  cursor: pointer;
  text-transform: uppercase;
}
button:active { background: var(--accent); color: var(--bg); }
button:disabled { opacity: .45; cursor: default; }
button:focus-visible { outline: 3px solid var(--edge); outline-offset: 3px; }

.ghost {
  background: rgba(16, 14, 40, .8);
  color: var(--ink);
  border: 2px solid var(--dim);
  padding: .4rem .6rem;
  font-size: .5rem;
}

.error {
  color: var(--bad);
  background: rgba(255, 92, 122, .12);
  border: 2px solid var(--bad);
  padding: .7rem .8rem;
  margin: 0;
  font-size: .88rem;
  line-height: 1.5;
}

.footnote { color: var(--dim); font-size: .8rem; margin: 0; line-height: 1.6; }

.linkish {
  background: none;
  border: 0;
  color: var(--accent);
  text-decoration: underline;
  padding: 0;
  font: inherit;
  text-transform: none;
  letter-spacing: 0;
  cursor: pointer;
  pointer-events: auto;
}

/* ---- the stage ---- */

.stage {
  /* Positioned and sized from the visual viewport, which fitStage() measures
     and writes into these variables -- the only number that accounts for the
     address bar and tab strip actually on screen right now. A fixed element is
     otherwise laid out against the layout viewport, which is the whole window
     as if that furniture were not there, so the bottom row lands underneath
     it. The dvh fallback is for anything without visualViewport; it is close,
     but it is not what the phone with the bars showing reports. */
  position: fixed;
  top: var(--vv-top, 0px);
  left: var(--vv-left, 0px);
  right: 0;
  width: var(--vv-width, auto);
  height: var(--vv-height, 100dvh);
  /* One hundredth of the stage, for anything inside it that wants to
     size itself as a fraction of the height. vh is the wrong unit
     here: it measures the window, which is taller than the stage by
     however much browser furniture is on screen. */
  --sh: calc(var(--vv-height, 100dvh) / 100);
  /* No black of its own. The picture's own element paints the letterbox, and
     it now stops short of the chips and the keyboard's buttons -- see the
     note by `video` -- so a black stage behind it would simply fill the gap
     back in and nothing would look any different. What shows there instead is
     the page's background, which is the point. */
  background: transparent;
  /* No pinch on the picture.
   *
   * `manipulation` only turns off double-tap zoom; a two-finger touch still
   * zooms the page. This is a game held in two hands with thumbs on glass,
   * so a stray pinch is not a rare accident -- and Safari remembers the zoom
   * for next time, which is why the page sometimes *opens* zoomed, with the
   * chips too big and the pad hanging off the right.
   *
   * `pan-y` and not `none`, which is the difference between refusing a pinch
   * and refusing everything. touch-action is intersected down the ancestor
   * chain, so a descendant can never re-enable a gesture an ancestor has
   * forbidden: `none` here met `pan-y` on .tab-panel, .shelf and .chat-log and
   * won, and not one panel inside the stage could be scrolled by touch. It was
   * reported as "currently I have no way to scroll with a mobile touchscreen
   * device", and the comment that used to sit here had the rule exactly the
   * wrong way round.
   *
   * pan-y still excludes pinch-zoom and double-tap zoom, which is the whole of
   * what this was for. The picture and the pad say `none` themselves and are
   * unaffected -- an ancestor allowing more does not loosen them. */
  touch-action: pan-y;
  overflow: hidden;             /* nothing here is ever reached by scrolling */
}

/* The canvas is the same picture by another route -- see paint.js -- so it
   is laid out as the video is rather than being given a parallel set of
   rules that would drift apart the first time one of them changed. */
#painted[hidden], video[hidden] { display: none !important; }

/* The canvas over the video rather than instead of it.
 *
 * The <video> element is what every pointer, wheel and gesture handler is
 * attached to, what the zoom measures itself against, and what
 * requestPointerLock is called on to take the keyboard and mouse. Hiding it
 * would have taken all of that away -- so while this page is drawing the
 * picture the element stays exactly where it was, showing black with its
 * video track removed, and this is laid over the top of it.
 *
 * pointer-events: none is the whole of why that works: every touch and click
 * passes straight through to the element underneath, which is the one that
 * knows what to do with it. The stage is position: fixed, so this anchors to
 * the stage box. */
#painted.over {
  /* The box is written in pixels by fitPainted(), from the video element's
     own rectangle. Not 100% of the stage: the stage is more than the picture
     -- the on-screen controller has the bottom of it on a phone -- and a
     canvas stretched over all of it centres the picture lower than the video
     was, under the controller. */
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
  z-index: 1;
}

video, #painted {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  background: #000;
  /* The picture takes its own gestures now: two fingers zoom it and one moves
     it about, both inside the page. `manipulation` left the browser's own
     pinch on top of that, and the two fought -- the page zoomed, the fixed
     stage slid out from under the visual viewport, and the hud went with it.
     What is given up is browser zoom over the picture alone; everywhere else
     on the page it is untouched, and what replaces it here stops at the edges
     of the picture instead of wandering off into the furniture. */
  touch-action: none;
  transform-origin: center center;
  /* Said out loud, because zooming changed where this element paints. An
     untransformed video is a plain block and sits underneath everything
     positioned over it -- the chips, the on-screen pad, the panels -- without
     anyone having to say so. A transformed one is its own stacking context,
     which browsers hand to the compositor, and a composited layer can come up
     over siblings that were painting above it a moment earlier. That is a
     zoomed picture drawn over the row of chips: still there, still tappable in
     theory, and covered.

     So the order is stated rather than inherited. Position and a zero put this
     back exactly where it was, underneath everything with a number below. */
  position: relative;
  z-index: 0;
}

/* Where the black is allowed to reach.
 *
 * A 16:9 stream inside an upright phone is a strip across the middle with
 * letterbox black above and below it, and that black used to run the whole
 * height of the screen -- behind the row of chips at the top and behind the
 * keyboard's buttons at the bottom, so the page was one continuous black
 * field with controls floating in it. The picture's box now stops just under
 * the chips and just above the buttons, and the page's own background shows
 * either side of it, which is what separates "the game" from "the furniture".
 *
 * Both numbers are measured and written by fitPicture(), and it never takes
 * more than the black there is: where the picture fills the height already --
 * a phone held sideways -- both come out zero and nothing moves. margin
 * rather than `top`, because this element is position: relative and a
 * relative offset would slide the picture without shortening its box, which
 * is the same trap as setting `top` against a percentage height. */
video {
  margin-top: var(--picture-top, 0px);
  height: calc(100% - var(--picture-top, 0px) - var(--picture-bottom, 0px));
}

.hud {
  position: absolute;
  /* Over the picture, under the panels that replace it. See the note on the
     video: none of these numbers used to be needed. The one thing this does
     change is that the chips now sit over a notice rather than under it, on a
     phone narrow enough for the row to wrap into it -- which is the right way
     round: a message can be read around a chip, and a chip that cannot be
     reached is a control nobody has. */
  z-index: 10;
  top: var(--top-safe);
  left: 0;
  right: 0;
  display: flex;
  gap: .35rem;
  align-items: center;
  /* Wrap rather than run off the side: five chips and two buttons do not fit
     across a phone held upright, and the ones that fell off were the clock and
     the connection state. */
  flex-wrap: wrap;
  padding: .5rem calc(.6rem + env(safe-area-inset-right, 0px))
           .5rem calc(.6rem + env(safe-area-inset-left, 0px));
  /* No scrim behind the row.
   *
   * There was a dark gradient here to keep the chips readable over a bright
   * picture, and it is not needed: every control in the row carries its own
   * background -- .chip at 90% and .hudbtn at 55% -- so the gradient was
   * darkening the game rather than the controls. It shows up as a band across
   * the top of the picture, which is most obvious once the picture is zoomed
   * and the eye has something to compare it against. Reported from a phone as
   * "the top buttons have a background behind them, which it shouldn't".
   *
   * If a chip ever does become hard to read over a bright scene, the answer is
   * that chip's own background, not a band over the game. */
  transition: opacity .25s ease;
}
/* The row always exists, because the menu button lives in it and the menu
   button is the only way back once the chips are put away. Collapsing takes
   the chips out of the layout rather than fading them, so what is left is a
   single button and not a full-width bar of empty space over the game. */
.hud:not(.show) > :not(.hudbtn) { display: none; }
/* No background to clear any more -- the row never had one to begin with
   since the scrim went -- so this only stops the empty strip eating taps. */
.hud:not(.show) { pointer-events: none; }
.hud:not(.show) .hudbtn { pointer-events: auto; }
/* While anything in the row has focus, the row stays.
 *
 * On a desktop the chips are revealed by hovering the picture, and opening a
 * native dropdown moves the pointer off the picture and onto an operating
 * system menu -- so the row faded out from under the open list, taking the
 * dropdown with it. The selection could not be made at all. Focus is the
 * signal hovering is not: it stays on the select for as long as its list is
 * open. */
.hud:focus-within { opacity: 1; pointer-events: auto; }

/* The one control that is always on screen, upright, sideways and on a
   desktop alike: it opens the chips and it puts them away. It sits at the head
   of the row, ahead of the name, rather than floating over the corner -- as an
   overlay it landed on top of the name it was supposed to sit beside. */
.hudbtn {
  display: flex;
  flex: 0 0 auto;
  width: 1.9rem;
  height: 1.9rem;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 2px solid var(--dim);
  border-radius: 0;
  background: rgba(16, 14, 40, .55);
  color: var(--ink);
  /* Full strength: it was half-lit when it floated over the picture and had to
     stay out of the way, but in the row it is one of the chips and reading
     fainter than its neighbours just looks like a mistake. */
  transition: opacity .15s ease;
}
.hudbtn svg { width: 1.05rem; height: 1.05rem; display: block; }

/* Hamburger when the chips are away, a cross when they are up, so the button
   always shows what pressing it will do. The bars snap between the two in a
   few discrete frames rather than gliding: steps() is what makes it read as a
   machine changing state instead of a phone animating, which is the rest of
   this page's manner. */
.hudbtn .bar {
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: square;
  transform-box: view-box;
  transform-origin: 12px 12px;
  transition: transform .16s steps(4, end), opacity .16s steps(2, end);
}
.hud.show .hudbtn .bar-top { transform: translateY(5px) rotate(45deg); }
.hud.show .hudbtn .bar-bot { transform: translateY(-5px) rotate(-45deg); }
.hud.show .hudbtn .bar-mid { opacity: 0; }
/* The middle bar going out is what sells the change, so it leads on the way to
   a cross and follows on the way back. */
.hudbtn .bar-mid { transition-delay: .08s; }
.hud.show .hudbtn .bar-mid { transition-delay: 0s; }

@media (prefers-reduced-motion: reduce) {
  .hudbtn .bar { transition: none; }
}
.hudbtn:active { opacity: .6; }

/* Hover reveals them only where hovering is a thing. A phone applies :hover to
   whatever was last tapped and keeps it there, so this rule held the chips on
   screen no matter what closed them -- the x appeared to do nothing. */
@media (hover: hover) and (pointer: fine) {
  /* Not once they have been put away on purpose: a pointer crossing the window
     is not somebody asking for them back. */
  .stage:hover .hud.show { opacity: 1; pointer-events: auto; }
}

.chip {
  font-family: var(--pixel);
  font-size: .48rem;
  line-height: 1.5;
  letter-spacing: .03em;
  padding: .4rem .5rem;
  border: 2px solid var(--dim);
  border-radius: 0;
  background: rgba(16, 14, 40, .9);
  color: var(--dim);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
  /* A controller's name is long enough on its own to push everything else off
     the screen. */
  max-width: 46vw;
  overflow: hidden;
  text-overflow: ellipsis;
  flex: 0 1 auto;
  min-width: 0;
}
.chip.ok   { color: var(--ok);     border-color: var(--ok); }
.chip.warn { color: var(--accent); border-color: var(--accent); }
.chip.bad  { color: var(--bad);    border-color: var(--bad); }

/* The controller chip: an icon with a real select laid over it.
 *
 * A native select always draws its chosen option when closed, and here that
 * was a whole controller name -- long enough that the row needed a 46vw cap
 * to stop it swallowing everything else. What the closed state has to say is
 * only "this is where the controller is chosen"; which one is chosen is what
 * the list shows, and it marks the active row itself.
 *
 * The select is kept and made invisible rather than replaced with a button
 * and a hand-built menu: a phone then still gets its own picker, which is
 * better than anything that could be built here. */
.padpick {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  padding: 0;
  flex: 0 0 auto;
}
.padpick-icon {
  width: 1.15rem;
  height: 1.15rem;
  display: block;
  pointer-events: none;          /* the select underneath takes the tap */
}
.padpick-select {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;                    /* the popup is drawn by the browser, not this */
  -webkit-appearance: none;
  appearance: none;
  border: 0;
  /* Solid, despite the opacity above and despite nothing here being visible.
     The popup is drawn by the browser as a widget rather than as part of the
     page, so the opacity does not reach it -- but its colours are taken from
     this element, and from `transparent` a desktop drew the list white on
     white. Only the row under the pointer could be read, because only that
     one gets a background of its own. Changed in place rather than added
     below: a second `background` further down this block would win, and that
     trap has been walked into here before. */
  background-color: var(--bg2);
  color: var(--ink);
  cursor: pointer;
}
/* And the rows themselves, which inherit none of the above on Firefox. The
   same pairing select.chip option gets; this select is not a chip, which is
   the whole reason it was missed. */
.padpick-select option { background: var(--bg2); color: var(--ink); }

/* The connection chip carries a lit pip instead of the word "connected".
   Square, because nothing else on this screen is round, and it reads as an
   indicator on a machine rather than a dot in a document. The frame stays the
   quiet colour whatever the state: the pip is the signal, and a chip that
   changed colour as well would be shouting the same thing twice. */
.chip.link {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  border-color: var(--dim);
  color: var(--dim);
}
.chip.link::before {
  content: "";
  width: .5rem;
  height: .5rem;
  flex: none;
  /* Disconnected: an empty outline, which is a different shape and not only a
     different colour -- it still reads as "off" to anybody who cannot tell
     these two colours apart. */
  border: 2px solid var(--dim);
  background: transparent;
}
.chip.link.ok::before {
  border-color: var(--ok);
  background: var(--ok);
  box-shadow: 0 0 .35rem var(--ok);
}
/* Anything with a reason attached keeps the reason readable. */
.chip.link.bad { color: var(--bad); }

/* The link chip reports the media route when tapped, so it stays tappable
   even while the rest of the overlay is not. */
#link { pointer-events: auto; cursor: pointer; }
#info { font-family: var(--pixel); font-size: .6rem; padding: .4rem .55rem; }

/* The layout picker is a chip like the rest, but it has to be operable while
   the overlay around it is not. */
select.chip {
  pointer-events: auto;
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  padding-right: 1.1rem;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--dim) 50%),
    linear-gradient(135deg, var(--dim) 50%, transparent 50%);
  background-position: right .45rem top 55%, right .25rem top 55%;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
}
select.chip option { background: var(--bg2); color: var(--ink); }

/* Both of these live at the top, over the picture. The bottom of the stage
   belongs to the on-screen controller, and anything put there in portrait is
   underneath it -- which meant the explanation of why there was no video was
   the one thing you could not read. */
.notice {
  /* Scrolls, and the stage above it now refuses every gesture to keep
     pinch off the picture, so it asks for the one it needs. */
  touch-action: pan-y;
  position: absolute;
  top: calc(var(--top-safe) + 3.2rem);
  left: .6rem;
  right: .6rem;
  z-index: 6;
  background: var(--bg2);
  border: 3px solid var(--edge);
  padding: .8rem .9rem;
  color: var(--ink);
  font-size: .85rem;
  line-height: 1.55;
  max-height: 42vh;
  overflow-y: auto;
  pointer-events: auto;
}
.notice p { margin: .25rem 0; }
.notice strong {
  display: block;
  font-family: var(--pixel);
  font-size: .66rem;
  line-height: 1.8;
  color: var(--accent);
  font-weight: normal;
  margin-bottom: .35rem;
}
.notice .footnote { color: var(--dim); font-size: .78rem; }

.prompt {
  position: absolute;
  /* Under the notice, which is where it was: the notice carries a number and
     this did not, and a numbered element paints over an unnumbered one
     whatever the page order says. */
  z-index: 5;
  top: calc(var(--top-safe) + 2.8rem);
  left: .6rem;
  right: .6rem;
  text-align: center;
  color: var(--ink);
  text-shadow: 0 2px 10px #000;
  pointer-events: none;
  padding: .6rem 1rem;
}
/* When both are up, the prompt sits under the notice rather than on it. */
.notice:not([hidden]) ~ .prompt { top: auto; bottom: 0; }
.prompt p { margin: .3rem 0; }
.prompt strong {
  font-family: var(--pixel);
  font-size: .72rem;
  line-height: 1.8;
  color: var(--accent);
  font-weight: normal;
}
.prompt[hidden] { display: none; }

/* ---- on-screen controller ----
 *
 * One set of markup, two arrangements. Portrait puts the picture above and the
 * pad below, because a thumb cannot reach the middle of an upright phone and
 * covering a 16:9 video wastes the half of the screen already letterboxed.
 * Landscape has no room below, so it floats over the bottom corners where the
 * thumbs already are.
 */

.touch {
  /* Above the chips, which is where it has always been -- it is later in the
     page and both were left to tree order. In landscape it lies over the
     picture, and the shoulder buttons share the strip the chips are in; a chip
     row that is not open takes no taps anyway. Upright it sits below the
     picture in the ordinary flow and this decides nothing. */
  z-index: 12;
  --key: #241c52;
  --key-edge: var(--dim);
  display: grid;
  gap: .5rem;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  touch-action: none;
}

.touch-left, .touch-right, .touch-mid {
  display: flex;
  align-items: center;
  justify-content: center;
}
.touch-left { grid-area: left; }
.touch-right { grid-area: right; }
/* Between the shoulders, across the top. A row rather than a column now: it
   is a short, wide space between two clusters, not a tall narrow one. */
.touch-mid { grid-area: mid; flex-direction: column; gap: .5rem; }

/* ---- on-screen sticks --------------------------------------------------
   A well with a knob in it. The knob follows the thumb to the edge and snaps
   back when it leaves, because glass has neither a spring nor a centre.

   Stacked under the cluster it belongs to -- the left stick below the d-pad,
   the right below the face buttons -- which is where a thumb already is on a
   modern pad, and which costs the grid nothing: both columns simply become
   two rows. */
.touch.has-sticks .touch-left,
.touch.has-sticks .touch-right {
  flex-direction: column;
  gap: .45rem;
}
.stick {
  position: relative;
  flex: 0 0 auto;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 2px solid var(--key-edge);
  background:
    radial-gradient(circle at 50% 50%,
                    rgba(122, 111, 192, .16) 0 38%, transparent 39%),
    var(--key);
  touch-action: none;
}
.stick[hidden] { display: none; }
.stick-knob {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 42%;
  height: 42%;
  border-radius: 50%;
  background: var(--ink);
  border: 2px solid var(--bg);
  transform: translate(-50%, -50%);
  /* No transition while a thumb is on it -- the knob must track the finger
     exactly -- but it eases home on release, which is the only moment the
     movement is not the player's own. */
  transition: transform .12s ease-out;
}
.stick.live .stick-knob {
  transition: none;
  background: var(--accent);
}
/* The stick pressed in, which on glass has to be shown or there is no way to
   know it took. A ring rather than a colour change: the knob is already
   recoloured while a thumb is on it, and a second colour there would be two
   states fighting over one signal. */
.stick.clicked .stick-knob {
  box-shadow: 0 0 0 3px var(--bg), 0 0 0 6px var(--accent);
}

/* Shoulders sit above the hand they belong to, triggers furthest out, the way
   they are reached for on a real pad. */
.shoulders {
  display: flex;
  flex-direction: column;           /* row 0 first: triggers furthest out */
  gap: .35rem;
  align-items: stretch;
  justify-content: flex-end;
}
.shoulders-left { grid-area: lsh; align-items: flex-start; }
.shoulders-right { grid-area: rsh; align-items: flex-end; }
.shoulders[hidden] { display: none; }


.centre-row { display: flex; gap: .5rem; align-items: center; }

.dpad {
  position: relative;
  width: min(38vw, 190px);
  aspect-ratio: 1;
  touch-action: none;
}

/* Each arm carries its outer edge and its two flanks, and no inner edge --
   twelve edges between them, which is exactly the outline of a cross. The
   centre is then a plain fill with no border of its own. */
.dpad-arm {
  position: absolute;
  background: var(--key);
  border: 2px solid var(--key-edge);
  transition: background .04s linear;
}
.dpad-up    { left: 33.5%; right: 33.5%; top: 0; height: 34%; border-bottom: 0; }
.dpad-down  { left: 33.5%; right: 33.5%; bottom: 0; height: 34%; border-top: 0; }
.dpad-left  { top: 33.5%; bottom: 33.5%; left: 0; width: 34%; border-right: 0; }
.dpad-right { top: 33.5%; bottom: 33.5%; right: 0; width: 34%; border-left: 0; }

.dpad-arm.live { background: var(--edge); }
/* An arm is a label around a switch now, the same as a button. Same ring to
   take away, same reason. */
.dpad-arm:focus,
.dpad-arm:focus-visible,
.dpad-arm:focus-within { outline: none; }
.dpad-arm { -webkit-tap-highlight-color: transparent; }

.dpad-hat {
  position: absolute;
  left: 33.5%;
  top: 33.5%;
  right: 33.5%;
  bottom: 33.5%;
  background: var(--key);
}

.face {
  position: relative;
  width: min(52vw, 250px);
  /* Must stay at or below 1.5:1 -- see the note beside LAYOUTS in app.js. */
  aspect-ratio: 1.55 / 1;
}

.tbtn {
  position: absolute;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: 3px solid var(--edge);
  background: var(--key);
  color: var(--accent);
  font: .9rem/1 var(--pixel);
  letter-spacing: 0;
  padding: 0;
  text-transform: none;
  touch-action: none;
  transition: background .05s linear, transform .05s linear;
}

/* The switch inside every button. A finger on the label activates it, and on
   an iPhone that flip is the only haptic a web page can still cause -- so it
   has to be a real control, in the page, activated by a real touch. Invisible
   and inert to the pointer: the label is what is touched, and the input is
   only what the touch ends up meaning. */
.tbtn-tap {
  position: absolute;
  /* The whole face of the button rather than a pixel of it, so the finger
     lands on the switch itself and not merely on a label that stands for one.
     iOS flips a switch on a swipe as well as on a tap, and a thumb that moves
     while it presses can flip it under the finger -- which is the closest
     this can get to the feeling arriving as the button goes down. */
  inset: 0;
  width: auto;
  height: auto;
  margin: 0;
  opacity: 0;
  /* Painted by nobody -- but by suppression, never by `appearance: none`.
     Setting the appearance, the border or the background of a native control
     is how WebKit is told to stop drawing it natively, and a switch that is
     no longer a switch plays no haptic: the rendering and the feeling are the
     same object. So only the ring is taken away, and the ring is what the
     rectangle across the button was. */
  outline: none;
  box-shadow: none;
  -webkit-tap-highlight-color: transparent;
}
.tbtn-tap:focus,
.tbtn-tap:focus-visible { outline: none; box-shadow: none; }
.tbtn:focus,
.tbtn:focus-visible,
.tbtn:focus-within { outline: none; }
/* The letter on the key. Its own element now that the button is a label with
   a control inside it, so the two cannot be confused for each other. */
.tbtn-cap { pointer-events: none; }
/* Not preventing the default of a press is what lets the switch flip, so the
   things preventDefault was quietly also doing have to be said out loud. */
.tbtn {
  -webkit-tap-highlight-color: transparent;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}

.tbtn.live {
  background: var(--edge);
  color: var(--bg);
  transform: scale(.94);
}

/* A latched button: pressed once to hold it down, pressed again to let go.
 *
 * Drawn pressed, because it is pressed -- the game is being told this button
 * is down, and a control that lies about that is worse than no control. The
 * fill is the same one `.live` uses so it reads as the same state at a
 * glance; the ring is what says the difference, which is that this one stays
 * down after the thumb leaves.
 *
 * Not scaled, deliberately. `.live` shrinks by a twentieth to answer the
 * finger that is on it *now*, and a button that sat shrunk for a whole level
 * would look like a rendering fault rather than a held button. So a latched
 * button that is also being touched still shrinks, and one that is merely
 * held does not. */
.tbtn.held {
  background: var(--edge);
  color: var(--bg);
  box-shadow: 0 0 0 3px var(--accent);
}
.dpad-arm.held { background: var(--edge); box-shadow: inset 0 0 0 3px var(--accent); }
/* Both at once is the ordinary case -- a thumb on a button that was already
   latched -- and `.live`'s scale is wanted then. Stated after both so it wins
   however they are ordered above. */
.tbtn.held.live { transform: scale(.94); }

.tbtn-face { width: 30%; aspect-ratio: 1; }

/* Stated after .tbtn on purpose: that rule makes every button absolutely
   positioned and round, which is right for a face button and wrong for a
   shoulder. Declared earlier, these lost and the shoulders came out as
   overlapping pills with one of each pair invisible. */
.tbtn-shoulder {
  /* Relative rather than static: in flow either way, but it gives the switch
     inside the label something to be positioned against instead of letting a
     stray pixel of it wander up to the nearest positioned ancestor. */
  position: relative;
  width: min(26vw, 120px);
  padding: .55rem 0;
  border-radius: 0;
  border-color: var(--dim);
  color: var(--dim);
  font-size: .55rem;
  letter-spacing: .08em;
}
.tbtn-shoulder.live { background: var(--dim); color: var(--bg); }

.tbtn-start {
  position: relative;
  width: auto;
  height: auto;
  padding: .5rem 1rem;
  /* The one rounded thing on the whole screen, and deliberately so. Nothing
     else here has a radius -- the console is drawn square on purpose -- but
     these two are pills on the pad this is copying, and they read as select
     and start because of it. */
  border-radius: 999px;
  border-color: var(--dim);
  color: var(--dim);
  font-size: .5rem;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.tbtn-start.live { background: var(--dim); color: var(--bg); }

@media (orientation: portrait) {
  /* No black here either. The picture's own element paints the letterbox and
     stops short of the chips and the keyboard's buttons; a black stage behind
     it would fill the gap straight back in. */
  .stage { display: flex; flex-direction: column; }
  /* The picture gets whatever is left, up to the height it has been given.
     Both halves matter and the first one is easy to lose. Growing is how a
     banner still costs the picture height: a notice that has to be read is in
     the flow here, so it takes its space out of the column and the picture
     gives it up. Capping is the change -- `max-height: none` let the picture
     grow straight back over whatever the bottom inset had just taken off it,
     so the top of the black moved under the chips and the bottom did not
     budge, which is half a change and reads as a bug.
     Nothing scrolls: a control you have to scroll to reach is not a control. */
  video, #painted {
    flex: 1 1 auto;
    min-height: 0;
    max-height: calc(100% - var(--picture-top, 0px)
                          - var(--picture-bottom, 0px));
  }
  .touch {
    flex: 0 0 auto;
    /* Select and start angled between the bumpers, the way the pad this is
       drawn from puts them. Upright the picture sits above the pad, so height
       the pad takes is width the picture cannot have -- the video keeps its
       shape, and grows in both directions or neither. A row of its own cost
       about three rem of that.
       The angle is not decoration: turned, the pair is shorter than the two
       stacked shoulder buttons beside it, so the row it joins does not get
       any taller. Straight, they were clipped between the clusters; that was
       a sizing mistake rather than an argument against the position. */
    /* Two columns, and the angled pair is not one of them. Giving it a
       column of its own took that width off the clusters either side: the
       d-pad and the buttons shrank to fit a narrower column and were pushed
       apart into the middle of it. Floated over the gap between the shoulders
       instead, it costs the grid nothing and the clusters keep every pixel
       they had. */
    position: relative;
    grid-template-areas:
      "lsh  rsh"
      "left right";
    grid-template-columns: 1fr 1fr;
    align-content: center;
    row-gap: .7rem;
    padding: .8rem 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
  }
  /* Upright there is room below the picture, so the clusters give up less --
     but they still give up something, because two rows of controls where
     there was one has to come from somewhere and the strip is not allowed to
     push the picture off the top. */
  .touch.has-sticks .dpad,
  .touch.has-sticks .face  { width: min(38vw, 180px); }
  .touch.has-sticks .stick { width: min(27vw, 135px); }
  .touch.has-sticks .touch-left,
  .touch.has-sticks .touch-right { gap: .5rem; }
  /* No rule along the top. The pad and the picture do not need a line drawn
     between them -- the picture stops and the buttons start, which is already
     the whole of it -- and a bright pink one across the bottom of the screen
     pulls the eye off the game it is edging. */
  .touch {
    background: var(--bg);
  }
  /* Zoomed in, the pad floats over the picture instead of sitting under it.
   *
   * Upright at 1x the pad belongs in the flow: a 16:9 picture on a tall phone
   * is letterboxed, the space below it is free, and a thumb cannot reach the
   * middle of the screen anyway. Zoomed there is no letterbox left -- every
   * pixel under the pad is picture somebody has deliberately magnified, and a
   * solid panel over the bottom third of it is the thing they zoomed in to
   * see. The chips along the top already work this way, which is where the
   * asking came from.
   *
   * ONLY the background. Nothing about the layout moves -- the pad keeps its
   * place in the flow and the picture keeps exactly the box it had, because a
   * scaled picture already paints outside its box and the solid panel was the
   * only thing covering it. Floating the pad instead was tried and was the
   * wrong fix twice over: the picture's box grew into the freed space, so a
   * picture somebody had lined up by dragging jumped to the middle of the new
   * box the instant they zoomed. Where the picture sits is theirs to decide by
   * dragging and scrolling, and zooming must not take a view away and hand
   * back a different one.
   *
   * Each key keeps its own background, so the buttons stay readable over a
   * bright scene rather than becoming outlines on top of a game. */
  .stage.zoomed .touch {
    background: none;
  }
  /* The same size as each other, and bigger than either was. The d-pad had
     been the smaller of the two for no reason worth keeping -- on the pad
     this copies they are a matched pair, and matching them is also what puts
     them level, since both are centred in equal halves. */
  .dpad, .face { width: min(42vw, 200px); }
  .tbtn-shoulder { width: min(21vw, 96px); padding: .45rem 0; min-width: 0; }
  .shoulders { min-width: 0; }
  /* Over the gap between the shoulder clusters, taking no space from
     anything. Anchored to the top padding so it sits on the shoulder row. */
  .touch-mid {
    position: absolute;
    /* The pad's own top padding, plus enough to clear the shoulder cluster:
       level with the lower of the two shoulder buttons rather than with the
       top of the pair. */
    top: 2.3rem;
    left: 50%;
    transform: translateX(-50%);
    flex-direction: row;
    justify-content: center;
    gap: 0;
  }
  .touch-mid .tbtn-start {
    /* Both the same way, parallel, as they are on the real thing -- and
       overlapping slightly, which is what lets them sit as close together as
       they do on it. */
    transform: rotate(-32deg);
    padding: .4rem .65rem;
    font-size: .42rem;
  }
  .touch-mid .tbtn-start + .tbtn-start { margin-left: -.35rem; }

  /* The clusters come in towards the middle rather than sitting in the centre
     of their half -- and which property does that depends on which way the
     column runs, which is the part this got wrong.
     Without sticks these are rows: justify-content is the horizontal one and
     align-items is the vertical one. Setting both to an edge pulled the
     d-pad down and the buttons up, so the two sat at different heights --
     which is exactly what "not in line with each other" looked like. */
  .touch-left  { justify-content: flex-end;   align-items: center; }
  .touch-right { justify-content: flex-start; align-items: center; }
  /* With sticks they stack, so the two swap jobs. */
  .touch.has-sticks .touch-left  { align-items: flex-end;   justify-content: center; }
  .touch.has-sticks .touch-right { align-items: flex-start; justify-content: center; }
  .touch-left  > * { margin-right: 3vw; }
  .touch-right > * { margin-left: 3vw; }
}

@media (orientation: landscape) {
  .touch {
    position: absolute;
    inset: auto 0 0 0;
    grid-template-columns: 1fr auto 1fr;
    /* Fill the screen rather than hang off the bottom of it.
       The pad used to be anchored to the bottom with an automatic height, so
       every row added grew it upwards and the top one -- the triggers -- went
       off the edge on a short screen. Filling the stage and giving the middle
       row all the slack puts the shoulders at the top, the pad and buttons at
       the height of the thumbs, and select and start at the bottom, none of
       which can be pushed anywhere. */
    inset: 0;
    grid-template-rows: auto 1fr auto;
    grid-template-areas:
      "lsh   .    rsh"
      "left  .    right"
      "mid   mid  mid";
    align-items: center;
    row-gap: .4rem;
    --side: clamp(.6rem, 3vw, 2rem);
    /* Sideways, the notch or camera island is at one end of the long edge --
       exactly where the left-hand cluster is put. The inset is added to the
       padding rather than to --side, so the negative margins below still pull
       the clusters out to the edge of the *safe* area rather than to the edge
       of the glass, where a thumb finds a lens instead of a d-pad. */
    padding-top: calc(var(--top-safe) + 2.9rem);
    padding-bottom: calc(.7rem + env(safe-area-inset-bottom, 0px));
    padding-left: calc(var(--side) + env(safe-area-inset-left, 0px));
    padding-right: calc(var(--side) + env(safe-area-inset-right, 0px));
    pointer-events: none;
  }
  /* Every cluster that holds a control, not just the three named ones. The
     shoulders are their own elements outside those, so leaving them off this
     list left LB, LT, RB and RT visible, lit on press, and sending nothing --
     the parent is what turns taps into buttons, and a child that does not take
     pointer events never reaches it. */
  .touch-left, .touch-right, .touch-mid,
  .shoulders { pointer-events: auto; }
  /* Sized against the stage, not the window. These were 30vh and 40vh, which
     on a phone measure the window as if the address bar and tab strip were not
     there -- so on a 390-tall phone showing 300, the pad sized itself for 390,
     the middle row grew past the space available, and select and start were
     pushed off the bottom. */
  /* Bigger than the letterbox bar, and see-through because of it. Sideways
     these used to be held inside the black margin beside the picture, which
     on a phone is a thumb's width if you are lucky -- so the controls were
     sized by how wide the bars happened to be rather than by the hand using
     them. They lie over the picture now and are faint enough to see through,
     which is the trade that buys the size. */
  .dpad { width: min(calc(38 * var(--sh)), 215px); }
  .face { width: min(calc(46 * var(--sh)), 265px); }
  /* Sideways they go back to the far corners: the middle of the screen is
     the picture, the corners are where the thumbs already are, and there is
     no height to save here because the pad floats over the picture rather
     than sitting under it. */
  .touch-mid { width: 100%; align-items: stretch; }
  .centre-row { width: 100%; justify-content: space-between; }

  /* Against the outer edges, sized by their contents. These used to be held
     inside the black bar beside the picture -- off the game, but on a phone
     that bar is a thumb's width and the controls came out too small to use.
     They reach back out through the padding to the edge of the safe area, and
     lie over the picture, which is what the transparency below is for. */
  .touch-left, .touch-right { justify-content: center; }
  /* Sized by their contents and left inside the padding. Reaching back out
     through it with a negative margin put the cluster flush against the screen
     edge, and the d-pad's arms are drawn outside its own box -- so the left arm
     was cut off by the edge, which is the same thing the camera island does to
     a control placed too far out. */
  .touch-left { justify-self: start; width: auto; }
  .touch-right { justify-self: end; width: auto; }

  /* Back in the grid, at the top corners, where the padding above keeps them
     clear of the chips. */
  .shoulders { align-self: start; }
  /* Pinned to the bottom of its row so it cannot drift upward. */
  .touch-mid { align-self: end; }
  .tbtn-shoulder { width: min(22vw, 105px); padding: .42rem 0; }
  /* Faint, because they are on the picture now rather than beside it. Enough
     to aim at, little enough to see the game through. */
  .dpad-arm, .dpad-hat, .tbtn, .stick { opacity: .58; }
  .dpad-arm.live, .tbtn.live, .stick.live { opacity: .85; }

  /* With sticks, the corner clusters become two rows in a screen that has no
     spare height, so both shrink to make room and the stick takes the smaller
     share -- a thumb needs less travel than a d-pad needs reach. Everything
     is in --sh, the stage height, so this tracks the letterbox rather than
     the window: on a phone the window is taller than what is actually shown. */
  .touch.has-sticks .dpad { width: min(calc(28 * var(--sh)), 165px); }
  .touch.has-sticks .face { width: min(calc(34 * var(--sh)), 200px); }
  .touch.has-sticks .stick { width: min(calc(25 * var(--sh)), 145px); }
  .touch.has-sticks .touch-left,
  .touch.has-sticks .touch-right { gap: .3rem; }
  /* The columns are as wide as their widest row now, so the gutter floor has
     to allow for the cluster above the stick rather than the stick alone. */
  .touch.has-sticks .touch-left,
  .touch.has-sticks .touch-right { width: auto; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

/* Portrait has room the picture is not using -- a 16:9 video on an upright
   phone is letterboxed either way -- so the chips get a strip of their own
   above it and cover nothing. */
@media (orientation: portrait) {
  .hud {
    position: static;
    order: -1;                    /* the video comes first in the markup */
    opacity: 1;
    pointer-events: auto;
    /* No fill and no rule under it.
       The strip used to be painted in --bg with a line along the bottom, which
       upright reads as a solid bar across the top of the screen -- and the
       chips are already legible without it, being 90% opaque themselves
       against the hudbtn's 55%. So the strip shows whatever is behind it and
       the chips float on that, which is what was asked for: "the black
       background along the top where the top chip buttons go should be
       transparent".
       The rule went with the fill rather than surviving it: a 2px line drawn
       across nothing, separating a transparent strip from the picture, is a
       stray mark rather than a separator. */
    background: transparent;
    transition: none;
    /* Static, so the `top` the floating version uses to clear the status bar
       does nothing here -- and this is the orientation in which the status bar
       is actually shown. The strip is the first thing on the screen, so it
       has to carry that gap itself, in its padding. Without this the chips sat
       behind the clock and the battery in the home-screen copy, which is the
       only place the page is drawn under the bar at all. */
    padding-top: calc(.5rem + var(--top-safe));
  }

  /* Messages join the flow as well, directly under the chips. Left floating
     they would sit on top of that strip, which is the problem this whole
     arrangement exists to avoid. */
  .notice {
    position: static;
    order: -1;
    margin: 0;
    border-left: 0;
    border-right: 0;
    /* A flex item shrinks below its content unless told not to, and the
       second line of the message is the half that explains anything. */
    flex: 0 0 auto;
    max-height: none;
  }

  /* Except while somebody is driving, when all of that is exactly wrong.
   *
   * The arrangement above is right for watching: the picture is letterboxed
   * on an upright phone, so a strip above it costs nothing that was being
   * looked at. Driving, the picture is zoomed and panned and is not
   * letterboxed at all -- so the strip takes real picture -- and worse, the
   * pointer's whole geometry is measured from the height of the video
   * element. A message arriving in the flow changes that height underneath
   * the arithmetic: the picture resizes, and where the pointer sits on the
   * screen moves with it, mid-drag.
   *
   * So they float again, over the picture, and nothing is resized. */
  .stage.driving .hud {
    position: absolute;
    order: 0;
    padding-top: .5rem;
    /* And still no fill. Floating it back over the picture brought the panel
       with it -- the strip was made transparent in the rule above, and this
       one painted it again the moment the keyboard and mouse were picked up,
       which is exactly when the picture underneath matters most. The chips
       carry their own backgrounds either way. */
    background: transparent;
  }
  .stage.driving .notice {
    position: absolute;
    order: 0;
    margin: 0;
    border-left: 3px solid var(--edge);
    border-right: 3px solid var(--edge);
    max-height: 42vh;
  }
}

/* ---- the game list ---- */

/* Over the picture and over the pad, because it is a mode: while it is open,
   the phone is a catalogue rather than a controller. */
.browser {
  position: absolute;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  /* Not quite opaque, and deliberately so: a video a browser considers
     completely obscured is a video it is entitled to pause, and one that was
     paused here stayed paused. Dark enough to read box art against, thin
     enough that the picture behind is still being painted. */
  background: color-mix(in srgb, var(--bg) 94%, transparent);
}
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .browser { background: rgba(16, 14, 40, .94); }
}

.browser-bar {
  display: flex;
  gap: .5rem;
  align-items: center;
  /* Wraps on a narrow screen rather than squeezing. Four controls and a
     controller's name do not fit across a phone held upright, and what they
     did instead was shrink the name until it read "< WIRELESS CONTROL" --
     which is the one thing on that row that says which controller the panel
     is about. */
  flex-wrap: wrap;
  flex: 0 0 auto;
  padding: calc(var(--top-safe) + .7rem)
           clamp(.6rem, 3vw, 1.4rem) .7rem;
  border-bottom: 2px solid var(--bg2);
}
/* The way out is always at the end of the row, wherever the row ends up. */
.browser-bar > .ghost:last-child { margin-left: auto; }
/* The name has a whole row to itself on a narrow screen. `.chip` truncates at
   46vw, which on a phone turns "Xbox Wireless Controller" into "BOX WIRELESS
   CONTROLLE" -- clipped at both ends, because a chip's text is centred, so
   the one thing on that row identifying the controller became the one thing
   on it that could not be read. */
.browser-bar > .chip:first-child { min-width: 0; }
@media (max-width: 30rem) {
  /* Not in the chat panel, whose first chip is the word "Chat" and needs no
     room: this is for a controller's name, which is long and is the one thing
     on that row worth reading. */
  .pads:not(.chat) .browser-bar > .chip:first-child {
    flex: 1 0 100%;
    max-width: 100%;
    text-align: center;
  }
}

/* Collapsed to its icon until asked for. A phone in landscape has room for
   the filters or a search box, not both, and most of the time the filters are
   what is wanted. */
.find {
  /* The bare `input` rule dresses the PIN box: pixel type, centred, widely
     letter-spaced. Right for six digits, wrong for a search field, and
     inherited here because both are inputs. */
  font-family: var(--sans);
  font-size: 1rem;                /* under 16px, iOS zooms the page on focus */
  letter-spacing: normal;
  text-indent: 0;
  text-align: left;
  color: var(--ink);
  background: var(--bg2);
  border: 2px solid var(--dim);
  border-radius: 0;               /* the console has no rounded corners */
  height: 1.75rem;                /* the height of everything else in the row */
  padding: 0 .6rem;

  /* Closed by default. */
  flex: 0 1 0;
  min-width: 0;
  padding-inline: 0;
  border-width: 0;
  opacity: 0;
  overflow: hidden;
  transition: flex-basis .18s ease, opacity .15s ease,
              padding .18s ease, border-width .18s ease;
}
.browser-bar.searching .find {
  flex: 1 1 8rem;
  min-width: 5rem;
  /* Capped, or it takes every pixel the bar has spare. On a desktop that is
     most of the panel, for a field holding two or three words -- and the
     wider it gets the further the eye has to travel from what is typed to the
     games it filters. It still shrinks below this on a phone, which is the
     case the growing was for. */
  max-width: 22rem;
  padding-inline: .6rem;
  border-width: 2px;
  opacity: 1;
}
.find:focus-visible, .chip:focus-visible, .card:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.shelf {
  /* Scrolls, and the stage above it now refuses every gesture to keep
     pinch off the picture, so it asks for the one it needs. */
  touch-action: pan-y;
  flex: 1 1 auto;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  display: grid;
  /* Wide enough for a box to read as a box, narrow enough that a phone in
     portrait still gets two of them side by side. */
  grid-template-columns: repeat(auto-fill, minmax(8.5rem, 1fr));
  gap: .8rem;
  align-content: start;
  padding: .8rem clamp(.6rem, 3vw, 1.4rem)
           calc(1rem + env(safe-area-inset-bottom, 0px));
}

.card {
  /* `button` is uppercased across this page, which suits a label reading
     START and does not suit "The Legend of Zelda: A Link to the Past". */
  text-transform: none;
  letter-spacing: normal;
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: .4rem;
  text-align: left;
  color: var(--ink);
  background: var(--bg2);
  border: 2px solid transparent;
  cursor: pointer;
  font: inherit;
}
@media (hover: hover) {
  .card:hover { border-color: var(--edge); }
}
.card:active { border-color: var(--accent); }

.box {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: contain;
  background: #000;
  display: block;
}
/* A game with no art still needs to be the same shape as one with art, or the
   grid staggers and the list looks broken rather than incomplete. */
.box-none {
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--pixel);
  font-size: .5rem;
  line-height: 1.6;
  color: var(--dim);
  text-align: center;
  padding: .5rem;
}

.card-name {
  font-size: .82rem;
  /* ROM labels run to parenthesised region and language lists with no spaces
     in them, which otherwise push out of the card rather than wrapping. */
  overflow-wrap: anywhere;
  line-height: 1.25;
  /* Long ROM labels are the norm, not the exception. Three lines, then stop. */
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.card-meta {
  font-family: var(--pixel);
  font-size: .42rem;
  line-height: 1.5;
  color: var(--accent);
}

.browse-note {
  margin: 0;
  padding: .6rem clamp(.6rem, 3vw, 1.4rem);
  color: var(--dim);
  font-size: .85rem;
}

/* One height for everything in a row of controls, rather than four heights
   that come out of four different paddings and font sizes. Measured before
   choosing: the chips were 28.3px, the word buttons 24.8 and 26.4, and the
   icon 33 -- which is what made it look wrong beside the others. */
.hud .chip,
.hud .ghost,
.browser-bar .chip,
.browser-bar .ghost {
  height: 1.75rem;
  padding-block: 0;
}
/* The buttons, and the chips that are spans. Giving a span a fixed height puts
   its text against the top edge -- the height is the box, not the line -- so
   anything with a height here has to say where the line sits inside it.
   Selects are left alone: they centre their own text, and telling one to be a
   flex container is asking for a different bug on every engine. */
.hud .ghost,
.browser-bar .ghost,
.hud .chip:not(select),
.browser-bar .chip:not(select) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
/* ---- volume ------------------------------------------------------------
   A speaker that says the level by how many waves it draws, and a slider that
   is not there until it is asked for. Sized and bordered like everything else
   in the row: it is one of the chips, not a widget visiting from elsewhere. */
.vol, .zoom {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
}
/* Lit while it is doing something, so a picture that is zoomed says so from
   the row rather than only from the picture. */
.zoom.is-on #zoom-btn { color: var(--accent); }
/* The waves and the cross are two readings of the same number, so exactly one
   of them is ever showing. */
.vol .vol-wave, .vol .vol-cross { display: none; }
.vol.is-low  .vol-wave-1 { display: inline; }
.vol.is-high .vol-wave-1,
.vol.is-high .vol-wave-2 { display: inline; }
.vol.is-off  .vol-cross  { display: inline; }
.vol.is-off  .vol-btn,
.vol.is-off  #vol-btn { color: var(--dim); }

.vol-range, .zoom-range {
  -webkit-appearance: none;
  appearance: none;
  width: 0;
  /* A flex item's min-width is auto, which for a form control means its own
     intrinsic width -- so width:0 did nothing and the collapsed slider went on
     holding a slider's worth of space between the speaker and the next chip.
     Nothing was visible in it, which is what made the gap look like a spacing
     bug rather than an element. */
  min-width: 0;
  flex: 0 0 auto;
  opacity: 0;
  margin: 0;
  padding: 0;
  border: 0;
  height: 1.75rem;
  background: transparent;
  transition: width .18s ease, opacity .18s ease, margin .18s ease;
  pointer-events: none;
}
.vol.open .vol-range,
.zoom.open .zoom-range {
  width: clamp(5rem, 22vw, 9rem);
  opacity: 1;
  margin-left: .35rem;
  pointer-events: auto;
}
/* A bar with a hard fill and no rounding anywhere: the same square, lit-pixel
   language as the rest of this. --fill is set from the level in app.js. */
.retro-range::-webkit-slider-runnable-track {
  height: .75rem;
  border: 2px solid var(--dim);
  background:
    linear-gradient(to right, var(--accent) var(--fill, 100%),
                    transparent var(--fill, 100%));
}
.retro-range::-moz-range-track {
  height: .75rem;
  border: 2px solid var(--dim);
  background:
    linear-gradient(to right, var(--accent) var(--fill, 100%),
                    transparent var(--fill, 100%));
}
.retro-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: .55rem;
  height: 1.15rem;
  margin-top: -.28rem;          /* centred on a .75rem track */
  background: var(--ink);
  border: 2px solid var(--bg);
  border-radius: 0;
}
.retro-range::-moz-range-thumb {
  width: .55rem;
  height: 1.15rem;
  background: var(--ink);
  border: 2px solid var(--bg);
  border-radius: 0;
}
.retro-range:focus-visible { outline: 2px solid var(--accent); }

/* The stick sliders. Same bar as the volume one -- that look is now on
   .retro-range, which both carry -- but always present rather than sliding
   out of an icon. */
.retro-range {
  -webkit-appearance: none;
  appearance: none;
  background: transparent;
  height: 1.4rem;
}
.pads-tune {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding: .2rem clamp(.6rem, 3vw, 1.4rem) .8rem;
}
.tune {
  display: flex;
  align-items: center;
  gap: .6rem;
  /* Capped, and left where the grid above it starts. A slider is a distance
     the thumb has to travel: stretched across a desktop it takes a swipe of
     the whole screen to move a dead zone by a few per cent, and every value
     in the middle is a pixel wide. Wider is not more precise here, it is
     less usable. */
  max-width: 26rem;
}
.tune-name {
  font-family: var(--pixel);
  font-size: .45rem;
  color: var(--dim);
  text-transform: uppercase;
  flex: 0 0 5.5rem;
}
.tune .pad-range { flex: 1 1 auto; min-width: 0; max-width: 16rem; }
.tune-value {
  font-family: var(--pixel);
  font-size: .45rem;
  color: var(--ink);
  flex: 0 0 2.6rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.pads-tune .footnote { margin: .1rem 0 0; }

/* A button in the grid waiting to be told which physical button it is. */
.key.learning {
  border-color: var(--accent);
  color: var(--accent);
  animation: learn-pulse 1s steps(2, end) infinite;
}
@keyframes learn-pulse { 50% { opacity: .45; } }
@media (prefers-reduced-motion: reduce) {
  .key.learning { animation: none; }
}
.pads-grid .key { cursor: pointer; }

/* An icon button is square: the same height as the row, and as wide. */
.ghost.icon {
  width: 1.75rem;
  padding-inline: 0;
  line-height: 0;
}
.ghost.icon svg {
  width: .95rem;
  height: .95rem;
  display: block;
}

/* Stripped back to the picture. The stage is already exactly the visible
   viewport, so this only takes away the chips -- and only the chips: the
   on-screen pad is the controls for anyone without a controller, and hiding it
   to make the picture bigger would leave them unable to play. */
.stage.immersive .hud { display: none !important; }

/* ---- choosing how to start ---- */

.chooser {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(16, 14, 40, .93);
}
.chooser-box {
  width: min(26rem, 100%);
  display: flex;
  flex-direction: column;
  gap: .6rem;
  padding: 1rem;
  background: var(--bg2);
  border: 3px solid var(--edge);
}
.chooser-name {
  margin: 0;
  font-size: 1.05rem;
  line-height: 1.3;
  overflow-wrap: anywhere;
}
.chooser-meta {
  margin: 0;
  font-family: var(--pixel);
  font-size: .45rem;
  line-height: 1.6;
  color: var(--accent);
}
/* Big enough to hit with a thumb without looking, which is how these get
   pressed -- one hand, phone held sideways, eyes on the television. */
.choice {
  font-family: var(--pixel);
  font-size: .55rem;
  line-height: 1.7;
  text-transform: none;
  color: var(--ink);
  background: rgba(16, 14, 40, .9);
  border: 2px solid var(--dim);
  padding: .8rem .7rem;
  text-align: left;
  cursor: pointer;
}
@media (hover: hover) {
  .choice:hover { border-color: var(--edge); }
}
.choice:active { border-color: var(--accent); }

/* ---- what your controller is doing ---- */

.pads {
  /* Scrolls, and the stage above it now refuses every gesture to keep
     pinch off the picture, so it asks for the one it needs. */
  touch-action: pan-y;
  position: absolute;
  inset: 0;
  z-index: 45;
  display: flex;
  flex-direction: column;
  background: color-mix(in srgb, var(--bg) 94%, transparent);
  /* Sideways there is less height than this panel needs -- measured at 317
     tall in a 297 tall stage on a phone in landscape -- and the stage clips
     what does not fit. Without this the bottom of the panel is not merely off
     the screen, it is unreachable: nothing here scrolls by default, and the
     one thing that could have scrolled was told not to. */
  overflow-y: auto;
  /* A panel that has run out of scroll must not hand the gesture to the page
     behind it, which is a game. */
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
@supports not (background: color-mix(in srgb, red 50%, transparent)) {
  .pads { background: rgba(16, 14, 40, .94); }
}

/* The panel is translucent so the video behind it keeps being composited --
   an obscured video is one the browser may pause -- which means anything else
   on the stage shows through it. The prompt is the only thing big enough to
   read, and it says the same as this panel's own hint. */
.stage:has(.pads:not([hidden])) .prompt { visibility: hidden; }

/* One scroller in this panel, and it is the panel.
 *
 * This grid used to scroll on its own, which on a desktop is a tidy way to
 * keep a long list of buttons in view and on a phone is the thing that makes
 * the whole panel feel broken: a flick lands in whichever scroller happens to
 * be under the thumb, one of them is a few rows tall, and neither moves the
 * other. Nested scrolling is a desktop habit. The panel scrolls; everything
 * in it is simply as tall as it is.
 *
 * Narrower cells on a phone, too: 6.2rem left three columns on a 393-wide
 * screen with the letters crammed at the top of each. Below that width they
 * are wider and there are fewer of them, which reads far better with a thumb.
 */
.pads-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(6.2rem, 1fr));
  gap: .4rem;
  padding: 0 clamp(.6rem, 3vw, 1.4rem);
}
/* One cell per button in the standard mapping. Lit means held right now, so
   the answer to "which button is this" is: press it and see what lights. */
.key {
  display: flex;
  flex-direction: column;
  gap: .15rem;
  padding: .4rem .45rem;
  background: var(--bg2);
  border: 2px solid transparent;
}
.key.on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 22%, var(--bg2));
}
@supports not (background: color-mix(in srgb, red 50%, blue)) {
  .key.on { background: #3a3320; }
}

/* A button that latches: pressed once to hold it down, pressed again to let
   go. Marked on its left edge rather than by colouring the cell, because the
   cell already uses colour for "this is pressed right now" -- and a latched
   button is very often both at once, which is the moment the difference
   matters most. */
.key.is-toggle {
  border-left-color: var(--accent);
  border-left-width: 5px;
  padding-left: calc(.45rem - 3px);
}
/* While the grid is being used to choose them, it is answering a different
   question and says so by the pointer it offers. */
.pads-grid.choosing-toggles .key {
  cursor: copy;
}
.key-name {
  font-family: var(--pixel);
  font-size: .45rem;
  line-height: 1.5;
  color: var(--ink);
}
.key-note { font-size: .62rem; color: var(--dim); }
.key.on .key-note { color: var(--accent); }

.pads-axes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  gap: .35rem .8rem;
  padding: .6rem clamp(.6rem, 3vw, 1.4rem) calc(1rem + env(safe-area-inset-bottom, 0px));
}
.axis { display: flex; align-items: center; gap: .4rem; }
.axis-name {
  font-family: var(--pixel);
  font-size: .4rem;
  color: var(--dim);
  width: 3.4rem;
}
.axis-track {
  position: relative;
  flex: 1 1 auto;
  height: 8px;
  background: var(--bg2);
}
.axis-fill {
  position: absolute;
  top: 0;
  bottom: 0;
  background: var(--ok);
}

/* The optional name on the join form. Deliberately quieter than the PIN: one
   is required to get in and the other is a courtesy. */
label.optional {
  font-size: .5rem;
  color: var(--dim);
}
/* The link box, which is pasted into rather than typed in. The bare `input`
   rule above dresses the PIN: pixel type, centred, widely letter-spaced --
   right for six digits and unreadable for forty-three characters of base64.
   Exactly 1rem, because iOS zooms the whole page in when a field smaller than
   16px takes focus, and a phone held sideways does not come back out of that
   gracefully. */
#key-row { display: flex; flex-direction: column; gap: .35rem; }

/* The key a button is bound to, in the cell with its name. Only there when
   the controls are a keyboard: the cells are otherwise a live picture of a
   controller, and an empty line under every one of them is furniture. */
.key-bind {
  display: block;
  font: .62rem/1.6 var(--pixel);
  letter-spacing: .04em;
  color: var(--accent);
  text-transform: none;
}
.pads:not(.keys) .key-bind { display: none; }

/* The controller's own settings, when there is no controller in this. */
.pads.keys .pad-only { display: none; }
.pads:not(.keys) .keys-only { display: none; }
/* The panel carries `onscreen` while the on-screen pad is the controls it is
   about; a buzz switch over a pad that is not on the screen is a switch for
   nothing. Named `onscreen` rather than `touch` because `.touch` is the pad
   itself, and sideways that is a transparent sheet taking no taps -- a name
   two elements answer to is a rule one of them never asked for. */
.pads:not(.onscreen) .touch-only { display: none; }
/* Shown for a controller and for the on-screen pad, hidden for a keyboard --
   which has "Fix my buttons" for the same job and no diamond to trade. */
.pads.keys .keys-hidden { display: none; }

/* While the television is in a menu, this pad reaches the page and stops
   there. Dimmed rather than hidden or disabled: it still lights under a
   thumb, which is what says the phone is working, and taking the controls
   away would make a pause look like a failure. */
html.held .touch { opacity: .45; }
html.held .touch::after {
  content: "controls paused";
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
  text-align: center;
  font: .55rem/1.6 var(--pixel);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--accent);
  pointer-events: none;
}

/* ---- tabs inside the options panel --------------------------------------
   Two rows of things that are not the same thing: how your own controller
   behaves, and what the television is doing. A tab each, because scrolling
   past every button binding to reach "end game" is a poor way to find it. */
.tabs {
  display: flex;
  gap: .3rem;
  padding: .5rem clamp(.6rem, 3vw, 1.4rem) 0;
  flex: 0 0 auto;
}
.tab {
  flex: 0 0 auto;
  min-height: 2.2rem;
  padding: .45rem .9rem;
  border: 2px solid var(--dim);
  border-bottom: none;
  background: transparent;
  color: var(--dim);
  font-family: var(--pixel);
  font-size: .5rem;
  line-height: 1.5;
  letter-spacing: .03em;
  text-transform: uppercase;
  cursor: pointer;
}
.tab.is-on {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
}
.tab-panel {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  /* It scrolls, and the stage above refuses gestures to keep pinch off the
     picture, so it asks for the one it needs. */
  touch-action: pan-y;
  overscroll-behavior: contain;
  border-top: 2px solid var(--dim);
}
/* The game tab's actions are decisions, not settings: full width, spaced, and
   each with its consequence written under it. */
#tab-game .choice {
  display: block;
  width: 100%;
  margin: .9rem 0 .3rem;
}
#tab-game .footnote { margin: 0 0 .2rem; }

/* ---- the controllers on this machine ------------------------------------
   A row each, name on the left and what it is doing on the right, with one
   button. Rows rather than a picker because "which of these is player three"
   is answered by pointing at a controller, and a row is the same target on a
   phone as across a room. It only appears when there is a choice to make. */
.pad-seats {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  margin: .5rem 0 0;
}
.pad-seat {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex-wrap: wrap;                 /* a long controller name on a narrow phone */
  padding: .35rem .5rem;
  border: 2px solid var(--bg2);
  background: rgba(16, 14, 40, .6);
}
.pad-seat.is-mine { border-color: var(--dim); }
.pad-seat-name {
  flex: 1 1 8rem;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: .8rem;
  color: var(--ink);
}
.pad-seat-state {
  flex: 0 0 auto;
  font-size: .72rem;
  color: var(--dim);
}
.pad-seat.is-mine .pad-seat-state { color: var(--accent); }
/* Comfortably tappable, and never the widest thing in the row. */
.pad-seat-do { flex: 0 0 auto; min-height: 2rem; cursor: pointer; }

/* The end of the shelf: where the next chunk is asked for.
   Across the whole grid rather than sitting in one cell, so it reads as the
   bottom of the list and not as a card that failed to load. */
.shelf-end {
  grid-column: 1 / -1;
  padding: 1rem .5rem 1.4rem;
  text-align: center;
  font-size: .78rem;
  color: var(--dim);
}

/* ---- toast --------------------------------------------------------------
   One line over the picture for something worth knowing and not worth
   interrupting for. The notice above it opens the chips and takes the stage
   out of the stripped-back view to fit itself in; this deliberately changes
   no layout at all -- it is out of flow, it never takes a pointer, and it
   goes on its own. Bottom left, because the top is where the chips appear and
   the middle is the game. */
.toast {
  position: absolute;
  left: .6rem;
  /* Above the keyboard when one is up, or it says its piece from underneath
     it. --desk-lift is zero for everybody else, so this is the same corner
     it has always been in. */
  bottom: calc(var(--bottom-safe, 0px) + .6rem + var(--desk-lift, 0px));
  z-index: 5;                      /* under the notice and the panels */
  max-width: min(22rem, 70vw);
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .45rem .6rem;
  background: rgba(16, 14, 40, .88);
  border-left: 3px solid var(--accent);
  color: var(--ink);
  pointer-events: none;            /* never in the way of the game */
  opacity: 0;
  transform: translateY(.4rem);
  transition: opacity .28s ease, transform .28s ease, bottom .18s ease;
}
.toast.show { opacity: 1; transform: none; }
.toast-what {
  font-family: var(--pixel);
  font-size: .5rem;
  line-height: 1.6;
  color: var(--accent);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.toast-note { font-size: .72rem; color: var(--dim); }

@media (prefers-reduced-motion: reduce) {
  .toast { transition: opacity .28s ease; transform: none; }
}

/* ---- chat --------------------------------------------------------------
   The panel is the controls panel's shape, because it is the same kind of
   thing: a mode that takes the screen while it is open. What differs is that
   it has to keep its footing while the keyboard comes up on a phone -- hence
   a column with one scrolling part and the form pinned under it, rather than
   a page that scrolls as a whole. */
.chat { display: flex; flex-direction: column; }
.chat-log {
  /* Scrolls, and the stage above it now refuses every gesture to keep
     pinch off the picture, so it asks for the one it needs. */
  touch-action: pan-y;
  flex: 1 1 auto;
  /* The keyboard's own animation is what the eye follows; the panel resizing
     underneath it should not be a second thing moving. Kept short: anything
     longer lags behind the keyboard rather than moving with it. */
  scroll-behavior: smooth;
  overscroll-behavior: contain;
  min-height: 0;                 /* or the log refuses to scroll inside flex */
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: .6rem clamp(.6rem, 3vw, 1.4rem);
  display: flex;
  flex-direction: column;
  gap: .45rem;
}
/* Who is here. A single row that scrolls sideways: on a phone in landscape
   the panel is a few hundred pixels tall, and a strip that wrapped to three
   rows would take the conversation off the screen to show a guest list
   nobody asked to see. `flex: 0 0 auto` keeps it out of the log's scrolling. */
.chat-people {
  flex: 0 0 auto;
  display: flex;
  gap: .35rem;
  align-items: center;
  overflow-x: auto;
  /* Sideways, for the same reason the others say pan-y. */
  touch-action: pan-x;
  overscroll-behavior-x: contain;
  scrollbar-width: none;                 /* the chips are the affordance */
  padding: .45rem clamp(.6rem, 3vw, 1.4rem);
  border-bottom: 2px solid var(--bg2);
}
.chat-people::-webkit-scrollbar { display: none; }
/* A chip, like every other chip on the page -- square, dim, in the display
   face -- but tall enough to be a target: this one is meant to be tapped,
   which the status chips are not. */
.chat-person-chip {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  min-height: 2rem;
  padding: .4rem .5rem;
  border: 2px solid var(--dim);
  background: rgba(16, 14, 40, .9);
  color: var(--dim);
  font-family: var(--pixel);
  font-size: .48rem;
  line-height: 1.5;
  letter-spacing: .03em;
  text-transform: uppercase;
  white-space: nowrap;
  max-width: 46vw;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
}
.chat-person-chip.is-me { border-color: var(--accent); color: var(--accent); }
.chat-person-chip.is-open {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}
/* Away is dimmed rather than dropped: somebody whose picture has stopped is
   still in the session and still holding a seat, and a list that quietly lost
   them would be a list that disagrees with the seat picker. */
.chat-person-chip.is-away { opacity: .55; }
.chat-person-ping {
  width: .5rem;
  height: .5rem;
  flex: 0 0 auto;
  background: var(--dim);                /* not measured yet */
}
/* The open chip is filled with the accent colour, which is also the colour a
   middling ping is drawn in -- so on that one chip the dot needs an edge, or
   it vanishes into its own background exactly when somebody is looking at it. */
.chat-person-chip.is-open .chat-person-ping { box-shadow: 0 0 0 1px var(--bg); }
.chat-person-ping.is-good { background: var(--ok); }
.chat-person-ping.is-fair { background: var(--accent); }
.chat-person-ping.is-poor { background: var(--bad); }
/* The numbers, in a readable face. This is the one part of the panel somebody
   reads rather than glances at. */
.chat-person {
  flex: 0 0 auto;
  padding: .55rem clamp(.6rem, 3vw, 1.4rem);
  border-bottom: 2px solid var(--bg2);
  font-family: var(--sans);
  font-size: .82rem;
}
.chat-person-facts {
  margin: 0;
  display: grid;
  /* The term column sizes to its content, so the values line up down the
     panel instead of stepping in and out with the length of each label. */
  grid-template-columns: auto 1fr;
  gap: .2rem .7rem;
}
.chat-person-facts dt { color: var(--dim); }
.chat-person-facts dd { margin: 0; color: var(--ink); font-variant-numeric: tabular-nums; }

.chat-line {
  margin: 0;
  font-size: .9rem;
  line-height: 1.45;
  color: var(--ink);
  /* A name and a sentence, wrapping under each other rather than beside: a
     long name should not squeeze what somebody said into a column. */
  overflow-wrap: anywhere;
}
.chat-who {
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--accent);
  margin-right: .5rem;
}
.chat-form {
  flex: 0 0 auto;
  display: flex;
  /* Stated, because the bare `form` rule above makes every form on this page
     a column -- which is right for the join form it was written for, where a
     label sits over a field, and wrong here, where a box and a Send button
     belong on one line. Inherited quietly, it put Send on its own row under a
     full-width field and looked like a styling choice somebody had made. */
  flex-direction: row;
  align-items: center;
  gap: .5rem;
  padding: .6rem clamp(.6rem, 3vw, 1.4rem)
           calc(.8rem + env(safe-area-inset-bottom, 0px));
  border-top: 2px solid var(--bg2);
}
/* `input.paste` is 100% wide, which is right for the link box it was written
   for and wrong in a row with a button beside it: the button wrapped onto its
   own line under a full-width field. */
.chat-form .paste { flex: 1 1 auto; width: auto; min-width: 0; }
.chat-form .choice { flex: 0 0 auto; align-self: stretch; }
/* The chat button: a bubble, and the count of what has been said since this
   page last looked at it. The icon is stroked rather than filled so it reads
   at chip size on a phone, where it is competing with the clock. */
.chat-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.chat-btn svg {
  width: 1.05rem;
  height: 1.05rem;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linejoin: miter;
}
.chat-btn .chat-dot { fill: currentColor; stroke: none; }

/* Something has been said since this page last looked.
   Square, like everything else here: at six pixels a square reads as a dot
   and a circle reads as the only round thing on the screen. */
.chat-new {
  position: absolute;
  top: -.2rem;
  right: -.2rem;
  width: .42rem;
  height: .42rem;
  background: var(--edge);
  /* A ring of the panel's own colour, so the dot stays visible where it
     overlaps the button's border rather than merging into it. */
  box-shadow: 0 0 0 2px var(--bg);
}
@media (prefers-reduced-motion: no-preference) {
  /* Appearing: two steps, the same motion as everything else here. */
  .chat-new { animation: chat-dot .18s steps(2, end); }
  /* And something arriving: three slow beats, then still. Long enough to
     catch the corner of an eye that is on the game, small enough that it is
     not asking to be looked at. It replaces a banner that covered the
     picture, which was the complaint. */
  .chat-new.arrived { animation: chat-beat 1.1s ease-in-out 3; }
}
@keyframes chat-dot {
  from { transform: scale(0); }
  to   { transform: scale(1); }
}
@keyframes chat-beat {
  0%, 100% { transform: scale(1); }
  35%      { transform: scale(1.65); }
}


/* The off-screen switch that taps an iPhone. Everything here is chosen to
   keep it laid out and animated while keeping it out of sight and out of the
   way of a thumb: display:none, visibility:hidden and zero size would each
   take the feeling with them. */
.haptic-switch {
  position: fixed;
  left: -60px;
  bottom: 0;
  width: 40px;
  height: 24px;
  opacity: 0;
  pointer-events: none;
  z-index: -1;
}

/* A setting and the sentence explaining it. Side by side where there is room
   and stacked where there is not -- the note is a whole sentence, and a
   sentence squeezed into the last two inches of a phone's width wraps into a
   column three words wide. */
.pads-feel {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .35rem .75rem;
  padding-bottom: 0;
}
.pads-feel .footnote { margin: 0; flex: 1 1 12rem; }
@media (max-width: 30rem) {
  /* Under the switch it belongs to, full width, rather than beside it. */
  .pads-feel .footnote { flex: 1 0 100%; }
}

/* A switch that looks like a switch. Square, like everything else here: a
   pill would be the only rounded thing on the screen. The input carries the
   state and is invisible, so the state lives in one place rather than in a
   class somebody has to remember to keep in step. */
.switch {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  cursor: pointer;
  /* iOS greys the whole label on tap otherwise, which reads as a button. */
  -webkit-tap-highlight-color: transparent;
}
.switch input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.switch-track {
  position: relative;
  flex: 0 0 auto;
  width: 2.5rem;
  height: 1.35rem;
  border: 2px solid var(--dim);
  background: var(--bg2);
  transition: border-color .1s steps(2), background .1s steps(2);
}
.switch-knob {
  position: absolute;
  top: 50%;
  left: .18rem;
  width: .75rem;
  height: .75rem;
  margin-top: -.375rem;
  background: var(--dim);
  /* Stepped rather than smooth: it should read as a thing that snapped, the
     way the switch it stands for does. */
  transition: transform .1s steps(3), background .1s steps(2);
}
.switch input:checked + .switch-track {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 20%, var(--bg2));
}
.switch input:checked + .switch-track .switch-knob {
  background: var(--accent);
  transform: translateX(1.05rem);
}
.switch input:focus-visible + .switch-track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.switch-name {
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--dim);
}
.switch input:checked ~ .switch-name { color: var(--accent); }
#pads-buzz-note { margin: 0; }


input.paste {
  font: 1rem/1.4 var(--sans);
  letter-spacing: normal;
  text-indent: 0;
  text-align: left;
  padding: .6rem;
  border-width: 2px;
  border-color: var(--dim);
  color: var(--ink);
  /* A key longer than the box scrolls inside it rather than making the gate
     wider than the phone it is on. */
  width: 100%;
  min-width: 0;
  box-sizing: border-box;
}

input.name {
  font: .8rem/1.4 var(--sans);
  letter-spacing: normal;
  text-indent: 0;
  text-align: center;
  padding: .6rem .5rem;
  border-width: 2px;
  border-color: var(--dim);
  color: var(--ink);
}

/* Which player you are, in the panel that is already about controls. Its own
   line, because it is a different question from what the buttons are called. */
.pads-seat {
  display: flex;
  align-items: center;
  flex-wrap: wrap;          /* three controls and a sentence, on a phone */
  gap: .5rem;
  padding-bottom: 0;
}
.pads-seat .chip { height: 1.75rem; padding-block: 0; }

/* The question before the game stops for everybody. Inside the panel rather
   than floating over it: it is about the row directly above it, and it should
   read as part of that decision. */
.repick-ask {
  margin: .2rem clamp(.6rem, 3vw, 1.4rem) .6rem;
  padding: .7rem .8rem;
  border: 2px solid var(--accent);
  background: var(--bg2);
  display: flex;
  flex-direction: column;
  gap: .45rem;
  align-items: flex-start;
}
.repick-title {
  font-family: var(--pixel);
  font-size: .55rem;
  color: var(--accent);
  text-transform: uppercase;
  margin: 0;
}
.repick-ask .footnote { margin: 0; }
.repick-ask .choice { align-self: stretch; }
.pads-seat .warn { color: var(--accent); }

/* A toggle that is on. The label already says so; this is so it reads as on
   from across the panel rather than only when the words are read. */
.pads .ghost.on {
  color: var(--accent);
  border-color: var(--accent);
}
.pads-seat .ghost { height: 1.75rem; padding-block: 0; }

/* ---- accounts -----------------------------------------------------------
 *
 * A section per kind of thing, each with a heading, rather than one run of
 * buttons and footnotes down the panel. Read as a single list they all looked
 * equally important -- "Log out" carried the same weight as "Lock to
 * accounts" -- and the footnotes, which are asides, took as much room as the
 * actions they explain.
 *
 * The gutter is the panel's, set once here, so headings, body text, buttons
 * and inputs all start on the same line down the left. That was the other
 * half of what looked wrong: .browse-note carried its own padding and
 * .footnote carried none, so alternate paragraphs sat flush against the edge.
 */
.acct {
  padding: 0 clamp(.7rem, 3.5vw, 1.4rem) 1.2rem;
}
.acct-block {
  padding: .9rem 0;
  border-bottom: 2px solid color-mix(in srgb, var(--dim) 45%, transparent);
}
.acct-block:last-child { border-bottom: 0; }
.acct-block[hidden] { display: none; }

/* Small, spaced and dim: a signpost for the section under it, not a
   competitor to the buttons in it. */
.acct-title {
  margin: 0 0 .55rem;
  font-family: var(--pixel);
  font-size: .6rem;
  font-weight: normal;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
}

.acct-name {
  margin: 0 0 .5rem;
  font-size: 1rem;
  color: var(--fg);
}
.acct-state {
  margin: 0 0 .55rem;
  color: var(--dim);
  font-size: .85rem;
}

/* What an account may do, as one chip each. It was a comma-separated sentence
   that ran to five lines on a phone and could not be scanned at all; these
   are the same words, countable at a glance. */
.acct-can {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
  margin: 0 0 .6rem;
}
.acct-can span {
  padding: .25rem .45rem;
  border: 2px solid color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--accent);
  font-size: .75rem;
  line-height: 1.3;
}
.acct-can.acct-none span {
  border-color: var(--dim);
  color: var(--dim);
}

/* One height for the things that sit side by side, so a button beside a box
   does not stand a few pixels proud of it. */
.acct-row {
  display: flex;
  align-items: stretch;
  gap: .5rem;
  margin: 0 0 .5rem;
}
.acct-row .choice,
.acct-row .ghost { flex: 0 0 auto; }

/* The picture presets. They wrap rather than scroll: there are five of them,
   the panel is narrow in portrait, and a row that scrolls sideways hides the
   options at the end from somebody who does not know they are there. */
.stream-presets {
  flex-wrap: wrap;
  gap: .4rem;
}
.stream-preset {
  flex: 0 0 auto;
  cursor: pointer;
}
#limit-count {
  width: 4.5rem;
  min-height: 2.6rem;
  text-align: center;
  font-size: 1rem;
}

/* Actions are decisions: full width, and the sentence that says what they do
   directly under them rather than beside them. */
.acct-block > .choice {
  display: block;
  width: 100%;
  margin: 0 0 .4rem;
}
.acct-block .footnote { margin: 0; }
.acct-block .footnote + .choice { margin-top: .7rem; }

/* The page's `input` rule is sized for the PIN box on the join screen: 1.6rem
   of pixel font with the padding to match, which is right for six digits
   typed off a television and absurd for a settings panel. Three of those
   stacked made the login four hundred and seventy pixels tall, most of it
   empty box. These are ordinary fields. */
#login-sheet label {
  display: block;
  margin: .5rem 0 .15rem;
  font-family: var(--sans);
  font-size: .78rem;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.3;
  color: var(--dim);
}
#login-sheet input[type="text"],
#login-sheet input[type="password"] {
  width: 100%;
  box-sizing: border-box;
  font: 1rem/1.2 var(--sans);
  min-height: 2.5rem;          /* a thumb, not a mouse */
  padding: .45rem .6rem;
  border-width: 2px;
}
#login-sheet input::placeholder { font-family: var(--sans); opacity: .5; }
/* The code is six digits and reads better spaced out, without being a
   billboard about it. */
#login-code { letter-spacing: .25em; text-align: center; }
#login-form .acct-row { margin-top: .7rem; }
#login-form { gap: 0; margin: 0; }
#login-form .acct-row .choice { flex: 1 1 auto; }

/* The tick and its words on one line, both of them tappable. */
.acct-tick {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin-top: .8rem;
  font-family: var(--sans);
  font-size: .85rem;
  line-height: 1.4;
  letter-spacing: 0;
  text-transform: none;
  color: var(--fg);
}
.acct-tick input { width: 1.15rem; height: 1.15rem; flex: 0 0 auto; }

/* Who is here, one per line: a name is easier to read across than a row of
   chips, and "remove" is a thing you want to be sure about before you tap. */
.acct-people {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  margin-bottom: .5rem;
}
.acct-person {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: .6rem;
  padding: .45rem .6rem;
  border: 2px solid var(--dim);
  background: rgba(16, 14, 40, .9);
}
.acct-person span { font-size: .9rem; }
.acct-person .ghost { flex: 0 0 auto; }

/* One account per block, its permissions under its name. A rule between them
   rather than a card each: these are rows in a list, and a border on every one
   would say they are separate objects when the only thing that matters is
   telling one name from the next. */
.grant-row {
  display: flex;
  flex-direction: column;
  gap: .1rem;
  padding: .6rem 0;
  border-top: 2px solid color-mix(in srgb, var(--dim) 45%, transparent);
}
.grant-row:first-child { border-top: 0; padding-top: 0; }
.grant-row .acct-name { margin-bottom: .35rem; font-size: .9rem; }
/* A permission is a sentence, not a heading. The page's global label rule is
   pixel-font, uppercase and letter-spaced -- right for "ENTER THE PIN" over a
   box, and wrong for seven of these in a column, where every one wrapped onto
   two lines and a list of five became a wall. */
.grant-row label {
  display: flex;
  align-items: center;
  gap: .5rem;
  min-height: 2.3rem;
  font-family: var(--sans);
  font-size: .85rem;
  line-height: 1.4;
  letter-spacing: 0;
  text-transform: none;
  color: var(--fg);
}
/* One a guest already has through a wider grant: shown ticked and left alone,
   because "steam" covers every game and unticking one game would not take it
   away. */
.grant-row label.acct-implied { color: var(--dim); }
.grant-row input[type="checkbox"] {
  width: 1.15rem;
  height: 1.15rem;
  flex: 0 0 auto;
}

/* The new link and PIN after a re-share. Selectable, because the whole point
   is to send it to somebody, and wrapped because a link is longer than a
   phone is wide. */
.acct-new-link {
  margin-top: .6rem !important;
  padding: .5rem .6rem;
  border: 2px solid var(--accent);
  user-select: all;
  word-break: break-all;
  color: var(--accent);
}

/* The three lock settings, one per line, with the current one lit. A row of
   choices where one is already true reads better than a button that toggles
   something you cannot see. */
.acct-choices {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  margin-bottom: .5rem;
}
.acct-choices .choice { width: 100%; display: block; margin: 0; }
.acct-choices .choice.is-on {
  border-color: var(--accent);
  color: var(--accent);
}

/* Why the login has come up by itself: an action that needs a fresh
   authenticator code asked for one. Said in the accent colour, because it is
   a request rather than a failure. */
.acct-asking {
  color: var(--accent) !important;
  margin-top: .6rem !important;
}

/* ---------------------------------------------------------------- *
 * Driving the console from a phone.
 *
 * The keyboard button sits bottom-right, out of the way of the on-screen
 * controller's sticks, and only exists while somebody holds the desk.
 * ---------------------------------------------------------------- */

/* How much of the screen the phone's keyboard is covering. Set from JS off
   visualViewport, because there is no CSS that knows. Everything that has to
   get out of the way reads this one number. */
:root { --desk-lift: 0px; }

/* One strip along the bottom holding the scrolling keys and the buttons.
 *
 * The buttons used to be fixed to the bottom-right corner on their own, which
 * the key row then covered whenever it was wider than the screen -- in a
 * browser it always is, because the row has more room to want than it does in
 * the installed app. Sharing a strip puts the row's scrolling and the
 * buttons' position in the same layout instead of leaving them to argue about
 * one corner.
 *
 * It takes no taps of its own: the strip spans the width, and most of that
 * width is picture. Its children take theirs back. */
.desk-dock {
  position: fixed;
  right: 0;
  left: 0;
  bottom: var(--desk-lift);
  z-index: 45;
  display: flex;
  align-items: flex-end;
  justify-content: flex-end;
  gap: .4rem;
  padding: 0 max(.6rem, env(safe-area-inset-right, 0px))
           max(.75rem, env(safe-area-inset-bottom, 0px))
           max(.6rem, env(safe-area-inset-left, 0px));
  pointer-events: none;
  transition: bottom .18s ease;
}
.desk-dock > * { pointer-events: auto; }

/* The countdown to a right click, drawn just above the pointer.
 *
 * Takes no taps of its own -- it appears under a finger that is already down,
 * and a control that could swallow that press would be sabotaging the very
 * gesture it is describing. Its width is the whole of the message, so it is
 * kept small enough not to cover what is being pressed. */
.hold-hint {
  position: absolute;
  z-index: 40;
  /* Small: it sits just above whatever is being pressed, and a wide bar covers
     the thing the press is about. Its job is to say a clock is running, not to
     be read. */
  width: 1.4rem;
  height: .22rem;
  margin: 0;
  padding: 0;
  transform: translate(-50%, -160%);
  background: rgba(16, 14, 40, .82);
  border: 1px solid var(--dim);
  pointer-events: none;
}
.hold-hint > i {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
  animation: hold-fill var(--hold-ms, 500ms) linear forwards;
}
@keyframes hold-fill { from { width: 0; } to { width: 100%; } }
/* Somebody who has asked for less movement gets the bar without the sweep:
   it still appears and still says a press is being counted. */
@media (prefers-reduced-motion: reduce) {
  .hold-hint > i { animation: none; width: 100%; opacity: .6; }
}

/* Out of the way while the pointer is captured.
 *
 * They sit in a corner over the picture, and a locked pointer cannot reach
 * them: it is on the console. So they are obstruction and nothing else until
 * the capture ends, which Escape does -- the same key that gives the pointer
 * back brings them back with it. Faded rather than removed, so the strip does
 * not jump when it returns, and left unclickable so a stray click through a
 * ghost of a button cannot happen. */
.stage.locked .desk-dock {
  opacity: 0;
  pointer-events: none;
}
.stage.locked .desk-dock > * { pointer-events: none; }

/* With the keys up, the strip is a bar across the screen and the padding that
   floats the buttons in a corner would be a gap in it. */
.desk-dock.has-keys {
  padding: 0;
  align-items: stretch;
  background: rgba(16, 14, 40, .95);
  border-top: 2px solid var(--dim);
}

.desk-bar {
  display: flex;
  flex: 0 0 auto;
  gap: .4rem;
  /* Nothing in the corner is text, and holding the button is a gesture here.
     iOS reads a long press on anything selectable as "select this word" and
     puts the blue highlight and its handles over the menu that the same press
     just opened, so the gesture that reaches the pointer settings also
     selects their labels. -webkit-touch-callout stops the magnifier and the
     share sheet that come with it. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
  /* The cursor list is positioned against this, so it has to be the thing
     that "absolute" means. Without it the list anchors to the page and lands
     somewhere else entirely. */
  position: relative;
}
.desk-dock.has-keys .desk-bar {
  /* Pinned to the right of the row rather than scrolling with it: these are
     how the row is put away, and a control that scrolls out of reach is a
     control nobody has. */
  gap: .25rem;
  padding: .3rem max(.4rem, env(safe-area-inset-right, 0px)) .3rem .3rem;
  border-left: 2px solid var(--dim);
  background: rgba(16, 14, 40, .95);
}
.desk-bar[hidden] { display: none; }
.desk-bar button {
  display: flex;
  width: 2.6rem;
  height: 2.6rem;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 2px solid var(--dim);
  border-radius: 0;
  background: rgba(16, 14, 40, .82);
  color: var(--ink);
}
.desk-bar button.is-on { border-color: var(--accent); color: var(--accent); }

/* One button in the corner, and nothing to open.
   Three buttons standing over a thumbstick was three too many, and the
   on-screen controller is the thing most likely to be underneath -- so there
   is one, it offers whichever of the two you are not using, and the pointer is
   reached by holding it.

   Quiet while nothing of the desk's is in use, so it sits over a game without
   asking for attention, and at full strength the moment it is touched or
   something it controls is live. */
#desk-swap { opacity: .55; }
#desk-swap.is-on,
#desk-swap:active,
#desk-swap:focus-visible { opacity: 1; }
.desk-bar svg {
  width: 1.5rem;
  height: 1.5rem;
  display: block;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* One transparent pixel. It must be focusable and rendered for a phone to
   raise its keyboard, and must never be seen or scrolled to.
   A contenteditable rather than an input, to dodge iOS's form accessory bar
   -- see the markup. */
.desk-input {
  position: fixed;
  /* At the top, not the bottom. A phone scrolls the page to bring a focused
     field into view above its keyboard, and a field pinned to the bottom is
     the one place guaranteed to make it do that -- which slides the whole
     picture up by an amount nothing here knows about. At the top there is
     nothing to scroll to. */
  top: 0;
  left: 0;
  z-index: -1;
  width: 1px;
  height: 1px;
  padding: 0;
  border: 0;
  opacity: 0;
  overflow: hidden;
  white-space: nowrap;
  font-size: 16px;          /* under 16px and iOS zooms the page on focus */
  /* Said out loud, and not optional. The stage refuses selection so that
     dragging the picture does not select the furniture, and a contenteditable
     that inherits `none` cannot be typed into at all -- which would be this
     change quietly breaking the thing it exists to improve. */
  user-select: text;
  -webkit-user-select: text;
  /* There is nothing to see in it, so nothing should blink there either. */
  caret-color: transparent;
}

.desk-keys {
  /* In the strip now rather than fixed to the viewport, so the buttons beside
     it are laid out against it instead of over it. min-width is what lets it
     actually scroll: a flex item refuses to shrink below its content without
     it, so the row would push the buttons off the screen instead. */
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-wrap: nowrap;
  gap: .25rem;
  overflow-x: auto;
  padding: .3rem .3rem .3rem max(.4rem, env(safe-area-inset-left, 0px));
  scrollbar-width: none;
  /* It scrolls sideways when there are more keys than room, and it must not
     do anything else: the stage below refuses gestures entirely, and a row
     that allowed pinching would be the one way to zoom the page while
     driving. */
  touch-action: pan-x;
}
.desk-keys[hidden] { display: none; }
.desk-keys::-webkit-scrollbar { display: none; }
.desk-keys button {
  flex: 0 0 auto;
  min-width: 2.6rem;
  padding: .45rem .5rem;
  border: 2px solid var(--dim);
  border-radius: 0;
  background: rgba(28, 22, 66, .9);
  color: var(--ink);
  font-family: var(--pixel);
  font-size: .58rem;
  line-height: 1;
}
/* Latched for the next key only, and locked until tapped off. Two states
   worth telling apart: one of them is about to disappear on its own. */
.desk-keys button.is-latched { border-color: var(--accent); color: var(--accent); }
.desk-keys button.is-locked {
  border-color: var(--accent);
  background: var(--accent);
  color: var(--bg);
}
/* Under the finger, and just after it fired. After the latch states on
   purpose, so both win over them: what a key is doing right now matters more
   than what it will do to the next one.

   These exist because there was nothing at all. The row refuses the tap's
   default -- that is what keeps the phone's keyboard from leaving -- and a
   refused default means :active never arrives, so a plain key like esc or
   home looked identical pressed and unpressed. There was no way to tell a key
   that had not been noticed from one that had been sent and ignored. */
.desk-keys button.is-pressing {
  background: var(--dim);
  border-color: var(--ink);
  color: var(--bg);
}
.desk-keys button.is-firing {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--bg);
}

/* The exact-size boxes beside the virtual display switch.
 *
 * Without this they take the base `input` rule, which is written for the PIN
 * box on the way in: 1.6rem of pixel font, .3em of letter-spacing and nearly
 * a rem of padding. Four digits in that is far wider than the field, so the
 * number was cut off inside a control that dwarfed the row it sat in.
 *
 * Sized like the chips around it instead, and wide enough for four digits
 * with the spinner the browser adds. */
.tune-number {
  font-family: var(--pixel);
  font-size: .48rem;
  line-height: 1.5;
  letter-spacing: .03em;
  text-indent: 0;
  text-align: center;
  padding: .4rem .35rem;
  border: 2px solid var(--dim);
  background: rgba(16, 14, 40, .9);
  color: var(--ink);
  width: 4.6rem;
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
}
.tune-number:focus { border-color: var(--accent); }
/* The times sign between them, so it does not inherit the row's gap as though
   it were another control. */
.tune-by {
  font-family: var(--pixel);
  font-size: .48rem;
  color: var(--dim);
  flex: 0 0 auto;
}

/* Off the screen but not out of the page.
 *
 * For the account-name field that sits beside a verification code so a
 * password manager knows whose code it is. display:none and hidden are both
 * wrong here: a manager skips a field it considers invisible, and then the
 * code is not offered at all -- which is the thing being fixed. */
.offscreen {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* The chip strip, while the pointer is captured.
 *
 * Same reasoning as the desk buttons just above, and asked for in the same
 * words: with the pointer locked it is on the console, so nothing up here can
 * be clicked. The chips are then a row of things sitting over the game that
 * cannot be used, which is all cost and no use. Escape gives the pointer back
 * and brings them with it.
 *
 * Faded rather than removed, so the strip does not jump when it returns, and
 * unclickable so a stray click cannot land on a ghost. */
.stage.locked .hud {
  opacity: 0;
  pointer-events: none;
}
.stage.locked .hud > * { pointer-events: none; }

/* The cursor button while it is in trackpad mode.
 *
 * One button with several meanings needs to say which it is in without a
 * word: the tap swaps the controller and the keyboard, and a press and hold
 * opens the pointer. Marked rather than relabelled, because the label is an
 * icon and there is no room for a sentence over a game. */
#desk-swap.is-relative {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}


/* The list of what the cursor can do.
 *
 * Opened by the cursor button, which used to change the mode on a press and
 * hold -- invisible until found by accident, and then silent about what it
 * had changed to. Sits above the dock rather than in it: the dock is already
 * three buttons wide on a phone and this needs room for a sentence.
 */
.cursor-menu {
  position: absolute;
  bottom: calc(100% + .4rem);
  right: 0;
  z-index: 6;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 15rem;
  max-width: min(22rem, 80vw);
  padding: 2px;
  background: var(--bg2);
  border: 2px solid var(--edge);
}
.cursor-menu button {
  display: block;
  width: 100%;
  text-align: left;
  padding: .5rem .6rem;
  background: none;
  border: 0;
  border-left: 3px solid transparent;
  color: var(--dim);
  font-family: inherit;
  cursor: pointer;
}
.cursor-menu button strong {
  display: block;
  font-family: var(--pixel);
  font-size: .5rem;
  letter-spacing: .04em;
  color: var(--ink);
  text-transform: uppercase;
}
.cursor-menu button span { display: block; font-size: .78rem; line-height: 1.5; }
.cursor-menu button:hover { background: rgba(255, 255, 255, .06); }
/* The one in force, said rather than implied. */
.cursor-menu button.on { border-left-color: var(--accent); }
.cursor-menu button.on strong { color: var(--accent); }
