/*
 * The visitor site: the gate, the lobby, the call and the blocked screen.
 *
 * Mobile first: the base rules are a phone held upright, and min-width queries add
 * room. Tokens and the shared components (buttons, fields, the agreement row, cards,
 * notices, chips, dialogs, toasts) come from styles.css; this file lays them out and
 * adds nothing that looks new. No colour is written here: the few values it needs
 * are component properties made from system tokens (--stage-ground below).
 *
 * Breakpoints:
 *   base                        a phone upright, from 320px
 *   (min-width: 22.5rem)        360: Stop's icon; Report's reasons in two columns
 *   (min-width: 25rem)          400: Next's icon
 *   (min-width: 37.5rem)        600: roomier gate; Report gets its label (not on a
 *                               phone on its side)
 *   (min-width: 56.25rem)       900: two columns, and the desktop call with a chat
 *                               column — but not on a short screen (below)
 *   (min-width: 67.5rem)        1080: a wider chat column
 *   (orientation: landscape) and (max-height: 32rem)
 *                               a phone on its side, whatever its width: full-bleed
 *                               call, compact toolbar, a header that scrolls away
 *   (hover: hover) / (hover: none)
 *                               what only a keyboard and mouse need: the N hint, the
 *                               shortcuts button
 * Touch sizes — 48px fields, .sm buttons kept at 44px — are (pointer: coarse) rules in
 * styles.css; every target here is 44px or more at any size.
 * The desktop call is "(min-width: 56.25rem)" and not "(max-height: 32rem)". app.js
 * must use the same two queries to decide whether the chat is a sheet (a phone) or a
 * column (desktop): a sheet when the first fails or the second matches.
 *
 * Never a transform, filter, contain or container-type on .screen, #call, main or
 * body: each makes the element the containing block of the phone's fixed chat sheet,
 * which would then scroll away with the call instead of sitting on the viewport.
 * Container queries go on leaf components only, and are named: "checks" (the lobby's
 * device row), "stage" (the other person's video) and "toolbar" (the desktop call's
 * controls).
 */

/* ============================================================== page frame */

/*
 * Video is dark whatever the theme: faces stay the brightest thing on screen, and a
 * pale wash around a black frame glares. These are the system's theme-independent
 * dark values, named for where they are used.
 */
#call,
.lobby-preview {
  --stage-ground: var(--ring-gap);
  --stage-dim: var(--on-video);
  --stage-dim: color-mix(in srgb, var(--on-video) 76%, var(--ring-gap));
}

/*
 * How far the call toolbar reaches up from the bottom of the viewport: the chat sheet
 * sits on it and toasts sit above it. Worked out from the toolbar's own rules — on a
 * phone the big Chat button (3.5rem) is its tallest control, so it follows the
 * browser's text size. Redefined per layout below.
 */
:root {
  /* The phone chat sheet's height: the sheet, and the toasts that sit above it. */
  --sheet-h: max(11.25rem, min(40vh, 21.25rem));
  --sheet-h: max(11.25rem, min(40dvh, 21.25rem));
  --toolbar-h: calc(
    2 * var(--sp-2) + 1px + max(var(--tap), 3.5rem) +
      env(safe-area-inset-bottom, 0px)
  );
}

/* Every screen but the call is a page that scrolls: the browser's own toolbars can
   collapse, and Space and Page Down work. One viewport below the header at least, so
   short content still centres. */
.screen {
  min-height: calc(100vh - var(--appbar-h) - env(safe-area-inset-top, 0px));
  min-height: calc(100dvh - var(--appbar-h) - env(safe-area-inset-top, 0px));
}
#gate,
#lobby,
#blocked {
  display: grid;
}

/* Screens arrive, they do not pop. Opacity only on the screen itself (see the header
   comment); only the children rise. */
.screen:not([hidden]) {
  animation: screen-fade var(--dur-4) var(--ease-drift);
}
.gate-wrap > *,
.lobby > * {
  animation: screen-rise var(--dur-4) var(--ease-drift) backwards;
}
.gate-wrap > :nth-child(2),
.lobby > :nth-child(2) {
  animation-delay: 60ms;
}
@keyframes screen-fade {
  from {
    opacity: 0;
  }
}
@keyframes screen-rise {
  from {
    opacity: 0;
    transform: translateY(var(--rise));
  }
}

/* The server connection is down. Slim, under the header, and gone on reconnect. */
.conn-banner {
  position: sticky;
  top: calc(var(--appbar-h) + env(safe-area-inset-top, 0px));
  z-index: calc(var(--z-sticky) - 1);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: 6px max(var(--sp-4), env(safe-area-inset-right, 0px)) 6px
    max(var(--sp-4), env(safe-area-inset-left, 0px));
  border-bottom: 1px solid var(--warn-line);
  background: var(--warn-bg);
  color: var(--text);
  font: 600 var(--t-small) / 1.45 var(--font-ui);
}

/* ==================================================================== gate */

/* What this is, why it is safe, then the one thing to do. Stacked on a phone, the
   promise first; side by side from 900px, reading left to right. Tighter on a
   phone, so its one action — Allow — is on the first screen of a 375×812 phone. */
.gate-wrap {
  display: grid;
  align-content: start;
  gap: var(--sp-3);
  width: min(100%, 70rem);
  margin: 0 auto;
  padding: var(--sp-3) max(var(--sp-4), env(safe-area-inset-right, 0px))
    calc(var(--sp-8) + env(safe-area-inset-bottom, 0px))
    max(var(--sp-4), env(safe-area-inset-left, 0px));
}

.gate-hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-1);
  text-align: center;
}
/* The headline: what this is, in the words people search with, as a quiet line over
   the big type. A search engine reads the h1 literally (Bing above all); the line
   people remember is .gate-slogan right under it. */
.gate-title {
  margin: 0;
  color: var(--text-dim);
  font: 700 var(--t-small) / 1.4 var(--font-ui);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.gate-slogan {
  margin: 0;
  font: 700 var(--t-display) / 1.06 var(--font-display);
  letter-spacing: -0.025em;
  text-wrap: balance;
  overflow-wrap: break-word;
}
.gate-slogan span {
  display: block;
}
.gate-hero .lede {
  margin: 0 auto;
}
/* The promises the headline leaves out, as two quiet tags under the lede: neutral,
   never blue (nothing to press). */
.gate-tags {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--sp-2);
  margin: var(--sp-1) 0 0;
}
.gate-tags > span {
  padding: 2px 10px;
  border: 1px solid var(--line);
  border-radius: var(--r-full);
  background: var(--surface);
  color: var(--text-dim);
  font: 600 var(--t-micro) / 1.5 var(--font-ui);
  letter-spacing: 0.02em;
  white-space: nowrap;
}
/* Narrow phones: no tags row. The card right under the lede already says 18+ and
   that moderators watch, and the row is what pushed Allow off a 375×812 phone's
   first screen once the headline took a line of its own (measured: 11px over). */
@media (max-width: 29.99rem) {
  .gate-tags {
    display: none;
  }
}

/* "18+" drawn as a small badge, the icon for adults only. */
.age-badge {
  display: inline-grid;
  flex: none;
  place-items: center;
  min-width: 1.75rem;
  height: 1.25rem;
  padding: 0 3px;
  border: 1.5px solid currentColor;
  border-radius: var(--r-xs);
  font: 700 var(--t-micro) / 1 var(--font-ui);
}

.gate-card {
  width: 100%;
  max-width: 30rem;
  margin: 0 auto;
}

.rules {
  display: grid;
  gap: var(--sp-1);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* The glyphs share one column, so the three sentences start on one line. */
.rules li {
  display: grid;
  grid-template-columns: 1.75rem minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-3);
}
.rules li > :first-child {
  justify-self: center;
  margin-top: 2px;
  color: var(--text-dim);
}
/* The gate's roomier spacing from 600px: after the base rules, so it wins. */
@media (min-width: 37.5rem) {
  .gate-wrap {
    gap: var(--sp-6);
    padding-top: var(--sp-6);
  }
  .gate-hero {
    gap: var(--sp-3);
  }
  .rules {
    gap: var(--sp-3);
  }
}

/* The disclosure people are agreeing to, at reading size: moderators can see video. */
.disclosure {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-radius: var(--r-md);
  background: var(--surface-2);
}
.disclosure .i {
  margin-top: 2px;
  color: var(--text-dim);
}

/* The two documents the box agrees to, side by side: full 44px targets without
   adding their height to the layout. */
.gate-links {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--sp-5);
  margin: calc(var(--sp-2) * -1) 0;
}
.gate-links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-weight: 600;
}

/* How it works: three numbered steps in neutral badges (blue means "press"). */
.gate-how {
  display: grid;
  gap: var(--sp-3);
  width: 100%;
  max-width: 30rem;
  margin: var(--sp-2) auto 0;
}
.gate-how-title {
  color: var(--text-dim);
  font: 600 var(--t-small) / 1.45 var(--font-ui);
  letter-spacing: 0.02em;
}
.gate-steps {
  display: grid;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.gate-steps li {
  display: grid;
  grid-template-columns: 2rem minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-3);
}
.step-num {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 1px solid var(--line);
  border-radius: 50%;
  background: var(--surface);
  color: var(--text-dim);
  font: 700 var(--t-small) / 1 var(--font-ui);
}
/* The first line's centre on the badge's centre. */
.gate-steps p {
  padding-top: calc(1rem - 0.75em);
  color: var(--text-dim);
}
.gate-steps strong {
  color: var(--text);
}

/* Pressing the locked button draws the eye to what unlocks it. */
.consent.nudge {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

/* Full width, and "Allow camera and microphone" on one line from 360px: the camera
   glyph only comes back where there is room for it beside the words. */
#allow,
#retry-allow {
  width: 100%;
  padding-right: var(--sp-4);
  padding-left: var(--sp-4);
}
#allow .i,
#retry-allow .i {
  display: none;
}
@media (min-width: 25rem) {
  #allow .i,
  #retry-allow .i {
    display: revert;
  }
}
.gate-hint {
  margin-top: calc(var(--sp-2) * -1);
  text-align: center;
}
/* Below 360px "Your browser will ask for permission next." wraps to two lines and the
   nudge's "Check the box above first." fits on one: without a reserved second line the
   card jumped 20px under the finger that had just pressed Allow. */
@media (max-width: 22.4375rem) {
  .gate-hint {
    min-height: 2.9em;
  }
}

/* A refusal's fix, one step per line, the way the browser's own menus read. */
.steps {
  display: grid;
  gap: var(--sp-2);
  margin: 0;
  padding-left: 1.5rem;
}
.steps li {
  padding-left: var(--sp-1);
}
.steps li::marker {
  color: var(--text-dim);
  font-weight: 700;
}
#copy-link {
  align-self: center;
}

/* Keeps Allow on the first screen of a short phone: the disclosure below grew once
   moderators could listen and keep chat, and this claws the height back. Scoped to
   phones only — from 600px the roomier query above already wins on gap and padding,
   and there is height to spare. */
@media (max-width: 37.4375rem) {
  .gate-slogan {
    font-size: clamp(1.375rem, 7vw, 1.75rem);
    line-height: 1.15;
  }
  .gate-hero .lede {
    margin-top: -2px;
  }
  .disclosure {
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--t-small);
    line-height: 1.45;
  }
  #agree-row {
    margin-block: -4px;
  }
  #allow {
    margin-top: -6px;
  }
}

/*
 * The logo's artwork, large, at the one size where the promise side of the gate has
 * room for a picture: the angel and the devil meeting, the site in one image. An
 * <img> per theme, still (no motion). Never on a phone: Allow owns that first
 * screen, and the image is lazy so a phone never downloads it.
 */
.gate-art {
  display: none;
}

@media (min-width: 56.25rem) {
  /* The promise and how it works on the left, the agreement on the right. */
  .gate-wrap {
    grid-template-areas:
      "hero card"
      "how card";
    grid-template-columns: minmax(0, 1fr) minmax(0, 28rem);
    align-content: center;
    align-items: center;
    column-gap: var(--sp-12);
    row-gap: var(--sp-8);
    padding: var(--sp-10) max(var(--sp-10), env(safe-area-inset-right, 0px))
      calc(var(--sp-10) + env(safe-area-inset-bottom, 0px))
      max(var(--sp-10), env(safe-area-inset-left, 0px));
  }
  .gate-hero {
    grid-area: hero;
    align-self: end;
    align-items: flex-start;
    gap: var(--sp-4);
    text-align: left;
  }
  .gate-hero .lede {
    margin: 0;
  }
  .gate-tags {
    justify-content: flex-start;
  }
  .gate-card {
    grid-area: card;
  }
  .gate-how {
    grid-area: how;
    align-self: start;
    max-width: 32rem;
    margin: 0;
  }
  .gate-art {
    display: block;
    width: 14rem;
    height: auto;
    margin-bottom: var(--sp-2);
  }
  /* A full screen of the gate, exactly as before the section below existed: that
     section starts under the fold. */
  .gate-wrap {
    min-height: calc(100vh - var(--appbar-h) - env(safe-area-inset-top, 0px));
    min-height: calc(100dvh - var(--appbar-h) - env(safe-area-inset-top, 0px));
  }
}

/*
 * What ChatDrift.Fun is, and the questions people ask (the gate's second half): for
 * someone deciding, and for search engines. Quiet on purpose — neutral, no blue (the
 * only thing to press is Allow, above), four short points and folded questions in one
 * card of rows, the lobby's shape.
 */
.gate-about {
  display: grid;
  gap: var(--sp-4);
  width: min(100%, 48rem);
  margin: 0 auto;
  padding: var(--sp-8) max(var(--sp-4), env(safe-area-inset-right, 0px))
    calc(var(--sp-10) + env(safe-area-inset-bottom, 0px))
    max(var(--sp-4), env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--line-soft);
}
.about-title {
  font-size: var(--t-heading);
}
.about-title:not(:first-child) {
  margin-top: var(--sp-6);
}
.about-lede {
  color: var(--text-dim);
}
.about-points {
  display: grid;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
.about-points li {
  display: grid;
  align-content: start;
  gap: var(--sp-1);
  padding: var(--sp-4);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
}
.about-points h3 {
  font-size: var(--t-body);
}
.about-points p {
  color: var(--text-dim);
  font-size: var(--t-small);
  line-height: 1.5;
}
@media (min-width: 37.5rem) {
  .about-points {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* =================================================================== lobby */

/* See yourself, see that it works, press Start. Stacked on a phone with Start pinned
   in the thumb zone; side by side from 900px. */
.lobby {
  display: grid;
  align-content: start;
  gap: var(--sp-5);
  width: min(100%, 66rem);
  margin: 0 auto;
  padding: var(--sp-4) max(var(--sp-4), env(safe-area-inset-right, 0px)) 0
    max(var(--sp-4), env(safe-area-inset-left, 0px));
}

.lobby-preview {
  overflow: hidden;
}

/* Capped on a phone so the heading and Start still share the first screen with it.
   The explicit width keeps the cap from narrowing the pane: through aspect-ratio a
   max-height would otherwise become a max-width too, and leave a strip of card. */
.preview-pane {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  max-height: 42vh;
  max-height: 42dvh;
  background: var(--stage-ground);
}
.preview-pane video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Mirror your own preview, as every video app does. */
  transform: scaleX(-1);
}
.preview-empty {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--sp-3);
  padding: var(--sp-6);
  background: var(--stage-ground);
  color: var(--stage-dim);
  font-size: var(--t-small);
  text-align: center;
}
.preview-tag {
  position: absolute;
  top: var(--sp-3);
  left: var(--sp-3);
  z-index: 2;
}
.preview-settings {
  position: absolute;
  top: var(--sp-2);
  right: var(--sp-2);
  z-index: 2;
}
.preview-meter {
  position: absolute;
  bottom: var(--sp-3);
  left: var(--sp-3);
  z-index: 2;
  gap: var(--sp-2);
}

/* An icon button on video: the dark glass the chips use, in both themes. */
.video-btn {
  border-color: transparent;
  background: var(--video-glass);
  color: var(--on-video);
}
@media (hover: hover) {
  .video-btn:hover:not(:disabled) {
    border-color: var(--on-video);
    background: var(--video-glass);
  }
}
.video-btn:active:not(:disabled) {
  background: var(--video-glass);
}

/*
 * Level meters. One element changes per animation frame, and only its transform, so
 * a speaking user costs a composite rather than a layout.
 */
.meter {
  position: relative;
  display: block;
  height: 8px;
  overflow: hidden;
  border-radius: var(--r-xs);
  background: var(--surface-3);
}
.meter-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--ok);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 80ms linear;
}
/* Over video it follows the glass, not the theme. */
.preview-meter .meter {
  width: 4.5rem;
  height: 6px;
  background: var(--line-strong);
  background: color-mix(in srgb, var(--on-video) 24%, transparent);
}
.preview-meter .meter-fill {
  background: var(--on-video);
}
.settings-meter {
  margin-top: var(--sp-3);
}
#mic-hint {
  margin-top: var(--sp-2);
}

/* Camera, mic and network in one row: the mark answers, the words say what about and
   how it is. Each state has its own shape as well as its colour. */
.checks {
  container: checks / inline-size;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: 0;
  padding: 0;
  list-style: none;
}
.checks li {
  display: grid;
  grid-template-areas: "mark name" "mark detail";
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: var(--sp-2);
  min-width: 0;
  padding: var(--sp-3);
}
.checks li + li {
  border-left: 1px solid var(--line-soft);
}
.check-mark {
  grid-area: mark;
}
.check-name {
  grid-area: name;
  font: 600 var(--t-small) / 1.3 var(--font-ui);
}
.check-detail {
  grid-area: detail;
  color: var(--text-dim);
  font-size: var(--t-small);
  line-height: 1.3;
}
/* Narrow: the mark over the words, still one row of three, nothing cut short. */
@container checks (max-width: 26rem) {
  .checks li {
    grid-template-areas: "mark" "name" "detail";
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    row-gap: var(--sp-1);
    padding: var(--sp-3) var(--sp-2);
    text-align: center;
  }
}

.check-mark {
  position: relative;
  display: grid;
  place-items: center;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  font: 700 var(--t-small) / 1 var(--font-ui);
}
/* Pending: a turning ring, always beside the word "Checking…" (it stands still
   under reduced motion). */
.checks li[data-state="pending"] .check-mark {
  border: 2px solid var(--line);
  border-top-color: var(--text-faint);
  animation: spin 0.9s linear infinite;
}
.checks li[data-state="ok"] .check-mark {
  background: var(--ok-bg);
  color: var(--ok);
}
.checks li[data-state="ok"] .check-mark::after {
  content: "";
  width: 0.55rem;
  height: 0.3rem;
  margin-top: -2px;
  border-bottom: 2px solid currentColor;
  border-left: 2px solid currentColor;
  transform: rotate(-45deg);
}
/* A warning is a square with "!", an error a circle with "×": not colour alone. */
.checks li[data-state="warn"] .check-mark {
  border-radius: var(--r-xs);
  background: var(--warn-bg);
  color: var(--warn);
}
.checks li[data-state="warn"] .check-mark::after {
  content: "!";
}
.checks li[data-state="error"] .check-mark {
  background: var(--danger-bg);
  color: var(--danger);
}
.checks li[data-state="error"] .check-mark::after {
  content: "×";
  font-size: var(--t-body);
}

/* One heading, one button, one list: nothing else asks to be read before Start. */
.lobby-setup {
  display: flex;
  flex-direction: column;
  gap: var(--sp-5);
  min-width: 0;
}

/* A notice's words, then its one fix underneath. */
.notice-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--sp-2);
}

/* Start stays in the thumb zone while the page scrolls under it, over a fade so the
   text passing beneath does not collide with it. */
.lobby-actions {
  position: sticky;
  bottom: 0;
  z-index: 2;
  margin: 0 calc(max(var(--sp-4), env(safe-area-inset-right, 0px)) * -1) 0
    calc(max(var(--sp-4), env(safe-area-inset-left, 0px)) * -1);
  padding: var(--sp-3) max(var(--sp-4), env(safe-area-inset-right, 0px))
    calc(var(--sp-3) + env(safe-area-inset-bottom, 0px))
    max(var(--sp-4), env(safe-area-inset-left, 0px));
  background: var(--bg);
  background: linear-gradient(to top, var(--bg) 75%, var(--fade-to));
}
.start {
  width: 100%;
}
/* While a problem is explained, Start is not pinned: pinned, it would sit on top of
   the notice and its fix, and a locked button in view with its reason out of view
   reads as broken. The notice comes first, then Start, in reading order. */
.lobby-setup:has(> #lobby-notice:not([hidden])) .lobby-actions {
  position: static;
  margin: 0;
  padding: 0;
  background: none;
}
/* A control reached by Tab never ends up under the pinned bar. */
html[data-screen="lobby"] {
  scroll-padding-bottom: calc(6rem + env(safe-area-inset-bottom, 0px));
}

/*
 * The quiet ways on from here — the rules, how the game works, staying safe — as one
 * card of rows. Every row is the same shape: a picture in a neutral tile (blue means
 * "press", and these are not the thing to press), the words, and at the end what
 * pressing does (leaves for a new tab, or opens here).
 */
.lobby-links {
  display: flex;
  flex-direction: column;
  margin-bottom: calc(var(--sp-4) + env(safe-area-inset-bottom, 0px));
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--surface);
}
.lobby-links > * + * {
  border-top: 1px solid var(--line-soft);
}
.info-row {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-height: 3.5rem;
  padding: var(--sp-2) var(--sp-4);
  border-radius: var(--r-lg);
  color: var(--text);
  font: 600 var(--t-body) / 1.3 var(--font-ui);
  text-decoration: none;
  cursor: pointer;
}
/* Inside the card's edge, so the ring is never cut by the next row. */
.info-row:focus-visible {
  outline-offset: -3px;
}
.info-icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-dim);
}
/* The game's two faces share one tile, still big enough to tell apart. */
.info-icon.emoji {
  font-size: var(--t-micro);
  letter-spacing: -0.05em;
  line-height: 1;
}
.info-label {
  flex: 1;
  min-width: 0;
}
.info-end {
  flex: none;
  margin-right: 2px;
  color: var(--text-faint);
}
@media (hover: hover) {
  .info-row:hover .info-icon {
    background: var(--surface-3);
    color: var(--text);
  }
  .info-row:hover .info-end {
    color: var(--text-dim);
  }
}

/*
 * The two folded rows: how to play Angel vs. Devil, and staying safe. Closed until
 * pressed, then five short lines, each with one picture and one idea. Each picture
 * sits in the tile's column and each line starts where the row's words do, so the
 * open panel reads as part of its row.
 */
.how-to > summary {
  list-style: none;
}
.how-to > summary::-webkit-details-marker {
  display: none;
}
/* A chevron that says "this opens", and turns when it has. */
.how-to > summary::after {
  content: "";
  flex: none;
  width: 7px;
  height: 7px;
  margin: 0 6px;
  border-right: 2px solid var(--text-faint);
  border-bottom: 2px solid var(--text-faint);
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur-2) var(--ease-drift);
}
.how-to[open] > summary::after {
  transform: translateY(2px) rotate(225deg);
}
.how-to-steps {
  display: grid;
  gap: var(--sp-3);
  margin: 0;
  padding: var(--sp-1) var(--sp-4) var(--sp-4);
  font-size: var(--t-small);
  line-height: 1.45;
  list-style: none;
}
.how-to-steps li {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  align-items: start;
  gap: var(--sp-3);
}
/* 1.4em of the small text (about 20px); a line box of 1.45em of the text, so the
   picture sits on the first line and a wrapped second line does not move it. */
.how-to-mark {
  font-size: 1.4em;
  line-height: calc(1.45em / 1.4);
  text-align: center;
}

/* Side by side from 900px: you on the left, the one heading, Start and the list on
   the right, both from the same top line. Top-aligned on purpose: centred, opening
   a panel grew the column and moved the camera preview under the reader. */
@media (min-width: 56.25rem) {
  /* Top-aligned columns (centred, opening a panel moved the camera preview), a
     little way down the screen: at 48px from the header on a 900px-tall screen
     the lobby sat in the top third with the lower half empty (18 Sep 2026). */
  .lobby {
    grid-template-columns: minmax(0, 1fr) minmax(20rem, 26rem);
    align-content: start;
    align-items: start;
    gap: var(--sp-10);
    padding: clamp(var(--sp-12), 12vh, 8rem)
      max(var(--sp-10), env(safe-area-inset-right, 0px)) var(--sp-10)
      max(var(--sp-10), env(safe-area-inset-left, 0px));
  }
  .preview-pane {
    max-height: none;
  }
  .lobby-setup {
    gap: var(--sp-6);
  }
  .lobby-actions {
    position: static;
    margin: 0;
    padding: 0;
    background: none;
  }
  html[data-screen="lobby"] {
    scroll-padding-bottom: 0;
  }
  .lobby-links {
    margin-bottom: 0;
  }
}

/* ================================================================= blocked */

.blocked-wrap {
  display: grid;
  place-items: center;
  padding: var(--sp-6) max(var(--sp-4), env(safe-area-inset-right, 0px))
    calc(var(--sp-6) + env(safe-area-inset-bottom, 0px))
    max(var(--sp-4), env(safe-area-inset-left, 0px));
}
.blocked-card {
  width: 100%;
  max-width: 30rem;
}
.blocked-card .card-body {
  align-items: center;
  text-align: center;
}
/* Bans are red; a moderator ending a session is not an accusation. */
.blocked-icon {
  display: grid;
  place-items: center;
  width: 4rem;
  height: 4rem;
  border-radius: 50%;
  background: var(--danger-bg);
  color: var(--danger);
}
.blocked-icon.neutral {
  background: var(--surface-2);
  color: var(--text-dim);
}
.blocked-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-3) var(--sp-5);
  margin-top: var(--sp-2);
}
.blocked-actions a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  font-weight: 600;
}

/* ================================================================== footer */

/* The rules and a way to a person, quietly, under every screen but the call (which
   is the whole window). Small, but reading size: this is where someone looks when
   they want to know who is behind the site. Who it is (the mark and one line) with
   the rules beside it, the pages to read on the next row, then the note — stacked
   and centred on a phone, rows from 600px (18 Sep 2026 redesign: three centred
   lines of dot-separated links read as an afterthought). */
.site-footer {
  display: grid;
  gap: var(--sp-2);
  justify-items: center;
  width: min(100%, 70rem);
  margin: 0 auto;
  padding: var(--sp-6) max(var(--sp-4), env(safe-area-inset-right, 0px))
    calc(var(--sp-6) + env(safe-area-inset-bottom, 0px))
    max(var(--sp-4), env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--line-soft);
  color: var(--text-dim);
  font-size: var(--t-small);
  text-align: center;
}
.footer-top {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  width: 100%;
}
.footer-brand {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2) var(--sp-3);
  color: var(--text);
}
.footer-brand img {
  display: block;
  width: auto;
  height: 28px;
}
.footer-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  column-gap: var(--sp-5);
}
.site-footer a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--text-dim);
  font-weight: 600;
  text-decoration: none;
}
.site-footer a:hover,
.site-footer a:focus-visible {
  color: var(--text);
  text-decoration: underline;
  text-underline-offset: 3px;
}
.footer-pages a {
  font-weight: 400;
}
.footer-note {
  color: var(--text-faint);
}
@media (min-width: 37.5rem) {
  .site-footer {
    justify-items: start;
    text-align: left;
  }
  .footer-top {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--sp-2) var(--sp-6);
  }
  .footer-brand,
  .footer-links {
    justify-content: flex-start;
  }
}
/* Nor before the first screen is chosen: every screen starts hidden, so on a slow
   phone the footer sat right under the header for a second, then jumped away (the
   gate's only layout shift, 0.16 at 375 px). */
html[data-screen="call"] .site-footer,
html:not([data-screen]) .site-footer {
  display: none;
}

/* ================================================================== report */

/* Right under the title: the way out and the help that cannot wait, before the
   reasons, so neither is ever below the fold. */
.report-safety {
  display: grid;
  justify-items: start;
  gap: var(--sp-1);
  margin-top: calc(var(--sp-2) * -1);
}
/* Help that cannot wait, in plain words: not red, not an alarm — someone scared needs
   to read it, not flinch at it. */
.report-emergency {
  color: var(--text-dim);
  font-size: var(--t-small);
  line-height: 1.45;
}

/* ==================================================================== call */

/*
 * The call is an app screen: exactly the viewport and never scrolling. The only
 * screen that locks the body, so the others keep native scrolling. On a phone the
 * header goes too: the faces get the height.
 */
html[data-screen="call"] body {
  display: flex;
  flex-direction: column;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
}
/* No pull-to-refresh in a call: a thumb dragging down on an Android phone reloaded
   the page and ended the chat. Only here: on every other screen it is the browser's
   own way to reload, and nothing live is lost. */
html[data-screen="call"],
html[data-screen="call"] body {
  overscroll-behavior-y: none;
}
html[data-screen="call"] main {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-height: 0;
}
html[data-screen="call"] .appbar {
  display: none;
}
html[data-screen="call"] .conn-banner {
  position: fixed;
  top: env(safe-area-inset-top, 0px);
  right: 0;
  left: 0;
  z-index: var(--z-banner);
}

/* Phone upright: the other person full-bleed, you in a corner, the toolbar at the
   bottom and the chat as a sheet above it. */
#call {
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  background: var(--stage-ground);
}

.stage {
  position: relative;
  display: flex;
  flex: 1;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}
.stage-media {
  position: relative;
  flex: 1;
  min-height: 0;
}
/* Video is looked at, not selected: a resting thumb raised iOS's long-press menu
   over the call, and a double press zoomed the page. Pinch-zoom still works. */
.stage-media,
.preview-pane {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
  touch-action: manipulation;
}

.remote-wrap {
  container: stage / size;
  position: absolute;
  inset: 0;
  overflow: hidden;
  background: var(--stage-ground);
}
#remote {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--stage-ground);
}
/* While Report is open their picture is gone at once — no fade — so nobody has to
   keep looking at what they are reporting. */
#call.shielded #remote {
  visibility: hidden;
}

/* The moment of meeting: one sky-blue ring passes over the new face, once per call, never
   on a reconnect, and not at all under reduced motion (app.js skips it). */
.remote-wrap.match-glow::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  border: 2px solid var(--brand);
  border-radius: inherit;
  opacity: 0;
  pointer-events: none;
  animation: match-ring var(--dur-match) var(--ease-drift) forwards;
}
@keyframes match-ring {
  0% {
    opacity: 0;
  }
  18% {
    opacity: 0.9;
  }
  100% {
    opacity: 0;
  }
}

/* You, small and out of the way: a mirror you glance at, not one you stare into.
   .local-wrap is only where it sits (each layout below moves and sizes it);
   .local-frame is the picture itself. */
.local-wrap {
  position: absolute;
  top: max(var(--sp-3), env(safe-area-inset-top, 0px));
  right: max(var(--sp-3), env(safe-area-inset-right, 0px));
  z-index: 3;
  width: 30%;
  max-width: 10rem;
  aspect-ratio: 3 / 4;
  /* Which way the picture tucks away: 1 into the right edge, -1 into the left. */
  --tuck: 1;
}
.local-frame {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  border: 1px solid var(--video-glass);
  border-radius: var(--r-sm);
  background: var(--stage-ground);
  box-shadow: var(--shadow-2);
  transform-origin: right center;
  transition:
    opacity var(--dur-3) var(--ease-drift),
    transform var(--dur-3) var(--ease-drift);
}

/*
 * Your picture hidden from you (setSelfHidden in app.js). It stays laid out, in the
 * viewport and playing, only transparent and out of reach: display: none,
 * visibility: hidden or a move off-screen can let the browser pause a video, and
 * moderators' snapshots and relayed live view draw from this element (frameSource).
 * The empty place takes no presses, so their video under it still does.
 */
#call.self-hidden .local-wrap {
  pointer-events: none;
}
#call.self-hidden .local-frame {
  opacity: 0;
  transform: translateX(calc(var(--tuck) * 12px)) scale(0.92);
}

/*
 * The two arrows. A quiet glass pill, drawn small inside a full 44px target, at the
 * middle of the picture's outer edge; the show tab takes the hide arrow's place, so
 * the same press undoes it. Positioned without a transform: the shared
 * button:active rule nudges every pressed button with one, and a centring transform
 * replaced by it moved the arrow from under the finger before the click landed.
 */
.self-toggle {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  width: var(--tap);
  height: var(--tap);
  min-width: 0;
  margin: auto 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  color: var(--on-video);
  pointer-events: auto;
  transition: none;
}
.self-toggle::before {
  content: "";
  position: absolute;
  top: 50%;
  right: 6px;
  width: 22px;
  height: 36px;
  margin-top: -18px;
  border-radius: var(--r-full);
  background: var(--video-glass);
  box-shadow: inset 0 0 0 1px
    color-mix(in srgb, var(--on-video) 20%, transparent);
  transition: background-color var(--dur-2) var(--ease-drift);
}
/* The chevron sits at the pill's centre, by the same numbers: pill from 6px to
   28px off the edge (centre 17px), icon 16px wide from 9px (centre 17px). */
.self-toggle .i {
  position: absolute;
  top: 50%;
  right: 9px;
  width: 16px;
  height: 16px;
  margin-top: -8px;
  stroke-width: 2.25;
}
/* The full 44px target never paints: the shared hover and active rules gave every
   button a surface colour, which drew a square around the pill on a pointer's
   hover (18 Sep 2026, the owner: "a square appearing over the button"). Only the
   pill (::before) shows a press or a hover. */
button.self-toggle:hover:not(:disabled),
button.self-toggle:active:not(:disabled) {
  background: transparent;
  transform: none;
}
button.self-toggle:active:not(:disabled)::before {
  background: var(--stage-ground);
}
@media (hover: hover) {
  button.self-toggle:hover::before {
    background: var(--stage-ground);
  }
}
button.self-toggle:focus-visible {
  outline: none;
}
button.self-toggle:focus-visible::before {
  box-shadow:
    inset 0 0 0 1px color-mix(in srgb, var(--on-video) 20%, transparent),
    0 0 0 2px var(--focus);
}
/* The show tab: the same pill in the same place (18 Sep 2026, the owner: "the
   buttons are not the same size" — it was 24×44 against the edge), only solid,
   since it is all that's left of the picture. */
.self-show::before {
  background: var(--stage-ground);
}

.local-wrap video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scaleX(-1);
}
.local-wrap .off {
  position: absolute;
  inset: 0;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: var(--sp-1);
  padding: var(--sp-2);
  background: var(--stage-ground);
  color: var(--stage-dim);
  font: 600 var(--t-micro) / 1.33 var(--font-ui);
  text-align: center;
}

/* The top-left row over their video while you talk: the call timer, and their
   Angel vs. Devil badge beside it at either end of the scale (game.js). */
.stage-bar {
  position: absolute;
  top: max(var(--sp-3), env(safe-area-inset-top, 0px));
  left: max(var(--sp-3), env(safe-area-inset-left, 0px));
  z-index: 4;
  display: flex;
  gap: var(--sp-2);
  pointer-events: none;
}

/*
 * Their volume: dark glass in the bottom-left corner of their video, the one corner
 * nothing else uses (the timer is top-left, you are top- or bottom-right). It fades in
 * on a move or a tap and out again (app.js); while faded it takes no clicks but stays
 * in the tab order, and focus brings it back. Hidden with their picture while Report
 * is open.
 */
.volume {
  position: absolute;
  bottom: var(--sp-3);
  left: max(var(--sp-3), env(safe-area-inset-left, 0px));
  z-index: 4;
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  min-height: var(--tap);
  padding: 0 var(--sp-4) 0 var(--sp-3);
  border-radius: var(--r-full);
  background: var(--video-glass);
  color: var(--on-video);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-3) var(--ease-exit);
}
.volume[data-shown="true"] {
  opacity: 1;
  pointer-events: auto;
  transition: opacity var(--dur-2) var(--ease-drift);
}
.volume .i {
  flex: none;
}
/* Over video the fill is white in both themes, like every other mark on the glass;
   the input's full box, not just its thin track, is the touch target. */
.volume input {
  width: 7.5rem;
  height: var(--tap);
  margin: 0;
  accent-color: var(--on-video);
  cursor: pointer;
}
/* A narrow stage (a 320-360px phone): the full slider ran under the Angel vs. Devil
   pop-up in the other corner, where a drag could land on a final pick. */
@container stage (max-width: 23rem) {
  .volume input {
    width: 4.5rem;
  }
}
#call.shielded .volume {
  display: none;
}

/*
 * What the stage says when there is no one to see. A centring grid holding exactly one
 * child: two children make two rows and strand the explanation at the bottom, behind
 * the self-view. It scrolls rather than clips when a short screen cannot fit it.
 */
.stage-overlay {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: grid;
  place-items: center;
  place-items: safe center;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: calc(var(--sp-10) + env(safe-area-inset-top, 0px))
    max(var(--sp-5), env(safe-area-inset-right, 0px)) var(--sp-5)
    max(var(--sp-5), env(safe-area-inset-left, 0px));
  background: var(--stage-ground);
  color: var(--on-video);
  text-align: center;
  transition: opacity var(--dur-3) var(--ease-exit);
}
/* Their face arriving: the overlay fades out rather than cutting away. */
.stage-overlay.fading-out {
  opacity: 0;
}

.overlay-block {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  max-width: 40ch;
}
.overlay-title {
  font: 700 var(--t-heading) / 1.3 var(--font-display);
  letter-spacing: -0.01em;
  text-wrap: balance;
}
.overlay-detail {
  color: var(--stage-dim);
  font-size: var(--t-body);
  line-height: 1.5;
  text-wrap: pretty;
  transition: opacity var(--dur-2) var(--ease-drift);
}
/* The waiting line cross-fades; app.js swaps the text while it is invisible. Two
   lines are reserved so a longer one does not shove the layout. */
.overlay-detail.rotating {
  min-height: 3em;
  max-width: 34ch;
}
.overlay-note {
  font: 600 var(--t-small) / 1.45 var(--font-ui);
  transition: opacity var(--dur-2) var(--ease-drift);
}
/* The search's note arrives after 20 s and can wrap: its two lines are held from the
   start, so the centred block does not creep up the screen when it appears. */
.overlay-detail.rotating + .overlay-note {
  min-height: 2.9em;
}
.overlay-detail.fading,
.overlay-note.fading {
  opacity: 0;
}
/* The search's pinned opener: quiet, under the status, and it does not move. */
.overlay-opener {
  color: var(--stage-dim);
  font: 500 var(--t-small) / 1.45 var(--font-ui);
  max-width: 34ch;
  text-wrap: pretty;
}
.overlay-meta {
  color: var(--stage-dim);
  font: 600 var(--t-small) / 1.45 var(--font-ui);
  font-variant-numeric: tabular-nums;
}
/* One neutral disc for every state: nothing on this stage is an alarm. A faint
   white wash, because the dark glass the chips use vanishes on the dark stage. */
.overlay-icon {
  display: grid;
  place-items: center;
  width: 3.5rem;
  height: 3.5rem;
  margin-bottom: var(--sp-1);
  border-radius: 50%;
  background: var(--video-glass);
  background: color-mix(in srgb, var(--on-video) 12%, transparent);
  color: var(--on-video);
}
/* "Leaving…": the system spinner, drawn in the stage's colours rather than the
   page's (in the light theme those are pale lines on a dark stage). */
.stage-overlay .spinner {
  margin-bottom: var(--sp-2);
  border-color: var(--stage-dim);
  border-color: color-mix(in srgb, var(--on-video) 25%, transparent);
  border-top-color: var(--on-video);
}

/*
 * The picture is the loading state: the owner's artwork, the angel and the devil
 * (17 Sep 2026; the drawn bubbles it replaces were "hated"). Searching: it rises in
 * and floats gently — three times (about 10 s), then rests. Motion that never stops
 * pulls the eye for as long as someone waits; the title already says it is still
 * looking (the waiting line holds still after four changes for the same reason).
 * They left: it sinks away first (.parted), then rises in again. Connecting: it
 * rises in once and stays (.met). A soft light behind it (a plate over video, the
 * one place a gradient is allowed) lifts the navy devil off the navy stage, and
 * breathes with the float. Transform and opacity only; nothing scales past full
 * size and nothing bounces.
 */
.crossing {
  position: relative;
  display: block;
  /* Scales with the stage (the "stage" container is .remote-wrap, sized to the
     video area): big enough to be the picture on a wide desktop, small enough to
     leave the words room on a phone; the short-stage query below shrinks it on a
     phone on its side. */
  width: clamp(10rem, 34cqw, 14rem);
  margin-bottom: var(--sp-3);
  -webkit-user-select: none;
  user-select: none;
}
.crossing::before {
  content: "";
  position: absolute;
  inset: -22% -12%;
  z-index: -1;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    color-mix(in srgb, var(--on-video) 16%, transparent),
    color-mix(in srgb, var(--on-video) 5%, transparent) 60%,
    transparent
  );
  opacity: 0.8;
  pointer-events: none;
}
.crossing-art {
  display: block;
  width: 100%;
  height: auto;
  -webkit-user-drag: none;
}
.crossing.searching {
  animation: var(--arrive) 700ms var(--ease-drift) both;
}
.crossing.searching .crossing-art,
.crossing.searching::before {
  animation: var(--drift) var(--dur-loop) ease-in-out 700ms var(--loops);
}
.crossing.parted {
  animation:
    var(--part) 600ms var(--ease-exit) both,
    var(--arrive) 700ms var(--ease-drift) 600ms forwards;
}
.crossing.parted .crossing-art,
.crossing.parted::before {
  animation: var(--drift) var(--dur-loop) ease-in-out 1300ms var(--loops);
}
.crossing.met {
  animation: var(--arrive) 700ms var(--ease-drift) both;
}
/* One set of rules, a set of keyframes per part: each names its own. */
.crossing {
  --arrive: crossing-arrive;
  --part: crossing-part;
}
.crossing-art {
  --drift: crossing-float;
}
.crossing::before {
  --drift: crossing-glow;
}
@keyframes crossing-arrive {
  from {
    opacity: 0;
    transform: translateY(14px) scale(0.92);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes crossing-part {
  from {
    opacity: 1;
    transform: none;
  }
  to {
    opacity: 0;
    transform: translateY(10px) scale(0.92);
  }
}
@keyframes crossing-float {
  0%,
  100% {
    transform: none;
  }
  50% {
    transform: translateY(-7px);
  }
}
@keyframes crossing-glow {
  0%,
  100% {
    opacity: 0.8;
  }
  50% {
    opacity: 1;
  }
}

/* Without motion the picture would end wherever its last keyframe left it — a
   parted one is invisible — so it stands still, whole, in every state; the title
   says what is happening. The match ring, which is nothing but its animation, is
   not drawn at all. */
@media (prefers-reduced-motion: reduce) {
  /* Each state by name, so no state's own rule outranks this one. */
  .crossing.searching,
  .crossing.parted,
  .crossing.met,
  .crossing .crossing-art,
  .crossing::before {
    animation: none;
    opacity: 1;
    transform: none;
  }
  .remote-wrap.match-glow::after {
    display: none;
  }
}

/* A short stage (a phone on its side, a small window): less frame, smaller picture,
   and the waiting line gives back the height it reserves. */
@container stage (max-height: 22rem) {
  .stage-overlay {
    padding-top: var(--sp-6);
  }
  .crossing {
    width: 7rem;
    margin-bottom: var(--sp-1);
  }
  .overlay-detail.rotating,
  .overlay-detail.rotating + .overlay-note {
    min-height: 0;
  }
}

@media (forced-colors: active) {
  /* A device check's circle and square are fills, which High Contrast removes: an
     edge keeps the shape (circle = fine or failed, square = a warning), so the state
     still reads without colour. The pending ring is already a border. */
  .checks li[data-state="ok"] .check-mark,
  .checks li[data-state="warn"] .check-mark,
  .checks li[data-state="error"] .check-mark {
    border: 1px solid CanvasText;
  }
  /* The chat toggle's pressed look is a fill, which High Contrast removes. */
  .chat-toggle[aria-expanded="true"] {
    outline: 2px solid Highlight;
    outline-offset: -2px;
  }
}

/* The browser refused to play their sound (or their picture) unprompted. */
.unmute {
  position: absolute;
  top: calc(max(var(--sp-3), env(safe-area-inset-top, 0px)) + 3rem);
  /* Left of the self-view (30% at the top right): centred, it ran under it. */
  left: max(var(--sp-3), env(safe-area-inset-left, 0px));
  z-index: 5;
  width: max-content;
  max-width: calc(70% - 2 * var(--sp-3) - var(--sp-2));
}

/* -------------------------------------------------------------- toolbar */

/*
 * Desktop: [Next] [Stop] … [Camera & mic] [Chat] [Report], in DOM order too. Report
 * sits at the opposite edge from Next, so a hand reaching for "move on" never lands
 * on it. Phone and tablet have their own arrangement (below, after the chat
 * toggle's rules; app.js arrangeToolbar moves the buttons to match). While the
 * phone's chat sheet is open the whole bar hides: typing and skipping share no
 * screen.
 */
.controls {
  position: relative;
  z-index: var(--z-toolbar);
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) max(var(--sp-3), env(safe-area-inset-right, 0px))
    calc(var(--sp-2) + env(safe-area-inset-bottom, 0px))
    max(var(--sp-3), env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--line);
  background: var(--raised);
}
.controls .group {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
}
.controls .moves {
  flex: 1;
  min-width: 0;
  /* Next and Stop read the same as any other pair of icon buttons at 8px apart, but
     one skips a person and the other ends the chat — the same gap Report keeps from
     Chat below, and for the same reason. */
  gap: var(--sp-4);
  margin-right: var(--sp-2);
}
/* While the phone's chat sheet is open the whole toolbar hides under it (the
   owner's rule, 13 Sep: typing gets the screen, nothing else to press beside it). Next
   was already hidden (no skipping mid-message); Stop, Settings, Chat and Report go
   with it. visibility, never display: the bar keeps its place and its height, so the
   call does not reflow under the sheet and nothing has moved when it closes (with
   display: none Stop once slid into Next's spot, under the thumb reaching for Next).
   Report stays one press away in the sheet's own header (.chat-report). */
#call.chat-open .next {
  visibility: hidden;
}
#call.chat-open .controls {
  min-height: var(--toolbar-h);
  visibility: hidden;
}

#fullscreen,
#shortcuts-open {
  display: none;
}

#stop {
  flex: none;
  padding-right: 14px;
  padding-left: 14px;
}
.next {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 14rem;
  padding: 15px 20px;
  /* At 150% text on a 320px phone the word ran out of the pill. */
  padding-inline: clamp(12px, 4vw, 20px);
}
#stop .i,
.next .i {
  display: none;
}
@media (min-width: 22.5rem) {
  #stop .i {
    display: revert;
  }
}
@media (min-width: 25rem) {
  .next .i {
    display: revert;
  }
}

/* The flag alone, at every size (the owner's call, 22 Sep): a 44px round button like
   the tools beside it, named by its aria-label and title (app.js words them). Its
   word made Report the loudest thing in the bar — red, and the only label at the
   right edge — for the one action almost nobody presses, and it pulled the eye from
   Next. The guidelines already say "Report (the flag)". */
.report {
  width: var(--tap);
  height: var(--tap);
  padding: 0;
}
/* Report never sits right against Chat: a slip onto it hides and mutes the other
   person. 16px apart, where the buttons elsewhere are 8. */
.controls .group.end {
  margin-inline-start: var(--sp-2);
}
.chat-toggle {
  position: relative;
}
/* Open: the pressed look, so the button that closes the sheet is easy to find. */
.chat-toggle[aria-expanded="true"] {
  border-color: var(--text-dim);
  background: var(--surface-3);
}
.chat-toggle .badge {
  position: absolute;
  top: -4px;
  right: -4px;
  box-shadow: 0 0 0 2px var(--raised);
}

/*
 * Phone and tablet — the sheet layout, the same two queries as isSheetLayout() — in
 * the owner's arrangement (13 Sep, asked for three times; order revised 22 Sep):
 * the icons small and packed at the left edge, [Report] [Camera & mic] [Stop]
 * [Next]; Chat alone, big and blue, at the right edge, where the thumb rests. Next
 * is nearest Chat, in the thumb's reach; Stop is one further out. Also since 22 Sep:
 * Next keeps its word here too. It is the action of the whole product, and as one
 * grey disc among four it had to be recalled by its icon in the middle of a
 * conversation; the word makes it recognised at a glance, and the one labelled
 * button in the row is the one to press. Report and Stop are discs at every phone
 * size (their words are their accessible names), drawn small and packed to the left
 * edge with the camera button between them — a quiet cluster, so the labelled Next
 * and the big Chat are what the eye lands on (the owner's call, 22 Sep). app.js
 * (arrangeToolbar) puts them in this order in the DOM too. Every target stays 44px:
 * the discs are drawn 24px inside it (padding and content-box, like the chat
 * sheet's header buttons), so they look small and are still easy to hit. Report's
 * flag is the text colour — white on the dark theme, ink on the light one's white
 * bar, where white would vanish — never red, and white even while there is nobody
 * to report: then it has no disc, the disc being what says "press".
 */
@media not all and (min-width: 56.25rem), (max-height: 32rem) {
  .controls {
    padding-right: max(var(--sp-2), env(safe-area-inset-right, 0px));
    /* The small cluster sits hard against the left edge (22 Sep): 4px, not 8. */
    padding-left: max(var(--sp-1), env(safe-area-inset-left, 0px));
  }
  /* Flag, camera and Stop touch (their 44px targets; the drawn discs are 20px
     apart), packed left. The one gap is 12px after Stop, before Next: a slip from
     Next onto Stop is the one that ends the chat. */
  .controls .moves {
    gap: 0;
  }
  .controls .moves #stop {
    margin-inline-end: var(--sp-3);
  }
  .controls .group.end {
    display: none;
  }
  .controls .moves button,
  .controls #stop {
    flex: none;
    width: var(--tap);
    min-width: 0;
    max-width: none;
    height: var(--tap);
    padding: 10px;
    border-color: transparent;
    background: var(--surface-3) content-box;
    box-shadow: none;
    color: var(--text);
  }
  .controls .moves button:active:not(:disabled),
  .controls #stop:active:not(:disabled) {
    background: var(--line-strong) content-box;
  }
  .controls .moves button:disabled,
  .controls #stop:disabled {
    background: transparent;
    color: var(--text-faint);
  }
  .controls .moves .report:disabled {
    color: var(--text);
  }
  .controls .moves .i,
  .controls #stop .i {
    display: revert;
    width: 14px;
    height: 14px;
  }
  /* Next is not one of the small discs: its icon keeps the pill's size. */
  .controls .moves .next .i {
    width: 16px;
    height: 16px;
  }
  /* Next: the one labelled button in the row (see above). A pill the height of the
     discs, filled edge to edge — not a 28px disc inside the target, the word needs
     the room — its icon and word together. It gives way before its neighbours do,
     and the word never wraps. */
  .controls .moves .next {
    flex: 0 1 auto;
    width: auto;
    min-width: 0;
    height: var(--tap);
    padding: 0 14px 0 12px;
    gap: 6px;
    border-color: transparent;
    background: var(--surface-3);
    white-space: nowrap;
  }
  .controls .moves .next:active:not(:disabled) {
    background: var(--line-strong);
  }
  .controls .moves .next:disabled {
    background: transparent;
  }
  /* Stop by its icon alone; its word is still its name for a screen reader. */
  .controls #stop > span {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  .controls .chat-toggle {
    width: 3.5rem;
    height: 3.5rem;
    border-color: var(--accent);
    background: var(--accent);
    box-shadow: var(--shadow-primary);
    color: var(--on-accent);
  }
  .controls .chat-toggle:active:not(:disabled) {
    border-color: var(--accent-press);
    background: var(--accent-press);
  }
  .controls .chat-toggle .i {
    width: 28px;
    height: 28px;
  }
  /* On the blue button a blue count would vanish: the text colour instead. */
  .controls .chat-toggle .badge {
    background: var(--text);
    color: var(--bg);
  }
}
@media (hover: hover) {
  @media not all and (min-width: 56.25rem), (max-height: 32rem) {
    .controls .moves button:hover:not(:disabled),
    .controls #stop:hover:not(:disabled) {
      background: var(--line-strong) content-box;
    }
    .controls .moves .next:hover:not(:disabled) {
      background: var(--line-strong);
    }
    .controls .chat-toggle:hover:not(:disabled) {
      border-color: var(--accent-hover);
      background: var(--accent-hover);
    }
  }
}
/* A wide pill from 360px, where the bar has the room. */
@media (min-width: 22.5rem) {
  @media not all and (min-width: 56.25rem), (max-height: 32rem) {
    .controls .chat-toggle {
      width: 5.5rem;
    }
  }
}
/* Below 360px the bar has no room for both: Next keeps its word and drops its icon,
   as the narrowest desktop bar does. */
@media (max-width: 22.4375rem) {
  .controls .moves .next .i {
    display: none;
  }
}

/* ------------------------------------------------------------ chat panel */

/*
 * Phone and tablet: a sheet over the call. Closed, the toolbar is there; open, the
 * sheet comes down to the bottom edge in the toolbar's place (the toolbar hides, see
 * above) and the call shows through it — its ground is 45% transparent, with the
 * header and the message box kept solid so their words and buttons read over any
 * picture. Small on purpose: a chat that hides the person you are talking to
 * defeats the point.
 *
 * --keyboard-inset lifts it above the on-screen keyboard where the browser does not
 * (iOS Safari); app.js measures it from the visual viewport. With the keyboard up the
 * toolbar is under the keys anyway, so the sheet sits on whichever is higher. Android
 * Chrome resizes the page instead (interactive-widget in index.html), and there the
 * inset stays 0.
 */
.side {
  position: fixed;
  right: 0;
  bottom: var(--toolbar-h);
  bottom: max(var(--toolbar-h), var(--keyboard-inset, 0px));
  left: 0;
  z-index: var(--z-sheet);
  display: flex;
  flex-direction: column;
  height: var(--sheet-h);
  min-height: 11.25rem;
  border-width: 1px 0 0;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  box-shadow: var(--sheet-shadow);
  transform: translateY(calc(100% + var(--toolbar-h)));
  visibility: hidden;
  transition:
    transform var(--dur-3) var(--ease-exit),
    visibility 0s linear var(--dur-3);
}
#call.chat-open .side {
  bottom: var(--keyboard-inset, 0px);
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  transform: none;
  visibility: visible;
  transition:
    transform var(--dur-3) var(--ease-drift),
    visibility 0s;
}
/* The message box stays solid: it is where you type. */
#call.chat-open #chat-form {
  background: var(--surface);
}
/* At the bottom edge now: clear of the home indicator. */
#call.chat-open #chat-form {
  padding-bottom: calc(var(--sp-2) + env(safe-area-inset-bottom, 0px));
}
/* What the stage says stays above the open sheet: centred in the part of the stage
   the sheet leaves, not behind it (the search clock sat half under its edge). The
   sheet covers the toolbar's place and --sheet-h minus that of the stage. */
#call.chat-open .stage-overlay {
  padding-bottom: calc(var(--sheet-h) - var(--toolbar-h) + var(--sp-5));
}
/* On the see-through part — the header too (the owner's call) — the words that
   are not bubbles sit on the video glass, like the chips over video: legible over
   a white frame or a black one. */
#call.chat-open .chat-title,
#call.chat-open .msg.system,
#call.chat-open #messages:empty::before,
#call.chat-open .typing {
  border-radius: var(--r-full);
  background: var(--video-glass);
  color: var(--on-video);
}
#call.chat-open #messages:empty::before {
  padding: var(--sp-2) var(--sp-3);
  /* Two or three lines: a rounded box, not a pill. */
  border-radius: var(--r-md);
}
#call.chat-open .typing {
  align-self: flex-start;
  margin: 0 var(--sp-3) var(--sp-2);
  padding: 2px var(--sp-3);
}
#call.chat-open .chat-title {
  padding: var(--sp-1) var(--sp-3);
}
/*
 * The header's two buttons, on glass discs: a small white Report flag and a red
 * Close (the owner's call: red for the way out of the sheet, the flag kept quiet).
 * The disc is painted 32px inside the 44px target (padding + content-box), so the
 * buttons look small and stay easy to hit.
 */
#call.chat-open .chat-report,
#call.chat-open .chat-close {
  padding: 6px;
  border-color: transparent;
  background: var(--video-glass);
  background-clip: content-box;
}
#call.chat-open .chat-report {
  color: var(--on-video);
}
#call.chat-open .chat-report:disabled {
  color: color-mix(in srgb, var(--on-video) 45%, transparent);
}
#call.chat-open .chat-close {
  color: var(--video-danger);
}
@media (hover: hover) {
  #call.chat-open .chat-report:hover:not(:disabled),
  #call.chat-open .chat-close:hover {
    background-color: var(--stage-ground);
  }
}

.side > header {
  padding: var(--sp-1) max(var(--sp-2), env(safe-area-inset-right, 0px))
    var(--sp-1) max(var(--sp-4), env(safe-area-inset-left, 0px));
  color: var(--text);
}
.chat-title {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
  border-radius: var(--r-full);
}
/* Report, in the sheet: the toolbar's Report is hidden under it. Beside the "Chat"
   title at the left (the owner's call, 16 Sep), the width of the header away from
   Close — a slip between the two would hide and mute the other person. */
.chat-report {
  margin-inline-start: var(--sp-2);
}
.chat-sub {
  display: none;
  color: var(--text-faint);
  font-weight: 400;
}

#messages {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: var(--sp-1);
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--sp-3) max(var(--sp-3), env(safe-area-inset-right, 0px))
    var(--sp-3) max(var(--sp-3), env(safe-area-inset-left, 0px));
}
#messages:empty::before {
  /* The words are in copy.js (UI.chat.empty, emptyNote), set by app.js. The note is
     the sheet's "Cleared when the chat ends": the desktop column says it in its
     header instead. */
  content: attr(data-empty) "\A" attr(data-empty-note);
  white-space: pre-line;
  max-width: 24ch;
  margin: auto;
  color: var(--text-faint);
  font-size: var(--t-small);
  text-align: center;
}

.msg {
  max-width: 80%;
  padding: 8px 12px;
  border-radius: var(--r-lg);
  font-size: var(--t-body);
  line-height: 1.4;
  overflow-wrap: anywhere;
  white-space: pre-wrap;
  animation: msg-in var(--dur-3) var(--ease-drift);
  /* Transparent until High Contrast paints it: there the bubbles lose their fill,
     and yours and theirs differed only by which side they sat on. */
  border: 1px solid transparent;
}
/* Yours in a quiet blue tint, theirs neutral: which side and a hint of colour say
   who is speaking. Tinted, never solid, so the chat never outshouts the face or
   looks like something to press. */
.msg.you {
  align-self: flex-end;
  border-bottom-right-radius: var(--r-xs);
  background: var(--you-bubble);
  color: var(--on-you-bubble);
}
.msg.them {
  align-self: flex-start;
  border-bottom-left-radius: var(--r-xs);
  background: var(--them-bubble);
  color: var(--text);
}
/* 4px between one person's messages, 12px when the speaker changes. */
.msg.you + .msg.them,
.msg.them + .msg.you {
  margin-top: var(--sp-2);
}
.msg.system {
  align-self: center;
  max-width: 100%;
  margin: var(--sp-1) 0;
  padding: 2px var(--sp-3);
  background: none;
  color: var(--text-faint);
  font-size: var(--t-small);
  text-align: center;
  animation: none;
}
@keyframes msg-in {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
}

/* "They're typing…": three dots fading in turn. Drift, not bounce. */
.typing {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 0 var(--sp-4) var(--sp-2);
  color: var(--text-faint);
  font-size: var(--t-small);
}
.typing-dots {
  display: inline-flex;
  gap: 3px;
}
.typing-dots i {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: currentColor;
  opacity: 0.3;
  animation: typing-fade 1.2s ease-in-out infinite;
}
.typing-dots i:nth-child(2) {
  animation-delay: 0.2s;
}
.typing-dots i:nth-child(3) {
  animation-delay: 0.4s;
}
@keyframes typing-fade {
  0%,
  60%,
  100% {
    opacity: 0.3;
  }
  30% {
    opacity: 1;
  }
}

#chat-form {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: var(--sp-2) max(var(--sp-3), env(safe-area-inset-right, 0px))
    var(--sp-2) max(var(--sp-3), env(safe-area-inset-left, 0px));
  border-top: 1px solid var(--line-soft);
}
#chat-input {
  border-radius: var(--r-full);
  padding-left: var(--sp-4);
}

/* ------------------------------------------------------------- desktop */

/*
 * Wide and tall enough: the stage and the chat side by side, the header back, and
 * the toolbar as a card under the video — [Next] [Stop] · [tools] [Report]. Not on
 * a short screen, which keeps the phone layout (see the header comment).
 */
@media (min-width: 56.25rem) {
  @media not all and (max-height: 32rem) {
    /* The toolbar card, plus the call's padding under it. */
    :root {
      --toolbar-h: calc(
        2 * var(--sp-2) + 2px + max(var(--tap), 24px + 1.25rem) + var(--sp-4) +
          env(safe-area-inset-bottom, 0px)
      );
    }
    html[data-screen="call"] .appbar {
      display: flex;
    }
    /* Under the header, in the flow: the stage gives up the banner's height rather
       than having the header's logo covered. */
    html[data-screen="call"] .conn-banner {
      position: relative;
      top: auto;
      z-index: auto;
    }
    #call {
      display: grid;
      grid-template-columns: minmax(0, 1fr) 20rem;
      gap: var(--sp-4);
      padding: var(--sp-4) max(var(--sp-4), env(safe-area-inset-right, 0px))
        calc(var(--sp-4) + env(safe-area-inset-bottom, 0px))
        max(var(--sp-4), env(safe-area-inset-left, 0px));
      background: var(--bg);
    }
    .stage {
      gap: var(--sp-3);
    }
    .remote-wrap {
      border-radius: var(--r-lg);
    }
    #remote {
      object-fit: contain;
    }
    .local-wrap {
      top: auto;
      right: var(--sp-3);
      bottom: var(--sp-3);
      width: clamp(9rem, 22%, 15rem);
      max-width: none;
      aspect-ratio: 4 / 3;
    }
    .stage-bar {
      top: var(--sp-3);
      left: var(--sp-3);
    }
    .stage-overlay {
      padding: var(--sp-10) var(--sp-6) var(--sp-6);
    }
    /* Centred again: here the self-view is at the bottom right. And the chat is a
       column, not a sheet, so toasts stay at the toolbar. */
    .unmute {
      top: var(--sp-10);
      right: 0;
      left: 0;
      max-width: none;
      margin: 0 auto;
    }
    html[data-screen="call"]:has(#call.chat-open) .toasts {
      bottom: calc(var(--toolbar-h) + var(--sp-3));
    }

    /* Next first here (the phone puts it beside Chat instead): [Next] [Stop] …
       [tools] [Report]. */
    .controls {
      container: toolbar / inline-size;
      z-index: auto;
      /* Last resort, after Next has shrunk and lost its icon (below): a second row,
         packed to the right so Report keeps the far edge from Next. Only reached with
         the browser's text set small, where 56.25rem is a 675px window around 44px
         buttons; --toolbar-h assumes one row. */
      flex-wrap: wrap;
      justify-content: flex-end;
      padding: var(--sp-2) var(--sp-3);
      border: 1px solid var(--line);
      border-radius: var(--r-lg);
      background: var(--surface);
      box-shadow: var(--shadow-1);
    }
    #fullscreen {
      display: inline-flex;
    }
    /* Never narrower than Next and Stop: squeezed below them at 900px, with a 20rem
       chat column beside the bar, Stop was drawn under the camera button ("Sto" and a
       gear). Next gives way instead, from 9rem down to its own word and icon. */
    .controls .moves {
      min-width: auto;
    }
    .next {
      flex: 0 1 9rem;
      min-width: auto;
      max-width: none;
      padding: 11px 24px;
    }
    .chat-toggle,
    .chat-close,
    .chat-report {
      display: none;
    }
    /* The column is not a sheet: solid, and the toolbar always there. */
    #call.chat-open .side {
      background: var(--surface);
    }
    #call.chat-open .stage-overlay {
      padding-bottom: var(--sp-6);
    }
    #call.chat-open .chat-title {
      padding: 0;
      background: none;
      color: inherit;
    }
    #call.chat-open .controls {
      visibility: visible;
    }

    /* The chat is a column, always there. */
    .side {
      position: static;
      height: auto;
      min-height: 0;
      border-width: 1px;
      border-radius: var(--r-lg);
      box-shadow: var(--shadow-1);
      transform: none;
      visibility: visible;
      transition: none;
    }
    .side > header {
      padding: 12px 8px 12px 16px;
    }
    .chat-sub {
      display: inline;
    }
    #messages:empty::before {
      content: attr(data-empty);
    }
    #messages,
    #chat-form {
      padding-right: var(--sp-3);
      padding-left: var(--sp-3);
    }

    #call:fullscreen {
      background: var(--bg);
    }
  }
}

/* The narrowest desktop bar has no room even for Next's icon: the word stays, as on a
   phone below 400px. Measured on the bar itself (the desktop .controls), not the
   window, because the chat column beside it decides how wide it is. */
@container toolbar (max-width: 33rem) {
  .next .i {
    display: none;
  }
}

/* The shortcuts button, only in the desktop call and only with a pointer that hovers:
   N, F and Enter need a keyboard. */
@media (min-width: 56.25rem) and (hover: hover) {
  @media not all and (max-height: 32rem) {
    #shortcuts-open {
      display: inline-flex;
    }
  }
}

@media (min-width: 67.5rem) {
  @media not all and (max-height: 32rem) {
    #call {
      grid-template-columns: minmax(0, 1fr) 21.25rem;
    }
  }
}

/* ------------------------------------------------------ phone on its side */

/*
 * A short screen: the other person full-bleed and uncropped, you in a corner, a
 * compact toolbar, the chat sliding in from the right, and the notch or Dynamic
 * Island padded on whichever side it is. On the gate and the lobby the header
 * scrolls away instead of taking a sixth of the height.
 */
@media (orientation: landscape) and (max-height: 32rem) {
  :root {
    --toolbar-h: calc(
      12px + 1px + max(var(--tap), 24px + 1.25rem) +
        env(safe-area-inset-bottom, 0px)
    );
  }
  .appbar {
    position: static;
  }
  .conn-banner {
    top: 0;
  }

  .gate-wrap {
    grid-template-columns: minmax(0, 1fr) minmax(0, 24rem);
    align-items: start;
    gap: var(--sp-6);
    padding-top: var(--sp-4);
  }
  .gate-hero {
    align-items: flex-start;
    text-align: left;
  }
  /* Height is what a phone on its side lacks: the headline at title size. */
  .gate-slogan {
    font-size: var(--t-title);
  }
  .gate-hero .lede {
    margin: 0;
  }
  .lobby {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: start;
    gap: var(--sp-5);
    padding-top: var(--sp-3);
  }
  /* Header, preview and the check row on the first screen. */
  .preview-pane {
    max-height: max(9rem, calc(100vh - 10.5rem));
    max-height: max(9rem, calc(100dvh - 10.5rem));
  }
  .lobby-actions {
    position: static;
    margin: 0;
    padding: 0;
    background: none;
  }
  html[data-screen="lobby"] {
    scroll-padding-bottom: 0;
  }

  #remote {
    object-fit: contain;
  }
  .local-wrap {
    top: max(var(--sp-2), env(safe-area-inset-top, 0px));
    width: auto;
    height: 38%;
    max-width: none;
    aspect-ratio: 4 / 3;
  }
  .stage-bar {
    top: max(var(--sp-2), env(safe-area-inset-top, 0px));
  }
  .stage-overlay {
    padding-top: var(--sp-6);
  }
  .controls {
    padding-top: 6px;
    padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
  }
  .next {
    padding-top: 11px;
    padding-bottom: 11px;
  }
  /* The compact bar's height: Chat as tall as the rest here. */
  .controls .chat-toggle {
    height: var(--tap);
  }

  :root {
    --sheet-w: min(22rem, 50vw);
  }
  /* The sheet is a column at the right here: toasts beside it, at the bottom
     edge, where the hidden toolbar was. */
  html[data-screen="call"]:has(#call.chat-open) .toasts {
    right: calc(var(--sheet-w) + var(--sp-3));
    bottom: calc(var(--sp-3) + env(safe-area-inset-bottom, 0px));
  }
  .side {
    left: auto;
    width: var(--sheet-w);
    height: min(70vh, 20rem);
    height: min(70dvh, 20rem);
    min-height: 0;
    border-width: 1px 0 0 1px;
    border-radius: var(--r-lg) 0 0 0;
    transform: translateX(calc(100% + 1px));
  }
  /* Beside the stage here, not under it: no room to give at the bottom. */
  #call.chat-open .stage-overlay {
    padding-bottom: var(--sp-5);
  }
  /* The sheet comes in over the self-view's corner, so the self-view moves to the
     other side, under the timer, where a letterboxed picture has its black edge. */
  #call.chat-open .local-wrap {
    top: calc(
      max(var(--sp-2), env(safe-area-inset-top, 0px)) + 28px + var(--sp-2)
    );
    right: auto;
    left: max(var(--sp-3), env(safe-area-inset-left, 0px));
    /* On the left, the picture tucks into the left edge: arrows mirrored. */
    --tuck: -1;
  }
  #call.chat-open .local-frame {
    transform-origin: left center;
  }
  #call.chat-open .self-toggle {
    right: auto;
    left: 0;
  }
  #call.chat-open .self-toggle::before {
    right: auto;
    left: 6px;
  }
  #call.chat-open .self-toggle .i {
    right: auto;
    left: 9px;
    transform: scaleX(-1);
  }
}

/* ================================================================= toasts */

/* One at a time. Under the header on the pages; above the toolbar in a call, so a
   toast never covers a face. */
.toasts {
  position: fixed;
  top: calc(var(--appbar-h) + env(safe-area-inset-top, 0px) + var(--sp-3));
  right: max(var(--sp-3), env(safe-area-inset-right, 0px));
  left: max(var(--sp-3), env(safe-area-inset-left, 0px));
  margin: 0 auto;
  max-width: 26.25rem;
}
html[data-screen="call"] .toasts {
  top: auto;
  bottom: calc(var(--toolbar-h) + var(--sp-3));
}
/* With the phone chat sheet open, above the sheet: at the toolbar a toast sat
   exactly on the message box ("You're back online" mid-message). The open sheet
   stands on the bottom edge (or the keyboard), not on the hidden toolbar. */
html[data-screen="call"]:has(#call.chat-open) .toasts {
  bottom: calc(var(--keyboard-inset, 0px) + var(--sheet-h) + var(--sp-3));
}
/* Site toasts carry no button, so they never take a tap meant for what is under
   them. */
.toast {
  pointer-events: none;
}
/* Inside an open dialog (the only place a toast can be seen while one is up, see
   app.js) it goes to the top: on a phone the dialog is a sheet at the bottom, and a
   toast there would sit on its buttons. */
dialog .toasts,
html[data-screen="call"] dialog .toasts {
  top: calc(env(safe-area-inset-top, 0px) + var(--sp-3));
  bottom: auto;
}

/* ================================================================ dialogs */

/* Report's reasons: native radios, the whole row the target. One column on the
   narrowest phones, where "Harassment or hate" would not fit half the width; two
   from 360px. The legend floats so it lays out like any label above a field. */
.reason-set {
  min-width: 0;
  margin: 0;
  padding: 0;
  border: 0;
}
.reason-set > legend {
  float: left;
  width: 100%;
  margin-bottom: var(--sp-2);
  padding: 0;
  color: var(--text-dim);
  font: 600 var(--t-small) / 1.45 var(--font-ui);
}
.reasons {
  display: grid;
  clear: both;
  gap: var(--sp-2);
}
.reasons .choice {
  gap: 10px;
  min-height: var(--tap);
  padding: 10px;
}
@media (min-width: 22.5rem) {
  .reasons {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.shortcuts {
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--sp-3) var(--sp-4);
  margin: 0;
}
.shortcuts dt {
  text-align: right;
}
.shortcuts dd {
  margin: 0;
  color: var(--text-dim);
}

/* ============================================================ diagnostics */

/* ?debug=1 only: what the peer connection is doing, for whoever is troubleshooting. */
.debug-panel {
  position: fixed;
  bottom: 8px;
  left: 8px;
  z-index: var(--z-debug);
  width: min(27rem, calc(100vw - 16px));
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  background: var(--surface);
  color: var(--text-dim);
  font: 400 var(--t-micro) / 1.5 var(--font-mono);
  box-shadow: var(--shadow-2);
}
.debug-panel > header {
  display: flex;
  align-items: center;
  gap: var(--sp-2);
  padding: 6px 9px;
  border-bottom: 1px solid var(--line-soft);
}
.debug-panel button {
  padding: 2px 10px;
  font: inherit;
}
.debug-log {
  max-height: 34vh;
  overflow: auto;
  padding: 8px 11px;
  white-space: pre-wrap;
}
