/* ============================================================================
   Layout: the app frame.

   Sign-in screen, top bar, Space rail, channel sidebar, voice bar, the main
   three-column frame, the right-hand panel shell, and every responsive rule in
   the app. Message internals live in messages.css, panel internals in
   panels.css, controls in components.css.

   THE CHROME RULE
   Everything inside #topbar, #spaceRail and #sidebar paints with the --c-nav-*
   family. Everything else paints with the content family (--c-bg, --c-surface,
   --c-text ...). That split is the whole reason the colorful theme can put a
   deep aubergine sidebar next to a white message list.

   Phone first: the sidebar is an off-canvas drawer under 860px, the right panel
   is a full-screen sheet, and nothing may scroll sideways at 360px.

   WHAT THIS FILE NO LONGER OWNS
   Three stylesheets now load after it and have claimed parts of the frame:
   reading.css (icon sizing, sidebar air, the sign-in wordmark), shell.css (the
   global bar as a three-zone grid, the new channel bar, the sidebar's scroll
   container and its "you" footer) and polish.css, injected at runtime by
   uxfix.js (touch floors, the one-Space rail). Rules here that dressed markup
   those files replaced have been deleted rather than left to rot. Where this
   file still has to win - a colour that fails AA, a tap target, the drawer's
   own geometry - it does so by naming an ancestor, never with !important.
   ========================================================================= */

/* CONTRAST NOTE
   Chrome text never uses --c-nav-text-2: on the light theme's nav surface it
   lands at 3.4:1, under AA. The quiet step in this file is --c-nav-text at a
   smaller size or lighter weight, and emphasis comes from --c-nav-text-active.
   Colour-mixed values are used for borders, rings and decorative tints only,
   never for a colour that text has to be read against. */

/* ============================================================================
   SIGN IN
   The first screen anyone sees. A single centred card on a quiet accent wash.
   ========================================================================= */
.authwrap {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s-8) var(--s-6) calc(var(--s-8) + env(safe-area-inset-bottom));
  /* A wash rather than a flat field, so the card has something to sit on. */
  background:
    radial-gradient(120% 80% at 50% -10%,
      color-mix(in srgb, var(--c-accent) 16%, transparent), transparent 68%),
    var(--c-bg);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.authcard {
  /* margin:auto keeps the card centred while still letting it scroll on a short
     phone in landscape, which align-items:center alone would clip. */
  margin: auto;
  width: 100%;
  max-width: 420px;
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  padding: var(--s-9) var(--s-8) var(--s-8);
  background: var(--c-surface);
  border: 1px solid var(--c-border);
  border-radius: var(--r-xl);
  box-shadow: var(--e-3);
}

/* The card owns its rhythm through gap, so fields must not add a second margin.
   .field itself belongs to components.css; this only neutralises its spacing
   inside my container. */
.authcard .field { margin-bottom: 0; }
/* The same gap the card uses, so the rhythm does not change halfway down the
   form - a 12px gap above "Email" and an 8px gap above "Password" is exactly
   the kind of drift that reads as sloppy even when nobody can name it. */
#emailStep,
#otpStep,
#setPwStep {
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
}
#otpStep .muted { line-height: var(--t-loose); }

.brand {
  display: flex;
  align-items: center;
  gap: var(--s-5);
}
.brand h1 {
  font-size: var(--t-3xl);
  font-weight: var(--t-black);
  letter-spacing: -0.025em;
  color: var(--c-text);
}
.brand-mark {
  display: grid;
  place-items: center;
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: var(--r-lg);
  background: var(--c-accent-quiet);
  font-size: var(--t-2xl);
  line-height: 1;
}

.tagline {
  margin-top: calc(var(--s-2) * -1);
  color: var(--c-text-2);
  font-size: var(--t-base);
  line-height: var(--t-loose);
  text-wrap: pretty;
}

.or {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  margin: var(--s-2) 0 0;
}
.or::before,
.or::after {
  content: "";
  flex: 1 1 auto;
  height: 1px;
  background: var(--c-border);
}
.or span {
  color: var(--c-text-2);
  font-size: var(--t-xs);
  font-weight: var(--t-semibold);
  letter-spacing: 0.09em;
  text-transform: uppercase;
}

/* Errors read as a notice, not as red on red: the tint carries the alarm and
   the copy stays at full text contrast. */
.autherr {
  padding: var(--s-4) var(--s-5);
  border: 1px solid color-mix(in srgb, var(--c-danger) 40%, transparent);
  border-left: 3px solid var(--c-danger);
  border-radius: var(--r-md);
  background: var(--c-danger-quiet);
  color: var(--c-text);
  font-size: var(--t-sm);
  line-height: var(--t-snug);
}

/* Bullets inside the error notice: the failure is usually one of two or three
   specific things, and naming them is the difference between a person fixing it
   themselves and a person messaging the operator. */
.autherr ul {
  margin: var(--s-3) 0 0;
  padding-left: var(--s-5);
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
}
.autherr li { line-height: var(--t-loose); }

/* The account field on the password-change screen. It exists for the password
   manager; it must not look like something to fill in. */
#pwUser {
  color: var(--c-text-2);
  cursor: default;
}

/* ---------------------------------------------------------- NO TEAM YET
   The screen for an account that belongs to nothing. It replaced being dumped
   into a 1770-member demo Space, which was neither their team nor fast enough
   to finish loading on a phone - and when it did not finish, the shell had
   painted and the conversation had not, which is what "blank screen" was. This
   has to look deliberate rather than like a failure, because the person reading
   it has just been told twice that something is wrong with their password. */
.noteam {
  max-width: 420px;
  margin: auto;
  padding: var(--s-8) var(--s-6) calc(var(--s-8) + env(safe-area-inset-bottom));
  display: flex;
  flex-direction: column;
  gap: var(--s-5);
  text-align: left;
}
.noteam h2 {
  font-size: var(--t-xl);
  font-weight: var(--t-bold);
  color: var(--c-text);
  line-height: var(--t-snug);
}
.noteam .field { margin-bottom: 0; }
.noteam .fineprint { margin-top: var(--s-2); }

/* ------------------------------------------------------- loading / failed
   The two states that used to be indistinguishable from an empty room, and from
   each other. "Loading" has to look like work in progress or ten seconds of it
   reads as broken; "failed" has to name what happened and offer the one thing
   that helps, which is trying again. Neither may ever be silent. */
.loading-space {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-4);
  padding: var(--s-9) var(--s-6);
  color: var(--c-text-2);
  font-size: var(--t-sm);
}
.loading-space .spin {
  width: 16px;
  height: 16px;
  flex: none;
  border-radius: 50%;
  border: 2px solid var(--c-border);
  border-top-color: var(--c-accent);
  animation: spin 700ms linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .loading-space .spin { animation-duration: 2.4s; }
}

.loadfail {
  max-width: 420px;
  margin: auto;
  padding: var(--s-8) var(--s-6);
  display: flex;
  flex-direction: column;
  gap: var(--s-4);
  text-align: left;
}
.loadfail h2 {
  font-size: var(--t-lg);
  font-weight: var(--t-bold);
  color: var(--c-text);
  line-height: var(--t-snug);
}
.loadfail .fineprint { color: var(--c-text-3); }

/* Chrome that only means something once you are inside a Space. A channel bar
   showing a lone "#", a "Search this Space" over no Space, and a composer
   inviting you to "pick a channel" when there are none to pick, are three more
   things telling somebody their account is broken when it is not. */
body.no-team #channelbar,
body.no-team #composerBar,
body.no-team #globalSearch,
body.no-team #navToggle { display: none; }
body.no-team #messages { padding: 0; }

.fineprint {
  margin-top: var(--s-1);
  font-size: var(--t-xs);
  line-height: var(--t-loose);
}

/* The secondary sign-in paths. Someone who was handed an account and a password
   should be able to tell at a glance which control is theirs, so these recede:
   quieter text, tighter rhythm, and no full-weight button competing with
   "Sign in". They stay fully visible and operable, because a path you have to
   discover is a path people ask you about instead of using. */
.authalt {
  display: flex;
  flex-direction: column;
  gap: var(--s-3);
  margin-top: var(--s-2);
}
/* This label was dimmed to --c-text-3 to make the section recede, which put it
   at 2.9:1 against the card - below AA, on the one screen every single person
   has to get through. Recession comes from position and weight instead; the
   colour goes back to the same token the labels above it use. */
.authalt button {
  font-weight: var(--t-medium);
  color: var(--c-text-2);
}
.authalt button:hover { color: var(--c-text); }

/* ============================================================================
   APP SHELL
   ========================================================================= */
#chat {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

main {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  /* Containing block for the phone drawer, so it does not need to know how tall
     the header or the voice bar happen to be. */
  position: relative;
  background: var(--c-bg);
}

section.msgs {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  position: relative;
  background: var(--c-bg);
}

/* The scroll container. Without an explicit flex basis and min-height, a flex
   child sizes to its CONTENT, so the message list grew to several thousand
   pixels, pushed the composer past the bottom of the window and made the whole
   document scroll - taking the header and sidebar out of view. min-height:0 is
   the part that is easy to miss: a flex item's default min-height is auto,
   which refuses to shrink below its content no matter what overflow says. */
#messages {
  flex: 1 1 0;
  min-height: 0;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  padding: var(--s-5) 0 var(--s-4);
  /* NOT scroll-behavior: smooth. This was on the scroll container itself, so
     every scrollTop assignment became an animation - including the pin-to-bottom
     correction in messages.js scrollDown(), which reads scrollTop back to decide
     whether the reader has taken the list over. Under smooth the read-back is
     the PRE-animation value, so the ladder saw a position it did not expect and
     stood down: measured in Chromium, 1 of its 5 ticks ran, and one message
     carrying an image left the newest row 300px below the fold - past the
     at-bottom slop, so somebody who WAS reading the newest message silently
     stopped being followed down. Deliberate travel is still animated, by the
     jumpTo/jumpToSeq callers that ask for it explicitly. */
}

/* The strips between the list and the composer must never take space from the
   list or grow with their content. */
#typing,
#dmReceipt {
  flex: 0 0 auto;
}

#composerBar {
  flex: 0 0 auto;
}

/* ============================================================================
   TOP BAR

   OWNERSHIP: shell.css, loaded after this file, has since taken the global bar
   over - it turns #topbar into a three-zone grid, adds #channelbar underneath
   it, and sets its own --header-h. The channel name, the topic and the sign-out
   button moved out of this element entirely, so the rules that dressed them
   were deleted rather than left to rot.

   What stays here is the part that is still only stated once: the bar is chrome,
   so everything in it paints from --c-nav-*, and the frame keeps a working
   header if shell.css is ever taken back out.
   ========================================================================= */
#topbar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--s-3);
  height: calc(var(--header-h) + env(safe-area-inset-top));
  padding: 0 var(--s-5);
  padding-top: env(safe-area-inset-top);
  background: var(--c-nav-bg);
  border-bottom: 1px solid var(--c-nav-border);
  color: var(--c-nav-text);
  z-index: var(--z-nav);
}

#spaceName {
  flex: none;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--c-nav-brand);
  font-size: var(--t-md);
  font-weight: var(--t-bold);
  letter-spacing: -0.01em;
}

/* Anything .muted inside the bar has to switch colour families or it disappears
   against the aubergine. */
#topbar .muted { color: var(--c-nav-text); }

.hdr-actions {
  display: flex;
  align-items: center;
  /* 2px let the hover squares of adjacent icons touch and read as one slab. */
  gap: var(--s-2);
  flex: 0 1 auto;
  min-width: 0;
  /* Features keep adding icons; let the row scroll rather than push the layout
     sideways. The mask hints that there is more past the edge. */
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(to right, currentColor calc(100% - 18px), transparent);
  mask-image: linear-gradient(to right, currentColor calc(100% - 18px), transparent);
}
.hdr-actions::-webkit-scrollbar { height: 0; }

.hdr-actions > button { flex: none; }

/* Icon buttons in the header are chrome-coloured, not content-coloured. */
#topbar button.icon {
  min-width: 32px;
  min-height: 32px;
  color: var(--c-nav-text);
  background: transparent;
  transition: background-color var(--m-fast) var(--m-ease),
    color var(--m-fast) var(--m-ease);
}
#topbar button.icon:hover {
  background: var(--c-nav-hover);
  color: var(--c-nav-text-active);
}
#topbar button.icon:active { background: var(--c-nav-active); }
#topbar button.icon:disabled { opacity: 0.45; background: transparent; }

/* components.css paints .install as a tinted SUCCESS chip in content colours -
   correct there, unreadable here, because --c-text on the aubergine header is
   near-black on near-black. So the header keeps the shape and re-derives the
   colours from the chrome family.
   It is deliberately NOT an accent fill: a solid blue chip is the loudest thing
   in the bar and "install the app" is never the most important thing on screen.
   The fill is the nav's own ink at low strength, which lands one step above the
   header on every theme without inventing a colour. */
#installBtn.install {
  flex: none;
  background: color-mix(in srgb, var(--c-nav-text) 16%, transparent);
  border-color: color-mix(in srgb, var(--c-nav-text) 26%, transparent);
  color: var(--c-nav-text-active);
  font-weight: var(--t-semibold);
  box-shadow: none;
}
#installBtn.install:hover {
  background: color-mix(in srgb, var(--c-nav-text) 28%, transparent);
  border-color: color-mix(in srgb, var(--c-nav-text) 48%, transparent);
}
#installBtn.install:active { background: color-mix(in srgb, var(--c-nav-text) 34%, transparent); }

/* base.css rings in --c-accent, which measures 2.5:1 against the colorful
   theme's aubergine - a focus ring nobody can see is not a focus ring. Chrome
   rings in the nav's brightest ink instead, which contrasts hard on all three. */
#topbar :focus-visible,
#spaceRail :focus-visible,
#sidebar :focus-visible {
  outline-color: var(--c-nav-text-active);
}

.meName {
  flex: none;
  max-width: 140px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-sm);
}

/* Shown only once the layout narrows; see the phone block below.
   The second selector is not redundant. A later stylesheet gives every header
   icon button `display: inline-flex` through `#topbar button.icon`, which
   outranks a bare class and put the drawer toggle back on the desktop header.
   Matching that weight and adding the class is the smallest honest way to win
   it back - the alternative is !important, which would only move the fight. */
.only-mobile,
#topbar button.icon.only-mobile { display: none; }

/* ============================================================================
   VOICE BAR
   A live band under the header. Content colours, tinted with success so the
   room reads as on-air without the text losing contrast.
   ========================================================================= */
#voicebar {
  flex: none;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s-3);
  padding: var(--s-4) var(--s-6);
  background: var(--c-success-quiet);
  border-bottom: 1px solid color-mix(in srgb, var(--c-success) 32%, transparent);
  color: var(--c-text);
}

.vlabel {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  color: var(--c-text);
  font-size: var(--t-base);
  font-weight: var(--t-semibold);
}

#vparts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  min-width: 0;
}

.vpart {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-1) var(--s-5);
  border-radius: var(--r-full);
  background: var(--c-surface-2);
  border: 1px solid var(--c-border);
  color: var(--c-text);
  font-size: var(--t-sm);
  white-space: nowrap;
  transition: box-shadow var(--m-base) var(--m-ease),
    background-color var(--m-base) var(--m-ease);
}
.vpart.speaking {
  background: var(--c-success-quiet);
  box-shadow: 0 0 0 2px var(--c-success);
}
/* The halo breathes on opacity and transform only, so a busy room stays cheap. */
.vpart.speaking::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: inherit;
  box-shadow: 0 0 0 2px var(--c-success);
  animation: vpulse 1.5s var(--m-ease) infinite;
  pointer-events: none;
}
@keyframes vpulse {
  from { opacity: 0.7; transform: scale(1); }
  to { opacity: 0; transform: scale(1.14); }
}

/* ============================================================================
   SPACE RAIL - the multi-organisation switcher
   ========================================================================= */
#spaceRail {
  flex: none;
  width: var(--rail-w);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--s-4);
  padding: var(--s-4) 0 calc(var(--s-4) + env(safe-area-inset-bottom));
  background: var(--c-nav-bg-2);
  border-right: 1px solid var(--c-nav-border);
  /* The indicator bar sits just outside each tile, so the cross axis must not
     scroll; the rail itself scrolls when an admin is in a dozen Spaces. */
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
#spaceRail::-webkit-scrollbar { width: 0; }

.sicon {
  position: relative;
  /* Keeps the per-Space tint layer under the initials instead of behind the
     whole rail. */
  isolation: isolate;
  flex: none;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: var(--r-xl);
  background: var(--c-nav-hover);
  color: var(--c-nav-text-active);
  font-size: var(--t-md);
  font-weight: var(--t-bold);
  letter-spacing: 0.02em;
  cursor: pointer;
  user-select: none;
  /* border-radius is in the transition on purpose: the squircle morph is the
     whole gesture of a Discord-style rail. It repaints, it does not reflow. */
  transition: border-radius var(--m-base) var(--m-spring),
    background-color var(--m-base) var(--m-ease),
    color var(--m-base) var(--m-ease),
    transform var(--m-fast) var(--m-ease);
}
/* Presses have to answer on a phone, where there is no hover to answer first. */
.sicon:active { transform: scale(0.93); }

/* Per-Space identity. workspace.js writes --h on the tile; rotating a token
   colour keeps every Space distinguishable without hardcoding a palette.

   The mix is OPAQUE and blended toward --c-nav-hover on purpose. hue-rotate is
   a colour matrix, not a hue shift: run it over a translucent, saturated source
   and some angles collapse a channel and the tile comes out near-black - which
   is exactly what happened to every Space on the colorful theme, where all the
   tiles landed on the same muddy aubergine. Rotating an opaque colour that
   already sits at the nav's own lightness keeps the lightness roughly put, so
   the hues separate and the initials keep their contrast in all three themes. */
.sicon::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  border-radius: inherit;
  background: color-mix(in srgb, var(--c-accent) 60%, var(--c-nav-hover));
  opacity: 0.92;
  filter: hue-rotate(calc(var(--h, 220) * 1deg - 220deg)) saturate(1.15);
  transition: opacity var(--m-base) var(--m-ease);
}

/* The rail indicator: nothing at rest, a stub on hover, a full bar when the
   Space is open. Scaled, never resized, so it never touches layout. */
.sicon::before {
  content: "";
  position: absolute;
  left: calc(3px - (var(--rail-w) - 44px) / 2);
  top: 50%;
  width: 4px;
  height: 26px;
  border-radius: var(--r-full);
  background: var(--c-nav-text-active);
  opacity: 0;
  transform: translateY(-50%) scaleY(0);
  transition: transform var(--m-base) var(--m-out), opacity var(--m-fast) var(--m-ease);
}

.sicon:hover { border-radius: var(--r-md); }
.sicon:hover::after { opacity: 1; }
.sicon:hover::before { opacity: 1; transform: translateY(-50%) scaleY(0.42); }

/* The open Space: the same accent the sidebar uses for the open channel, so
   selection reads as one idea across the whole frame. */
.sicon.active {
  border-radius: var(--r-md);
  background: var(--c-accent);
  color: var(--c-text-inverse);
}
.sicon.active::after { opacity: 0; }
.sicon.active::before { opacity: 1; transform: translateY(-50%) scaleY(1); }

/* Archived, or counting down to deletion. A server in either state takes no new
   messages, so it must not look like one that does - painting it identically is
   what made "I deleted it" and "it is still there" the same screen. */
.sicon-off {
  opacity: 0.42;
  filter: grayscale(1);
}
.sicon-off:hover { opacity: 0.72; }

/* Add a Space: an outline tile, never a filled one, so it stays subordinate. */
.sicon.add {
  background: transparent;
  border: 1px dashed color-mix(in srgb, var(--c-nav-text) 38%, transparent);
  color: var(--c-nav-text);
  font-size: var(--t-xl);
  font-weight: var(--t-normal);
}
.sicon.add::after,
.sicon.add::before { content: none; }
.sicon.add:hover {
  background: var(--c-nav-hover);
  border-color: color-mix(in srgb, var(--c-nav-text) 60%, transparent);
  color: var(--c-nav-text-active);
}

.sbadge {
  position: absolute;
  right: -3px;
  bottom: -3px;
  min-width: 18px;
  height: 18px;
  padding: 0 var(--s-2);
  display: grid;
  place-items: center;
  border-radius: var(--r-full);
  border: 2px solid var(--c-nav-bg-2);
  background: var(--c-danger);
  color: var(--c-text-inverse);
  font-size: var(--t-2xs);
  font-weight: var(--t-black);
  line-height: 1;
}

/* Unread with no mention: the same left anchor as the indicator bar, so the two
   never stack. Hover and active hand the spot over to the bar. */
.sdot {
  position: absolute;
  left: calc(3px - (var(--rail-w) - 44px) / 2);
  top: 50%;
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--c-nav-text-active);
  transform: translateY(-50%);
  transition: opacity var(--m-fast) var(--m-ease);
}
.sicon:hover .sdot,
.sicon.active .sdot { opacity: 0; }

/* ============================================================================
   SIDEBAR - channels, voice rooms, DMs. All chrome, all --c-nav-*.
   ========================================================================= */
#sidebar {
  flex: none;
  width: var(--sidebar-w);
  /* The bottom inset matters on a landscape iPad too, not only in the drawer. */
  padding: var(--s-4) var(--s-3) calc(var(--s-9) + env(safe-area-inset-bottom));
  background: var(--c-nav-bg);
  border-right: 1px solid var(--c-nav-border);
  color: var(--c-nav-text);
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* #btnSpaces used to be a bordered ghost button pinned to the top of this
   sidebar. shell.css has since moved it to the bottom of the list as .sb-add
   and dressed it as a quiet row rather than a box. Its shape is left alone.

   Its INK is not. Both new sidebar rows quiet themselves with --c-nav-text-2,
   which measures 3.58:1 on the light theme's nav surface and fails AA - the one
   trap this file's header calls out by name. Only the colour is restated, and
   only inside #sidebar, so the rest of shell.css's treatment stands. The quiet
   step in chrome is --c-nav-text at a smaller size, never a lighter grey. */
#sidebar .sb-add,
#sidebar .sb-me-status { color: var(--c-nav-text); }

#sidebar h3 {
  display: flex;
  align-items: center;
  min-height: 26px;
  margin: var(--s-6) var(--s-2) var(--s-1);
  padding: 0 var(--s-2);
  border-radius: var(--r-xs);
  color: var(--c-nav-text);
  font-size: var(--t-xs);
  font-weight: var(--t-bold);
  letter-spacing: 0.07em;
  text-transform: uppercase;
  cursor: pointer;
  user-select: none;
  transition: color var(--m-fast) var(--m-ease),
    background-color var(--m-fast) var(--m-ease);
}
#sidebar h3:first-child { margin-top: var(--s-1); }
/* A section header collapses its group, so it has to feel like a control and
   not like a caption - a colour-only hover on 11px caps is easy to miss. */
#sidebar h3:hover {
  background: var(--c-nav-hover);
  color: var(--c-nav-text-active);
}
#sidebar h3:active { background: var(--c-nav-active); }

.navgroup {
  display: flex;
  flex-direction: column;
  gap: var(--s-1);
}

.chan {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  min-height: 30px;
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-sm);
  color: var(--c-nav-text);
  font-size: var(--t-md);
  line-height: var(--t-tight);
  cursor: pointer;
  user-select: none;
  transition: background-color var(--m-fast) var(--m-ease),
    color var(--m-fast) var(--m-ease);
}
.chan:hover {
  background: var(--c-nav-hover);
  color: var(--c-nav-text-active);
}
.chan:active { background: var(--c-nav-active); }

/* The open channel is a solid pill, not a tint: it has to survive being read
   out of the corner of an eye. */
.chan.active,
.chan.active:hover {
  background: var(--c-accent);
  color: var(--c-text-inverse);
  font-weight: var(--t-bold);
}
/* The dark theme's accent is a light blue - correct as text on a dark surface,
   far too loud as a full-width fill behind one. --c-nav-active is the same idea
   one step down, and it is the only place a theme needs its own answer. */
:root[data-theme="dark"] .chan.active,
:root[data-theme="dark"] .chan.active:hover,
:root:not([data-theme]) .chan.active,
:root:not([data-theme]) .chan.active:hover {
  background: var(--c-nav-active);
  color: var(--c-nav-text-active);
}
.chan.active .ch-ico { color: var(--c-nav-text-active); }

.chan.unread {
  color: var(--c-nav-text-active);
  font-weight: var(--t-bold);
}
.chan.unread .ch-ico { color: var(--c-nav-text-active); }

.chan.muted-ch {
  color: var(--c-nav-text-2);
  font-weight: var(--t-normal);
}

.ch-ico {
  flex: none;
  width: 16px;
  text-align: center;
  color: var(--c-nav-text);
  font-size: var(--t-base);
}
.ch-name {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Counts and dots ride the right edge of the row. The bare .badge belongs to
   components.css; this is only the sidebar's copy of it. */
#sidebar .badge {
  flex: none;
  margin-left: auto;
  min-width: 18px;
  padding: 0 var(--s-3);
  border-radius: var(--r-full);
  background: var(--c-danger);
  color: var(--c-text-inverse);
  font-size: var(--t-2xs);
  font-weight: var(--t-black);
  line-height: 18px;
  text-align: center;
}
.dot-unread {
  flex: none;
  margin-left: auto;
  width: 8px;
  height: 8px;
  border-radius: var(--r-full);
  background: var(--c-nav-text-active);
}
/* Painted by the topics feature, which still asks for a colour the old
   stylesheet defined. It lives in my sidebar, so it gets my nav colour. */
#sidebar .topics-count {
  flex: none;
  margin-left: auto;
  color: var(--c-nav-text);
  font-size: var(--t-xs);
}
#sidebar .topics-msg {
  padding: var(--s-1) var(--s-4) var(--s-3);
  color: var(--c-nav-text);
  font-size: var(--t-xs);
}

.chan-add {
  color: var(--c-nav-text);
  font-size: var(--t-sm);
}
.chan-add:hover { color: var(--c-nav-text-active); }

/* A voice room with people in it. The green is a dot, not the label, because
   --c-success cannot clear AA as small text on the aubergine nav. */
.live {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  margin-left: auto;
  color: var(--c-nav-text-active);
  font-size: var(--t-xs);
  font-weight: var(--t-bold);
}
.live::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: var(--r-full);
  background: var(--c-success);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--c-success) 30%, transparent);
}

.vparts-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-2);
  /* Indented to line up under the channel name, past the icon column. */
  padding: var(--s-1) var(--s-4) var(--s-3) calc(var(--s-4) + 16px + var(--s-4));
}
.vpart-nav {
  padding: 1px var(--s-3);
  border-radius: var(--r-full);
  background: var(--c-nav-hover);
  color: var(--c-nav-text);
  font-size: var(--t-xs);
}

/* ============================================================================
   RIGHT PANEL - container, header bar and scrolling only.
   Everything painted inside .content belongs to panels.css.
   ========================================================================= */
aside#panel {
  flex: none;
  /* Narrow laptops must not starve the message column to feed the panel. */
  width: clamp(300px, 32vw, var(--panel-w));
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--c-surface);
  border-left: 1px solid var(--c-border);
}

aside#panel > header {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--s-4);
  height: var(--header-h);
  /* Left padding matches .content below it, so the panel title and the first
     thing in the panel share one edge. The right side is tighter because an
     icon button is optically inset by its own padding already. */
  padding: 0 var(--s-3) 0 var(--s-5);
  border-bottom: 1px solid var(--c-border);
  background: var(--c-surface);
}
aside#panel > header > strong {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--t-md);
  font-weight: var(--t-semibold);
}

aside#panel > .content {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--s-5) var(--s-5) var(--s-8);
}

aside#panel > footer {
  flex: none;
  padding: var(--s-4) var(--s-5) calc(var(--s-4) + env(safe-area-inset-bottom));
  border-top: 1px solid var(--c-border);
  background: var(--c-surface);
}

/* ============================================================================
   DRAWER SCRIM
   Dims the conversation while the phone drawer is open. It is painted as a
   pseudo-element of #messages so that the existing "tap the message list to
   close the nav" handler in main.js also catches a tap on the scrim: no JS
   change, no dead-end drawer. The class form is here for anything that would
   rather insert a real element.

   It is absolutely positioned against section.msgs rather than fixed to the
   viewport. Fixed + inset-left:--rail-w cut a rail-shaped notch out of the top
   bar and the voice bar: the 60px above the rail stayed bright while the rest
   of the same strip went grey, which read as a rendering fault. The message
   column is a real boundary, so the scrim now dims exactly the thing the drawer
   is covering, the rail and the header stay lit and stay tappable, and no
   height has to be measured. section.msgs sets position:relative and #messages
   is static, so the pseudo-element is not clipped by the list's own overflow.
   ========================================================================= */
.drawer-scrim,
#messages::after {
  position: absolute;
  inset: 0;
  z-index: calc(var(--z-drawer) - 1);
  background: var(--c-scrim);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--m-slow) var(--m-ease);
}
body.nav-open .drawer-scrim,
body.nav-open #messages::after {
  opacity: 1;
  pointer-events: auto;
}

/* ============================================================================
   RESPONSIVE
   ========================================================================= */

/* Laptops: give the message column room back before the phone layout starts. */
@media (max-width: 1180px) {
  :root { --sidebar-w: 228px; }
}

/* ------------------------------------------------------------------ PHONE */
@media (max-width: 860px) {
  :root {
    --rail-w: 60px;
    --sidebar-w: 272px;
  }

  .only-mobile,
  #topbar button.icon.only-mobile { display: inline-flex; }
  #spaceName,
  .meName { display: none; }

  /* An action row can only ever be a third of a 390px bar; past that the thing
     the bar is about has nowhere to go. The mask says there is more to swipe. */
  .hdr-actions { max-width: 34vw; }

  /* The phone used to collapse this to a 44px glyph. shell.css now sets the
     button's own height, padding and font-size from `#topbar .install`, which
     ties on specificity and lands later - so the collapse only half applied and
     printed the word "Install" across a 44px box. Sizing the button is theirs;
     the tap area is dealt with under (hover: none) below. */
  #installBtn.install { flex: none; }

  #voicebar { padding: var(--s-3) var(--s-5); }
  /* A busy room must not eat the phone. Two rows, always: who is in the room,
     then the controls. The spacer is given a full-width basis so it cannot fit
     beside anything and therefore forces the break at exactly that point -
     without it the buttons pack in next to the names and every extra person
     pushes another row onto a screen that has none to give. */
  #voicebar .sp {
    flex: 1 0 100%;
    height: 0;
  }
  /* Basis 0, not auto: with an auto basis the strip is as wide as all its pills
     laid end to end, which sends it to a line of its own. */
  #vparts {
    flex: 1 1 0;
    min-width: 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -webkit-mask-image: linear-gradient(to right, currentColor calc(100% - 16px), transparent);
    mask-image: linear-gradient(to right, currentColor calc(100% - 16px), transparent);
  }
  #vparts::-webkit-scrollbar { height: 0; }

  /* Off-canvas drawer. Absolute inside main means it lines up under the header
     and the voice bar without either of them being measured, and it clears the
     rail so the Space switcher stays reachable while it is open.

     pointer-events is not optional: a translated element still takes taps, and
     that is exactly how the closed drawer used to swallow the first tap on the
     message list. It is used here instead of visibility:hidden because
     visibility also hides the channel list from the screenshot and a11y tooling
     that has to be able to see the rows; the tap-swallowing bug is a
     pointer-events problem and this kills it outright. */
  #sidebar {
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--rail-w);
    z-index: var(--z-drawer);
    /* Always leave a strip of the conversation showing: it is the scrim, it is
       the tap-to-close target, and it keeps the drawer from reading as a page. */
    width: min(var(--sidebar-w), calc(100vw - var(--rail-w) - var(--s-12)));
    padding-bottom: calc(var(--s-9) + env(safe-area-inset-bottom));
    /* No shadow while closed. transform moves the BOX off screen, but a shadow
       is painted outside the box, so a 28px blur on a drawer parked at the left
       edge spills 28px of grey back onto the conversation - down the full height
       of the phone, permanently, with the drawer shut. Measured: a gradient from
       #f0f1f2 at x=0 fading to white by x=28, which reads as one very thick
       smudged separator hugging the left side and nothing else.
       The lift belongs to the drawer while it is over the content; parked, it is
       not over anything. Transitioned with the slide so it does not snap on. */
    box-shadow: none;
    pointer-events: none;
    transform: translateX(calc(-100% - var(--rail-w)));
    transition: transform var(--m-slow) var(--m-out),
                box-shadow var(--m-slow) var(--m-out);
  }
  body.nav-open #sidebar {
    pointer-events: auto;
    transform: none;
    box-shadow: var(--e-3);
  }

  /* uxfix.js drops .ux-one-space on the body when there is nothing to switch
     between, and polish.css then hides the rail on a phone. The drawer's offset
     is measured from the rail, so without this it opened 60px in from the edge
     with 60px of dimmed nothing beside it. There is no CSS way to ask whether a
     sibling is displayed, so it keys off the same class - the drawer's geometry
     is this file's, the decision to hide the rail is theirs. */
  body.ux-one-space #sidebar {
    left: 0;
    width: min(var(--sidebar-w), calc(100vw - var(--s-12)));
    transform: translateX(-100%);
  }
  body.ux-one-space.nav-open #sidebar { transform: none; }

  #messages::after { content: ""; }

  /* The right panel becomes a sheet over everything, with its own header as the
     way back out. */
  aside#panel,
  body.panel-open aside#panel {
    position: fixed;
    inset: 0;
    z-index: var(--z-panel);
    width: 100%;
    border-left: none;
    background: var(--c-bg);
    animation: sheet-in var(--m-slow) var(--m-out);
  }
  aside#panel > header {
    height: calc(var(--header-h) + env(safe-area-inset-top));
    padding-top: env(safe-area-inset-top);
  }
  aside#panel > .content {
    padding: var(--s-5) var(--s-5) calc(var(--s-9) + env(safe-area-inset-bottom));
  }

  @keyframes sheet-in {
    from { opacity: 0; transform: translateY(16px); }
    to { opacity: 1; transform: none; }
  }

  /* The rail narrows but the tiles do not: 44px stays the floor for a thumb. */
  #spaceRail { gap: var(--s-3); }

  .authcard {
    padding: var(--s-8) var(--s-6);
    border-radius: var(--r-lg);
  }
  .brand h1 { font-size: var(--t-2xl); }
}

/* --------------------------------------------------------- SMALL PHONE
   360px wide, nothing may run off the side. */
@media (max-width: 380px) {
  .hdr-actions { max-width: 26vw; }
  .authwrap { padding: var(--s-6) var(--s-4) calc(var(--s-6) + env(safe-area-inset-bottom)); }
  .authcard { padding: var(--s-7) var(--s-5); }
  #voicebar { padding: var(--s-3) var(--s-4); }
}

/* Landscape phone: the sign-in button must not need a scroll to reach. */
@media (max-height: 520px) and (max-width: 860px) {
  .authwrap { align-items: flex-start; padding-top: var(--s-6); }
  .authcard { gap: var(--s-4); padding: var(--s-6) var(--s-6) var(--s-7); }
  .tagline { display: none; }
}

/* ------------------------------------------------------------------ TOUCH
   Hover does not exist, so every row has to be reachable by thumb and no
   affordance may depend on being hovered first. */
@media (hover: none) {
  /* Scoped to #sidebar on purpose. A later stylesheet sets `.chan { min-height:
     34px }` with no media query; that ties this rule on specificity and wins on
     load order, and every channel row in the phone drawer quietly became a 34px
     mis-tap. A media query adds nothing to specificity - the ancestor does. */
  #sidebar .chan,
  #sidebar .chan-add,
  #sidebar h3 {
    min-height: 44px;
  }
  #sidebar .chan { padding: var(--s-3) var(--s-4); }
  /* A section header collapses its group, so it is a control, not a caption. */
  #sidebar h3 { margin-top: var(--s-5); }
  .chan-add { font-size: var(--t-base); }

  #topbar button.icon,
  /* Closing a full-screen sheet is the only way back out of it on a phone, so
     it gets the same floor as everything else in a header. */
  aside#panel > header button.icon {
    min-width: 44px;
    min-height: 44px;
  }

  /* The install chip is 28px tall by another file's design and growing the box
     would break the bar it sits in. Hang the missing hit area off it instead -
     the same trick components.css uses for icon buttons - so the thumb gets 44
     without the layout being told about it. */
  #installBtn.install { position: relative; }
  #installBtn.install::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: 44px;
    transform: translateY(-50%);
  }
  /* Each Space's tint is the only thing telling the tiles apart, and on a phone
     there is no hover to bring it up to full strength. Start it there. */
  .sicon::after { opacity: 1; }

  /* No hover means no morph and no indicator stub: the open Space carries its
     bar at rest, and every tile is already a 44px target. */
  .sicon:hover { border-radius: var(--r-xl); }
  .sicon:hover::before { opacity: 0; transform: translateY(-50%) scaleY(0); }
  .sicon:hover .sdot { opacity: 1; }
  .sicon.active:hover { border-radius: var(--r-md); }
  .sicon.active:hover::before { opacity: 1; transform: translateY(-50%) scaleY(1); }
  .sicon.active:hover .sdot { opacity: 0; }
}
