/* ==========================================================================
   vchat — Eperf standard UI
   Dark ground, neon accents, granite panels, restrained motion.

   The granite is a single inline SVG turbulence tile reused via a CSS variable,
   so the texture costs one decode for the whole page rather than one per panel.
   Every animation is transform/opacity only and pauses under
   prefers-reduced-motion, because this runs next to a video encoder.
   ========================================================================== */

:root {
  --bg-0: #07090c;
  --bg-1: #0c1014;
  --bg-2: #12171d;

  --granite-1: #1a1f26;
  --granite-2: #10141a;
  --granite-edge: #2a323d;

  /* Gears are granite grey rather than neon. The panel granites are far too
     dark to show a gear against a dark panel, so these are the lit face of the
     same stone — mid grey with the cool cast granite actually has. */
  --gear: #96a0ae;
  --gear-2: #6e7887;

  /* Accents, not neon signs. The originals were fully saturated (#39f6e6,
     #ff4fd8) which glowed against the dark ground and pulled the eye away from
     the video — the thing anyone is actually here to look at. These keep the
     same hues at roughly two thirds the saturation, so they still read as
     accents and stop competing. */
  --neon: #5fc4b8;
  --neon-dim: #2f7d76;
  --neon-2: #c47bae;
  --neon-3: #86bf86;
  --amber: #d9b866;
  --danger: #d97070;

  --text: #dfe6ee;
  --text-dim: #8a94a6;
  --text-faint: #5b6472;

  --radius: 10px;
  --radius-sm: 6px;
  --panel-shadow: 0 2px 18px rgba(0, 0, 0, .55);
  --glow: 0 0 0 1px rgba(95, 196, 184, .24);

  --granite:
    linear-gradient(160deg, var(--granite-1) 0%, var(--granite-2) 100%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.055'/%3E%3C/svg%3E");
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg-0);
  color: var(--text);
  font: 14px/1.5 "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
  overscroll-behavior: none;
}

body {
  background-image:
    radial-gradient(1100px 620px at 78% -12%, rgba(95, 196, 184, .05), transparent 62%),
    radial-gradient(900px 520px at 8% 108%, rgba(196, 123, 174, .04), transparent 60%);
}

h1, h2, h3, h4 { margin: 0 0 .5rem; font-weight: 600; letter-spacing: .3px; }
h1 { font-size: 1.35rem; }
h2 { font-size: 1.05rem; }
h3 { font-size: .92rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: 1.1px; }
a { color: var(--neon); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ---------- Granite panels ---------------------------------------------- */

.panel {
  background: var(--granite);
  background-blend-mode: normal;
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius);
  box-shadow: var(--panel-shadow);
  position: relative;
}

/* A single hairline of neon along the top edge reads as "machined". */
.panel::before {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(163, 189, 185, .18), transparent);
  border-radius: var(--radius) var(--radius) 0 0;
  pointer-events: none;
}

.panel-head {
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .6rem .8rem;
  border-bottom: 1px solid var(--granite-edge);
}
.panel-body { padding: .8rem; }
.panel-body.tight { padding: .5rem; }

/* ---------- Controls ----------------------------------------------------- */

button, .btn {
  font: inherit;
  color: var(--text);
  background: linear-gradient(180deg, #1c232b, #141a20);
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius-sm);
  padding: .42rem .8rem;
  cursor: pointer;
  transition: border-color .14s, box-shadow .14s, transform .08s;
}
button:hover, .btn:hover { border-color: var(--neon-dim); box-shadow: var(--glow); }
button:active, .btn:active { transform: translateY(1px); }
button:disabled { opacity: .42; cursor: not-allowed; box-shadow: none; }
button:focus-visible, .btn:focus-visible, input:focus-visible, select:focus-visible,
textarea:focus-visible { outline: 2px solid var(--neon); outline-offset: 2px; }

button.primary {
  background: linear-gradient(180deg, #10403d, #0b2d2b);
  border-color: var(--neon-dim);
  color: #ccfffa;
}
button.danger { border-color: #6b2b2b; color: #ffc9c9; }
button.danger:hover { border-color: var(--danger); box-shadow: 0 0 12px rgba(255, 92, 92, .3); }
button.ghost { background: transparent; }
button.on { border-color: var(--neon); box-shadow: var(--glow); color: #cfe9e5; }
button.icon { padding: .4rem .55rem; line-height: 1; }

input, select, textarea {
  font: inherit;
  color: var(--text);
  background: #0a0e12;
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius-sm);
  padding: .45rem .6rem;
  width: 100%;
}
textarea { resize: vertical; min-height: 64px; }
label { display: block; font-size: .8rem; color: var(--text-dim); margin: .55rem 0 .2rem; }

.row { display: flex; gap: .5rem; align-items: center; }
.row.wrap { flex-wrap: wrap; }
.grow { flex: 1 1 auto; min-width: 0; }
.muted { color: var(--text-dim); }
.faint { color: var(--text-faint); font-size: .82rem; }
.hidden { display: none !important; }

/* ---------- 3D gears ------------------------------------------------------
   Interlocking gears signal "parts working together". Two counter-rotating
   wheels, CSS-only, paused whenever the app is idle so they never burn a core
   during a call.                                                             */

/* Real extrusion rather than a shadow pretending to be one.

   A single tilted silhouette reads as a sticker no matter how it is shaded. So
   each gear is a stack of copies of the same outline spaced along Z, and the
   whole stack is tilted toward the viewer — the layers are then visible as
   thickness and the teeth have actual sides.

   The tilt and the spin have to sit on different elements. Put both on one and
   the extrusion axis rotates with the wheel, so the "thickness" swings around
   the face like a shadow chasing the light. Here .gear3d holds the fixed tilt
   and .spin turns inside it, which is how a real gear behaves: the axle stays
   put and the wheel goes round.

   Layers are generated by buildGears() in util.js. */

.gearbox { display: inline-flex; align-items: center; gap: 1px; perspective: 340px; }

.gear3d {
  position: relative;
  width: var(--sz, 26px);
  height: var(--sz, 26px);
  transform-style: preserve-3d;
  transform: rotateX(40deg);
}
.gear3d > .spin {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: gear-spin var(--dur, 7s) linear infinite;
}
.gear3d.reverse > .spin { animation-direction: reverse; }

.gear-layer {
  position: absolute;
  inset: 0;
  transform: translateZ(var(--z));
  color: var(--layer);
}
/* Only the top face is worth a shadow; the rest are inside the solid. */
.gear-layer.face {
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, .55));
}

.gearbox.idle .spin { animation-play-state: paused; }

@keyframes gear-spin {
  from { transform: rotateZ(0deg); }
  to   { transform: rotateZ(360deg); }
}

/* ---------- App shell ----------------------------------------------------- */

#app { display: flex; flex-direction: column; height: 100%; }

.topbar {
  display: flex;
  align-items: center;
  gap: .8rem;
  padding: .5rem .9rem;
  background: var(--granite);
  border-bottom: 1px solid var(--granite-edge);
  z-index: 20;
}
.brand { display: flex; align-items: center; gap: .55rem; font-weight: 700; letter-spacing: 1px; }
.brand .mark {
  color: var(--neon);
  text-shadow: 0 1px 0 rgba(0, 0, 0, .6);
  font-size: 1.05rem;
}
.brand img { width: 26px; height: 26px; border-radius: 6px; }

.view { flex: 1 1 auto; overflow: auto; padding: 1rem; }
.view.room-view { padding: 0; overflow: hidden; }

.lobby-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(290px, 1fr));
  gap: 1rem;
  max-width: 1180px;
  margin: 0 auto;
}
.stack { display: flex; flex-direction: column; gap: 1rem; max-width: 1180px; margin: 0 auto; }

/* ---------- Room layout ---------------------------------------------------
   Centre stage holds the active speaker and is resizable; the surrounding rail
   holds everyone else in tiles that can be dragged anywhere over the stage.    */

.room {
  display: grid;
  grid-template-columns: 1fr 330px;
  grid-template-rows: 1fr auto;
  height: 100%;
  gap: .6rem;
  padding: .6rem;
}
.stage-wrap { position: relative; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: .6rem; }

/* The stage is sized by --stage-h once the user has dragged the handle, and
   flexes to fill until then.

   It used to carry `resize: vertical`, which put a handle on screen that did
   nothing: the browser sets an inline height, and `flex: 1 1 auto` inside the
   column immediately recomputes it away on the next layout pass. A visible
   control that silently does nothing is worse than no control, so the handle is
   now a real splitter (see .stage-resizer) writing --stage-h. */
.stage {
  position: relative;
  flex: 1 1 auto;
  min-height: 140px;
  background: #05070a;
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius);
  overflow: hidden;
}
.stage-wrap.sized .stage {
  flex: 0 0 auto;
  height: var(--stage-h);
}

.stage-resizer {
  flex: 0 0 auto;
  height: 12px;
  margin: -3px 0;
  cursor: ns-resize;
  display: grid;
  place-items: center;
  touch-action: none;
}
.stage-resizer::before {
  content: "";
  width: 54px;
  height: 4px;
  border-radius: 2px;
  background: var(--granite-edge);
  transition: background .14s, width .14s;
}
.stage-resizer:hover::before,
.stage-resizer.dragging::before { background: var(--neon-dim); width: 78px; }
.stage-resizer:focus-visible { outline: 2px solid var(--neon); outline-offset: -2px; }
.stage video { width: 100%; height: 100%; object-fit: contain; background: #05070a; display: block; }

/* ---------- The barroom television ----------------------------------------
   Covers the centre screen while a channel is on. It is deliberately the whole
   stage rather than a floating pane: a television in a bar is the thing on the
   wall everyone is already looking at, not another window to arrange. */
.tvscreen { position: absolute; inset: 0; background: #000; z-index: 3; }
.tvscreen video { width: 100%; height: 100%; object-fit: contain; background: #000; display: block; }

.tv-badge {
  position: absolute; top: 10px; left: 12px;
  display: flex; align-items: center; gap: .35rem;
  padding: .16rem .5rem;
  border-radius: 999px;
  background: rgba(10, 14, 18, .78);
  color: var(--danger);
  font-size: .68rem; font-weight: 700; letter-spacing: 1.2px;
}
.tv-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--danger);
  animation: tv-pulse 2s ease-in-out infinite;
}
@keyframes tv-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
@media (prefers-reduced-motion: reduce) { .tv-dot { animation: none; } }

.tv-title {
  position: absolute; top: 10px; right: 12px;
  padding: .16rem .5rem;
  border-radius: var(--radius-sm);
  background: rgba(10, 14, 18, .78);
  color: var(--text-dim);
  font-size: .72rem;
  max-width: 55%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.tv-sound {
  position: absolute; left: 50%; bottom: 14px;
  transform: translateX(-50%);
  z-index: 4;
  padding: .4rem .9rem;
  border-radius: 999px;
  border: 1px solid var(--neon);
  background: rgba(10, 14, 18, .92);
  color: var(--neon);
  font-size: .8rem;
}
.tv-status {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: var(--text-dim);
  font-size: .85rem;
  pointer-events: none;
}
.stage .stage-label {
  position: absolute; left: .6rem; bottom: .6rem;
  background: rgba(6, 9, 12, .78);
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius-sm);
  padding: .25rem .6rem;
  font-size: .84rem;
}
.stage .stage-empty {
  position: absolute; inset: 0;
  display: grid; place-content: center; gap: .6rem; justify-items: center;
  color: var(--text-faint);
}

/* Participant tiles rail */
.tiles {
  display: flex;
  gap: .5rem;
  overflow-x: auto;
  padding: .2rem;
  min-height: 116px;
  scrollbar-width: thin;
}
.tile {
  position: relative;
  flex: 0 0 auto;
  width: 152px;
  height: 106px;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: #05070a;
  border: 2px solid var(--granite-edge);
  cursor: grab;
  transition: border-color .16s, box-shadow .16s;
}
.tile.dragging { cursor: grabbing; opacity: .85; z-index: 60; }
.tile.floating { position: fixed; z-index: 55; box-shadow: 0 10px 30px rgba(0, 0, 0, .6); }
.tile video { width: 100%; height: 100%; object-fit: cover; }
.tile .avatar {
  position: absolute; inset: 0;
  display: grid; place-content: center;
  font-size: 1.6rem; font-weight: 700; color: var(--text-faint);
  background: radial-gradient(circle at 50% 35%, #171d24, #0a0e12);
}
.tile .name {
  position: absolute; left: 0; right: 0; bottom: 0;
  padding: .18rem .4rem;
  font-size: .74rem;
  background: linear-gradient(180deg, transparent, rgba(4, 6, 9, .92));
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tile .badges { position: absolute; top: .25rem; right: .25rem; display: flex; gap: .18rem; }
.tile.speaking { border-color: var(--neon-3) !important; box-shadow: 0 0 0 2px rgba(134, 191, 134, .45); }

.badge {
  font-size: .66rem;
  padding: .08rem .3rem;
  border-radius: 4px;
  background: rgba(6, 9, 12, .85);
  border: 1px solid var(--granite-edge);
}
.badge.good { color: var(--neon-3); border-color: #2c5a2c; }
.badge.degraded { color: var(--amber); border-color: #6b5a1f; }
.badge.poor { color: var(--danger); border-color: #6b2b2b; animation: pulse-warn 1.8s ease-in-out infinite; }
.badge.hand { color: var(--amber); border-color: #6b5a1f; }
.badge.host { color: var(--neon-2); border-color: #6b2b5f; }
.badge.muted { color: var(--text-faint); }

@keyframes pulse-warn {
  0%, 100% { opacity: 1; }
  50% { opacity: .5; }
}

/* ---------- Side rail ----------------------------------------------------- */

.rail { display: flex; flex-direction: column; gap: .6rem; min-height: 0; }
.rail .panel { display: flex; flex-direction: column; min-height: 0; }

.tabs { display: flex; gap: .25rem; flex-wrap: wrap; }
.tabs button { padding: .3rem .55rem; font-size: .8rem; }
.tab-body { flex: 1 1 auto; overflow: auto; min-height: 120px; }

.roster-item {
  display: flex; align-items: center; gap: .5rem;
  padding: .35rem .45rem;
  border-left: 3px solid transparent;
  border-radius: var(--radius-sm);
}
.roster-item:hover { background: rgba(255, 255, 255, .035); }
.roster-item .who { flex: 1 1 auto; min-width: 0; }
.roster-item .who .nm { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.chat-log { display: flex; flex-direction: column; gap: .4rem; padding: .2rem; }
.chat-msg { padding: .3rem .45rem; border-radius: var(--radius-sm); background: rgba(255, 255, 255, .028); }
.chat-msg .meta { font-size: .72rem; color: var(--text-faint); display: flex; gap: .4rem; }
.chat-msg.private { border-left: 2px solid var(--neon-2); }
.chat-msg.tohost { border-left: 2px solid var(--amber); }
.chat-msg.system { background: none; color: var(--text-faint); font-style: italic; }
.chat-msg .body { word-wrap: break-word; overflow-wrap: anywhere; }
.chat-msg .body.emoji-only { font-size: 1.8rem; line-height: 1.1; }

.emoji-bar { display: flex; gap: .15rem; flex-wrap: wrap; }
.emoji-bar button { padding: .18rem .3rem; font-size: 1rem; background: transparent; border-color: transparent; }
.emoji-bar button:hover { border-color: var(--neon-dim); }

/* ---------- Whiteboard ---------------------------------------------------- */

.wb-tools { display: flex; gap: .3rem; align-items: center; flex-wrap: wrap; }
.swatch { width: 20px; height: 20px; border-radius: 50%; border: 2px solid var(--granite-edge); cursor: pointer; padding: 0; }
.swatch.on { border-color: #fff; box-shadow: var(--glow); }

/* ---------- Captions ------------------------------------------------------ */

.captions {
  position: absolute; left: 50%; bottom: 3.2rem; transform: translateX(-50%);
  max-width: 82%; text-align: center;
  background: rgba(4, 6, 9, .82);
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius-sm);
  padding: .35rem .8rem;
  font-size: 1rem;
  pointer-events: none;
}
.captions .who { color: var(--neon); font-weight: 600; margin-right: .35rem; }
.captions .interim { color: var(--text-dim); font-style: italic; }

/* ---------- Toolbar ------------------------------------------------------- */

.toolbar {
  grid-column: 1 / -1;
  display: flex; gap: .4rem; align-items: center; flex-wrap: wrap;
  padding: .5rem .6rem;
  background: var(--granite);
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius);
}
.toolbar .sep { width: 1px; height: 22px; background: var(--granite-edge); margin: 0 .25rem; }

/* ---------- Telemetry ("behind the scenes") ------------------------------- */

.telemetry { font-family: ui-monospace, "Cascadia Code", Consolas, monospace; font-size: .76rem; }
.telemetry .bar { height: 4px; background: #0a0e12; border-radius: 2px; overflow: hidden; margin-top: 2px; }
.telemetry .bar > i { display: block; height: 100%; background: var(--neon-dim); transition: width .4s ease; }
.telemetry .bar > i.degraded { background: var(--amber); }
.telemetry .bar > i.poor { background: var(--danger); }

.flow {
  position: relative; height: 26px; margin: .4rem 0;
  border: 1px solid var(--granite-edge); border-radius: var(--radius-sm);
  background: #070a0e; overflow: hidden;
}
.flow .pkt {
  position: absolute; top: 50%; width: 5px; height: 5px; margin-top: -2.5px;
  border-radius: 50%; background: var(--neon);
  box-shadow: 0 0 6px var(--neon);
  animation: flow-move 2.4s linear infinite;
}
.flow .pkt.up { background: var(--neon-2); box-shadow: 0 0 6px var(--neon-2); animation-direction: reverse; }
@keyframes flow-move { from { left: -6px; } to { left: 100%; } }

/* ---------- Drop zone ----------------------------------------------------- */

.dropzone {
  border: 1px dashed var(--granite-edge);
  border-radius: var(--radius-sm);
  padding: .8rem;
  text-align: center;
  color: var(--text-dim);
  transition: border-color .15s, background .15s;
}
.dropzone.hot { border-color: var(--neon); background: rgba(95, 196, 184, .06); color: var(--text); }

.file-item { display: flex; gap: .5rem; align-items: center; padding: .3rem .2rem; }
.file-item .nm { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ---------- Toasts -------------------------------------------------------- */

#toasts { position: fixed; right: 1rem; bottom: 1rem; display: flex; flex-direction: column; gap: .45rem; z-index: 200; }
.toast {
  background: var(--granite);
  border: 1px solid var(--granite-edge);
  border-left: 3px solid var(--neon);
  border-radius: var(--radius-sm);
  padding: .5rem .75rem;
  box-shadow: var(--panel-shadow);
  max-width: 340px;
  animation: toast-in .18s ease-out;
}
.toast.err { border-left-color: var(--danger); }
.toast.warn { border-left-color: var(--amber); }
@keyframes toast-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ---------- Modal --------------------------------------------------------- */

.modal-back {
  position: fixed; inset: 0; z-index: 150;
  background: rgba(3, 5, 8, .72);
  display: grid; place-items: center;
  padding: 1rem;
}
.modal { width: min(560px, 100%); max-height: 88vh; overflow: auto; }

/* ---------- Tables -------------------------------------------------------- */

table { width: 100%; border-collapse: collapse; font-size: .86rem; }
th, td { text-align: left; padding: .4rem .5rem; border-bottom: 1px solid var(--granite-edge); }
th { color: var(--text-dim); font-weight: 600; font-size: .78rem; text-transform: uppercase; letter-spacing: .6px; }

/* ---------- Mobile -------------------------------------------------------- */

@media (max-width: 860px) {
  /* The room scrolls on a phone.
     On a desktop the room is a full-height grid that always fits, so the view
     around it clips anything that does not. On a 393x659 screen it does not
     fit: the rail wanted 362px, got 192px, and the rest — including the box
     you type a message into — was cut off with no way to reach it. Nothing
     scrolled, so there was no way to reach it at all. */
  .view.room-view { overflow: auto; -webkit-overflow-scrolling: touch; }
  .room {
    height: auto;
    min-height: 100%;
    grid-template-columns: 1fr;
    /* Every row takes what it needs and the page carries the total.
       Capping the first row at a fraction of the screen looked tidier and was
       wrong: the stage asks for 140px and the tile strip for 116px, so the row
       overflowed its own limit and the panel below was drawn straight over the
       participant tiles. */
    grid-template-rows: auto auto auto;
    padding: .4rem;
    gap: .4rem;
  }
  /* The stage is bounded here instead, so it cannot eat the whole screen. */
  .stage { flex: 0 0 auto; height: 34vh; min-height: 150px; }
  .stage-wrap.sized .stage { height: 34vh; }
  .tiles { min-height: 96px; }
  /* Let the rail be as tall as it needs; the page carries it now. */
  .rail { max-height: none; }
  .rail .panel { min-height: 0; }
  /* Chat still scrolls within itself rather than making the page enormous. */
  .tab-body { max-height: 50vh; }
  .stage { resize: none; }

  /* The whiteboard is 3:2, so inside a short rail it renders as a sliver.
     When the board is the active tab, give the rail the room to show it and
     shrink the stage instead — you came here to draw, not to watch. */
  .room:has(#tab-board:not(.hidden)) .stage { height: 18vh; min-height: 0; }
  .room:has(#tab-board:not(.hidden)) .rail { max-height: none; }
  .room:has(#tab-board:not(.hidden)) .tiles { display: none; }
  .tile { width: 118px; height: 84px; }

  /* The top bar is a single non-wrapping row, and on a 393px screen the last
     button ran off the right edge — which widens the layout viewport, so the
     whole page renders zoomed out and everything else is measured against a
     width the phone does not have. Let it wrap, and drop the padding and the
     signed-in-as line, which is the least useful thing up there when space is
     this tight. */
  .topbar { flex-wrap: wrap; gap: .4rem .5rem; padding: .5rem .6rem; }
  .topbar #who { display: none; }
  .topbar button { padding: .35rem .6rem; }
  .brand .mark { font-size: .95rem; }

  .toolbar { position: sticky; bottom: 0; }
  /* Comfortable hit targets on glass. */
  .toolbar button { min-height: 40px; min-width: 44px; }
  .view { padding: .6rem; }
}

/* Measure against the viewport that is actually visible.
   `vh` on iOS Safari counts the space behind the address bar, so a stage asked
   for 34vh takes noticeably more than a third of what you can see and shoves
   the rest of the room off the bottom. `dvh` shrinks and grows with the
   browser chrome. Guarded, because it is ignored by anything older. */
@supports (height: 1dvh) {
  @media (max-width: 860px) {
    .stage, .stage-wrap.sized .stage { height: 34dvh; }
    .room:has(#tab-board:not(.hidden)) .stage,
    .room:has(#tab-board:not(.hidden)) .stage-wrap.sized .stage { height: 18dvh; }
    .tab-body { max-height: 50dvh; }
  }
}

/* iOS safe areas (notch / home indicator). */
@supports (padding: env(safe-area-inset-bottom)) {
  .room { padding-bottom: calc(.6rem + env(safe-area-inset-bottom)); }
  .topbar { padding-top: calc(.5rem + env(safe-area-inset-top)); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
}

/* ---------- Detachable surfaces --------------------------------------------
   The whiteboard and shared images can be popped out to float over the stage.
   Geometry is owned by whoever opened the surface and mirrored to everyone
   else, so position and size are fractions of the stage rather than pixels —
   a phone and a 4K desktop then land on the same layout instead of the same
   numbers.

   Only the owner gets the drag bar and the resize grip. Everyone else sees the
   same pane with no handles, which is what "controlled by the user who starts
   it" has to mean if it is to mean anything.                                 */

.floatlayer {
  position: absolute;
  inset: 0;
  pointer-events: none;   /* panes opt back in; the stage stays clickable */
  z-index: 40;
}

.floatpane {
  position: absolute;
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  min-width: 160px;
  min-height: 120px;
  background: var(--granite);
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius);
  box-shadow: 0 14px 40px rgba(0, 0, 0, .6);
  overflow: hidden;
}
.floatpane.owned { border-color: var(--neon-dim); }

.floatpane-bar {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .3rem .5rem;
  background: linear-gradient(180deg, #1c232b, #141a20);
  border-bottom: 1px solid var(--granite-edge);
  font-size: .78rem;
  flex: 0 0 auto;
  user-select: none;
}
.floatpane.owned .floatpane-bar { cursor: move; touch-action: none; }
.floatpane-title { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.floatpane-bar .owner { color: var(--text-faint); font-size: .72rem; }
.floatpane-bar button { padding: .1rem .4rem; font-size: .74rem; line-height: 1.2; }

.floatpane-body { flex: 1 1 auto; min-height: 0; overflow: auto; background: #0a0e12; }
.floatpane-body img { display: block; width: 100%; height: 100%; object-fit: contain; background: #05070a; }
/* Pinned to the pane rather than sized as a percentage of it.
   A percentage height against a flex item whose own height comes from
   flex-basis does not resolve reliably: the wrapper measured correctly but the
   <video> inside it was laid out a full frame-height further down, so it hung
   out of the bottom of the pane, behind the participant tiles. The viewer saw
   the filename, the title bar and an empty black box — reported from a real
   room as "I cannot see the video". inset:0 has nothing to resolve against. */
.floatpane-body { position: relative; }
.videowrap { position: absolute; inset: 0; }

/* The presenter's transport. Always on screen: the native bar hides itself a
   couple of seconds into playback and only returns on pointer movement over
   the video, which left the person running the session with nothing to press. */
.vidbar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: .3rem;
  padding: .35rem .5rem;
  background: linear-gradient(180deg, rgba(8, 11, 15, .1), rgba(8, 11, 15, .92));
}
.vidbar button {
  padding: .2rem .45rem;
  line-height: 1;
  font-size: .95rem;
  background: rgba(10, 14, 18, .75);
}
.vidbar-scrub {
  flex: 1 1 auto;
  min-width: 60px;
  width: auto;
  padding: 0;
  accent-color: var(--neon);
  cursor: pointer;
}
.vidbar-time {
  flex: 0 0 auto;
  font-size: .76rem;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}
/* Clear of the transport, so it never sits under the buttons. */
.videowrap:has(.vidbar) .soundprompt { bottom: 44px; }
.floatpane-body video {
  display: block; width: 100%; height: 100%;
  object-fit: contain; background: #05070a;
}
/* A viewer's player has no controls, so nothing about it should invite a
   click — the presenter drives. */
.floatpane:not(.owned) .floatpane-body video { cursor: default; }

/* Shown only when the browser refused to start the sound without being asked.
   It has to be unmissable: the alternative is someone watching a silent
   lesson and assuming that is all there is. */
.soundprompt {
  position: absolute;
  left: 50%; bottom: 12px;
  transform: translateX(-50%);
  z-index: 3;
  padding: 8px 16px;
  border: 1px solid var(--neon);
  border-radius: 999px;
  background: rgba(10, 14, 18, .92);
  color: var(--neon);
  font: inherit;
  font-size: .85rem;
  cursor: pointer;
  box-shadow: 0 4px 18px rgba(0, 0, 0, .5);
}
.soundprompt::before { content: '🔇  '; }
.soundprompt:hover { background: var(--neon); color: #0a0e12; }

/* Nothing the viewer can do about this one, so it does not look like a button
   that will help if pressed harder. */
.soundprompt.fatal {
  border-color: var(--danger);
  color: var(--danger);
  cursor: default;
  opacity: 1;
}
.soundprompt.fatal::before { content: '⚠  '; }
.soundprompt.fatal:hover { background: rgba(10, 14, 18, .92); color: var(--danger); }
.floatpane-body .wb-wrap { height: 100%; }

/* The grip only exists for the owner; nobody else can move the surface. */
.floatpane-grip {
  position: absolute;
  right: 0; bottom: 0;
  width: 18px; height: 18px;
  cursor: nwse-resize;
  touch-action: none;
  background:
    linear-gradient(135deg, transparent 50%, var(--granite-edge) 50%, var(--granite-edge) 62%,
                    transparent 62%, transparent 74%, var(--granite-edge) 74%,
                    var(--granite-edge) 86%, transparent 86%);
}
.floatpane:not(.owned) .floatpane-grip { display: none; }

/* Whiteboard on paper: the surrounding chrome stays dark, the sheet does not. */
.wb-wrap { position: relative; background: #f7f8fa; border-radius: var(--radius-sm); overflow: hidden; }
.wb-wrap canvas { display: block; width: 100%; touch-action: none; cursor: crosshair; }
.swatch { border: 2px solid var(--granite-edge); }
.swatch.on { border-color: var(--text); box-shadow: var(--glow); }

@media (max-width: 720px) {
  /* Floating panes over a phone-sized stage are unusable; pin them to the
     panel instead and let the tab hold them. */
  .floatpane { position: static !important; width: auto !important; height: auto !important; }
  .floatlayer { position: static; }
}

/* ---------- Companion device ------------------------------------------------
   Shown on a second device signed into the same account. It has to be
   impossible to miss: someone who does not realise this device is muted will
   talk into it and wonder why nobody answers.                                */

.companion {
  position: absolute;
  left: 50%;
  top: .8rem;
  transform: translateX(-50%);
  max-width: 90%;
  display: flex;
  align-items: center;
  gap: .6rem;
  padding: .5rem .8rem;
  background: rgba(10, 14, 18, .94);
  border: 1px solid var(--amber);
  border-radius: var(--radius-sm);
  font-size: .84rem;
  z-index: 60;
}
.companion button { flex: 0 0 auto; }

/* ---------- Docked surfaces on a phone -------------------------------------
   Last in the file on purpose. These rules and the base .floatpane ones have
   the same specificity, so whichever comes later wins; sitting in the mobile
   block further up meant `min-height: 210px` lost to the plain
   `.floatpane-body { min-height: 0 }` below it and the docked pane collapsed
   to whatever the video felt like being.

   A pane's position and size are fractions of the stage belonging to whoever
   opened it. That is right on a desktop and useless on a phone: a video opened
   from a laptop landed under the tile strip with its transport 64px below the
   fold, so the presenter could see neither the picture nor the buttons. Docked
   panes are full width, in the flow, and scroll with the rest of the room. */
@media (max-width: 860px) {
  .floatlayer {
    position: static;
    pointer-events: auto;
    display: flex;
    flex-direction: column;
    gap: .4rem;
  }
  .floatlayer:not(:empty) { margin-top: .4rem; }
  .floatpane {
    position: static !important;
    left: auto !important;
    top: auto !important;
    width: auto !important;
    height: auto !important;
    min-height: 0;
  }
  /* Roughly 16:9 of the phone's width, so the whole thing plus its transport
     lands inside one screenful instead of trailing off the bottom. */
  .floatpane-body { height: 200px; min-height: 0; flex: 0 0 auto; }
  .floatpane-grip { display: none; }
  .floatpane.owned .floatpane-bar { cursor: default; }
}

/* ---------- Channel picker ------------------------------------------------- */
.tvpicker {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 12;
  width: min(360px, 88%);
  max-height: 82%;
  display: flex; flex-direction: column; gap: .5rem;
  padding: .7rem;
  background: var(--granite);
  border: 1px solid var(--granite-edge);
  border-radius: var(--radius);
  box-shadow: 0 18px 50px rgba(0, 0, 0, .7);
}
.tvpicker-head { display: flex; align-items: center; justify-content: space-between; }
.tvpicker-list { flex: 1 1 auto; overflow: auto; display: flex; flex-direction: column; gap: .25rem; }
.tvpicker-item {
  display: flex; align-items: baseline; justify-content: space-between; gap: .6rem;
  width: 100%; text-align: left;
  padding: .45rem .6rem;
  background: rgba(10, 14, 18, .5);
}
.tvpicker-item.on { border-color: var(--neon); color: #cfe9e5; }
.tvpicker-name { font-weight: 600; }
.tvpicker-foot { display: flex; gap: .4rem; }
.tvpicker-foot input { flex: 1 1 auto; min-width: 0; }
