:root {
  --bg: #14161a;
  --panel: #1d2027;
  --line: #2c313b;
  --text: #e7e9ee;
  --muted: #9aa3b2;
  --ok: #4ade80;
  --warn: #fbbf24;
  --bad: #f87171;
  --accent: #60a5fa;
}

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 2rem 1.25rem;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  display: flex;
  justify-content: center;
}

main {
  width: 100%;
  max-width: 34rem;
}

h1 {
  font-size: 1.35rem;
  margin: 0 0 0.25rem;
}

.sub {
  color: var(--muted);
  margin: 0 0 1.5rem;
  font-size: 0.9rem;
}

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 1.25rem;
  margin-bottom: 1rem;
}

label {
  display: block;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--muted);
  margin-bottom: 0.35rem;
}

input,
select,
button {
  font: inherit;
  border-radius: 7px;
  border: 1px solid var(--line);
  background: #12141a;
  color: var(--text);
  padding: 0.55rem 0.7rem;
  width: 100%;
}

button {
  background: var(--accent);
  border-color: var(--accent);
  color: #0b1020;
  font-weight: 600;
  cursor: pointer;
}

button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

button.secondary {
  background: transparent;
  border-color: var(--line);
  color: var(--text);
  font-weight: 500;
}

.field {
  margin-bottom: 0.9rem;
}

.row {
  display: flex;
  gap: 0.6rem;
}

.status {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.9rem;
  margin-bottom: 0.75rem;
}

.dot {
  width: 9px;
  height: 9px;
  border-radius: 50%;
  background: var(--muted);
  flex: none;
}

.dot.ok {
  background: var(--ok);
}
.dot.warn {
  background: var(--warn);
}
.dot.bad {
  background: var(--bad);
}

.meter {
  height: 8px;
  background: #12141a;
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--line);
}

.meter > div {
  height: 100%;
  width: 0%;
  background: var(--ok);
  transition: width 80ms linear;
}

dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.3rem 1rem;
  margin: 0.9rem 0 0;
  font-size: 0.85rem;
}

dt {
  color: var(--muted);
}

dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

a {
  color: var(--accent);
}

.log {
  margin-top: 0.9rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.78rem;
  color: var(--muted);
  max-height: 9rem;
  overflow-y: auto;
  white-space: pre-wrap;
}

/* ---------------------------------------------------------------------------
 * The speaking indicator
 *
 * Deliberately the largest thing on the page. Its only job is to be readable
 * from across a room, out of the corner of an eye, while its owner is busy
 * talking to someone — so it is a word and a colour, not an icon, and the
 * reason underneath is a full sentence rather than a symbol.
 * ------------------------------------------------------------------------- */

.indicator {
  border: 2px solid var(--line);
  border-radius: 10px;
  padding: 1.4rem 1rem;
  text-align: center;
  background: #12141a;
  transition:
    background 90ms linear,
    border-color 90ms linear;
}

.indicator-word {
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  line-height: 1.15;
}

.indicator-why {
  color: var(--muted);
  font-size: 0.85rem;
  margin-top: 0.4rem;
}

/* Hiro's "behind" readout (web/behind.js): small, under the reason line. */
.indicator-behind {
  font-size: 0.8rem;
  margin-top: 0.3rem;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  opacity: 0.85;
}

.indicator.speaking {
  background: #0d2b18;
  border-color: var(--ok);
}
.indicator.speaking .indicator-word {
  color: var(--ok);
}

.indicator.playing {
  background: #16233a;
  border-color: var(--accent);
}
.indicator.playing .indicator-word {
  color: var(--accent);
}

/* Focus lost. Dimmed AND labelled: a dimmed indicator alone reads as "off",
 * which is the one thing it must not be mistaken for. */
.indicator.inactive {
  opacity: 0.55;
  border-style: dashed;
}
.indicator.inactive .indicator-word {
  color: var(--warn);
}

.indicator.fault-state {
  background: #2c1417;
  border-color: var(--bad);
}
.indicator.fault-state .indicator-word {
  color: var(--bad);
}

.fault {
  border: 1px solid var(--warn);
  /* inline-start: the left edge, or the right one on a right-to-left page */
  border-inline-start-width: 4px;
  border-radius: 8px;
  padding: 0.7rem 0.85rem;
  margin-bottom: 0.6rem;
  font-size: 0.9rem;
  color: var(--warn);
  background: #221b0c;
}

.fault.fatal {
  border-color: var(--bad);
  color: var(--bad);
  background: #2c1417;
}

dd.bad {
  color: var(--bad);
  font-weight: 600;
}

button.secondary.on {
  background: var(--ok);
  border-color: var(--ok);
  color: #06210f;
  font-weight: 600;
}

.kbd {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.85em;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 4px;
  padding: 0.05em 0.4em;
  background: #12141a;
}

table.keys {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.85rem;
  margin-top: 0.9rem;
}
table.keys th,
table.keys td {
  text-align: left;
  padding: 0.35rem 0.5rem;
  border-bottom: 1px solid var(--line);
  font-variant-numeric: tabular-nums;
}
table.keys th {
  color: var(--muted);
  font-weight: 500;
  text-transform: uppercase;
  font-size: 0.72rem;
  letter-spacing: 0.05em;
}
.verdict {
  border-radius: 8px;
  padding: 0.8rem 0.9rem;
  margin-top: 0.9rem;
  border: 1px solid var(--line);
}
.verdict.ok {
  border-color: var(--ok);
  color: var(--ok);
}
.verdict.bad {
  border-color: var(--bad);
  color: var(--bad);
}

/* ---------------------------------------------------------------------------
 * Video
 *
 * Above the controls, never over them. Hiro's speaking indicator, the clock and
 * the interpreter status all have to stay readable while he is talking, so the
 * video takes a fixed block at the top and everything else keeps its place
 * underneath. The self-view is the one thing allowed to overlap, because it is
 * the one thing nobody needs to read.
 * ------------------------------------------------------------------------- */

.stage {
  margin-bottom: 1rem;
}

.remote-wrap {
  position: relative;
  width: 100%;
  aspect-ratio: 4 / 3;
  background: #0b0d12;
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
}

.remote-wrap > #remote-video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.video-placeholder {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: 0.9rem;
}

/* WITHOUT THIS, `hidden` DOES NOTHING HERE.
 *
 * The browser hides [hidden] with `display: none` from its own stylesheet,
 * and ANY author rule beats the whole user-agent sheet regardless of
 * specificity. So `display: flex` above silently won every time, and
 * "No camera" has been painted across the middle of every working remote
 * video since this page was written. Nobody saw it until two real cameras
 * were on at once, which is the last thing on the page that had never been
 * tested together. */
.video-placeholder[hidden] {
  display: none;
}

#local-video {
  position: absolute;
  right: 0.6rem;
  bottom: 0.6rem;
  width: 28%;
  max-width: 9rem;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 7px;
  border: 1px solid var(--line);
  background: #0b0d12;
}

#local-video.off {
  opacity: 0.25;
}

.controls {
  margin-top: 0.6rem;
}

button.danger {
  border-color: var(--bad);
  color: var(--bad);
}

/* ---------------------------------------------------------------------------
 * The language chooser
 *
 * The client may not read a word of English, so this is names in their own
 * scripts and nothing else. Big enough to hit with a thumb on a phone, which is
 * where most clients will open the link.
 * ------------------------------------------------------------------------- */

.langgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 0.6rem;
  margin-top: 0.9rem;
}

button.lang {
  background: #12141a;
  border: 1px solid var(--line);
  color: var(--text);
  font-size: 1.15rem;
  font-weight: 500;
  padding: 1rem 0.6rem;
  min-height: 3.6rem;
}

button.lang.suggested {
  border-color: var(--accent);
}

button.lang.chosen {
  background: var(--accent);
  color: #0b1020;
}

/* Refused for this session's translator (an older, simultaneous link). */
button.lang.unavailable {
  opacity: 0.35;
  cursor: not-allowed;
}

h2 {
  font-size: 1.05rem;
  margin: 0 0 0.2rem;
}

.hint {
  color: var(--muted);
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
}

/* ---------------------------------------------------------------------------
 * The new-session page
 * ------------------------------------------------------------------------- */

.linkrow {
  display: flex;
  gap: 0.6rem;
  align-items: flex-end;
  margin-bottom: 0.8rem;
}

.linkrow > div {
  flex: 1;
  min-width: 0;
}

.linkrow button {
  width: auto;
  white-space: nowrap;
}

.panel.made {
  border-color: var(--ok);
}

.session {
  border-top: 1px solid var(--line);
  padding: 0.8rem 0 0;
  margin-top: 0.8rem;
}

.session:first-child {
  border-top: 0;
  margin-top: 0;
}

.session-head {
  display: flex;
  justify-content: space-between;
  gap: 0.8rem;
  align-items: baseline;
  margin-bottom: 0.5rem;
  flex-wrap: wrap;
}

.session.expired {
  opacity: 0.5;
}

.session .row button {
  width: auto;
  flex: 0 0 auto;
  font-size: 0.85rem;
  padding: 0.4rem 0.6rem;
}

/* ---------------------------------------------------------------------------
 * A link that has run out
 *
 * Said first in English, then in every language this build speaks — the one
 * person most likely to see this page is a client who cannot read the first
 * line, and detecting their language is guesswork when the session that knew it
 * is gone.
 * ------------------------------------------------------------------------- */

.ended-panel {
  text-align: center;
}

.ended-headline {
  font-size: 1.4rem;
  font-weight: 600;
  margin-bottom: 1rem;
}

.ended-others {
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1.9;
}

/* ---------------------------------------------------------------------------
 * "Tap to hear the interpreter"
 *
 * The fallback for a browser that still refuses to start audio after the
 * language tap. Sized to be impossible to miss and impossible to mistake for
 * decoration: a client who scrolls past this hears total silence for the whole
 * session and has no way to work out why.
 *
 * Sticky, so it stays on screen on a phone however far they scroll, and full
 * width so the tap target is the banner rather than a word inside it.
 * ------------------------------------------------------------------------- */

.unblock {
  position: sticky;
  top: 0;
  z-index: 20;
  margin: -0.5rem 0 1rem;
}

.unblock-btn {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.7rem;
  min-height: 4.2rem;
  font-size: 1.15rem;
  font-weight: 600;
  background: var(--accent);
  border-color: var(--accent);
  color: #0b1020;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
}

.unblock-icon {
  font-size: 1.1rem;
  line-height: 1;
}

/* A plain blue button, on Hiro's request (2026-09-27). It used to pulse, and
 * with the phone's Reduce Motion setting on it wore an orange outline instead;
 * its size, position at the top of the screen and wording carry it. */

/* ---------------------------------------------------------------------------
 * The mark
 *
 * Its own row at the top of <main>, never positioned over anything. The brief
 * asks that it not overlap the speaking indicator or the video, and the way to
 * guarantee that at every width is to give it space of its own rather than to
 * place it absolutely and hope the reflow is kind.
 *
 * The white wordmark is the one that belongs here: the colour version's text is
 * navy, for light backgrounds, and this page is #14161a.
 * ------------------------------------------------------------------------- */

.logo {
  display: block;
  width: 160px;
  height: auto;
  margin: 0 0 1.1rem;
  /* The asset is 3x for a 160px slot, so it stays sharp on a phone. */
}

@media (max-width: 420px) {
  .logo {
    width: 132px;
    margin-bottom: 0.9rem;
  }
}

/* ---------------------------------------------------------------------------
 * Captions
 *
 * Verification, not subtitles: each person reads their own words as the system
 * heard them, so they can catch a mishearing. Small and secondary by design —
 * capped in height so they can never push the speaking indicator or the video
 * off the screen, which is the one thing they must not do.
 *
 * Last few lines only. No history, no export, nothing to scroll back through.
 * ------------------------------------------------------------------------- */

.captions {
  margin-top: 0.7rem;
  max-height: 7.5rem;
  overflow-y: auto;
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--muted);
  border-top: 1px solid var(--line);
  padding-top: 0.55rem;
}

.captions:empty {
  display: none;
}

.caption {
  display: flex;
  gap: 0.55rem;
  margin-bottom: 0.3rem;
}

.caption-who {
  flex: 0 0 auto;
  min-width: 3.4rem;
  color: var(--line);
  text-transform: uppercase;
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  padding-top: 0.15rem;
}

/* Your own words get the brighter treatment: catching YOUR mishearing is the
 * thing you are being shown this for. */
.caption.own .caption-text {
  color: var(--text);
}

.caption.own .caption-who {
  color: var(--accent);
}

/* Still arriving. Dimmed rather than hidden, because a line that appears and
 * then grows reads as live; a line that appears complete and then changes
 * reads as a correction. */
.caption.pending .caption-text {
  opacity: 0.72;
}

/* A turn that carried nothing. Marked out from the captions around it, because
 * it is the one line in that stream that is ABOUT the conversation rather than
 * part of it — and because the person reading it has, by definition, just been
 * given silence to interpret. Warning-coloured, not error-coloured: nothing is
 * broken, a turn was lost. */
.caption.empty .caption-text {
  color: var(--warn);
  font-style: italic;
}

/* A turn the streaming translator was too short to answer, transcribed and
 * spoken instead. Marked because the voice changes, and an unexplained change
 * of voice mid-session reads as a fault rather than a design decision. */
.caption.rescued .caption-who::after {
  content: '·';
  margin-inline-start: 0.3rem;
  color: var(--accent);
}

.caption.rescued .caption-text {
  border-inline-start: 2px solid var(--line);
  padding-inline-start: 0.45rem;
}

/* The other person's own voice, underneath the interpretation.
 *
 * Only their microphone track. The interpreter is never attenuated by this at
 * any setting, including zero — a control that can silence the interpretation
 * is a way to lose a session without knowing why. */
/* One full-width button that says what pressing it does.
 *
 * It began as a label beside a control, and on a phone the label collapsed to
 * a one-character-wide column down the left — many languages and a narrow
 * screen leave no room for a caption next to anything. The button carries the
 * whole meaning instead, so there is nothing to squeeze. */
.voice-row {
  margin-top: 0.9rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
}

.voice-row button {
  width: 100%;
}

/* No state colour, deliberately.
 *
 * It first reused `.on`, which is the LATCH button's style — a green fill that
 * is loud on purpose, because the latch holds a microphone open and someone
 * needs to notice. This is a listening preference. It is not what the session
 * is about, and a control that shouts competes with the things that should.
 *
 * The state is already in the words: the button says what pressing it will do,
 * so "turn the original voice on" can only appear while it is off. */

/* ---------------------------------------------------------------------------
 * The session controls.
 *
 * First thing in Hiro's panel and deliberately large. He presses Start with a
 * client already watching him on camera, so it has to be one unambiguous
 * target rather than something to aim at. End is red and never sits where
 * Start was a moment ago — a misfire here drops a paying client mid-session.
 * ------------------------------------------------------------------------- */

.session-controls {
  display: flex;
  gap: 0.6rem;
  margin-bottom: 0.7rem;
}

.session-btn {
  flex: 1;
  padding: 0.85rem 1rem;
  font-size: 1.05rem;
  font-weight: 600;
  border-radius: 9px;
}

/* Start and Resume take the page's existing primary look — the filled button
   Join already uses — rather than a colour invented for this row. Pause takes
   `secondary` and End takes `secondary danger`, both of which already exist.
   The only thing this file adds is the size. */
/* End is the smaller of the two, deliberately. The button he reaches for
   mid-session is Pause; End drops a paying client and should not be the
   easiest thing on the row to hit. `width: auto` because the base rule gives
   every button 100%, which would otherwise make this the wide one. */
.session-btn.danger {
  flex: 0 0 auto;
  width: auto;
  padding-left: 1.1rem;
  padding-right: 1.1rem;
}

.session-note {
  margin: 0 0 0.2rem;
  font-size: 0.82rem;
}

/* ---------------------------------------------------------------------------
 * Bigger picture.
 *
 * One class on <body>. It widens the page and gives the video the height,
 * and it hides the reference readouts — the numbers are for diagnosing, and
 * he is not diagnosing while he is looking at someone's face. What stays is
 * what he acts on: the video, whether he has the floor, and the controls.
 *
 * This is NOT native fullscreen. Native fullscreen is requested as well where
 * the browser has it, but iPhone Safari will not give it for anything but a
 * bare <video>, and that is where his clients are. So the layout has to carry
 * this on its own everywhere.
 * ------------------------------------------------------------------------- */

body.expanded {
  padding: 0;
  align-items: stretch;
}

body.expanded main {
  max-width: none;
  width: 100%;
  padding: 0.5rem;
}

body.expanded .logo,
body.expanded h1,
body.expanded #title,
body.expanded .sub,
body.expanded #log-panel,
body.expanded #host-panel dl {
  display: none;
}

/* The session note stays: it is the one line that says whether anything is
   being translated right now. */
body.expanded .session-note {
  display: block;
}

body.expanded .stage {
  margin-bottom: 0.5rem;
}

/* Sized so that what is UNDER it still fits: the controls and the indicator.
   A picture that filled the screen and pushed "SPEAKING" off the bottom would
   be a worse trade than a smaller picture — he can see the client either way,
   but he cannot tell whether he is on the air any other way. */
body.expanded .remote-wrap {
  aspect-ratio: auto;
  height: min(70vh, calc(100vh - 20rem));
  min-height: 12rem;
}

body.expanded .remote-wrap > #remote-video {
  object-fit: contain;
}

body.expanded .panel {
  padding: 0.75rem;
  margin-bottom: 0.5rem;
}

/* ---------------------------------------------------------------------------
 * Before Start: the box shows YOU.
 *
 * Outside a running session the far camera is not being received, so the big
 * frame is dark and the self-view is a thumbnail in the corner of it — which
 * is backwards, because checking his own framing and lighting is the whole
 * reason he opens this page ten minutes early. So the self-view takes the
 * frame until the session starts, and shrinks back to the corner when the
 * other person's picture arrives to fill it.
 * ------------------------------------------------------------------------- */

.stage.preflight #local-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  aspect-ratio: auto;
  border: 0;
  border-radius: 0;
}


/* ---------------------------------------------------------------------------
 * The session clock, in the corner of the picture.
 *
 * On the video rather than in the readout table because the table is the
 * first thing "Bigger" hides, and the time is the one number he wants while
 * he is looking at someone's face. Dimmed and small: it is for glancing at
 * between sentences, not for reading.
 * ------------------------------------------------------------------------- */

.video-clock {
  position: absolute;
  top: 0.55rem;
  right: 0.55rem;
  padding: 0.2rem 0.5rem;
  border-radius: 6px;
  background: rgba(8, 10, 14, 0.62);
  color: var(--text);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  opacity: 0.8;
  pointer-events: none;
}

.video-clock[hidden] {
  display: none;
}

/* The clock pauses with the session, and says so rather than just stopping —
   a frozen number is indistinguishable from a broken one. */
.video-clock.paused {
  color: var(--warn);
  opacity: 1;
}

/* ---------------------------------------------------------------------------
 * The client's way out.
 *
 * They are on a phone. Without this they close the lid on a live session, and
 * the next time the browser restores that tab it walks them back into a
 * session that finished hours ago.
 * ------------------------------------------------------------------------- */

.client-end-row {
  margin-top: 0.7rem;
}

.rejoin-row {
  margin-top: 1rem;
}

/* ---------------------------------------------------------------------------
 * Which microphone
 *
 * No `display` rule on .mic-row, deliberately: it is hidden with the `hidden`
 * attribute until a microphone is published, and an author `display` would
 * beat the browser's [hidden] rule — the bug that painted "No camera" across
 * every working video. tools/test-hidden-attribute.mjs enforces it.
 * ------------------------------------------------------------------------- */

.mic-row {
  margin-top: 0.9rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
}

.mic-row .mic-label {
  margin-bottom: 0.35rem;
}

/* What the track is actually using — read back from it, not from the menu. */
.mic-device {
  margin-top: 0.4rem;
  font-size: 0.85rem;
  color: var(--muted);
  overflow-wrap: anywhere;
}

.mic-device.bad {
  color: var(--bad);
}

/* ---------------------------------------------------------------------------
 * Hiro's Speak button
 *
 * Fixed to the bottom, where a thumb is, and outside the panels so "Bigger"
 * keeps it. No `display` rule on .speak-bar: it is shown and hidden with the
 * `hidden` attribute, which an author display would silently disarm (see
 * tools/test-hidden-attribute.mjs).
 *
 * Two states that cannot be mistaken for each other: a plain "Tap to speak",
 * and a solid red "Speaking — tap to finish". Disabled says why on the button.
 * ------------------------------------------------------------------------- */

.speak-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 20;
  padding: 0.6rem 1rem calc(0.6rem + env(safe-area-inset-bottom));
  background: rgba(11, 16, 32, 0.92);
  border-top: 1px solid var(--line);
}

/* Room at the foot of the page so the bar never covers the last controls. */
body.has-speak-bar {
  padding-bottom: 6.5rem;
}
/* The preset row makes the bar taller: keep the page's last rows clear of it. */
body.has-speak-bar:has(#preset-row:not([hidden])) {
  padding-bottom: 11rem;
}

/* Hiro's voice presets (web/presets.js): above Speak, in the bar "Bigger"
   keeps. Large enough to tap; side by side, wrapping on a phone. Filled while
   on — unmistakable during a meditation, in a glance. */
.preset-row {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  margin-bottom: 0.5rem;
}
.preset-row[hidden] { display: none; }
.preset-buttons {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.5rem;
}
.preset-btn {
  min-height: 44px;
  padding: 0.55rem 1.1rem;
  border-radius: 999px;
  border: 2px solid var(--accent, #6b8cff);
  background: transparent;
  color: var(--text);
  font-size: 1rem;
  font-weight: 600;
}
.preset-btn.on {
  background: var(--accent, #6b8cff);
  color: #0b1020;
}
.preset-btn.on::before { content: '\2713\00a0'; }
.preset-btn.pending { opacity: 0.7; }
.preset-btn:disabled { opacity: 0.45; }
.preset-note {
  font-size: 0.8rem;
  color: var(--muted);
  min-height: 1em;
}

#speak-btn {
  display: block;                   /* the button is never [hidden]; the bar is */
  width: 100%;
  max-width: 40rem;
  margin: 0 auto;
  min-height: 4.25rem;
  font-size: 1.3rem;
  font-weight: 600;
  touch-action: manipulation;       /* no double-tap zoom delay */
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;      /* no long-press menu on iOS */
  -webkit-tap-highlight-color: transparent;
}

#speak-btn.speaking {
  background: var(--bad);
  border-color: var(--bad);
  color: #fff;
}

#speak-btn:disabled {
  background: #12141a;
  border-color: var(--line);
  color: var(--muted);
}

/* ---------------------------------------------------------------------------
 * Hiro's Settings, folded under a <details>.
 *
 * No `display` rule on anything here that is shown or hidden with the
 * `hidden` attribute (see tools/test-hidden-attribute.mjs); the fold is the
 * browser's own <details>. Not `.sub`, which "Bigger" hides — the panel stays
 * reachable there.
 * ------------------------------------------------------------------------- */

.settings {
  margin-top: 0.9rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--line);
}

.settings summary {
  cursor: pointer;
  font-weight: 600;
}

.settings-body select {
  width: 100%;
}

.settings-label {
  margin: 0.7rem 0 0.3rem;
}

.settings-why,
.settings-state {
  margin: 0.5rem 0 0;
  font-size: 0.85rem;
  color: var(--muted);
}

.settings-why:empty,
.settings-state:empty {
  margin: 0;
}

.settings-state.bad {
  color: var(--bad);
}

/* ---------------------------------------------------------------------------
 * The translation on the video.
 *
 * Bottom-left of the picture: the self-view is bottom-right and the clock
 * top-right, so this stops short of the self-view's width. Two turns at most,
 * capped at a third of the frame — on a phone held upright the frame is only
 * ~280 px tall and the face must stay visible. Carried by the page's own
 * layout, so it shows in "Bigger" (iPhone Safari has no page fullscreen).
 *
 * NO `display` rule: it is shown and hidden with the `hidden` attribute
 * (tools/test-hidden-attribute.mjs).
 * ------------------------------------------------------------------------- */

.video-captions {
  position: absolute;
  left: 0.55rem;
  bottom: 0.55rem;
  right: calc(min(28%, 9rem) + 1.2rem);
  max-height: 34%;
  overflow: hidden;
  padding: 0.3rem 0.6rem;
  border-radius: 6px;
  background: rgba(8, 10, 14, 0.72);
  color: var(--text);
  font-size: clamp(0.8rem, 2.6vw, 1.05rem);
  line-height: 1.35;
  pointer-events: none;
}

/* Moved to the top (the button under the video, Hiro's page): the clock is
 * top-right, so it stops short of that instead of the self-view. */
.video-captions.top {
  top: 0.55rem;
  bottom: auto;
  right: 5.2rem;
}

.video-captions p {
  margin: 0;
}

.video-captions p + p {
  margin-top: 0.2rem;
}

.video-captions p:not(:last-child) {
  opacity: 0.7;
}

/* A phone: the frame is ~250 px tall and the face fills it, so the overlay
 * shrinks to a strip — the latest turn only, smaller, at most two lines (a
 * longer turn shows its opening and an ellipsis; the whole of it is in the
 * caption list below). Hiro saw it cover the host's face on his phone
 * (2026-09-28). The line clamp is on the <p>, which is never [hidden]; the
 * overlay itself still has no display rule. */
@media (max-width: 600px) {
  .video-captions {
    left: 0.4rem;
    bottom: 0.4rem;
    max-height: 22%;
    padding: 0.15rem 0.45rem;
    font-size: 0.72rem;
    line-height: 1.3;
  }

  .video-captions.top {
    top: 0.4rem;
    bottom: auto;
    right: 4.4rem;
  }

  .video-captions p:not(:last-child) {
    display: none;
  }

  .video-captions p:last-child {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
  }
}

.settings-local {
  width: 100%;
  margin-top: 0.7rem;
}

/* A session without interpretation, said once on each page. No display rule:
 * shown and hidden with [hidden] (tools/test-hidden-attribute.mjs). */
.mode-note {
  margin: 0.7rem 0 0;
  padding: 0.55rem 0.7rem;
  border: 1px solid var(--line);
  border-radius: 8px;
  font-size: 0.9rem;
  color: var(--text);
}


/* ---------------------------------------------------------------------------
 * Hiro's full-screen layout (web/layout.js, 2026-10-02).
 *
 * The video fills the window. Everything he operates is in a panel on the left:
 * beside the picture on a wide screen, a drawer over it on a phone. The Speak
 * bar stays fixed at the bottom, full width, and both the panel and the picture
 * stop above it. Only under body.host-fs, which only his page ever gets.
 * ------------------------------------------------------------------------- */
body.host-fs {
  --side-w: 22rem;
  --bar-h: 5.6rem;   /* the Speak bar's measured height (5.5rem) */
  padding: 0;
  display: block;
  height: 100dvh;
  overflow: hidden;
}
body.host-fs:has(#preset-row:not([hidden])) {
  --bar-h: 10.2rem;
}
body.host-fs main {
  max-width: none;
  height: 100dvh;
  position: relative;
}
/* The speak bar's own page padding is for the stacked layout. */
body.host-fs.has-speak-bar {
  padding-bottom: 0;
}

body.host-fs #side {
  position: fixed;
  z-index: 30;
  top: 0;
  left: 0;
  bottom: var(--bar-h);
  width: min(var(--side-w), 88vw);
  overflow-y: auto;
  padding: 0.9rem 0.9rem 1.2rem;
  background: var(--bg);
  border-right: 1px solid var(--line);
  transform: translateX(-100%);
  transition: transform 0.18s ease;
}
body.host-fs.side-open #side {
  transform: none;
}
body.host-fs #side .logo {
  height: 1.8rem;
  margin: 0 2.4rem 0.4rem 0;
}
body.host-fs #side h1 {
  font-size: 1.05rem;
  margin: 0 2.4rem 0.7rem 0;
}
body.host-fs #side .panel {
  padding: 0.7rem;
  margin-bottom: 0.7rem;
}
body.host-fs #subtitle {
  display: none;
}

/* The picture: the whole window above the bar, beside the panel when it is
 * open on a wide screen. */
body.host-fs .stage {
  position: fixed;
  z-index: 1;
  top: 0;
  right: 0;
  left: 0;
  bottom: var(--bar-h);
  margin: 0;
}
body.host-fs .remote-wrap {
  height: 100%;
  aspect-ratio: auto;
  border: 0;
  border-radius: 0;
}
body.host-fs .remote-wrap > #remote-video {
  object-fit: contain;
}
@media (min-width: 900px) {
  body.host-fs.side-open .stage {
    left: var(--side-w);
  }
  /* The panel is open beside the picture: the HUD would only repeat it. */
  body.host-fs.side-open .video-hud {
    display: none;
  }
}

/* The one control that is always on screen: opens and closes the panel. */
body.host-fs #side-toggle {
  position: fixed;
  z-index: 40;
  top: 0.6rem;
  left: 0.6rem;
  width: 2.4rem;
  height: 2.4rem;
  padding: 0;
  font-size: 1.3rem;
  line-height: 1;
  border-radius: 8px;
  background: rgba(8, 10, 14, 0.7);
  color: var(--text);
  border-color: var(--line);
}
body.host-fs.side-open #side-toggle {
  left: calc(min(var(--side-w), 88vw) - 2.9rem);
  background: transparent;
}

/* With the panel shut, the caption must clear the toggle. */
body.host-fs:not(.side-open) .video-captions {
  left: 3.5rem;
}

/* What the panel would have said, on the picture: under the clock, right side,
 * away from the caption. No pointer events: it is for reading. */
.video-hud {
  position: absolute;
  top: 2.9rem;
  right: 0.55rem;
  max-width: 62%;
  text-align: right;
  font-size: 0.8rem;
  line-height: 1.3;
  pointer-events: none;
}
.video-hud p {
  margin: 0 0 0.25rem;
  padding: 0.2rem 0.5rem;
  border-radius: 6px;
  background: rgba(8, 10, 14, 0.72);
}

/* Faults (no interpreter, lost heartbeat, microphone) must show with the panel
 * shut: above the bar, over the picture, not inside the panel. */
body.host-fs #faults {
  position: fixed;
  z-index: 25;
  left: 0.6rem;
  right: 0.6rem;
  bottom: calc(var(--bar-h) + 0.4rem);
}

/* The "tap to hear" banner must stay above the full-screen picture and the panel. */
body.host-fs .unblock {
  position: fixed;
  z-index: 45;
  top: 0.6rem;
  left: 3.6rem;
  right: 0.6rem;
  margin: 0;
}


/* ---------------------------------------------------------------------------
 * Session notes (web/notes-ui.js): the switch, the status, the summary, and
 * Download / Copy / Discard. In Hiro's panel, or over the video left or right.
 * [hidden] only; the one display rule (.notes-actions) has its own [hidden].
 * ------------------------------------------------------------------------- */
.notes {
  margin: 0.7rem 0;
  padding-top: 0.7rem;
  border-top: 1px solid var(--line);
}
.notes-status {
  margin: 0.45rem 0 0;
  font-size: 0.82rem;
  color: var(--muted);
}
.notes-status.paused {
  color: var(--warn);
  font-weight: 600;
}
.notes-view {
  margin: 0.5rem 0 0;
  max-height: 16rem;
  overflow-y: auto;
  font-size: 0.85rem;
  line-height: 1.4;
}
.notes-view h4 {
  margin: 0.6rem 0 0.15rem;
  font-size: 0.85rem;
}
.notes-view p,
.notes-view ul {
  margin: 0.15rem 0;
}
.notes-view ul {
  padding-left: 1.1rem;
}
.notes-actions {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}
.notes-actions[hidden] {
  display: none;
}
.notes-actions button {
  flex: 1;
}
/* Over the video: a dim panel down one side, scrolling, clear of the clock. */
.notes-view.notes-overlay {
  position: absolute;
  top: 3.4rem;
  bottom: 0.55rem;
  width: min(24rem, 42%);
  max-height: none;
  margin: 0;
  padding: 0.5rem 0.7rem;
  border-radius: 8px;
  background: rgba(8, 10, 14, 0.78);
  z-index: 2;
}
.notes-view.notes-left { left: 0.55rem; }
.notes-view.notes-right { right: 0.55rem; }
.notes-place { margin-top: 0.7rem; }

.notes-view.notes-overlay {
  top: max(3.4rem, calc(var(--cap-top, 0px) + 0.5rem));
  bottom: calc(var(--cap-bottom, 0px) + 0.55rem);
}

/* ---------------------------------------------------------------------------
 * The client's full screen (2026-10-02, Hiro): when they choose "Full screen"
 * they expect the camera to fill the screen and little else. Only the picture,
 * a slim row of buttons across the top (mute, camera, and the way out), a small
 * status chip, and any faults or the tap-to-hear banner. The captions list, the
 * heading, the notes about waiting and the rest are hidden until they leave.
 * The bottom is kept clear: on an iPhone Safari's own toolbar floats there.
 * Every rule is under body.client-fs, which only the client's page ever gets.
 * ------------------------------------------------------------------------- */
body.client-fs {
  padding: 0;
  display: block;
  /* NOT overflow: hidden. The picture is fixed, but the page behind it is made
   * taller than the screen so a swipe up scrolls it, and iOS Safari then folds
   * its address and tab bars away. A page cannot hide them by itself; this only
   * lets the person's own swipe do it. (Added to Home Screen, the page opens
   * with no bars at all: the apple-mobile-web-app-capable meta in session.html.) */
  min-height: 200dvh;
  background: #000;
}
body.client-fs main {
  max-width: none;
  padding: 0;
}
body.client-fs .logo,
body.client-fs h1,
body.client-fs #title,
body.client-fs .sub,
body.client-fs .captions {
  display: none;
}
body.client-fs .stage {
  position: fixed;
  z-index: 10;
  inset: 0;
  margin: 0;
  background: #000;
}
body.client-fs .remote-wrap {
  height: 100%;
  min-height: 0;
  aspect-ratio: auto;
  border: 0;
  border-radius: 0;
  background: #000;
}
/* FILL the screen (Hiro, 2026-10-02): the picture fits the HEIGHT and the sides
 * are cropped, in portrait as well as sideways. The other person is centred in
 * their own camera, so it is the room around them that goes. */
body.client-fs .remote-wrap > #remote-video {
  object-fit: cover;
}
/* The one slim row: mute, camera, and the way out. */
body.client-fs .controls {
  position: fixed;
  z-index: 30;
  top: calc(env(safe-area-inset-top, 0px) + 0.5rem);
  left: 0.5rem;
  right: 0.5rem;
  margin: 0;
  gap: 0.4rem;
}
body.client-fs .controls button {
  padding: 0.4rem 0.5rem;
  font-size: 0.8rem;
  background: rgba(8, 10, 14, 0.62);
}
/* The status, small, under the row. Everything else in the panel waits. */
body.client-fs #client-panel {
  position: fixed;
  z-index: 30;
  top: calc(env(safe-area-inset-top, 0px) + 3.2rem);
  left: 0.5rem;
  right: 0.5rem;
  margin: 0;
  padding: 0;
  background: none;
  border: 0;
  /* The status chip on the left and "Leave the session" on the right, side by
   * side IN THE FLOW (not laid over it, which is how it went missing). */
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 0.5rem;
}
/* It has a display rule now, so [hidden] needs its own (tools/test-hidden-attribute.mjs). */
body.client-fs #client-panel[hidden] {
  display: none;
}
body.client-fs #client-panel > *:not(#client-indicator):not(.client-end-row) {
  display: none;
}
body.client-fs .client-end-row {
  margin: 0;
  flex: none;
}
body.client-fs #client-end {
  width: auto;
  padding: 0.35rem 0.7rem;
  font-size: 0.8rem;
  background: rgba(8, 10, 14, 0.62);
}
body.client-fs #client-indicator {
  display: inline-block;
  padding: 0.25rem 0.6rem;
  border-radius: 6px;
  background: rgba(8, 10, 14, 0.62);
}
body.client-fs #client-indicator .indicator-word {
  font-size: 0.85rem;
  letter-spacing: 0.02em;
}
body.client-fs #client-indicator .indicator-why {
  display: none;
}
body.client-fs #faults {
  position: fixed;
  z-index: 35;
  top: calc(env(safe-area-inset-top, 0px) + 5.6rem);
  left: 0.5rem;
  right: 0.5rem;
}
body.client-fs .unblock {
  position: fixed;
  z-index: 45;
  top: calc(env(safe-area-inset-top, 0px) + 0.5rem);
  left: 0.5rem;
  right: 0.5rem;
  margin: 0;
}
/* The translation on the video and the self-view stay, above the bottom edge
 * where an iPhone's toolbar floats. */
body.client-fs .video-captions {
  bottom: calc(env(safe-area-inset-bottom, 0px) + 5.5rem);
}
body.client-fs #local-video {
  bottom: calc(env(safe-area-inset-bottom, 0px) + 5.5rem);
}
