:root {
  --ground: #0d131a;
  --surface: #131c25;
  --surface-2: #1a242f;
  --surface-3: #22303d;
  --line: #26333f;
  --line-soft: #1e2a35;
  --text: #dbe3ea;
  --dim: #94a3b0;
  --faint: #64737f;
  --brass: #c99a5b;
  --brass-soft: rgba(201, 154, 91, 0.14);
  --sage: #6fa88c;
  --oxblood: #c4605a;
  /* An explanatory aside, and nothing else. Deliberately not --brass (which means "you can click
     this") or --oxblood (which now means "this name is lying to you" - #67): a colour that already
     carries a meaning cannot also mean "by the way". Cooler than --faint so it separates from grey
     body text, and lighter, because --faint is 3.52:1 on the card and fails WCAG AA - this clears
     4.5:1, which a *security* warning like the "Remember me" note ought to. */
  --aside: #7d8fa0;
  --mono: ui-monospace, "Cascadia Mono", Consolas, "SF Mono", monospace;
  --sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  background: var(--ground);
  color: var(--text);
  font: 15px/1.6 var(--sans);
  -webkit-font-smoothing: antialiased;
}

.hidden { display: none !important; }

/* The browser's own rule for the `hidden` attribute is `[hidden] { display: none }` - a bare
   element selector, so ANY class rule with a `display` beats it. `.tray { display: flex }` on a
   `<div class="tray" hidden>` therefore shows the tray, forever, and nothing fails: the element is
   simply empty. That is how the drag-and-drop veil ended up painted over the whole chat and the
   "cannot scan files" warning ended up permanently on screen - both invisible to the tests, because
   neither one *breaks* anything, it just looks wrong.
   Stated once, here, so the next `hidden` element cannot repeat it. Same family as the duplicate
   `.msg` rule (ISSUES.md #8): a stylesheet with no scoping needs its baseline written down. */
[hidden] { display: none !important; }

/* ---------- auth screens ---------- */

.auth-wrap {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
}

.card {
  width: 100%;
  max-width: 460px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 30px;
}

/* The mark (#58). It used to be the character ◆ in a bordered box; it is now the real icon, so the
   box is gone and the artwork carries itself. Same file as the favicon - one source of truth. */
.seal {
  width: 34px;
  height: 34px;
  display: block;
  margin-bottom: 14px;
}

h1 { margin: 0 0 4px; font-size: 22px; letter-spacing: -0.01em; }
h2 { margin: 0 0 4px; font-size: 18px; }
.sub { margin: 0 0 24px; color: var(--dim); font-size: 14px; }

.tabs { display: flex; gap: 4px; margin-bottom: 22px; }
.tab {
  flex: 1;
  padding: 8px;
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 7px;
  color: var(--dim);
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
.tab[aria-selected="true"] { background: var(--brass-soft); border-color: var(--brass); color: var(--text); }
.tab:focus-visible, .btn:focus-visible, input:focus-visible { outline: 2px solid var(--brass); outline-offset: 2px; }

.field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 15px; }
/* Direct child only. A descendant selector here also hit the <label> around each person in the
   New server dialog, and rendered their names as BOB and CHRIS in tiny uppercase mono. Same shape of
   mistake as the duplicate .msg rule (ISSUES.md #8) - in a stylesheet with no scoping, be exact. */
.field > label {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}
input {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 10px 12px;
  color: var(--text);
  font: inherit;
  font-size: 15px;
  width: 100%;
  min-height: 44px;
}
input:focus { border-color: var(--brass); outline: none; }
input::placeholder { color: var(--faint); }
/* The app's first <select> (#85's retention picker). Same box as an input, deliberately - one field
   shape everywhere. `font: inherit` matters more here than on an input: a select left alone renders
   in the OS font and looks like it wandered in from another program. */
select {
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 10px 12px;
  color: var(--text);
  font: inherit;
  font-size: 15px;
  width: 100%;
  min-height: 44px;
}
select:focus { border-color: var(--brass); outline: none; }
/* A label only a screen reader needs: the <h3> above the field already says what it is, so a second
   visible label would be the same sentence twice. Not `display: none` - that hides it from the
   screen reader too, which is the entire point of having it. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}
/* A note *about* a field, never a label or a value - so it is italic and tinted, not just small.
   Size alone was not enough: it still read as part of the form (Robin, 14 July). */
.hint { font-size: 12px; color: var(--aside); font-style: italic; line-height: 1.45; }

.btn {
  width: 100%;
  background: var(--brass);
  color: #17110a;
  border: 0;
  border-radius: 6px;
  padding: 12px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  min-height: 46px;
}
.btn:hover { filter: brightness(1.08); }
.btn:disabled { opacity: 0.55; cursor: default; filter: none; }
.btn.ghost { background: transparent; border: 1px solid var(--line); color: var(--dim); font-weight: 500; }
.btn.danger { background: transparent; border: 1px solid rgba(196, 96, 90, 0.5); color: var(--oxblood); width: auto; padding: 7px 12px; min-height: 38px; font-size: 13px; }

.linkish {
  background: none;
  border: 0;
  color: var(--brass);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  padding: 0;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* The banner above the sign-in form. Do NOT call this .msg - that name belongs to a chat message
   row further down, which is a grid with a 34px avatar column. Two rules, one class name, and the
   later one wins: the banner's text ends up in the 34px column, one word per line. (ISSUES.md #8) */
.banner { border-radius: 6px; padding: 10px 12px; font-size: 13.5px; margin-bottom: 15px; }
.banner.error { background: rgba(196, 96, 90, 0.1); border: 1px solid rgba(196, 96, 90, 0.4); color: var(--text); }
.banner.work { background: var(--brass-soft); border: 1px solid var(--brass); color: var(--text); }

.warn {
  display: flex;
  gap: 10px;
  font-size: 13px;
  color: var(--dim);
  background: rgba(196, 96, 90, 0.08);
  border: 1px solid rgba(196, 96, 90, 0.3);
  border-radius: 6px;
  padding: 12px;
  margin-bottom: 18px;
}
.warn .mark { color: var(--oxblood); }

.recovery-code {
  background: var(--ground);
  border: 1px dashed var(--brass);
  border-radius: 6px;
  padding: 16px;
  font-family: var(--mono);
  font-size: 15px;
  letter-spacing: 0.06em;
  color: var(--brass);
  text-align: center;
  margin-bottom: 14px;
  user-select: all;
  word-break: break-all;
}

.checkline { display: flex; gap: 9px; align-items: flex-start; margin-bottom: 16px; font-size: 13.5px; color: var(--dim); }
.checkline input { width: auto; min-height: 0; margin-top: 3px; }
.checkline .hint { display: block; margin-top: 3px; } /* the warning under "Remember me" (#10) */

/* A setting that is a number rather than a switch - the two presence clocks (#31). The unit and the
   sentence are part of the label, not a note underneath: "5" means nothing without "minutes". */
.numline {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-bottom: 12px;
  font-size: 13.5px;
  color: var(--dim);
}
.numline input {
  width: 5.5em;
  min-height: 0;
  padding: 5px 8px;
  font-family: var(--mono);
}
.numline-unit { color: var(--faint); font-size: 12px; }

/* ---------- chat shell ---------- */

.chat {
  display: grid;
  grid-template-columns: 220px 1fr;
  height: 100vh;
  height: 100dvh; /* so the composer isn't under a phone's browser bar */
  position: relative;
}
/* The access panel is a real third column when there's room for it. Below 980px it stops being a
   column and floats over the chat instead - see the media query at the bottom. */
.chat.show-members { grid-template-columns: 220px 1fr 230px; }

.sidebar {
  background: var(--surface-2);
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  min-height: 0;
  /* The anchor your own menu hangs off (#31/#68). It opens upwards from the row at the bottom of
     this column, so it needs this to be the positioned ancestor - otherwise it escapes to the
     viewport and lands over the chat. */
  position: relative;
}
.brand {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line-soft);
}
.brand .seal { margin: 0; width: 24px; height: 24px; }
.brand-text { display: flex; flex-direction: column; line-height: 1.25; min-width: 0; }
.brand-name { font-weight: 600; font-size: 14px; }

/* The build, under the name. Same mono/faint pairing as every other bit of metadata in here
   (timestamps, the edited marker): present when you look for it, invisible when you are not. */
.brand-version {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--faint);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
a.brand-version:hover { color: var(--dim); text-decoration: underline; }

.nav { flex: 1; overflow-y: auto; padding: 14px 8px; display: flex; flex-direction: column; gap: 18px; }
.group { display: flex; flex-direction: column; gap: 1px; }
.group-label {
  display: flex;
  align-items: center;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--faint);
  padding: 0 8px 6px;
}
.add {
  margin-left: auto;
  background: transparent;
  border: 0;
  color: var(--faint);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 2px 4px;
  border-radius: 4px;
}
.add:hover { color: var(--brass); background: var(--surface-3); }

.chan {
  display: flex;
  align-items: center;
  /* No gap: the sigil sits tight against the name, so "#standup" and "@thomas" read as one token
     rather than two words. The things on the right push themselves away with margin-left: auto. */
  gap: 0;
  width: 100%;
  padding: 7px 8px;
  min-height: 38px;
  border: 0;
  background: transparent;
  color: var(--dim);
  font: inherit;
  font-size: 14px;
  border-radius: 5px;
  cursor: pointer;
  text-align: left;
  position: relative;
}
.chan:hover { background: var(--surface-3); color: var(--text); }
.chan[aria-selected="true"] { background: var(--brass-soft); color: var(--text); font-weight: 500; }
.chan[aria-selected="true"]::before {
  content: "";
  position: absolute;
  left: -8px; top: 6px; bottom: 6px;
  width: 3px;
  border-radius: 0 3px 3px 0;
  background: var(--brass);
}
/* The sigil: # on a server, @ on a person (#61). A fixed column, not a bare glyph - the mono @ is
   wider than the # and carries a descender, so left to itself it sits low and shunts the name right.
   Pinning the width means both sigils centre in the same box and every name starts at the same x. */
.chan .sigil {
  flex: none;
  width: 13px;
  text-align: center;
  font-family: var(--mono);
  font-size: 13px;
  line-height: 1.4;
  color: var(--faint);
}
.chan[aria-selected="true"] .sigil { color: var(--brass); }

/* #87's fold twirl on a server heading. A fixed column like the sigil, for the same reason: ▾ and ▸
   are different widths, so left to themselves they shunt the name sideways as you fold. Pinning the
   width means the name does not move when the caret flips.

   Unlike the star and the gear, it is NEVER hover-revealed. Those are controls you go looking for;
   this one is also a STATE - it is the only thing on screen saying a server has channels folded away
   under it, and a fold you cannot see is just a channel list that vanished. So it stays visible, and
   only brightens on hover. */
.chan .fold {
  flex: none;
  width: 12px;
  margin-left: -3px;
  text-align: center;
  /* Not smaller than this, and not fainter. The first cut drew it at 9px in --faint and a folded
     server was indistinguishable from an open one at a glance - the triangle read as a speck rather
     than a state. It is the ONLY thing on screen that says a server has channels hidden under it,
     so it has to be legible; --dim is the same weight as the row's own name. */
  font-size: 10px;
  line-height: 1.4;
  color: var(--dim);
  cursor: pointer;
  transition: color 0.12s ease;
}
.chan .fold:hover { color: var(--brass); }
.chan .fold:focus-visible { outline: 1px solid var(--brass); border-radius: 3px; }
/* A server with no channels: the column, without the triangle. Keeps every server name on the same
   x whether or not it has anything to fold. */
.chan .fold.blank { cursor: default; }
.chan .locked { margin-left: auto; font-size: 11px; opacity: 0.7; }
/* #85's clock on a channel row: this channel deletes on a timer, hover for how long. Always
   visible (unlike the gear) - it is information for everyone in the channel, not a control. Muted,
   so it reads as a property of the row rather than competing with the unread badge. */
.chan .row-clock { margin-left: auto; padding-left: 6px; font-size: 11px; opacity: 0.6; cursor: help; }
/* flex:1 makes the NAME absorb the row's free space, so every icon after it (badge, star, clock,
   gear) sits tight at the right edge in order. Before this, each icon carried its own
   margin-left:auto and flexbox SPLIT the free space between them - invisible while at most one icon
   was visible at a time, wrong the day a row had an unread badge and #85's always-on clock at once
   (the badge would float mid-row). The auto margins on the icons are inert now, kept for the rows
   where the name is absent. min-width:0 is what lets a long name actually ellipsize inside flex. */
.chan .cname { flex: 1; min-width: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A channel inside a server (#24c): indented one step so the server heading above it reads as the
   group it belongs to. The whole point of the nesting is visible here and nowhere else. */
.chan.channel-row { padding-left: 22px; }
/* `.chan.add-channel` lived here - the "+ channel" row under a server you manage. The button moved
   into the server's settings dialog (#84), so every .channel-row in the sidebar is a real channel
   now. Worth knowing if you are hunting for why a selector like `:not(.add-channel)` exists in an
   old test. */

/* A server whose name contains lookalike or invisible characters (ISSUES.md #67). It is the only
   thing in the sidebar we draw in the warning colour, and that is deliberate: the whole job of this
   mark is to break the resemblance between a name that is pretending and the one you trust.
   Note what it does NOT catch: "#genera1" with a digit one is plain ASCII and passes. This closes
   the Unicode class - the Cyrillic "a", the zero-width space - not human misreading. */
.chan.unsafe-name .cname { text-decoration: underline wavy var(--oxblood); text-underline-offset: 3px; }
.chan .warn,
.pane-title .warn { margin-left: auto; font-size: 12px; color: var(--oxblood); cursor: help; }
.pane-title .warn { margin-left: 6px; }

/* Voice channels (#77). A voice channel is a call, not a text stream - it wears a speaker for a
   sigil, and when you are in its call the row is tinted so you can see at a glance where your
   microphone is live. */
.chan.voice-row .sigil { font-size: 12px; } /* the 🔊 glyph runs large next to a mono # */
.chan.voice-row.in-call {
  background: var(--brass-soft);
  color: var(--text);
}
.chan.voice-row.in-call .sigil { color: var(--brass); }

/* The people in a voice call, listed under the channel's row - this is "see who is in the call in
   the left menu, under the channel name". Indented past the channel's own indent so it reads as
   belonging to the row above it, and quiet, because it is a live status and not a control. */
.voice-occupants {
  padding: 2px 8px 4px 34px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.voice-occupant {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--dim);
}
.voice-occupant .voice-glyph { font-size: 10px; opacity: 0.8; }
.voice-occupant .voice-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The "you are in a call" bar above your own row. Only shown while in a call; it names the channel
   and carries Disconnect - the one always-visible sign the microphone is on. */
.voice-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  margin: 0 8px 6px;
  border-radius: 8px;
  background: var(--surface-3);
  border: 1px solid var(--line);
}
.voice-bar[hidden] { display: none; }
.voice-bar .voice-bar-label {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.voice-bar .voice-leave {
  flex: none;
  padding: 4px 10px;
  font-size: 12px;
}

/* Unread badge on a channel or DM (#39). A red pill with the count, or a bare dot when the count is
   unknown (the number is a subtraction of two totals the server gave us, and a deletion can zero it
   out for a moment). margin-left:auto pushes it - and any star or gear after it - to the right edge.
   The count never carries a sender or a word of the text: it is a number and nothing else. */
.chan .unread {
  margin-left: auto;
  flex: none;
  min-width: 17px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--oxblood);
  color: #fff;
  font-family: var(--mono);
  font-size: 10px;
  line-height: 16px;
  text-align: center;
}
.chan .unread.dot {
  min-width: 0;
  width: 8px;
  height: 8px;
  padding: 0;
  align-self: center;
}
/* A row with something new reads louder - the name goes from dim to full strength and a touch
   heavier, so you can pick it out of the list without hunting for the small badge. */
.chan.has-unread .cname { color: var(--text); font-weight: 600; }

/* The "new messages" line in the stream (#39): a rule across the conversation with a small label,
   drawn only where you left off in a chat that has since gone quiet. */
.new-divider {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 10px 4px;
  color: var(--oxblood);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.new-divider::before,
.new-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--oxblood);
  opacity: 0.5;
}

.me {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 11px 14px;
  border: 0;
  border-top: 1px solid var(--line-soft);
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  width: 100%;
}
.me:hover { background: var(--surface-3); }
.me-text { min-width: 0; flex: 1; text-align: left; }
.me-text small {
  display: block;
  color: var(--faint);
  font-size: 11px;
  font-family: var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The gear that says this row does something (#68). Dim until you hover the row, like the gear on a
   server row (rowGear) - the same affordance, so it reads the same way. It is not a second button:
   the whole row is the button, and this is what admits it. */
.me-gear {
  flex: none;
  margin-left: 4px;
  color: var(--faint);
  font-size: 13px;
  line-height: 1;
}
.me:hover .me-gear { color: var(--dim); }

/* ---------------------------------------------------------------------------
   presence (#30/#31) - the dot on an avatar, and your own menu

   These four colours are their own vocabulary, and that is why they are new variables rather than
   reuses of --brass and --oxblood. The stylesheet already rules that a colour carrying a meaning
   cannot also mean something else (--brass is "you can click this"; --oxblood is "this name is
   lying to you", #67), and green/amber/red on a dot is a meaning every chat app in existence has
   already taught everybody. So: separate names, no overloading.

   Invisible and offline are THE SAME GREY, deliberately and load-bearingly. They must be
   indistinguishable to everybody else or invisible is not invisible - and there is nothing to get
   wrong here anyway, because the server turns invisible into offline before it broadcasts. The
   greys meet in the CSS as well only so that your OWN dot (drawn from your choice) looks to you
   exactly like what your friends are seeing.
   --------------------------------------------------------------------------- */
:root {
  --dot-online: var(--sage);
  --dot-away: #d3a04a;
  --dot-busy: var(--oxblood);
  --dot-grey: #5d6b77;
}

.avatar-stack { position: relative; display: inline-flex; flex: none; }
.presence {
  position: absolute;
  right: -2px;
  bottom: -2px;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--dot-grey);
  /* A dark ring so the dot reads as separate from the avatar under it. A shadow rather than a
     border because these rows sit on three different backgrounds (the sidebar, the members column,
     the people dialog) and a solid ring would have to pick one of them and be wrong on the others. */
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.5);
}
.presence.is-online { background: var(--dot-online); }
.presence.is-away { background: var(--dot-away); }
.presence.is-busy { background: var(--dot-busy); }
.presence.is-invisible,
.presence.is-offline { background: var(--dot-grey); }

/* The status picker (#31) and the two items that were already yours (#68). Anchored to the row
   itself - `bottom: 100%` means "sit on top of it" - rather than positioned in JS or nudged up by a
   pixel count. The first version guessed an offset, and the guess was wrong the moment the row grew
   a third line for your status: the menu sat over your own name. A number that has to be re-derived
   every time the thing above it changes is a bug with a delay on it. */
.me-wrap { position: relative; }
.me-menu {
  position: absolute;
  bottom: 100%;
  left: 8px;
  right: 8px;
  z-index: 7;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-3);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4);
}
.me-menu hr { width: 100%; margin: 4px 0; border: 0; border-top: 1px solid var(--line); }
.me-menu-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border: none;
  border-radius: 4px;
  background: none;
  color: var(--text);
  font-family: var(--sans);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.me-menu-item:hover { background: var(--surface-2); }
/* The dots in the menu are the only ones not sitting on an avatar, so they are laid out inline
   instead of pinned to a corner. */
.me-menu-item .presence { position: static; flex: none; }
.me-menu-tick { margin-left: auto; color: var(--brass); font-size: 12px; }
.me-menu-note {
  margin: 4px 6px 2px;
  color: var(--aside);
  font-size: 11px;
  line-height: 1.45;
}

.avatar {
  width: 26px; height: 26px;
  border-radius: 6px;
  display: grid;
  place-items: center;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  color: #17110a;
  flex: none;
}

/* ---------- message pane ---------- */

/* position: relative so the drag-and-drop veil can cover exactly this pane and nothing else. */
.pane { display: flex; flex-direction: column; min-width: 0; min-height: 0; position: relative; }
.pane-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 18px;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
}
.pane-title { font-weight: 600; font-size: 15px; display: flex; align-items: center; gap: 0; min-width: 0; }
/* Tight against the name, like the sidebar: "#standup", one token. */
.pane-title .sigil { margin-right: 2px; font-family: var(--mono); color: var(--faint); }
/* Where the "🔒 encrypted" chip used to be. It said nothing a user could check - the padlock a
   browser draws is about TLS, and this one was about nothing at all - so it went (ISSUES.md #29).
   The composer footnote makes the same claim and is actually true of the thing you are typing. */
.chan-search {
  margin-left: auto;
  width: 180px;
  flex: none;
  font-size: 12px;
  padding: 5px 9px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 6px;
}
.chan-search:focus { outline: none; border-color: var(--brass); }
.chan-search:disabled { opacity: 0.4; }

.search-note {
  margin: 0;
  padding: 7px 18px;
  font-size: 11px;
  color: var(--faint);
  background: var(--surface-2);
  border-bottom: 1px solid var(--line);
}

.side-search {
  width: calc(100% - 20px);
  margin: 0 10px 10px;
  font-size: 12px;
  padding: 6px 9px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 6px;
}
.side-search:focus { outline: none; border-color: var(--brass); }

.stream {
  flex: 1;
  overflow-y: auto;
  padding: 20px 18px 10px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
.empty { margin: auto; text-align: center; color: var(--faint); font-size: 14px; max-width: 40ch; }

.msg { display: grid; grid-template-columns: 34px 1fr; gap: 11px; position: relative; }
/* Hovering a message lifts its background a touch, so you can see which row the ⋯ and the react
   button belong to. The negative margins + matching padding let the tint bleed into the gutter
   without nudging the text - the grid keeps its place. */
.msg:hover {
  background: var(--surface-2);
  border-radius: 6px;
  margin: -4px -8px;
  padding: 4px 8px;
}
.msg-body { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.msg-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
/* --name is set per row by authorColour() (#66): hashed from the username in a channel, and the
   fixed you-are-blue/they-are-green pair in a DM. The fallback matters - a row drawn before the
   colour is set still reads as normal text rather than vanishing. Note this is a custom property
   and not an inline colour ON PURPOSE: .msg.editing .author below has to be able to override it,
   and an inline style.color would outrank it. */
.msg .author { font-weight: 600; font-size: 14px; color: var(--name, var(--text)); }
.msg .time { font-family: var(--mono); font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums; }
.msg .from-device,
.msg .from-place {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--faint);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 1px 7px;
}
/* Where they sent it from (#21/#22). The same pill as the device chip, because it is the same kind
   of fact: something the sender sealed inside the message, which the server never saw. It is a WORD
   ("Sweden"), never a flag emoji - Windows ships no country-flag font, so Chrome and Edge would draw
   the Swedish flag as the letters "SE"; and a flag looks authoritative in a way a timezone has not
   earned, since Europe/Zurich covers three countries and a VPN flies the wrong one entirely.
   `cursor: help` because the title says exactly that, and it is worth reading once. */
.msg .from-place { cursor: help; }
/* No `white-space: pre-wrap` any more: the markdown renderer (#18) owns the line breaks now - a
   newline you typed becomes a <br>, a blank line becomes a new <p>. Leaving pre-wrap on would
   double every gap. */
.msg .text { font-size: 14.5px; overflow-wrap: anywhere; }
.msg .text a { color: var(--brass); text-decoration: underline; text-underline-offset: 2px; }
.msg.undecryptable .text { color: var(--faint); font-style: italic; }
.msg .edited { font-family: var(--mono); font-size: 10px; color: var(--faint); cursor: help; }

/* markdown (#18). Rendered from the source text every time we draw a message, which is why "copy
   source" needs no new storage: what you typed IS the message body inside the ciphertext. */

.msg .text p { margin: 0; }
.msg .text p + p,
.msg .text > * + * { margin-top: 6px; }
.msg .text strong { font-weight: 650; }
.msg .text del { color: var(--faint); }
.msg .text ul,
.msg .text ol { margin: 0; padding-left: 20px; }
.msg .text li { margin: 1px 0; }

.msg .text blockquote {
  margin: 0;
  padding: 2px 0 2px 10px;
  border-left: 2px solid var(--line);
  color: var(--muted);
}

.msg .text code {
  font-family: var(--mono);
  font-size: 12.5px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0 4px;
}

/* A fenced block: monospace, whitespace kept, and NO colours - the honest 90%. Every syntax
   highlighter builds HTML strings out of attacker-chosen text, which is the one thing #18 says we
   will never do. */
.code-block { position: relative; }
.code-block pre {
  margin: 0;
  padding: 9px 11px;
  overflow-x: auto;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 5px;
}
/* Specificity, and it is not a nitpick: `.msg .text code` above out-ranks a plain `.code-block pre
   code`, so the inline-code chip - background, border, padding - was being painted on every line
   INSIDE a fenced block as well, one box per line. Hence `.msg .text` here too. */
.msg .text .code-block pre code {
  display: block;
  background: none;
  border: 0;
  padding: 0;
  white-space: pre;
}
.code-block .copy {
  position: absolute;
  top: 5px;
  right: 5px;
  opacity: 0;
  padding: 2px 7px;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--muted);
  background: var(--surface-3);
  border: 1px solid var(--line);
  border-radius: 3px;
  cursor: pointer;
}
.code-block:hover .copy,
.code-block .copy:focus-visible { opacity: 1; }

/* The real destination of a link whose label hides it. `[your bank](http://evil.example)` is a
   phishing footgun markdown hands you by design, so the host is printed, not just hinted at on a
   hover that a phone does not have. */
.link-host { font-family: var(--mono); font-size: 11px; color: var(--faint); }

/* Cosmetic only, and it must not be mistaken for a security feature: the text is in the DOM, so a
   spoiler hides it from a glance, not from anybody who looks. */
.spoiler {
  background: var(--surface-3);
  color: transparent;
  border-radius: 3px;
  cursor: pointer;
}
.spoiler.revealed { background: none; color: inherit; cursor: auto; }
.spoiler * { visibility: hidden; }
.spoiler.revealed * { visibility: visible; }

/* what you can do to a message: copy, edit, delete, report */

.msg-more {
  position: absolute;
  top: 0; /* sits inside the row's hover tint (which pads 4px up), not poking above it */
  right: 0;
  opacity: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--surface-2);
  color: var(--dim);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.msg:hover .msg-more,
.msg-more:focus-visible { opacity: 1; }
.msg-more:hover { background: var(--surface-3); color: var(--text); }

.msg-actions {
  position: absolute;
  top: 26px; /* clears the taller .msg-more above it */
  right: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 200px;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-3);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4);
}
.msg-actions button {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border: none;
  border-radius: 4px;
  background: none;
  color: var(--text);
  font-family: var(--sans);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
/* A fixed column for the glyph, so the labels start at the same x however wide the icon renders -
   a ⧉ is wider than a ✎, and fonts disagree about both. Monochrome text glyphs, not emoji: they
   inherit currentColor, so a danger item goes oxblood icon and label together. */
.msg-actions .act-icon {
  flex: none;
  width: 16px;
  text-align: center;
  font-size: 13px;
  line-height: 1;
  color: var(--dim);
}
.msg-actions button:hover { background: var(--surface-2); }
.msg-actions button:hover .act-icon { color: var(--text); }
.msg-actions button.danger { color: var(--oxblood); }
.msg-actions button.danger .act-icon { color: var(--oxblood); }

/* reactions (#34) - the pills under a message, and the panel you pick one from.

   The count on a pill was worked out in this browser, after decrypting. The server cannot produce
   it: it holds sealed bytes and does not know a 👍 from a 😂. */

/* Same box as .msg-more - the two sit side by side, so if one grows and the other doesn't, the pair
   looks broken. */
.msg-react {
  position: absolute;
  top: 0; /* matches .msg-more - inside the hover tint, not above it */
  right: 34px; /* immediately left of .msg-more */
  opacity: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 26px;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--surface-2);
  color: var(--dim);
  font-family: var(--sans);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.msg:hover .msg-react,
.msg-react:focus-visible { opacity: 1; }
.msg-react:hover { background: var(--surface-3); color: var(--text); }

.reactions { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 2px; }
.reactions[hidden] { display: none; }

.reaction {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 1px 7px;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--surface-2);
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1.6;
  cursor: pointer;
}
.reaction:hover { background: var(--surface-3); }
/* Yours, so a second click takes it back. */
.reaction.mine { border-color: var(--brass); background: var(--surface-3); }
.reaction .glyph { font-size: 13px; }
.reaction .count { color: var(--dim); font-variant-numeric: tabular-nums; }
.reaction.mine .count { color: var(--text); }

/* One panel, two anchors: the ☺ on a message row, and the composer's 😊 (#62). */
.emoji-panel {
  position: absolute;
  top: 26px; /* clears the taller .msg-react above it */
  right: 0;
  z-index: 6; /* over .msg-actions, which it can open on top of */
  width: 214px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-3);
  box-shadow: 0 8px 22px rgba(0, 0, 0, 0.4);
}
/* Opened on a message with no room below it - the last one in the stream, whose "below" is behind
   the composer (#72). JS adds this after measuring; both popups flip the same way, so the shadow
   turns over with them. */
.msg-actions.drops-up,
.emoji-panel.drops-up {
  top: auto;
  bottom: 100%;
  margin-bottom: 4px;
  box-shadow: 0 -8px 22px rgba(0, 0, 0, 0.4);
}

.emoji-row { display: grid; grid-template-columns: repeat(6, 1fr); gap: 2px; }
.emoji-choice {
  padding: 5px 0;
  border: none;
  border-radius: 4px;
  background: none;
  font-size: 17px;
  line-height: 1.2;
  cursor: pointer;
}
.emoji-choice:hover,
.emoji-choice:focus-visible { background: var(--surface-2); }

/* the super admin's report inbox (#32) */

.me .badge {
  margin-left: auto;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--oxblood);
  color: #fff;
  font-family: var(--mono);
  font-size: 11px;
  text-align: center;
}

/* ---------- invitations you have not answered (#25) ----------
   Above your name, so it is the last thing between the server list and you - an invitation is not a
   server, and it must not sit in the list of servers pretending to be one. */
.invites {
  border-top: 1px solid var(--line-soft);
  padding: 12px 8px 8px;
  background: var(--brass-soft);
}
.invites .group-label { padding-bottom: 8px; }
.invites .badge {
  margin-left: auto;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--brass);
  color: #17110a;
  font-size: 10px;
  text-align: center;
}

.invitation {
  padding: 8px;
  border-radius: 6px;
  margin-bottom: 6px;
  background: var(--surface);
  border: 1px solid var(--line);
}
.invitation:last-child { margin-bottom: 0; }
.invitation .who { display: block; min-width: 0; }
.invitation .name {
  display: block;
  font-size: 13.5px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.invitation small { display: block; font-size: 11px; color: var(--faint); margin-top: 1px; }

.invitation-actions { display: flex; gap: 6px; margin-top: 8px; }
/* Not `width: 100%` like the big .btn - two of these sit side by side. */
.btn.tiny {
  width: auto;
  flex: 1;
  min-height: 30px;
  padding: 5px 8px;
  font-size: 12px;
  font-weight: 500;
}
.report {
  margin-bottom: 12px;
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface-2);
  text-align: left;
}
.report-head {
  display: flex;
  align-items: baseline;
  gap: 8px;
  margin-bottom: 8px;
  font-size: 13px;
  font-weight: 600;
}
.report-head .time { margin-left: auto; font-family: var(--mono); font-size: 10px; color: var(--faint); }
.report-text {
  padding: 9px 11px;
  border-left: 2px solid var(--brass);
  border-radius: 3px;
  background: var(--ground);
  font-size: 14px;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.report-text.undecryptable { color: var(--faint); font-style: italic; border-left-color: var(--line); }
.report .note { margin: 8px 0; }
.report .btn { width: auto; padding: 5px 12px; font-size: 12px; }

/* Server status (#57) - six counts. auto-fit rather than a fixed column count, so it is three
   across on a laptop and two on a phone without a media query. */
.stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
  gap: 8px;
  text-align: left;
}
.stat {
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--surface-2);
}
.stat-n { font-family: var(--mono); font-size: 26px; line-height: 1.2; color: var(--brass); }
.stat-label { margin-top: 2px; font-size: 13px; font-weight: 600; }
.stat-hint { margin-top: 2px; font-size: 11.5px; line-height: 1.4; color: var(--faint); }

.composer { padding: 12px 18px 16px; }
.composer-inner {
  /* The emoji panel (#34) hangs off this box when the composer's 😊 opens it (#62), the same way it
     hangs off a message row when the ⋯ does. Without this it would position against the page. */
  position: relative;
  display: flex;
  /* flex-end, not center: once the textarea grows to several lines, centred buttons float in the
     middle of the box. The paperclip and the send arrow should sit on the bottom line, next to the
     line you are actually typing. */
  align-items: flex-end;
  gap: 9px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 6px 6px 6px 12px;
}
.composer-inner:focus-within { border-color: var(--brass); }
.composer textarea {
  flex: 1;
  background: transparent;
  border: 0;
  padding: 0;
  /* The height is set in JS (growComposer) as you type. These are the floor and the ceiling. */
  min-height: 34px;
  max-height: 180px;
  line-height: 1.45;
  padding-top: 7px;
  resize: none;
  overflow-y: auto;
  font: inherit;
  color: inherit;
}
.composer textarea:focus { outline: none; }
.send {
  width: 38px; height: 38px;
  border-radius: 6px;
  border: 0;
  background: var(--brass);
  color: #17110a;
  font-size: 16px;
  cursor: pointer;
  flex: none;
}
/* While editing it says "Save", which does not fit in a square meant for one arrow. */
.send.saving {
  width: auto;
  padding: 0 14px;
  font-size: 13px;
  font-weight: 600;
  font-family: var(--sans);
}
.send:disabled { opacity: 0.4; cursor: default; }

/* The paperclip sits on the right, next to Send - where a chat app puts it, and where your hand
   already is. The smiley (#62) sits immediately to its left, which is where Robin asked for it -
   the pair stays on the right rather than moving to the far side of the box. */
.composer-icon {
  width: 34px; height: 34px;
  border-radius: 6px;
  border: 0;
  background: transparent;
  color: var(--dim);
  cursor: pointer;
  flex: none;
  /* The SVG is smaller than the hit area on purpose: 34px is a comfortable target, 19px is a
     comfortable icon, and centring one in the other is what grid does in one line. */
  display: grid;
  place-items: center;
  padding: 0;
}
.composer-icon svg { width: 19px; height: 19px; display: block; }
.composer-icon:hover:not(:disabled) { background: var(--surface-3); color: var(--text); }
.composer-icon:disabled { opacity: 0.35; cursor: default; }

.composer-note {
  margin: 7px 2px 0;
  font-size: 11px;
  color: var(--faint);
  display: flex;
  gap: 10px;
}
.composer-keys { margin-left: auto; white-space: nowrap; }

/* ---------- muted ----------
   Red, and above the composer, because a greyed-out textbox with no explanation just reads as a
   broken app. It sits where the message you were about to type would have gone. */

.muted-banner {
  margin-bottom: 8px;
  padding: 9px 12px;
  border: 1px solid rgba(196, 96, 90, 0.5);
  border-left: 3px solid var(--oxblood);
  border-radius: 0 6px 6px 0;
  background: rgba(196, 96, 90, 0.1);
  color: var(--text);
  font-size: 12.5px;
  line-height: 1.45;
}

/* ---------- editing (#51) ----------
   The strip is what makes the composer stop looking like a composer. Same colour as the mark on the
   message row, so the two read as one thing: this box is pointed at that message. */

.editing-strip {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 8px;
  padding: 6px 8px 6px 11px;
  border-left: 2px solid var(--brass);
  border-radius: 0 6px 6px 0;
  background: var(--brass-soft);
  font-size: 12px;
}
.editing-what { color: var(--brass); font-weight: 600; white-space: nowrap; }
.editing-limit { color: var(--muted); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.editing-strip .x {
  margin-left: auto;
  padding: 2px 6px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--faint);
  cursor: pointer;
  flex: none;
}
.editing-strip .x:hover { color: var(--text); background: var(--surface-3); }

/* And the message itself, marked where it lives - in the scrollback, where your eye already is.
   Robin's note, and he was right: a faint tint is not a mark. It gets a real background, a thick
   brass bar, and a label, because "which message am I about to overwrite" is not a question anyone
   should have to squint at. */
.msg.editing {
  border-left: 3px solid var(--brass);
  margin: 0 -8px 0 -14px;
  padding: 6px 8px 6px 11px;
  background: var(--brass-soft);
  border-radius: 0 8px 8px 0;
}
.msg.editing .author { color: var(--brass); }

/* "editing" spelled out on the row, so the mark says what it means rather than just being a colour -
   which is also the only version of this that works if you cannot see the difference between brass
   and grey. */
.msg.editing .msg-head::after {
  content: "editing";
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--brass);
  border: 1px solid var(--brass);
  border-radius: 999px;
  padding: 1px 7px;
}

/* ---------- attachments (#18) ---------- */

/* Staged: encrypted and uploaded, but not sent. */
.tray {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 9px;
}
.tray-item {
  display: flex;
  align-items: center;
  gap: 7px;
  max-width: 240px;
  padding: 5px 6px 5px 5px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 7px;
}
.tray-item.busy { opacity: 0.55; }
.tray-item img {
  width: 34px; height: 34px;
  object-fit: cover;
  border-radius: 4px;
  flex: none;
}
.tray-item .file-icon { font-size: 18px; width: 34px; text-align: center; flex: none; }
.tray-name {
  font-size: 12px;
  color: var(--dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tray-x {
  border: 0;
  background: transparent;
  color: var(--faint);
  cursor: pointer;
  font-size: 12px;
  padding: 2px 4px;
  flex: none;
}
.tray-x:hover { color: var(--oxblood); }

/* Sent: in the message stream. */
.attachments {
  display: flex;
  flex-wrap: wrap;
  /* Without this, a file tile next to an image tile stretches to the image's full height and reads
     as a big empty box with a filename floating in the middle of it. */
  align-items: flex-start;
  gap: 8px;
  margin-top: 6px;
}
.att {
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--surface-2);
  overflow: hidden;
}
.att-image {
  display: flex;
  flex-direction: column;
  max-width: 320px;
}
.att-image img {
  display: block;
  max-width: 100%;
  max-height: 260px;
  min-height: 40px;
  object-fit: cover;
  cursor: zoom-in;
  background: var(--surface-3);
}
.att-image .att-name {
  padding: 5px 9px;
  font-size: 11px;
  color: var(--faint);
  border-top: 1px solid var(--line-soft);
}
.att-file {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
}
.att-file .file-icon { font-size: 20px; }
.att-file .att-name { font-size: 13px; color: var(--dim); }
.att-file .ghost {
  border: 1px solid var(--line);
  background: transparent;
  color: var(--dim);
  border-radius: 5px;
  padding: 4px 10px;
  font-size: 12px;
  cursor: pointer;
}
.att-file .ghost:hover { color: var(--text); border-color: var(--brass); }
/* An image whose blob has aged out, or that we hold no key for. Say so; never draw a broken tile. */
.att.gone { border-style: dashed; opacity: 0.7; }
.att.gone img { display: none; }
.att.gone .att-name { color: var(--faint); border-top: 0; padding: 9px 11px; }

.drop-veil {
  position: absolute;
  inset: 0;
  z-index: 30;
  display: grid;
  place-items: center;
  background: rgba(13, 19, 26, 0.86);
  border: 2px dashed var(--brass);
  border-radius: 10px;
  margin: 8px;
  pointer-events: none;
}
.drop-veil-inner { text-align: center; color: var(--dim); }
.drop-veil .drop-icon { width: 40px; height: 40px; color: var(--brass); }
.drop-veil p { margin: 6px 0 0; }
.drop-veil .small { font-size: 12px; color: var(--faint); }

.lightbox {
  border: 0;
  background: transparent;
  padding: 0;
  max-width: 92vw;
  max-height: 92vh;
}
.lightbox::backdrop { background: rgba(0, 0, 0, 0.8); }
.lightbox img {
  display: block;
  max-width: 92vw;
  max-height: 82vh;
  border-radius: 8px;
}
.lightbox-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 9px;
  color: var(--dim);
  font-size: 12px;
}
.lightbox-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lightbox-bar .ghost {
  border: 1px solid var(--line);
  background: var(--surface);
  color: var(--dim);
  border-radius: 5px;
  padding: 5px 12px;
  font-size: 12px;
  cursor: pointer;
  text-decoration: none;
}
.lightbox-bar .ghost:hover { color: var(--text); border-color: var(--brass); }

.scrim { display: none; }
.hamburger { display: none; }

/* ---------- who has access ---------- */

.who-btn {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 7px;
  color: var(--dim);
  font: inherit;
  font-size: 13px;
  padding: 6px 11px;
  min-height: 34px;
  cursor: pointer;
  flex: none;
  white-space: nowrap;
}
.who-btn:hover { border-color: var(--brass); color: var(--brass); }
.chat.show-members .who-btn { border-color: var(--brass); color: var(--brass); background: var(--brass-soft); }
/* On a wide screen the word is the button and the glyph is not drawn; a phone flips this (#107). */
.who-icon { display: none; width: 17px; height: 17px; }

/* Hidden unless .chat has .show-members - see the grid columns above. */
.members { display: none; }
.chat.show-members .members {
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--surface-2);
  border-left: 1px solid var(--line);
}
.members-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 15px 16px 10px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--faint);
}
.members-head .count {
  margin-left: auto;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
}
.member-list { flex: 1; overflow-y: auto; padding: 0 8px; }
.member {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 8px;
  border-radius: 5px;
  min-height: 40px;
}
.member .who { min-width: 0; }
.member .name {
  display: block;
  font-size: 14px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* Somebody the server lists as a member who has never been handed the key. They see that this server
   exists and nothing in it - say so plainly rather than implying they can read along. */
.member.waiting .avatar { opacity: 0.45; }
.member.waiting .name { color: var(--dim); }

/* Invited, and has not answered (#25). Greyed like `waiting`, and for a related reason: neither of
   them can read the server. The difference is who is waiting on whom - `waiting` is us owing them a
   key, `pending` is them owing us an answer. */
.member.pending .avatar { opacity: 0.45; }
.member.pending .name { color: var(--dim); }
.member .state {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--brass);
  cursor: help;
}
.members-note { padding: 12px 16px 16px; margin: 0; border-top: 1px solid var(--line-soft); }

/* ---------- server settings dialog (Robin, 15 July) ----------
   A modal, styled like the admin panels. Everything a server admin runs a server with, in one
   place, instead of scattered through the "People" side panel. */
.cs-section { margin-top: 18px; }
.cs-section:first-of-type { margin-top: 8px; }
.cs-section h3 {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 10px;
  border-top: 1px solid var(--line-soft);
  padding-top: 14px;
}
.cs-section:first-of-type h3 { border-top: 0; padding-top: 0; }
.cs-section h3 .count { color: var(--faint); }

.cs-rename { display: flex; gap: 8px; }
.cs-rename input { flex: 1; }
.cs-rename .btn { width: auto; flex: none; }

.cs-buttons { display: flex; flex-wrap: wrap; gap: 8px; }
.cs-buttons .btn { width: auto; flex: 1; min-width: 140px; }

.cs-section .checkline { margin-bottom: 8px; font-size: 13.5px; }
.cs-section .checkline:last-child { margin-bottom: 0; }

#csMemberList .member,
#csBanList .member { padding: 8px 6px; }

/* ---------- moderating a server ---------- */

.member-role,
.member-timeout {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
}
.member-role {
  color: var(--faint);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* This one WRAPS, and the row grows to fit it. It used to be clipped like the role above, which
   turned "muted until 14/07/2026, 18:32:00" into "muted until 14/07/20..." in a pane this narrow -
   the one part of the line you actually needed was the part that got cut. A role is one short word
   and can afford an ellipsis; a date cannot. The row is centred either way, so a note that runs to
   two lines just makes the row taller. */
.member-timeout { color: var(--brass); line-height: 1.4; }

.member-more {
  margin-left: auto;
  flex: none;
  padding: 2px 7px;
  border: 1px solid transparent;
  border-radius: 5px;
  background: none;
  color: var(--faint);
  font-family: var(--sans);
  font-size: 12px;
  cursor: pointer;
}
.member:hover .member-more { border-color: var(--line); color: var(--text); }
.member-more:hover { background: var(--surface-3); color: var(--text); }

.member-actions {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 2px 8px 8px;
  padding: 6px;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: var(--surface-3);
}
.member-actions button {
  padding: 7px 9px;
  border: none;
  border-radius: 4px;
  background: none;
  color: var(--text);
  font-family: var(--sans);
  font-size: 13px;
  text-align: left;
  cursor: pointer;
}
.member-actions button:hover { background: var(--surface-2); }
.member-actions button.danger { color: var(--oxblood); }
.member-actions .note { margin: 6px 3px 2px; font-size: 11px; line-height: 1.45; }

.member.banned .name { color: var(--dim); text-decoration: line-through; }

/* ---------- dialogs: people, new server ---------- */

.browse {
  margin-top: 6px;
  width: 100%;
  text-align: left;
  padding: 7px 8px;
  background: transparent;
  border: 1px dashed var(--line);
  border-radius: 5px;
  color: var(--faint);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  min-height: 38px;
}
.browse:hover { color: var(--brass); border-color: var(--brass); }

.modal {
  width: min(440px, calc(100vw - 32px));
  border: 1px solid var(--line);
  border-radius: 12px;
  background: var(--surface);
  color: var(--text);
  padding: 22px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.modal::backdrop { background: rgba(0, 0, 0, 0.55); }
.modal-head { display: flex; align-items: center; margin-bottom: 4px; }
.modal-head h2 { margin: 0; }
.modal .x {
  margin-left: auto;
  background: transparent;
  border: 0;
  color: var(--faint);
  font-size: 15px;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 5px;
}
.modal .x:hover { color: var(--text); background: var(--surface-3); }
.modal .sub { margin-bottom: 14px; }
.modal .btn { margin-top: 4px; }

/* the quick switcher (Ctrl+K) - a filter over names this browser decrypted, and nothing else */

.switcher {
  width: min(520px, calc(100vw - 32px));
  padding: 14px;
  margin-top: 12vh; /* up where your eyes are, not floating in the middle of the screen */
}
.switcher input { font-size: 15px; padding: 10px 12px; }
.switcher .note { margin-top: 10px; text-align: center; }

.switcher-list {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 44vh;
  overflow-y: auto;
  margin-top: 10px;
}
.switcher-list .note { margin: 10px 0; text-align: center; }

.switcher-row {
  display: flex;
  align-items: center;
  gap: 0; /* the sigil sits tight against the name, as it does in the sidebar */
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: inherit;
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}
.switcher-row:hover { background: var(--surface-2); }
.switcher-row[aria-selected="true"] { background: var(--surface-3); color: var(--brass); }
/* Same fixed column as the sidebar, so # and @ line up down the list. */
.switcher-row .sigil {
  flex: none;
  width: 13px;
  text-align: center;
  color: var(--faint);
  font-family: var(--mono);
}
/* The server a channel lives in (#99): pushed to the right edge and faint, like the sidebar's
   clock - metadata you can read, not a name competing with the one you are scanning for. It
   yields to the channel name rather than wrapping the row. */
.switcher-row .where {
  margin-left: auto;
  padding-left: 12px;
  font-size: 12px;
  color: var(--faint);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.people {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 46vh;
  overflow-y: auto;
  margin-top: 10px;
  border: 1px solid var(--line-soft);
  border-radius: 8px;
  padding: 6px;
}
.person {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px;
  border-radius: 6px;
  min-height: 44px;
}
.person:hover { background: var(--surface-2); }
.person .who { min-width: 0; }
.person .name { font-weight: 500; display: block; }
.person .seen { display: block; font-family: var(--mono); font-size: 10.5px; color: var(--faint); }
.person .spacer { margin-left: auto; }
.person .star {
  background: transparent;
  border: 0;
  color: var(--faint);
  font-size: 16px;
  cursor: pointer;
  padding: 4px 6px;
  border-radius: 5px;
  line-height: 1;
}
.person .star:hover { background: var(--surface-3); }
.person .star.on { color: var(--brass); }
.person .msg-btn {
  background: transparent;
  border: 1px solid var(--line);
  border-radius: 6px;
  color: var(--dim);
  font: inherit;
  font-size: 13px;
  padding: 6px 11px;
  cursor: pointer;
  min-height: 34px;
  white-space: nowrap;
}
.person .msg-btn:hover { border-color: var(--brass); color: var(--brass); }
.person input[type="checkbox"] { width: auto; min-height: 0; }
.person label { display: flex; align-items: center; gap: 10px; flex: 1; cursor: pointer; min-width: 0; }
.person.self { opacity: 0.55; }
/* The star on a Friends row (#16/#28). It used to be a ★ you could not click, which meant the only
   way to favourite somebody was to open a different list - so nobody ever did.

   A starred friend's ★ is always on, because it is telling you something. An unstarred ☆ appears on
   hover, so four rows of empty outlines don't compete with the names. On a touchscreen there IS no
   hover, so the ☆ is simply always there - a hover-only control does not exist on a phone, and that
   is the whole reason for the media query at the bottom of this block. */
.chan .fav {
  margin-left: auto;
  padding: 0 2px 0 6px;
  color: var(--faint);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease;
}
.chan .fav.on { color: var(--brass); opacity: 1; }
.chan:hover .fav,
.chan .fav:focus-visible { opacity: 1; }
.chan .fav:hover { color: var(--brass); }
.chan .fav:focus-visible { outline: 1px solid var(--brass); border-radius: 3px; }

/* The server settings gear on the row (Robin, 15 July). Same hover-reveal behaviour as the star
   above: quiet until you hover the row, so the sidebar stays calm. It only exists at all on a server
   you may manage, so its mere appearance is the "you have access" cue - there is no disabled state. */
.chan .row-gear {
  margin-left: auto;
  padding: 0 2px 0 6px;
  color: var(--faint);
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.12s ease, color 0.12s ease;
}
.chan:hover .row-gear,
.chan .row-gear:focus-visible { opacity: 1; }
.chan .row-gear:hover { color: var(--brass); }
.chan .row-gear:focus-visible { outline: 1px solid var(--brass); border-radius: 3px; }

/* No hover to reveal it, so never hide it. This is a coarse-pointer test, not a width test: a small
   window on a laptop still has a mouse. */
@media (hover: none) {
  .chan .fav,
  .chan .row-gear { opacity: 1; }

  /* ---------- the phone (#107) ----------
     Four fixes Robin hit using the app on a phone, all of them CSS and none of them touching a
     desktop. A coarse-pointer test, not a width test, on purpose: these are about a touch keyboard
     and a fat finger, not about a narrow window on a laptop that still has a mouse.

     (4) The zoom trap, first, because it is the one that is a real bug. iOS Safari zooms the page in
     whenever you focus an input whose font is under 16px - and never zooms back out - so at our 15px
     body font every tap into the composer left you zoomed in. Nothing "zooms back": we stop it
     zooming in at all by handing the three inputs you actually type into a 16px font. */
  .composer textarea,
  .chan-search,
  .side-search { font-size: 16px; }

  /* (1) + (2) The composer footnote. "Enter sends · Shift+Enter is a new line" describes keys a
     phone keyboard does not have, and the encryption line is repeated in three other places and
     eats height the small screen needs. Both go; the composer keeps its room for the message. */
  .composer-note { display: none; }

  /* (3) The pane header runs out of width with a 180px search box AND the word "People" in it, so
     the title has nowhere to sit. The search collapses to a magnifier and only grows when you tap
     into it; "People" becomes its roster glyph. Pure CSS - the input expanding on :focus is the
     whole interaction, no JavaScript decides anything. */
  .chan-search {
    width: 38px;
    padding-left: 30px;               /* room for the magnifier drawn as a background image */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='10.5' cy='10.5' r='6.5'/%3E%3Cpath d='M15.5 15.5 L20 20'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: 8px center;
    background-size: 16px;
    /* Its own transition, so :focus growth is smooth even though * transitions are otherwise off. */
    transition: width 160ms ease;
  }
  /* Tapped in, or holding a query you typed: take the width you need. */
  .chan-search:focus,
  .chan-search:not(:placeholder-shown) { width: 62vw; }

  .who-btn { padding: 6px; }
  .who-icon { display: block; }
  .who-word { display: none; }

  /* (5) Reacting on a phone. The ☺ and ⋯ buttons live behind :hover, which a touch screen never
     fires - so on a phone they were invisible and there was no way to react or reach copy/edit/
     delete at all. Two fixes together: show them, faintly, on every row so there is something to
     tap; and (in app.js) long-press a message to pop the emoji row open in one gesture. Faint until
     touched, so a screen full of messages does not turn into a screen full of buttons. */
  .msg-react,
  .msg-more { opacity: 0.45; }
  .msg-react:active,
  .msg-more:active { opacity: 1; }
}

/* ---------- settings ---------- */

.panel h2 { margin-bottom: 3px; }
.note { font-size: 12.5px; color: var(--faint); margin: 14px 0 0; }

/* Not enough width for three columns: the access panel slides over the chat from the right, the
   mirror image of what the server list does from the left, and the scrim closes it. */
@media (max-width: 980px) {
  .chat.show-members { grid-template-columns: 220px 1fr; }
  .chat.show-members .members {
    position: absolute;
    inset: 0 0 0 auto;
    width: 250px;
    z-index: 3;
    box-shadow: -12px 0 30px rgba(0, 0, 0, 0.3);
  }
  .chat.show-members .scrim {
    display: block;
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 2;
  }
}

@media (max-width: 780px) {
  .chat { grid-template-columns: 1fr; }
  .chat.show-members { grid-template-columns: 1fr; }
  .sidebar {
    position: absolute;
    inset: 0 auto 0 0;
    width: 250px;
    z-index: 3;
    transform: translateX(-100%);
    transition: transform 180ms ease;
    box-shadow: 12px 0 30px rgba(0, 0, 0, 0.3);
  }
  .chat.nav-open .sidebar { transform: translateX(0); }
  .chat.nav-open .scrim {
    display: block;
    position: absolute;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 2;
  }
  .hamburger {
    display: grid;
    place-items: center;
    width: 38px; height: 38px;
    border-radius: 7px;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--dim);
    cursor: pointer;
    flex: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; }
}

.device {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 13px 0;
  border-top: 1px solid var(--line-soft);
}
.device:first-of-type { border-top: 0; }
.device .dot { width: 8px; height: 8px; border-radius: 999px; background: var(--faint); flex: none; }
.device.is-current .dot { background: var(--sage); }
.device .name { font-weight: 500; display: block; }
.device .meta {
  display: block;
  font-size: 11.5px;
  color: var(--faint);
  font-family: var(--mono);
  margin-top: 2px;
}
.device .tag {
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sage);
  white-space: nowrap;
  flex: none;
}
.device .spacer { margin-left: auto; }

.note { font-size: 13px; color: var(--faint); margin: 16px 0 0; }
.note.aside { color: var(--aside); }

/* ---------- People, and invites (#26, #50) ----------
   The same row as .device, because they are the same shape of thing: an identifying line, a grey
   line of metadata under it, and controls pushed to the right. Everything on these two panels is
   metadata the server can already see - a username, a join date, whether a code was spent - which is
   exactly why they can exist at all. Nothing here is decrypted; there is nothing here to decrypt. */
.person-row,
.invite-row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 0;
  border-top: 1px solid var(--line-soft);
  /* The card is ~390px and a row can carry a name, a date, "invited by robin", a role tag and a
     button. Let it wrap rather than push the button off the edge - `flex: none` below stops the
     button collapsing, so without this it overflows instead. */
  flex-wrap: wrap;
}
.person-row:first-child,
.invite-row:first-child { border-top: 0; }

.person-row .who { min-width: 0; }
.person-row .name { font-weight: 500; display: block; }
.person-row .meta,
.invite-row .meta {
  display: block;
  font-size: 11.5px;
  color: var(--faint);
  margin-top: 2px;
}
.person-row .spacer,
.invite-row .spacer { margin-left: auto; }

.person-row .tag,
.invite-row .tag {
  font-family: var(--mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--sage);
  white-space: nowrap;
  flex: none;
}

/* The code shown after minting a server link (#24b), big enough to read out. Not a key: it decrypts
   nothing, so it is safe on screen. */
.invite-code-box {
  font-family: var(--mono);
  font-size: 18px;
  letter-spacing: 0.08em;
  color: var(--brass);
  background: var(--ground);
  border: 1px dashed var(--brass);
  border-radius: 6px;
  padding: 14px;
  text-align: center;
  user-select: all;
  margin-bottom: 12px;
  word-break: break-all;
}

/* An invite code is not a key - it opens an account and decrypts nothing - so unlike the recovery
   code it is safe to leave sitting on screen. It still gets the monospace treatment, because it is a
   string somebody has to read out or type. */
.invite-code {
  font-family: var(--mono);
  font-size: 13.5px;
  letter-spacing: 0.05em;
  color: var(--brass);
  user-select: all;
  flex: none;
}
/* A spent code opens nothing. It must not look like something to copy. */
.invite-code.spent {
  color: var(--faint);
  text-decoration: line-through;
  user-select: auto;
}
.invite-row.is-used .tag { color: var(--sage); }
.invite-row.is-expired .tag,
.invite-row.is-cancelled .tag { color: var(--faint); }

/* flex: none, and it is load-bearing rather than tidy-mindedness. A <button> is a flex item like any
   other, so in a card this narrow it happily shrinks to ZERO width - still in the DOM, still
   `visibility: visible`, still reported as present by anything that asks - and completely
   unclickable. "Make moderator" did exactly that, and the only reason it was caught is that a real
   browser refused to click it. Anything in one of these rows that a human has to hit says so. */
.person-row .linkish,
.invite-row .linkish { flex: none; }

/* A suspended account. Greyed, but still legible - this is the list you go to in order to LIFT a
   ban, so the one person you must be able to find on it is the one who is banned. */
.person-row.suspended .avatar { opacity: 0.4; }
.person-row.suspended .name { color: var(--faint); }
.person-row.suspended .meta { color: var(--oxblood); }

@media (max-width: 520px) {
  .card { padding: 22px; }
  .device { flex-wrap: wrap; }
}

/* ---------- printing the recovery code (#63) ----------
   The page that says "write this down now" used to offer exactly one way to get the code off the
   screen: the clipboard, which every other app on the machine can read. So there is a printer here
   too - and printing happens entirely in the browser. The code is already in this page's memory;
   window.print() sends nothing anywhere. No PDF service, no request, no third party.

   What comes out of the printer is #printSheet and NOTHING else. The rule below is deliberately a
   blunt one - hide every child of <body>, then un-hide the sheet - rather than a list of things to
   suppress, because a list is a thing you forget to add to. Add a new top-level view to index.html
   and it stays off the paper for free; miss one from a list and you print somebody's chat. */
.codeways { display: flex; align-items: center; justify-content: center; gap: 8px; }
.codeways .sep { color: var(--line); }
#printCode { display: inline-flex; align-items: center; gap: 5px; }
#printCode svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* On screen the sheet does not exist, and that is a CSS fact rather than a promise the JavaScript
   makes. The print handler fills it in and empties it again, and if that handler ever gets its
   bookkeeping wrong the worst case is an empty node - not a recovery code painted onto the page. */
.print-sheet { display: none; }

@media print {
  /* Every top-level view, gone - including the one the code is on. */
  body > * { display: none !important; }

  /* ...and this one back. */
  body > .print-sheet {
    display: block !important;
    background: #fff;
    color: #000;
    padding: 0;
  }

  body { background: #fff; color: #000; }

  .print-sheet h1 { font-size: 16pt; margin: 0 0 18pt; font-weight: 600; }
  .print-code {
    font-family: var(--mono);
    font-size: 20pt;
    letter-spacing: 0.08em;
    padding: 14pt;
    border: 1.5pt dashed #000;
    text-align: center;
    margin-bottom: 14pt;
  }
  .print-meta { font-size: 10pt; margin: 0 0 24pt; }
  .print-warn { font-size: 10pt; max-width: 38em; line-height: 1.5; }
}
