/* CynchCom — Discord-style dark UI, mobile-first */
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
  /* Slightly cooler, deeper base with more separation between layers than the old ramp —
     panels now read as distinct surfaces instead of three near-identical greys. */
  --bg-0: #0c0d12;
  --bg-1: #14161d;
  --bg-2: #1e2129;
  --bg-3: #292d38;
  --text: #e8eaef;
  --muted: #8f949f;
  --accent: #5b8cff;
  --accent-2: #7aa2ff;
  --green: #3ecf8e;
  --red: #f0556b;
  --line: #262a35;
  --radius: 10px;
  --radius-lg: 14px;
}
html, body { height: 100%; }
body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg-0);
  color: var(--text);
  overscroll-behavior: none;
}
.hidden { display: none !important; }
.muted { color: var(--muted); font-size: 0.8rem; }
button { cursor: pointer; font: inherit; }

/* ---------- auth ---------- */
.auth-screen {
  height: 100dvh; display: flex; align-items: center; justify-content: center;
  background: radial-gradient(1200px 600px at 20% -10%, #1e2743 0%, var(--bg-0) 60%);
  padding: 16px;
}
.auth-card {
  width: 100%; max-width: 380px; background: var(--bg-1);
  border: 1px solid #2e3140; border-radius: 16px; padding: 32px 28px;
  box-shadow: 0 20px 60px rgba(0,0,0,.5);
}
.auth-card h1 { font-size: 1.7rem; letter-spacing: -0.02em; }
.auth-card h1 span, .sidebar-head h2 span { color: var(--accent); }
.tagline { color: var(--muted); margin: 4px 0 24px; font-size: .9rem; }
.auth-card form { display: flex; flex-direction: column; gap: 10px; }
.auth-card input {
  background: var(--bg-2); border: 1px solid #333645; color: var(--text);
  border-radius: var(--radius); padding: 12px 14px; font-size: 1rem; outline: none;
}
.auth-card input:focus { border-color: var(--accent); }
.auth-error { color: var(--red); font-size: .85rem; min-height: 1em; }
.form-note { color: var(--muted); font-size: .9rem; margin-bottom: 6px; }
.record-note { color: #e8b33e; font-size: .8rem; margin-top: 8px; }

.btn {
  border: none; border-radius: var(--radius); padding: 11px 16px;
  background: var(--bg-3); color: var(--text); font-weight: 600;
}
.btn.primary { background: var(--accent); color: #fff; }
.btn.primary:hover { background: var(--accent-2); }
.btn.sso { background: var(--bg-2); border: 1px solid #333645; }
.btn.sso:hover { border-color: var(--accent); }
.auth-divider {
  display: flex; align-items: center; gap: 10px;
  color: var(--muted); font-size: .8rem; margin: 2px 0;
}
.auth-divider::before, .auth-divider::after {
  content: ""; flex: 1; height: 1px; background: #2e3140;
}
.btn.call { background: var(--green); color: #06281a; }
.icon-btn {
  background: transparent; border: none; color: var(--muted); font-size: 1rem;
  padding: 6px 8px; border-radius: 8px;
}
.icon-btn:hover { background: var(--bg-3); color: var(--text); }

/* ---------- layout ---------- */
.app { display: flex; height: 100dvh; overflow: hidden; }
.sidebar {
  width: 250px; min-width: 250px; background: var(--bg-1); display: flex;
  flex-direction: column; border-right: 1px solid #262935;
}
.sidebar-head { padding: 16px; border-bottom: 1px solid #262935; display: flex; justify-content: space-between; align-items: center; }
/* The wordmark replaces the text logotype. It is a transparent PNG on a dark sidebar, so it needs
   no background — but it does need a height cap, because the source is 720px wide and would
   otherwise blow the header open on a narrow sidebar. */
/* The login card has room, so the lockup runs bigger there than in the sidebar. Same asset, same
   32%-text problem — it just has the space to solve it by size. */
.auth-card h1.wordmark { line-height: 0; margin-bottom: 4px; }
.auth-card h1.wordmark img { height: 72px; width: auto; max-width: 100%; display: block; }
.sidebar-head h2.wordmark { line-height: 0; }
/* The letters are only 32% of this image's height — the diamond sets the rest — so the lockup has to
   run taller than a text logotype would to stay readable. At 28px the words rendered at 9px and read
   as a smudge; 44px puts them at 14px. Cropping the diamond off instead was tried and doesn't work:
   the "c" nests inside the diamond, so a straight cut takes the "cy" with it. */
.sidebar-head h2.wordmark img { height: 44px; width: auto; max-width: 100%; display: block; }
.sidebar-section { flex: 1; overflow-y: auto; padding: 12px 8px; }
.section-title {
  display: flex; justify-content: space-between; align-items: center;
  padding: 0 8px 6px; color: var(--muted); font-size: .75rem;
  text-transform: uppercase; letter-spacing: .08em;
}
/* The second heading, above the text-only list. It needs air over it that the first one doesn't —
   without this the label sat against the last room and read as belonging to it. */
.section-title.group-next { padding-top: 14px; }
.channel-list { list-style: none; }
/* The one job of this list is answering "where is everybody" at a glance, so an occupied room has
   to look different from an empty one before you read a single name. Empty rooms stay flat and
   quiet; occupied rooms lift into a soft card with a tinted spine. */
.channel-list li {
  padding: 6px 10px; border-radius: 9px; color: var(--muted); cursor: pointer;
  font-size: .93rem; position: relative;
  margin-bottom: 1px;
  transition: background .13s ease, color .13s ease;
  -webkit-tap-highlight-color: transparent;
}
.channel-list li:hover { background: var(--bg-2); color: var(--text); }
.channel-list li.occupied {
  background: rgba(255,255,255,.035);
  padding-top: 8px; padding-bottom: 9px;
  margin: 3px 0 6px;
}
/* The name needs to finish being a heading before the people start. Without this the first
   occupant sat directly against the room name and the two read as one clump. */
.channel-list li.occupied .ch-row { padding-bottom: 2px; }
.channel-list li.occupied:hover { background: rgba(255,255,255,.06); }
.channel-list li.occupied .ch-name { color: var(--text); font-weight: 600; }
/* Spine: accent for the room you're viewing, quiet green for a room that simply has people in it.
   Two different questions, so two different colours. */
.channel-list li.occupied::before {
  content: ""; position: absolute; left: 0; top: 8px; bottom: 8px;
  width: 2px; border-radius: 0 2px 2px 0; background: rgba(62,207,142,.5);
}
.channel-list li.active { background: var(--bg-3); color: var(--text); font-weight: 600; }
.channel-list li.active::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 3px; height: 62%; border-radius: 0 3px 3px 0; background: var(--accent);
}
.channel-list li.active.occupied::before { top: 8px; bottom: 8px; height: auto; transform: none; }
/* The hash is structure, not signal — it was accent-blue on every row, competing with the things
   that actually mean something. */
.channel-list li .hash { color: #4d5464; font-weight: 600; }
.channel-list li.active .hash, .channel-list li.occupied .hash { color: #6b7386; }
/* The speaker that marks a room you can talk in, opposite the hash on a text-only one. Same weight
   as the hash on purpose: it says which kind the row is, and nothing about whether to click it. */
.ch-kind { color: #4d5464; display: inline-flex; flex: 0 0 auto; }
.channel-list li.active .ch-kind, .channel-list li.occupied .ch-kind { color: #6b7386; }
.channel-list li .lock { font-size: .75rem; }

/* ---------- admin channel reordering ---------- */
.drag-handle {
  color: #5a606e; cursor: grab; font-size: .95rem; line-height: 1;
  opacity: 0; transition: opacity .13s ease; flex: 0 0 auto;
  margin-left: -2px; margin-right: 1px;
}
.channel-list.reorderable li:hover .drag-handle,
.channel-list.reorderable li.active .drag-handle { opacity: 1; }
.drag-handle:active { cursor: grabbing; }
.channel-list li.dragging { opacity: .4; }
.channel-list li.drop-above { box-shadow: inset 0 2px 0 var(--accent); }
.channel-list li.drop-below { box-shadow: inset 0 -2px 0 var(--accent); }
.ch-row { display: flex; align-items: center; gap: 6px; }
.ch-name { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ch-unread {
  margin-left: auto; font-size: .68rem; font-weight: 700; color: #fff;
  background: var(--accent, #e0455b); border-radius: 8px; padding: 1px 7px;
}
.ch-unread.hidden, .ch-unread:empty { display: none; }
.ch-online {
  font-size: .66rem; font-weight: 700; color: var(--muted);
  background: var(--bg-2); border-radius: 8px; padding: 1px 6px;
}
/* Empty is empty: this pill with no text was a padded grey box that read as an em dash on every
   quiet room, which is where the list's clutter came from. */
.ch-online:empty { display: none; }
.channel-list li.active .ch-online { background: var(--bg-1); }
/* Occupants hang off their room with a hairline, so the group reads as belonging to it instead of
   floating as its own list item. */
.voice-list { list-style: none; padding-left: 11px; margin-top: 7px; position: relative; }
/* The list is always in the DOM, so on a quiet room its margin was still padding out the row —
   every empty room was 4px taller for a list with nothing in it. */
.voice-list:empty { margin-top: 0; }
.voice-list:not(:empty)::before {
  content: ""; position: absolute; left: 3px; top: 4px; bottom: 4px;
  width: 1px; background: rgba(255,255,255,.09);
}
.voice-list li { padding: 0; }
.voice-list .voice-row, .voice-list li {
  display: flex; align-items: center; gap: 8px;
  font-size: .78rem; color: var(--muted); padding: 4px 0 4px 8px;
}
.voice-icon { font-size: .7rem; }
.dm-title { margin-top: 16px; }
#dm-list .avatar { width: 22px; height: 22px; font-size: .6rem; }
.avatar.anahi { background: linear-gradient(135deg, #7aa2ff, #b07aff); font-size: .8rem; }
.members-panel .m-dm { display: none; font-size: .85rem; padding: 2px 4px; }
.members-panel li:hover .m-dm { display: inline-flex; }
.members-panel .m-dm.hidden { display: none !important; }
.um-row.clickable { cursor: pointer; }
.um-row.clickable:hover { background: var(--bg-3); }
.sidebar-foot {
  padding: 10px 12px; border-top: 1px solid #262935; display: flex;
  justify-content: space-between; align-items: center; gap: 8px;
}
.me { display: flex; align-items: center; gap: 8px; min-width: 0; }
.me-info { min-width: 0; }
.me-info > div:first-child { font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.avatar {
  width: 34px; height: 34px; border-radius: 50%; background: var(--accent);
  display: inline-flex; align-items: center; justify-content: center;
  font-weight: 700; color: #fff; font-size: .9rem; flex-shrink: 0;
  position: relative; overflow: hidden;
}
.avatar-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; border-radius: 50%;
}
.foot-actions { display: flex; }

.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.channel-head {
  display: flex; align-items: center; gap: 10px; padding: 12px 16px;
  border-bottom: 1px solid #262935; background: var(--bg-1);
}
.channel-title { flex: 1; min-width: 0; }
/* Sigue leyéndose como un título — nada de fondo ni borde de botón — pero el cursor y el subrayado
   al pasar por encima avisan de que, a diferencia de un <h3>, esto hace algo: es la puerta de
   WhatsApp hacia el panel de fotos y búsqueda. */
.channel-title .channel-name-btn {
  display: block; max-width: 100%; text-align: left; color: inherit;
  background: none; border: none; padding: 0; margin: 0; font: inherit;
  font-size: 1.05rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.channel-title .channel-name-btn:hover { text-decoration: underline; text-decoration-color: var(--muted); text-underline-offset: 3px; }
.channel-title .channel-name-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
.head-actions { display: flex; gap: 4px; align-items: center; }

.record-banner {
  background: #3a1420; color: #ff9daf; text-align: center; font-size: .78rem;
  padding: 5px 10px; letter-spacing: .03em;
}
.record-banner.warn { background: #3a2e10; color: #e8b33e; }
.record-banner.private { background: #10331f; color: #63d495; }

/* ---------- call stage ---------- */
.call-stage {
  background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-0) 100%);
  padding: 14px; border-bottom: 1px solid var(--line);
}
.rtk-mount { height: min(72dvh, 640px); }
.rtk-mount rtk-meeting { display: block; height: 100%; }
.video-grid {
  display: grid; gap: 10px;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
/* Each tile owns a hue (set per person in JS) so the camera-off state reads as a deliberate,
   identifiable presence card instead of the dead black rectangle it used to be. */
.video-tile {
  position: relative; border-radius: 14px; overflow: hidden;
  aspect-ratio: 16/9;
  background:
    radial-gradient(120% 120% at 30% 20%,
      hsl(var(--tile-hue, 226) 46% 30%) 0%,
      hsl(var(--tile-hue, 226) 42% 18%) 55%,
      hsl(var(--tile-hue, 226) 38% 12%) 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.06), 0 4px 18px rgba(0,0,0,.35);
  transition: box-shadow .18s ease, transform .18s ease;
}
.tile-placeholder {
  position: absolute; inset: 0; display: grid; place-items: center;
}
.tile-avatar {
  width: 72px; height: 72px; border-radius: 50%;
  display: grid; place-items: center; position: relative; overflow: hidden;
  background: hsl(var(--tile-hue, 226) 55% 42%);
  color: #fff; font-weight: 700; font-size: 1.5rem; letter-spacing: .02em;
  box-shadow: 0 0 0 4px hsl(var(--tile-hue, 226) 45% 22% / .85), 0 6px 18px rgba(0,0,0,.4);
}
.tile-avatar .avatar-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
/* Video sits on top of the placeholder and is only revealed when a live track exists. */
.video-tile video {
  position: relative; z-index: 1;
  width: 100%; height: 100%; object-fit: cover;
  background: #000; opacity: 0; transition: opacity .2s ease;
}
.video-tile.cam-on video { opacity: 1; }
.video-tile.cam-on .tile-placeholder { opacity: 0; }
.video-tile.screen video { object-fit: contain; }
/* Spotlight while a screen is shared: the screen takes every pixel left over after a thin filmstrip
   of faces, and NOTHING scrolls. A shared screen you have to scroll to see defeats the point — the
   whole room is meant to be looking at the same thing at once. So the stage height is divided
   exactly: strip height + gap for the faces, all the rest for the screen. */
/* Written with the id so it outranks `.call-stage #video-grid { overflow-y: auto }` — a class-only
   selector lost to it and the grid kept its scrollbar. */
/* `.call-stage #video-grid { overflow-y: auto }` further down has the SAME specificity as
   `#video-grid.has-screen` (one id, one class), so source order decided it and the scrollbar came
   back. Scoping to the stage as well settles it. */
.call-stage #video-grid.has-screen {
  --strip-h: 66px;
  display: flex; flex-wrap: wrap; gap: 10px;
  justify-content: center; align-content: center;
  /* Both axes: the strip is capped to one row that always fits, so any scrollbar here is a bug
     rather than something to scroll. */
  overflow: hidden;
}
.video-grid.has-screen .video-tile { flex: 0 1 auto; min-width: 0; }
/* Driven by height, not width, so the strip is always one predictable row. Faces may crop narrower
   than 16:9 when a lot of people are in the call; that beats a second row pushing the screen out. */
.video-grid.has-screen .video-tile:not(.screen) {
  height: var(--strip-h); width: auto; aspect-ratio: 16 / 9;
}
.video-grid.has-screen .video-tile .tile-avatar { width: 52px; height: 52px; font-size: 1.1rem; }
.video-grid.has-screen .video-tile.screen {
  order: -1; flex: 0 0 100%; width: 100%; aspect-ratio: auto;
  height: calc(100% - var(--strip-h) - 10px); min-height: 0;
  background: #000;
}
/* Nobody else on the call: the screen gets the whole stage. */
.video-grid.has-screen .video-tile.screen:only-child { height: 100%; }

/* ---- a second (or third) screen at the same time ----
   Anyone can share whenever they want, so the shares that don't hold the big view become small cards
   in the top-left corner: the screen behind them BLURRED, with the sharer's name and a Watch button.
   Click one and it takes the big view.

   Blurred, not a live thumbnail (which is what this was first): a second moving picture steals your
   eye from the screen you chose to watch, is unreadable at card size, and puts a colleague's screen on
   yours before you asked for it. Blurred it says "someone else is sharing" and nothing more.

   Absolutely positioned, so the filmstrip's one-row measurement never has to count these. Top-RIGHT:
   the faces are along the bottom and their button is bottom-right, and of the two free corners this is
   the one that covers less of what people actually share — tabs, menus and file trees live top-left.
   Sized and offset in % of the stage's WIDTH — percentages resolve against the container's width for
   both `width` and `right`, so the row scales as one piece and cannot drift out of the corner on a
   narrow window. */
.call-stage #video-grid.has-screen { --mini-w: clamp(104px, 27%, 200px); }
/* Three or more (i.e. four people sharing at once) — narrower, so the row still fits. Set from
   layoutScreenTiles(), which is the only place that knows the count. */
.call-stage #video-grid.has-screen.screens-crowded { --mini-w: clamp(84px, 18%, 140px); }
.video-grid.has-screen .video-tile.screen.screen-mini {
  position: absolute; top: 10px; z-index: 6;
  right: calc(10px + var(--mini-i, 0) * (var(--mini-w) + 8px));
  width: var(--mini-w); height: auto; aspect-ratio: 16 / 9;
  flex: 0 0 auto; min-height: 0;
  border-radius: 10px; background: #000;
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.16), 0 8px 24px rgba(0,0,0,.55);
}
/* Scaled up under the blur because blurring an element samples past its edges, which otherwise
   shows as a pale border all the way around the card. */
.video-tile.screen-mini video,
.video-tile.screen-mini .tile-placeholder {
  filter: blur(7px) brightness(.62); transform: scale(1.12);
  transition: filter .15s ease;
}
.video-tile.screen-mini:hover video { filter: blur(5px) brightness(.8); }
/* The name lives in the button instead, where it reads as "watch <name>'s screen". */
.video-tile.screen-mini > .label, .video-tile.screen-mini > .tile-mic { display: none; }

/* The button IS the card: the whole thing is one hit target, and focus, hover and screen-reader
   behaviour are the browser's rather than ours to reimplement. */
.mini-watch {
  position: absolute; inset: 0; z-index: 3;
  display: grid; place-content: center; justify-items: center; gap: 5px;
  padding: 6px; border: 0; color: var(--text); cursor: pointer;
  border-radius: 10px;
  /* A scrim, not decoration: dimming the blurred video alone leaves white text on a pale wash when
     what's being shared is a document or a light IDE. This keeps the name legible over any content. */
  background: linear-gradient(rgba(7,8,11,.42), rgba(7,8,11,.58));
}
.mini-watch:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
.mini-who {
  max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  font-size: .7rem; font-weight: 650; letter-spacing: .01em;
  text-shadow: 0 1px 6px rgba(0,0,0,.8);
}
.mini-cta {
  display: inline-flex; align-items: center; gap: 4px;
  padding: 3px 9px; border-radius: 999px;
  background: rgba(12,14,19,.72); border: 1px solid rgba(255,255,255,.18);
  font-size: .68rem; font-weight: 650; white-space: nowrap;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.mini-watch:hover .mini-cta, .mini-watch:focus-visible .mini-cta {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.mini-cta .ic { width: 12px; height: 12px; vertical-align: 0; }
/* Cards this small can't carry the name AND the word: the name stays (knowing who is sharing is the
   point of the card) and the button becomes its icon. The full "Watch X's screen" is still on the
   tooltip and the aria-label. Without this the name was clipped off the top of the card. */
.screens-crowded .mini-watch { gap: 3px; padding: 4px; }
.screens-crowded .mini-who { font-size: .62rem; }
.screens-crowded .mini-cta { padding: 3px 5px; }
.screens-crowded .mini-cta span { display: none; }

/* ---- faces out of the screen's way ----
   Three levels, because "how much do the faces matter right now" genuinely changes: a design review
   wants to see reactions, a log walkthrough wants every pixel of the terminal. Strip is the default;
   overlay floats the faces over a full-size screen; hidden gives the screen everything. */
.call-stage #video-grid.has-screen { position: relative; }
.has-screen.faces-overlay .video-tile.screen,
.has-screen.faces-hidden .video-tile.screen { height: 100%; }
.has-screen.faces-overlay .video-tile:not(.screen),
.has-screen.faces-overlay .strip-more {
  position: absolute; bottom: 8px; z-index: 4;
  height: var(--strip-h); box-shadow: 0 4px 16px rgba(0,0,0,.55);
  opacity: .92;
}
.has-screen.faces-overlay .video-tile:not(.screen):hover { opacity: 1; }
.has-screen.faces-hidden .video-tile:not(.screen),
.has-screen.faces-hidden .strip-more { display: none !important; }

/* Sits at the bottom-right of the video area, beside the row of faces it controls — next to the
   thing it affects rather than in a far corner. */
.faces-btn {
  position: absolute; bottom: 8px; right: 10px; z-index: 7;
  display: none; align-items: center; gap: 5px;
  padding: 5px 9px; border-radius: 999px;
  border: 1px solid rgba(255,255,255,.14); background: rgba(10,11,15,.72);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  color: var(--text); font-size: .72rem; font-weight: 650; cursor: pointer;
}
#video-grid.has-screen .faces-btn { display: inline-flex; }
.faces-btn:hover { background: rgba(20,22,30,.9); border-color: var(--accent); }
.faces-btn .ic { width: 14px; height: 14px; vertical-align: 0; }
/* Overflow of the filmstrip, as a count rather than more unreadable thumbnails. */
.strip-more {
  height: var(--strip-h); aspect-ratio: 16 / 9; flex: 0 1 auto; min-width: 0;
  display: grid; place-items: center; border-radius: 10px;
  background: var(--bg-2); border: 1px solid var(--line);
  color: var(--muted); font-size: .8rem; font-weight: 700;
}
.video-tile.connecting { opacity: .45; outline: 2px dashed var(--muted); animation: pulse-tile 1.2s ease infinite; }
.video-tile.speaking {
  box-shadow: inset 0 0 0 2px var(--green), 0 0 0 3px hsl(158 60% 45% / .28), 0 6px 22px rgba(0,0,0,.4);
  transform: translateY(-1px);
}
.video-tile.speaking .label { background: rgba(62, 207, 142, .9); color: #06281a; }
@keyframes pulse-tile { 50% { opacity: .25; } }
.video-tile .label {
  position: absolute; left: 10px; bottom: 10px; z-index: 2;
  background: rgba(8,9,12,.66); backdrop-filter: blur(6px);
  padding: 3px 9px; border-radius: 999px; font-size: .75rem; font-weight: 600;
  max-width: calc(100% - 52px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Muted badge — shown only when JS marks the tile as mic-off. */
.tile-mic {
  position: absolute; right: 10px; bottom: 10px; z-index: 2; display: none;
  background: rgba(240,85,107,.92); border-radius: 999px;
  width: 24px; height: 24px; place-items: center; font-size: .72rem;
}
/* Show Anahi out, from her own tile. Top-right, away from the label and the mic badge along the
   bottom, and always visible rather than on hover: a phone has no hover and this is the fast way out.
   Sized as a real touch target, which is why the padding is bigger than the glyph. */
.anahi-dismiss {
  position: absolute; top: 6px; right: 6px; z-index: 3;
  display: grid; place-items: center;
  /* Scaled off --tile-w like .tile-avatar, so it is proportionate in a two-person call and does not
     swallow her face in a twelve-person one. The floor is the touch target: below about 24px this
     stops being reliably tappable, which for the one fast way to get her out of a call is the wrong
     thing to trade away for tidiness. */
  width: clamp(24px, calc(var(--tile-w, 160px) * .17), 32px);
  height: clamp(24px, calc(var(--tile-w, 160px) * .17), 32px);
  padding: 0; border: none; border-radius: 50%;
  background: rgba(8,9,12,.62); backdrop-filter: blur(6px);
  color: var(--muted); cursor: pointer; line-height: 0;
}
.anahi-dismiss:hover, .anahi-dismiss:focus-visible { background: var(--red); color: #fff; }
.anahi-dismiss .ic { width: 52%; height: 52%; }
.video-tile.mic-off .tile-mic { display: grid; }
/* Wraps, and nothing on it is allowed to shrink.
 *
 * This row used to be a plain non-wrapping flex line whose children kept the default `flex-shrink: 1`,
 * so a narrow stage — the chat panel open beside a call is enough — did not wrap it: it CRUSHED it.
 * Measured at a 230px stage: the seven 46px buttons came out 46, 46, 46, 21, 46, 0, 21 wide, and
 * because `justify-content: center` overflows in both directions the ends spilled out of the stage
 * entirely, over the sidebar on one side and under the chat panel on the other. Leave-call was the
 * button at 0. A control you cannot hit is worse than a control that moved to a second line. */
.call-controls {
  display: flex; flex-wrap: wrap; justify-content: center; align-items: center;
  gap: 12px; row-gap: 8px; padding: 10px 0 4px; min-width: 0;
}
.call-controls > * { flex: 0 0 auto; }
/* The one thing here that may give up space: it is a label, and the buttons are not. */
.call-chan {
  color: var(--muted); font-size: .8rem;
  flex: 0 1 auto; min-width: 0; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ctl {
  width: 46px; height: 46px; border-radius: 50%; border: none;
  background: var(--bg-3); color: var(--text); font-size: 1.15rem;
}
.ctl.off { background: #4a1f29; }
.ctl.on { background: var(--accent); }
.ctl.danger { background: var(--red); }
.ctl.ptt-live { background: var(--green); animation: ptt-pulse 1.2s ease infinite; }
@keyframes ptt-pulse { 50% { box-shadow: 0 0 0 6px rgba(62, 207, 142, .25); } }
.ptt-hint { color: var(--muted); font-size: .8rem; }
#btn-mute { touch-action: none; user-select: none; } /* press-and-hold gesture */

/* ---------- chat ---------- */
.chat { flex: 1; display: flex; flex-direction: column; min-height: 0; }
/* overflow-anchor: none — prependHistory() (public/app.js) already compensates scrollTop by hand
   for content inserted above the fold. Chromium/Firefox scroll anchoring would adjust scrollTop
   again during the same forced layout that reads scrollHeight/scrollTop, doubling the correction
   and jumping the reader forward by about a page. Safari has no scroll anchoring, so it was the
   only browser where the un-prefixed version behaved as written. */
.messages { flex: 1; overflow-y: auto; overflow-anchor: none; padding: 16px; display: flex; flex-direction: column; gap: 2px; }
/* Solo aparece cuando estás leyendo una ventana antigua, que es el único momento en que hay un
   "final" al que volver. Flotante sobre la lista, no en el flujo: no debe empujar los mensajes. */
.back-latest {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: 74px; z-index: 6;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px 12px; border-radius: 999px;
  border: 1px solid var(--accent); background: rgba(12,14,19,.92); color: var(--text);
  font-size: .74rem; font-weight: 650; cursor: pointer;
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
.back-latest .ic { width: 13px; height: 13px; transform: rotate(90deg); }
.back-latest.hidden { display: none; }
/* Below 560px the composer wraps to two rows (see .chat-form above) — the icon row and Send drop
   under the textarea instead of beside it, making the whole bar about one row taller. The pill's
   fixed offset from the bottom of .chat has to grow by the same amount, or it lands inside the now
   taller composer instead of above it. */
@media (max-width: 560px) {
  .back-latest { bottom: 133px; }
}
.msg { display: flex; gap: 10px; padding: 3px 6px; border-radius: 8px; }
.msg:hover { background: var(--bg-1); }
.msg.first { margin-top: 12px; }
.msg .avatar { width: 38px; height: 38px; }
.msg .spacer { width: 38px; flex-shrink: 0; }
.msg-body { min-width: 0; }
.msg-head { display: flex; align-items: baseline; gap: 8px; }
.msg-head .name { font-weight: 600; font-size: .95rem; }
.msg-head .name.guest { color: #e8b33e; }
.msg-head .time { color: var(--muted); font-size: .72rem; }
/* The date a run of messages belongs to, as a rule with the day sitting in the break. Sticky, so
   scrolling back through a long day keeps its date on screen instead of leaving it behind. */
/* The generous padding-top is doing real work: pinned at top: -16px (flush with the scrollport,
   cancelling the list's own padding) the band of background below the top edge is the box height
   minus that 16px, so a thin box would let the message underneath show through above the label. */
.day-divider {
  position: sticky; top: -16px; z-index: 1;
  display: flex; align-items: center; gap: 10px;
  margin: 6px 6px 2px; padding: 16px 0 6px;
  background: var(--bg-0);
}
.day-divider::before, .day-divider::after { content: ""; flex: 1; height: 1px; background: var(--line); }
/* The message that named you, when a channel opens on it. Same shape as the day divider so the list
   still reads as one thing, and NOT sticky: a day label is a heading that should follow you down the
   day, and this is a mark on one spot you are meant to scroll past and leave behind. Amber rather
   than the accent, to match the bar already drawn down the side of a message that mentions you. */
.unread-divider {
  display: flex; align-items: center; gap: 10px;
  margin: 10px 6px 4px;
}
.unread-divider::before, .unread-divider::after {
  content: ""; flex: 1; height: 1px; background: #e8b33e;
}
.unread-label {
  color: #e8b33e; font-size: .7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.day-label {
  color: var(--muted); font-size: .7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; white-space: nowrap;
}
.msg-text { font-size: .95rem; line-height: 1.45; word-wrap: break-word; white-space: pre-wrap; }
.msg { position: relative; }
.msg.mentioned { background: rgba(232, 179, 62, .08); border-left: 2px solid #e8b33e; }
.msg.flash { background: rgba(91, 140, 255, .18); transition: background 1s ease; }
/* Dos segundos de "es este". Sin esto, saltar a un mensaje te deja en medio de una conversación sin
   decirte cuál de los ochenta era. */
.msg.msg-found { background: hsl(45 90% 60% / .12); border-radius: 8px; }
.mention { color: var(--accent-2); background: rgba(91, 140, 255, .15); border-radius: 4px; padding: 0 3px; font-weight: 600; }
.edited { color: var(--muted); font-size: .7rem; }
.deleted-note { color: var(--muted); font-size: .85rem; }
/* "Seen" under your last message in a DM — see renderSeenMarker() in app.js. A sibling of .msg
   rather than part of it, so it can appear and disappear on its own without the message-building
   code (shared with the scroll-back history seam logic) needing to know it exists. */
.seen-marker { color: var(--muted); font-size: .7rem; margin: 0 12px 4px; text-align: right; }
.reply-quote {
  color: var(--muted); font-size: .78rem; cursor: pointer; margin-bottom: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 60ch;
}
.reply-quote:hover { color: var(--text); }

/* hover action toolbar */
.msg-actions {
  position: absolute; top: -14px; right: 12px; display: none; gap: 2px;
  background: var(--bg-2); border: 1px solid #333645; border-radius: 8px; padding: 2px;
  z-index: 5;
}
.msg:hover .msg-actions { display: flex; }
.msg-actions button {
  background: transparent; border: none; font-size: .9rem; padding: 4px 7px;
  border-radius: 6px; color: var(--text);
}
.msg-actions button:hover { background: var(--bg-3); }
.emoji-pop {
  position: absolute; top: 100%; right: 0; display: flex; gap: 2px;
  background: var(--bg-2); border: 1px solid #333645; border-radius: 10px;
  padding: 6px; z-index: 6; box-shadow: 0 8px 30px rgba(0,0,0,.5);
}
.emoji-pop button { background: transparent; border: none; font-size: 1.15rem; padding: 4px 6px; border-radius: 6px; }
.emoji-pop button:hover { background: var(--bg-3); }

/* reactions */
.reactions { display: flex; flex-wrap: wrap; gap: 4px; }
.reactions:not(:empty) { margin-top: 4px; }
.reaction {
  background: var(--bg-2); border: 1px solid #333645; color: var(--text);
  border-radius: 10px; padding: 2px 8px; font-size: .8rem;
}
.reaction.mine { border-color: var(--accent); background: rgba(91, 140, 255, .15); }
.reaction:hover { border-color: var(--accent-2); }

/* attachments */
.attach-box:not(:empty) { margin-top: 6px; }
.attach-img { max-width: min(380px, 100%); max-height: 300px; border-radius: 10px; cursor: pointer; display: block; }
.attach-video { max-width: min(480px, 100%); border-radius: 10px; display: block; }
.attach-file {
  display: inline-flex; align-items: center; gap: 8px; background: var(--bg-2);
  border: 1px solid #333645; border-radius: 10px; padding: 10px 14px;
  color: var(--accent-2); text-decoration: none; font-size: .9rem;
}
.attach-file:hover { border-color: var(--accent); }
.attach-size { color: var(--muted); font-size: .75rem; }

/* inline edit + reply bar */
.edit-input {
  width: 100%; background: var(--bg-2); border: 1px solid var(--accent); color: var(--text);
  border-radius: 8px; padding: 8px 10px; font-size: .95rem; outline: none;
}
.reply-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  background: var(--bg-2); border-top: 1px solid #262935;
  padding: 6px 16px; font-size: .82rem; color: var(--muted);
}
.reply-bar span { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Queued-but-unconfirmed messages — see pendingSends in app.js. Warm rather than red: nothing has
 * failed yet, it's just waiting for a reconnect, and an error-colored bar every time a deploy blips
 * the socket would teach people to ignore it exactly when it later means something worse. */
.send-pending {
  padding: 4px 16px; font-size: .78rem; color: var(--warn, #d9a441);
}
/* "Sent · Undo" window — see heldSends in app.js. Calm, not the pending queue's amber and not an
 * error color: nothing has gone wrong, the message just isn't irreversible yet. */
.undo-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  background: var(--bg-2); border-top: 1px solid #262935;
  padding: 6px 16px; font-size: .82rem; color: var(--muted);
}
.undo-bar-btn {
  background: transparent; border: none; color: var(--accent-2); font-size: .82rem;
  cursor: pointer; padding: 2px 4px; font: inherit;
}
.undo-bar-btn:hover { color: var(--accent); text-decoration: underline; }
.chat-form .attach { font-size: 1.2rem; flex-shrink: 0; }
.typing { min-height: 20px; padding: 0 18px; color: var(--muted); font-size: .78rem; font-style: italic; }
/* Anahi's half of that line. Her turn is a long silence — the dots are the only thing saying it is a
   wait and not a failure, which is the whole reason this exists. */
.typing-anahi { display: inline-flex; align-items: center; gap: 2px; color: var(--accent); }
.typing-dots { display: inline-flex; align-items: flex-end; gap: 2px; margin-left: 3px; }
.typing-dots i {
  width: 3px; height: 3px; border-radius: 50%; background: currentColor;
  animation: typing-dot 1.2s infinite ease-in-out;
}
.typing-dots i:nth-child(2) { animation-delay: .2s; }
.typing-dots i:nth-child(3) { animation-delay: .4s; }
@keyframes typing-dot {
  0%, 60%, 100% { opacity: .25; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-2px); }
}
/* Reduced motion still needs to say "working" — the dots stay and only stop moving. */
@media (prefers-reduced-motion: reduce) {
  .typing-dots i { animation: none; opacity: .6; }
}
/* align-items: flex-end so the buttons stay on the last line as the box grows upward. */
.chat-form {
  display: flex; align-items: flex-end; gap: 8px;
  padding: 10px 14px calc(12px + env(safe-area-inset-bottom)); background: var(--bg-1);
}
.chat-form input, #chat-input {
  flex: 1; background: var(--bg-2); border: 1px solid #333645; color: var(--text);
  border-radius: var(--radius); padding: 12px 14px; font-size: 1rem; outline: none; min-width: 0;
}
.chat-form input:focus, #chat-input:focus { border-color: var(--accent); }
/* The height is set in JS from the content; everything here just keeps it looking like the input it
   replaced instead of a resizable box with its own scrollbar. */
#chat-input {
  resize: none; overflow-y: hidden;
  font-family: inherit; line-height: 1.45;
  /* min-height is the floor JS can't undercut: one line plus its padding. Without it, a height
     measured while the app was still hidden collapsed the box to nothing. */
  min-height: calc(1.45em + 26px);
  max-height: 160px;
}
/* A textarea wraps its placeholder, unlike an input, so a long hint broke onto a second line inside a
   one-line box and got clipped — which is what looked broken. The placeholder is short now and the
   hint moved to the tooltip. */

/* Narrow: the box, three icons and Send in one row leaves the box too small to be worth typing in. It
   takes its own full-width line and the buttons sit underneath. */
@media (max-width: 560px) {
  .chat-form { flex-wrap: wrap; }
  #chat-input { flex: 1 1 100%; order: -1; }
  .chat-form .icon-btn, .chat-form .btn { align-self: center; margin-bottom: 0; }
  .chat-form .btn.primary { margin-left: auto; }
}
#chat-input::-webkit-scrollbar { width: 8px; }
#chat-input::-webkit-scrollbar-thumb { background: #3a4050; border-radius: 8px; }
/* Buttons keep their own height while the box grows past them. */
.chat-form .icon-btn, .chat-form .btn { flex: 0 0 auto; align-self: flex-end; margin-bottom: 1px; }
.qm-join {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px; width: 100%;
  background: rgba(62,207,142,.14); border: 1px solid rgba(62,207,142,.42); color: #b8f3d6;
}
.qm-join:hover { background: rgba(62,207,142,.22); }
.qm-join .ic { width: 15px; height: 15px; vertical-align: 0; }

/* ---------- members panel ---------- */
.members-panel {
  width: 230px; background: var(--bg-1); border-left: 1px solid #262935;
  padding: 12px; overflow-y: auto; flex-shrink: 0;
}
.members-panel.collapsed { display: none; }
.members-panel header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.members-panel h4 { font-size: .78rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }
.members-panel ul { list-style: none; display: flex; flex-direction: column; gap: 6px; }
.members-panel li {
  display: flex; align-items: center; gap: 8px; font-size: .9rem;
  padding: 4px 6px; border-radius: 8px;
}
.members-panel li:hover { background: var(--bg-2); }
.members-panel li.me-row .m-name { color: var(--accent-2); }
.members-panel .avatar.sm { width: 26px; height: 26px; font-size: .68rem; }
.members-panel .m-name { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.members-panel .m-tags { flex-shrink: 0; font-size: .8rem; }

/* Hereda la geometría del panel de miembros (misma columna, mismo cajón en móvil) y solo añade lo
   que es suyo: las pestañas, el campo y el cuerpo que hace scroll por sí mismo. */
.archive-panel { display: flex; flex-direction: column; gap: 10px; min-height: 0; }
.archive-panel.collapsed { display: none; }
.archive-tabs { display: flex; gap: 4px; flex-wrap: wrap; }
.archive-tab {
  padding: 4px 9px; border-radius: 999px; font-size: .72rem; font-weight: 650;
  border: 1px solid var(--line); background: transparent; color: var(--muted); cursor: pointer;
}
.archive-tab[aria-selected="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
.archive-search input {
  width: 100%; padding: 7px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--bg-2); color: var(--text); font: inherit;
}
/* Los dos botones de alcance, lado a lado y del mismo ancho — no es una lista de pestañas, es un
   interruptor de dos posiciones, así que se ven como tal. */
.archive-scope { display: flex; gap: 6px; margin-bottom: 6px; }
.archive-scope-btn {
  flex: 1; padding: 5px 8px; border-radius: 7px; font-size: .72rem; font-weight: 650;
  border: 1px solid var(--line); background: transparent; color: var(--muted); cursor: pointer;
}
.archive-scope-btn[aria-selected="true"] { background: var(--accent); border-color: var(--accent); color: #fff; }
.archive-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; }
.archive-empty { color: var(--muted); font-size: .8rem; padding: 8px 2px; }
.archive-note { color: var(--muted); font-size: .74rem; }
.archive-row {
  display: flex; flex-direction: column; gap: 2px; text-align: left; width: 100%;
  padding: 7px 8px; border-radius: 9px; border: 1px solid transparent;
  background: var(--bg-2); color: var(--text); cursor: pointer; font: inherit;
}
.archive-row:hover { border-color: var(--accent); }
.archive-row .ar-meta { color: var(--muted); font-size: .7rem; }
.archive-row .ar-text { font-size: .8rem; overflow-wrap: anywhere; }
.archive-row .ar-channel { color: var(--accent-2); font-size: .68rem; }
.archive-row mark { background: hsl(45 90% 60% / .35); color: inherit; border-radius: 3px; }
.archive-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.archive-grid img, .archive-grid .ag-missing {
  width: 100%; aspect-ratio: 1; object-fit: cover; border-radius: 8px;
  background: var(--bg-2); border: 1px solid var(--line); cursor: pointer;
}
.archive-grid .ag-missing {
  display: grid; place-items: center; text-align: center; padding: 4px;
  color: var(--muted); font-size: .64rem; cursor: default;
}

/* ---------- modal / toast ---------- */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex;
  align-items: center; justify-content: center; z-index: 50; padding: 16px;
}
.modal {
  background: var(--bg-1); border: 1px solid #2e3140; border-radius: 14px;
  padding: 22px; width: 100%; max-width: 420px; display: flex; flex-direction: column; gap: 10px;
}
.modal h3 { margin-bottom: 4px; }
/* Checkboxes and radios must be excluded: width:100% stretched the private-channel checkbox
   across the modal and shoved its label into a two-line mess. */
.modal input:not([type="checkbox"]):not([type="radio"]), .modal select {
  background: var(--bg-2); border: 1px solid #333645; color: var(--text);
  border-radius: var(--radius); padding: 11px 13px; font-size: 1rem; outline: none; width: 100%;
}
.modal input[type="checkbox"], .modal input[type="radio"] {
  width: 16px; height: 16px; flex: 0 0 auto; accent-color: var(--accent); margin: 0;
}
.modal label { cursor: pointer; }
.modal .row { display: flex; gap: 8px; justify-content: flex-end; margin-top: 6px; }
.modal label { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: .9rem; }
/* A radio with a line of explanation under it — the channel kind, and who can see it. Written as
   `.modal label.pick-opt` because `.modal label` centres its children, and here the control has to
   sit against the first line of a two-line block rather than in the middle of both. */
.modal label.pick-opt {
  align-items: flex-start; gap: 10px; padding: 9px 11px;
  background: var(--bg-2); border: 1px solid transparent; border-radius: 10px;
}
.modal label.pick-opt input { margin-top: 2px; }
/* Which one is chosen, without waiting for the eye to find a 16px dot. Silently ignored by a
   browser without :has(), which costs the outline and nothing else. */
.modal label.pick-opt:has(input:checked) { border-color: var(--accent); background: var(--bg-3); }
.po-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.po-text strong { color: var(--text); font-weight: 600; }
.po-sub { font-size: .8rem; color: var(--muted); line-height: 1.35; }
/* Both channel dialogs run taller than the viewport on a laptop once the people list is open. */
.chan-modal { max-height: 88dvh; overflow-y: auto; }
.modal .link-out {
  background: var(--bg-2); padding: 10px; border-radius: 8px; font-size: .8rem;
  word-break: break-all; color: var(--accent-2);
}
/* ---------- settings ---------- */
.settings-modal { max-width: 540px; max-height: 88dvh; overflow-y: auto; }
.settings-section { border-top: 1px solid #262935; padding-top: 12px; margin-top: 8px; }
.settings-section h4 {
  text-transform: uppercase; font-size: .72rem; color: var(--muted);
  letter-spacing: .08em; margin-bottom: 10px;
}
.setting-row {
  display: flex; justify-content: space-between; align-items: center;
  gap: 12px; margin-bottom: 10px;
}
.setting-row > label { color: var(--text); font-size: .9rem; flex-shrink: 0; }
.setting-row select, .setting-row input { width: auto; flex: 1; min-width: 0; max-width: 62%; }
.check-row {
  display: flex; align-items: center; gap: 10px; color: var(--text);
  font-size: .9rem; margin-bottom: 8px; cursor: pointer;
}
.check-row input { width: auto; accent-color: var(--accent); }
.mic-test { display: flex; align-items: center; gap: 10px; flex: 1; max-width: 62%; }
.meter { height: 10px; background: var(--bg-2); border-radius: 6px; overflow: hidden; flex: 1; }
.meter-fill { height: 100%; width: 0; background: var(--green); transition: width .06s linear; }
/* Two stacked bars: the microphone, and what actually leaves for the call. */
.meters { display: flex; flex-direction: column; gap: 5px; flex: 1; min-width: 0; }
.meter-line { display: flex; align-items: center; gap: 7px; }
.meter-tag { font-size: .62rem; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); width: 30px; }
.meter-db { font-size: .68rem; color: var(--muted); width: 42px; text-align: right; font-variant-numeric: tabular-nums; }
.meter-fill.clean { background: var(--accent); }
/* Lit while the gate is open. The point is to be able to watch it close when you stop talking. */
.gate-lamp {
  font-size: .6rem; text-transform: uppercase; letter-spacing: .05em; padding: 2px 6px;
  border-radius: 5px; border: 1px solid var(--line); color: var(--muted); background: var(--bg-2);
}
.gate-lamp.on { color: #fff; background: var(--green); border-color: var(--green); }
.cam-preview {
  width: 100%; aspect-ratio: 16/9; background: #000; border-radius: 10px;
  display: none; margin-bottom: 10px;
}
.cam-preview.active { display: block; }
.perm-note {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  background: var(--bg-2); border-radius: 8px; padding: 10px 12px;
  color: var(--muted); font-size: .85rem; margin-bottom: 10px;
}

/* ---------- profile & user management ---------- */
.profile-row { display: flex; align-items: center; gap: 14px; margin-bottom: 12px; flex-wrap: wrap; }
.profile-avatar { width: 64px; height: 64px; font-size: 1.3rem; }
.field-label { color: var(--muted); font-size: .8rem; display: block; margin: 4px 0 2px; }
.bio-input {
  width: 100%; background: var(--bg-2); border: 1px solid #333645; color: var(--text);
  border-radius: var(--radius); padding: 10px 12px; font: inherit; font-size: .92rem;
  outline: none; resize: vertical; margin-bottom: 8px;
}
.bio-input:focus { border-color: var(--accent); }
.chan-row { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 6px; }
.chan-row .field-label { margin: 0; }
.chan-row .check-row { margin: 0; }
.avatar.clickable, .members-panel li.clickable { cursor: pointer; }
.profile-card { display: flex; align-items: center; gap: 16px; }
.pc-head h3 { margin-bottom: 2px; }
.pc-bio { white-space: pre-wrap; font-size: .92rem; line-height: 1.5; color: var(--text); }
.pc-contact { background: var(--bg-2); border-radius: 10px; padding: 10px 12px; }
.pc-phone { font-size: .95rem; font-weight: 600; display: block; margin-bottom: 8px; }
.pc-links { display: flex; gap: 6px; flex-wrap: wrap; }
.chan-link { text-decoration: none; display: inline-flex; align-items: center; }
/* Three buttons live here now (character, upload, remove); without wrapping they pushed
   ~100px past the modal edge on a 320px screen. */
.profile-btns { display: flex; gap: 8px; flex-wrap: wrap; min-width: 0; }
.profile-btns .btn { flex: 0 1 auto; }
.btn.sm { padding: 6px 10px; font-size: .78rem; }
.btn.warn { background: #3a2e10; color: #e8b33e; }
.btn.danger { background: var(--red); color: #fff; }
.um-list { display: flex; flex-direction: column; gap: 6px; max-height: 55dvh; overflow-y: auto; }
.um-row {
  display: flex; align-items: center; gap: 10px; padding: 8px;
  background: var(--bg-2); border-radius: 10px;
}
.um-row.disabled { opacity: .55; }
.um-info { flex: 1; min-width: 0; }
.um-name { font-size: .9rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.um-email { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.um-row select { width: auto; padding: 6px 8px; font-size: .8rem; flex-shrink: 0; }
/* The manager tick. Muted and small on purpose: it sits beside the role dropdown but is not a role,
   and giving it the same weight would invite reading it as one. */
.um-manager {
  display: flex; align-items: center; gap: 5px; flex-shrink: 0;
  font-size: .8rem; color: var(--muted); cursor: pointer; user-select: none;
}
.um-manager input { margin: 0; cursor: pointer; }
.um-manager:has(input:disabled) { opacity: .5; cursor: default; }
.um-manager:has(input:disabled) input { cursor: default; }
.um-actions { display: flex; gap: 4px; flex-shrink: 0; }
@media (max-width: 560px) {
  .um-row { flex-wrap: wrap; }
  .um-actions { width: 100%; justify-content: flex-end; }
}

.toast {
  position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
  background: var(--bg-3); color: var(--text); padding: 10px 18px;
  border-radius: 10px; font-size: .9rem; z-index: 100;
  box-shadow: 0 8px 30px rgba(0,0,0,.5);
}

/* ---------- collapsible side panels ----------
 * Both sides hide at any width. The only way back to the channel list is the hamburger in the
 * channel header, so it reveals itself whenever the left side is hidden — a collapse with no
 * visible way to undo it is a trap. The right panel reopens from the people button, which is
 * always there. */
.app.left-hidden #sidebar { display: none; }
/* Keyed on the id because a second .icon-btn block further down this sheet redeclares display
   and would otherwise win on source order. */
#btn-open-sidebar.reveal-left { display: none; }
.app.left-hidden #btn-open-sidebar.reveal-left { display: inline-flex; }

/* ---------- mobile ---------- */
.mobile-only { display: none; }
@media (max-width: 760px) {
  .mobile-only, #btn-open-sidebar.reveal-left { display: inline-flex; }
  /* On mobile the sidebar is an overlay that is closed by default, so left-hidden means nothing
     here — the drawer's own .open state is what decides visibility. */
  .app.left-hidden #sidebar { display: flex; }
  .sidebar {
    position: fixed; z-index: 40; height: 100dvh; left: 0; top: 0;
    transform: translateX(-100%); transition: transform .2s ease;
    box-shadow: 0 0 40px rgba(0,0,0,.6);
  }
  .sidebar.open { transform: translateX(0); }
  .members-panel {
    display: none;
    position: fixed; right: 0; top: 0; height: 100dvh; z-index: 40;
    box-shadow: 0 0 40px rgba(0,0,0,.6);
  }
  .members-panel.open { display: block; }
  .video-grid { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
  .video-grid.has-screen { --strip-h: 58px; }
  /* Wider share of a phone-width stage, or the name and the button don't fit on the card. */
  .call-stage #video-grid.has-screen { --mini-w: clamp(96px, 32%, 150px); }
  .call-stage #video-grid.has-screen.screens-crowded { --mini-w: clamp(80px, 22%, 120px); }
  .video-grid.has-screen .video-tile.screen.screen-mini {
    top: 8px; right: calc(8px + var(--mini-i, 0) * (var(--mini-w) + 6px));
  }
  .btn.call { font-size: .85rem; padding: 8px 10px; }
}

/* ===================================================================
   Refined dark pass — layered on top of the base rules above so the
   original layout/behaviour is untouched and only the finish changes.
   =================================================================== */

body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }

/* Call button doubles as the leave control once you're in — colour carries the state. */
.btn.call {
  border-radius: 999px; padding: 9px 18px; font-weight: 650;
  box-shadow: 0 2px 10px rgba(62, 207, 142, .22);
  transition: background .15s ease, box-shadow .15s ease, transform .12s ease;
}
.btn.call:hover { transform: translateY(-1px); }
.btn.call.leaving {
  background: var(--red); color: #fff;
  box-shadow: 0 2px 10px rgba(240, 85, 107, .28);
}
.btn.call.pulse { animation: call-pulse 1.9s ease infinite; }
@keyframes call-pulse {
  50% { box-shadow: 0 2px 10px rgba(62,207,142,.22), 0 0 0 7px rgba(62,207,142,.10); }
}

/* Headers and rails: thinner lines, a touch more breathing room. */
.channel-head { border-bottom-color: var(--line); padding: 13px 18px; }
.channel-title .channel-name-btn { letter-spacing: -.01em; font-weight: 650; }
.sidebar { border-right-color: var(--line); }
.sidebar-head { border-bottom-color: var(--line); }
.section-title { font-weight: 700; letter-spacing: .1em; opacity: .85; }

/* Messages: tighter grouping, clearer hover, softer mention highlight. */
.messages { padding: 18px 18px 8px; }
.msg { padding: 4px 8px; border-radius: 10px; transition: background .1s ease; }
.msg:hover { background: rgba(255,255,255,.028); }
.msg.first { margin-top: 14px; }
.msg-head .name { letter-spacing: -.01em; }
.msg-text { line-height: 1.5; }
.msg.mentioned {
  background: linear-gradient(90deg, rgba(232,179,62,.11), rgba(232,179,62,0) 70%);
  border-left: 2px solid #e8b33e;
}

/* Composer: pill field that lifts on focus. */
.composer input, .composer textarea, #msg-input {
  border-radius: 12px !important;
  transition: border-color .15s ease, box-shadow .15s ease;
}
.composer input:focus, .composer textarea:focus, #msg-input:focus {
  border-color: var(--accent) !important;
  box-shadow: 0 0 0 3px rgba(91,140,255,.14);
}

/* Call controls: rounder, with a clearer pressed/active read. */
.ctl { transition: background .14s ease, transform .1s ease, box-shadow .14s ease; }
.ctl:hover { transform: translateY(-1px); }
.ctl:active { transform: translateY(0); }
.ctl.on { box-shadow: 0 0 0 3px rgba(91,140,255,.18); }
.ctl.danger { box-shadow: 0 0 0 3px rgba(240,85,107,.16); }

/* Unread pill: accent-filled and slightly tighter than the online count next to it. */
.ch-unread {
  min-width: 18px; text-align: center; letter-spacing: 0;
  box-shadow: 0 1px 4px rgba(91,140,255,.35);
}

/* Scrollbars that don't fight the dark surfaces. */
* { scrollbar-width: thin; scrollbar-color: #343947 transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: #343947; border-radius: 8px; border: 3px solid transparent; background-clip: content-box; }
::-webkit-scrollbar-thumb:hover { background: #434a5c; background-clip: content-box; }
::-webkit-scrollbar-track { background: transparent; }

/* Respect reduced-motion: keep the state changes, drop the movement. */
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
  .btn.call:hover, .ctl:hover, .video-tile.speaking { transform: none; }
}

/* ---------- inline SVG icon system ---------- */
.ic {
  width: 20px; height: 20px; flex: 0 0 auto;
  display: inline-block; vertical-align: -.25em;
  color: currentColor; pointer-events: none;
}
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; padding: 0;
  transition: background .14s ease, color .14s ease, transform .1s ease;
}
.icon-btn:hover { transform: translateY(-1px); }
.icon-btn:active { transform: none; }
.icon-btn .ic { width: 19px; height: 19px; vertical-align: 0; }

/* Call button: icon + label pill. */
.btn.call { display: inline-flex; align-items: center; gap: 7px; }
.btn.call .ic { width: 18px; height: 18px; vertical-align: 0; }
.call-label { line-height: 1; }

/* Round call controls hold a slightly larger glyph and no text. */
.ctl { display: inline-flex; align-items: center; justify-content: center; padding: 0; }
.ctl .ic { width: 21px; height: 21px; vertical-align: 0; }
/* "Off" states carry the slashed glyph, so the tint only needs to reinforce it. */
.ctl.off { background: #43202a; color: #ff9daf; }
.ctl.danger, .ctl.danger:hover { color: #fff; }

/* Sidebar glyphs sit optically smaller than body text. */
.channel-list li .lock .ic { width: 13px; height: 13px; vertical-align: -.15em; opacity: .75; }
.ch-kind .ic { width: 13px; height: 13px; vertical-align: -.15em; }
.voice-icon .ic { width: 13px; height: 13px; vertical-align: -.15em; }
.drag-handle .ic { width: 14px; height: 14px; vertical-align: -.2em; }
.section-title .icon-btn { width: 24px; height: 24px; }
.section-title .icon-btn .ic { width: 15px; height: 15px; }

/* Message hover actions. */
.msg-actions button { display: inline-flex; align-items: center; justify-content: center; }
.msg-actions .ic { width: 15px; height: 15px; vertical-align: 0; }

/* Tile mute badge holds a glyph now instead of an emoji. */
.tile-mic .ic { width: 13px; height: 13px; vertical-align: 0; color: #fff; }

/* Profile-card phone link. */
.chan-link .ic { width: 15px; height: 15px; vertical-align: -.2em; margin-right: 4px; }

/* ---------- reactions ---------- */
.rx { width: 17px; height: 17px; display: block; flex: 0 0 auto; }
.rx-emoji { font-size: 15px; line-height: 1; }
.rx-count { font-variant-numeric: tabular-nums; font-weight: 650; font-size: .74rem; }
.reaction {
  display: inline-flex; align-items: center; gap: 5px;
  padding: 3px 8px; border-radius: 999px; line-height: 1;
  transition: background .13s ease, border-color .13s ease, transform .1s ease;
}
.reaction:hover { transform: translateY(-1px); }
/* Your own reaction reads as a filled, accented chip. */
.reaction.mine { border-color: var(--accent); background: rgba(91,140,255,.16); }
.reaction.mine .rx-count { color: var(--accent-2); }

.emoji-pop .rx { width: 21px; height: 21px; }
.emoji-pop button {
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 9px; transition: background .12s ease, transform .1s ease;
}
.emoji-pop button:hover { transform: translateY(-1px) scale(1.06); }

/* Grouped messages: timestamp appears in the gutter on hover only. */
.msg .spacer { position: relative; }
.hover-time {
  position: absolute; inset: 0; display: none;
  align-items: center; justify-content: center;
  font-size: .64rem; color: var(--muted); font-variant-numeric: tabular-nums;
  white-space: nowrap; pointer-events: none;
}
.msg:hover .hover-time { display: flex; }

/* ---------- resizable call stage ---------- */
:root { --stage-h: 320px; }
/* The stage is a fixed, user-draggable height; the tile grid scrolls inside it so shrinking
   the area never clips anyone, and the chat below always keeps its room. */
.call-stage {
  position: relative;
  /* Back to a fixed area, but for a different reason than before: the tiles now grow to fill it
     (see layoutCallGrid), so there is no dead space with two people — they simply become large.
     Fitting the stage to small tiles was the wrong half of the fix. */
  height: var(--stage-h);
  display: flex; flex-direction: column;
  padding-bottom: 8px;
  flex: 0 1 auto;
}
.call-stage #video-grid,
.call-stage .rtk-mount { flex: 1; min-height: 0; overflow-y: auto; }
/* Centred both ways: a measured grid leaves at most one short row, and centring it reads as
   deliberate instead of top-left stranded. */
.call-stage #video-grid { align-content: center; justify-content: center; }
.call-stage .rtk-mount { height: auto; }
.call-stage .call-controls { flex: 0 0 auto; padding-top: 8px; }

/* While a screen has the spotlight the stage grows past the stored height — the shared screen
   is the point of the call at that moment. */
/* A shared screen is the point of the call, so it gets room regardless of the usual cap. */
.call-stage.screen-focused { height: max(var(--stage-h), min(68dvh, 720px)); }
/* Getting the faces out of the way should buy real screen, not just the 66px the filmstrip used.
   The limit on a shared screen was never the faces — it was the stage height — so with faces
   overlaid or hidden the stage itself grows downward and the picture grows with it. */
.call-stage.screen-focused.faces-roomy { height: max(var(--stage-h), min(84dvh, 960px)); }

.stage-resizer {
  position: absolute; left: 0; right: 0; bottom: -4px; height: 9px;
  cursor: ns-resize; z-index: 5;
  display: flex; align-items: center; justify-content: center;
}
.stage-resizer::before {
  content: ""; width: 46px; height: 3px; border-radius: 3px;
  background: #3a4050; transition: background .14s ease, width .14s ease;
}
.stage-resizer:hover::before { background: var(--accent); width: 66px; }
.stage-resizer:focus-visible { outline: none; }
.stage-resizer:focus-visible::before { background: var(--accent); width: 66px; }
/* Kill text selection and pointer noise mid-drag. */
body.resizing-stage { user-select: none; cursor: ns-resize; }
body.resizing-stage iframe, body.resizing-stage video { pointer-events: none; }

/* (The spotlight sizing lives with the rest of the spotlight rules above. An override here used to
   set min-height: 55% and flex-grow on the screen, which together could exceed the stage and was
   what forced the grid to scroll.) */

/* ---------- sidebar room occupancy ---------- */
.voice-list li {
  display: flex; align-items: center; gap: 7px;
  padding: 2px 0 2px 2px; font-size: .8rem; color: var(--muted);
}
/* Colour-coded initial: matches the person's call tile, so a glance at the sidebar tells you
   who's in the room without reading the names. */
.voice-av {
  width: 19px; height: 19px; border-radius: 50%; flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center; overflow: hidden;
  position: relative;
  background: hsl(var(--tile-hue, 226) 52% 44%);
  color: #fff; font-size: .56rem; font-weight: 700; letter-spacing: 0;
}
.voice-av .avatar-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.voice-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Hidden, not removed. A speaker icon next to every unmuted person carries no information — the eye
   still has to check each row — so only exceptions show: muted, deafened, or currently talking. It
   keeps its space so a row doesn't shift width the moment someone starts speaking. */
.voice-state { margin-left: auto; display: inline-flex; align-items: center; opacity: 0; }
.voice-state .ic { width: 12px; height: 12px; vertical-align: 0; }
.voice-state.off { color: var(--red); opacity: 1; }
.voice-list li.is-off .voice-name { opacity: .62; }

/* ---------- direct messages ---------- */
.dm-row .ch-row { gap: 9px; align-items: center; }
.dm-av-wrap { position: relative; flex: 0 0 auto; line-height: 0; }
#dm-list .dm-avatar { width: 30px; height: 30px; font-size: .72rem; }
/* In a call: ringed rather than solid, so "around" and "busy talking" differ at a glance. */
.dm-presence.in-call { background: var(--bg-1); box-shadow: 0 0 0 2px var(--bg-1), inset 0 0 0 3px var(--accent); }
.dm-main { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.dm-sub {
  font-size: .68rem; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dm-sub.urgent { color: #ff9daf; font-weight: 650; }
.dm-row.has-unread .ch-name { font-weight: 700; color: var(--text); }
.dm-actions { margin-left: auto; display: flex; align-items: center; }
.dm-join { width: 26px; height: 26px; color: var(--green); }
.dm-join .ic { width: 14px; height: 14px; }
.dm-join:hover { background: rgba(62,207,142,.16); }

/* ---------- online panel ---------- */
.members-panel li { display: flex; align-items: center; gap: 9px; }
.m-body { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.m-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.m-where { font-size: .68rem; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Admin marker: a small accent dot before the name beats a gold star competing with the avatar. */
.members-panel li.is-admin .m-name::before {
  content: ""; display: inline-block; width: 5px; height: 5px; border-radius: 50%;
  background: var(--accent); margin-right: 6px; vertical-align: .1em;
}
.m-state { margin-left: auto; display: inline-flex; align-items: center; }
.m-state .ic { width: 14px; height: 14px; vertical-align: 0; }
.m-state .st-live { color: var(--green); opacity: .9; }
.m-state .st-off { color: var(--red); }
.members-panel .avatar.sm { width: 28px; height: 28px; font-size: .68rem; }
#btn-members { display: inline-flex; align-items: center; gap: 5px; width: auto; padding: 0 8px; }
#btn-members .count { font-size: .75rem; font-variant-numeric: tabular-nums; }

/* Deafen shares the mute button's "off" treatment. */
#btn-deafen.off { background: #43202a; color: #ff9daf; }

/* ---------- character avatars ---------- */
/* When an avatar holds a character the initials must not show through behind it. */
.avatar.has-char { background: transparent; color: transparent; }
.avatar.has-char .char { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.tile-avatar.has-char { box-shadow: none; }
/* ---------- buttons under one of Anahi's messages ----------
   The "Voy en camino / Dame 5 minutos / No puedo" on a summons she could not deliver in person. They
   wrap, because three Spanish labels do not fit a phone on one line, and each one is a full touch
   target rather than a link — the whole point of them is answering with a thumb. */
.msg-buttons { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 2px; }
.msg-buttons:empty { display: none; }
.msg-btn {
  border: 1px solid #3a4059; background: var(--bg-2); color: var(--text);
  border-radius: 999px; padding: 7px 14px; font-size: .86rem; font-weight: 600;
  cursor: pointer; min-height: 34px;
}
.msg-btn:hover:not(:disabled), .msg-btn:focus-visible:not(:disabled) {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
/* Pressed, and waiting for the server to say so. Not hidden: a row that vanishes under your thumb
   reads as a misfire, and the answer replaces them a round trip later. */
.msg-btn:disabled { opacity: .45; cursor: default; }
.msg-chose {
  display: inline-flex; align-items: center; gap: 6px;
  color: var(--muted); font-size: .86rem; font-weight: 600;
}
.msg-chose .ic { width: 14px; height: 14px; color: var(--green); }

.voice-av.has-char { background: transparent; }
/* Her own gradient in the sidebar too. The circle in this list is .voice-av and not .avatar, so the
   rule up at .avatar.anahi never reached it — she would be the only name in the room drawn on a hue
   generated from her user id, which is not the face she has on her tile, in the members panel or in
   a DM. Kept in step with .avatar.anahi. */
.voice-av.anahi { background: linear-gradient(135deg, #7aa2ff, #b07aff); }

/* ---------- character designer ---------- */
.char-designer { display: flex; gap: 18px; align-items: flex-start; flex-wrap: wrap; }
.char-stage {
  flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 10px;
  position: sticky; top: 0;
}
.char-preview {
  width: 168px; height: 168px; border-radius: 50%;
  box-shadow: 0 10px 34px rgba(0,0,0,.45), inset 0 0 0 1px rgba(255,255,255,.07);
}
.char-preview .char { display: block; border-radius: 50%; }
.char-stage-btns { display: flex; gap: 8px; }
.char-controls {
  flex: 1 1 300px; min-width: 260px;
  max-height: min(56dvh, 460px); overflow-y: auto; padding-right: 4px;
  display: flex; flex-direction: column; gap: 14px;
}
.char-group-label {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .09em;
  color: var(--muted); font-weight: 700; margin-bottom: 6px;
}
.char-chips { display: flex; flex-wrap: wrap; gap: 7px; }
/* Every option is a live miniature of the actual character, so choices are made by eye. */
.char-chip {
  width: 46px; height: 46px; padding: 0; border: 2px solid transparent; border-radius: 12px;
  background: var(--bg-2); overflow: hidden; line-height: 0;
  transition: border-color .12s ease, transform .12s ease;
}
.char-chip .char { display: block; width: 100%; height: 100%; }
.char-chip:hover { transform: translateY(-2px); }
.char-chip.on { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(91,140,255,.18); }
.char-swatches { display: flex; flex-wrap: wrap; gap: 7px; }
.char-swatch {
  width: 26px; height: 26px; border-radius: 50%; border: 2px solid transparent; padding: 0;
  transition: transform .12s ease, border-color .12s ease;
}
.char-swatch:hover { transform: scale(1.12); }
.char-swatch.on { border-color: var(--text); box-shadow: 0 0 0 3px rgba(255,255,255,.12); }

.tone-pick { display: inline-flex; gap: 6px; align-items: center; }
.btn.sm { padding: 6px 10px; font-size: .82rem; }

@media (max-width: 560px) {
  .char-designer { gap: 12px; }
  .char-stage { position: static; width: 100%; }
  .char-preview { width: 132px; height: 132px; }
  .char-controls { max-height: none; }
}

/* The designer needs room for the preview and the option grid side by side. */
.modal.modal-wide { max-width: 720px; width: 100%; }

/* ---------- install as a desktop app ---------- */
#btn-install.ready { color: var(--accent); }
#btn-install.ready:hover { background: rgba(91,140,255,.14); }
.install-card {
  display: flex; gap: 14px; align-items: flex-start;
  background: var(--bg-2); border: 1px solid var(--line);
  border-radius: var(--radius-lg); padding: 14px;
}
.install-icon { flex: 0 0 auto; line-height: 0; }
.install-icon img { border-radius: 14px; display: block; }
/* The iOS copy is numbered steps separated by newlines, so they have to survive rendering. */
.install-body { margin: 0; line-height: 1.5; white-space: pre-line; }
.install-link {
  display: flex; align-items: center; gap: 8px; margin-top: 12px;
  background: var(--bg-2); border: 1px solid var(--line); border-radius: 10px; padding: 8px 8px 8px 11px;
}
.install-link code { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .82rem; color: var(--accent); }
.install-link .btn { flex: 0 0 auto; padding: 6px 11px; font-size: .8rem; }
.install-note { font-size: .8rem; margin: 0; }
.install-note a { color: var(--accent); text-decoration: none; }
.install-note a:hover { text-decoration: underline; }
/* The Settings download row, once a build exists: the primary .btn stays as-is, the Intel link
   sits beside it as plain understated text rather than a second button — one clear default, one
   quiet way out for the one Mac in twenty that needs it. */
.mac-download-links { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.mac-download-links a:not(.btn) { color: var(--muted); font-size: .82rem; text-decoration: underline; }
.mac-download-links a:not(.btn):hover { color: var(--text); }

/* Default avatar: the person's initial on a colour derived from their id, so the same person is
   always the same colour everywhere (sidebar, DM list, call tile, member panel). */
.avatar.tinted { background: hsl(var(--tile-hue, 226) 52% 46%); }
.avatar.tinted .avatar-img { border-radius: 50%; }

/* Touch devices need a finger-sized target; the compact 24px version is a mouse affordance. */
@media (pointer: coarse) {
  .section-title .icon-btn { width: 32px; height: 32px; }
  .section-title .icon-btn .ic { width: 17px; height: 17px; }
  .dm-join { width: 32px; height: 32px; }
}

/* ---------- short-viewport layout integrity ---------- */
/* On a phone in landscape (≈375px tall) with a call open, the composer sat 55px below the fold:
   .messages inherited min-height:auto so it refused to shrink past its content and pushed the
   composer out of a .chat box that was itself correctly sized. Pinning the composer and letting
   the message list collapse (it scrolls internally anyway) keeps typing reachable at any height. */
.main { min-height: 0; position: relative; }
.messages { min-height: 0; }
.chat-form { flex: 0 0 auto; }
.typing { flex: 0 0 auto; }
/* Reclaim the typing-indicator reserve when vertical space is genuinely scarce. */
@media (max-height: 460px) {
  .typing { min-height: 0; }
  .messages { padding-top: 10px; }
}

/* ---------- team directory (right panel) ---------- */
.m-group {
  padding: 12px 4px 6px; font-size: .66rem; font-weight: 800;
  text-transform: uppercase; letter-spacing: .1em; color: var(--muted); opacity: .75;
}
.m-group:first-child { padding-top: 2px; }
.m-row { display: flex; align-items: center; gap: 9px; border-radius: 8px; }
.m-av { position: relative; flex: 0 0 auto; line-height: 0; }
/* Presence dot doubles as the state legend: filled = around, ring = in a call, hollow = offline. */
.m-dot {
  position: absolute; bottom: -1px; right: -1px; width: 10px; height: 10px; border-radius: 50%;
  box-shadow: 0 0 0 2px var(--bg-1);
}
.m-dot.online { background: var(--green); }
.m-dot.in-call { background: var(--bg-1); box-shadow: 0 0 0 2px var(--bg-1), inset 0 0 0 3px var(--accent); }
.m-dot.away { background: #e8b33e; }
.m-dot.offline { background: #3a4050; }
/* Offline people stay listed and clickable, just visually recessed. */
.m-row.is-offline .m-name { color: var(--muted); }
.m-row.is-offline .avatar { opacity: .45; }
.m-row.is-away .avatar { opacity: .72; }
.m-row .m-dm { margin-left: 2px; opacity: 0; transition: opacity .12s ease; }
.m-row:hover .m-dm, .m-row:focus-within .m-dm { opacity: 1; }
.m-row .avatar { cursor: pointer; }
@media (pointer: coarse) { .m-row .m-dm { opacity: 1; } }

/* ---------- install banner ---------- */
.install-banner {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  margin: 14px 0 4px; padding: 10px 11px;
  background: linear-gradient(135deg, rgba(91,140,255,.16), rgba(91,140,255,.06));
  border: 1px solid rgba(91,140,255,.34); border-radius: 12px;
  color: var(--text); cursor: pointer;
  transition: border-color .15s ease, transform .12s ease, background .15s ease;
}
.install-banner:hover { transform: translateY(-1px); border-color: var(--accent); }
/* It sits directly under the sidebar header now, not inside .sidebar-section, so it no longer
   inherits that section's 8px side padding and has to carry its own or it runs edge to edge. */
.sidebar > .install-banner { width: auto; margin: 10px 8px 2px; flex: 0 0 auto; }
.ib-icon {
  flex: 0 0 auto; width: 30px; height: 30px; border-radius: 9px;
  display: grid; place-items: center; background: var(--accent); color: #fff;
}
.ib-icon .ic { width: 17px; height: 17px; }
.ib-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ib-text strong { font-size: .8rem; font-weight: 700; }
.ib-sub { font-size: .68rem; color: var(--muted); line-height: 1.3; }
/* A pulse only when a one-click install is genuinely available. */
.install-banner.ready .ib-icon { animation: ib-pulse 2.4s ease infinite; }
@keyframes ib-pulse { 50% { box-shadow: 0 0 0 6px rgba(91,140,255,.16); } }

/* ---------- quick message popover ---------- */
.quick-msg {
  position: fixed; z-index: 400;
  background: var(--bg-1); border: 1px solid #33384a; border-radius: 14px;
  padding: 12px; display: flex; flex-direction: column; gap: 10px;
  box-shadow: 0 18px 50px rgba(0,0,0,.55);
}
.qm-head { display: flex; align-items: center; gap: 10px; }
.qm-av { width: 36px; height: 36px; font-size: .78rem; }
.qm-id { min-width: 0; flex: 1; }
.qm-name { font-weight: 700; font-size: .92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.qm-sub { font-size: .7rem; color: var(--muted); }
.qm-form { display: flex; gap: 7px; align-items: center; }
.qm-clip { width: 32px; height: 32px; flex: 0 0 auto; }
.qm-clip .ic { width: 15px; height: 15px; }
/* Sin el padding de .staged-tray: allí separa la bandeja del compositor, y aquí el popover ya
   reparte su propio gap. Las fichas de dentro son .staged-chip, las mismas de la conversación. */
.qm-tray { display: flex; flex-wrap: wrap; gap: 8px; }
.qm-tray .staged-chip { max-width: 100%; }
.qm-input {
  flex: 1; min-width: 0; background: var(--bg-2); border: 1px solid #333645; color: var(--text);
  border-radius: 10px; padding: 9px 11px; font-size: .9rem; outline: none;
}
.qm-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(91,140,255,.14); }
.qm-send { padding: 9px 13px; font-size: .85rem; }
.qm-status:empty { display: none; }
.qm-status { font-size: .72rem; }
.voice-name.clickable { cursor: pointer; }
.voice-name.clickable:hover { color: var(--text); text-decoration: underline; }
.dm-sub.dim { opacity: .7; }

/* ---------- drag and drop ---------- */
.drop-overlay {
  position: absolute; inset: 0; z-index: 60; display: grid; place-items: center;
  background: rgba(12,13,18,.82); backdrop-filter: blur(3px);
  border: 2px dashed var(--accent); border-radius: 12px; margin: 6px;
  pointer-events: none; /* the window-level handler owns the drop */
}
.chat { position: relative; }
.drop-card { display: flex; flex-direction: column; align-items: center; gap: 6px; color: var(--text); }
.drop-card .ic { width: 30px; height: 30px; color: var(--accent); }
.drop-card strong { font-size: 1rem; }
.drop-card span { font-size: .8rem; color: var(--muted); }

/* ---------- voice notes ---------- */
.voice-bar {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 14px; background: var(--bg-2); border-top: 1px solid var(--line);
  font-size: .85rem;
}
.voice-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--red); animation: vrec 1.1s ease infinite; }
@keyframes vrec { 50% { opacity: .25; } }
#voice-time { font-variant-numeric: tabular-nums; font-weight: 700; }
.voice-bar .btn { margin-left: auto; }
.voice-bar .btn + .btn { margin-left: 0; }
#btn-voice.recording { color: var(--red); background: rgba(240,85,107,.14); }
/* Audio attachments render as a player, so give it room to breathe. */
.attach-box audio { width: 100%; max-width: 320px; margin-top: 4px; display: block; }

/* Locked controls (AFK) must read as deliberately unavailable, not broken. */
.ctl.locked, .ctl:disabled { opacity: .45; cursor: not-allowed; }
.attach-file { display: inline-flex; align-items: center; gap: 6px; }
.attach-file .ic { width: 15px; height: 15px; flex: 0 0 auto; }

/* On touch, nothing interactive may depend on :hover — iOS spends the first tap applying hover
   styles and only activates on the second, which is what made the quick popover need two taps. */
@media (hover: none) {
  .m-row .m-dm { opacity: 1; }
  .m-row:hover, .channel-list li:hover, .msg:hover { background: inherit; }
  .msg:hover .msg-actions { display: none; }
  .voice-name.clickable:hover { text-decoration: none; }
}
/* Kill the 300ms tap delay on the things people tap most. */
.m-row, .m-row *, .voice-name.clickable, .dm-row, .channel-list li, .icon-btn, .btn, .ctl {
  touch-action: manipulation;
}
/* A join already in flight is not clickable. */
.btn.call:disabled { opacity: .7; cursor: default; }
.btn.call:disabled:hover { transform: none; }

/* ---------- compact call grid (Discord-like) ---------- */
/* The old grid used minmax(220px, 1fr), which on a narrow stage produced one enormous tile per
   row and clipped everyone. Small fixed-ish tiles fit a 12-person standup and keep every mute
   badge visible at a glance. */
.video-grid { gap: 8px; }
/* Width comes from layoutCallGrid; the aspect ratio then fixes the height. */
.video-tile { aspect-ratio: 16 / 9; border-radius: 10px; }
.tile-avatar {
  /* Scales with the tile so a two-person call doesn't show a small circle in a big card. The ceiling
     rose with the tile ceiling: at 900px the old 104px cap left the initial looking lost again. */
  width: clamp(38px, calc(var(--tile-w, 160px) * .26), 168px);
  height: clamp(38px, calc(var(--tile-w, 160px) * .26), 168px);
  font-size: clamp(.8rem, calc(var(--tile-w, 160px) * .095), 3.2rem);
  box-shadow: 0 0 0 3px hsl(var(--tile-hue, 226) 45% 22% / .85);
}
.video-tile .label {
  left: 6px; bottom: 6px; font-size: .66rem; padding: 2px 7px;
  max-width: calc(100% - 34px);
}
.tile-mic { right: 6px; bottom: 6px; width: 20px; height: 20px; }
.tile-mic .ic { width: 11px; height: 11px; }
/* Deafened reads differently from merely muted. */
.video-tile.deafened .tile-mic { background: rgba(240,85,107,.95); }
.video-tile.mic-off .label { opacity: .75; }

/* With a screen shared the cameras shrink further into a filmstrip. */
.video-grid.has-screen .video-tile .tile-avatar { width: 38px; height: 38px; font-size: .8rem; }

@media (max-width: 700px) {
  .video-grid { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 6px; }
  .tile-avatar { width: 38px; height: 38px; font-size: .82rem; }
}

/* ---------- staged attachments ---------- */
.staged-tray {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding: 10px 14px 0; background: var(--bg-1);
}
.staged-chip {
  display: flex; align-items: center; gap: 8px;
  background: var(--bg-2); border: 1px solid var(--line); border-radius: 10px;
  padding: 6px 8px; max-width: 260px;
}
.staged-thumb { width: 34px; height: 34px; border-radius: 7px; object-fit: cover; flex: 0 0 auto; }
.staged-ic { width: 34px; height: 34px; border-radius: 7px; display: grid; place-items: center; background: var(--bg-3); }
.staged-ic .ic { width: 16px; height: 16px; }
.staged-meta { display: flex; flex-direction: column; min-width: 0; }
.staged-name { font-size: .8rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.staged-size { font-size: .68rem; color: var(--muted); }
.staged-x { width: 24px; height: 24px; flex: 0 0 auto; }
.staged-x .ic { width: 13px; height: 13px; }

/* ---------- speaking + sharing indicators in the sidebar ---------- */
/* During a screen share the tiles shrink to a filmstrip, so the sidebar is where you actually
   see who's talking. */
.voice-list li.talking .voice-av {
  box-shadow: 0 0 0 2px var(--green), 0 0 0 4px rgba(62,207,142,.22);
}
.voice-list li.talking .voice-name { color: var(--green); font-weight: 650; }
.voice-list li.talking .voice-state { color: var(--green); opacity: 1; }
.voice-share { display: inline-flex; align-items: center; color: var(--accent); margin-left: auto; }
.voice-share .ic { width: 13px; height: 13px; vertical-align: 0; }
/* .voice-state also uses margin-left:auto; two auto margins would split the gap and park the
   monitor in the middle of the row, so the state gives up its auto when a share sits before it. */
.voice-share + .voice-state { margin-left: 4px; }

/* ---------- chat heads (Messenger-style floating bubbles) ---------- */
/* Fixed to the viewport, above everything except modals, so a pending reply follows you around
   the app instead of waiting in a list you may not be looking at. */
@keyframes head-in { from { opacity: 0; transform: translateX(14px) scale(.85); } }
/* The name only appears on hover, so the resting state is just a row of faces. */

/* Give the DM section a little more presence now that it leads the sidebar. */
.dm-title { margin-top: 2px; }
#dm-list { margin-bottom: 6px; }

@media (max-width: 700px) {
  /* Clear of the composer and the mobile keyboard. */
      }


@media (hover: none) {
}
/* The + on the toggle replaces the dashed circle while collapsed. */
/* A hairline only when there is something under the icon to separate. */


/* ---------- image viewer ---------- */
.lightbox {
  position: fixed; inset: 0; z-index: 600; background: rgba(6, 7, 11, .93);
  display: grid; place-items: center; padding: 40px 24px;
  -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px);
}
.lightbox.hidden { display: none; }
#lightbox-img {
  max-width: 100%; max-height: 100%; object-fit: contain;
  border-radius: 8px; box-shadow: 0 24px 80px rgba(0,0,0,.7);
  /* The backdrop closes on click; the image itself must not. */
  cursor: default;
}
.lightbox-close {
  position: absolute; top: 14px; right: 16px; width: 40px; height: 40px; border-radius: 50%;
  border: 1px solid var(--line); background: var(--bg-2); color: var(--text);
  display: grid; place-items: center; cursor: pointer; z-index: 2;
  transition: background .14s ease;
}
.lightbox-close:hover { background: var(--bg-3); }
.lightbox-close .ic { width: 20px; height: 20px; }
.lightbox-download {
  position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 14px; border-radius: 999px; border: 1px solid var(--line);
  background: var(--bg-2); color: var(--muted); font-size: .82rem; font-weight: 600;
  text-decoration: none; transition: color .14s ease, background .14s ease;
}
.lightbox-download:hover { color: var(--text); background: var(--bg-3); }
.lightbox-download .ic { width: 15px; height: 15px; vertical-align: 0; }
.attach-img { cursor: zoom-in; }
@media (max-width: 700px) {
  .lightbox { padding: 56px 10px 64px; }
}

/* ---------- per-person volume ---------- */
.vol-pop {
  position: fixed; z-index: 420;
  background: var(--bg-1); border: 1px solid #33384a; border-radius: 14px;
  padding: 12px; display: flex; flex-direction: column; gap: 9px;
  box-shadow: 0 18px 50px rgba(0,0,0,.55);
}
.vol-head { display: flex; align-items: center; gap: 9px; }
.vol-av { width: 30px; height: 30px; font-size: .72rem; }
.vol-name { font-weight: 700; font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vol-label {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: .72rem; color: var(--muted); text-transform: uppercase; letter-spacing: .06em;
}
.vol-pct { font-size: .85rem; font-weight: 700; color: var(--text); letter-spacing: 0; text-transform: none; }
.vol-pop.is-muted .vol-pct { color: var(--red); }
.vol-pop.is-boosted .vol-pct { color: var(--green); }
.vol-range { width: 100%; accent-color: var(--accent); cursor: pointer; }
.vol-pop.is-muted .vol-range { accent-color: var(--red); }
.vol-pop.is-boosted .vol-range { accent-color: var(--green); }
.vol-foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; }
.vol-hint { font-size: .72rem; }
.vol-reset {
  border: 1px solid var(--line); background: var(--bg-2); color: var(--muted);
  border-radius: 8px; padding: 4px 9px; font-size: .75rem; font-weight: 600; cursor: pointer;
}
.vol-reset:hover { color: var(--text); background: var(--bg-3); }

/* A tile whose level you changed says so, so a silenced person is never a mystery.
 *
 * The wording is not here. CSS can't reach the translation catalog, so the text comes in as
 * data-badge and app.js sets it from t() — see applyPeerVolume(). The [data-badge] in the
 * selector is deliberate: with no attribute there is no badge at all, rather than an empty
 * pill floating on the tile, so this rule degrades to invisible instead of to broken. */
.video-tile.vol-off[data-badge]::after, .video-tile.vol-boost[data-badge]::after {
  content: attr(data-badge); position: absolute; top: 6px; left: 6px; z-index: 3;
  padding: 2px 6px; border-radius: 6px; font-size: .62rem; font-weight: 700;
  background: rgba(10,11,15,.78); color: var(--red); pointer-events: none;
}
/* Boosted differs only in colour now — both labels arrive through data-badge. */
.video-tile.vol-boost[data-badge]::after { color: var(--green); }

/* ---------- emoji picker ---------- */
.emoji-panel {
  position: fixed; z-index: 430; display: flex; flex-direction: column; gap: 8px;
  background: var(--bg-1); border: 1px solid #33384a; border-radius: 14px; padding: 10px;
  box-shadow: 0 18px 50px rgba(0,0,0,.55); overflow: hidden;
}
.ep-search {
  width: 100%; padding: 8px 10px; border-radius: 9px; font-size: .88rem;
  background: var(--bg-2); border: 1px solid var(--line); color: var(--text);
}
.ep-search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(91,140,255,.14); outline: none; }
.ep-tabs { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; }
.ep-tabs::-webkit-scrollbar { display: none; }
.ep-tab {
  flex: 0 0 auto; width: 32px; height: 30px; border: none; background: none; cursor: pointer;
  border-radius: 8px; font-size: 1.05rem; line-height: 1; opacity: .6;
  border-bottom: 2px solid transparent;
}
.ep-tab:hover { opacity: 1; background: var(--bg-2); }
.ep-tab.active { opacity: 1; border-bottom-color: var(--accent); }
.ep-grid {
  display: grid; grid-template-columns: repeat(8, 1fr); gap: 2px;
  /* Basis of 240px, but allowed to shrink so a short window still shows the search and tabs. */
  flex: 1 1 240px; min-height: 60px; overflow-y: auto; align-content: start;
}
.ep-cell {
  border: none; background: none; cursor: pointer; border-radius: 8px;
  font-size: 1.35rem; line-height: 1; padding: 4px 0; aspect-ratio: 1;
}
.ep-cell:hover { background: var(--bg-3); transform: scale(1.12); }
.ep-empty { grid-column: 1 / -1; padding: 16px 4px; font-size: .82rem; text-align: center; }
#btn-emoji.on { color: var(--accent); background: var(--bg-3); }

/* ---------- links and inline media in messages ---------- */
.msg-link { color: var(--accent); text-decoration: none; word-break: break-word; }
.msg-link:hover { text-decoration: underline; }
.msg-embeds { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
.embed-img { max-width: 320px; max-height: 260px; border-radius: 10px; display: block; }

@media (max-width: 700px) {
  .ep-grid { grid-template-columns: repeat(7, 1fr); flex-basis: 200px; }
  .embed-img { max-width: 100%; }
}

/* ---------- device pickers on the call bar ---------- */
.ctl-wrap { position: relative; display: inline-flex; }
.ctl-caret {
  position: absolute; right: -2px; bottom: -2px; width: 20px; height: 20px; border-radius: 50%;
  border: 2px solid var(--bg-0); background: var(--bg-3); color: var(--text);
  display: grid; place-items: center; cursor: pointer; padding: 0; z-index: 2;
  opacity: 0; transform: scale(.7); transition: opacity .12s ease, transform .12s ease, background .12s ease;
}
.ctl-caret .ic { width: 12px; height: 12px; vertical-align: 0; }
.ctl-wrap:hover .ctl-caret, .ctl-caret.on, .ctl-caret:focus-visible { opacity: 1; transform: scale(1); }
.ctl-caret.on { background: var(--accent); }
/* Without hover there is nothing to reveal it, so on touch it stays put. */
@media (hover: none) { .ctl-caret { opacity: 1; transform: scale(1); } }

.dev-pop {
  position: fixed; z-index: 440; display: flex; flex-direction: column; overflow: hidden;
  background: var(--bg-1); border: 1px solid #33384a; border-radius: 14px; padding: 8px;
  box-shadow: 0 18px 50px rgba(0,0,0,.55);
}
.dev-head {
  font-size: .7rem; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em; padding: 4px 8px 8px;
}
.dev-list { display: flex; flex-direction: column; gap: 2px; overflow-y: auto; min-height: 0; }
.dev-row {
  display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  border: none; background: none; color: var(--text); cursor: pointer;
  padding: 8px 8px; border-radius: 9px; font-size: .85rem;
}
.dev-row:hover { background: var(--bg-3); }
.dev-row.active { color: var(--accent); font-weight: 650; }
.dev-tick { width: 16px; flex: 0 0 16px; display: inline-flex; color: var(--accent); }
.dev-tick .ic { width: 15px; height: 15px; vertical-align: 0; }
.dev-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dev-note { padding: 8px; font-size: .8rem; line-height: 1.45; }

/* ---------- mobile install bar ---------- */
.install-bar {
  display: none; align-items: center; gap: 10px; width: 100%; text-align: left;
  flex: 0 0 auto; margin: 0; padding: 9px 11px;
  background: linear-gradient(135deg, rgba(91,140,255,.18), rgba(91,140,255,.07));
  border: none; border-top: 1px solid rgba(91,140,255,.3); color: var(--text); cursor: pointer;
}
.install-bar.hidden { display: none; }
.install-bar-x {
  flex: 0 0 auto; width: 26px; height: 26px; border-radius: 50%;
  display: grid; place-items: center; color: var(--muted);
}
.install-bar-x .ic { width: 15px; height: 15px; }
.install-bar-x:hover { color: var(--text); background: var(--bg-3); }
/* Only on a phone — desktop already has the sidebar banner, and two would be nagging. */
@media (max-width: 760px) {
  .install-bar:not(.hidden) { display: flex; }
}

/* ---------- microphone blocked ---------- */
/* Red, not green: this is not a "join" you can press again and again, it's a wall with a way out. */
#btn-join-call.mic-blocked {
  background: #4a1f29; color: #ffd9df; border: 1px solid rgba(240,85,107,.5);
}
#btn-join-call.mic-blocked:hover { background: #5a2531; }
.mic-help-intro { line-height: 1.55; margin: 0 0 10px; }
.mic-help-steps {
  margin: 0 0 4px; padding-left: 22px; display: flex; flex-direction: column; gap: 7px;
  font-size: .88rem; line-height: 1.5; color: var(--text);
}
.mic-help-steps li::marker { color: var(--accent); font-weight: 700; }

/* ---------- live captions ---------- */
/* In the flow between the video and the controls, not floating over them. As an overlay pinned to
   the bottom of the stage it landed exactly on top of the call buttons, so with captions on you
   could not mute, switch camera or hang up. Taking real space costs a little video height and makes
   overlap structurally impossible, which is the better trade for controls you need in a hurry. */
.caption-box {
  flex: 0 0 auto; margin: 6px 12px 0;
  max-height: 30%; overflow-y: auto;
  display: flex; flex-direction: column; gap: 3px;
  padding: 9px 12px; border-radius: 12px;
  background: rgba(8,9,13,.82);
  border: 1px solid rgba(255,255,255,.07);
  scrollbar-width: none;
}
.caption-box::-webkit-scrollbar { display: none; }
.caption-box.hidden { display: none; }
.cap-line {
  margin: 0; font-size: .92rem; line-height: 1.45;
  color: var(--text); text-shadow: 0 1px 2px rgba(0,0,0,.6);
}
.cap-who { font-weight: 750; }
/* A line still being spoken is dimmer, so you can tell a finished sentence from a guess in
   progress — recognition rewrites the tail of an interim result as it hears more. */
.cap-line.live { opacity: .62; font-style: italic; }
/* A translated line is marked, so nobody mistakes the machine's wording for the speaker's. The
   original is on the element's title for when the phrasing looks off. */
.cap-line.translated .cap-text { border-bottom: 1px dotted rgba(255,255,255,.22); cursor: help; }
.cap-notice {
  display: flex; align-items: center; gap: 8px;
  margin: -2px 0 6px; padding: 6px 8px; border-radius: 9px;
  background: rgba(91,140,255,.14); border: 1px solid rgba(91,140,255,.3);
  font-size: .78rem; color: var(--text);
}
.cap-notice span { flex: 1; min-width: 0; }
.cap-notice-off {
  flex: 0 0 auto; border: 1px solid var(--line); background: var(--bg-2); color: var(--text);
  border-radius: 7px; padding: 3px 8px; font-size: .74rem; font-weight: 600; cursor: pointer;
}
.cap-notice-off:hover { background: var(--bg-3); }
.cap-notice-x {
  flex: 0 0 auto; width: 22px; height: 22px; border: none; background: none; color: var(--muted);
  display: grid; place-items: center; cursor: pointer; border-radius: 50%;
}
.cap-notice-x .ic { width: 14px; height: 14px; }
.cap-notice-x:hover { color: var(--text); background: var(--bg-3); }
.dev-head-2 { border-top: 1px solid var(--line); margin-top: 6px; padding-top: 9px; }
#btn-captions.on { background: var(--accent); }

@media (max-width: 700px) {
  .caption-box { margin: 5px 6px 0; padding: 8px 10px; max-height: 38%; }
  .cap-line { font-size: .85rem; }
}

/* ---------- chat as a side panel during a call ----------
 * Out of a call, the conversation IS the view and the call stage sits above it. In a call that split
 * is backwards: the shared screen — the thing everyone is looking at — gets half the window while
 * the chat nobody is reading gets the other half. So during a call the layout turns sideways: the
 * call takes the whole height and the conversation becomes a panel you open when you want it.
 */
.main.call-side {
  display: grid;
  grid-template-columns: minmax(0, 1fr) var(--chat-w, 440px);
  grid-template-rows: auto auto minmax(0, 1fr);
  grid-template-areas:
    "head head"
    "rec  rec"
    "stage chat";
}
.main.call-side > .channel-head { grid-area: head; }
.main.call-side > #record-banner { grid-area: rec; }
.main.call-side > #call-stage {
  grid-area: stage; min-width: 0; min-height: 0;
  /* The row gives it its height now; the dragged height and the spotlight bump don't apply. */
  height: auto !important;
  /* So the call bar can size itself to the STAGE rather than to the window: beside an open panel the
     stage is a fraction of the window, and a window-width media query never fires for it. */
  container-type: inline-size;
}
/* Smaller buttons before a wrapped second row, and the room's name is the first thing to go — the
   header above already says which room you are in. */
@container (max-width: 470px) {
  .call-stage .call-controls { gap: 9px; }
  .call-stage .ctl { width: 40px; height: 40px; }
  .call-stage .ctl .ic { width: 19px; height: 19px; }
}
@container (max-width: 370px) {
  .call-stage .call-chan { display: none; }
  .call-stage .call-controls { gap: 7px; }
  .call-stage .ctl { width: 36px; height: 36px; }
  .call-stage .ctl .ic { width: 17px; height: 17px; }
  .call-stage .ctl-caret { width: 17px; height: 17px; }
}
.main.call-side > #call-stage .stage-resizer { display: none; }
.main.call-side > .chat {
  grid-area: chat; min-width: 0; border-left: 1px solid var(--line);
  position: relative;
  /* The panel, not the window, decides how its own contents lay out. Without this the composer inside a
     440px panel was styled for a 1600px window: four controls and the box crammed onto one line. */
  container-type: inline-size;
}

/* Drag the panel wider. "Big enough to read" depends on the message, the screen and the person, so this
   is a handle rather than a number someone else picked. */
.chat-resizer {
  position: absolute; left: -3px; top: 0; bottom: 0; width: 7px; z-index: 12;
  cursor: ew-resize; display: none;
}
.main.call-side:not(.chat-closed) > .chat > .chat-resizer { display: block; }
.chat-resizer::before {
  content: ""; position: absolute; left: 2px; top: 0; bottom: 0; width: 3px;
  background: transparent; transition: background .14s ease;
}
.chat-resizer:hover::before, .chat-resizer:focus-visible::before, body.resizing-chat .chat-resizer::before {
  background: var(--accent);
}
body.resizing-chat { cursor: ew-resize; user-select: none; }
body.resizing-chat iframe, body.resizing-chat video { pointer-events: none; }

/* Container queries on the panel: the composer stacks when the PANEL is narrow, regardless of how wide
   the window is. The old window-width media query never fired for a narrow panel on a big screen. */
@container (max-width: 520px) {
  .chat-form { flex-wrap: wrap; }
  #chat-input { flex: 1 1 100%; order: -1; }
  .chat-form .icon-btn, .chat-form .btn { align-self: center; margin-bottom: 0; }
  .chat-form .btn.primary { margin-left: auto; }
}
@container (max-width: 400px) {
  /* Tight enough that avatars cost more than they give; the name still carries who said it. */
  .msg > .avatar { width: 26px; height: 26px; font-size: .7rem; }
  .messages { padding-left: 10px; padding-right: 10px; }
}
/* Closed: the call gets everything. This is the default in a call — seeing the share is the point.
 *
 * Sets the columns, not the variable. Overriding `--chat-w` here used to be enough, but the resizer
 * writes the user's width as an INLINE custom property on this same element, and an inline declaration
 * beats any stylesheet rule no matter how specific. So the column stayed 440px wide and empty whenever
 * the chat was closed, which is most of the time. A real property loses to nothing here: the inline
 * style only carries the variable. */
.main.call-side.chat-closed { grid-template-columns: minmax(0, 1fr) 0px; }
.main.call-side.chat-closed > .chat { display: none; }

#btn-chat-drawer { position: relative; }
#btn-chat-drawer.on { color: var(--accent); background: var(--bg-3); }
.cd-badge {
  position: absolute; top: -2px; right: -3px; min-width: 15px; height: 15px; padding: 0 4px;
  border-radius: 999px; background: var(--red); color: #fff;
  font-size: .58rem; font-weight: 800; line-height: 15px; text-align: center;
  box-shadow: 0 0 0 2px var(--bg-1);
}
.cd-badge.hidden { display: none; }

/* Conversation switcher at the top of the panel. */
.chat-tabs {
  flex: 0 0 auto; display: flex; gap: 6px; align-items: center;
  padding: 8px 10px; border-bottom: 1px solid var(--line);
  /* No sideways scroll: what fits is measured, and the rest is behind the count chip. A scrollbar
     here would mean bubbles hidden with nothing saying so, which is what it looked like before. */
  overflow: hidden;
}
.chat-tabs.hidden { display: none; }
.ct-item {
  position: relative; flex: 0 0 auto; border: none; background: none; padding: 0; cursor: pointer;
  line-height: 0; border-radius: 50%;
}
.ct-item .avatar { width: 30px; height: 30px; font-size: .68rem; }
.ct-room {
  width: 30px; height: 30px; border-radius: 9px; display: grid; place-items: center;
  background: var(--bg-2); border: 1px solid var(--line);
  color: var(--muted); font-size: .9rem; font-weight: 700; line-height: 1;
}
.ct-item.active .avatar, .ct-item.active .ct-room { box-shadow: 0 0 0 2px var(--accent); }
.ct-item.active .ct-room { color: var(--text); }
.ct-count {
  position: absolute; top: -3px; right: -4px; min-width: 15px; height: 15px; padding: 0 3px;
  border-radius: 999px; background: var(--red); color: #fff;
  font-size: .56rem; font-weight: 800; line-height: 15px; text-align: center;
  box-shadow: 0 0 0 2px var(--bg-1);
}

/* ---- cover: the conversation over the call instead of beside it ----
 *
 * The split only works when both halves are worth looking at. Below that it is two useless columns:
 * measured at a 1150px window with the sidebar open, a 440px panel left the stage 230px — too narrow
 * to read a face on and too narrow to hold the call bar (see .call-controls). So the panel takes the
 * whole area and the call waits behind it.
 *
 * The switch used to be `@media (max-width: 900px)`, which asked the wrong question: the window is not
 * what the stage gets. The sidebar, the members panel and the panel's own dragged width all come out
 * of it first. syncChatCover() in app.js measures what is actually left and sets this class, and the
 * button in the panel lets that be overridden either way for as long as the call lasts. */
.main.call-side.chat-cover { grid-template-columns: minmax(0, 1fr) 0px; }
/* `grid-area: stage` is what makes `inset: 0` mean the stage.
 *
 * An absolutely-positioned grid item is laid out inside its own GRID AREA, not the grid container, so
 * while it stayed in the `chat` column `inset: 0` resolved against a column this rule had just set to
 * 0px — position: absolute, and still nothing to see. (The old `@media (max-width: 900px)` overlay this
 * replaces had the same hole; measured here as a 0px-wide panel.) Moving it onto the stage's area also
 * keeps the channel header and the recording banner above it, which a plain `inset: 0` on the container
 * would have covered. */
.main.call-side.chat-cover:not(.chat-closed) > .chat {
  grid-area: stage; position: absolute; inset: 0;
  z-index: 20; background: var(--bg-0); border-left: none;
}
.main.call-side.chat-cover:not(.chat-closed) > #call-stage { visibility: hidden; }
/* Nothing to drag: there is no second column to take the width from. */
.main.call-side.chat-cover > .chat > .chat-resizer { display: none; }

/* …which is what the condensed bar is for: with the stage hidden, this IS the call — the only place to
   mute or hang up without uncovering it first, and the dot is the only moving thing saying the mic is
   still open.
 *
 * Keyed on .chat-cover, not on `@media (max-width: 900px)` where it arrived. Both halves of this file
 * fixed the same grid-area bug at the same time, from opposite directions: that media query was the old
 * home of the cover state, and the class replaced it because the window is not what the stage gets. Left
 * on the media query the bar would have gone dead across the whole 900–1150px band, which is exactly
 * where the class newly decides to cover — a covered call with no visible way back to it. Tied to the
 * same condition as the covering itself, it can only appear when there is genuinely nothing else on
 * screen to look at, which is what it was scoped for. */
.main.call-side.chat-cover:not(.chat-closed) > .chat > .call-mini {
  display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
  margin: 6px 14px 0; width: calc(100% - 28px);
  padding: 0;
  /* Sin superficie propia: la fila es solo la disposición. Antes tenía fondo y borde verdes, y cuando
     "Volver a la llamada" pasó a ser un control con su propio fondo verde quedaba verde sobre verde —
     el botón desaparecía dentro de su contenedor, que es justo lo que se reportó. Cada control se
     defiende solo: la pastilla, el micrófono y el colgar. */
}

/* Close and cover/split, at the right end of the conversation row. The header's chat button toggles
   the same panel, but while the panel covers the call that header is behind it — so the way out has
   to live inside the thing it closes. */
.ct-ctl { flex: 0 0 auto; display: flex; align-items: center; gap: 2px; margin-left: auto; }
.ct-ctl .icon-btn { width: 28px; height: 28px; }
.ct-ctl .icon-btn .ic { width: 16px; height: 16px; }

/* The conversation row replaced the left rail, so it carries the "start a new one" affordance the
   rail used to have. */
.ct-new .ct-room { color: var(--accent); border-color: rgba(91,140,255,.4); }
.ct-new:hover .ct-room { background: var(--bg-3); }
.ct-new .ic { width: 17px; height: 17px; }

/* Overflow chip: the conversations that didn't fit, and a badge if any of them are waiting. */
.ct-more .ct-room { font-size: .72rem; letter-spacing: -.02em; }
.ct-more:hover .ct-room { background: var(--bg-3); color: var(--text); }
.ct-count.static { position: static; box-shadow: none; margin-left: auto; }
#mc-search {
  width: 100%; padding: 9px 11px; border-radius: 10px; font-size: .9rem; margin-bottom: 8px;
  background: var(--bg-2); border: 1px solid var(--line); color: var(--text);
}
#mc-search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(91,140,255,.14); outline: none; }
#mc-list { max-height: 46vh; overflow-y: auto; }

/* The conversation's own name, inside the panel — the header is busy naming the room you're in. */
/* The conversation's name and its own call button, side by side. No padding of its own: when both
   children are hidden — which is every moment you are not in a call reading something else — the row
   has to collapse to nothing rather than leave a gap above the messages. */
.chat-title-row {
  flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.chat-title {
  flex: 0 0 auto; padding: 8px 14px 4px;
  font-size: .82rem; font-weight: 700; color: var(--muted);
}
.chat-title.hidden { display: none; }
/* Smaller than the header's: this sits over a conversation, not across a room's title bar, and at full
   size it competes with the messages for attention. */
#chat-call-btn { margin: 6px 14px 2px; padding: 5px 10px; font-size: .8rem; }
#chat-call-btn.hidden { display: none; }

/* "Ale está en #Administration · Entrar" */
.dm-call-bar {
  flex: 0 0 auto; display: flex; align-items: center; gap: 8px; width: 100%; text-align: left;
  margin: 6px 14px 0; width: calc(100% - 28px);
  padding: 8px 11px; border-radius: 10px; cursor: pointer;
  background: rgba(62,207,142,.12); border: 1px solid rgba(62,207,142,.38);
  color: var(--text); font-size: .82rem;
}
.dm-call-bar.hidden { display: none; }
.dm-call-bar:hover { background: rgba(62,207,142,.2); }
.dm-call-bar .ic { width: 15px; height: 15px; vertical-align: 0; color: var(--green); flex: 0 0 auto; }
.dm-call-bar span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dm-call-bar .dcb-go {
  margin-left: auto; flex: 0 0 auto; font-weight: 700; color: var(--green);
}

/* ---------- the call, condensed ---------- */
/* Off at every width except the one that needs it. On a wide screen the call sits beside this panel
   with its own name and its own controls; repeating them here would be saying the same thing twice.
   The rule that shows it is in the max-width block further down, next to the one that makes the
   panel cover the stage in the first place — the two are the same decision. */
.call-mini { display: none; }
/* Se lee como un control, no como una etiqueta de estado.
 *
 * Era `padding: 0; background: none; border: 0` — desnudo del todo — junto a dos botones redondos que sí
 * parecen botones. Resultado: nadie veía que "Volver a la llamada" se pudiera pulsar; parecía el rótulo
 * que dice que sigues en llamada. El `cursor: pointer` global no basta, porque en un teléfono no hay
 * puntero que lo revele: ahí el único aviso posible es que se vea como algo que se toca.
 *
 * Superficie propia y flecha al final, que es el patrón de "ir allí" de una fila de lista. El punto
 * verde se queda a la izquierda porque no es decoración: es el pixel que dice que el micrófono sigue
 * abierto. */
.cm-back {
  display: flex; align-items: center; gap: 8px; min-width: 0; flex: 1;
  padding: 6px 8px 6px 10px; border-radius: 9px;
  background: rgba(62,207,142,.16); border: 1px solid rgba(62,207,142,.5);
  color: var(--text); font: inherit; font-size: .82rem; text-align: left;
  transition: background .15s ease, border-color .15s ease;
}
.cm-back:hover { background: rgba(62,207,142,.24); border-color: rgba(62,207,142,.6); }
.cm-back:active { transform: translateY(1px); }
.cm-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* La flecha va al final y encoge antes que el texto: en un teléfono la barra es estrecha y lo que no
   puede perderse es la palabra. */
/* El caret del sprite apunta abajo; -90° lo deja apuntando a la derecha, hacia donde te lleva. */
.cm-go { width: 13px; height: 13px; flex: 0 0 auto; margin-left: auto; opacity: .75; transform: rotate(-90deg); }
.cm-back:hover .cm-go { opacity: 1; }
.cm-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
/* Alive, not decorative: this is the one pixel on screen saying the microphone is still open. The
   global prefers-reduced-motion rule above stops the pulse and leaves the dot. */
.cm-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto;
  background: var(--green); animation: cm-pulse 2s ease-in-out infinite;
}
@keyframes cm-pulse { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
/* Deliberately not .ctl: those are the round controls the call stage measures and wires device
   carets onto, and two more of them in a different part of the page would join both. */
.cm-btn {
  display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto;
  width: 30px; height: 30px; padding: 0; border: 0; border-radius: 50%;
  background: var(--bg-3); color: var(--text);
}
.cm-btn .ic { width: 15px; height: 15px; vertical-align: 0; }
.cm-btn.off { background: #43202a; color: #ff9daf; }
.cm-btn.danger { background: var(--red); color: #fff; }

/* PTT paused because the window lost focus: amber, because it's a state you need to know about, not
   an error and not normal operation. */
.ptt-hint.ptt-blocked { color: #e8b33e; font-weight: 650; }
.setting-note { font-size: .76rem; line-height: 1.5; margin: -4px 0 6px 2px; }

/* ---------- update available ---------- */
/* Top centre, above everything, but not a modal: it must never block the app. Running old code is a
   thing to know, not an emergency to interrupt for. */
.update-bar {
  position: fixed; top: 12px; left: 50%; transform: translateX(-50%); z-index: 700;
  display: flex; align-items: center; gap: 10px;
  padding: 9px 10px 9px 14px; border-radius: 999px;
  background: rgba(16,18,26,.96); border: 1px solid rgba(91,140,255,.45);
  box-shadow: 0 10px 34px rgba(0,0,0,.6);
  font-size: .86rem; color: var(--text);
  max-width: calc(100vw - 24px);
}
.update-bar.hidden { display: none; }
.update-bar > .ic { width: 17px; height: 17px; color: var(--accent); flex: 0 0 auto; }
.update-bar span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.update-bar .btn.sm { padding: 5px 11px; font-size: .8rem; flex: 0 0 auto; }
.update-bar .icon-btn { width: 26px; height: 26px; flex: 0 0 auto; }
.update-bar .icon-btn .ic { width: 14px; height: 14px; }

@media (max-width: 560px) {
  .update-bar { left: 8px; right: 8px; transform: none; max-width: none; }
}

/* ---------- a headset that just connected ----------
 * Same family as the update bar above and for the same reason: it has something to offer, refusing it
 * must cost nothing, and it must never block the app. Green rather than blue — it is a thing that
 * arrived, not a thing that went wrong or needs installing. */
.device-bar {
  position: fixed; top: 12px; left: 50%; transform: translateX(-50%); z-index: 690;
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px 8px 14px; border-radius: 999px;
  background: rgba(16,18,26,.96); border: 1px solid rgba(62,207,142,.45);
  box-shadow: 0 10px 34px rgba(0,0,0,.6);
  font-size: .86rem; color: var(--text);
  max-width: calc(100vw - 24px);
}
.device-bar.hidden { display: none; }
.device-bar > .ic { width: 17px; height: 17px; color: var(--green); flex: 0 0 auto; }
/* Two lines that must both truncate rather than push the button off the edge of a phone. */
.db-text { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
.db-text > * { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.db-what { color: var(--muted); font-size: .78rem; }
.device-bar .btn.sm { padding: 5px 11px; font-size: .8rem; flex: 0 0 auto; }
.device-bar .icon-btn { width: 26px; height: 26px; flex: 0 0 auto; }
.device-bar .icon-btn .ic { width: 14px; height: 14px; }
/* Both bars at once is rare but real — a deploy lands while you plug in a headset. The sibling order in
   index.html is what lets this happen in CSS instead of in a layout callback. */
.update-bar:not(.hidden) ~ .device-bar { top: 62px; }

@media (max-width: 560px) {
  .device-bar { left: 8px; right: 8px; transform: none; max-width: none; }
}

/* ---------- incoming call (DM only) ----------
 * Same "top centre, above everything" family as .update-bar/.device-bar — fixed, so it can never be
 * the thing that covers the composer or the call controls, both of which live at the BOTTOM of the
 * layout no matter how narrow the screen gets. Unlike those two it does not use the shared
 * "never block the app" framing: this one is meant to be answered, not glanced past, so it stays up
 * (persistent, see checkIncomingCalls()) instead of dismissing itself on a timer. Highest z-index of
 * the three top bars — a ringing call outranks "there's an update" and "a headset connected". */
.call-ring-bar {
  position: fixed; top: 12px; left: 50%; transform: translateX(-50%); z-index: 710;
  display: flex; align-items: center; gap: 10px;
  padding: 8px 10px 8px 8px; border-radius: 999px;
  background: rgba(16,18,26,.96); border: 1px solid rgba(62,207,142,.5);
  box-shadow: 0 10px 34px rgba(0,0,0,.6);
  font-size: .86rem; color: var(--text);
  max-width: calc(100vw - 24px);
}
.call-ring-bar.hidden { display: none; }
.call-ring-bar .avatar { width: 26px; height: 26px; font-size: .68rem; flex: 0 0 auto; }
.call-ring-bar .crb-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
.call-ring-bar .btn.sm { padding: 5px 11px; font-size: .8rem; flex: 0 0 auto; }
@media (max-width: 560px) {
  .call-ring-bar { left: 8px; right: 8px; transform: none; max-width: none; }
}

/* ---- Anahi: the / command menu and the ? help button ---- */
/* Anchored to the form so it sits just above the composer, full width of the text box. */
.chat-form { position: relative; }
.cmd-menu {
  position: absolute; bottom: calc(100% + 6px); left: 14px; right: 14px; z-index: 440;
  background: var(--bg-1); border: 1px solid #33384a; border-radius: 14px; padding: 8px;
  box-shadow: 0 18px 50px rgba(0,0,0,.55); max-height: 46vh; overflow-y: auto;
}
.cmd-head {
  font-size: .7rem; font-weight: 700; color: var(--muted);
  text-transform: uppercase; letter-spacing: .07em; padding: 4px 8px 8px;
}
.cmd-row {
  display: flex; align-items: baseline; gap: 10px; width: 100%; text-align: left;
  border: none; background: none; color: var(--text); cursor: pointer;
  padding: 8px 10px; border-radius: 9px; font-size: .92rem;
}
.cmd-row:hover, .cmd-row.sel { background: var(--bg-2); }
.cmd-row .cmd-name { font-weight: 700; white-space: nowrap; }
.cmd-row .cmd-desc { color: var(--muted); font-size: .82rem; min-width: 0; }
/* The ? is text, not a sprite icon: the sprite has no question mark and one glyph doesn't earn one. */
.anahi-help .qmark { font-weight: 800; font-size: 1.05rem; line-height: 1; }
.anahi-help-body p { margin: 0 0 10px; }
.anahi-help-body .cmd-row { cursor: default; }
