/* ============================================================================
   Vector Widescreen — multi-pane desktop layout
   ----------------------------------------------------------------------------
   Vector's panes (#chats, #chat, #profile, …) are siblings inside
   `main.container`, shown one at a time by imperative `style.display` writes
   scattered across main.js. Widescreen does NOT unwind that: it grids the
   container and uses `!important` visibility locks, which beat the inline
   `display: none` those call sites keep writing. Every pane's state logic
   (headers, message DOM, strOpenChat, the back stack) runs unchanged.

   CONTRACT: every IN-FLOW child of `main.container` must be placed explicitly
   in §2. Out-of-flow children (`position: fixed`, i.e. every overlay panel)
   are not grid items and need no placement — only a `left` offset if they
   should clear the rail.

   `body.ws` is owned by js/widescreen.js (width + logged-in), never by a media
   query, so the class and the layout can never disagree.
   ========================================================================= */

:root {
  --ws-rail-open-w: 200px;
  --ws-rail-w: var(--ws-rail-open-w);
  --ws-rail-collapsed-w: 72px;
  --ws-list-w: 240px;           /* live value; js/widescreen.js persists it */
  --ws-list-min: 200px;
  --ws-list-max: 480px;
  --ws-details-w: 240px;

  /* Surfaces are translucent white over the theme's own root background, so
     all 7 themes get a coherent rail/list without per-theme overrides. */
  --ws-rail-bg: rgba(255, 255, 255, 0.024);
  --ws-list-bg: rgba(255, 255, 255, 0.012);
  --ws-seam: #171717;
  --ws-hover: rgba(255, 255, 255, 0.045);
  /* Selection is a brighter step of the same neutral, not the accent: the accent
     is the app's "this is live/yours" signal, and spending it on every selected
     row leaves nothing to say it with. */
  --ws-active: rgba(255, 255, 255, 0.09);

  /* The desktop title strip; 0 wherever the OS draws its own (js/chrome.js). */
  --chrome-h: 0px;
}

/* ========================= 0a. The window chrome ========================= */
/* Every desktop layout, wide or narrow, sits under this strip; its height is
   carved out of the shell below rather than laid over it. */

body.chrome {
  --chrome-h: 32px;
}

/* Three cells so the banner centres on the window, not on what the lockup and
   the controls leave over. */
#window-chrome {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  box-sizing: border-box;
  height: var(--chrome-h);
  /* Opaque: the tint over the themes' shared root colour, so nothing the shell
     lifts above its top edge shows through the strip. */
  background-color: #030303;
  background-image: linear-gradient(var(--ws-rail-bg), var(--ws-rail-bg));
  border-bottom: 1px solid var(--ws-seam);
  -webkit-user-select: none;
  user-select: none;
  position: relative;
  z-index: 41;
}

#chrome-left {
  display: flex;
  align-items: center;
  height: 100%;
  justify-self: start;
}

body.ws #chrome-left {
  width: var(--ws-rail-w);
}

#chrome-lockup {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 100%;
  padding: 0 12px;
}

body.ws.ws-rail-collapsed #chrome-typeface {
  display: none;
}

#chrome-lockup:hover {
  opacity: 1;
}

#chrome-mark {
  width: 12px;
  height: 20px;
  flex-shrink: 0;
}

#chrome-typeface {
  width: 92px;
  height: 12px;
  flex-shrink: 0;
}

/* The banner is the strip's one line of text, in the accent because it is the
   only thing here that asks for a click. */
.chrome-update {
  flex-shrink: 0;
  grid-column: 2;
  max-width: 60vw;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  height: 100%;
  padding: 0 12px;
  border: 0;
  background: none;
  color: var(--primary-color, #33db98);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.chrome-update:hover {
  text-decoration: underline;
}

#chrome-controls {
  display: flex;
  height: 100%;
  grid-column: 3;
  justify-self: end;
}

.chrome-ctl {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 46px;
  height: 100%;
  border: 0;
  border-radius: 0;
  padding: 0;
  background: none;
  color: rgba(255, 255, 255, 0.7);
  cursor: default;
  transition: background-color 0.12s ease, color 0.12s ease;
}

.chrome-ctl svg {
  width: 10px;
  height: 10px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1;
}

.chrome-help svg {
  width: 16px;
  height: 16px;
  stroke-width: 1.2;
  stroke-linecap: round;
}

.chrome-help .dot {
  fill: currentColor;
  stroke: none;
}

.chrome-help {
  margin-right: 8px;
}

.chrome-ctl.chrome-help:hover {
  background-color: transparent;
}

.chrome-ctl:hover {
  background-color: var(--ws-hover);
  color: #fff;
}

.chrome-close:hover {
  background-color: #e81123;
}

/* ===================== 0. Widescreen-only furniture ====================== */
/* These live in the markup permanently (the rail is #navbar re-dressed, so its
   extras are its children) and must be invisible in single-pane mode. */

#ws-rail-shortcuts,
#ws-rail-spacer,
#ws-rail-collapse,
#ws-rail-account,
#ws-list-resize {
  display: none;
}

/* ============================ 1. Shell grid ============================== */

/* Column 4 is the details sidebar. It always exists so opening it animates a
   width rather than restructuring the grid; `ws-details` gives it its size. */
body.ws main.container {
  display: grid;
  grid-template-columns: var(--ws-rail-w) var(--ws-list-w) minmax(0, 1fr) 0px;
  grid-template-rows: minmax(0, 1fr);
  height: calc(100% - var(--chrome-h));
  overflow: hidden;
}

body.ws.ws-details main.container {
  grid-template-columns: var(--ws-rail-w) var(--ws-list-w) minmax(0, 1fr) var(--ws-details-w);
}

/* Left edge of the "active pane" column (rail + list), for the fixed-position
   descendants inside the tab panes that can't be grid-placed.

   Declared on the BODY, never on :root: a custom property substitutes the
   referenced value AS COMPUTED ON ITS OWN ELEMENT, so at :root this baked in the
   expanded 232px rail and the collapse below — which overrides --ws-rail-w on
   body — could never reach it. That left every pane using it inset 160px past
   the rail it was meant to clear. (--ws-list-w tracked only because
   js/widescreen.js writes the live width to the root itself.) */
body.ws {
  --ws-main-left: calc(var(--ws-rail-w) + var(--ws-list-w));
}

body.ws.ws-rail-collapsed {
  --ws-rail-w: var(--ws-rail-collapsed-w);
}

/* ========================== 2. Pane placement ============================ */

body.ws #chats {
  grid-column: 2;
  grid-row: 1;
  background-color: var(--ws-list-bg);
  border-right: 1px solid var(--ws-seam);
  /* `.chats` centres its flow children vertically; at full pane height that
     would float the composer row into the middle of the list. */
  justify-content: flex-start;
  min-height: 0;
  overflow: hidden;
}

body.ws #chat,
body.ws #ws-conv-empty {
  grid-column: 3;
  grid-row: 1;
}

/* The tab panes (Profile / Settings / Invites) and the composer flows are the
   "ACTIVE PANE": they take the conversation's column rather than covering the
   list, so the chat list is permanently on screen in widescreen. They stack over
   #chat / #ws-conv-empty in the same cell and win on z-index. */
body.ws #profile,
body.ws #chat-new,
body.ws #create-group,
body.ws #invites {
  grid-column: 3;
  grid-row: 1;
  z-index: 30;
  background-color: #030303;
}

/* Community details becomes the member sidebar: same pane, same renderer, same
   moderation controls — docked beside the conversation instead of replacing it. */
body.ws #group-overview {
  grid-column: 4;
  grid-row: 1;
  z-index: 20;
  overflow: hidden;
  background-color: var(--ws-rail-bg);
  border-left: 1px solid var(--ws-seam);
}

/* Login can reappear mid-session (Add Profile, bunker re-auth), so it covers
   the rail too — belt and braces with the logged-in gate in widescreen.js. */
body.ws #login-form {
  grid-column: 1 / -1;
  grid-row: 1;
  z-index: 500;
  background-color: #030303;
}


/* ======================= 3. Visibility locks ============================= */

/* These beat the inline `display: none` that openChat/closeChat/openProfile
   keep writing. Panes stay live; only their geometry changed. */
body.ws #navbar {
  display: flex !important;
}

body.ws #chats {
  display: flex !important;
}

body.ws.ws-chat-open #chat {
  display: flex !important;
}

body.ws:not(.ws-chat-open) #ws-conv-empty {
  display: flex;
}

/* Pane fade-ins are a single-pane remnant: they mark a whole-SCREEN transition.
   Here the panes are columns that never leave, so the fade reads as a flash on
   every chat switch, details toggle and tab change. `.fadein-anim`'s `forwards`
   fill also means one interrupted mid-flight leaves its pane stuck at the 0%
   keyframe (opacity: 0).

   Zeroed rather than `animation: none`, deliberately: the open* functions add the
   class and remove it again on `animationend`, so killing the animation outright
   would strand the class AND leak a listener per open. A 0s animation still fires
   the event, so the existing cleanup runs with nothing drawn. */
body.ws #chat,
body.ws #chats,
body.ws #group-overview,
body.ws #profile,
body.ws #settings,
body.ws #invites,
body.ws #chat-new,
body.ws #create-group,
body.ws #ws-rail-account #account {
  animation-duration: 0s !important;
  animation-delay: 0s !important;
}

/* The list is always on screen, so its per-pane back affordances are noise. */
body.ws #chat-back-btn,
body.ws #chat-bookmarks-btn {
  display: none !important;
}

/* openChat hides the Settings tab so a chat can own the whole screen; in the
   rail every tab has to stay reachable. (#invites-btn is deliberately absent:
   it hides when there are no invites, which is still true here.) */
body.ws #settings-btn {
  display: flex !important;
}

/* =================== 4. Un-viewporting the pane internals ================ */
/* Rules whose viewport assumptions (100% width, bottom: 65px for the mobile
   navbar) only held when a pane WAS the viewport. */

/* The conversation header joins the flex flow, which also retires the 75px
   scroll clearance under it and the blur behind an opaque strip. */
body.ws #chat {
  height: 100%;
  min-height: 0;
}

/* The message list's top fade shares z-index 100 with the header and paints
   after it, washing the header's own seam roughly halfway to black — which is
   why this one line read fainter than every other seam in the app. The header
   is nearly opaque and hides what scrolls under it anyway, so it belongs above
   the fade. */
body.ws #chat > .chat-header {
  z-index: 101;
}

body.ws #chat > .chat-header {
  position: relative;
  backdrop-filter: none;
  background-color: #0c0c0c;
  border-bottom: 1px solid var(--ws-seam);
}

body.ws .chat-messages {
  padding-top: 12px;
}

/* File boxes size their cap off the VIEWPORT (`min(500px, 100vw - 100px)`), which was
   the conversation's own width when the conversation was the whole screen. Here it's the
   middle column, so the cap moves to the row they sit in. */
body.ws .file-attachment,
body.ws .file-box {
  max-width: min(100%, 500px);
}

/* The list fills whatever the header block leaves, instead of measuring the
   viewport with hardcoded insets. */
body.ws #chat-list {
  position: relative;
  inset: auto;
  flex: 1;
  min-height: 0;
  padding-left: 8px;
  padding-right: 8px;
  padding-bottom: 8px;
  /* The rail's fade, as a mask for the same reason: the depth tracks what is left to
     scroll (ChatListPane.svelte), so the last row is only dim while there is more. */
  --ws-list-fade: 0px;
  --ws-list-fade-top: 0px;
  mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--ws-list-fade-top),
    #000 calc(100% - var(--ws-list-fade)),
    transparent 100%
  );
}

/* The narrow layout's bottom chrome is what the last row clears; there is none here. */
body.ws #chat-list > :last-child {
  margin-bottom: 0;
}

body.ws #chats {
  padding-top: 12px;
}

body.ws #chats > .sync-line {
  margin-bottom: 8px;
}

body.ws #chats > .fadeout-bottom {
  display: none;
}

/* The active pane starts after the rail AND the list, so every `position: fixed`
   descendant that used to span the viewport needs that inset. `.profile-content`
   matters most: the entire profile body lives inside it, and left-unset it drops
   its banner-anchored Edit button on top of the rail. */
body.ws .profile-header,
body.ws .profile-content,
body.ws .chat-new-header {
  left: var(--ws-main-left);
}

/* The tab handlers all hide the details pane, so ws-details is normally off while
   Settings shows; the variant is for the frame where it hasn't caught up yet. */
body.ws.ws-details #settings {
  right: var(--ws-details-w);
}

/* The pins drawer sheets down from under the conversation header, so it takes the
   same inset. Unlike Settings its details-pane variant is the COMMON case: pins
   are a Community affordance, and a Community is what docks that column. */
body.ws .pins-drawer {
  left: var(--ws-main-left);
}

/* The attachment panel rises out of the composer, so it belongs to the composer's
   box, not the window's. It was viewport-centred (`left: 50%` + a translateX) and a
   viewport width, which put a panel wider than the conversation over the rail and
   the list. Left and right define it here instead — the 8px matches the composer's
   own inset, so the two line up edge to edge — and dropping the centring means
   restating both ends of the slide, since the transform carried it. */
body.ws .attachment-panel {
  left: calc(var(--ws-main-left) + 8px);
  right: 8px;
  width: auto;
  transform: translateY(100%);
}

body.ws .attachment-panel.visible {
  transform: translateY(0);
}

body.ws.ws-details .attachment-panel {
  right: calc(var(--ws-details-w) + 8px);
}

/* The Viktor welcome was anchored to the single-pane layout: fixed against the
   VIEWPORT — left/right 18px spanning the whole window, bottom 82px clearing the
   bottom navbar that no longer exists here. Re-anchor it to the list column:
   left at the rail's edge, right at the conversation pane's, 18px off the pane's
   own bottom. Var-based, so the collapsed rail tracks automatically. */
body.ws .chatlist-intro {
  left: calc(var(--ws-rail-w) + 18px);
  right: calc(100vw - var(--ws-main-left) + 18px);
  bottom: 18px;
}

body.ws.ws-details .pins-drawer {
  right: var(--ws-details-w);
}

/* The details sidebar is a narrow column, so its header joins the flex flow
   (like the conversation's) instead of being pinned to a viewport edge. */
body.ws #group-overview > .chat-header {
  position: relative;
  backdrop-filter: none;
  background-color: #0c0c0c;
  border-bottom: 1px solid var(--ws-seam);
  flex-shrink: 0;
}

/* The scroll region is inline-styled `height: 100%`, which overflows once a
   60px header shares the column. */
body.ws #group-overview > div:not(.chat-header) {
  flex: 1;
  min-height: 0;
  height: auto !important;
}

/* Points away from the conversation now that the pane docks on the right. */
body.ws #group-overview-back-btn .nav-icon {
  transform: rotate(180deg);
}

/* The overview's vertical rhythm was drawn for a full phone screen: 100px of
   lead-in above a 100px avatar, 75px of tail. In a 300px column that pushed the
   member list below the fold. */
/* The avatar and its lead-in are gone from this pane (it's the member list now),
   so the space they reserved has to go too or the roster starts a third of the
   way down an empty column. */
body.ws #group-overview {
  --group-overview-avatar: 0px;
  --group-overview-avatar-lead: 0px;
  --group-overview-tail: 8px;
}

body.ws #group-mute-btn {
  top: 12px;
  right: 12px;
}

/* Kick/Ban sit beside the name in a narrow column; let the name give way first. */
body.ws #group-overview .member-pick-row .compact-member-name {
  min-width: 0;
}

body.ws .profile-content,
body.ws #settings {
  bottom: 0;
}

/* Settings takes the list column as well as the active pane: its nav sits where
   the list does, at the list's width, and its page fills the rest. Fixed, so it is
   inset by hand rather than grid-placed; under the resizer, so the drag still
   sizes the nav. The rail's own layer, not above it: the rail's seam overhangs
   this edge by its border, and the rail comes later in the DOM. */
body.ws #settings {
  top: var(--chrome-h);   /* its 10px inset would let the list peek above it */
  left: var(--ws-rail-w);
  right: 0;               /* the stock 2px would leave a seam at the pane's edge */
  display: flex;
  overflow: hidden;
  z-index: 40;
  background-color: #030303;
}

body.ws #settings > .cs-nav {
  width: var(--ws-list-w);
  background-color: var(--ws-list-bg);
  border-right: 1px solid var(--ws-seam);
}

/* The account row moves into the rail footer (see widescreen.js), so its
   in-list incarnation must not also reserve 60px at the top of the list. */
body.ws #ws-rail-account #account {
  height: auto;
  background-color: transparent;
  justify-content: flex-start;
  width: 100%;
}

/* Expanded, the chip is a destination rather than a list row: a soft tile with
   the avatar beside a name stacked over its status. Restyled, never rebuilt —
   #account is the list's own row, re-parented (see js/widescreen.js). The
   :not() keeps this off the collapsed rail, whose rules live further down. */
body.ws:not(.ws-rail-collapsed) #ws-rail-account #account {
  background-color: var(--ws-hover);
  border-radius: 12px;
  /* 9px left: the avatar lands on the same pixel the collapsed tile centres it on. */
  padding: 8px 10px 8px 9px;
  align-items: center;
  transition: background-color 0.12s ease;
  margin-bottom: 0;
}

body.ws:not(.ws-rail-collapsed) #ws-rail-account #account:hover {
  background-color: var(--ws-active);
}

/* Two rows, avatar spanning both. `display: contents` dissolves the name row so
   the avatar and name become grid items in their own right — the alternative is
   moving nodes, which breaks the row for the list it goes back to. */
body.ws:not(.ws-rail-collapsed) #ws-rail-account .profile-header-info {
  flex: 1;
  min-width: 0;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  column-gap: 10px;
  align-items: center;
}

body.ws:not(.ws-rail-collapsed) #ws-rail-account .profile-header-name-row {
  display: contents;
}

body.ws:not(.ws-rail-collapsed) #ws-rail-account #account-avatar-container {
  grid-row: 1 / span 2;
}

/* One size in both rail states: collapsing changes what stands beside the avatar,
   never the avatar. Flex, so the image carries no baseline gap to shift it by.
   Inline-sized by ui/Avatar.svelte, hence !important. */
body.ws #ws-rail-account #account-avatar-container {
  display: flex;
}

body.ws #ws-rail-account #account-avatar-container img {
  width: 38px !important;
  height: 38px !important;
}

body.ws:not(.ws-rail-collapsed) #ws-rail-account #account-name {
  grid-column: 2;
  min-width: 0;
  margin: 0;
  color: #fff;
  line-height: 19px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* One line, truncated: a status is often a sentence, and the rail is 232px. */
body.ws:not(.ws-rail-collapsed) #ws-rail-account #account-status {
  grid-column: 2;
  min-width: 0;
  height: auto;
  font-size: 12px;
  line-height: 16px;
  color: rgba(255, 255, 255, 0.5);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

body.ws:not(.ws-rail-collapsed) #ws-rail-account .ws-account-caret {
  flex: none;
  margin-left: 8px;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 6px solid rgba(255, 255, 255, 0.4);
  transition: border-top-color 0.12s ease;
}

body.ws:not(.ws-rail-collapsed) #ws-rail-account #account:hover .ws-account-caret {
  border-top-color: rgba(255, 255, 255, 0.75);
}

/* Collapsed shows the avatar alone — the caret would be furniture at 72px. */
body.ws.ws-rail-collapsed #ws-rail-account .ws-account-caret {
  display: none;
}

/* ---- The list pane as a channel list ---- */

/* ONE gutter for the pane. The community's icon, the section label and every
   channel glyph start on the same left edge; each of these used to carry its own
   padding and the column read as three ragged columns.

   Declared on the BODY, not on #chat-list: the header sits OUTSIDE that list now,
   and a var() its element can't see makes the whole `padding` shorthand invalid —
   which is a silent collapse to zero, not a fallback to the pane's edge. Same trap
   as --ws-main-left above. */
body.ws {
  --ws-pane-gutter: 12px;
}

/* The community's identity heads its own pane, hosted above the scroller so the
   channels scroll under it rather than carrying it away. */
body.ws .chatlist-community-head {
  display: flex;
  align-items: center;
  gap: 10px;
  /* #chat-list centres its text — fine for rows whose text fills them, not for a
     block that spans the pane: the name's BOX started on the gutter while the name
     itself floated to the middle, which is what read as the icon "offsetting" it. */
  text-align: left;
  padding: 10px var(--ws-pane-gutter);
  background-color: #171717;
  border-bottom: 1px solid var(--ws-seam);
  transition: background-color 0.12s ease;
  /* Level with the conversation header beside it: `.chat-header`'s 60px plus the
     1px seam. Stated as a border-box total so the padding and the seam come out of
     it rather than adding to it — sized by its content (a 36px avatar) the head
     landed 4px short. */
  box-sizing: border-box;
  height: 61px;
}

body.ws .chatlist-community-head:hover {
  background-color: var(--ws-hover);
}

/* Inline-sized by ui/Avatar.svelte, hence !important (as everywhere it's re-used). */
body.ws .chatlist-community-head-avatar {
  width: 36px !important;
  height: 36px !important;
  border-radius: 11px !important;
  flex-shrink: 0;
  object-fit: cover;
}

/* flex + min-width:0 is what lets the name ellipsis at the pane's width instead
   of sizing itself to some intrinsic guess. */
body.ws .chatlist-community-head-meta {
  display: flex;
  flex-direction: column;
  justify-content: center;
  flex: 1;
  min-width: 0;
  gap: 2px;
}

body.ws .chatlist-community-head-name {
  font-size: 15px;
  font-weight: 600;
  line-height: 18px;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

body.ws .chatlist-community-head-members {
  display: flex;
  align-items: center;
  gap: 2.5px;
  font-size: 12px;
  line-height: 14px;
  color: rgba(255, 255, 255, 0.42);
  white-space: nowrap;
  overflow: hidden;
}

body.ws .chatlist-community-head-members > span:last-child {
  overflow: hidden;
  text-overflow: ellipsis;
}

body.ws .chatlist-community-head-members-icon {
  position: relative;
  flex-shrink: 0;
  width: 13px;
  height: 13px;
  /* `.icon` ships `margin: auto` to centre itself inside its absolute inset.
     As a flex item that instead absorbs the free space and floats the glyph to
     the middle of the row, so it has to be zeroed here. */
  margin: 0;
  background-color: rgba(255, 255, 255, 0.42);
}

/* Says the header is a menu, not a label. `position: relative` is load-bearing:
   .icon fills its nearest positioned ancestor, which would otherwise be the
   sticky header itself. These are mask icons, so they tint by background-color. */
/* Raid pip on the community header. Sits before the caret so it reads as part of the
   community's status, not as a second control. It pulses because a raid is the one
   state in this app where noticing late costs you the community. */
body.ws .chatlist-community-head-alert {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
  background-color: var(--danger-color);
  box-shadow: 0 0 0 0 color-mix(in srgb, var(--danger-color) 70%, transparent);
  animation: community-raid-pulse 1.9s ease-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  body.ws .chatlist-community-head-alert { animation: none; }
}

body.ws .chatlist-community-head-caret {
  position: relative;
  flex-shrink: 0;
  width: 30px;
  height: 30px;
}

body.ws .chatlist-community-head-caret .icon {
  background-color: rgba(255, 255, 255, 0.4);
  transition: background-color 0.12s ease;
}

body.ws .chatlist-community-head:hover .chatlist-community-head-caret .icon {
  background-color: rgba(255, 255, 255, 0.85);
}

/* As the pane, the channel list is the navigation rather than a disclosure under
   a row: no nested indent, and a label so the community and its channels read as
   two things rather than one run of rows. */
/* The nested list hangs off its community row on a 34px indent behind a guide
   line. As the pane there's no row to hang from — and that 1px border was also
   what put every channel a pixel right of the icon above them. */
body.ws .chatlist-channels-pane {
  margin: 0 0 50px;
  padding-left: 0;
  border-left: none;
}

/* The sections name themselves now, so a blanket "CHANNELS" heading over them
   labelled a list that already has labels. */
body.ws .chatlist-channels-pane::before {
  content: none;
}

/* Same horizontal box as the header above them — no margin, padding on the
   gutter — so the glyphs and the community's icon share one edge no matter what
   fractional width the resizer leaves the pane at. An inset pill would have to
   subtract its own margin back out, and lands a subpixel off. */
/* Floating rows, like the rail's: an inset pill rather than a band that runs
   wall to wall, so the selection reads as an object you picked. */
body.ws .chatlist-channels-pane .chatlist-channel {
  height: 34px;
  margin: 0 8px;
  padding: 0 8px;
  gap: 8px;
  border-radius: 4px;
  font-size: 15px;
}

/* ── Sections ──────────────────────────────────────────────────────────── */

body.ws .chatlist-channel-section + .chatlist-channel-section {
  margin-top: 14px;
}

body.ws .chatlist-channel-section-head {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 10px 12px 4px;
}

body.ws .chatlist-channel-section-toggle {
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 1;
  min-width: 0;
  padding: 2px 4px;
  border-radius: 6px;
  opacity: 1;
}

body.ws .chatlist-channel-section-label {
  font-family: 'Rubik', Inter, Avenir, Helvetica, Arial, sans-serif;
  font-size: 15px;
  color: rgba(255, 255, 255, 0.38);
  transition: color 0.12s ease;
}

body.ws .chatlist-channel-section-toggle:hover .chatlist-channel-section-label {
  color: rgba(255, 255, 255, 0.7);
}

body.ws .chatlist-channel-section-caret {
  position: relative;
  flex-shrink: 0;
  width: 18px;
  height: 18px;
}

body.ws .chatlist-channel-section-caret .icon {
  background-color: rgba(255, 255, 255, 0.38);
  transition: background-color 0.12s ease, transform 0.15s ease;
}

body.ws .chatlist-channel-section-toggle:hover .chatlist-channel-section-caret .icon {
  background-color: rgba(255, 255, 255, 0.7);
}

/* Closed points the way it will go, not the way it came from. */
body.ws .chatlist-channel-section.is-closed .chatlist-channel-section-caret .icon {
  transform: rotate(-90deg);
}

body.ws .chatlist-channel-section.is-closed .chatlist-channel-section-body {
  display: none;
}

body.ws .chatlist-channel-section-add {
  position: relative;
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  border-radius: 6px;
  opacity: 1;
  transition: background-color 0.12s ease;
}

body.ws .chatlist-channel-section-add .icon {
  background-color: rgba(255, 255, 255, 0.38);
  transition: background-color 0.12s ease;
}

body.ws .chatlist-channel-section-add:hover {
  background-color: var(--ws-hover);
}

body.ws .chatlist-channel-section-add:hover .icon {
  background-color: #fff;
}

/* ── Row tiers ─────────────────────────────────────────────────────────── */

body.ws .chatlist-channels-pane .chatlist-channel-hash {
  opacity: 1;
}

body.ws .chatlist-channels-pane .chatlist-channel.has-unread {
  color: #fff;
  font-weight: 500;
}

body.ws .chatlist-channels-pane .chatlist-channel.is-read {
  color: rgba(255, 255, 255, 0.55);
  font-weight: 400;
}

body.ws .chatlist-channels-pane .chatlist-channel.is-muted {
  color: rgba(255, 255, 255, 0.3);
  font-weight: 400;
}

body.ws .chatlist-channels-pane .chatlist-channel:hover,
body.ws .chatlist-channels-pane .chatlist-channel.active {
  color: #fff;
}

/* New Chat and Group Chat both start DMs; over a community's channels they are
   answers to a question nobody asked. The rail's mark is one click away. */
body.ws.ws-community-pane #chat-new-actions {
  display: none !important;
}

/* The community's header is a HEADER, so it starts where the pane does — level with
   the conversation header beside it. The lead-in and the sync line below it are both
   there to float the New Chat row off the top edge; with the row gone they left the
   header sitting in a frame of pane background.

   The line comes out of the flow rather than being trimmed, because 2px in the flow
   is still 2px of misalignment. It keeps `width: 100%` (its fill is a scaleX from the
   centre, so it must span the pane to scale) and rides ABOVE the sticky header, whose
   background is opaque and would otherwise swallow it on scroll.

   The side padding goes too, and BOTH the header and the channels sit in that padding
   — so they lose it together and keep the shared left edge (the icon, the CHANNELS
   label and every channel glyph on one gutter) that the pane is built on. Nothing is
   lost to the scrollbar either; it overlays rather than taking width. */
body.ws.ws-community-pane #chats {
  padding-top: 0;
}

body.ws.ws-community-pane #sync-line {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  margin: 0;
  z-index: 3;
}

body.ws.ws-community-pane #chat-list {
  padding-left: 0;
  padding-right: 0;
}

/* The header's host (js/render/chatlist/list.js fills it): a sibling ABOVE the
   scroller rather than a sticky child inside it. Sticky is pinned to the scrollport,
   and the elastic overscroll moves the scrollport — so scrolling up past the top
   carried the header down with it. Out here no scroll position can reach it. Empty
   and zero-height whenever the pane isn't a community's. */
body.ws #ws-community-head {
  flex-shrink: 0;
}

/* ---- The details pane is the member list ---- */

/* Everything the community says about ITSELF now lives in the header menu at the
   top of its channel pane (js/render/chatlist/channels.js). What's left here is
   the roster — Discord's member sidebar. Hidden rather than deleted from the
   markup: the narrow layout still renders this pane as a full screen, and its
   own header is the only place that names the community there.

   Leave / Delete Community moved to that menu too, so the pane is the roster and
   nothing else. The button stays in the markup for the narrow layout, which has
   no community header to hang a menu on. */
body.ws #group-overview-avatar,
body.ws #group-overview-header-avatar-container,
body.ws #group-overview-secondary-name,
body.ws #group-overview-description,
body.ws #group-overview-status,
body.ws #group-mute-btn,
body.ws #group-invite-member-btn,
body.ws #group-leave-btn,
body.ws #group-moderate-btn,
body.ws #group-migrate-row {
  display: none !important;
}

/* Its header keeps only the title — "Members", set by the renderer — so the
   roster starts at the top of the column instead of a screen down. */
body.ws #group-overview .group-overview-head,
body.ws #group-overview > div:first-child {
  padding-top: 0;
}

body.ws #group-member-search-input {
  margin-top: 0;
}

/* The body's block clears the avatar, which overlaps it in the narrow pane. Here
   the avatar frame collapses to 0, so that 40px is just a hole above the search
   field. (Not `:first-child` — the hidden name and description are still that.) */
body.ws #group-overview .group-overview-body > div {
  /* Both written inline by the renderer, hence !important — the same reason the
     rail's avatar sizing needs it. The 90% width centres a narrow column inside
     an already-narrow one, costing ~30px of roster for nothing. */
  margin-top: 0 !important;
  width: auto !important;
}

/* The roster IS the pane, not a card sitting in it: the frame drew a line around
   a column that already has an edge, and its border + inset spent width the
   member names needed. */
/* The search and the roster share ONE gutter down the pane, so the field's edge
   and every member row line up. */
body.ws #group-overview .emoji-search-container {
  /* Inline `padding: 10px 0` from the renderer — the fifth of these in this pane,
     so: assume inline first when a rule here looks ignored. */
  padding: 10px !important;
}

/* The magnifier is positioned against the CONTAINER, so the container's own inset
   left it 2px inside the field's border while the placeholder started 40px in.
   Sits 12px into the field now, with the text clearing it by a hair under 10px. */
body.ws #group-overview .emoji-search-container > .emoji-search-icon {
  left: 22px !important;
  width: 20px !important;
  height: 20px !important;
}

/* Member rows hover like the rail's rows do: the same soft tint, so a row here
   and a shortcut there feel like the same kind of target. */
body.ws #group-overview .member-pick-row {
  border-radius: 6px;
  transition: background-color 0.12s ease;
}

body.ws #group-overview .member-pick-row:hover {
  background-color: var(--ws-hover);
}

/* The row ships its own hover: an accent-gradient overlay that sits ON TOP of the
   grey above, so setting a background alone just painted underneath it. Selection
   is neutral here (see --ws-active), and the roster is no place to spend the
   accent either. */
body.ws #group-overview .member-pick-hover {
  display: none !important;
}

body.ws #group-overview-members {
  /* Inline again — the renderer writes the frame onto the element, so nothing but
     !important reaches it. */
  border: none !important;
  border-radius: 0 !important;
  /* Matches the search container's inset, so field and rows share one edge. */
  padding: 0 10px !important;
}

/* Its action row is Invite + Leave, both of which now live in the community's
   header menu: zero height, still spending a bottom margin — inline, so it took
   an !important to actually collect the 10px back from above the search. */
body.ws #group-overview .group-overview-body > div > div:first-child {
  margin-bottom: 0 !important;
}

/* The pane is unmistakably one community's member list: the channel list already
   names the community above it, and the roster names itself. A truncated title
   here was the third copy. */
/* ---- A channel's own header ----------------------------------------------
   Inside a community the header names the CHANNEL. The community's icon and
   member count already sit at the top of the channel pane one column left, so
   repeating them here spends the widest row in the app on what the eye just
   read. Name and hash, flush left, and nothing else. */
/* The header centres its own contents; a channel's name belongs at the start of
   the row, level with the messages under it. */
body.ws.ws-community-pane #chat > .chat-header {
  grid-template-columns: 0 minmax(0, 1fr) max-content;
}

body.ws.ws-community-pane #chat > .chat-header .profile-header-info {
  text-align: left;
  padding-left: 20px;
}

body.ws.ws-community-pane #chat > .chat-header .profile-header-name-row {
  justify-content: flex-start;
  gap: 8px;
}

body.ws.ws-community-pane #chat > .chat-header #chat-header-avatar-container,
body.ws.ws-community-pane #chat > .chat-header #chat-contact-status {
  display: none;
}

/* The hash is drawn here rather than inserted, so no render path has to know
   about it and nothing can rebuild the header without it. */
body.ws.ws-community-pane #chat > .chat-header #chat-contact {
  position: relative;
  padding-left: 27px;
}

/* Centred on the title's own box rather than sat on its baseline: a 19px glyph
   and an 18.7px face on a 24px line never agree by baseline maths, which is
   what left the hash riding a little high. Out of flow, so `.cutoff` still
   ellipsises the name. */
body.ws.ws-community-pane #chat > .chat-header #chat-contact::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  transform: translateY(-50%);
  width: 19px;
  height: 19px;
  background-color: rgba(255, 255, 255, 0.45);
  mask-image: url("./icons/channel-hash.svg");
  mask-size: contain;
  mask-repeat: no-repeat;
  mask-position: center;
}

body.ws #group-overview > .chat-header .profile-header-info {
  display: none;
}

/* ---- The account switcher ---- */

/* The Profile tab is column 3, and its dropdown hangs off that tab's header —
   so it centres on the COLUMN, not the window. Viewport-centred, it sat off to
   one side of the tab it belongs to, and its blur greyed out the rail, the list
   and the conversation the user never left. */
body.ws .profile-switcher-backdrop,
body.ws .profile-switcher-panel {
  left: var(--ws-main-left);
}

body.ws.ws-details .profile-switcher-backdrop,
body.ws.ws-details .profile-switcher-panel {
  right: var(--ws-details-w);
}

/* Opened from the rail chip it's a menu, not a takeover: it drops UP from the
   chip, keeps the rail's width, and its backdrop only catches the click that
   dismisses it. `bottom` is set inline against the chip (js/widescreen.js), so
   the panel tracks the chip rather than a number that has to be maintained. */
body.ws .profile-switcher-panel.ws-dropup {
  top: auto;
  left: 8px;
  right: auto;
  /* The open rail's width even from a collapsed rail: it pops out over the list. */
  width: calc(var(--ws-rail-open-w) - 16px);
  max-width: none;
  max-height: min(50vh, 420px);
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.08);
  animation: none;
}

body.ws .profile-switcher-backdrop.ws-dropup {
  top: 0;
  left: 0;
  right: 0;
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}

/* Quick-switch density: face and name, nothing else. The npub is for the Profile
   tab's full list, where there's room to read it. */
body.ws .profile-switcher-panel.ws-dropup .profile-switcher-npub {
  display: none;
}

body.ws .profile-switcher-panel.ws-dropup .profile-switcher-row {
  padding: 6px 10px;
  gap: 10px;
}

body.ws .profile-switcher-panel.ws-dropup .profile-switcher-avatar {
  width: 24px;
  height: 24px;
}

body.ws .profile-switcher-panel.ws-dropup .profile-switcher-header,
body.ws .profile-switcher-panel.ws-dropup .profile-switcher-add {
  padding-left: 10px;
  padding-right: 10px;
}

/* ============================== 5. The rail ============================== */

body.ws #navbar {
  position: fixed;
  top: var(--chrome-h);
  bottom: 0;
  left: 0;
  width: var(--ws-rail-w);
  height: auto;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-start;
  padding: 0;
  gap: 2px;
  background-color: var(--ws-rail-bg);
  border-right: 1px solid var(--ws-seam);
  z-index: 40;
  overflow: hidden;
  /* Snaps with its grid column rather than animating out of step with it. */
}


/* ---- Rail shortcuts: recent DMs, then the community strip ---- */

body.ws #ws-rail-shortcuts {
  display: flex;
  flex-direction: column;
  /* Takes the room between the head and the nav tabs and hands it to the
     community list, which is the only part that scrolls. */
  flex: 1 1 auto;
  min-height: 0;
  padding: 6px 0 6px;
}

/* New Messages holds its size: at most three rows, and it must not be the thing
   that gives way when the rail runs short. */
body.ws #ws-rail-dms {
  flex-shrink: 0;
}

/* The Messages tile: a labelled door in the unread rows' slot, lit while the DM
   list is the pane on screen the way a chosen community's shortcut lights. It
   stands as far under the header seam as it does above the divider. */
body.ws #ws-rail-messages {
  padding-top: 10px;
}

body.ws .ws-rail-messages {
  display: flex;
  align-items: center;
  gap: 11px;
  margin: 0 8px;
  padding: 0 10px 0 14px;
  height: 40px;
  border-radius: 6px;
  color: rgba(255, 255, 255, 0.7);
  font-family: 'Rubik', Inter, Avenir, Helvetica, Arial, sans-serif;
  font-size: 14px;
  font-weight: 500;
  -webkit-user-select: none;
  user-select: none;
  transition: background-color 0.12s ease, color 0.12s ease;
}

/* The same three states as a tab below it: bare at rest, one fill for hover and lit. */
body.ws .ws-rail-messages:hover {
  opacity: 1;
  background-color: var(--ws-hover);
  color: #fff;
}

body.ws:not(.ws-community-pane) .ws-rail-messages {
  background-color: var(--ws-hover);
  color: #fff;
}

body.ws .ws-rail-messages svg {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
}

body.ws.ws-rail-collapsed .ws-rail-messages {
  justify-content: center;
  gap: 0;
  width: 40px;
  height: 40px;
  padding: 0;
  margin: 0 auto;
  border-radius: 12px;
}

body.ws.ws-rail-collapsed .ws-rail-messages-label {
  display: none;
}

/* Communities takes what's left, and scrolls inside it. */
body.ws #ws-rail-spaces {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}

body.ws #ws-rail-spaces .ws-rail-rows {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Same bottom fade as the chat list, but as a MASK rather than a gradient
     overlay: the rail's surface is a translucent tint over whatever the theme's
     root colour is, so there's no single colour to fade into. Its depth tracks the
     scroll that's LEFT (js/render/rail.js), because a fixed ramp dims the last row
     permanently once you've reached it. */
  --ws-rail-fade: 24px;
  --ws-rail-fade-top: 0px;
  mask-image: linear-gradient(
    to bottom,
    transparent 0,
    #000 var(--ws-rail-fade-top),
    #000 calc(100% - var(--ws-rail-fade)),
    transparent 100%
  );
}

/* No scrollbar in either state: a thumb shrinks the content box and pushes every
   avatar off the nav tabs' axis. The bottom fade says there is more; the wheel
   still scrolls it. */
body.ws #ws-rail-spaces .ws-rail-rows {
  scrollbar-width: none;
}

body.ws #ws-rail-spaces .ws-rail-rows::-webkit-scrollbar {
  width: 0;
}

/* ---- Group headings ----------------------------------------------------
   The label carries its own rule, so a hidden group takes its heading and its
   hairline with it — one box to hide rather than three siblings to keep in
   step. `hidden` is set by js/render/rail.js when a group has no rows. */
body.ws .ws-rail-group[hidden] {
  display: none;
}

/* A hidden group is still a sibling: the gap belongs between two SHOWN groups,
   whichever they are. */
body.ws .ws-rail-group:not([hidden]) ~ .ws-rail-group:not([hidden]) {
  margin-top: 10px;
}

body.ws .ws-rail-group-label {
  /* The rail is the mobile tab bar restyled, and that centres its text. */
  text-align: left;
  margin: 0 16px;
  font-family: 'Rubik', Inter, Avenir, Helvetica, Arial, sans-serif;
  font-size: 15px;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.38);
  -webkit-user-select: none;
  user-select: none;
}

/* One rule above Communities, not one under each label. It belongs to the seam
   BETWEEN the groups, so it only exists while something shows above. */
body.ws .ws-rail-group:not([hidden]) ~ #ws-rail-spaces::before {
  content: "";
  flex-shrink: 0;
  height: 1px;
  margin: 6px 16px 16px;
  background: var(--ws-seam);
}

/* Collapsed: the words go, the divider between the groups carries the rhythm. */
body.ws.ws-rail-collapsed .ws-rail-group-label {
  display: none;
}

/* The seam between the shortcut strip and the nav tabs below it. */
body.ws #ws-rail-shortcuts {
  border-bottom: 1px solid var(--ws-seam);
  margin-bottom: 8px;
}



.ws-rail-item {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0 8px;
  padding: 2.5px 8px;
  border-radius: 10px;
  min-width: 0;
  transition: background-color 0.12s ease;
}

/* Rail rows carry `btn` for the cursor, which also drags in its blanket
   `opacity: .75` on hover. These rows answer hover with a surface and a
   brighter name, so the fade is a second, contradictory signal — invisible on a
   photo avatar and obvious on a flat placeholder glyph. */
.ws-rail-item:hover,
body.ws .chatlist-community-head:hover {
  opacity: 1;
}

.ws-rail-item:hover {
  background-color: var(--ws-hover);
}

.ws-rail-item.active {
  background-color: var(--ws-active);
}

.ws-rail-item-avatar {
  width: 26px;
  height: 26px;
  flex-shrink: 0;
  object-fit: cover;
  border-radius: 50%;
  margin: 0;
}

/* Communities read as rounded squares, DMs as circles: a room is not a person.
   ui/Avatar.svelte writes an inline `border-radius: 50%`, hence !important. */
.ws-rail-item.is-community .ws-rail-item-avatar {
  border-radius: 8px !important;
}

/* White is the resting state: every row in this strip has something waiting in
   it, and `is-quiet` below is the one case that does not. */
.ws-rail-item-name {
  flex: 1;
  min-width: 0;
  font-size: 14px;
  font-weight: 400;
  text-align: left;
  color: #fff;
}

.ws-rail-item-name .emoji {
  width: 15px;
  height: 15px;
  vertical-align: -3px;
  margin: 0;
}

.ws-rail-item:hover .ws-rail-item-name,
.ws-rail-item.active .ws-rail-item-name {
  color: #fff;
}

.ws-rail-item-badge {
  flex-shrink: 0;
  min-width: 20px;
  padding: 0 6px;
  border-radius: 10px;
  background-color: var(--primary-color, #33db98);
  color: #000;
  font-size: 11px;
  font-weight: 600;
  line-height: 19px;
  text-align: center;
}

/* Unread with nobody calling your name: a dot says the room is awake without
   asking you to act. It sits where the count would, so a room that starts
   pinging swaps the mark in place rather than shifting the row. */
.ws-rail-item-badge.is-dot {
  min-width: 7px;
  width: 7px;
  height: 7px;
  padding: 0;
  border-radius: 50%;
  background-color: rgba(255, 255, 255, 0.32);
  margin-right: 6px;
}

/* A community nobody has posted in since you last looked recedes: the rail is
   scanned for what changed, and 36 equally bright rows hide the two that did. */
.ws-rail-item.is-quiet .ws-rail-item-name {
  color: #b2b2b2;
}

/* Avatars never fade: standing is the name's job alone, and dimming the picture
   said the same thing twice while making the room look switched off. */
.ws-rail-item.is-quiet:hover .ws-rail-item-name,
.ws-rail-item.is-quiet.active .ws-rail-item-name {
  color: #fff;
}

/* Muted sits below quiet: quiet is "nothing new", muted is "and do not tell me". */
.ws-rail-item.is-muted .ws-rail-item-name {
  color: rgba(255, 255, 255, 0.42);
}

.ws-rail-item.is-muted:hover .ws-rail-item-name,
.ws-rail-item.is-muted.active .ws-rail-item-name {
  color: #fff;
}

/* Muted rooms still count, they just stop shouting. */
.ws-rail-item-badge.muted {
  background-color: rgba(255, 255, 255, 0.16);
  color: rgba(255, 255, 255, 0.75);
}

/* Collapsed: avatars only, with the badge as a corner dot. The row becomes a
   SQUARE centred on the rail's own axis — a text row's rectangle reads as a
   mis-sized pill once the label is gone, and the nav tabs below are centred on
   that axis. */
body.ws.ws-rail-collapsed .ws-rail-item {
  justify-content: center;
  gap: 0;
  position: relative;
  width: 40px;
  height: 40px;
  padding: 0;
  margin: 0 auto;
  border-radius: 12px;
}

/* At icon-only size the avatar IS the affordance, so it fills its square.
   ui/Avatar.svelte writes width/height inline alongside the radius, hence
   !important here too. */
body.ws.ws-rail-collapsed .ws-rail-item-avatar {
  width: 32px !important;
  height: 32px !important;
}

/* Radius tracks the size, or a 32px tile reads as a circle with corners. */
body.ws.ws-rail-collapsed .ws-rail-item.is-community .ws-rail-item-avatar {
  border-radius: 10px !important;
}

body.ws.ws-rail-collapsed .ws-rail-item-name {
  display: none;
}

body.ws.ws-rail-collapsed .ws-rail-item-badge {
  position: absolute;
  /* Centred on the avatar's corner (the avatar sits 4px inside the square row),
     cut out from it by a ring in the rail's own colour. */
  top: 0;
  right: 0;
  min-width: 10px;
  width: 10px;
  height: 10px;
  padding: 0;
  margin: 0;
  border-radius: 50%;
  /* The rail's painted colour: the app's #030303 under the rail tint. */
  box-shadow: 0 0 0 2px #090909;
  font-size: 0;
  line-height: 0;
  overflow: hidden;
}

/* With no name beside it, the quiet dot has to carry the whole signal itself. */
body.ws.ws-rail-collapsed .ws-rail-item-badge.is-dot {
  background-color: rgba(255, 255, 255, 0.9);
}

body.ws.ws-rail-collapsed .ws-rail-item-badge.muted {
  background-color: rgba(255, 255, 255, 0.45);
}

/* ── Rail arrangement: drag to reorder, drop onto a community to make a folder.
   The gesture and its look are the emoji pack rail's (lib/reorder.js): pan-y until
   a hold arms, a lift once armed, a glowing line where the drop lands. */
.ws-rail-item {
  position: relative;
}
#ws-rail-spaces .ws-rail-item {
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
}
#ws-rail-spaces .ws-rail-item,
#ws-rail-spaces .ws-rail-item img {
  -webkit-user-drag: none;
}
#ws-rail-spaces .ws-rail-item.is-dragging { opacity: 0.3; }
#ws-rail-spaces .ws-rail-item.is-drag-armed {
  transform: scale(1.04);
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.5);
  transition: transform 120ms ease-out, box-shadow 120ms ease-out;
  z-index: 3;
}
.ws-rail-ghost {
  opacity: 0.92;
  transition: none !important;
  background-color: #161616;
  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.55);
}
/* ::after, so the badge and the row's own states keep their pseudo-elements. */
#ws-rail-spaces .ws-rail-item.drop-above::after,
#ws-rail-spaces .ws-rail-item.drop-below::after {
  content: '';
  position: absolute;
  left: 4px;
  right: 4px;
  height: 3px;
  border-radius: 2px;
  background-color: var(--icon-color-primary);
  box-shadow: 0 0 8px var(--icon-color-primary);
  pointer-events: none;
}
#ws-rail-spaces .ws-rail-item.drop-above::after { top: -2px; }
#ws-rail-spaces .ws-rail-item.drop-below::after { bottom: -2px; }
/* Held over the middle of a row: the two become (or join) a folder. */
#ws-rail-spaces .ws-rail-item.drop-combine {
  background-color: color-mix(in srgb, var(--icon-color-primary) 16%, transparent);
  box-shadow: inset 0 0 0 2px var(--icon-color-primary);
}

/* A folder. Uncoloured, it is neutral grey: the accent is reserved for what
   needs your attention, and a folder is only furniture. */
.ws-rail-folder {
  --folder-tint: var(--folder-color, #9a9a9a);
  margin: 0 4px;
  border-radius: 12px;
  transition: background-color 0.24s ease;
}
.ws-rail-folder .ws-rail-item {
  margin: 0 4px;
}
/* Open, the folder is a band of its colour holding its communities. */
.ws-rail-folder.is-open {
  background-color: color-mix(in srgb, var(--folder-tint) 9%, transparent);
}
.ws-rail-folder-body {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}
.ws-rail-folder.is-open > .ws-rail-folder-body {
  grid-template-rows: 1fr;
}
.ws-rail-folder-inner {
  min-height: 0;
  overflow: hidden;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.2s ease, transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}
.ws-rail-folder.is-open .ws-rail-folder-inner {
  opacity: 1;
  transform: none;
  padding-bottom: 4px;
}

/* Shut: up to four of its communities in a tile of the folder's colour. Open: the
   tile becomes the folder glyph, so the head reads as a lid over what it holds. */
.ws-rail-folder-tile {
  position: relative;
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  border-radius: 8px;
  background-color: color-mix(in srgb, var(--folder-tint) 26%, transparent);
  transition: background-color 0.24s ease;
}
.ws-rail-folder-grid {
  position: absolute;
  inset: 3px;
  display: grid;
  /* minmax(0, …): a plain 1fr floors at the image's own size, which let two
     members stretch down both rows. Four fixed slots, filled in reading order,
     the empty ones left empty, as an iOS folder does. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  grid-template-rows: repeat(2, minmax(0, 1fr));
  gap: 2px;
  transition: opacity 0.2s ease, transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}
/* ui/Avatar.svelte writes its size and radius inline, hence !important. */
.ws-rail-folder-grid .ws-rail-folder-mini {
  /* Height from the width, not a percentage: WebKit resolves a grid item's
     percentage height against the whole grid rather than its row. */
  width: 100% !important;
  height: auto !important;
  aspect-ratio: 1;
  min-width: 0;
  margin: 0;
  border-radius: 30% !important;
  object-fit: cover;
}
.ws-rail-folder-glyph {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 0.2s ease, transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}
.ws-rail-folder-glyph .icon {
  width: 60%;
  height: 60%;
  background-color: var(--folder-tint);
}
.ws-rail-folder-tile.is-open {
  background-color: transparent;
}
.ws-rail-folder-tile.is-open .ws-rail-folder-grid {
  opacity: 0;
  transform: scale(0.7);
}
.ws-rail-folder-tile.is-open .ws-rail-folder-glyph {
  opacity: 1;
  transform: none;
}

body.ws.ws-rail-collapsed .ws-rail-folder {
  margin: 0 auto;
  width: 48px;
}
body.ws.ws-rail-collapsed .ws-rail-folder .ws-rail-item {
  margin: 0 auto;
}
body.ws.ws-rail-collapsed .ws-rail-folder-tile {
  width: 32px;
  height: 32px;
  border-radius: 10px;
}
body.ws.ws-rail-collapsed .ws-rail-folder-grid {
  inset: 4px;
  gap: 2px;
}

/* Nav rows: icon-left + label, with a neutral pill on the active tab. The row
   is the collapsed tile's height, and its 14px left inset puts the glyph on the
   pixel a 40px tile centres it on, so collapsing moves nothing but the label. */
body.ws #navbar .navbar-btn {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 11px;
  margin: 0 8px;
  height: 40px;
  padding: 0 10px 0 14px;
  border-radius: 6px;
  flex-shrink: 0;
  transition: background-color 0.12s ease, opacity 0.12s ease;
}

body.ws #navbar .navbar-btn:hover {
  background-color: var(--ws-hover);
}

/* Selected tab reads as a neutral surface, not an accent: the accent is spent on
   unread badges and the open conversation, and a green tab competed with both. */
body.ws #navbar .navbar-btn:not(.navbar-btn-inactive) {
  background-color: var(--ws-hover);
}

body.ws #navbar .navbar-icon {
  width: 28px;
  height: 28px;
  margin: 0;
  flex-shrink: 0;
}

body.ws #navbar .navbar-text {
  margin: 0;
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  text-align: left;
}

body.ws #navbar .navbar-btn .ws-rail-item-badge {
  margin-left: auto;
}

/* The dot rides the row's right edge here, not the icon's corner. */
body.ws #navbar .navbar-btn .update-notification-dot {
  top: 50%;
  right: 10px;
  margin-top: -4px;
}

/* Collapse is just another setting, so it sits with them rather than exiled to
   the bottom. The spacer stops pushing; the account chip anchors itself. */
body.ws #ws-rail-spacer {
  display: none;
}

body.ws #ws-rail-collapse {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 11px;
  margin: 0 8px;
  height: 40px;
  padding: 0 10px 0 14px;
  border-radius: 6px;
  flex-shrink: 0;
  opacity: 0.4;
  transition: opacity 0.12s ease, background-color 0.12s ease;
}

body.ws #ws-rail-collapse:hover {
  opacity: 1;
  background-color: var(--ws-hover);
}

/* The window is too narrow for the rail to expand: nothing to toggle. Coming back is
   a fade, not a pop: the rail's width jumps at the same moment and the toggle
   arriving with it read as a flash. */
body.ws.ws-rail-locked #ws-rail-collapse {
  opacity: 0;
  pointer-events: none;
}

@keyframes ws-rail-toggle-in {
  from { opacity: 0; }
  to { opacity: 0.4; }
}

body.ws:not(.ws-rail-locked) #ws-rail-collapse {
  animation: ws-rail-toggle-in 0.4s ease;
}

body.ws #ws-rail-collapse .navbar-icon {
  width: 28px;
  height: 28px;
  margin: 0;
  flex-shrink: 0;
  transition: transform 0.18s cubic-bezier(0.33, 1, 0.68, 1);
}

body.ws.ws-rail-collapsed #ws-rail-collapse .navbar-icon {
  transform: rotate(180deg);
}

/* No seam above the account chip: the nav tabs and your own row are the same
   band of controls, and a line between them split one thing into two. */
body.ws #ws-rail-account {
  display: block;
  flex-shrink: 0;
  /* Holds the floor now that the spacer is gone. */
  margin-top: auto;
  padding: 8px;
}

/* The re-parented #account block: left-aligned name over status, full width.
   The status sits one avatar + gap in from the edge so it lines up under the NAME,
   which means the name row itself has to be flush left (it ships centred). */
body.ws #ws-rail-account #account {
  padding: 0;
  box-sizing: border-box;
}

body.ws #ws-rail-account .profile-header-info {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  max-width: 100%;
  min-width: 0;
  width: 100%;
  overflow: hidden;
}

body.ws #ws-rail-account .profile-header-name-row {
  display: flex;
  align-items: center;
  justify-content: flex-start;
  gap: 8px;
  width: 100%;
  min-width: 0;
  /* The list row's top margin would sit the avatar 5px low in the collapsed tile. */
  margin: 0;
}

body.ws #ws-rail-account #account-name {
  margin: 0;
  max-width: 100%;
  font-size: 14px;
  text-align: left;
}

body.ws #ws-rail-account #account-status {
  width: 100%;
  box-sizing: border-box;
  margin: 0;
  /* No indent: the status shares the name's grid column, which already clears
     the avatar. The old 30px cleared a 22px avatar sitting on the line above. */
  padding-left: 0;
  font-size: 12px;
  text-align: left;
}

/* Collapsed: icons only. The account chip keeps its avatar (that IS its icon)
   and drops just the text. */
body.ws.ws-rail-collapsed #navbar .navbar-text,
body.ws.ws-rail-collapsed #ws-rail-collapse .navbar-text,
body.ws.ws-rail-collapsed #ws-rail-account #account-name,
body.ws.ws-rail-collapsed #ws-rail-account #account-status {
  display: none;
}

/* Collapsed, every row in the column is the same 40px tile as a rail shortcut —
   tabs, the collapse toggle and the account alike — so the whole rail reads as
   one column of squares rather than shortcuts above a stack of wide pills. */
body.ws.ws-rail-collapsed #navbar .navbar-btn,
body.ws.ws-rail-collapsed #ws-rail-collapse {
  justify-content: center;
  gap: 0;
  width: 40px;
  height: 40px;
  padding: 0;
  margin: 0 auto;
  border-radius: 12px;
}

body.ws.ws-rail-collapsed #ws-rail-account {
  display: flex;
  justify-content: center;
}

/* The account is a tile the width of the others and the height of its expanded
   self, so the avatar sits on the same pixel in both states; the name and status
   are already hidden at this width. */
body.ws.ws-rail-collapsed #ws-rail-account #account {
  justify-content: center;
  width: 40px;
  height: 54px;
  padding: 0;
  margin: 0 auto;
  border-radius: 12px;
}

body.ws.ws-rail-collapsed #ws-rail-account .profile-header-info,
body.ws.ws-rail-collapsed #ws-rail-account .profile-header-name-row {
  width: auto;
  align-items: center;
}

/* Collapsed, the column's own controls are bare glyphs: no fill at rest, on hover
   or when selected, and no fade out of the expanded fill. Standing is the glyph's
   colour alone. The shortcuts keep their tiles; an avatar needs its plate. */
body.ws.ws-rail-collapsed #navbar .navbar-btn,
body.ws.ws-rail-collapsed #navbar .navbar-btn:hover,
body.ws.ws-rail-collapsed .ws-rail-messages,
body.ws.ws-rail-collapsed .ws-rail-messages:hover,
body.ws.ws-rail-collapsed #ws-rail-collapse,
body.ws.ws-rail-collapsed #ws-rail-collapse:hover,
body.ws.ws-rail-collapsed #ws-rail-account #account,
body.ws.ws-rail-collapsed #ws-rail-account #account:hover {
  background-color: transparent;
  border: 0;
  box-shadow: none;
  transition: none;
}

/* ========================= 6. The list resizer =========================== */

body.ws #ws-list-resize {
  display: block;
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  width: 11px;
  margin-right: -5px;
  z-index: 45;
  cursor: col-resize;
  position: relative;
}

body.ws #ws-list-resize::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: 5px;
  width: 1px;
  background-color: var(--accent-color, #59fcb3);
  opacity: 0;
  transition: opacity 0.12s ease;
}

body.ws #ws-list-resize:hover::after,
body.ws.ws-resizing #ws-list-resize::after {
  opacity: 0.7;
}

/* A drag must not select list text or hand the pointer to a row underneath. */
body.ws.ws-resizing {
  cursor: col-resize;
  -webkit-user-select: none;
  user-select: none;
}

body.ws.ws-resizing #chats,
body.ws.ws-resizing #chat {
  pointer-events: none;
}

/* Column changes snap rather than animate. Transitioning `grid-template-columns`
   relayouts every pane on every frame, and column 3 holds the message list — a
   1000-row relayout per frame for 180ms of easing is the worst trade in the file.
   Panes that want motion should animate transform/opacity instead. */

/* ====================== 7. Empty conversation pane ======================= */

#ws-conv-empty {
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  min-height: 0;
  -webkit-user-select: none;
  user-select: none;
}

/* Full stacked lockup here (unlike the rail's cropped shield) — at this size
   the wordmark reads, and the watermark wants the whole mark. */
#ws-conv-empty img {
  width: 128px;
  height: 132px;
  opacity: 0.1;
}

#ws-conv-empty p {
  margin: 0;
  font-size: 14px;
  color: rgba(255, 255, 255, 0.35);
}

/* ===================== 8. Widescreen list-row polish ===================== */

/* A DM row was drawn for a full-width phone list: a 50px avatar and 16/15px text
   have room there, and none at all in a 260px pane, where every name clipped to
   "Yuurin…" and the preview to four words. The channel rows next door read fine at
   the same width because a channel is a glyph and a word.

   So the row gets the pane's proportions: a 40px avatar, and type a step down.
   Nothing is squeezed structurally — it's the same row, sized for its column.
   min/max are the load-bearing pair here: `.chatlist-contact img` pins all four to
   50px, so setting width alone does nothing (the same cap the inline emoji rule
   already fights). */
body.ws #chat-list .chatlist-contact > div:first-child > img,
body.ws #chat-list .chatlist-contact > div:first-child > .placeholder-avatar {
  width: 40px !important;
  height: 40px !important;
  min-width: 40px !important;
  min-height: 40px !important;
  max-width: 40px !important;
  max-height: 40px !important;
  /* An inline image leaves descender space under itself, which the frame then
     counts as its own height: 7px that reads as the avatar sitting high, and that
     the status dot (anchored to the frame) follows down. Placeholders were already
     immune — styles.css makes those flex. */
  display: block;
}

/* The avatar's frame is a flex item, so it STRETCHED to the row's height and left a
   40px avatar pinned to its top. Sized to the avatar instead, it centres — and the
   dot goes back to the avatar's own corner rather than the frame's. */
body.ws #chat-list .chatlist-contact > div:first-child {
  align-self: center;
}

/* The row comes down with the avatar, keeping the proportion it was drawn at
   (50/62, now 40/50): the avatar fills the row's left end and the 30px corner —
   which clamps to half the height — closes around it. Left at 62px the same curve
   was drawn around empty space. The two text lines need 38 of the 40. */
body.ws #chat-list .chatlist-contact {
  height: 40px;
  padding: 4px;
}

body.ws .chatlist-contact-header h4 {
  font-size: 15px;
  line-height: 20px;
}

body.ws .chatlist-contact-preview > p {
  font-size: 13px;
  line-height: 18px;
}

/* The 120px reservation for avatar + timestamp + badge was tuned against a
   full-viewport list; in a 330px pane it ate the name — and it was spent whether or
   not anything used it. The unread pill is the only thing that ever sits there, and
   most rows are read, so the space was reserved for nothing on nearly every row.
   Flexing takes it back and hands it over the moment a pill does appear: the pill is
   a flex sibling, so the text gives up exactly its width and no more.

   The padding is the row's rounded end, not decoration. At 25px the corner cuts
   ~9px into where the two text lines sit — measured, against the 5px the row's own
   padding left — which is the ellipsis clipping the border. The pill never needs it:
   it sits on the vertical centre, where the curve intrudes nothing. */
body.ws .chatlist-contact-preview {
  width: auto;
  flex: 1;
  min-width: 0;
  padding-right: 10px;
  box-sizing: border-box;
}

body.ws .chatlist-contact-count {
  flex-shrink: 0;
}

/* `margin: auto` on both sides floated the timestamp into the middle of the
   gap it was meant to sit at the end of. */
body.ws .chatlist-contact-timestamp {
  margin-left: auto;
  margin-right: 8px;
}

/* The pair of composer buttons has less room per button here, and "Group Chat"
   was wrapping to a second line while "New Chat" stayed on one. */
body.ws .new-chat-btn {
  font-size: 13px;
}

/* Below about a 300px pane the two buttons cannot both hold their label on one
   line, so they stop sharing a line and become full-width rows instead. The basis
   drives it: two of them plus the gap outgrow the container right about where the
   text would wrap, so flex-wrap decides and no width threshold lives in JS.

   132px, not the 128px "Group Chat" actually measures. A basis equal to the label's
   own width leaves the buttons exactly 128.00px wide on the pane width where they
   unstack — and the label needs a hair over that, so one single pane width still
   wrapped. The 4px is the margin that window closes into. nowrap is the backstop:
   at a width nobody predicted the label clips rather than reflowing the row.

   Both inline `width: 50%` and the inline margins have to go for the basis to mean
   anything. */
body.ws #chat-new-actions {
  flex-wrap: wrap;
  gap: 8px;
}

body.ws #chat-new-actions .new-chat-btn {
  flex: 1 1 132px;
  min-width: 0;
  width: auto !important;
  margin: 0 !important;
}

body.ws #chat-new-actions .new-chat-btn > span:last-child {
  white-space: nowrap;
}

body.ws .new-chat-btn .icon {
  width: 24px;
  height: 24px;
  flex-shrink: 0;
}

/* The list stays on screen while a chat is open, so the open chat needs to
   read as selected. */
body.ws .chatlist-contact.ws-active {
  background-color: color-mix(in srgb, var(--accent-color, #59fcb3) 9%, transparent);
  border-color: color-mix(in srgb, var(--accent-color, #59fcb3) 45%, transparent);
}

/* Mobile taps a row and leaves; a desktop pointer lives over the list. */
body.ws .chatlist-contact {
  cursor: pointer;
}

/* ── Member list ──────────────────────────────────────────────────────────
   The details header carries the search: its title and status are hidden in
   this column (the community is named one column left), so without it the bar
   is height spent on nothing. */
body.ws #group-overview > .chat-header .ws-member-search {
  flex: 1;
  min-width: 0;
  margin: 0 14px;
  padding: 0 !important;
}

body.ws .ws-member-search input {
  width: 100%;
  box-sizing: border-box;
  padding: 9px 14px 9px 38px !important;
  border-radius: 9px;
  border: 1px solid var(--ws-seam) !important;
}

body.ws .ws-member-search .emoji-search-icon {
  left: 12px;
}

/* Rank marks sit with the name. Colours are the roles' own, not the theme's:
   a crown that changed hue per theme would stop meaning "owner". */
.member-rank-mark {
  position: relative;
  flex-shrink: 0;
  width: 15px;
  height: 15px;
  margin: 0 0 0 5px;
}

.member-rank-owner { background-color: #fce459; }
.member-rank-admin { background-color: #ef6a60; }

/* A bot is a fact about the account, not a rank — it recedes. */
.member-pick-bot {
  background-color: #5b6060 !important;
}

/* The details column's collapse chevron shares the header with the search. */
body.ws #group-overview > .chat-header .ws-member-search {
  margin-left: 54px;
}

/* The avatar carries the taller row rather than floating in it. */
body.ws #group-overview .member-pick-avatar {
  width: 29px !important;
  height: 29px !important;
}

body.ws #group-overview .compact-member-name {
  font-size: 15px;
}

/* ── Search: a slim pill, not a boxed field ───────────────────────────── */
body.ws .ws-member-search input {
  padding: 7px 14px 7px 36px !important;
  font-size: 13px;
  border-radius: 999px;
}

/* The icon carried a hardcoded `top` measured against the taller boxed field it
   used to live in; in a 34px pill that put it 10px below centre. `!important`
   because the undocked rule further up sets `left: 22px !important` for the
   search in its original home, and that offset is wrong inside the pill. */
body.ws #group-overview > .chat-header .ws-member-search .emoji-search-icon {
  left: 12px !important;
  top: 50% !important;
  transform: translateY(-50%) scale(0.8);
}

/* Slightly tighter than the two lines need, so the roster stays a list. */
/* One height for every row, status or not: a roster whose rhythm changes per
   person reads as broken alignment rather than as extra information. A member
   with nothing to say simply centres in the same box. */
body.ws #group-overview .member-pick-row {
  padding: 3px 10px;
  gap: 0;
  min-height: 46px;
  box-sizing: border-box;
}

/* The empty-state logo is no drag source: a press that misses a floating player's rounded
   corner lands on it. */
#ws-conv-empty img {
    -webkit-user-drag: none;
    user-select: none;
}
