:root {
  color-scheme: dark;
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #111;
  color: #eee;
}

* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body { background: #111; color: #eee; overflow: hidden; }
button, input { font: inherit; }
.hidden { display: none !important; }
button {
  border: 1px solid #444;
  border-radius: 6px;
  background: #242424;
  color: #eee;
  padding: 0.45rem 0.75rem;
  cursor: pointer;
}
button:hover { background: #303030; }
button.active { background: #305d35; border-color: #5d9b63; }
button.danger { background: #6a2424; border-color: #a64848; }
button:disabled { opacity: 0.55; cursor: default; }

.host-shell {
  display: grid;
  grid-template-columns: 1fr;
  /* The final row is auto so the control bar can wrap on narrow screens. */
  grid-template-rows: 150px 1fr auto;
  height: 100vh;
  gap: 8px;
  padding: 8px;
  position: relative;
}

.broadcast-strip-wrap {
  grid-column: 1;
  grid-row: 1;
  position: relative;
  min-width: 0;
  min-height: 0;
}
.broadcast-strip {
  width: 100%;
  height: 100%;
  display: flex;
  gap: 8px;
  justify-content: safe center;
  overflow-x: auto;
  min-height: 0;
  border: 1px solid #242424;
  background: #151515;
  padding: 6px;
}
.broadcast-strip:empty::before {
  content: "Broadcast-only video sources appear here";
  color: #666;
  margin: auto;
}
.broadcast-tile {
  position: relative;
  height: 100%;
  min-width: 280px;
  background: #050505;
}
.broadcast-tile video { width: 100%; height: 100%; object-fit: contain; display: block; }
.broadcast-strip-page {
  position: absolute;
  z-index: 4;
  top: 50%;
  transform: translateY(-50%);
  width: 36px;
  height: 56px;
  padding: 0;
  border-radius: 8px;
  border-color: rgba(255,255,255,.45);
  background: rgba(10,10,10,.78);
  box-shadow: 0 2px 8px rgba(0,0,0,.55);
  font-size: 34px;
  line-height: 1;
}
.broadcast-strip-page:hover { background: rgba(45,45,45,.92); }
.broadcast-strip-prev { left: 12px; }
.broadcast-strip-next { right: 12px; }
.video-label {
  position: absolute;
  left: 5px;
  bottom: 5px;
  padding: 2px 6px;
  border-radius: 4px;
  background: rgba(0,0,0,.65);
  color: #ddd;
  font-size: 12px;
}

/* Wraps the recording canvas so the split divider can be positioned over it.
   The divider is a DOM element, so it is visible to the host but never ends
   up in the recorded pixels (the recording captures the canvas content only). */
.canvas-wrap {
  position: relative;
  min-height: 0;
  /* Width is capped so the 16:9 canvas fits the row below the 150px
     broadcast strip (same vertical budget the row gets; keeps the old
     280px constant shifted by the 50px strip reduction). */
  width: min(100%, calc((100vh - 230px) * 16 / 9));
  height: auto;
  max-height: 100%;
  aspect-ratio: 16 / 9;
}
#recordCanvas {
  display: block;
  width: 100%;
  height: 100%;
  background: #000;
}
/* The host canvas has a uniform dark background, so the full-height divider
   line (fine over the viewer's wallpaper) would stand out. Keep only the
   grip pill — the whole seam stays draggable (cursor + pill highlight). */
.canvas-wrap .split-divider::before {
  content: none;
}
.recording-preview {
  grid-column: 1;
  grid-row: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  min-height: 0;
  padding: 6px 8px 0;
}
.recording-preview label {
  font-size: 11px;
  color: #888;
  letter-spacing: .04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.control-bar {
  grid-column: 1;
  grid-row: 3;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 8px;
  border: 1px solid #242424;
  background: #181818;
  position: relative;
}
.control-bar button {
  position: relative;
}
#sourcesButton { background: #305d35; border-color: #5d9b63; }
#sourcesButton:hover { background: #3a6e42; }

/* Broadcast state chip (host control bar): off (grey), ready (amber —
   broadcasting but nobody watching), live (green, gently pulsing). */
.live-state {
  white-space: nowrap;
  font-size: 13px;
  font-weight: 700;
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid #3a3a3a;
  background: #1c1c1c;
  color: #9a9a9a;
}
.live-state.ready {
  color: #ffd97a;
  border-color: #8a6d1f;
  background: #26200f;
}
.live-state.live {
  color: #c9f7cd;
  border-color: #4d8d54;
  background: #14301b;
  animation: livepulse 2s ease-in-out infinite;
}
@keyframes livepulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(77, 141, 84, .45); }
  50% { box-shadow: 0 0 0 5px rgba(77, 141, 84, 0); }
}

.viewer-list {
  list-style: none;
  padding: 0;
  margin: 0 2px;
  max-height: min(40vh, 400px);
  overflow-y: auto;
}
.viewer-list li { padding: 5px 6px; border-bottom: 1px solid #282828; font-size: 13px; }

.menu {
  position: absolute;
  bottom: 52px;
  left: 8px;
  min-width: 360px;
  max-width: min(620px, calc(100vw - 24px));
  max-height: min(70vh, 620px);
  overflow: auto;
  z-index: 10;
  padding: 8px;
  border: 1px solid #4a4a4a;
  border-radius: 8px;
  background: #202020;
  box-shadow: 0 8px 26px rgba(0,0,0,.55);
}
.menu.hidden { display: none; }
/* Menus that anchor to the right edge of the bar (pages + viewers). */
.menu.menu-right { left: auto; right: 8px; }
#navMenu { min-width: 240px; }
.chat-panel {
  bottom: 52px;
  right: 8px;
  left: auto;
}
.chat-panel ul {
  list-style: none;
  padding: 0;
  margin: 0 0 6px 0;
  max-height: min(320px, calc(70vh - 100px));
  overflow-y: auto;
}
.chat-panel li {
  padding: 4px 0;
  font-size: 13px;
  border-bottom: 1px solid #282828;
  line-height: 1.35;
}
.chat-panel li .chat-name {
  font-weight: 600;
}
.chat-panel form {
  display: flex;
  gap: 4px;
}
.chat-panel input[type="text"] {
  flex: 1;
  padding: 5px 8px;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #eee;
  font-size: 13px;
  outline: none;
}
.chat-panel input[type="text"]:focus {
  border-color: #5d9b63;
}
.chat-panel button {
  padding: 5px 12px;
  border-radius: 6px;
}
.chat-panel.hidden { display: none; }
.menu h3 { margin: 4px 4px 8px; font-size: 14px; }
.menu-row {
  display: grid;
  grid-template-columns: 1fr auto auto;
  gap: 12px;
  align-items: center;
  padding: 7px 6px;
  border-top: 1px solid #303030;
}
.menu-row.mic { grid-template-columns: 1fr auto; }
/* Remote source rows carry a third button (Fullscreen) after the
   Broadcast/Record pair. */
.menu-row.remote { grid-template-columns: 1fr auto auto auto; }
.menu-row label { white-space: nowrap; font-size: 13px; }
/* Local mic rows grow a middle input-level meter column while capturing. */
.menu-row.mic.metered { grid-template-columns: 1fr auto auto; }
/* Input-level meter (host Sources menu mic rows): a width-animating fill
   inside a thin frame; the fill color is zone-based (green -> yellow -> red)
   via the data-zone attribute set by the meter. Fixed width in menu rows so
   the buttons don't shift as the level moves. */
.mic-meter {
  position: relative;
  height: 8px;
  border: 1px solid #333;
  border-radius: 4px;
  background: #161616;
  overflow: hidden;
}
.mic-meter-fill {
  height: 100%;
  width: 0%;
  background: #2ed573;
}
.mic-meter-fill[data-zone="mid"] { background: #ffd700; }
.mic-meter-fill[data-zone="high"] { background: #ff6348; }
.menu-row .mic-meter { width: 90px; }
/* Source-menu toggle buttons: grey when off, green when on or hovered.
   Hovering Record also lights the Broadcast button next to it, previewing
   that recording always broadcasts. On-state always stays green. */
.menu-row .src-btn {
  font-size: 13px;
  white-space: nowrap;
  color: #aaa;
}
.src-btn:hover,
.src-btn:focus-visible,
.src-btn[aria-pressed="true"] {
  background: #305d35;
  border-color: #5d9b63;
  color: #d9f2dc;
}
.menu-row:has(.src-btn-record:hover) .src-btn-broadcast {
  background: #305d35;
  border-color: #5d9b63;
  color: #d9f2dc;
}
/* Hovering a button that would turn its source OFF (pressing the button for
   the mode that is already active) shows red instead of green, warning that
   the source is about to be disabled. Hovering Record while recording also
   lights the Broadcast button next to it (recording always broadcasts). */
.menu-row:has(.src-btn-record[aria-pressed="true"]:is(:hover, :focus-visible)) .src-btn-record,
.menu-row:has(.src-btn-record[aria-pressed="true"]:is(:hover, :focus-visible)) .src-btn-broadcast,
.menu-row:has(.src-btn-broadcast[aria-pressed="true"]:is(:hover, :focus-visible)):not(:has(.src-btn-record[aria-pressed="true"])) .src-btn-broadcast,
.src-btn-use[aria-pressed="true"]:is(:hover, :focus-visible) {
  background: #5d3030;
  border-color: #9b5d5d;
  color: #f2d9d9;
}
.menu-empty { color: #888; padding: 8px; }
.small-note { color: #888; font-size: 12px; margin: 6px; }
/* Context menu shown on right-clicking a viewer's video in the broadcast
   strip: the same per-viewer permission toggles as the viewers list (mute,
   camera) plus ban. Fixed-positioned and placed at the cursor, so it can
   appear anywhere on the page. */
.viewer-context-menu {
  position: fixed;
  z-index: 100;
  min-width: 230px;
  max-width: min(320px, calc(100vw - 16px));
  padding: 6px;
  border: 1px solid #4a4a4a;
  border-radius: 8px;
  background: #202020;
  box-shadow: 0 8px 26px rgba(0,0,0,.55);
}
.viewer-context-menu h3 {
  margin: 2px 4px 6px;
  font-size: 13px;
  font-weight: 600;
  color: #ddd;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.viewer-context-menu button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 6px 8px;
  font-size: 13px;
}
.viewer-context-menu button + button { margin-top: 2px; }
.viewer-context-menu button.danger:hover,
.viewer-context-menu button.danger:focus-visible {
  background: #5d3030;
  border-color: #9b5d5d;
  color: #f2d9d9;
}
.spacer { flex: 1; }
.nav-links {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 2px 4px 4px;
}
.nav-links a {
  text-align: center;
  font-size: 13px;
  padding: 8px 12px;
  border: 1px solid #2c2c2c;
  border-radius: 8px;
  background: #141414;
  text-decoration: none;
  transition: border-color .15s ease, color .15s ease;
}
.nav-links a.viewer-page { color: #8fd69a; }
.nav-links a.viewer-page:hover { border-color: #5d9b63; color: #c4f0c8; }
.nav-links a.contrib-page { color: #c98fd6; }
.nav-links a.contrib-page:hover { border-color: #9b5d9b; color: #eec8ef; }
.nav-links a.home-page { color: #8fb8d6; }
.nav-links a.home-page:hover { border-color: #5d8db3; color: #cfe6f5; }
/* Recordings/Passwords actions in the pages menu: same card look as the
   page links. */
.nav-links button {
  text-align: center;
  font-size: 13px;
  padding: 8px 12px;
  border: 1px solid #2c2c2c;
  border-radius: 8px;
  background: #141414;
  color: #ddd;
  transition: border-color .15s ease, color .15s ease;
}
.nav-links button:hover { border-color: #4a4a4a; color: #eee; }

.join-shell {
  position: relative;
  height: 100vh;
  display: grid;
  place-items: center;
  /* Reserve the top strip for the room bar / host-contrib links row. */
  padding: 76px 16px 16px;
  background: radial-gradient(circle at 50% 30%, #222, #090909 70%);
}
/* The top row of the landing page: the room bar, centered on the page, with
   the host/contrib links on the same row to the right. */
.join-topbar {
  position: absolute;
  top: 16px;
  left: 16px;
  right: 16px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.join-topbar .join-links {
  position: absolute;
  right: 8px;
  display: flex;
  gap: 8px;
}
/* The room bar: a compact, distinct pill. */
.room-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 12px;
  border: 1px solid #333;
  border-radius: 999px;
  background: rgba(25, 25, 25, 0.72);
}
.room-bar label {
  font-size: 13px;
  color: #999;
}
.room-bar input {
  width: 210px;
  padding: 0.45rem 0.6rem;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #eee;
  font-size: 14px;
}
.join-shell .host-link {
  padding: 6px 14px;
  border: 1px solid #444;
  border-radius: 6px;
  background: #242424;
  color: #ccc;
  text-decoration: none;
  font-size: 13px;
}
.join-shell .host-link:hover {
  background: #303030;
  color: #eee;
}
.join-card {
  width: min(420px, calc(100vw - 32px));
  padding: 24px;
  border: 1px solid #333;
  border-radius: 12px;
  background: #191919;
  box-shadow: 0 10px 32px rgba(0,0,0,.5);
}
.join-card h1 { margin: 0 0 16px; font-size: 24px; }
.join-card input {
  width: 100%;
  margin-bottom: 12px;
  padding: 0.65rem;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #eee;
}
.join-card .login-error {
  margin: 0 0 12px;
  color: #ff8f8f;
  font-size: 14px;
}
.join-card button { width: 100%; }
/* Join + "request access" (waiting room) on one row: Join keeps its full
   width, the request button hugs its label. */
.join-buttons {
  display: flex;
  gap: 8px;
}
.join-buttons #joinButton {
  flex: 1 1 auto;
}
.join-buttons .request-button {
  flex: 0 0 auto;
  width: auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 0.65rem 0.9rem;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #ccc;
  font-size: 13px;
  white-space: nowrap;
  cursor: pointer;
}
.join-buttons .request-button:hover:not(:disabled) {
  color: #eee;
  border-color: #666;
}
.join-buttons .request-button:disabled {
  opacity: 0.6;
  cursor: default;
}
/* Button spinner while a waiting-room request is pending. */
@keyframes spin {
  to { transform: rotate(360deg); }
}
.request-button .spinner,
.request-access-button .spinner {
  display: none;
  width: 12px;
  height: 12px;
  border: 2px solid #9fd0ff;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
.request-button.pending .spinner,
.request-access-button.pending .spinner {
  display: inline-block;
}
.join-hint {
  margin: 12px 0 0;
  font-size: 13px;
  color: #888;
  text-align: center;
}

.viewer-shell {
  height: 100vh;
  display: grid;
  grid-template-rows: 1fr auto;
  background: #050505;
}
.viewer-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 0 12px;
  border-top: 1px solid #242424;
  background: #181818;
  height: 42px;
}
.chat-toggle {
  flex-shrink: 0;
  font-size: 13px;
  position: relative;
}
.viewer-mic-wrap {
  display: flex;
  align-items: stretch;
  flex-shrink: 0;
}
.mic-button, .mic-menu-button {
  position: relative;
  min-width: 34px;
  font-size: 14px;
}
.mic-button { border-radius: 6px 0 0 6px; }
.mic-menu-button { min-width: 24px; border-radius: 0 6px 6px 0; margin-left: -1px; }
/* Wallpaper picker button (viewer bar): the base button look at the mic
   button's size. */
.wallpaper-button { min-width: 34px; font-size: 14px; }
.mic-button.active::after {
  content: '';
  position: absolute;
  top: -4px;
  right: -4px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #ff4757;
  border: 2px solid #181818;
}
.mic-button.muted-by-host {
  opacity: .45;
  filter: grayscale(1);
}
.mic-device-menu {
  min-width: 260px;
  max-width: min(360px, calc(100vw - 24px));
}
.mic-device-row {
  display: block;
  width: 100%;
  text-align: left;
  margin: 3px 0;
}
.viewer-list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.viewer-mute-button { padding: 2px 8px; }
.viewer-mute-all-row button { width: 100%; margin-bottom: 4px; }
.unread-dot {
  position: absolute;
  top: -4px;
  right: -4px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #2ed573;
  border: 2px solid #181818;
}
.status-bar-text {
  color: #bbb;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
  flex: 1;
}
.viewer-shell.hidden, .join-shell.hidden, .contrib-shell.hidden { display: none; }
#videoStage {
  min-height: 0;
  display: grid;
  gap: 0;
  position: relative; /* containing block for the absolutely-positioned split divider */
  background: #000 url("./sky.webp") center / cover no-repeat;
}
/* Waiting lobby card, shown in the stage while no video is present.
   viewer.js keeps its text in sync with the current viewer state. The dark
   outline (directional text shadows — no background box) keeps the text
   legible over light wallpapers; over dark ones the outline nearly
   disappears and the light text reads as before. */
.lobby-card {
  display: grid;
  place-items: center;
  color: #aaa;
  font-size: 18px;
  line-height: 1.5;
  text-align: center;
  padding: 24px;
  max-width: 520px;
  text-shadow:
    -1px -1px 0 rgba(0, 0, 0, .9),
     1px -1px 0 rgba(0, 0, 0, .9),
    -1px  1px 0 rgba(0, 0, 0, .9),
     1px  1px 0 rgba(0, 0, 0, .9),
     0   -2px 0 rgba(0, 0, 0, .5),
     0    2px 0 rgba(0, 0, 0, .5),
    -2px   0  0 rgba(0, 0, 0, .5),
     2px   0  0 rgba(0, 0, 0, .5),
     0    5px 12px rgba(0, 0, 0, .4);
}
#videoStage.single {
  grid-template-columns: 1fr;
  align-items: center;
  justify-items: stretch;
}
#videoStage.multi {
  grid-template-columns: var(--main-pct, 70%) calc(100% - var(--main-pct, 70%));
}
.main-slot, .side-slot { min-width: 0; min-height: 0; background: transparent; }
.main-slot {
  display: grid;
  align-items: center;
  justify-items: stretch;
  overflow: hidden;
}
#videoStage.single .viewer-tile,
.main-slot .viewer-tile {
  width: 100%;
  height: auto;
}
#videoStage.single .viewer-tile {
  justify-self: center;
  width: min(100%, calc((100vh - 42px) * 16 / 9));
  aspect-ratio: 16 / 9;
}
.side-slot {
  display: flex;
  flex-direction: column;
  gap: 4px;
  justify-content: center;
  align-items: stretch;
  overflow: hidden;
}
.side-slot .viewer-tile {
  flex: 0 1 min(calc(100% / var(--side-count, 1)), calc(var(--side-pct, 30vw) * 9 / 16));
}
.stack-page-button {
  position: absolute;
  z-index: 6;
  right: calc((100% - var(--main-pct, 70%)) / 2);
  transform: translateX(50%);
  width: 52px;
  height: 34px;
  padding: 0;
  border-color: rgba(255,255,255,.45);
  background: rgba(10,10,10,.78);
  box-shadow: 0 2px 8px rgba(0,0,0,.55);
  font-size: 18px;
  line-height: 1;
}
.stack-page-button:hover { background: rgba(45,45,45,.92); }
.stack-page-up { top: 10px; }
.stack-page-down { bottom: 10px; }
/* Draggable divider between the main and side columns (viewer page).
   Centered exactly on the grid boundary via --main-pct; the 18px element is
   the hit area, the visible part is a thin line with a small grip pill.
   --divider-top/--divider-bottom are set by viewer.js so the bar matches the
   main video's vertical extent instead of floating over the wallpaper. */
.split-divider {
  position: absolute;
  top: var(--divider-top, 0);
  bottom: var(--divider-bottom, 0);
  left: var(--main-pct, 70%);
  width: 18px;
  margin-left: -9px;
  cursor: col-resize;
  touch-action: none;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: center;
}
.split-divider::before {
  content: '';
  width: 5px;
  height: 34px;
  border-radius: 4px;
  background: rgba(255, 255, 255, .35);
  border: 1px solid rgba(255, 255, 255, .25);
  transition: background .15s;
}
.split-divider:hover::before,
.split-divider:focus-visible::before,
body.split-dragging .split-divider::before {
  background: rgba(46, 213, 115, .8);
}
.split-divider:focus-visible { outline: none; }
/* Lock the cursor and text selection while a drag is in progress */
body.split-dragging,
body.split-dragging * {
  user-select: none !important;
  -webkit-user-select: none !important;
  cursor: col-resize !important;
}
.viewer-tile { position: relative; min-height: 0; background: transparent; }
.viewer-tile video { width: 100%; height: 100%; object-fit: contain; display: block; }
/* A tier flip (low <-> high) swaps a tile's stream, which changes its track
   id and re-creates the element. The outgoing element is kept as a frozen
   backdrop behind the fresh one until it paints, so the tile never flashes
   to black (most visible on the big main tile). */
.viewer-tile .tile-backdrop { position: absolute; inset: 0; z-index: 0; }
.viewer-tile .tile-pending { position: relative; z-index: 1; opacity: 0; transition: opacity 0.12s ease-in; }
.viewer-tile .tile-controls {
  position: absolute;
  top: 6px;
  right: 6px;
  display: flex;
  gap: 4px;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 2;
}
.viewer-tile:hover .tile-controls { opacity: 1; }
.tile-controls button {
  width: 32px;
  height: 32px;
  border-radius: 6px;
  border: 1px solid rgba(255,255,255,.2);
  background: rgba(0,0,0,.55);
  color: #fff;
  font-size: 13px;
  display: grid;
  place-items: center;
  cursor: pointer;
}
.tile-controls button:hover { background: rgba(0,0,0,.8); }
.tile-controls .btn-max {
  content: '⛶';
}
.tile-controls .btn-promote {
  font-size: 11px;
}
.tile-controls .btn-promote.active {
  background: rgba(0,0,0,.85);
  border-color: rgba(255,255,255,.4);
}
.tile-controls .btn-fs {
  font-size: 12px;
}
.max-overlay .tile-controls {
  position: absolute;
  top: 62px;
  right: 12px;
  opacity: 0;
  transition: opacity 0.15s;
  z-index: 2;
}
.max-overlay:hover .tile-controls { opacity: 1; }
.max-overlay .tile-controls button {
  width: 40px;
  height: 40px;
  font-size: 16px;
}
.max-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: #000;
  display: grid;
  place-items: center;
}
.max-overlay.hidden { display: none; }
.max-tile {
  width: 100%;
  height: 100%;
  position: relative;
  background: #000;
}
.max-tile video {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.max-tile .video-label {
  position: absolute;
  left: 16px;
  bottom: 70px;
  padding: 4px 10px;
  border-radius: 4px;
  background: rgba(0,0,0,.65);
  color: #ddd;
  font-size: 14px;
}
/* A viewer with no camera (or a host-disabled camera): a grey tile with the
   name centered, in a light per-viewer color (set inline on .no-video-name)
   so it reads against the dark grey. Used by both the host strip and the
   viewer stage. */
.no-video {
  background: #2a2a2a;
  display: flex;
  align-items: center;
  justify-content: center;
}
.no-video-name {
  font-weight: 600;
  font-size: 14px;
  text-align: center;
  padding: 8px;
  word-break: break-word;
  color: #cfcfcf;
}
/* The viewer stage sizes grey tiles to 16:9 so a grey tile occupying the main
   slot lines up with a video tile (a video tile takes its height from the
   video element). Side-slot tiles get their box from the flex layout, where
   this ratio is a no-op. */
.viewer-tile.no-video { aspect-ratio: 16 / 9; }
.close-max {
  position: absolute;
  top: 12px;
  right: 12px;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  border: none;
  background: rgba(0,0,0,.6);
  color: #fff;
  font-size: 18px;
  cursor: pointer;
  display: grid;
  place-items: center;
}
.close-max:hover { background: rgba(255,255,255,.2); }

.contrib-shell {
  height: 100vh;
  display: grid;
  grid-template-columns: 1fr 300px;
  grid-template-rows: 1fr 56px;
  gap: 8px;
  padding: 8px;
  background: #050505;
}
.contrib-stage {
  min-height: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(420px, 100%), 1fr));
  gap: 8px;
  overflow: auto;
  background: #050505;
  border: 1px solid #222;
  border-radius: 6px;
  padding: 8px;
}
.contrib-empty {
  display: grid;
  place-items: center;
  color: #777;
  min-height: 100%;
  text-align: center;
  padding: 24px;
}
.contrib-tile {
  position: relative;
  min-height: 220px;
  background: #000;
  border: 1px solid #222;
  border-radius: 4px;
}
.contrib-tile video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.contrib-badge {
  position: absolute;
  top: 6px;
  right: 6px;
  padding: 3px 8px;
  border: 1px solid #555;
  border-radius: 999px;
  background: rgba(0,0,0,.65);
  color: #bbb;
  font-size: 12px;
  font-weight: 700;
}
/* Audio-only sources (a shared microphone): a compact tile with an icon,
   the label, and an input-level meter instead of a video element. */
.contrib-tile.audio-tile {
  min-height: 150px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 16px;
  background: #101010;
}
.contrib-tile.audio-tile .audio-tile-icon {
  font-size: 36px;
  line-height: 1;
}
.contrib-tile.audio-tile .mic-meter {
  width: min(60%, 180px);
}
/* ON AIR: the host has selected this source — the audience sees it. */
.contrib-badge.onair {
  color: #d8ffd9;
  background: rgba(28,74,33,.92);
  border-color: #57a85f;
  animation: onairpulse 2s ease-in-out infinite;
}
@keyframes onairpulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(87, 168, 95, .5); }
  50% { box-shadow: 0 0 0 6px rgba(87, 168, 95, 0); }
}
.contrib-tile.onair {
  border-color: #57a85f;
  box-shadow: 0 0 0 3px rgba(77, 141, 84, .3);
}
.slides-picker {
  margin: 0 0 10px;
}
.slides-picker button {
  width: 100%;
  padding: 8px 10px;
  border: 1px dashed #4a4a4a;
  border-radius: 6px;
  background: #101010;
  color: #ccc;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
}
.slides-picker button:hover:not(:disabled) {
  border-color: #6aa8c4;
  color: #fff;
}
.slides-picker button:disabled {
  opacity: .55;
  cursor: wait;
}
/* Slide transition picker (only meaningful while a deck is shared, so it
   starts disabled). */
.slides-transition {
  margin: 0;
}
.slides-transition label {
  display: block;
  margin-bottom: 4px;
  color: #bbb;
  font-size: 12px;
}
.slides-transition select {
  width: 100%;
  box-sizing: border-box;
  padding: 6px 8px;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #eee;
  font-size: 13px;
  outline: none;
}
.slides-transition select:disabled { opacity: .5; cursor: not-allowed; }
/* Fullscreen toggle for the live slide canvas (only meaningful while a
   deck is shared, so it starts disabled). */
.slides-fullscreen {
  margin-top: 10px;
}
.slides-fullscreen button {
  width: 100%;
  box-sizing: border-box;
  padding: 6px 8px;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #eee;
  font-size: 13px;
  cursor: pointer;
}
.slides-fullscreen button:hover:not(:disabled) { border-color: #6aa8c4; color: #fff; }
.slides-fullscreen button:disabled { opacity: .5; cursor: not-allowed; }
/* Slide deck tile: the whole tile is the navigation surface (tap left half
   = previous slide, right half = next slide), so it gets a pointer cursor.
   The HUD overlay is purely visual (pointer-events: none). */
.slide-tile {
  cursor: pointer;
}
.slide-hud {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 8px;
  pointer-events: none;
}
.slide-hint {
  color: rgba(255,255,255,.5);
  font-size: 34px;
  font-weight: 700;
  line-height: 1;
  text-shadow: 0 1px 4px rgba(0,0,0,.8);
}
.slide-counter {
  position: absolute;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(0,0,0,.65);
  color: #ddd;
  font-size: 12px;
  font-weight: 700;
  white-space: nowrap;
}
/* Fullscreen host for the slide canvas: a stable, always-in-document
   container in <body> (never touched by renderStage() re-renders) holding
   the live canvas + a HUD. The Slides panel's Fullscreen button makes it
   the fullscreen element; until then it is invisible and inert. */
.slide-fs-host {
  position: fixed;
  inset: 0;
  z-index: 0;
  opacity: 0;
  pointer-events: none;
}
.slide-fs-host canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.slide-fs-host:fullscreen {
  opacity: 1;
  pointer-events: auto;
  background: #000;
  cursor: pointer;
}
/* ✕ in the corner of the fullscreen view: the primary way to exit without
   a keyboard (mobile). Lives in the host, so it is invisible and
   non-interactive unless the host is the fullscreen element. */
.slide-fs-exit {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1;
  width: 34px;
  height: 34px;
  border: 1px solid rgba(255, 255, 255, .3);
  border-radius: 999px;
  background: rgba(0, 0, 0, .55);
  color: #eee;
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  pointer-events: none;
  touch-action: manipulation;
}
.slide-fs-host:fullscreen .slide-fs-exit { pointer-events: auto; }
.slide-fs-host:fullscreen .slide-fs-exit:hover {
  background: rgba(0, 0, 0, .8);
  color: #fff;
}
/* Stable host for the dedicated fullscreen source player (see host.js).
   It remains a rendered 1×1 element until its container enters fullscreen;
   the source element used by WebGL is never moved or fullscreened. */
.video-fs-host {
  position: fixed;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  z-index: 0;
  overflow: hidden;
}
.video-fs-host:fullscreen {
  width: 100%;
  height: 100%;
  background: #000;
}
.video-fs-host video {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.sources-panel {
  grid-column: 2;
  grid-row: 1 / 3;
  min-height: 0;
  overflow: auto;
  border: 1px solid #242424;
  background: #181818;
  padding: 10px;
}
.sources-panel h2 {
  font-size: 16px;
  margin: 0 0 8px;
}
.sources-panel .sources-hint {
  margin: 0 0 10px;
  color: #888;
  font-size: 12px;
  line-height: 1.45;
}
.sources-panel .sources-empty {
  color: #777;
  font-size: 13px;
  padding: 8px 0;
}
/* Slides section: deck upload + transition picker grouped together,
   visually separated from the live source list above. */
.sources-panel .slides-group {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid #2c2c2c;
}
/* Source rows reuse the host page's Sources-menu row/button styles
   (.menu-row + .src-btn); panel-specific bits are just the label ellipsis
   and the blue on-air glow on the Share button. */
#sourceList { margin: 0 0 8px; }
/* Section headers inside the source list (e.g. the "Audio" group for
   shared microphones). */
#sourceList h3 {
  margin: 10px 0 4px;
  padding: 0 6px;
}
#sourceList h3 + .menu-row { border-top: none; }
#sourceList .menu-row { font-size: 13px; }
#sourceList .menu-row:first-child { border-top: none; }
#sourceList .source-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* On air: the host is broadcasting this source. A blue glow around the
   Share button replaces the old "ON AIR" chip. */
#sourceList .src-btn.onair {
  box-shadow: 0 0 0 1px rgba(74, 158, 255, .9), 0 0 12px 2px rgba(74, 158, 255, .5);
}
.contrib-bar {
  grid-column: 1;
  grid-row: 2;
}

/* ==========================================================================
   Mobile optimizations
   Everything below is scoped to touch input and/or narrow viewports, so the
   desktop layout and behavior above are unaffected.
   ========================================================================== */

/* Touch devices have no :hover — controls that rely on it must stay visible */
@media (hover: none) and (pointer: coarse) {
  .tile-controls { opacity: 1; }
  .tile-controls button {
    width: 40px;
    height: 40px;
    font-size: 15px;
  }
  .max-overlay .tile-controls { opacity: 1; }
  .max-overlay .tile-controls button {
    width: 46px;
    height: 46px;
  }
  .close-max {
    width: 46px;
    height: 46px;
  }
  /* Contrib page: the slide tile is the whole navigation surface on mobile —
     drop the double-tap-zoom pause so tap-to-page feels instant — and give
     the source-row Share buttons a more thumb-friendly hit area. */
  .slide-tile { touch-action: manipulation; }
  #sourceList .src-btn { padding: 8px 14px; }
}

@media (max-width: 768px), (max-height: 500px) and (orientation: landscape) {
  /* Dynamic viewport height avoids mobile browser chrome clipping/jumping content.
     Browsers without dvh support simply ignore this line and keep the vh above. */
  .join-shell,
  .viewer-shell,
  .max-overlay {
    height: 100vh;
    height: 100dvh;
  }

  /* Landing page: stack the top row so the centered room bar and the
     host/contrib links don't collide on narrow screens. */
  .join-shell { padding-top: 108px; }
  .join-topbar {
    top: 12px;
    flex-direction: column;
    gap: 10px;
  }
  .join-topbar .join-links { position: static; }

  /* Chat becomes a full-width bottom sheet and tracks the on-screen keyboard
     via --kb-inset (set by viewer.js from the visualViewport API) */
  .chat-panel {
    left: 0;
    right: 0;
    width: 100%;
    max-width: none;
    min-width: 0;
    bottom: calc(42px + var(--kb-inset, 0px));
    border-radius: 12px 12px 0 0;
    max-height: min(60vh, 60dvh);
  }
  .chat-panel ul {
    max-height: calc(min(60vh, 60dvh) - 90px);
  }

  .viewer-bar {
    height: calc(42px + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
}

@media (max-width: 768px) and (orientation: portrait) {
  /* Video stage: one main speaker full-width, everyone else as a horizontal
     filmstrip below (tap a thumbnail to swap it into the main slot).
     Portrait only — landscape keeps the side-by-side desktop-style layout,
     since there's no spare vertical room to stack a filmstrip under it. */
  #videoStage.multi {
    display: flex;
    flex-direction: column;
    grid-template-columns: none;
    min-width: 0;
  }
  #videoStage.multi .main-slot {
    flex: 1;
    min-height: 0;
    min-width: 0;
  }
  #videoStage.multi .side-slot {
    flex: 0 0 auto;
    flex-direction: row;
    justify-content: flex-start;
    width: 100%;
    max-width: 100%;
    min-width: 0;
    height: 108px;
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    padding: 4px 0;
  }
  #videoStage.multi .side-slot .viewer-tile {
    flex: 0 0 auto;
    width: 148px;
    min-width: 148px;
    height: 100%;
    cursor: pointer;
  }
  /* Promote button is redundant once tapping a filmstrip thumbnail does the same job */
  #videoStage.multi .side-slot .tile-controls .btn-promote {
    display: none;
  }

  /* The main/side split ratio is meaningless once side videos become a filmstrip */
  #videoStage .split-divider { display: none; }

  /* Portrait phones have vertical room to spare — make filmstrip thumbnails
     bigger and more legible than the landscape default set above. */
  #videoStage.multi .side-slot {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    height: 40%;
  }
  #videoStage.multi .side-slot .viewer-tile {
    width: 50%;
    min-width: 224px;
  }
  /* Exactly one secondary video (2 total) — give it the full filmstrip width
     instead of the multi-video 50% sizing */
  #videoStage.multi .side-slot .viewer-tile:only-child {
    width: 100%;
    min-width: 100%;
  }
}

/* ==========================================================================
   Recordings management
   ========================================================================== */

.recordings-modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
}
.recordings-modal.hidden {
  display: none !important;
}
.recordings-modal-content {
  width: min(640px, calc(100vw - 32px));
  max-height: min(70vh, 560px);
  background: #1e1e1e;
  border: 1px solid #4a4a4a;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.recordings-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid #333;
  flex-shrink: 0;
}
.recordings-modal-header h3 {
  margin: 0;
  font-size: 16px;
}
.recordings-close-btn {
  background: none;
  border: 1px solid #444;
  border-radius: 6px;
  color: #aaa;
  cursor: pointer;
  padding: 2px 8px;
  font-size: 14px;
}
.recordings-close-btn:hover {
  background: #303030;
  color: #eee;
}
.recordings-list-wrap {
  overflow: auto;
  flex: 1;
}
#recordingsList {
  width: 100%;
  min-width: 560px;
  border-collapse: collapse;
  table-layout: fixed;
  font-size: 13px;
}
#recordingsList th {
  position: sticky;
  top: 0;
  z-index: 1;
  background: #1e1e1e;
  text-align: left;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #888;
  padding: 10px 16px 8px;
  border-bottom: 1px solid #333;
}
#recordingsList th.rec-col-date {
  width: 120px;
}
#recordingsList th.rec-col-actions {
  width: 290px;
  text-align: right;
}
#recordingsList td {
  padding: 8px 16px;
  border-bottom: 1px solid #2a2a2a;
  vertical-align: middle;
}
#recordingsList tbody tr:last-child td {
  border-bottom: none;
}
.rec-filecell {
  display: flex;
  align-items: center;
  gap: 8px;
}
.rec-filename {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  font-family: monospace;
  font-size: 12px;
}
.rec-date {
  color: #888;
  font-size: 11px;
  white-space: nowrap;
}
.rec-indicator {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
.rec-indicator.uploaded {
  background: #2ed573;
}
.rec-indicator.processed {
  background: #48dbfb;
}
.rec-buttons {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
}
.rec-buttons button {
  padding: 4px 10px;
  font-size: 12px;
  border-radius: 5px;
  white-space: nowrap;
}
.rec-empty {
  color: #888;
  text-align: center;
  padding: 32px 16px;
  font-size: 14px;
}

/* Set-passwords modal (admin) — reuses the recordings-modal frame. */
.passwords-modal-content {
  width: min(640px, calc(100vw - 32px));
  max-height: min(80vh, 640px);
}
.passwords-modal-body {
  padding: 16px;
  overflow: auto;
}
.passwords-hint {
  margin: 0 0 16px;
  color: #999;
  font-size: 13px;
}
.passwords-row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: 10px;
}
.passwords-row span {
  width: 100px;
  flex-shrink: 0;
  font-size: 13px;
  color: #ccc;
}
.passwords-row input {
  flex: 1;
  min-width: 0;
  padding: 0.55rem;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #eee;
  font-size: 14px;
}
.passwords-toggle {
  flex-shrink: 0;
  width: 52px;
  padding: 0.55rem 0;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #999;
  font-size: 12px;
  cursor: pointer;
}
.passwords-toggle:hover {
  color: #eee;
  border-color: #666;
}
.passwords-toggle.shown {
  color: #ffd54f;
}
#passwordsError {
  color: #ff8f8f;
  font-size: 13px;
  margin: 4px 0 8px;
}
.passwords-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 8px;
}
.passwords-actions button {
  padding: 8px 18px;
  border-radius: 6px;
}
/* Set-passwords modal section headings */
.passwords-section-title {
  margin: 20px 0 8px;
  font-size: 14px;
  color: #eee;
}

/* Access methods (set-passwords modal): one row with a radio per mode
   (password required / waiting room / admit all). */
.access-methods {
  width: 100%;
  border-collapse: collapse;
  margin: 0 0 8px;
  font-size: 12px;
}
.access-methods td {
  padding: 6px 8px;
  text-align: center;
  vertical-align: middle;
  border-bottom: 1px solid #2c2c2c;
  color: #ccc;
  white-space: nowrap;
}
.access-methods input[type="radio"] {
  cursor: pointer;
  margin-right: 6px;
}
/* Waiting-room admission notice (host page): a slim floating pill,
   Zoom-style, anchored above the control bar. It never dims or blocks the
   page — the host keeps working while they decide. One line: the
   requester, then Deny / Admit. It floats above the modals (z-index 1000)
   so a pending request can be answered without closing an open dialog. */
.access-dialog {
  position: fixed;
  bottom: 72px; /* 8px above the one-row control bar (it ends 64px up) */
  left: 50%;
  transform: translateX(-50%);
  z-index: 1000;
  display: flex;
  align-items: center;
  gap: 12px;
  max-width: calc(100vw - 24px);
  padding: 8px 8px 8px 16px;
  border: 1px solid #444;
  border-radius: 999px;
  background: #1e1e1e;
  box-shadow: 0 8px 24px rgba(0,0,0,.5);
}
.access-dialog.hidden {
  display: none;
}
.access-dialog-text {
  min-width: 0;
  font-size: 14px;
  color: #eee;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.access-dialog-buttons {
  display: flex;
  gap: 6px;
}
.access-dialog-buttons button {
  padding: 5px 14px;
  border: 1px solid #444;
  border-radius: 999px;
  background: #0d0d0d;
  color: #ccc;
  font-size: 13px;
  cursor: pointer;
  white-space: nowrap;
}
.access-dialog-buttons button:hover {
  color: #eee;
  border-color: #666;
}
.access-dialog-buttons .access-admit {
  background: #1c4a2a;
  border-color: #2e7d4f;
  color: #c8f0d4;
}
.access-dialog-buttons .access-admit:hover {
  background: #226037;
}
/* Contributor requests render in a brighter palette so the host can tell
   the two apart at a glance. */
.access-dialog.access-dialog-contrib {
  background: #2a2a3f;
  border-color: #7a7ac0;
}
.access-dialog.access-dialog-contrib .access-dialog-text {
  color: #e6e6ff;
}
/* At-capacity indicator on the host control bar (the room has reached its
   viewer limit). */
.capacity-warning {
  color: #ffb347;
  font-size: 12px;
  white-space: nowrap;
  padding: 2px 8px;
  border: 1px solid #6b4e1f;
  border-radius: 10px;
  background: #2a2010;
}
/* The login page's button row (waiting room): Sign in + "request access" on
   one line, like the viewer page's join card (`.join-buttons`) — Sign in
   keeps its full width, the request button hugs its label. */
.login-buttons {
  display: flex;
  gap: 8px;
}
.login-buttons button[type="submit"] {
  flex: 1 1 auto;
}
.request-access-button {
  flex: 0 0 auto;
  width: auto;
  padding: 0.65rem 0.9rem;
  border: 1px solid #444;
  border-radius: 6px;
  background: #0d0d0d;
  color: #ccc;
  font-size: 13px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  white-space: nowrap;
  cursor: pointer;
}
.request-access-button:hover:not(:disabled) {
  color: #eee;
  border-color: #666;
}
.request-access-button:disabled {
  opacity: 0.6;
  cursor: default;
}

/* Logout link on the viewer join screen */
.logout-link {
  display: inline-block;
  margin-top: 12px;
  color: #888;
  font-size: 13px;
  text-decoration: none;
}
.logout-link:hover {
  color: #ccc;
}

/* Confirm deletion modal */
.confirm-modal {
  position: fixed;
  inset: 0;
  z-index: 250;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
}
.confirm-modal.hidden {
  display: none !important;
}
.confirm-modal-content {
  width: min(400px, calc(100vw - 32px));
  background: #1e1e1e;
  border: 1px solid #4a4a4a;
  border-radius: 10px;
  padding: 20px 24px;
  text-align: center;
}
.confirm-modal-content p {
  margin: 0 0 16px;
  font-size: 14px;
  color: #ddd;
}
.confirm-modal-buttons {
  display: flex;
  gap: 10px;
  justify-content: center;
}
.confirm-modal-buttons button {
  padding: 8px 20px;
  font-size: 13px;
  border-radius: 6px;
}

/* Recording save dialog popup */
.save-dialog-overlay {
  position: fixed;
  inset: 0;
  z-index: 180;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
}
.save-dialog-overlay.hidden {
  display: none !important;
}
.save-dialog {
  width: min(420px, calc(100vw - 32px));
  background: #202020;
  border: 1px solid #4a4a4a;
  border-radius: 10px;
  padding: 20px 24px;
}
.save-dialog h3 {
  margin: 0 0 16px;
  font-size: 15px;
}
.save-dialog-buttons {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
}
.save-dialog-buttons button {
  flex: 1;
  padding: 8px 12px;
  font-size: 13px;
  border-radius: 6px;
  text-align: center;
}
.save-dialog-checkbox {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 16px;
  font-size: 13px;
  color: #bbb;
}
.save-dialog-checkbox input[type="checkbox"] {
  accent-color: #2ed573;
}
.save-dialog-cancel {
  width: 100%;
  padding: 8px 12px;
  font-size: 13px;
  border-radius: 6px;
  text-align: center;
}

/* Upload progress bar: fixed toast at the top of the page while a
   recording uploads. */
#uploadProgress {
  position: fixed;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  width: min(420px, calc(100vw - 32px));
  z-index: 400;
  display: none;
}
#uploadProgress.active {
  display: block;
}
.upload-progress-bar {
  height: 4px;
  border-radius: 2px;
  background: #333;
  overflow: hidden;
  margin-bottom: 4px;
}
.upload-progress-fill {
  height: 100%;
  width: 0%;
  background: #48dbfb;
  transition: width 0.2s ease;
}
.upload-progress-text {
  font-size: 11px;
  color: #aaa;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ==========================================================================
   Host page mobile fix
   Same underlying problem as the viewer had: the Sources/chat/pages/viewers
   popups anchor with a fixed min-width and no keyboard awareness, so on a
   narrow screen they get clipped or covered. This does not change the
   desktop grid layout (golden banner row, broadcast strip, recording
   canvas).
   ========================================================================== */

@media (max-width: 768px), (max-height: 500px) and (orientation: landscape) {
  .host-shell {
    height: 100vh;
    height: 100dvh;
  }

  /* Both the Sources menu and chat panel share the .menu popup pattern —
     turn them into full-width bottom sheets that track the on-screen
     keyboard via --kb-inset (set by host.js from the visualViewport API). */
  .control-bar .menu {
    position: fixed;
    left: 0;
    right: 0;
    width: 100%;
    max-width: none;
    min-width: 0;
    bottom: calc(52px + var(--kb-inset, 0px));
    border-radius: 12px 12px 0 0;
    max-height: min(60vh, 60dvh);
  }
  .control-bar .chat-panel ul {
    max-height: calc(min(60vh, 60dvh) - 90px);
  }

  /* Hide the keyboard-only screen-capture hint on narrow screens. */
  #screenHint {
    display: none;
  }
}

/* ==========================================================================
   Contrib page mobile fix
   The desktop layout is a two-column grid (stage + fixed 300px sources
   sidebar). On a narrow phone that leaves the stage — where the slide tile
   the presenter actually watches — a sliver. On narrow/short screens the
   sidebar becomes a full-width bottom sheet above the control bar (the same
   pattern as the host page's Sources menu and the viewer's chat panel),
   toggled by the Sources button in the bar and starting closed so the stage
   gets the full screen. The chat panel gets the viewer page's bottom-sheet +
   keyboard-tracking treatment. Desktop layout and behavior are unaffected.
   ========================================================================== */

.sources-toggle {
  display: none;
  flex-shrink: 0;
}

@media (max-width: 768px), (max-height: 500px) and (orientation: landscape) {
  .contrib-shell {
    height: 100vh;
    height: 100dvh;
    grid-template-columns: 1fr;
    grid-template-rows: 1fr auto;
    /* The control bar's rendered height on mobile; the bottom sheets below
       share it so they always sit exactly above the bar (including the
       home-indicator safe area). */
    --contrib-bar-h: calc(44px + env(safe-area-inset-bottom, 0px));
  }
  .contrib-stage {
    grid-column: 1;
    grid-row: 1;
    min-width: 0;
    /* One full-width column. (The desktop auto-fit minmax() becomes circular
       on a stage narrower than 420px — the track's 100% fallback grows the
       stage past the viewport — so pin an explicit single track.) */
    grid-template-columns: 1fr;
    /* A single source (the slide tile) fills the whole stage; several
       sources keep a usable size, stack, and the stage scrolls. */
    grid-auto-rows: minmax(220px, 1fr);
  }
  .contrib-bar {
    grid-column: 1;
    grid-row: 2;
    height: var(--contrib-bar-h);
    padding: 4px 8px;
  }
  .sources-toggle {
    display: inline-block;
  }

  /* Sources sidebar → full-width bottom sheet above the bar, tracking the
     on-screen keyboard via --kb-inset (set by contrib.js from the
     visualViewport API). */
  .sources-panel {
    position: fixed;
    left: 0;
    right: 0;
    width: 100%;
    max-width: none;
    bottom: calc(var(--contrib-bar-h) + var(--kb-inset, 0px));
    max-height: min(60vh, 60dvh);
    border-radius: 12px 12px 0 0;
    box-shadow: 0 -8px 26px rgba(0, 0, 0, .55);
    z-index: 20;
  }

  /* Chat: the viewer's global mobile rule above already makes .chat-panel a
     full-width sheet, but the base .menu is position:absolute — which inside
     the control bar would anchor the sheet to the bar instead of the
     viewport. Pin it to the viewport above the bar. */
  .contrib-bar .chat-panel {
    position: fixed;
    bottom: calc(var(--contrib-bar-h) + var(--kb-inset, 0px));
  }
}

/* Remote source indicator dot on the Sources button */
.remote-dot {
  position: absolute;
  top: -4px;
  right: -4px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #ffa94d;
  border: 2px solid #181818;
}

/* ==========================================================================
   Wallpaper picker (viewer page)
   The modal lists a thumbnail of every available wallpaper: the built-in
   default plus custom images the user has uploaded (stored in this
   browser's IndexedDB). See wallpapers.js.
   ========================================================================== */
.wallpaper-modal {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
}
.wallpaper-modal.hidden { display: none !important; }
.wallpaper-modal-content {
  width: min(660px, calc(100vw - 32px));
  max-height: min(80vh, 600px);
  background: #1e1e1e;
  border: 1px solid #4a4a4a;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.wallpaper-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid #333;
  flex-shrink: 0;
}
.wallpaper-modal-header h3 {
  margin: 0;
  font-size: 16px;
}
.wallpaper-close-btn {
  background: none;
  border: 1px solid #444;
  border-radius: 6px;
  color: #aaa;
  cursor: pointer;
  padding: 2px 8px;
  font-size: 14px;
}
.wallpaper-close-btn:hover {
  background: #303030;
  color: #eee;
}
.wallpaper-grid {
  overflow: auto;
  flex: 1;
  padding: 14px 16px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 12px;
  align-content: start;
}
.wallpaper-tile {
  display: flex;
  flex-direction: column;
  gap: 6px;
  cursor: pointer;
  border: none;
  background: none;
  padding: 0;
}
.wallpaper-tile:focus-visible {
  outline: none;
}
.wallpaper-tile:focus-visible .wallpaper-thumb {
  border-color: #8fd69a;
  box-shadow: 0 0 0 2px rgba(143, 214, 154, .4);
}
.wallpaper-thumb {
  position: relative;
  aspect-ratio: 16 / 10;
  border: 2px solid #3a3a3a;
  border-radius: 8px;
  overflow: hidden;
  background: #000;
  transition: border-color .15s;
}
.wallpaper-tile:hover .wallpaper-thumb { border-color: #6a6a6a; }
.wallpaper-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The selected wallpaper: the app's green ring plus a check badge. */
.wallpaper-tile.selected .wallpaper-thumb {
  border-color: #2ed573;
  box-shadow: 0 0 0 2px rgba(46, 213, 115, .35);
}
.wallpaper-tile.selected .wallpaper-thumb::after {
  content: '✓';
  position: absolute;
  right: 6px;
  bottom: 6px;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: #2ed573;
  color: #0a3320;
  font-size: 13px;
  font-weight: 700;
  pointer-events: none;
}
.wallpaper-delete-btn {
  position: absolute;
  top: 6px;
  right: 6px;
  width: 22px;
  height: 22px;
  border: 1px solid rgba(255, 255, 255, .35);
  border-radius: 50%;
  background: rgba(0, 0, 0, .65);
  color: #fff;
  font-size: 11px;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
}
.wallpaper-delete-btn:hover {
  background: #6a2424;
  border-color: #a64848;
}
.wallpaper-name {
  font-size: 12px;
  color: #ccc;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Upload tile: a dashed placeholder with a plus; the whole tile is the
   label for the (hidden) file input, so a click opens the picker. */
.wallpaper-tile.upload .wallpaper-thumb {
  border-style: dashed;
  border-color: #4a4a4a;
  display: grid;
  place-items: center;
  background: #141414;
}
.wallpaper-tile.upload:hover .wallpaper-thumb { border-color: #6aa8c4; }
.wallpaper-upload-plus {
  font-size: 30px;
  line-height: 1;
  color: #888;
}
.wallpaper-tile.upload:hover .wallpaper-upload-plus { color: #6aa8c4; }
.wallpaper-tile.upload input[type="file"] { display: none; }
/* One-line error (non-image file, oversized, storage full, …). */
.wallpaper-modal-status {
  padding: 0 16px 12px;
  font-size: 12px;
  color: #ff8f8f;
  flex-shrink: 0;
}
.wallpaper-modal-status.hidden { display: none; }

/* ==========================================================================
   Host help modal
   ========================================================================== */
.help-modal {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(0, 0, 0, 0.7);
  display: flex;
  align-items: center;
  justify-content: center;
}
.help-modal.hidden { display: none !important; }
.help-modal-content {
  width: min(720px, calc(100vw - 32px));
  max-height: min(80vh, 640px);
  background: #1e1e1e;
  border: 1px solid #4a4a4a;
  border-radius: 10px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.help-modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid #333;
  flex-shrink: 0;
}
.help-modal-header h3 {
  margin: 0;
  font-size: 16px;
}
.help-body {
  overflow: auto;
  flex: 1;
  padding: 14px 18px 18px;
  font-size: 13.5px;
  line-height: 1.5;
  color: #ddd;
}
.help-body h4 {
  margin: 18px 0 6px;
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: #8fd69a;
}
.help-body h4:first-child { margin-top: 0; }
.help-body p, .help-body li { margin: 0 0 8px; }
.help-body ul, .help-body ol { margin: 0 0 8px; padding-left: 22px; }
.help-body li:last-child, .help-body p:last-child { margin-bottom: 0; }
.help-body strong { color: #eee; }
.help-body a { color: #8fd69a; }

/* --- Rooms ----------------------------------------------------------- */

/* Room picker on the viewer join card (landing page). Matches the card's
   input styling; the label text is dim and the dropdown fills the row. */

/* "Back to landing page" link on the right of the active pages' bottom
   bars. Dim by default, brightens on hover (the bars are dark). */
.landing-link {
  margin-left: auto;
  flex-shrink: 0;
  color: #888;
  font-size: 13px;
  text-decoration: none;
  white-space: nowrap;
}
.landing-link:hover {
  color: #ccc;
}
.control-bar .landing-link {
  margin-left: 0;
}

/* Recordings modal: the all-rooms checkbox (admin only) and the room
   column it reveals. */
.recordings-all-rooms {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
  font-size: 13px;
  color: #aaa;
  border-bottom: 1px solid #2a2a2a;
  background: #181818;
  position: sticky;
  top: 0;
  z-index: 2;
}
#recordingsList th.rec-col-room {
  width: 110px;
}
#recordingsList td.rec-room {
  color: #9ab;
  font-size: 12px;
  word-break: break-all;
}

/* Passwords modal: the global-admin note (shown only to an admin in the
   main room, next to the admin field). */
.passwords-note {
  margin: 4px 0 12px;
  font-size: 12px;
  line-height: 1.4;
  color: #999;
}
