/* The apps' palette, transcribed component for component.
 *
 * Not "close enough": somebody moving between the panel and the handset should
 * feel one product, and a background a few points off is the thing that gives
 * that away first. Values come from ios/BesBes/App/BesBesApp.swift and
 * android/.../ui/Theme.kt. */
:root {
  --bg: #070a14;
  --surface: #141927;
  --accent: #13a3fc;
  --live: #33cc73;
  --danger: #f0544f;
  --own: #ffc24b;
  --text: #ffffff;
  --text-dim: rgba(255, 255, 255, 0.6);
  --hairline: rgba(255, 255, 255, 0.08);
}

* { box-sizing: border-box; }

/* The `hidden` attribute is implemented by the browser as `display: none`, and
 * any author rule that sets `display` beats it. Several things here are grids
 * and flex rows, so `hidden` quietly stopped working on exactly the elements
 * that most needed it — the expired overlay sat on top of a perfectly good QR
 * code, and the sign-in card would have stayed over the console after signing
 * in. One line, because the alternative is remembering this at every call
 * site. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.45 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ── Sign-in ─────────────────────────────────────────────────────────── */

.auth {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 24px;
  background: var(--bg);
  z-index: 20;
}

.card {
  width: 100%;
  max-width: 380px;
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 22px;
  padding: 32px 26px 26px;
}

.brand { text-align: center; margin-bottom: 30px; }

.logo-img {
  width: 88px;
  height: 88px;
  object-fit: contain;
}

.tagline { margin: 6px 0 0; color: var(--text-dim); font-size: 14px; }

.field-label {
  display: block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--text-dim);
  margin-bottom: 8px;
}

.field {
  display: flex;
  align-items: center;
  gap: 10px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--hairline);
  border-radius: 14px;
  padding: 14px 16px;
}
.field:focus-within { border-color: var(--accent); }

.prefix { color: var(--text-dim); font-variant-numeric: tabular-nums; }

.field input {
  flex: 1;
  background: none;
  border: 0;
  outline: none;
  color: var(--text);
  font-size: 17px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 1px;
}
.field input::placeholder { color: rgba(255, 255, 255, 0.28); }

.code-input { text-align: center; font-size: 24px; letter-spacing: 8px; }

.primary {
  width: 100%;
  margin-top: 18px;
  padding: 14px;
  border: 0;
  border-radius: 14px;
  background: var(--accent);
  color: #000;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
}
.primary:disabled { opacity: 0.35; cursor: default; }

.link {
  width: 100%;
  margin-top: 10px;
  padding: 10px;
  background: none;
  border: 0;
  color: var(--text-dim);
  font-size: 13px;
  cursor: pointer;
}

/* ── Console ─────────────────────────────────────────────────────────── */

.console { position: fixed; inset: 0; }

.map { position: absolute; inset: 0; background: var(--bg); }

/* The channel name is white text on whatever the map happens to show. A
 * motorway or a snow-covered field is enough to make it unreadable, so the
 * chrome sits on a gradient rather than directly on the map. */
.scrim {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 130px;
  background: linear-gradient(to bottom, rgba(7, 10, 20, 0.92), rgba(7, 10, 20, 0));
  pointer-events: none;
}

.header {
  position: absolute;
  top: 0; left: 0; right: 0;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 16px;
  padding: 18px 20px;
}

.channel-name {
  background: none;
  border: 0;
  padding: 0;
  color: var(--text);
  font-size: 22px;
  font-weight: 700;
  cursor: pointer;
}
.chev { color: var(--text-dim); font-size: 16px; }

.status {
  display: flex;
  align-items: center;
  gap: 7px;
  margin-top: 3px;
  color: var(--text-dim);
  font-size: 13px;
}

.dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--text-dim);
}
.dot.live { background: var(--live); }
.dot.bad { background: var(--danger); }

.actions { display: flex; gap: 10px; }

.pill {
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 999px;
  color: var(--text);
  padding: 9px 14px;
  font-size: 14px;
  cursor: pointer;
}
.pill:hover { border-color: rgba(255, 255, 255, 0.2); }

.icon {
  background: none;
  border: 0;
  color: var(--text-dim);
  font-size: 16px;
  cursor: pointer;
}

/* ── Roster ──────────────────────────────────────────────────────────── */

.roster {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: 320px;
  max-width: 86vw;
  background: rgba(20, 25, 39, 0.96);
  backdrop-filter: blur(18px);
  border-left: 1px solid var(--hairline);
  padding: 18px;
  overflow-y: auto;
}

.roster-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.roster-head h2 { margin: 0; font-size: 15px; letter-spacing: 0.5px; }

.roster ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

.roster li {
  display: flex;
  align-items: center;
  gap: 12px;
  background: rgba(255, 255, 255, 0.04);
  border-radius: 14px;
  padding: 11px 12px;
}

.avatar {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(19, 163, 252, 0.16);
  color: var(--accent);
  font-weight: 700;
  font-size: 13px;
  flex: none;
}

.who { flex: 1; min-width: 0; }
.who b { display: block; font-weight: 500; font-size: 15px; }
.who small { color: var(--text-dim); font-size: 12px; }

.badge {
  font-size: 10px;
  font-weight: 700;
  color: var(--accent);
  background: rgba(19, 163, 252, 0.16);
  border-radius: 999px;
  padding: 3px 8px;
}
.badge.stale { color: var(--text-dim); background: rgba(255, 255, 255, 0.08); }

/* ── Channel menu ────────────────────────────────────────────────────── */

.menu {
  position: absolute;
  top: 66px;
  left: 20px;
  background: rgba(20, 25, 39, 0.98);
  border: 1px solid var(--hairline);
  border-radius: 16px;
  padding: 6px;
  min-width: 220px;
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.5);
}
.menu button {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  color: var(--text);
  padding: 11px 12px;
  border-radius: 11px;
  font-size: 14px;
  cursor: pointer;
}
.menu button:hover { background: rgba(255, 255, 255, 0.07); }
.menu button.active { color: var(--accent); }

/* ── Markers ─────────────────────────────────────────────────────────── */

/* Drawn as DOM annotations rather than MapKit's own pins, so the shapes match
 * the handsets exactly — same circle, same initials, same ring. */

/* The pulse and the label are both absolutely positioned, so the wrapper has
 * to be the thing they measure from. Without this they anchor to whatever
 * MapKit happens to have positioned above them, and the ring drifts off the
 * pin it is supposed to be around. */
.marker-wrap {
  position: relative;
  width: 38px;
  height: 38px;
}

.marker {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid rgba(255, 255, 255, 0.9);
  display: grid;
  place-items: center;
  color: #000;
  font-weight: 700;
  font-size: 12px;
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.45);
}
.marker.speaking { background: var(--live); }

/* The pulse. Same timing as both handsets: 1.15s, ease-out, fully transparent
 * at its widest.
 *
 * Deliberately slower than a heartbeat, so it reads as "this one is live" at a
 * glance without competing with the map underneath — on a console showing a
 * busy channel, several people talking in turn must not make the screen
 * flicker. It exists only while its subject is speaking, so nothing has to
 * remember to stop it: removing the element removes the animation, and a ring
 * left pulsing points at the wrong person. */
.pulse {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 38px;
  height: 38px;
  margin: -19px 0 0 -19px;
  border-radius: 50%;
  border: 3px solid var(--live);
  animation: pulse 1.15s ease-out infinite;
  pointer-events: none;
}

@keyframes pulse {
  from { transform: scale(1); opacity: 0.85; }
  to   { transform: scale(2.6); opacity: 0; }
}

/* Somebody watching a control room screen all day should not be given a
 * throbbing dot they cannot turn off. */
@media (prefers-reduced-motion: reduce) {
  .pulse { animation: none; opacity: 0.5; }
}
.marker.stale { opacity: 0.6; border-color: rgba(255, 255, 255, 0.35); }

.marker-label {
  position: absolute;
  top: 42px;
  left: 50%;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 11px;
  font-weight: 600;
  padding: 2px 7px;
  border-radius: 999px;
  background: rgba(7, 10, 20, 0.78);
  backdrop-filter: blur(8px);
}

/* ── Toast ───────────────────────────────────────────────────────────── */

.toast {
  position: fixed;
  left: 50%;
  bottom: 28px;
  transform: translateX(-50%);
  background: var(--surface);
  border: 1px solid var(--hairline);
  border-radius: 14px;
  padding: 12px 18px;
  font-size: 14px;
  z-index: 30;
  box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5);
}
.toast.bad { border-color: var(--danger); color: var(--danger); }

/* ── QR sign-in ──────────────────────────────────────────────────────── */

.qr-frame {
  position: relative;
  background: #fff;          /* scanners want quiet-zone white, not our navy */
  border-radius: 18px;
  padding: 16px;
  display: grid;
  place-items: center;
  min-height: 232px;
}

#qr img, #qr canvas { display: block; width: 200px; height: 200px; }

.qr-expired {
  position: absolute;
  inset: 0;
  background: rgba(20, 25, 39, 0.96);
  border-radius: 18px;
  display: grid;
  place-content: center;
  gap: 6px;
  text-align: center;
  padding: 20px;
}
.qr-expired p { margin: 0; color: var(--text-dim); font-size: 14px; }
.qr-expired .primary { margin-top: 8px; }

.steps {
  margin: 20px 0 0;
  padding-left: 20px;
  color: var(--text-dim);
  font-size: 13.5px;
  line-height: 1.9;
}
.steps b { color: var(--text); font-weight: 600; }

.verify {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.verify span { color: var(--text-dim); font-size: 12.5px; }
.verify b {
  font-size: 20px;
  letter-spacing: 4px;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* ── Countdown ───────────────────────────────────────────────────────── */

.countdown { margin-top: 14px; }

.bar {
  height: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.bar i {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--accent);
  transform-origin: left;
}

#countdown-text {
  display: block;
  margin-top: 7px;
  color: var(--text-dim);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
}

/* ── Activity ────────────────────────────────────────────────────────── */

.activity {
  position: absolute;
  left: 20px;
  bottom: 20px;
  width: 280px;
  max-width: 40vw;
  background: rgba(20, 25, 39, 0.9);
  backdrop-filter: blur(18px);
  border: 1px solid var(--hairline);
  border-radius: 18px;
  padding: 14px 16px;
}
.activity h2 {
  margin: 0 0 10px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--text-dim);
}
.activity ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: 7px;
  max-height: 190px;
  overflow-y: auto;
}
.activity li {
  display: flex;
  align-items: baseline;
  gap: 9px;
  font-size: 13px;
}
.activity li.empty { color: var(--text-dim); font-size: 12.5px; }
.activity time {
  color: var(--text-dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  flex: none;
}
.activity .live-dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--live);
  flex: none;
  align-self: center;
}

/* ── Invite ──────────────────────────────────────────────────────────── */

.invite {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--hairline);
}
.invite h3 {
  margin: 0 0 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--text-dim);
}
.invite code {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
  word-break: break-all;
  margin-bottom: 10px;
}

.pill.off { opacity: 0.4; }


/* ═══ Console — map first ═════════════════════════════════════════════════
 *
 * The map is the page and everything else floats on it. On a dispatch screen
 * the map *is* the work; chrome that steals a column from it is chrome that
 * makes the job harder. Every surface below is a rounded, blurred card with
 * the same radius and the same border, so the whole thing reads as one object
 * laid over one picture.
 *
 * Sizes are driven by three custom properties so the responsive rules have one
 * place to change rather than a dozen.
 */

:root {
  --gap: 14px;
  --nav-h: 56px;
  --roster-w: 300px;
  --radius: 16px;
  --glass: rgba(13, 18, 31, 0.82);
}

.console { position: fixed; inset: 0; overflow: hidden; }

.map { position: absolute; inset: 0; background: var(--bg); }

/* Shared card look. One definition, so nothing drifts. */
.nav-left,
.nav-right,
.roster,
.activity,
.dropdown,
.search-field,
.search-results,
.sheet {
  background: var(--glass);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
}

/* ── Navbar ──────────────────────────────────────────────────────────── */

/* Two islands, not one bar.
 *
 * A single bar spanning the window lays a solid band across the top of the map
 * and pins two unrelated groups to the same rectangle — the channel you are
 * watching on the left, the tools you act with on the right. Splitting them
 * gives the map its top corners back and lets each side be exactly as wide as
 * it needs to be. */
.navbar {
  position: absolute;
  top: var(--gap);
  left: var(--gap);
  right: var(--gap);
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  z-index: 5;
  /* The container is only a layout guide; clicks belong to the two cards. */
  pointer-events: none;
  background: none;
  border: 0;
  box-shadow: none;
}

.nav-left, .nav-right {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  height: var(--nav-h);
  padding: 0 10px;
  pointer-events: auto;
}
.nav-left { padding-left: 12px; }

.nav-logo { width: 30px; height: 30px; border-radius: 8px; flex: none; }

.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: 36px;
  padding: 0 12px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-size: 13.5px;
  cursor: pointer;
  white-space: nowrap;
}
.chip:hover { background: rgba(255, 255, 255, 0.09); }
.chip.icon { width: 36px; padding: 0; justify-content: center; font-size: 15px; }
.chip.off { opacity: 0.4; }

.chip.channel { max-width: 240px; }
.chip.channel span { overflow: hidden; text-overflow: ellipsis; }
.chip .swatch { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none; }
.chip .caret { color: var(--text-dim); font-weight: 400; }

.status { display: flex; align-items: center; gap: 7px; color: var(--text-dim); font-size: 13px; }
/* Stood down in favour of another window: the text is the button. */
.status.takeover { cursor: pointer; text-decoration: underline dotted; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-dim); flex: none; }
.dot.live { background: var(--live); }
.dot.bad { background: var(--danger); }

.account { display: flex; align-items: center; gap: 9px; padding-left: 6px; }
.account .who b { display: block; font-size: 12.5px; font-weight: 500; line-height: 1.25; }
.account .who small { color: var(--text-dim); font-size: 11px; }
.avatar.sm { width: 30px; height: 30px; font-size: 11px; }

/* ── Channel dropdown ────────────────────────────────────────────────── */

.dropdown {
  position: absolute;
  /* Under the left island, not under a bar that no longer spans the window. */
  top: calc(var(--gap) + var(--nav-h) + 6px);
  left: calc(var(--gap) + 52px);
  min-width: 230px;
  max-height: 320px;
  overflow-y: auto;
  list-style: none;
  margin: 0;
  padding: 6px;
  z-index: 6;
}
.dropdown li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  border-radius: 11px;
  cursor: pointer;
  font-size: 14px;
  color: var(--text-dim);
}
.dropdown li:hover { background: rgba(255, 255, 255, 0.07); color: var(--text); }
.dropdown li.active { color: var(--text); background: rgba(19, 163, 252, 0.14); }
.dropdown .count { margin-left: auto; font-size: 11.5px; font-variant-numeric: tabular-nums; }
.dropdown .empty { cursor: default; }
.dropdown .empty:hover { background: none; }

/* ── Roster ──────────────────────────────────────────────────────────── */

.roster {
  position: absolute;
  top: calc(var(--gap) + var(--nav-h) + var(--gap));
  right: var(--gap);
  bottom: var(--gap);
  width: var(--roster-w);
  display: flex;
  flex-direction: column;
  z-index: 4;
}

.roster > header,
.activity > header,
.sheet > header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 14px;
  border-bottom: 1px solid var(--hairline);
}
.roster h2, .activity h2, .sheet h2 {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  color: var(--text-dim);
}
.sheet h2 { font-size: 14px; letter-spacing: 0; color: var(--text); }
.roster .count {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
}

/* The box is fixed; only the list moves. A panel that grows with its contents
 * would push the map around every time somebody joined. */
.roster-list {
  list-style: none;
  margin: 0;
  padding: 8px;
  overflow-y: auto;
  display: grid;
  gap: 6px;
  /* Rows take their own height and stack from the top. Without this the grid
     stretches its rows to fill the box, so three members in a tall panel each
     became a card the height of a hand — the list looked like a menu rather
     than a roster, and the height changed every time somebody joined. */
  align-content: start;
  flex: 1;
  min-height: 0;
}

.roster-list li {
  display: flex;
  align-items: center;
  gap: 11px;
  /* Fixed, not minimum: every row the same height means the list can be read
     by position, and a long name or a missing phone number cannot make one
     card taller than its neighbours. */
  height: 56px;
  padding: 0 10px;
  border-radius: 12px;
  cursor: pointer;
  box-sizing: border-box;
}
.roster-list li:hover { background: rgba(255, 255, 255, 0.06); }
.roster-list li.empty {
  cursor: default;
  color: var(--text-dim);
  font-size: 12.5px;
  height: auto;
  padding: 10px;
  line-height: 1.5;
}
.roster-list li.empty:hover { background: none; }

.avatar {
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: rgba(19, 163, 252, 0.16);
  color: var(--accent);
  font-weight: 700;
  font-size: 12.5px;
  flex: none;
}
.avatar.speaking { background: rgba(51, 204, 115, 0.2); color: var(--live); }

.who { flex: 1; min-width: 0; }
.who b {
  display: block;
  font-weight: 500;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.who small {
  display: block;
  color: var(--text-dim);
  font-size: 11.5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tag {
  font-size: 10px;
  font-weight: 700;
  border-radius: 999px;
  padding: 3px 8px;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-dim);
  flex: none;
}
.tag.live { background: rgba(51, 204, 115, 0.18); color: var(--live); }
.tag.on { background: rgba(19, 163, 252, 0.16); color: var(--accent); }

/* ── Activity ────────────────────────────────────────────────────────── */

.activity {
  position: absolute;
  left: var(--gap);
  bottom: var(--gap);
  width: 268px;
  max-height: 230px;
  display: flex;
  flex-direction: column;
  z-index: 4;
}
.activity.collapsed ul { display: none; }
.activity ul {
  list-style: none;
  margin: 0;
  padding: 8px 12px 12px;
  display: grid;
  gap: 7px;
  overflow-y: auto;
}
.activity li { display: flex; align-items: baseline; gap: 9px; font-size: 13px; }
.activity li.empty { color: var(--text-dim); font-size: 12.5px; }
.activity time {
  margin-left: auto;
  color: var(--text-dim);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
}
.activity .live-dot {
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--live); flex: none; align-self: center;
}
.activity > header .chip { margin-left: auto; }

/* ── Settings sheet ──────────────────────────────────────────────────── */

.sheet-scrim {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
  display: grid;
  place-items: center;
  padding: 24px;
  z-index: 10;
}
.sheet { width: min(560px, 100%); max-height: 86vh; display: flex; flex-direction: column; }
.sheet > header .chip { margin-left: auto; }
.sheet-body { padding: 18px; overflow-y: auto; display: grid; gap: 22px; }

.group > label { display: block; font-size: 13px; color: var(--text-dim); margin-bottom: 9px; }
.group code {
  display: block;
  font-size: 12.5px;
  color: var(--text-dim);
  word-break: break-all;
  margin-bottom: 11px;
}
.group .hint { display: block; margin-top: 10px; color: var(--text-dim); font-size: 12px; }

.inline-field { display: flex; gap: 10px; }
.inline-field input {
  flex: 1;
  min-width: 0;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--hairline);
  border-radius: 11px;
  color: var(--text);
  padding: 11px 13px;
  font-size: 15px;
  outline: none;
}
.inline-field input:focus { border-color: var(--accent); }
.primary.inline { width: auto; margin: 0; padding: 0 18px; }

.switch-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18px;
  cursor: pointer;
  margin: 0;
}
.switch-row > span { font-size: 14.5px; color: var(--text); }
.switch-row small {
  display: block;
  margin-top: 6px;
  color: var(--text-dim);
  font-size: 12.5px;
  line-height: 1.55;
}
.switch-row input { width: 20px; height: 20px; accent-color: var(--accent); flex: none; }

.ghost {
  background: none;
  border: 1px solid var(--hairline);
  border-radius: 10px;
  color: var(--text-dim);
  padding: 8px 13px;
  font-size: 13px;
  cursor: pointer;
}
.ghost:hover { color: var(--text); border-color: rgba(255, 255, 255, 0.2); }
.ghost.danger { color: var(--danger); border-color: rgba(240, 84, 79, 0.35); }
.row-actions { display: flex; gap: 10px; }

.fineprint { margin: 16px 0 0; text-align: center; color: var(--text-dim); font-size: 11.5px; }

.narrow-only { display: none; }

/* ── Responsive ──────────────────────────────────────────────────────────
 *
 * Three real layouts, not one squeezed. A console is used on a control-room
 * monitor and on a phone in a van, and the middle case — a laptop — is the one
 * where the roster starts costing more map than it is worth.
 */

@media (max-width: 1180px) {
  :root { --roster-w: 260px; }
  .account .who { display: none; }
}

@media (max-width: 900px) {
  :root { --gap: 10px; }

  /* The roster becomes a drawer: on a small screen it would otherwise take
     half the map, and the map is what somebody opened this for.
     It is left peeking by its own header rather than hidden behind a button —
     a drawer with no visible edge is a drawer nobody finds. Tapping the
     header opens it. */
  .roster {
    top: auto;
    bottom: var(--gap);
    left: var(--gap);
    right: var(--gap);
    width: auto;
    height: 46vh;
    transform: translateY(calc(46vh - 47px));
    transition: transform 0.24s ease;
  }
  .roster.open { transform: none; }
  .roster > header { cursor: pointer; }
  .narrow-only { display: inline-flex; }

  .activity { display: none; }
  .status span { display: none; }
}

@media (max-width: 560px) {
  /* Stacked rather than side by side: two islands squeezed onto one narrow row
     leave neither readable. */
  .navbar { flex-direction: column; align-items: stretch; }
  .nav-left, .nav-right { justify-content: space-between; }
  .roster { max-height: 60vh; }
  .dropdown {
    top: calc(var(--gap) + var(--nav-h) * 2 + 18px);
    left: var(--gap);
    right: var(--gap);
    min-width: 0;
  }
}

/* ── Listen ──────────────────────────────────────────────────────────── */

.chip.listening {
  background: rgba(51, 204, 115, 0.16);
  border-color: rgba(51, 204, 115, 0.4);
  color: var(--live);
}

/* A level meter, not an equaliser: it exists to answer "is anything actually
 * coming through", which on a silent channel is otherwise unanswerable. */
.chip.listening .speaker { position: relative; }
.chip .speaker { font-style: normal; }

/* ── Push to talk ────────────────────────────────────────────────────── */

.ptt {
  position: absolute;
  left: 50%;
  bottom: var(--gap);
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 54px;
  padding: 0 26px;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  background: var(--accent);
  color: #000;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  z-index: 6;
  box-shadow: 0 10px 30px rgba(19, 163, 252, 0.3);
  transition: background 0.14s ease, transform 0.14s ease, box-shadow 0.14s ease;
  user-select: none;
  -webkit-user-select: none;
  touch-action: none;
}
.ptt .mic { font-style: normal; font-size: 18px; }

.ptt:active { transform: translateX(-50%) scale(0.97); }

.ptt.talking {
  background: var(--live);
  box-shadow: 0 10px 34px rgba(51, 204, 115, 0.42);
}

.ptt.requesting { background: var(--own); box-shadow: none; }

/* Locked while somebody else holds the floor. Dimmed rather than hidden: a
 * control that disappears makes people hunt for it, and the reason it is
 * unavailable is exactly what the label is saying. */
.ptt:disabled {
  background: rgba(255, 255, 255, 0.1);
  color: var(--text-dim);
  border-color: var(--hairline);
  box-shadow: none;
  cursor: not-allowed;
}
.ptt:disabled:active { transform: translateX(-50%); }

@media (max-width: 900px) {
  /* Above the roster drawer's peeking header, not under it. */
  .ptt { bottom: calc(var(--gap) + 58px); }
}


/* ── Address search ──────────────────────────────────────────────────────
 *
 * Centred on the window rather than placed between the navbar islands: those
 * two size themselves to their contents, so a middle item would slide left and
 * right whenever the channel name or the signed-in account changed. A search
 * box that moves while you are aiming at it is worse than one slightly off
 * centre.
 *
 * It sits at the same height as the navbar and shares its glass, so the three
 * read as one row of floating chrome rather than as a box dropped on top. */
.search {
  position: absolute;
  top: var(--gap);
  left: 50%;
  transform: translateX(-50%);
  z-index: 6;
  width: min(420px, 38vw);
  /* Below the roster and the navbar cards in stacking terms is fine; what
   * matters is that the empty space around the field never eats map clicks. */
  pointer-events: none;
}

.search-field {
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--nav-h);
  padding: 0 12px;
  pointer-events: auto;
}

.search-icon {
  color: var(--text-dim);
  font-size: 18px;
  line-height: 1;
  flex: none;
}

/* The search box draws its own field — the island around it *is* the field —
 * so the general input styling has to be undone here rather than avoided
 * there.
 *
 * Stated with the type attribute on purpose. Without it this rule and the
 * general `input[type="text"]` one carry the same specificity, the general one
 * comes later in the file and therefore wins, and the box, the border and the
 * focus ring all reappear inside an island that already has them. Winning by
 * position is not winning; the next thing appended to this file would take it
 * back. */
.search-field input[type="text"] {
  flex: 1;
  min-width: 0;
  width: auto;
  height: 100%;
  min-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  outline: none;
}
.search-field input[type="text"]:hover,
.search-field input[type="text"]:focus {
  border: 0;
  background: none;
  box-shadow: none;
}
.search-field input::placeholder { color: var(--text-dim); }
/* The type=search clear affordance is inconsistent across browsers and does
 * not match anything else here; we draw our own. */
.search-field input::-webkit-search-cancel-button { display: none; }

.search-clear {
  flex: none;
  width: 24px;
  height: 24px;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: var(--text-dim);
  font-size: 11px;
  cursor: pointer;
}
.search-clear:hover { color: var(--text); }

.search-results {
  margin-top: 6px;
  padding: 6px;
  max-height: min(50vh, 380px);
  overflow-y: auto;
  list-style: none;
  pointer-events: auto;
}

.search-results li {
  padding: 9px 10px;
  border-radius: 10px;
  cursor: pointer;
  line-height: 1.35;
}
.search-results li:hover,
.search-results li[aria-selected="true"] { background: rgba(255, 255, 255, 0.08); }

.search-results .title {
  display: block;
  font-size: 14px;
  color: var(--text);
}
.search-results .subtitle {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.search-results .empty {
  padding: 12px 10px;
  color: var(--text-dim);
  font-size: 13px;
  cursor: default;
}
.search-results .empty:hover { background: none; }

/* Narrow: the three islands cannot share a row, so search drops to its own
 * line under the navbar and takes the full width. */
@media (max-width: 900px) {
  .search { width: min(520px, calc(100% - var(--gap) * 2)); }
}

/* Very narrow: the navbar has already stacked into two rows, so the search
 * clears both of them and spans the width. Absolute, not static — the navbar
 * is absolutely positioned and takes no room in the flow, so a static box here
 * would sit underneath it rather than below it. */
@media (max-width: 560px) {
  .search {
    top: calc(var(--gap) + var(--nav-h) * 2 + 20px);
    left: var(--gap);
    right: var(--gap);
    transform: none;
    width: auto;
  }
}

/* ── Teams ───────────────────────────────────────────────────────────────
 *
 * A channel is a department and teams are the squads inside it. Both lists
 * live in the settings sheet rather than on the map: arranging people is
 * something you sit down to do, not something you do while watching traffic. */

.tag.team {
  border: 1px solid var(--hairline);
  background: none;
  color: var(--text-dim);
}


/* The colour row for a team: the palette plus a "no colour" slot.
 *
 * Four fixed choices rather than a colour wheel. The markers share a dark map
 * where any two teams can end up side by side, and a free picker invites two
 * shades of the same blue — indistinguishable at 38px — or the exact green that
 * already means "talking". */
.swatches {
  display: flex;
  align-items: center;
  gap: 5px;
  flex: none;
}

.team-swatch {
  width: 16px;
  height: 16px;
  padding: 0;
  border-radius: 50%;
  border: 2px solid transparent;
  background: var(--text-dim);
  cursor: pointer;
  /* Bigger than it looks: 16px of paint is too small a target, so the hit area
   * is padded out without changing the dot. */
  outline-offset: 3px;
}
.team-swatch:disabled { cursor: default; opacity: 0.5; }
.team-swatch:not(:disabled):hover { transform: scale(1.15); }

/* Selection is a ring, not a checkmark: a tick drawn on a coloured dot at this
 * size is unreadable on half the palette. */
.team-swatch.on {
  border-color: var(--text);
  box-shadow: 0 0 0 1px var(--surface) inset;
}

.team-swatch.none {
  background: none;
  border: 2px dashed var(--hairline);
}
.team-swatch.none.on { border-style: solid; border-color: var(--text); }

@media (prefers-reduced-motion: reduce) {
  .team-swatch:not(:disabled):hover { transform: none; }
}

.team-list .who, .vehicle-list .who { flex: 1; min-width: 0; }
.team-list .who b { display: block; font-size: 14px; }
.team-list .who small { color: var(--text-dim); font-size: 12px; }

/* A plain dot in the list; the palette itself only appears in the dialog. */
.team-swatch.static { width: 10px; height: 10px; border: 0; cursor: default; }

.people-list .who { flex: 1; min-width: 0; }
.people-list .who b {
  display: block;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.people-list .who small { color: var(--text-dim); font-size: 12px; }

.people-list select, .team-list select {
  flex: none;
  max-width: 120px;
  height: 30px;
  padding: 0 6px;
  border: 1px solid var(--hairline);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}
.people-list select:disabled { opacity: 0.5; }

/* Narrow: the two dropdowns stop fitting beside the name and wrap under it. */
@media (max-width: 520px) {
  .people-list li { flex-wrap: wrap; }
  .people-list .who { flex-basis: 100%; }
  .people-list select { flex: 1; max-width: none; }
}

/* ── Settings page ───────────────────────────────────────────────────────
 *
 * Full screen and on its own URL, not a sheet over the map. Teams, roles and
 * invites are arranged sitting down; a modal gave them no address, so a tab
 * could not be linked to and closing it lost your place. */

.settings-page {
  display: grid;
  grid-template-columns: 248px 1fr;
  min-height: 100dvh;
}

.settings-rail {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: var(--gap);
  border-right: 1px solid var(--hairline);
  background: rgba(255, 255, 255, 0.02);
}

.settings-rail .back {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 14px;
}
.settings-rail .back:hover { color: var(--text); }

.rail-group { display: flex; flex-direction: column; gap: 6px; }
.rail-group label { color: var(--text-dim); font-size: 12px; }
.rail-group select {
  height: 34px;
  padding: 0 8px;
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font: inherit;
  font-size: 14px;
}

.rail-tabs { display: flex; flex-direction: column; gap: 2px; }
.rail-tabs a {
  padding: 9px 12px;
  border-radius: 10px;
  color: var(--text-dim);
  text-decoration: none;
  font-size: 14px;
}
.rail-tabs a:hover { background: rgba(255, 255, 255, 0.05); color: var(--text); }
.rail-tabs a.on { background: rgba(255, 255, 255, 0.09); color: var(--text); }

.rail-foot {
  margin-top: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding-top: 14px;
  border-top: 1px solid var(--hairline);
}
.rail-foot .who { flex: 1; min-width: 0; }
.rail-foot .who b { display: block; font-size: 14px; }
.rail-foot .who small { color: var(--text-dim); font-size: 12px; }

.settings-body {
  padding: calc(var(--gap) * 2);
  min-width: 0;
}

/* Tables get the whole width — a fleet is read across, and columns squeezed
 * into a narrow measure wrap and stop lining up. Prose does not: a sentence
 * running the width of a monitor loses the reader at every line break, so the
 * explanatory text keeps a readable measure while the grid beside it does not. */
.settings-body .hint,
.settings-body .sub,
.settings-body .switch-row small { max-width: 78ch; }

.settings-head {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  margin-bottom: 26px;
}
.settings-head .head-text { flex: 1; min-width: 0; }
.settings-head .head-actions { flex: none; }
.settings-head h1 { margin: 0; font-size: 24px; font-weight: 650; }
.settings-head .sub { margin: 6px 0 0; color: var(--text-dim); font-size: 14px; }

/* Narrow: the rail becomes a header strip and the tabs scroll sideways, which
 * keeps every tab reachable without a menu inside a menu. */
@media (max-width: 720px) {
  .settings-page { grid-template-columns: 1fr; }
  .settings-rail {
    border-right: 0;
    border-bottom: 1px solid var(--hairline);
  }
  .rail-tabs { flex-direction: row; overflow-x: auto; }
  .rail-tabs a { white-space: nowrap; }
  .rail-foot { margin-top: 0; }
  .settings-body { padding: var(--gap); }
}

/* ── Roster groups ───────────────────────────────────────────────────────
 *
 * The roster is grouped by team, with everyone unassigned under "Diğer".
 * Collapsible because a channel with six teams in it is a list you scroll
 * past, not one you read — and the operator usually cares about two of them.
 *
 * The team name is on the heading and not repeated on every row: under a
 * heading that already says "Saha", a "Saha" tag on each line is noise. */

li.roster-group {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 10px;
  margin-top: 6px;
  border: 0;
  background: none;
  cursor: pointer;
  user-select: none;
  color: var(--text-dim);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
li.roster-group:first-child { margin-top: 0; }
li.roster-group:hover { color: var(--text); }

li.roster-group .caret {
  font-size: 13px;
  line-height: 1;
  transition: transform 140ms ease;
}
li.roster-group.closed .caret { transform: rotate(-90deg); }

li.roster-group .title {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

li.roster-group .count {
  flex: none;
  padding: 1px 7px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  font-size: 11px;
  letter-spacing: 0;
}

@media (prefers-reduced-motion: reduce) {
  li.roster-group .caret { transition: none; }
}

/* ── Marker badges ───────────────────────────────────────────────────────
 *
 * Two corners, two meanings. Busy takes the bottom right because it is what
 * decides whether you call somebody at all; the vehicle goes top right because
 * it is context about a person you have already picked out.
 *
 * Neither is drawn across the pin. The first version put a slash over it and
 * the line ran straight through the initials — the marker became harder to
 * identify at exactly the moment it needed a second look. */
.marker { position: relative; }

.marker-badge {
  position: absolute;
  width: 17px;
  height: 17px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--surface);
  font-size: 9px;
  font-style: normal;
  line-height: 1;
}

.marker-badge.vehicle { right: -4px; top: -4px; }

.marker-badge.busy {
  right: -4px;
  bottom: -4px;
  background: var(--danger);
  border: 1.5px solid var(--surface);
}
/* A bar, not an emoji: at seventeen pixels an emoji is a smudge, and this is
 * the universal do-not-disturb mark. */
.marker-badge.busy::before {
  content: "";
  width: 8px;
  height: 2px;
  border-radius: 1px;
  background: #fff;
}

/* Still slightly recessive, so a busy marker reads as busy even before the
 * badge is resolved — but without anything drawn over the initials. */
.marker.dimmed { opacity: 0.72; }


/* ── Trail playback ──────────────────────────────────────────────────────
 *
 * One person, one time range, played against a clock — in a window with its
 * own map. The first cut drew on the console's map and floated a sheet over
 * it, which covered the very thing it was asking you to watch. */

.trail-scrim {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: grid;
  place-items: center;
  padding: var(--gap);
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(3px);
}
.trail-scrim[hidden] { display: none !important; }

.trail-window {
  display: flex;
  flex-direction: column;
  width: min(980px, 100%);
  height: min(760px, 100%);
  background: var(--glass);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
  overflow: hidden;
}

.trail-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--hairline);
}
.trail-head .who { flex: 1; min-width: 0; }
.trail-head .who b { display: block; font-size: 16px; }
.trail-head .who small { color: var(--text-dim); font-size: 13px; }

.trail-ranges {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  padding: 12px 16px;
  border-bottom: 1px solid var(--hairline);
}

.quick-ranges { display: flex; flex-wrap: wrap; gap: 6px; }
.quick-ranges button { white-space: nowrap; }

.custom-range {
  display: flex;
  gap: 6px;
  margin-left: auto;
  flex-wrap: wrap;
}
.custom-range input[type="datetime-local"] {
  height: 32px;
  padding: 0 8px;
  border: 1px solid var(--hairline);
  border-radius: 9px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font: inherit;
  font-size: 13px;
  color-scheme: dark;
}

/* The map is the window: it takes whatever is left after the chrome. */
.trail-map { flex: 1; min-height: 0; }

.player {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-top: 1px solid var(--hairline);
}

.player input[type="range"] {
  flex: 1;
  min-width: 0;
  accent-color: var(--accent);
}

.player .clock {
  flex: none;
  color: var(--text-dim);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
}

.player select {
  flex: none;
  height: 30px;
  padding: 0 6px;
  border: 1px solid var(--hairline);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}

/* The moving dot. Deliberately not a pin: a pin points at a place, and this
 * points at a moment. */
.playhead {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  border: 3px solid var(--accent);
  box-shadow: 0 0 12px rgba(19, 163, 252, 0.8);
}

@media (max-width: 720px) {
  .custom-range { margin-left: 0; }
  .player { flex-wrap: wrap; }
  .player input[type="range"] { flex-basis: 100%; order: 3; }
}

/* ── Busy ────────────────────────────────────────────────────────────────
 *
 * Drawn on the pin rather than as a second badge. Busy is not a detail about
 * somebody you have already decided to call — it is what decides whether you
 * call them, so it has to read out of the corner of an eye on a map carrying
 * thirty markers.
 *
 * A dimmed pin with a slash, not a colour: the colour channel is spoken for
 * twice over already, by the team and by whoever is talking. */

.tag.busy {
  border: 1px solid var(--hairline);
  background: none;
  color: var(--text-dim);
}

/* ── Fleet ───────────────────────────────────────────────────────────────
 *
 * Vehicles are arranged sitting down, like teams, so the list lives on the
 * settings page and the console only ever reads the result. */
.vehicle-list .who small { color: var(--text-dim); font-size: 12px; }
.vehicle-list select {
  flex: none;
  max-width: 130px;
  height: 30px;
  padding: 0 6px;
  border: 1px solid var(--hairline);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}



/* ── The form dialog ─────────────────────────────────────────────────────
 *
 * Adding and editing happen in a dialog rather than in the row. Editing in
 * place read as typing into a list: nothing marked the moment a change was
 * committed, and a rename saved itself on blur whether or not that was the
 * intention. */

.sheet-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 14px 20px;
  border-top: 1px solid var(--hairline);
}

#form-body .group { margin-bottom: 16px; }
#form-body label {
  display: block;
  margin-bottom: 6px;
  color: var(--text-dim);
  font-size: 13px;
}
#form-body input[type="text"],
#form-body select {
  width: 100%;
  height: 38px;
  padding: 0 10px;
  border: 1px solid var(--hairline);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font: inherit;
  font-size: 14px;
}
#form-body .hint { display: block; margin-top: 6px; }

/* ── Vehicle stickers ────────────────────────────────────────────────────
 *
 * Two codes, side by side, labelled. They are printed and stuck to a vehicle —
 * driver by the wheel, passenger on the other side — and which one gets
 * scanned is what decides the role, so they must never be confusable on the
 * page they are printed from. */
.qr-sheet { max-width: 560px; }

.qr-body {
  display: flex;
  gap: 20px;
  justify-content: center;
}
.qr-body figure { margin: 0; text-align: center; flex: 1; }
.qr-body figcaption {
  margin-bottom: 8px;
  color: var(--text-dim);
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* White plate behind the code: a QR printed on a dark background does not
 * scan, and this dialog is dark. */
.qr-box {
  background: #fff;
  border-radius: 12px;
  padding: 10px;
  display: grid;
  place-items: center;
}
.qr-box img { display: block; width: 100%; height: auto; max-width: 220px; }

.qr-note {
  margin: 18px 0 0;
  color: var(--text-dim);
  font-size: 13px;
  line-height: 1.5;
}

/* `.primary` is full width by default, which is right for the one button at
 * the end of a form and wrong for a footer that holds a single action beside
 * nothing. */
.primary.compact { width: auto; margin-top: 0; padding: 10px 22px; }

/* Printing gives just the two codes, on white, with their labels. Nothing
 * else on the page belongs on a sticker. */
@media print {
  body * { visibility: hidden; }
  #qr, #qr * { visibility: visible; }
  #qr {
    position: absolute;
    inset: 0;
    background: #fff;
    display: block;
    padding: 24px;
  }
  .qr-sheet { max-width: none; border: 0; box-shadow: none; background: #fff; }
  .qr-sheet header button, #qr-print { display: none; }
  .qr-sheet h2, .qr-body figcaption, .qr-sheet .hint { color: #000; }
}


/* ── Tables ──────────────────────────────────────────────────────────────
 *
 * Real tables, because these are tabular: a fleet is read by scanning down the
 * plate column and across to the team, and rows built out of flex boxes cannot
 * line up when one brand is longer than another.
 *
 * Header row is sticky so the column meanings survive a long fleet, and the
 * whole thing scrolls sideways inside its own box rather than pushing the page
 * wider — a settings page that scrolls horizontally is a page where the rail
 * disappears. */
.table-wrap {
  overflow-x: auto;
  border: 1px solid var(--hairline);
  border-radius: 14px;
}

table.grid {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

table.grid thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  padding: 10px 14px;
  text-align: left;
  white-space: nowrap;
  background: var(--surface);
  color: var(--text-dim);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  border-bottom: 1px solid var(--hairline);
}

table.grid td {
  padding: 11px 14px;
  border-bottom: 1px solid var(--hairline);
  vertical-align: middle;
  color: var(--text);
}
/* No line under the last row: the container already draws that edge, and two
 * hairlines a pixel apart look like a mistake. */
table.grid tbody tr:last-child td { border-bottom: 0; }
table.grid tbody tr:hover td { background: rgba(255, 255, 255, 0.03); }

table.grid td.strong { font-weight: 600; }
table.grid td.dim { color: var(--text-dim); }
table.grid td.num, table.grid th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* Plates are compared down the column, and proportional digits make that
 * harder than it needs to be. */
table.grid td.plate {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.03em;
}
table.grid td.veh-icon { font-size: 18px; width: 1%; padding-right: 0; }

table.grid td.who b { display: block; font-weight: 600; }
table.grid td.who small { color: var(--text-dim); font-size: 12px; }

table.grid td.actions {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}
table.grid td.actions button { margin-left: 6px; }

table.grid td.empty {
  padding: 22px 14px;
  color: var(--text-dim);
  text-align: center;
}
table.grid tbody tr:hover td.empty { background: none; }

table.grid select {
  width: 100%;
  min-width: 120px;
  height: 32px;
  padding: 0 8px;
  border: 1px solid var(--hairline);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.05);
  color: var(--text);
  font: inherit;
  font-size: 13px;
}
table.grid select:disabled { opacity: 0.5; }

th.w-swatch, td:has(> .team-swatch.static) { width: 1%; padding-right: 0; }
th.w-select { width: 160px; }

/* One column absorbs the slack.
 *
 * A table at `width: 100%` with every column sized to its content leaves the
 * spare width wherever the browser feels like putting it, which on a wide
 * monitor bunches everything against the left edge with a gulf before the
 * buttons. Naming the column that should stretch — the longest free text in
 * each table — spreads the rest evenly and keeps the actions pinned right.
 *
 * `width: 100%` on one cell is how tables are told this; it is a request for
 * everything left over, not a literal width. */
table.grid th.flex, table.grid td.flex { width: 100%; }

/* The fixed columns then only need to not collapse. */
table.grid td, table.grid th { white-space: nowrap; }
table.grid td.flex, table.grid td.who { white-space: normal; }

@media (max-width: 560px) {
  table.grid { font-size: 13px; }
  table.grid thead th, table.grid td { padding: 9px 10px; }
}

/* ── Bayat konum ─────────────────────────────────────────────────────── */

/* The age rides inside the name label rather than beside the pin.
 *
 * It has to be read together with the name — "Ahmet, 2 saat önce" is one fact,
 * not two — and a second floating chip beside every old marker turned a map of
 * twenty people into a map of forty things. */
.marker-age {
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

/* ── Yer çizme ───────────────────────────────────────────────────────── */

/* Nearly the whole window. Drawing the edge of a site means seeing the site,
 * and a dialog the size of a form would have somebody panning a postage stamp
 * around looking for their own depot. */
.draw-sheet {
  width: min(1100px, 94vw);
  height: min(720px, 90vh);
  display: flex;
  flex-direction: column;
}

.draw-body {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr 280px;
}

.draw-map {
  min-height: 0;
  border-right: 1px solid var(--hairline);
  cursor: crosshair;
}

.draw-side {
  padding: 14px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.draw-side .group { margin: 0; }
.draw-side .hint { color: var(--text-dim); font-size: 12px; line-height: 1.45; }

.swatch-row { display: flex; gap: 8px; flex-wrap: wrap; }

/* The segmented control lives in the form layer at the end of this file. */

@media (max-width: 820px) {
  .draw-body { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
  .draw-map { border-right: 0; border-bottom: 1px solid var(--hairline); }
  .draw-side { max-height: 42vh; }
}

/* The place's name, written across its shape.
 *
 * Quiet on purpose: it names the ground, and the ground must not compete with
 * the people standing on it. The coloured edge is what ties the word to the
 * outline it belongs to when three of them overlap. */
.poi-label {
  transform: translate(-50%, -50%);
  white-space: nowrap;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.2px;
  padding: 2px 8px;
  border-radius: 999px;
  color: var(--text);
  background: rgba(7, 10, 20, 0.72);
  border: 1px solid transparent;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
}

/* ── Mürettebat işaretçisi ───────────────────────────────────────────── */

/* A vehicle is a box, a person is a disc.
 *
 * The shape is the first thing read at a glance, before colour and long before
 * text — so "that is a lorry with people in it" and "that is a person" must not
 * be the same silhouette.
 *
 * Two squares side by side, each the size of a member's pin: what it is on the
 * left, who is in it on the right. Bigger than a person deliberately — a van
 * carrying four people is four times the thing a single walker is, and on a
 * dispatch map the eye should land on it first. */
/* The box is wider and taller than a person's pin, so the wrapper that
 * positions the plate under it has to be too. */
.crew-wrap { width: 92px; height: 46px; }
.crew-wrap .marker-label { top: 50px; }
/* The ring follows the box rather than a circle: a round pulse around a
 * rectangle reads as two unrelated things. */
.crew-wrap .pulse {
  width: 92px;
  height: 46px;
  margin: -23px 0 0 -46px;
  border-radius: 15px;
}

.vehicle-marker {
  display: grid;
  grid-template-columns: 46px 46px;
  align-items: center;
  height: 46px;
  border-radius: 13px;
  background: rgba(16, 21, 36, 0.94);
  border: 2px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
  overflow: hidden;
}
.vehicle-marker.speaking {
  border-color: var(--live, #22c55e);
  background: rgba(20, 40, 28, 0.95);
}
.vehicle-marker.stale { opacity: 0.6; }

.vehicle-kind {
  display: grid;
  place-items: center;
  font-size: 22px;
  line-height: 1;
  height: 100%;
  /* A hairline between the two halves, so the icon reads as a label for the
   * box rather than as a fifth passenger. */
  border-right: 1px solid rgba(255, 255, 255, 0.14);
}

/* Two by two, always. The cells keep their places whether there are one or
 * four, so a crew that gains somebody grows into the empty square instead of
 * shuffling the faces that were already there. */
.crew {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  gap: 2px;
  padding: 3px;
  height: 100%;
  box-sizing: border-box;
  place-items: center;
}

/* One rider takes the whole right half rather than a quarter of it: a marker
 * with a single tiny disc in its top-left corner reads as broken. */
.crew[data-count="1"] {
  grid-template-columns: 1fr;
  grid-template-rows: 1fr;
}
.crew[data-count="1"] .crew-dot { width: 30px; height: 30px; font-size: 12px; }

/* Two sit side by side and use the full height. */
.crew[data-count="2"] { grid-template-rows: 1fr; }
.crew[data-count="2"] .crew-dot { width: 19px; height: 19px; }

.crew-dot {
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #3987e5;
  border: 1px solid rgba(0, 0, 0, 0.3);
  box-sizing: border-box;
  font-size: 8px;
  font-weight: 800;
  font-style: normal;
  letter-spacing: -0.3px;
  line-height: 1;
  color: #fff;
}
.crew-dot.speaking { background: var(--live, #22c55e); }
.crew-dot.more {
  background: rgba(255, 255, 255, 0.18);
  font-size: 8.5px;
  letter-spacing: 0;
}

.marker-label.plate {
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.4px;
}

/* ── Vasıta açılır paneli ────────────────────────────────────────────── */

/* Anchored to the pin rather than thrown over the map: the question is "who is
 * in that lorry", and an answer that covers the map hides the other lorries it
 * is being compared with. */
.veh-callout {
  min-width: 240px;
  max-width: 300px;
  background: var(--glass, rgba(16, 21, 36, 0.92));
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--hairline);
  border-radius: 14px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
  overflow: hidden;
  color: var(--text);
}
.veh-callout > header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 11px 12px;
  border-bottom: 1px solid var(--hairline);
}
.veh-callout .veh-icon { font-size: 20px; }
.veh-callout header b {
  display: block;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.3px;
}
.veh-callout header small,
.veh-callout li small {
  display: block;
  font-size: 11px;
  color: var(--text-dim);
}
.veh-callout ul {
  list-style: none;
  margin: 0;
  padding: 4px 0;
  max-height: 210px;
  overflow-y: auto;
}
.veh-callout li {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 12px;
}
.veh-callout li > div { flex: 1; min-width: 0; }
.veh-callout li b {
  display: block;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.veh-callout li .crew-dot { width: 26px; height: 26px; font-size: 10.5px; }
.veh-callout li button {
  flex: none;
  font-size: 11px;
  padding: 4px 9px;
}
.veh-note {
  margin: 0;
  padding: 8px 12px 10px;
  font-size: 11px;
  line-height: 1.4;
  color: var(--text-dim);
  border-top: 1px solid var(--hairline);
}

/* A result from our own places, not from Apple's index.
 *
 * Marked because "Depo" from a channel's own list and "Depo" from a search of
 * every business in the country are different claims, and the operator is about
 * to move the map on the strength of one of them. The pin is enough — a word
 * would push the address onto a second line. */
.search-results li.own .title::before {
  content: "📍";
  margin-right: 6px;
  font-size: 11px;
}
.search-results li.own {
  background: rgba(255, 255, 255, 0.04);
}

/* The "look this up as an address instead" row.
 *
 * Set apart from the results above it because it is not a result: it is an
 * action, and one that replaces everything on the list. Dimmer and separated,
 * so the eye reaches it last — most of the time the place above it is what was
 * wanted. */
.search-results li.lookup {
  margin-top: 4px;
  padding-top: 10px;
  border-top: 1px solid var(--hairline);
  border-radius: 0 0 10px 10px;
}
.search-results li.lookup .title {
  color: var(--text-dim);
  font-weight: 500;
}
.search-results li.lookup .title::before {
  content: "🔍";
  margin-right: 6px;
  font-size: 11px;
}

/* The map's own right-click menu.
 *
 * Small, and only two entries: this is the shortcut, not the settings screen.
 * Anything more belongs where places are managed properly. */
.mapmenu {
  position: fixed;
  z-index: 60;
  min-width: 200px;
  padding: 5px;
  background: var(--glass);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  border: 1px solid var(--hairline);
  border-radius: 12px;
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
  display: flex;
  flex-direction: column;
}
.mapmenu button {
  border: 0;
  background: transparent;
  color: var(--text);
  font: inherit;
  font-size: 13px;
  text-align: left;
  padding: 9px 10px;
  border-radius: 8px;
  cursor: pointer;
}
.mapmenu button:hover { background: rgba(255, 255, 255, 0.08); }

/* Acting on what is already there, above the rule; making something new, below
 * it. Two different intentions, and the separator is what stops somebody
 * reaching for the wrong one. */
.mapmenu hr {
  margin: 4px 6px;
  border: 0;
  border-top: 1px solid var(--hairline);
}

#newplace .sheet { width: min(420px, 92vw); }
#newplace input[type="range"] { width: 100%; accent-color: var(--accent); }

/* ── Alan köşeleri ───────────────────────────────────────────────────── */

/* Big enough to grab, small enough not to hide the corner it marks.
 *
 * A boundary taken from map data is nearly always a few metres out somewhere,
 * and if correcting that means redrawing the whole outline then nobody ever
 * corrects it. */
.vertex-handle {
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  background: #fff;
  border: 2px solid rgba(0, 0, 0, 0.55);
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
  cursor: grab;
}
.vertex-handle:active { cursor: grabbing; }

/* The plus between two corners.
 *
 * Small and quiet until the pointer is on it, then unmistakable. It is an
 * offer, not a thing that is already there, and a line of solid dots between
 * every pair of corners would make the outline harder to read than the shape
 * it describes. */
.vertex-handle.mid {
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  display: grid;
  place-items: center;
  background: rgba(255, 255, 255, 0.5);
  border-width: 1px;
  color: transparent;
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  cursor: copy;
  transition: width 0.1s ease, height 0.1s ease, margin 0.1s ease,
              background 0.1s ease, color 0.1s ease;
}
.vertex-handle.mid:hover {
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  background: var(--accent);
  color: #04121d;
  border-color: rgba(0, 0, 0, 0.4);
}

/* The grip that moves the whole outline.
 *
 * In the middle, where nothing else is, and shaped differently from the
 * corners: dragging one corner and dragging thirty are different acts and the
 * handles should not look interchangeable. */
.shape-grip {
  width: 30px;
  height: 30px;
  margin: -15px 0 0 -15px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: rgba(7, 10, 20, 0.82);
  border: 1.5px solid rgba(255, 255, 255, 0.7);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.55);
  color: #fff;
  font-size: 14px;
  line-height: 1;
  cursor: move;
}
.shape-grip:active { cursor: grabbing; background: var(--accent); }

/* The pointer must be able to reach these while the cursor over the map is a
 * crosshair for placing new ones. */
.draw-map .vertex-handle { pointer-events: auto; }

/* ── Yan panel sekmeleri ─────────────────────────────────────────────── */

/* Two lists in one box. The map is what both are read against — "who is near
 * the Kayseri job" is one question — and a second panel would take a third of
 * the map to answer half of it. */
.side-tabs {
  display: flex;
  gap: 2px;
  margin-right: auto;
}
.side-tabs button {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 1px;
  text-transform: uppercase;
  padding: 4px 8px;
  border-radius: 8px;
  cursor: pointer;
}
.side-tabs button.on { color: var(--text); background: rgba(255, 255, 255, 0.07); }

/* ── İş satırı ───────────────────────────────────────────────────────── */

.job-list .job {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 9px 12px;
  flex-wrap: wrap;
  cursor: pointer;
}
.job-list .job:hover { background: rgba(255, 255, 255, 0.04); }

/* The number people say on the radio, given the weight that implies. */
.job-list .seq {
  min-width: 26px;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}
.job-list .job.urgent .seq { color: var(--danger, #f2555a); }

.job-list .who { flex: 1; min-width: 0; }
.job-list .who b {
  display: block;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.job-list .who small {
  display: block;
  font-size: 11px;
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.job-list .assignee { color: var(--accent); }

/* The actions wrap onto their own line rather than squeezing the title: on a
 * 300px panel something has to give, and it should not be the thing that says
 * what the job is. */
.job-actions {
  display: flex;
  gap: 4px;
  flex-basis: 100%;
  padding-left: 36px;
}
.job-actions button { font-size: 11px; padding: 3px 8px; }

#jobform .sheet { width: min(460px, 92vw); }
#jobform .custom-range { display: flex; gap: 6px; }
#jobform .custom-range input { flex: 1; min-width: 0; }

/* ── Form kontrolleri ────────────────────────────────────────────────────
 *
 * One layer for every input, select and picker in the panel.
 *
 * There was none. Each form got the browser's defaults — a white box on a dark
 * sheet, a native select with a system chevron, a date field that shrank to
 * whatever the platform thought — and the result read as unfinished next to
 * everything around it. Styling them one form at a time is how three forms end
 * up looking like three products, so this is written once and applies
 * everywhere.
 */

.group {
  display: flex;
  flex-direction: column;
  gap: 7px;
  margin-bottom: 16px;
}
.group:last-child { margin-bottom: 0; }

.group > label,
.sheet-body > .group > label {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.3px;
  color: var(--text-dim);
}

/* The controls themselves. Listed by type rather than by element so a date
 * field and a text field cannot drift apart. */
input[type="text"],
input[type="search"],
input[type="tel"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
select,
textarea {
  width: 100%;
  box-sizing: border-box;
  min-height: 40px;
  padding: 9px 12px;
  background: rgba(255, 255, 255, 0.045);
  border: 1px solid var(--hairline);
  border-radius: 10px;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  line-height: 1.35;
  outline: none;
  /* The native pickers — the calendar, the clock, the select popup — take
   * their palette from this. Without it the little icons come out black on a
   * dark field and read as missing rather than dark. */
  color-scheme: dark;
  transition: border-color 0.12s ease, background 0.12s ease;
}

input::placeholder,
textarea::placeholder { color: rgba(255, 255, 255, 0.32); }

input:hover:not(:disabled),
select:hover:not(:disabled),
textarea:hover:not(:disabled) { border-color: rgba(255, 255, 255, 0.18); }

input:focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
  background: rgba(19, 163, 252, 0.07);
  /* A ring rather than a thicker border: a border that grows on focus moves
   * everything beside it by a pixel, and a row of fields visibly twitches. */
  box-shadow: 0 0 0 3px rgba(19, 163, 252, 0.18);
}

input:disabled,
select:disabled,
textarea:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

textarea { min-height: 84px; resize: vertical; }

/* The select's own chevron, because the platform's sits in a light well and
 * cannot be recoloured. Drawn as a background image so the control stays a
 * real <select> and keeps its keyboard behaviour. */
select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 34px;
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='rgba(255,255,255,0.55)' d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 12px center;
  background-size: 11px;
  cursor: pointer;
}
/* The options are drawn by the operating system; the only thing worth setting
 * is that they are not white on white. */
select option { background: var(--surface); color: var(--text); }

/* The date and time fields carry an indicator the platform draws. Left in
 * place — it is the affordance people click — but dimmed to the same weight as
 * everything else on the sheet. */
input[type="date"]::-webkit-calendar-picker-indicator,
input[type="time"]::-webkit-calendar-picker-indicator,
input[type="datetime-local"]::-webkit-calendar-picker-indicator {
  opacity: 0.45;
  cursor: pointer;
  transition: opacity 0.12s ease;
}
input[type="date"]:hover::-webkit-calendar-picker-indicator,
input[type="time"]:hover::-webkit-calendar-picker-indicator,
input[type="datetime-local"]:hover::-webkit-calendar-picker-indicator { opacity: 0.9; }

/* A field the user has not filled shows its format in the dimmer weight, so
 * "gg.aa.yyyy" does not read as a value. */
input[type="date"]:invalid,
input[type="datetime-local"]:invalid { color: rgba(255, 255, 255, 0.45); }

/* Sliders. */
input[type="range"] {
  width: 100%;
  min-height: 0;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  accent-color: var(--accent);
  cursor: pointer;
}
input[type="range"]:focus { background: none; box-shadow: none; }

/* Checkboxes keep the platform's box — it is a well-understood control and
 * redrawing it buys nothing — but take the accent so they belong here. */
input[type="checkbox"],
input[type="radio"] {
  width: 18px;
  height: 18px;
  min-height: 0;
  padding: 0;
  background: none;
  border: 0;
  box-shadow: none;
  accent-color: var(--accent);
  cursor: pointer;
}
input[type="checkbox"]:focus,
input[type="radio"]:focus { background: none; box-shadow: none; }

/* The hint under a field is part of the field, not a paragraph after it. */
.group .hint,
.group small.hint {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--text-dim);
}

/* Two fields on one line — a from/to pair — share the row evenly rather than
 * letting the wider format win. */
.custom-range { display: flex; gap: 8px; }
.custom-range > input { flex: 1 1 0; min-width: 0; }

/* The segmented control, now that it is used in three places. */
.seg {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--hairline);
  border-radius: 11px;
  background: rgba(255, 255, 255, 0.03);
}
.seg button {
  border: 0;
  background: transparent;
  color: var(--text-dim);
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 8px 10px;
  border-radius: 8px;
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease;
}
.seg button:hover:not(.on) { color: var(--text); background: rgba(255, 255, 255, 0.05); }
.seg button.on { background: var(--accent); color: #04121d; }

/* Inside a sheet the grid gap already separates the groups, so the margin that
 * spaces them elsewhere would be counted twice. */
.sheet-body > .group { margin-bottom: 0; }

/* ---- Messages -------------------------------------------------------- */

/* The unread count, on the tab rather than beside a row: the tab is what a
   dispatcher is not looking at when a message arrives. */
.tab-badge {
  display: inline-block;
  min-width: 16px;
  margin-left: 6px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 10px;
  font-style: normal;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  vertical-align: middle;
}

.chat-list li { cursor: pointer; }

.chat-list .chat-row {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  flex: 1;
}

.chat-list .chat-last {
  color: var(--text-dim);
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.chat-list .chat-last.unread { color: var(--text); }

.chat-scrim {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: end center;
  background: rgba(0, 0, 0, 0.45);
  padding: var(--gap);
}

/* Anchored to the bottom right, over the roster it was opened from: the map
   keeps the space it is read on, and the conversation sits where the eye
   already is. */
.chat-box {
  display: flex;
  flex-direction: column;
  width: min(420px, 100%);
  height: min(560px, 80vh);
  margin-right: 0;
  background: var(--glass);
  backdrop-filter: blur(18px);
  border: 1px solid var(--hairline);
  border-radius: var(--radius);
  overflow: hidden;
}

.chat-box > header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--hairline);
}

.chat-log {
  list-style: none;
  margin: 0;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 6px;
  overflow-y: auto;
  flex: 1;
}

.chat-log li {
  /* Sized to its text, not to the column.
     A flex column stretches its children by default, so every incoming bubble
     ran the full width of the panel — a row of dark slabs with a word in the
     corner. The outgoing ones looked right only because `align-self` on
     `.mine` happened to opt them out. */
  align-self: flex-start;
  max-width: 78%;
  padding: 8px 12px;
  border-radius: 14px;
  /* Lighter than the app's surface: this box is glass over a map, and a
     bubble the colour of the panel behind it is not a bubble. */
  background: rgba(255, 255, 255, 0.09);
  font-size: 14px;
  line-height: 1.35;
  word-break: break-word;
}

.chat-log li.mine {
  align-self: flex-end;
  background: var(--accent);
}

.chat-log .who {
  display: block;
  color: var(--accent);
  font-size: 11px;
  font-weight: 600;
}

.chat-log .at {
  display: block;
  margin-top: 2px;
  color: var(--text-dim);
  font-size: 10px;
}

.chat-log li.mine .at { color: rgba(255, 255, 255, 0.7); }

.chat-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px;
  border-top: 1px solid var(--hairline);
}

/* The composer's buttons are not the page's full-width primary buttons.
   `.primary` is 100% wide with a top margin, which in a row swallowed the
   whole strip and squeezed the text field to nothing. */
.chat-foot .primary {
  width: auto;
  margin: 0;
  flex: 0 0 auto;
}

.chat-foot .chip.icon { flex: 0 0 auto; }

.chat-foot input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 10px 12px;
  border: 1px solid var(--hairline);
  border-radius: 999px;
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: 14px;
}

.chat-foot input:focus {
  outline: none;
  border-color: var(--accent);
}

.chat-foot button { padding: 0 16px; border-radius: 999px; }

.chat-empty {
  margin: auto;
  color: var(--text-dim);
  font-size: 13px;
}

/* A recording in a bubble: the button is the bubble's own content, so it takes
   the bubble's colour rather than looking like a control dropped into it. */
.chat-log .voice {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}

.chat-foot .chip.icon.on { background: var(--danger); color: #fff; }

/* The bars of a recording. Drawn as elements rather than a canvas: there are
   forty of them, they never animate individually, and the played/unplayed
   split is one class toggle. */
.chat-log .voice-bars {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  height: 26px;
}

.chat-log .voice-bars i {
  width: 2px;
  border-radius: 1px;
  background: currentColor;
  opacity: 0.4;
}

.chat-log .voice-bars i.on { opacity: 1; }

.chat-log .voice-icon { font-style: normal; }

.chat-log .voice-time {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  opacity: 0.75;
}

/* A photograph in a bubble. The width is fixed and the height follows the
   picture, which is why the message carries its dimensions: the log does not
   jump as pictures arrive. */
.chat-log .photo-wrap {
  position: relative;
  display: block;
  width: 208px;
  border-radius: 10px;
  overflow: hidden;
}

/* The blurred stand-in that came with the message, behind the real picture
   until it lands. Scaled up a little because a blur softens the edges of what
   it is blurring and would otherwise show the bubble through them. */
.chat-log .photo-blur {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
  filter: blur(10px);
  transform: scale(1.15);
}

.chat-log .photo {
  position: relative;
  display: block;
  width: 208px;
  height: auto;
  max-height: 280px;
  object-fit: cover;
  border-radius: 10px;
  cursor: zoom-in;
  opacity: 0;
  transition: opacity 0.18s ease;
}

.chat-log .photo-wrap.ready .photo { opacity: 1; }

/* Going out, or did not. */
.chat-log li.sending { opacity: 0.75; }

.chat-log li.failed { outline: 1.5px solid var(--danger); }

.chat-log .resend {
  margin-left: 6px;
  padding: 0 6px;
  border: 0;
  border-radius: 999px;
  background: var(--danger);
  color: #fff;
  font-weight: 700;
  font-size: 10px;
  line-height: 15px;
  cursor: pointer;
}

/* A photograph with nothing written under it is the whole message: no fill and
   no padding, because those hold text off an edge and a picture has its own.
   A border keeps it from bleeding into the glass, and the time sits on the
   picture rather than under it. */
.chat-log li.photo-only {
  position: relative;
  padding: 0;
  background: none;
  border: 2px solid rgba(255, 255, 255, 0.22);
  border-radius: 14px;
  line-height: 0;
  overflow: hidden;
}

.chat-log li.photo-only.mine { border-color: var(--accent); }

.chat-log li.photo-only .photo,
.chat-log li.photo-only .photo-wrap {
  border-radius: 12px;
}

.chat-log li.photo-only .at {
  position: absolute;
  right: 6px;
  bottom: 6px;
  margin: 0;
  padding: 2px 6px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.45);
  color: #fff;
  line-height: 1.3;
}

/* A picture, full size, over everything. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 32px;
  background: rgba(0, 0, 0, 0.85);
  cursor: zoom-out;
}

.lightbox[hidden] { display: none; }

.lightbox img {
  max-width: 100%;
  max-height: 100%;
  border-radius: 8px;
}
