/* ==========================================================================
   css/components/channel-rail.css
   Split from app.html (build 2026.10.08.9-beta), original lines 538-910.
   Rules are in their ORIGINAL order; load order is defined in index.html.
   ========================================================================== */
/* ═══════════════════════════════════════
   CHANNEL SIDEBAR
═══════════════════════════════════════ */
/* One source of truth for the sidebar's rounded corner: the radius itself, the divider's arc
   and the frame patch behind it (see #app::before below) all read this, so they cannot drift
   apart and leave a sliver of wallpaper showing. */
:root { --sidebar-corner: 8px; }
.channel-rail {
  width: var(--ch-w, 193px);
  flex-shrink: 0;
  background: var(--bg-deep);
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  /* Separator lines on the channel list's TOP + LEFT edges, meeting at a smooth 8px
     rounded corner (blue horizontal + orange vertical connect). The top line only
     spans the content (never over the server rail), and the left line is the single
     server-area <-> channel-list divider (folder-safe). */
  /* padding, not border: the visible line is drawn by ::before below, which needs the
     padding box to start at the element's top edge (overflow:hidden clips to it, so the
     pseudo cannot be pulled above it). 1px here keeps the layout identical to the border
     it replaces. */
  padding-top: 1px;
  border-radius: var(--sidebar-corner) 0 0 0;
  position: relative;
}
/* The server-area <-> channel-list divider as a pseudo-element that STOPS at the user
   panel's top edge, instead of a full-height border-left that ran down behind the
   floating panel (the "separator goes under the user panel / list ends short" issue).
   --bottom-bar-h is the live-measured panel height, set on this element in JS. */
.channel-rail::before {
  /* ONE box draws the whole L - top edge, the 8px arc, and the left edge - so the line is
     continuous around the corner. It used to be a straight bar starting at top:8px (right
     where the radius ended) while the top edge was a separate border-top on the element,
     and a border-top with a radius and no matching border-left tapers away across the arc
     instead of turning the corner. Bottom still stops at the floating panel. */
  content: ''; position: absolute; left: 0; top: 0; right: 0;
  bottom: calc(var(--bottom-bar-h, 70px) - 2px);
  border-top: 1px solid var(--border-strong);
  border-left: 1px solid var(--border-strong);
  border-start-start-radius: var(--sidebar-corner);
  /* see the banner rule below */
  pointer-events: none; z-index: 1;
}
/* A server BANNER supplies its own top edge, so the divider drawn across it reads as a
   stray box sitting on the artwork. The reference drops it for exactly this case:
   .sidebarList__5e434:has([data-has-banner=true][data-banner-visible=true]) { border-top: none }
   The corner arc goes with it - there is nothing left to turn - leaving only the vertical
   rail/channel-list divider, which is what should be there. */
.channel-rail:has(.server-header.has-banner)::before {
  border-top: none;
  border-start-start-radius: 0;
}
/* The pinned strip does NOT get the same treatment, and the first attempt at this was wrong.
   Taking the sidebar's top border away takes its corner arc with it, and the sidebar's
   BACKGROUND is still rounded - so what was left was a straight vertical line running past a
   curved edge, and the separator and the header's smooth corner stopped meeting.
   The strip's own border-bottom was the redundant one. .channel-area and .tray-area each draw
   their own border-top, so every column already supplies its top edge; the strip drawing one
   as well is what made it a double. It is dropped in pinned-server-rail.css instead, and the
   sidebar keeps the whole L - top edge, arc and left edge - exactly as before the strip. */
/* The frame rung BEHIND the sidebar's rounded top-left corner.
   The sidebar's background is clipped by its own radius and #app paints nothing, so under a
   custom theme that corner showed the wallpaper un-scrimmed - measured rgb(71,49,252) against
   the rail's rgb(43,31,134), twice as bright, which is the blue notch at the top of the server
   header. Nothing inside the sidebar can reach it: the wedge is outside the sidebar's rounded
   border box and overflow:hidden clips to that same box. So the parent fills it.
   The gradient is the sidebar's own corner circle - transparent inside it (where the sidebar
   paints, so no second scrim stacks there) and the frame rung outside it (the wedge). The 1px
   feather must be CENTRED on the arc - 7.5px to 8.5px for an 8px corner - so its rising alpha
   is the inverse of the sidebar's falling antialiasing and the two sum to one copy of the
   scrim. It used to run 7px to 8px, entirely inside the arc, which left the ring just outside
   8px carrying the patch's full scrim AND the sidebar's antialiased edge on top of it:
   measured 0.63-0.66 effective alpha against 0.565 on the frame and 0.604 in the sidebar.
   That is a 1px dark ring following the curve - the "why is it darker on the smooth corner".
   --bg-void is what the rail, the folder rail and the title bar already use, so the corner now
   reads as the frame wrapping around the sidebar - which is what the reference shows.
   Desktop only: under 769px the mobile nav transforms the rail and sidebar for the slide-in,
   which moves this corner. */
@media (min-width: 769px) {
  /* Safe: measured on the live app, ZERO absolutely-positioned descendants of #app currently
     resolve against the initial containing block, so nothing re-anchors. Positioning #app
     rather than using `fixed` keeps the patch tied to #app's own top edge, so it stays correct
     whatever the title bar's height is (32px native, absent on web). */
  #app { position: relative; }
  #app::before {
    content: '';
    position: absolute;
    top: 0;
    left: var(--rail-width);
    width: var(--sidebar-corner);
    height: var(--sidebar-corner);
    background: radial-gradient(
      circle var(--sidebar-corner) at 100% 100%,
      transparent 0 calc(var(--sidebar-corner) - 0.5px),
      var(--titlebar-bg, var(--bg-void)) calc(var(--sidebar-corner) + 0.5px));
    pointer-events: none;
  }
  /* An open folder inserts a second frame-rung column, so the sidebar - and its corner - start
     one rail further in. */
  #app:has(> .folder-secondary-rail.visible)::before {
    left: calc(var(--rail-width) * 2);
  }
  /* The pinned strip pushes the sidebar down by its height, and the patch has to go with it or
     it sits 42px above the corner it is there to fill - which leaves the wedge showing the raw
     wallpaper again, twice as bright as everything around it. That was the hole in the corner.
     The strip itself starts after the server rail, so the corner is the only part of #app that
     is still exposed. */
  body.has-pinned-rail #app::before { top: var(--pinrail-h, 42px); }
}

/* A voice channel while someone is being dragged onto it. The reference carries a matching
   state on its channel row (`.containerUserOver_c69b6d`, beside `.containerDefault_c69b6d`).
   An INSET ring rather than a border, so the row does not change size and shove the list. */
.ch-item.vc-user-over {
  background: var(--bg-active, var(--bg-hover));
  box-shadow: inset 0 0 0 2px var(--accent);
  border-radius: 6px;
}
/* The row being dragged dims, the same way a dragged channel does. */
.voice-sidebar-member.dragging { opacity: .45; }
.voice-sidebar-member[draggable="true"] { cursor: grab; }
.voice-sidebar-member[draggable="true"]:active { cursor: grabbing; }

/* Drag strip on the right edge to resize the channel list. */
.ch-resize-handle {
  position: absolute; top: 0; right: 0; bottom: 0; width: 5px;
  cursor: col-resize; z-index: 50; transition: background .15s;
}
.ch-resize-handle:hover, .ch-resize-handle.dragging { background: var(--accent); }

.server-header {
  min-height: 48px;
  padding: 0 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  user-select: none;
  transition: background 0.1s;
  position: relative;
  overflow: hidden;
  flex-shrink: 0;
}
.server-header:hover { background: var(--bg-hover); }
.server-header.has-banner {
  min-height: 120px;
  align-items: flex-end;
  padding-bottom: 12px;
  /* Full-bleed to the panel's top + left separators. The channel-list panel is
     overflow:hidden with border-radius:8px 0 0 0, so the banner is automatically clipped
     to that one smooth rounded corner where the two separators meet - no gap, no square. */
  border-top-left-radius: 8px;
}
.server-header.has-banner:hover { background: transparent; }
#sidebarBannerImg {
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center top;
  pointer-events: none;
  transition: opacity 0.2s;
}
.server-header.has-banner::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, transparent 30%, rgba(0,0,0,0.72));
  pointer-events: none;
}
.server-header.has-banner .server-name {
  position: relative;
  z-index: 1;
  color: #fff;
}
.server-header.has-banner .server-header-chevron {
  position: relative;
  z-index: 1;
  color: rgba(255,255,255,0.8);
}

.server-name {
  font-family: var(--font-display, 'Space Grotesk', sans-serif);
  font-size: 14px;
  font-weight: 700;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.server-header-chevron {
  color: var(--text-secondary);
  font-size: 12px;
  flex-shrink: 0;
}

.channel-list {
  flex: 1;
  overflow-y: auto;
  padding: 8px 0;
  /* margin (not padding) so the scroll VIEWPORT itself ends above the floating user
     panel - otherwise channels scroll through the area behind the panel and leak out
     around its rounded corners + the 8px bottom gap. The freed strip shows the
     sidebar's own (blank) background instead.
     --bottom-bar-h is measured live off the real panel/voice-bar height (see
     _initBottomBarHeightSync) instead of guessing a fixed number - the old static
     70px/128px guess broke whenever the voice bar's actual height differed (long
     channel name wrapping to 2 lines, zoom, etc) and channels scrolled under it. */
  margin-bottom: var(--bottom-bar-h, 70px);
}

.ch-category {
  padding: 16px 8px 4px 16px;
  display: flex;
  align-items: center;
  gap: 4px;
  cursor: pointer;
  user-select: none;
  border-radius: 6px;
}
/* Highlight a category header when a channel is dragged over it. */
.ch-category.cat-drop-target {
  background: color-mix(in srgb, var(--accent) 22%, transparent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.ch-category-name {
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text-muted);
  flex: 1;
  transition: color 0.1s;
}
.ch-category:hover .ch-category-name { color: var(--text-secondary); }

/* Reorderable / collapsible DM sidebar sections. */
.dm-section-header {
  display: flex;
  align-items: center;
  gap: 4px;
  padding: 4px 8px;
  margin-top: 8px;
  user-select: none;
}
.dm-section-header .dm-section-title { flex: 0 1 auto; }
.dm-drag-grip { color: var(--text-muted); font-size: 11px; opacity: 0; transition: opacity 0.1s; cursor: grab; }
.dm-section-header:hover .dm-drag-grip { opacity: 0.65; }
.dm-drag-grip:active { cursor: grabbing; }
.dm-section-chevron { color: var(--text-muted); font-size: 10px; transition: transform 0.15s; cursor: pointer; padding: 0 2px; }
.dm-section.collapsed .dm-section-chevron { transform: rotate(-90deg); }
.dm-section.collapsed .dm-section-body { display: none; }
.dm-section.dragging { opacity: 0.45; }
.dm-section.drag-over { box-shadow: inset 0 2px 0 var(--accent); }
.dm-section-badge .dm-incoming-badge,
.dm-incoming-badge { background: #ed4245; color: #fff; border-radius: 999px; font-size: 10px; font-weight: 700; padding: 1px 6px; }

.ch-category-add {
  color: var(--text-muted);
  font-size: 18px;
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: color 0.15s;
}
.ch-category-add:hover { color: var(--text-secondary); }

/* Channel drag-and-drop */
.ch-item.dragging { opacity: 0.35; pointer-events: none; }
.ch-drop-indicator { height: 2px; background: var(--accent); border-radius: 1px; margin: 2px 8px; pointer-events: none; }
/* Server folder in rail */
.server-folder-icon {
  width: 48px; height: 48px; border-radius: var(--radius-md);
  box-sizing: border-box; /* keep the 1px outline INSIDE the 48px so it matches the servers */
  background: var(--bg-elevated); display: grid;
  /* (48 - 5*2 - 2)/2 = 18px EXACTLY. Was padding 7 / gap 3 -> 15.5px, and a half-pixel box
     cannot land on a device pixel, so the preview rendered soft and off-centre. */
  grid-template-columns: 1fr 1fr; gap: var(--folder-gap); padding: var(--folder-pad);
  transition: border-radius 0.15s; cursor: pointer; position: relative;
}
.server-folder-icon span { border-radius: 4px; display: block; }
/* Open folder: single centered folder-shape icon instead of the 2x2 preview grid. */
.server-folder-icon.open { display: flex; align-items: center; justify-content: center; padding: 0; border-radius: var(--radius-md); }
.server-folder-open { display: flex; align-items: center; justify-content: center; line-height: 0; }
.server-item.folder-hover .server-icon,
.server-item.folder-hover .server-folder-icon { box-shadow: 0 0 0 3px var(--accent); border-radius: 16px; }
/* modern-style expanded-folder container: the folder's servers stacked inside a
   rounded, folder-coloured pill directly under the folder icon (tint set inline). */
.folder-children {
  display: flex; flex-direction: column; align-items: center;
  /* 48 + 4 = 52px pitch, the SAME as outside the folder (48 + 2px item margin-bottom +
     2px rail gap). It was 6px here, i.e. 54px, and that 2px-per-row drift is exactly what
     the `margin: 1px` was compensating for. With the pitch matched the nudge is not needed
     and the block lines up on its own. */
  gap: var(--rail-pitch-gap); padding: 8px 0; width: 56px; border-radius: 20px; margin: 0 0 2px;
}
.folder-children .server-item { margin: 0; }
.server-folder-expand {
  /* Tracks the rail width - a second hardcoded 64 here would silently desync the flyout
     from the rail the moment either changes. */
  position: absolute; left: var(--rail-width); top: 0; bottom: 0;
  display: flex; flex-direction: column; gap: 6px; justify-content: center;
  background: var(--bg-surface); border-right: 1px solid var(--border-strong);
  padding: 8px 4px; z-index: 10;
}
/* Server header dropdown */
.srv-header-menu {
  position: fixed; width: 220px;
  background: var(--bg-elevated); border: 1px solid var(--border-strong);
  border-radius: 8px; padding: 6px; z-index: 500;
  box-shadow: 0 12px 40px rgba(0,0,0,0.7);
  animation: srv-menu-down 0.28s var(--ease-out, ease);
}
/* Slides down out of the server header: the clip-path keeps the part that
   would overlap the header/banner hidden, so it looks like it emerges from
   behind it instead of fading in over the channel list. */
@keyframes srv-menu-down {
  from { opacity: 0; transform: translateY(-18px); clip-path: inset(18px 0 0 0); }
  to   { opacity: 1; transform: translateY(0);     clip-path: inset(0 0 0 0); }
}
.srv-menu-item {
  display: flex; align-items: center; gap: 10px; padding: 9px 10px;
  border-radius: 4px; font-size: 13px; color: var(--text-secondary); cursor: pointer;
  transition: background 0.1s, color 0.1s;
}
.srv-menu-item:hover { background: var(--accent); color: #fff; }
.srv-menu-item.danger { color: #ed4245; }
.srv-menu-item.danger:hover { background: #ed4245; color: #fff; }
.srv-menu-sep { height: 1px; background: rgba(255,255,255,0.08); margin: 4px 0; }
/* Lazy load */
.lazy-sentinel { height: 4px; flex-shrink: 0; }
.lazy-loading-bar { padding: 8px 16px; font-size: 12px; color: var(--text-muted); text-align: center; }
/* Delete message button */
.msg-action-btn.del:hover { color: #ed4245 !important; }

.ch-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 8px 3px 10px;
  margin: 1px 6px;
  border-radius: var(--radius-md);
  cursor: pointer;
  transition: background 0.12s, color 0.1s, transform 0.18s var(--ease-out, ease);
  position: relative;
}

.ch-item:hover { background: var(--bg-hover); }
.ch-item:active { transform: scale(0.98); }
/* Neutral gray escalation like NONONONONO's channel list, not an accent-color tint:
   idle -> hover (--bg-hover) -> selected (--bg-active), each just a touch brighter
   than the last. No --accent mixed in - that was reading as a distracting blue
   highlight instead of a subtle "this one's open" cue. */
.ch-item.active { background: var(--bg-active); }
.ch-item.active .ch-name { color: var(--text-primary); }
.ch-item:hover .ch-name { color: var(--text-secondary); }

/* Channel list only: fixed neutral-gray shading matched against NONONONONO's own channel
   list for comparison (not an exact copy, just the same escalation shape), scoped so
   it does NOT touch --bg-hover/--bg-active anywhere else in the app - every other
   hover/active surface (DM list, settings nav, member rows, etc.) is untouched.
   Only applies to default dark + light - the accent-colored themes (core/quantum/lofi/
   etc.) keep using the app-wide token above instead, since their whole identity is
   built around that color tint. */
/* NOT under a custom paint. A custom theme leaves data-theme unset (it uses
   data-bg="gradient" + body.custom-paint), so this block was matching it and painting fixed
   greys - measured rgb(44,44,48) on the active channel against a purple app. The app-wide
   tokens these override are translucent white lifts, which already work in any theme, so the
   fix is to stop overriding them rather than to pick a second set of colours. */
html:not([data-theme]) body:not(.custom-paint) .ch-item:hover          { background: #222225; }
html:not([data-theme]) body:not(.custom-paint) .ch-item.active         { background: #2C2C30; }
html:not([data-theme]) body:not(.custom-paint) .ch-name                { color: #797A81; }
html:not([data-theme]) body:not(.custom-paint) .ch-item:hover .ch-name { color: #D8D8D8; }
html:not([data-theme]) body:not(.custom-paint) .ch-item.active .ch-name { color: #EFEFEF; }

html[data-theme="light"] .ch-item:hover          { background: #E7E7E9; }
html[data-theme="light"] .ch-item.active         { background: #DDDDE0; }
html[data-theme="light"] .ch-name                { color: #74757C; }
html[data-theme="light"] .ch-item:hover .ch-name { color: #28282D; }
html[data-theme="light"] .ch-item.active .ch-name { color: #2F2F34; }

/* A 28x28 SLOT, the way Root lays its channel list out: the slot holds either the channel's
   type glyph or its custom image, and the name sits beside it, centred against the slot. The
   slot is a fixed square so a row with an image and a row with a glyph line up exactly - it
   was a 20px box before, which an image filled and a glyph did not. */
.ch-icon {
  font-size: 17px;
  font-weight: 600;              /* '#'/glyph fallbacks - close to but not heavier than ch-name */
  color: var(--text-muted);
  width: 28px;
  height: 28px;
  text-align: center;
  flex-shrink: 0;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  /* A TILE, the way Root draws it: the glyph sits on its own rounded surface rather than
     floating in an empty 28px box, so a row with a glyph and a row with an image read as the
     same component. The lift is mixed from --text-primary rather than taken from a --bg token,
     so it works out of the current text colour in every theme instead of being a grey chip on
     a painted background. */
  border-radius: 8px;
  background: color-mix(in srgb, var(--text-primary) 7%, transparent);
  position: relative;            /* the type badge below is positioned against it */
}
/* An image fills the tile edge to edge, so the surface under it would only ever show as a rim. */
.ch-icon:has(.ch-icon-img) { background: transparent; }
.ch-item.active .ch-icon, .ch-item:hover .ch-icon {
  background: color-mix(in srgb, var(--text-primary) 12%, transparent);
}
.ch-item.active .ch-icon:has(.ch-icon-img),
.ch-item:hover .ch-icon:has(.ch-icon-img) { background: transparent; }
/* The channel TYPE, kept on the corner when a custom image has taken the glyph's place -
   otherwise setting an image costs you the one thing the glyph was there to tell you. */
.ch-icon-type {
  position: absolute; left: -4px; bottom: -4px;
  width: 15px; height: 15px; border-radius: 5px;
  background: var(--bg-surface);
  border: 1px solid var(--border-strong);
  display: flex; align-items: center; justify-content: center;
  color: var(--text-muted);
  pointer-events: none;
  box-sizing: border-box;
}
/* more specific than the 16px rule below, which is for the tile's own glyph */
.ch-icon .ch-icon-type svg { width: 9px; height: 9px; }
/* Slim, lighter type-glyph SVGs so the icon weight sits closer to the channel-name
   text next to it instead of overpowering it. CSS stroke-width overrides the
   stroke-width="2" baked into each inline <svg>, so this sets them all at once. */
/* 2px, not 1.8. With non-scaling-stroke the declared width is the rendered width, so 1.8
   lands between pixels on both edges - the same smear the blanket 1px rule caused everywhere
   else. The channel icons read a touch heavier at 2 and are actually solid. */
.ch-icon svg { width: 16px; height: 16px; stroke-width: 2px; }
.ch-item.active .ch-icon, .ch-item:hover .ch-icon { color: var(--text-secondary); }
.ch-icon-img { width: 28px; height: 28px; border-radius: 8px; object-fit: cover; display: block; }
.ch-app-tag { flex-shrink: 0; font-size: 9px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase; color: var(--accent); background: color-mix(in srgb, var(--accent) 16%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); border-radius: 4px; padding: 0 5px; line-height: 15px; }

.ch-name {
  font-size: 16px;
  font-weight: 500;
  color: var(--text-secondary);
  flex: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: color 0.1s;
}

.ch-badge {
  background: var(--red);
  color: #fff;
  font-size: 10px;
  font-weight: 700;
  padding: 1px 5px;
  border-radius: var(--radius-full);
  min-width: 16px;
  text-align: center;
}

.ch-actions { display: none; gap: 2px; }
.ch-item:hover .ch-actions { display: flex; }
.ch-item:hover .ch-badge { display: none; }
/* Force-muted ("Mute mode") channels: dimmed + a muted-bell marker */
.ch-item.ch-muted .ch-name, .ch-item.ch-muted .ch-icon { opacity: 0.45; }
.ch-item.ch-muted.active .ch-name, .ch-item.ch-muted:hover .ch-name { opacity: 0.7; }
.ch-muted-icon { color: var(--text-muted); display: flex; align-items: center; flex-shrink: 0; }
.ch-item:hover .ch-muted-icon { display: none; }

.ch-action-btn {
  width: 18px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 3px;
  color: var(--text-muted);
  font-size: 13px;
  transition: background 0.1s, color 0.1s;
}
.ch-action-btn:hover { background: var(--bg-active); color: var(--text-secondary); }

/* Voice bar - the TOP half of the joined user panel when in a call: same fill,
   rounded top only, and its bottom border is the single divider line to the panel. */
