/* ==========================================================================
   css/components/members-rail.css
   Split from app.html (build 2026.10.08.9-beta), original lines 2695-3044.
   Rules are in their ORIGINAL order; load order is defined in index.html.
   ========================================================================== */
/* ═══════════════════════════════════════
   MEMBERS SIDEBAR
═══════════════════════════════════════ */
.tray-area {
  width: 256px;
  flex-shrink: 0;
  background: var(--bg-chat, var(--bg-base));
  border-top: 1px solid var(--border-strong);
  /* Match the channel-list <-> channel-area divider (also --border-strong) so all three
     column seams are one weight, and it adapts to light mode. A hardcoded hex here (was
     #45464e) stayed dark on a light background - a harsh line in light mode. */
  border-left: 1px solid var(--border-strong);
  display: flex;
  flex-direction: column;
  /* VISIBLE, so the header's and the panel's tooltips can leave the tray - it is against the
     right edge of the window, so a bubble that cannot escape is a bubble you cannot read.
     Nothing inside needs this to clip: .members-list scrolls (and so clips) on its own, and
     #trayModeBody has its own overflow:hidden. The one case that does need it is `.collapsed`,
     which hides the tray by taking its width to 0 - so that keeps it, below. */
  overflow: visible;
  transition: width 0.2s ease;
}

.tray-area.collapsed { width: 0; overflow: hidden; }
/* Auto-hide the members list on narrower windows so the fixed sidebars never
   push content (and the title bar) off-screen. Above this width the members
   toggle works normally. */
@media (max-width: 1080px) { .tray-area { display: none !important; } }
/* DM profile panel is a nicety, not essential - on a small/phone screen it just
   squeezes the conversation, so drop it entirely (reliable screen-size gate, not
   device sniffing). Wins over .visible via !important. */
@media (max-width: 900px) { .dm-profile-panel, .dm-profile-panel.visible { display: none !important; } }

.tray-header {
  min-height: 54px;
  padding: 8px 12px;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}
.tray-header-title {
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  font-size: 13px;
  font-weight: 700;
  color: var(--text-secondary);
}
.members-stats {
  display: flex;
  gap: 12px;
}
.members-stat {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-muted);
}
.members-stat .stat-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}
/* When a custom background is active, brighten + halo the navigation/label chrome
   so it stays readable over a busy or dark background. Dim greys (text-muted) are
   unreadable over a dark image, so we also lift the colour - not just add a shadow.
   Display names, channel names and other coloured/stateful text are left alone. */
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .tray-header-title,
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .members-stat,
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .dm-nav-item,
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .ch-category-name,
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .server-name {
  color: rgba(255,255,255,.9) !important;
}
/* Member-list role categories keep their ROLE colour - just give them a halo so
   they stay readable over a busy background (don't recolour them). */
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .member-category {}
/* Keep the active/hover nav highlight readable too. */
html[data-bg]:not([data-theme="light"]) .dm-nav-item.active, html[data-bg]:not([data-theme="light"]) .dm-nav-item:hover { color: #fff !important; }
/* LIGHT theme over a gradient/image background: the white-text lift above would put
   white blurry text on pale panels ("cooked"). Use DARK text with a soft white halo. */
html[data-theme="light"][data-bg] .tray-header-title,
html[data-theme="light"][data-bg] .members-stat,
html[data-theme="light"][data-bg] .dm-nav-item,
html[data-theme="light"][data-bg] .ch-category-name,
html[data-theme="light"][data-bg] .server-name {
  color: rgba(24, 20, 38, .88) !important;
}
html[data-theme="light"][data-bg] .member-category {}
html[data-theme="light"][data-bg] .dm-nav-item.active, html[data-theme="light"][data-bg] .dm-nav-item:hover { color: #0d0b14 !important; }
/* Ticket hub text auto-adjusts over a custom background (no box - like the calendar):
   lift dim text to near-white and add a dark halo so it reads on light OR dark images. */
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .ticket-hub .tkh-title { color: #fff !important;}
html[data-bg]:not([data-theme="light"]) body:not(.theme-grad) .ticket-hub .tkh-text { color: rgba(255,255,255,.9) !important;}
html[data-bg]:not([data-theme="light"]) .ticket-hub .tkh-text span { color: rgba(255,255,255,.8) !important; }

.members-list { flex: 1; overflow-y: auto; padding: 8px 0; }

.member-category {
  padding: 16px 8px 4px 12px;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-muted);
}
.member-category.has-role-img { display: flex; align-items: center; gap: 6px; }
.role-cat-img { width: 16px; height: 16px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }

.member-item {
  display: flex;
  align-items: center;
  gap: 8px;
  height: 42px;
  box-sizing: border-box;
  padding: 0 8px 0 10px;
  border-radius: var(--radius-md);
  margin: 0 6px;
  cursor: pointer;
  transition: background 0.12s;
}
/* Off-screen member rows should cost nothing.
   Every visible member can carry an animated nameplate (plate-flow = background-position)
   AND an animated avatar decoration (deco-hue = filter:hue-rotate, deco-dash =
   stroke-dashoffset). All three repaint EVERY FRAME - the perf-mode comment above already
   identifies them as the bulk of Recalculate-Style + Paint. In a busy server most of those
   rows are scrolled out of sight, yet Chromium still styles, lays out and paints them, and
   re-does the lot on every member-list re-render (which is why one click spikes the
   renderer). content-visibility:auto makes it skip all of that for anything off-screen.
   The intrinsic size MUST equal the real row height or the scrollbar jumps as rows enter
   and leave - 42px, matching the rule above. */
.members-list .member-item {
  content-visibility: auto;
  contain-intrinsic-size: 0 42px;
}
.member-item:hover { background-color: var(--bg-active); }
.member-owner-crown { margin-left: auto; flex-shrink: 0; display: flex; align-items: center; padding-left: 6px; }
/* Device icon (mobile/desktop/VR) sits on the RIGHT of the member row; crown is now inline-left of the name. */
.member-device-right { margin-left: auto; flex-shrink: 0; display: flex; align-items: center; padding-left: 6px; }

.member-avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  font-weight: 700;
  font-size: 11px;
  color: #fff;
  flex-shrink: 0;
  position: relative;
}

.member-avatar img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }

/* Client-side avatar shape preference (Settings > Appearance). Square = softly
   rounded corners. Covers the main avatar surfaces: self bar, message authors,
   member list, profile popout. Only affects the current viewer. */
body[data-pfp="square"] .user-avatar,
body[data-pfp="square"] .user-avatar img,
body[data-pfp="square"] .msg-avatar,
body[data-pfp="square"] .msg-avatar img,
body[data-pfp="square"] .member-avatar,
body[data-pfp="square"] .member-avatar img,
body[data-pfp="square"] .profile-avatar,
body[data-pfp="square"] .profile-avatar img,
body[data-pfp="square"] .profile-avatar-overlay,
body[data-pfp="square"] .voice-tile-avatar,
body[data-pfp="square"] .voice-tile-avatar img,
body[data-pfp="square"] .voice-sidebar-avatar,
body[data-pfp="square"] .voice-sidebar-avatar img,
body[data-pfp="square"] .voice-spotlight-avatar,
body[data-pfp="square"] .voice-spotlight-avatar img,
body[data-pfp="square"] .profile-popup-avatar,
body[data-pfp="square"] .profile-popup-avatar-inner,
body[data-pfp="square"] .profile-popup-avatar-inner img,
body[data-pfp="square"] .dm-profile-avatar,
body[data-pfp="square"] .dm-profile-avatar img { border-radius: 22% !important; }
.voice-spotlight-avatar { border-radius: 50%; }
.voice-spotlight-avatar img { border-radius: inherit; }

.member-status {
  position: absolute;
  /* The dot's centre sits at 84.375% of the avatar, exactly as the reference's mask places it,
     so a 16px dot on a 32px avatar overhangs the corner by 3px. */
  bottom: -3px;
  right: -3px;
  width: var(--status-dot);
  height: var(--status-dot);
  border-radius: 50%;
  border: var(--status-ring-w) solid var(--status-ring);
}
.status-online { background: var(--green); }
.status-idle { background: var(--yellow); }
.status-dnd { background: var(--red); }
.status-offline { background: var(--text-muted); }

/* ── STATUS INDICATORS (reference model) ───────────────────────────────────────────────
   The dot carries the SHAPE; the avatar carries a transparent hole. No ring colour exists
   anywhere, so nothing can mismatch the surface behind it - which is what kept breaking.
   --dot-size is set per call site (11px in the member list, 10px in the user panel) and
   everything else derives from it. */
.member-status, .user-status-dot {
  /* border-box so the 2px ring is counted INSIDE the declared size, which keeps the dot the
     same overall diameter as before the shapes went in. */
  box-sizing: border-box;
}
/* The silhouettes are drawn as OPAQUE overlays, never as mask cut-outs.
   A mask makes the cut-out genuinely transparent, and this dot is a CHILD of the avatar - so the
   slot in DND showed the user's profile picture straight through it, and idle/offline had the
   same hole. (The reference can use a transparent cut-out because its dot sits over a hole in
   the avatar rather than on top of it; Rift's markup puts the dot inside, which is also why the
   avatar-hole approach failed here.)
   So each shape is a ::after filled with --status-ring - the same opaque colour as the ring
   around the dot - giving the identical silhouette with nothing showing through.
   The dot is position:absolute, which already makes it the containing block for these. */
/* The overlay is position:absolute, so the DOT has to be its containing block. Most call sites
   position their dot already (.member-status is absolute, .usi-dot / .user-menu-dot are relative,
   the popout dot is absolute inline) - but a call site that emits a bare styled <span> does not,
   and the overlay then resolved against the nearest positioned ancestor instead. In the full
   profile modal that was the whole card, so `inset: 26%` drew a circle the size of the panel.
   :where() has ZERO specificity, so every dot that sets its own position still wins and only an
   unpositioned one picks this up. position:relative with no offsets changes no layout. */
:where(.status-online, .status-idle, .status-dnd, .status-offline) {
  position: relative;
}
.status-idle::after,
.status-dnd::after,
.status-offline::after {
  content: '';
  position: absolute;
  background: var(--status-ring);
  pointer-events: none;
}
/* The geometry below is NOT eyeballed. The reference defines these as SVG masks in its DOM, in
   objectBoundingBox units, so each one is an exact fraction of the dot:
     idle     <circle cx=0.25 cy=0.25 r=0.375>              bite at 25%/25%, diameter 75%
     dnd      <rect x=0.125 y=0.375 w=0.75 h=0.25 rx=0.125> a PILL, 75% wide, 25% tall
     offline  <circle cx=0.5 cy=0.5 r=0.25>                 hole diameter 50%
   Percentages here resolve against the dot's PADDING box - the coloured disc inside the 2px
   ring - and that disc is exactly what the reference's whole dot is, so they map one to one. */
/* online: a plain filled circle - no overlay at all. */
/* idle: a crescent. The bite is a 75%-diameter circle centred at 25%,25%, so it spans
   25% +/- 37.5% = -12.5% to 62.5% on both axes. */
.status-idle::after {
  inset: -12.5% 37.5% 37.5% -12.5%;
  border-radius: 50%;
}
/* dnd: a pill across the middle, 75% wide and 25% tall, with fully rounded ends (the mask's
   rx of 0.125 is half the bar's height). It was 58% wide, 20% tall and square-cornered - a
   stubby dash where the reference has a full-width bar. */
.status-dnd::after {
  inset: 37.5% 12.5%;
  border-radius: 999px;
}
/* offline / invisible: a hollow ring.
   Drawn as ONE radial gradient rather than border + border-radius + an ::after hole. Those are
   three independently-rounded circles, and at a 16px dot the browser does not rasterise them
   concentrically - the dark ring came out thicker on one side and the grey annulus varied
   around the circle ("one side is bigger and the rest different").
   One gradient is concentric by construction, and the stops sit on whole-pixel radii of the
   8px-radius dot: hole 0-3, colour 3-5, ring 5-8. `background-color` still carries the status
   colour, so the live colour update keeps working; this only decides where it shows.
   Idle and DND keep their ::after overlays - they are not concentric circles. */
.status-offline {
  /* `closest-side` makes the gradient's ray exactly the dot's radius, so the stops below are
     proportions and the ring scales with the dot. They were absolute radii off --status-dot,
     which was right for the 16px dot this was built for and wrong for every bigger one - on the
     28px popout dot the bands landed well inside it and the ring read tiny. */
  /* A background's positioning area is the PADDING box, so `closest-side` measures to the edge
     of the 10px COLOURED DISC - a 5px ray - not to the edge of the 16px dot. Earlier stops were
     computed against the 8px dot radius, which is why the annulus kept rendering one or two
     pixels wide instead of the width they described, and why an outer ring band was in here at
     all (the 3px border already draws that).
     Against the real ray this is just the reference's offline dot: hole to 50%, colour to the
     edge - a 5px hole with a 2.5px annulus on a 10px disc. It scales with any dot size.
     The stop is FEATHERED (44%->56%, about 0.6px) rather than hard. With a hard stop every pixel
     is fully in or fully out, and at a 5px-diameter hole the pixels whose centres fall inside
     form an exact SQUARE - there is no curvature left to sample at that size. The partial
     coverage is what reads as round. The outer edge needs no feather: border-radius already
     antialiases it. */
  background-image: radial-gradient(circle closest-side at 50% 50%,
    var(--status-ring) 0 44%,
    transparent        56% 100%);
}
.status-offline::after { content: none; }

/* -- CUT-OUTS, where the dot is NOT sitting on an avatar -----------------------------------
   The silhouettes above are an OPAQUE patch in --status-ring. On an avatar that is right: the
   ring around the dot is the same flat colour, so the patch is continuous with it. On a menu
   row or a profile card it is wrong - under a gradient theme those surfaces are a wallpaper
   repaint, so no flat colour can match and the bite reads as a mismatched dark blob.
   The reference never has that problem because its cut-outs are real MASKS; the bite shows
   whatever is behind it. These are its own mask shapes, transcribed as a path whose outer
   rectangle and inner bite are combined with fill-rule:evenodd, so the bite is a hole:
     idle    circle cx=.25 cy=.25 r=.375
     dnd     pill   x=.125 y=.375 w=.75 h=.25 rx=.125
     offline circle cx=.5  cy=.5  r=.25
   The dot already has border-radius:50%, so the mask only has to punch the bite - it does not
   need to redraw the outer circle. Dots ON an avatar deliberately do NOT get this: a
   transparent bite there shows the profile picture straight through it. */
.usi-dot.status-idle, .user-menu-dot.status-idle, .status-cutout.status-idle {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h100v100H0z M25 -12.5a37.5 37.5 0 1 0 0 75a37.5 37.5 0 1 0 0 -75z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h100v100H0z M25 -12.5a37.5 37.5 0 1 0 0 75a37.5 37.5 0 1 0 0 -75z'/%3E%3C/svg%3E");
}
.usi-dot.status-dnd, .user-menu-dot.status-dnd, .status-cutout.status-dnd {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h100v100H0z M25 37.5H75a12.5 12.5 0 0 1 0 25H25a12.5 12.5 0 0 1 0 -25z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h100v100H0z M25 37.5H75a12.5 12.5 0 0 1 0 25H25a12.5 12.5 0 0 1 0 -25z'/%3E%3C/svg%3E");
}
.usi-dot.status-offline, .user-menu-dot.status-offline, .status-cutout.status-offline {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h100v100H0z M50 25a25 25 0 1 0 0 50a25 25 0 1 0 0 -50z'/%3E%3C/svg%3E"); mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath fill='%23fff' fill-rule='evenodd' d='M0 0h100v100H0z M50 25a25 25 0 1 0 0 50a25 25 0 1 0 0 -50z'/%3E%3C/svg%3E");
}
.usi-dot.status-idle, .user-menu-dot.status-idle, .status-cutout.status-idle,
.usi-dot.status-dnd, .user-menu-dot.status-dnd, .status-cutout.status-dnd,
.usi-dot.status-offline, .user-menu-dot.status-offline, .status-cutout.status-offline {
  -webkit-mask-size: 100% 100%; mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
}
/* The opaque patch would be clipped to the masked shape anyway, but drop it so the two
   mechanisms can never both be live on one dot. */
.usi-dot::after, .user-menu-dot::after, .status-cutout::after { display: none; }

/* The hole. Punched only when the avatar actually carries a dot, or every avatar without
   one would get a bite taken out of its corner. The dot sits at bottom/right -1px, so its
   centre is 4px in from each edge for an 11px dot; the hole is the dot radius plus the gap. */
/* NO avatar mask here, deliberately. A `mask` clips the element AND its whole subtree, and
   the dot is a CHILD of the avatar sitting exactly where the hole goes - so punching the
   hole punched out the dot. (Verified by screenshotting the dot's own rect: element present,
   11x11, background rgb(59,165,93), nothing painted.) The reference can use a hole because
   its dot is a SIBLING of the masked avatar, not a child; matching that means hoisting the
   dot out in six render call sites. Logged in TODO.md. Until then the ring stays - a ring
   that exists beats a hole that erases the dot. */


.member-info { flex: 1; min-width: 0; }

.member-name {
  font-size: 13px;
  font-weight: 500;
  line-height: 1.2;
  color: var(--text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.member-item:hover .member-name { color: var(--text-primary); }
.member-name.mod { color: #faa61a; }
.member-name.admin { color: var(--red); }
/* Custom Display Name Styles carry their own font-size (up to ~1.6em) inline. In the
   compact member list that made styled names tower over the plain ones and look messy, so
   pin every name to the row's size here - the custom font, colour and effect still show,
   just at one consistent, tidy size. (Messages/profile keep the full styled size.) */
.member-item .member-name { font-size: 13px !important; line-height: 1.2 !important; }

.member-activity {
  font-size: 11px;
  line-height: 1.15;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* DM search */
.dm-header { padding: 12px 12px 8px; }
.dm-nav-item {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px; border-radius: var(--radius-sm);
  font-size: 14px; font-weight: 500; color: var(--text-muted);
  cursor: pointer; margin: 1px 4px; transition: background .12s, color .12s, transform .15s var(--ease-out, ease);
}
.dm-nav-item:hover { background: var(--bg-hover); color: var(--text-secondary); transform: translateX(2px); }
.dm-nav-item:active { transform: translateX(2px) scale(0.98); }
.dm-nav-item.active { background: var(--bg-active); color: var(--text-primary); }

.dm-search {
  width: 100%;
  padding: 6px 10px;
  background: var(--bg-void);
  border: none;
  border-radius: var(--radius-sm);
  font-size: 13px;
  color: var(--text-primary);
  box-sizing: border-box;
}
.dm-search::placeholder { color: var(--text-muted); }
.dm-search:focus { outline: none; }

/* Tooltip */
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute;
  left: calc(100% + 10px);
  top: 50%;
  transform: translateY(-50%) translateX(-4px);
  background: var(--bg-surface);
  color: var(--text-primary);
  padding: 5px 10px;
  border-radius: var(--radius-sm);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.18s var(--ease-out, ease), transform 0.18s var(--ease-out, ease);
  border: 1px solid var(--border-strong);
  box-shadow: var(--shadow-md);
  z-index: var(--z-tooltip, 1001);
}
[data-tip]:hover::after { opacity: 1; transform: translateY(-50%) translateX(0); }

/* WS status indicator */
#wsStatus {
  position: fixed;
  bottom: 8px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--bg-surface);
  border: 1px solid var(--border);
  border-radius: 20px;
  padding: 6px 14px;
  font-size: 12px;
  color: var(--text-secondary);
  display: none;
  align-items: center;
  gap: 6px;
  z-index: 100;
}
#wsStatus.visible { display: flex; }
#wsStatus .ws-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--yellow); }
#wsStatus.connected .ws-dot { background: var(--green); }
#wsStatus.error .ws-dot { background: var(--red); }

/* ── TRAY PANEL ────────────────────────────────────────────────────────────────────────────
   The tray's counterpart to the user panel, and deliberately the same card: same height,
   fill, border, radius and 6px inset, so the two bottom corners of the app read as a pair.
   NOT position:fixed like the user panel, though - the tray area is already a flex column, so
   this is just its last child with flex-shrink:0. The user panel is fixed because it has to
   span the server rail AND the channel rail, which is not a problem this one has.
   It is hidden whenever the tray itself is, and in DMs. What is in it: the custom themes and
   settings. The member counts are NOT - they belong in the tray header, where they read as
   part of the heading rather than as a readout at the far end of the list. */
.tray-panel {
  flex-shrink: 0;
  height: 58px;
  margin: 0 6px 6px;
  padding: 0 6px;
  justify-content: center;
  background: #1e1f24;
  border: 1px solid var(--border-strong);
  border-radius: 8px;
  display: flex;
  align-items: center;
  gap: 8px;
}
html[data-theme="light"] .tray-panel { background: var(--bg-elevated); }
/* Under a custom theme it is painted by the shared floating-chrome rule in GlobalRiftCss.css,
   listed right next to .user-panel - the pair has to match or the two bottom corners stop
   looking like the same component. It is NOT a flat --bg-elevated slab there: that was the
   grey-wash case, next to a user panel that carries the wallpaper. */
/* A tray with no member list has nothing to report. */
body.dm-call-active .tray-panel { display: none !important; }
