/* ServerMod dashboard — single stylesheet (replaces Bootstrap + inline layout CSS).
   Loaded once and cached. Includes a focused subset of Bootstrap-compatible utilities
   so existing page HTML keeps working without rewrites.
   Theme: squared slate-dark, single steel-teal accent (redesign). */

@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');

:root {
  /* The top bar's height, named so the full-bleed Inbox can size its panes
     from what is left beneath it. Set on .topbar as a min-height, so this is
     the single source rather than a number copied into a calc(). */
  --sm-topbar-h: 3.5rem;
  /*
     Progression tiers. Metals, so they are recognisable without a legend —
     but as tokens, so light mode and a rebrand are overrides rather than
     four hexes scattered through the markup. See util/achievementTiers.js,
     which names these and nothing else about how they look.
  */
  --sm-tier-bronze: #c88a52;
  --sm-tier-silver: #b9c2cf;
  --sm-tier-gold: #e0b341;
  --sm-tier-diamond: #6fd3e8;
  /* Surface ramp — page darkest, panels lifted. Var names kept so the whole
     stylesheet inherits the new palette without rule rewrites. */
  --sm-bg-base: #0e1116;   /* page / main content background */
  --sm-bg-raised: #181d27; /* cards, sidebar, panels */
  --sm-bg-deep: #11151d;   /* topbar, inputs, code, deepest insets */
  --sm-bg-hover: #1e2530;  /* hovered nav items / table rows */
  --sm-text: #e8ecf2;
  --sm-text-muted: #aab3c2;
  --sm-text-dim: #79839a;
  --sm-text-faint: #5a6376;
  --sm-accent: #4d97a1;        /* steel-teal */
  --sm-accent-hover: #6cacb5;  /* brightened accent */
  --sm-accent-cyan: #6cacb5;   /* unified to one accent (was a 2nd cyan) */
  --sm-accent-ghost: rgba(77,151,161,0.12);
  /* Text/icons on a filled accent surface. Overridden per-guild by the bot
     colour (see dashboard/accent.js) — a pale brand colour flips this to dark
     so anything actually painted in the accent stays legible. Buttons no
     longer use it; pills, switch thumbs and the bot tag still do. */
  --sm-accent-fg: #ffffff;
  /* Buttons: filled in the brand colour, no border, label always white.
     --sm-btn-fg is a fixed #ffffff and NOT --sm-accent-fg, which is the
     luminance-derived token. That distinction is the whole point: accent-fg
     flips to near-black on a pale brand colour, which is what turned the
     buttons dark-on-white and — via a specificity leak, since fixed — made the
     transparent outline buttons disappear entirely. The label is white here
     regardless of what the accent does.
     CAVEAT, and it is a real one: a very light brand colour gives white on
     near-white. See the note on --sm-accent-fg. */
  --sm-btn-bg: var(--sm-accent);
  --sm-btn-bg-hover: var(--sm-accent-hover);
  --sm-btn-fg: #ffffff;
  --sm-danger: #d06257;
  --sm-warning: #cf9f4e;
  --sm-success: #57a578;
  --sm-info: #5c86c6;
  --sm-border: rgba(255,255,255,0.07);
  --sm-border-strong: rgba(255,255,255,0.13);
  --sm-pill-bg: rgba(77,151,161,0.16);
  --sm-pill-border: rgba(77,151,161,0.26);
  --sm-font-ui: "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --sm-font-mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", "Menlo", "Consolas", monospace;

  /* ---------- Tokens the light theme flips ----------

     Everything below exists because the rules further down used to write the
     colour in place, and every one of those was an assumption that the page is
     dark: a white film lightens a dark surface and dirties a white one, a black
     drop shadow that reads as depth on slate reads as soot on paper, and #fff
     body text on a white card is nothing at all.

     WASH - a film over whatever is underneath, for a striped row, a heat-map
     floor, a hovered item inside a panel. White on dark, black on light: the
     job is "a shade away from the surface", and which direction that is IS the
     theme. */
  --sm-wash-1: rgba(255,255,255,0.035);
  --sm-wash-2: rgba(255,255,255,0.07);
  --sm-wash-3: rgba(255,255,255,0.12);

  /* SHADOW - the colour only; each rule keeps its own geometry, because a
     dropdown's shadow and a drawer's are different shapes of the same idea.
     Light mode is not just "less black": paper shadows are tighter and cooler,
     so these carry a blue cast there rather than pure grey. */
  --sm-shadow-1: rgba(0,0,0,0.25);
  --sm-shadow-2: rgba(0,0,0,0.35);
  --sm-shadow-3: rgba(0,0,0,0.45);

  /* The dimmer behind a modal. Near-black in both themes - a modal backdrop is
     the one place a light UI is also expected to go dark - but lighter and
     bluer on paper so the page behind it stays legible as context. */
  --sm-backdrop: rgba(5,8,14,0.72);

  --sm-scroll-thumb: #3a3d44;
  --sm-scroll-thumb-hover: #4a4d56;

  /* Text on a FILLED semantic colour - a danger button, an avatar circle, a
     status chip with a solid ground. White in both themes, because the fill
     underneath is the same colour in both. Distinct from --sm-text, which is
     the page's body colour and inverts. */
  --sm-on-solid: #ffffff;

  /* Type on an AMBER fill, which is the one semantic that does not take white.
     Dark here, because dark-mode amber (#cf9f4e) is bright enough to be paper;
     white in light mode, where amber has been taken down far enough to be ink.
     The other three fills stay dark enough in both themes for --sm-on-solid. */
  --sm-on-warning: #1e1f22;

  /* A neutral filled button (.btn-secondary, .bg-secondary): the one button
     that is a surface rather than a meaning. */
  --sm-neutral: #4e5058;
  --sm-neutral-hover: #5d5f68;

  /* SEMANTIC TINTS, DERIVED RATHER THAN LISTED.

     A status chip is the semantic colour at low opacity with a slightly
     stronger line around it, and there were two dozen hand-written rgba()
     triples saying exactly that - each one a copy of a hex from the top of this
     file that would not follow when the light theme changed it.

     color-mix keeps them tied to the token: redefine --sm-danger for light mode
     and every danger chip, border and caption re-derives. `transparent` as the
     second colour is what makes them tints rather than opaque swatches, so they
     sit correctly on a card, a table row or a modal alike.

     The `-text` variants pull the semantic hue most of the way towards the body
     colour: a caption has to be READ, and #d06257 at full strength on white is
     a colour, not a sentence. */
  --sm-danger-soft: color-mix(in srgb, var(--sm-danger) 15%, transparent);
  --sm-danger-line: color-mix(in srgb, var(--sm-danger) 35%, transparent);
  --sm-danger-text: color-mix(in srgb, var(--sm-danger) 62%, var(--sm-text));
  --sm-warning-soft: color-mix(in srgb, var(--sm-warning) 15%, transparent);
  --sm-warning-line: color-mix(in srgb, var(--sm-warning) 35%, transparent);
  --sm-warning-text: color-mix(in srgb, var(--sm-warning) 62%, var(--sm-text));
  --sm-success-soft: color-mix(in srgb, var(--sm-success) 15%, transparent);
  --sm-success-line: color-mix(in srgb, var(--sm-success) 35%, transparent);
  --sm-success-text: color-mix(in srgb, var(--sm-success) 62%, var(--sm-text));
  --sm-info-soft: color-mix(in srgb, var(--sm-info) 15%, transparent);
  --sm-info-line: color-mix(in srgb, var(--sm-info) 35%, transparent);
  --sm-info-text: color-mix(in srgb, var(--sm-info) 62%, var(--sm-text));

  /* CHART CHROME. Read by the browser at chart-build time rather than by CSS —
     Chart.js paints to a canvas and cannot resolve a variable itself, so
     dashboard/pages/chartPalette.js hands it the resolved value. Only the
     axes and gridlines: the series colours are the same on either ground, and
     live in that file. */
  --sm-chart-tick: #b5bac1;
  --sm-chart-grid: rgba(255,255,255,0.06);

  /* Form controls, the scrollbar gutter and the canvas the browser paints
     before this stylesheet arrives are the UA's to draw, and this is the only
     way to tell it which way up the page is. */
  color-scheme: dark;
}

/* ============================================================================
   LIGHT MODE

   Set by dashboard/layout.js as `data-theme` on <html>, from a per-user
   preference stored in `dashboard_user_prefs`. Dark is the default and stays
   the default: this block only exists for the reader who asked for the other
   one, or whose system did.

   ONLY TOKENS ARE REDEFINED HERE. Not one rule below this block is repeated for
   light mode, and that is the constraint that keeps two themes from becoming
   two stylesheets that drift. If something looks wrong in light mode, the fix
   is a token - and if no token can express it, the rule that hardcoded a colour
   is the bug.

   THE RAMP INVERTS ITS DIRECTION, NOT ITS MEANING. In dark, `base` is the
   darkest and panels are LIFTED out of it towards the light. On paper the page
   is the mid tone, cards are lifted to pure white, and `deep` - the topbar,
   inputs, code blocks, insets - is RECESSED into grey. The names go on meaning
   what they meant: base is the page, raised is a card, deep is a well.

   THE ACCENT IS NOT REDEFINED HERE, BUT IT IS READ HERE. It is the guild's
   brand colour, injected after this stylesheet by dashboard/accent.js, and it
   means the same thing in both themes — so nothing below sets it. Several
   things below are mixed FROM it, because a light theme that ignored it came
   out belonging to nobody: the surfaces carry a few percent of it, and the
   values derived from it that a light ground needs differently are in the
   block after this one.
   ========================================================================= */
:root[data-theme="light"] {
  /* THE SURFACES CARRY THE GUILD'S COLOUR.

     Dark mode's greys are not grey — they are a blue-slate, and that cast is
     most of why the dark dashboard reads as one designed surface rather than as
     a black page. Light mode was built the same way at first and it came out
     as generic white: correct, and belonging to nobody.

     So the paper is mixed with the accent. A few percent only: enough that the
     page, the top bar and a hovered row are visibly the server's colour, not
     enough to compete with anything painted IN that colour — a button, an
     active nav item and a pill all still have to read as accented against it,
     which is what sets the ceiling here rather than taste.

     `var(--sm-accent)` resolves at use, and dashboard/accent.js overrides that
     variable per guild AFTER this stylesheet, so these follow whatever colour a
     server has set without this block knowing anything about it. A guild on the
     house teal gets the house tint by the same route.

     CARDS STAY WHITE. They are what the reader looks at, the tinted page is
     what they are lifted out of, and tinting both would flatten the one
     distinction the light ramp has. It is also the surface every contrast
     figure in the palette was measured against. */
  --sm-bg-base: color-mix(in srgb, var(--sm-accent) 6%, #f1f3f7);
  --sm-bg-raised: #ffffff;
  --sm-bg-deep: color-mix(in srgb, var(--sm-accent) 9%, #e7eaf0);
  --sm-bg-hover: color-mix(in srgb, var(--sm-accent) 13%, #e4e8ef);
  --sm-text: #161b24;
  --sm-text-muted: #4b5666;
  --sm-text-dim: #66717f;
  --sm-text-faint: #8892a0;

  /* Darkened from their dark-mode values, which were chosen to glow on slate
     and go pastel on white. These are the same hues taken down until a caption
     in one of them is readable on a card. */
  --sm-danger: #b8453a;
  --sm-warning: #8a6414;
  --sm-success: #2c7a55;
  --sm-info: #3a63ab;

  /* Ink, not light. Borders on paper are the absence of the card, so they are
     the page's own dark blue at low alpha rather than white film. */
  --sm-border: rgba(20,28,42,0.12);
  --sm-border-strong: rgba(20,28,42,0.22);
  --sm-wash-1: rgba(20,28,42,0.03);
  --sm-wash-2: rgba(20,28,42,0.06);
  --sm-wash-3: rgba(20,28,42,0.11);

  /* Tighter and far weaker than the dark set: a heavy black shadow on a white
     page reads as dirt rather than as height. */
  --sm-shadow-1: rgba(16,24,40,0.06);
  --sm-shadow-2: rgba(16,24,40,0.10);
  --sm-shadow-3: rgba(16,24,40,0.16);
  --sm-backdrop: rgba(20,28,42,0.45);

  --sm-scroll-thumb: #c3cbd6;
  --sm-scroll-thumb-hover: #a8b2c0;

  /* Ticks go dark enough to read against white; the gridline becomes ink at
     low alpha, which is the same "barely there" it was and the opposite
     colour. */
  --sm-chart-tick: #5a6473;
  --sm-chart-grid: rgba(20,28,42,0.10);

  --sm-neutral: #5b6472;
  --sm-neutral-hover: #4a525e;
  /* See the note on the dark value: the light amber is an ink, not a paper. */
  --sm-on-warning: #ffffff;

  color-scheme: light;
}

/* THE ACCENT-DERIVED OVERRIDES, and they need a word about their selector.

   `--sm-accent-cyan` is the stylesheet's alias for "the accent, as text":
   links, active nav labels, inline code. It is the accent BRIGHTENED, which is
   correct on slate and is exactly the wrong direction on paper - a brightened
   teal on white is a pale line nobody over forty can read.

   The selector is `html:root[data-theme="light"]` rather than the plain
   `:root[...]` above because of specificity: dashboard/accent.js emits its
   guild-colour block as `:root{...}` AFTER this file, so at equal specificity
   it wins on source order - which is right for the accent itself and wrong for
   a value derived from it. The extra element selector puts these ahead of that
   block while still READING var(--sm-accent) from it, so a guild's own colour
   is what gets darkened and tinted.

   The surface tints in the block above need none of this: nothing in the accent
   block touches --sm-bg-*, so they are never in that contest. */
html:root[data-theme="light"] {
  --sm-accent-cyan: color-mix(in srgb, var(--sm-accent) 82%, #04070d);

  /* The pill pair lives here for the same specificity reason, and it matters
     more than it looks. Both tokens are ALSO written by the accent block, at
     alphas chosen for slate (0.10 fill, 0.26 line); on paper that fill is a
     solid block rather than a tint. Left in the plain :root[data-theme] rule
     above, these values applied only to a guild that had not set a colour —
     so the house teal got the light-tuned pill and every branded server got the
     dark-tuned one, which is precisely backwards. Here they win in both cases,
     and still derive from whatever --sm-accent the guild set. */
  --sm-pill-bg: color-mix(in srgb, var(--sm-accent) 12%, transparent);
  --sm-pill-border: color-mix(in srgb, var(--sm-accent) 30%, transparent);
}

* { box-sizing: border-box; }

html, body { max-width: 100%; overflow-x: hidden; }

/* Scrollbars (Chromium + Firefox). The thumb is a token because a light
   grey bar is invisible on paper and a dark one is invisible on slate. */
* { scrollbar-width: thin; scrollbar-color: var(--sm-scroll-thumb) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--sm-scroll-thumb); border-radius: 5px; border: 2px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background: var(--sm-scroll-thumb-hover); background-clip: content-box; }

body {
  margin: 0;
  font-family: var(--sm-font-ui);
  background: var(--sm-bg-base);
  color: var(--sm-text);
  /* dvh, not vh. On a phone the URL bar is counted inside 100vh but sits over
     the page, so a 100vh app shell is taller than what you can actually see and
     the bottom of it — the Copilot composer, a form's Save row — is pushed
     under the browser chrome. 100dvh tracks the visible area as the bar hides
     and reappears. vh stays first as the fallback for browsers without dvh. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  -webkit-text-size-adjust: 100%;
}

/* ============== Topbar / sidebar / layout ============== */
/* `position: relative` is load-bearing: it is the containing block the
   notification panel falls back to on mobile, where the panel spans the page
   gutters instead of hanging off the bell. See the .sm-notif-panel mobile rule.
   No z-index, so this does not create a stacking context and the panel's
   z-index still competes globally with the sidebar. */
/* Named, because the full-bleed Inbox sizes its panes from what is left under
   it and a hard-coded twin would drift the first time this padding changed. */
.topbar { position: relative; min-height: var(--sm-topbar-h); background: var(--sm-bg-deep); padding: 0.75rem 1.5rem; display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; border-bottom: 1px solid var(--sm-border); flex-shrink: 0; }
/* An <a> home to the dashboard. Below 768px .brand-text is hidden and the icon
   alone carries the link, so the anchor keeps a tap-sized box of its own. */
.topbar .brand { display: flex; align-items: center; gap: 0.5rem; min-width: 0; color: inherit; text-decoration: none; padding: 0.15rem 0.1rem; }
.topbar .brand:hover, .topbar .brand:focus { color: inherit; text-decoration: none; }
.topbar .brand:hover .brand-icon { opacity: 0.85; }
.topbar .brand:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }
.topbar .brand-icon { width: 32px; height: 32px; border-radius: 0; object-fit: contain; flex-shrink: 0; }
.topbar .brand-text { margin: 0; font-size: 1.25rem; font-weight: 600; white-space: nowrap; }
.topbar a { color: var(--sm-accent); text-decoration: none; }
.topbar a:hover, .topbar a:active { color: var(--sm-accent-cyan); text-decoration: underline; }
.user-bar { display: flex; align-items: center; gap: 0.75rem; margin-left: auto; }
.user-bar .avatar { width: 32px; height: 32px; border-radius: 50%; object-fit: cover; }
/* Your name and face, which are now the way to your own page. Styled as
   furniture rather than as a link — no underline, no accent — because it has to
   keep reading as "this is you" first and as a control second. The hover is the
   whole affordance, and it is the one the account menu trigger already uses. */
.user-bar .user-identity { display: flex; align-items: center; gap: 0.5rem; padding: 0.15rem 0.35rem; color: inherit; text-decoration: none; border-radius: 0; }
.user-bar .user-identity:hover { background: var(--sm-bg-raised); text-decoration: none; color: var(--sm-text); }
.user-bar .user-identity:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }
/* Above 768px the identity in the bar is the link, so the menu's copy is off.
   Below it, the identity is hidden and the menu row is the only route. */
/* Off above 768px, where the identity in the bar is the link instead.

   The colour is explicit for the same reason `.user-bar .sign-out` sets one:
   this is an <a> inside `.topbar`, and `.topbar a` paints links in the accent —
   which rendered one menu row in the guild's brand colour and the three beside
   it in body text, as though it were a different kind of thing. */
.user-menu-me { display: none; color: var(--sm-text); }
.user-menu-me:hover, .user-menu-me:focus { color: var(--sm-text); text-decoration: none; }
.user-bar .username { color: var(--sm-text); font-size: 0.9rem; }
.user-bar .sign-out { color: var(--sm-text-muted); padding: 0.35rem 0.5rem; border-radius: 0; font-size: 1.1rem; line-height: 1; }
.user-bar .sign-out:hover { color: var(--sm-text); background: var(--sm-bg-raised); text-decoration: none; }

/* The server's icon in the title bar.

   Only below 768px, and there it is the ONLY mark: above 768px the brand is
   back and the sidebar header already carries the server, so a second copy is
   furniture — and below it, two marks side by side cost the title row twice
   the space to say one thing the reader needs and one they do not.

   Squared, like every other surface in the theme. It was the one round tile
   left in the bar, which is also what made it read as a second avatar. */
.topbar-server-home { display: none; line-height: 0; flex-shrink: 0; padding: 0.15rem; }
.topbar-server-home:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 2px; }
.topbar-server-home:hover .topbar-server-icon { opacity: 0.85; }
.topbar-server-icon { width: 28px; height: 28px; border-radius: 0; object-fit: cover; display: block; }
@media (max-width: 768px) {
  .topbar-server-home { display: block; }
  /* The bot's mark goes, and its link with it — which is why the server icon
     above is an <a>. Losing both would leave the menu button as the only route
     back to Overview. */
  .topbar .brand { display: none; }
}

/* The account controls, and where they sit.

   ONE SET OF CONTROLS, TWO PLACEMENTS. The bell, Copilot and Sign out are
   rendered once (layout.js) — the bell is a form POST and a second copy would
   be a second thing to keep in step. Above 768px they are laid out in the bar;
   below it the same three become the rows of a dropdown behind the avatar.

   DESKTOP IS THE DEFAULT HERE, and the phone is the media query. That is the
   way round it has to be: the panel is a plain flex row in the bar, and only
   the narrow rule lifts it out into a positioned dropdown. Written the other
   way, every desktop declaration would be an undo. */
.user-menu-panel {
  display: flex; align-items: center; gap: 0.25rem;
}
/* The identity — an avatar and a name, no more. It is a control on a phone and
   plain furniture on a desktop, so the desktop copy is a bare <img> and <span>
   rather than a button nobody may press. */
.user-identity-avatar, .user-identity-name { display: inline-block; }
.user-menu-trigger { display: none; }

/* In the bar the three controls are glyphs. `.control-label` carries the word
   for the dropdown, where a column of bare icons is unreadable — see the
   narrow rule below. Every one of them keeps its aria-label and title, so the
   accessible name never depends on the span. */
.control-label { display: none; }

.user-menu-caret { font-size: 0.7rem; color: var(--sm-text-muted); transition: transform 120ms ease; }
.user-menu-trigger[aria-expanded="true"] .user-menu-caret { transform: rotate(180deg); }

/* Below 768px: the avatar becomes the control and the three go under it.

   Four things competed for the right-hand edge of a phone — the menu button,
   two icons, a name — and none of the three is about the page, they are all
   about the session. */
@media (max-width: 768px) {
  .user-identity-avatar, .user-identity-name { display: none; }
  .user-menu-trigger {
    display: flex; align-items: center; gap: 0.5rem;
    padding: 0.15rem 0.25rem; border: none; border-radius: 0;
    background: none; color: inherit; cursor: pointer;
  }
  .user-menu-trigger:hover { background: var(--sm-bg-raised); }
  .user-menu-trigger:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }

  /* Anchored to .topbar rather than to the 32px avatar, and pinned to the
     right so it cannot hang off the edge. z-index above the sidebar, which is
     the only thing on the page competing for this corner. */
  .user-menu-panel {
    position: absolute; top: calc(100% + 0.4rem); right: 0.6rem; left: auto; z-index: 60;
    min-width: 13rem; padding: 0.35rem;
    flex-direction: column; align-items: stretch; gap: 0.15rem;
    background: var(--sm-bg-raised); border: 1px solid var(--sm-border);
    box-shadow: 0 10px 24px var(--sm-shadow-3);
  }
  .user-menu-panel:not(.is-open) { display: none; }

  /* Every child reads as one labelled row, whatever element it happens to be —
     the bell is a form, Copilot is a button and Sign out is an anchor. Icon,
     gap, label, and the same padding on all three, which is what makes them
     line up rather than each sitting at its own indent. */
  .user-menu-panel > *,
  .user-menu-panel .copilot-launch,
  .user-menu-panel .sign-out,
  .user-menu-panel .sm-theme-toggle,
  .user-menu-panel .sm-notif-open {
    display: flex; align-items: center; gap: 0.55rem; width: 100%;
    padding: 0.45rem 0.6rem; font-size: 0.9rem; text-align: left; line-height: 1.2;
    color: var(--sm-text); background: none; border: none; border-radius: 0;
  }
  /* A fixed column for the glyph, so the labels start at the same x whatever
     the icon's natural width — a bell, a wand and an arrow are three different
     widths, and left to themselves they staggered the three words. */
  .user-menu-panel > * > i:first-child,
  .user-menu-panel .sm-notif-open > i:first-child {
    width: 1.1rem; flex-shrink: 0; text-align: center; font-size: 1rem;
  }
  .user-menu-panel .control-label { display: inline; }
  .user-menu-panel .copilot-launch:hover,
  .user-menu-panel .user-menu-me:hover,
  .user-menu-panel .sign-out:hover,
  .user-menu-panel .sm-theme-toggle:hover,
  .user-menu-panel .sm-notif-open:hover { background: var(--sm-bg-hover); color: var(--sm-text); }
  /* The form is a wrapper, not a row: display:contents lets the button inside
     it be the row, the same trick .sm-notif-wrap uses two rules down. */
  .user-menu-panel .sm-theme-form { display: contents; }
  /* The bell keeps its unread pill, but as a trailing count rather than a dot
     floating over a glyph — in a labelled row there is room to just say it. */
  .user-menu-panel .sm-notif-badge { position: static; margin-left: auto; }
  /* The phone's route to the Me page — a labelled row like every other. */
  .user-menu-me { display: flex; }
  /* The wrap is a positioning context in the bar; inside a menu row it must not
     be, or it shrinks the row to the width of the glyph. */
  .user-menu-panel .sm-notif-wrap { display: contents; }
}

.wrap { display: flex; flex: 1; min-height: 0; }
.sidebar { width: 240px; background: var(--sm-bg-raised); padding: 1rem 0; border-right: 1px solid var(--sm-bg-deep); flex-shrink: 0; display: flex; flex-direction: column; overflow-y: auto; }
.sidebar-empty { padding: 0 1rem; margin: 0; }
/* Credit line, not a nav item — sized down so it reads as a footnote rather
   than competing with the navigation above it. */
.sidebar-footer { margin-top: auto; padding: 0.6rem 1rem; font-size: 0.62rem; color: var(--sm-text-faint); border-top: 1px solid var(--sm-border); display: flex; align-items: center; gap: 0.35rem; }
/* Squared like the topbar mark, and shrink-proof so a long footer line can't
   squash it into a sliver. */
.sidebar-footer-icon { width: 18px; height: 18px; flex-shrink: 0; border-radius: 0; object-fit: contain; opacity: 0.7; }
.sidebar-footer a { color: inherit; }
.sidebar-footer a:hover, .sidebar-footer a:active { color: var(--sm-accent-cyan); }
.sidebar-section { margin-bottom: 1.25rem; }
.sidebar-section-title { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--sm-text-muted); padding: 0 1rem; margin-bottom: 0.5rem; }
.server-header { display: flex; align-items: center; gap: 0.5rem; padding: 0 1rem 0.5rem; }
.server-icon { width: 40px; height: 40px; border-radius: 0; object-fit: cover; flex-shrink: 0; }
/* The name is shown in full — it is never truncated.
   It used to be one nowrap line with an ellipsis, so anything past about
   eighteen characters was cut, and two servers whose names differ only at the
   end were indistinguishable in the sidebar. It wraps instead, and layout.js
   steps the size down for a long one so the wrap is usually unnecessary.
   overflow-wrap:anywhere so a single unbroken 40-character word still fits
   rather than overflowing the sidebar. */
/* nowrap so the badges stay on the name's row rather than dropping beneath it.
   They were in a wrapping flex row, so a name wide enough to leave less than a
   badge's width pushed one badge onto a second line while the other stayed put
   — the two ended up on different rows, which reads as a rendering fault
   rather than as two badges. The name still wraps internally (min-width:0), so
   it gives up width first and the badges never move. */
.server-name-wrap { display: flex; flex-wrap: nowrap; align-items: center; gap: 0.3rem; min-width: 0; flex: 1; }
.server-name { font-size: var(--sm-name-size, 0.95rem); font-weight: 600; color: var(--sm-text); white-space: normal; overflow-wrap: anywhere; line-height: 1.25; min-width: 0; }
/* Two classes deep so these beat the mobile bump below, which is a bare
   .server-name and would otherwise win on source order. */
.server-name.sm-name-m { --sm-name-size: 0.85rem; }
.server-name.sm-name-s { --sm-name-size: 0.78rem; }
.server-name.sm-name-xs { --sm-name-size: 0.72rem; }
/* Discord Partner / Verified. flex-shrink:0 so the badge is the one thing that
   never gets squeezed — it is a fact about the server, and half a badge reads
   as a rendering fault. Sized off the name rather than fixed, so it stays in
   proportion at the larger mobile font size. */
/* The badge carries its own colours in the SVG — Discord's green and blurple —
   so it deliberately inherits nothing from the theme or the guild's accent.
   These are Discord's marks and mean the same thing on every server. */
.server-badge { flex-shrink: 0; display: inline-flex; align-items: center; line-height: 1; }
.server-badge-svg { display: block; }
.server-badge + .server-badge { margin-left: -0.15rem; }
.sidebar-section-title .server-badge { margin-left: 0.35rem; }

/* ==================== Calendar =====================

   A month is a seven-column grid of equal cells, and every hard part of it is a
   consequence of that: a cell has to hold an unknown number of chips without
   changing the height of its row, and the row cannot scroll because a month
   that scrolls internally is a list with extra steps.

   So the cell has a minimum height and the chips inside it are allowed to
   overflow into a scroll of their own — the day keeps its place in the grid,
   and a day with nine things in it is scrollable rather than nine times taller
   than its neighbours. */
.sm-cal-nav { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
.sm-cal-month { font-size: 1rem; font-weight: 600; min-width: 9rem; }
/* Pushed to its own end of the row, and first to go when the row runs out. */
.sm-cal-tz { margin-left: auto; }

/* THE FILTER IS ALSO THE LEGEND.

   Two layers share this calendar, and a reader has to be able to tell which is
   which before they can decide to hide one. So each tab carries the swatch its
   layer is drawn in — a filled chip for an event, a dashed outline for leave —
   which means the control explains the colours rather than needing a second
   thing beside it that does. */
.sm-cal-filters { display: inline-flex; border: 1px solid var(--sm-border); background: var(--sm-bg-deep); }
.sm-cal-filter {
  display: inline-flex; align-items: center; gap: 0.35rem;
  padding: 0.3rem 0.6rem; font-size: 0.8rem; line-height: 1.2;
  color: var(--sm-text-muted); text-decoration: none; white-space: nowrap;
}
.sm-cal-filter + .sm-cal-filter { border-left: 1px solid var(--sm-border); }
.sm-cal-filter:hover { background: var(--sm-bg-hover); color: var(--sm-text); text-decoration: none; }
.sm-cal-filter.is-active { background: var(--sm-accent-ghost); color: var(--sm-accent-cyan); font-weight: 600; }
.sm-cal-swatch { width: 0.7rem; height: 0.7rem; flex-shrink: 0; }
.sm-cal-swatch-event { background: var(--sm-accent-ghost); border-left: 3px solid var(--sm-accent); }
.sm-cal-swatch-leave { border: 1px dashed var(--sm-text-dim); }

.sm-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: 1px;
  /* The gap IS the gridline: one background behind seven columns of cells, so
     the rules are hairlines that cannot fall out of step with the cell borders
     the way seven separate borders do. */
  background: var(--sm-border);
  border: 1px solid var(--sm-border);
}
.sm-cal-head {
  background: var(--sm-bg-deep); color: var(--sm-text-dim);
  font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
  padding: 0.35rem 0.5rem; text-align: center;
}
.sm-cal-day {
  background: var(--sm-bg-raised); min-height: 6.5rem; padding: 0.3rem 0.35rem;
  display: flex; flex-direction: column; gap: 0.2rem; min-width: 0;
}
/* The days either side of the month are shown rather than blanked: a week that
   straddles the boundary is still a week, and an empty cell reads as a fault.
   Recessed, so the month itself is what the eye lands on. */
.sm-cal-day.is-outside { background: var(--sm-bg-base); }
.sm-cal-day.is-outside .sm-cal-daynum { color: var(--sm-text-faint); }
.sm-cal-day.is-today { box-shadow: inset 0 0 0 2px var(--sm-accent); }
.sm-cal-daynum { font-size: 0.75rem; font-weight: 600; color: var(--sm-text-muted); font-family: var(--sm-font-mono); }
.sm-cal-day.is-today .sm-cal-daynum { color: var(--sm-accent-cyan); }
.sm-cal-chips { display: flex; flex-direction: column; gap: 0.15rem; overflow-y: auto; min-height: 0; }

/* A chip is a tint of its own colour with a solid left edge — the same shape as
   an alert, at a quarter the size, so the vocabulary carries over. The tints
   are derived from the semantic tokens, so they follow the theme. */
.sm-cal-chip {
  display: flex; align-items: baseline; gap: 0.3rem;
  padding: 0.12rem 0.3rem; font-size: 0.7rem; line-height: 1.25;
  color: var(--sm-text); text-decoration: none;
  border-left: 3px solid var(--sm-accent); background: var(--sm-accent-ghost);
  min-width: 0;
}
a.sm-cal-chip:hover { background: var(--sm-pill-bg); color: var(--sm-text); text-decoration: none; }
.sm-cal-chip-time { font-family: var(--sm-font-mono); font-size: 0.62rem; color: var(--sm-text-dim); flex-shrink: 0; }
.sm-cal-chip-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.sm-cal-chip-lock { font-size: 0.55rem; color: var(--sm-text-faint); margin-left: auto; flex-shrink: 0; }
.sm-cal-dot { display: inline-block; width: 0.55rem; height: 0.55rem; background: var(--sm-accent); vertical-align: middle; }

.sm-cal-info { border-left-color: var(--sm-info); background: var(--sm-info-soft); }
.sm-cal-success { border-left-color: var(--sm-success); background: var(--sm-success-soft); }
.sm-cal-warning { border-left-color: var(--sm-warning); background: var(--sm-warning-soft); }
.sm-cal-danger { border-left-color: var(--sm-danger); background: var(--sm-danger-soft); }
.sm-cal-neutral { border-left-color: var(--sm-text-dim); background: var(--sm-wash-2); }
.sm-cal-dot.sm-cal-info { background: var(--sm-info); }
.sm-cal-dot.sm-cal-success { background: var(--sm-success); }
.sm-cal-dot.sm-cal-warning { background: var(--sm-warning); }
.sm-cal-dot.sm-cal-danger { background: var(--sm-danger); }
.sm-cal-dot.sm-cal-neutral { background: var(--sm-text-dim); }

/* Leave is the second layer and must not be mistaken for an event: dashed
   edge, no fill, muted type. It is somebody's absence, not something happening. */
.sm-cal-leave {
  border-left: 3px dashed var(--sm-text-dim); background: transparent; color: var(--sm-text-muted);
}

/* ---------- The phone's calendar is a list, not a squeezed grid ----------

   A seven-column month on a 390px screen gives each day about fifty pixels:
   room for the date and no part of an event's title. Collapsing that grid to
   one column — which is what this used to do — is not a calendar either. It is
   forty-two rows, thirty of them empty, with nothing to say where one week ends
   and the next begins.

   So the phone gets the week list the page also renders (see weekList in
   dashboard/pages/calendar.js): a heading per week, and under it only the days
   that have something on them. The grid and the list are built from the same
   two buckets, so they cannot disagree about what is on a date — one is simply
   the shape that fits.

   The swap is at 768px, the same breakpoint the sidebar becomes a drawer at. */
.sm-cal-list { display: none; }

@media (max-width: 768px) {
  .sm-cal-grid { display: none; }
  .sm-cal-list { display: block; }
  .sm-cal-nav { gap: 0.4rem; }
  .sm-cal-month { min-width: 0; flex: 1; }
  /* The timezone note stops earning its line when the row is this tight. */
  .sm-cal-tz { display: none; }
  .sm-cal-filters { width: 100%; }
  .sm-cal-filter { flex: 1; justify-content: center; }
}

.sm-cal-week + .sm-cal-week { margin-top: 1rem; }
.sm-cal-week-head {
  font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--sm-text-dim); margin: 0 0 0.4rem; padding-bottom: 0.25rem;
  border-bottom: 1px solid var(--sm-border);
}
.sm-cal-week-empty { padding: 0.1rem 0 0.3rem; }
.sm-cal-day-block + .sm-cal-day-block { margin-top: 0.6rem; }
.sm-cal-day-label { font-size: 0.78rem; font-weight: 600; color: var(--sm-text-muted); margin-bottom: 0.2rem; }
.sm-cal-day-block.is-today .sm-cal-day-label { color: var(--sm-accent-cyan); }
.sm-cal-today-tag {
  font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
  background: var(--sm-accent-ghost); color: var(--sm-accent-cyan); padding: 0.05rem 0.3rem; margin-left: 0.3rem;
}

/* A row is the chip's roomier cousin: same colour vocabulary, same left edge,
   but with space for the whole title and a tap target that is not 20px tall. */
.sm-cal-row {
  display: flex; align-items: baseline; gap: 0.5rem;
  padding: 0.45rem 0.55rem; margin-bottom: 0.2rem;
  font-size: 0.85rem; line-height: 1.3; text-decoration: none; color: var(--sm-text);
  border-left: 3px solid var(--sm-accent); background: var(--sm-accent-ghost);
}
a.sm-cal-row:hover { background: var(--sm-pill-bg); color: var(--sm-text); text-decoration: none; }
.sm-cal-row-time {
  font-family: var(--sm-font-mono); font-size: 0.72rem; color: var(--sm-text-dim);
  flex-shrink: 0; min-width: 3.2rem;
}
.sm-cal-row-title { flex: 1; min-width: 0; }
.sm-cal-row-lock { font-size: 0.7rem; color: var(--sm-text-faint); flex-shrink: 0; }
/* Leave keeps the shape it has on the grid: outlined and dashed, never filled,
   because it is somebody's absence rather than something happening. */
.sm-cal-row.sm-cal-leave { border-left: 3px dashed var(--sm-text-dim); background: transparent; color: var(--sm-text-muted); }

/* ============== The Me page ============== */

/* Three cards, one per mode, rather than a select: the choice is visual and
   the control should be too. Each is its own form so the whole thing works
   without scripting — see the note in dashboard/pages/me.js. */
.sm-theme-choices { display: grid; grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr)); gap: 0.5rem; margin-bottom: 0.75rem; }
.sm-theme-choice-form { display: contents; }
/* `.main` is load-bearing on every one of these: `.main button` paints every
   bare button in the accent with white type, and a bare `.sm-theme-choice`
   loses to it on specificity — which rendered the two modes you are NOT in as
   filled accent cards and the one you are in as a pale ghost. Exactly
   backwards, and invisible until you look at it. */
.main .sm-theme-choice {
  display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-start;
  gap: 0.15rem; width: 100%; min-width: 0; height: 100%;
  padding: 0.7rem 0.8rem; text-align: left; cursor: pointer;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border); color: var(--sm-text);
}
.main .sm-theme-choice:hover { border-color: var(--sm-border-strong); background: var(--sm-bg-hover); }
.main .sm-theme-choice.is-active { border-color: var(--sm-accent); background: var(--sm-accent-ghost); }
.main .sm-theme-choice.is-active .sm-theme-choice-label { color: var(--sm-accent-cyan); }
.main .sm-theme-choice:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }
.sm-theme-choice-icon { font-size: 1.1rem; color: var(--sm-accent-cyan); }
.sm-theme-choice-label { font-weight: 600; font-size: 0.9rem; }
.sm-theme-choice-note { font-size: 0.75rem; color: var(--sm-text-muted); }

/* Three one-button forms on a row. They are separate forms because each posts a
   different thing, and inline-flex keeps them reading as one control group. */
.sm-pin-order { display: inline-flex; gap: 0.25rem; justify-content: flex-end; }
.sm-pin-order form { display: inline; margin: 0; }

/* Approve / decline, with the note they will read. Wraps on a narrow screen
   rather than squeezing the field to nothing. */
.sm-leave-decide { display: flex; gap: 0.35rem; justify-content: flex-end; align-items: center; flex-wrap: wrap; }
.sm-leave-decide .form-control { max-width: 16rem; }

/* ============== Pinned pages, and the pin that fills the list ==============

   A nav entry is now a ROW: the link, and a pin button beside it. The button
   cannot live inside the anchor — a button inside a link is invalid and, worse,
   unclickable in some browsers — so the two are siblings and the row is what
   holds them together.

   THE PIN BUTTON IS GONE FROM THE NAV and its rules with it. It was a real
   <button> in every row, so a stylesheet that was stale, slow or blocked left
   forty browser-default boxes stacked down the sidebar — reported twice. The
   nav is anchors again, which have nothing to fall back to, and pinning lives
   on the Me page. */

/* The list itself, above the nav proper. Separated by a rule rather than by
   space alone: without the line it read as the first nav group and the reader
   met the same page twice with no explanation of why. */
.sidebar-pinned { padding-bottom: 0.6rem; margin-bottom: 0.6rem; border-bottom: 1px solid var(--sm-border); }
/* Its heading is the section title's smaller sibling — this is a label for six
   items, not a peer of the server's own name. */
.sidebar-pinned .sidebar-section-title { font-size: 0.62rem; color: var(--sm-text-faint); margin-bottom: 0.25rem; }
/* Pinned entries are flat by definition: whatever indent they had inside their
   group does not survive being lifted out of it. */
.sidebar-pinned .nav-item-sub { padding-left: 1rem; font-size: 0.9rem; }

.nav-item { display: flex; align-items: center; gap: 0.6rem; padding: 0.5rem 1rem; color: var(--sm-text-muted); text-decoration: none; font-size: 0.9rem; border-left: 3px solid transparent; transition: color 0.12s ease, background 0.12s ease, border-left-color 0.12s ease; }
.nav-item .nav-icon { width: 1.1rem; text-align: center; flex-shrink: 0; opacity: 0.9; }
.nav-item:hover { color: var(--sm-text); background: var(--sm-bg-hover); }
.nav-item.active { color: var(--sm-accent-cyan); background: var(--sm-accent-ghost); border-left-color: var(--sm-accent); font-weight: 500; }
.nav-item.active .nav-icon { color: var(--sm-accent-cyan); }
.nav-group { margin-bottom: 0; }
.nav-group-toggle { display: flex; align-items: center; gap: 0.6rem; width: 100%; padding: 0.5rem 1rem; color: var(--sm-text-dim); font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; background: none; border: none; cursor: pointer; text-align: left; border-left: 3px solid transparent; }
.nav-group-toggle:hover { color: var(--sm-text); background: var(--sm-bg-hover); }
.nav-group-toggle .nav-icon { width: 1.1rem; text-align: center; opacity: 0.9; flex-shrink: 0; }
.nav-group-toggle span { flex: 1; }
.nav-group-chevron { width: 0.75rem; opacity: 0.8; transition: transform 0.2s ease; flex-shrink: 0; }
.nav-group.nav-group-open .nav-group-chevron { transform: rotate(-180deg); }
.nav-group-children { display: none; padding-left: 0; }
.nav-group.nav-group-open .nav-group-children { display: block; }
.nav-item-sub { padding: 0.5rem 1rem 0.5rem 1.75rem; font-size: 0.85rem; }

/* Tighter left/right gutter than top/bottom: horizontal space is what the wide
   tables and chart panels actually need, and the sidebar already separates the
   content from the window edge. */
/*
  THE VERTICAL SCROLLER, AND ONLY THE VERTICAL ONE.

  This was `overflow: auto`, which made it a scroller on BOTH axes — so any
  child even a pixel too wide turned the whole page into a horizontal scroll.
  That failure is not subtle and it is not local: `.main` slides sideways, the
  topbar does not, and every heading and paragraph on the page is clipped at the
  left edge at once. It was reported from a phone on the Levels page, where
  every panel title read as "keeping", "oice XP", "P curve".

  `html, body { overflow-x: hidden }` at the top of this file did not prevent
  it, because the body was never the thing scrolling.

  So the axis is pinned. The rule this enforces is already the house rule —
  "wide content scrolls inside its own `overflow-x: auto` container, the page
  never does" — and every legitimately wide thing already has one: tables have
  `.table-responsive`, code blocks have their own box, charts have theirs. What
  is left over is a layout mistake, and clipping one element is a far better
  failure than sliding the entire page out from under the reader.

  `hidden` rather than `clip`: this element is already a scroll container on Y,
  so the two behave identically here, and `hidden` has no support caveat.
*/
.main { flex: 1; padding: 1.5rem 0.75rem; overflow-x: hidden; overflow-y: auto; color: var(--sm-text); min-width: 0; }
.main .card { color: var(--sm-text); }
.main h2, .main h3, .main h4, .main h5, .main h6, .main p, .main strong, .main dl, .main dd, .main dt { color: var(--sm-text); }
.main h2 { margin: 0 0 1.25rem; font-size: 1.35rem; font-weight: 600; display: flex; align-items: center; gap: 0.55rem; }
.main h2 .label-icon { font-size: 1.1rem; color: var(--sm-text-muted); margin-right: 0.5rem; opacity: 0.9; }

.card { background: var(--sm-bg-raised); border: 1px solid var(--sm-border); border-radius: 0; padding: 1.25rem 1rem; margin-bottom: 1rem; }
.main .card + .card { margin-top: 0; }
.main .card > p:last-child, .main .card > form:last-child, .main .card > div:last-child { margin-bottom: 0; }
.main .settings-card { margin-bottom: 1.5rem; }

/* Two settings panels side by side, without Bootstrap's row.

   A `.row` carries negative side margins equal to half its gutter, so cards
   inside one render a few pixels wider than the plain cards stacked above them
   — the overhang nobody can name but everybody sees, and the reason the stats
   pane was flattened to one column in the first place. A grid has no negative
   margins, so the pair keeps the same left and right edges as everything else.

   Only the column gap is set: each panel already carries `margin-bottom: 1.5rem`
   and that is what spaces the rows, so the vertical rhythm is identical to the
   stack. `align-items: start` because two collapsibles are rarely the same
   height and neither should stretch to match the other. */
.main .sm-panel-pair {
  display: grid;
  grid-template-columns: 1fr;
  column-gap: 1rem;
  /*
     EACH PANEL IS ITS OWN HEIGHT, and the shared bottom edge is not worth what
     it cost.

     This was `align-items: stretch`, with a `:has()` rule dropping back to
     `start` once either side was folded. Both halves worked, and together they
     made the two panels in a pair behave as one: with both open the shorter one
     was stretched to its neighbour, so opening or closing EITHER panel resized
     the other — on Settings → Portal and Settings → Statistics that reads as
     two panels that open and collapse together, which is the one thing a pair
     of separate disclosures must not do.

     Measured rather than argued: two open panels came out 197px each, and
     folding the left took the right from 197 to 123 without it being touched.

     `start` costs a ragged bottom edge where one panel has more in it than the
     other. That is what two panels with different amounts in them look like.
  */
  align-items: start;
}
@media (min-width: 992px) {
  .main .sm-panel-pair { grid-template-columns: 1fr 1fr; }
}
/* A settings card that shares a row with siblings stretches to the tallest of
   the set, so the row has one bottom edge instead of three ragged ones.
   Twenty-five settings cards were written without `h-100`, so every row on
   Settings ended in a staircase — Immune roles and Immune users stopping a
   hundred pixels short of Protected names beside them.
   Done here rather than by adding `h-100` to each call site: the columns
   already stretch (`.row` is flex, so `align-items: stretch` applies), it is
   only the card inside that did not fill one. Keeping the card's own
   margin-bottom is what preserves the gap between stacked rows, which a plain
   `height: 100%` would have overflowed by exactly that margin. */
.main .row > [class*="col"]:has(> .settings-card) { display: flex; flex-direction: column; }
/* `:only-child` — a column holding one card stretches it to the row height. A
   column deliberately stacking two (Ban purge above Native cases, filling the
   space Message log leaves beside them) keeps both at their natural height
   instead of splitting the slack between them. */
.main .row > [class*="col"] > .settings-card:only-child { flex: 1 1 auto; }
.muted { color: var(--sm-text-muted); font-size: 0.9rem; }
.main .text-muted { color: var(--sm-text-muted) !important; }
.main code { background: var(--sm-bg-deep); color: var(--sm-accent-cyan); padding: 0.15em 0.4em; border-radius: 0; font-size: 0.875em; font-family: var(--sm-font-mono); }

/* ============== Tables ============== */
.table { width: 100%; border-collapse: collapse; }
.table th, .table td { text-align: left; padding: 0.5rem; border-bottom: 1px solid var(--sm-border); }
.table th { color: var(--sm-text-muted); font-weight: 600; }
.main .table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Moderator activity: one row per moderator, at every width.

   Eight columns — name, cases, all-time, a case-type chip strip, modmail,
   notes, messages and a "last seen" timestamp. Given less than about 62rem the
   browser does not overflow them, it WRAPS them: the name goes to two lines,
   the chips stack, the timestamp breaks, and a row per moderator becomes a
   block per moderator. That is the squish.

   NOT IN THE MOBILE BLOCK, which is where the first fix put it. The shared
   `min-width: 44rem` floor below stops at 991.98px, and at exactly 992px the
   sidebar still takes 240px — leaving about 730px of content for eight
   columns, which is the same squish one pixel above the breakpoint. The
   pressure is on the CONTENT width, not the viewport, so the rule belongs
   where it applies at both.

   The floor and the nowrap go together. A wider floor alone still lets a long
   moderator name wrap inside its column; nowrap alone would push the table past
   its floor anyway, which is harmless because `.table-responsive` above is
   already scrolling it. The chips get nowrap too, so a moderator with six case
   types runs off the side of the row rather than stacking inside it. */
.main .mod-activity-table { min-width: 62rem; }
.main .mod-activity-table th,
.main .mod-activity-table td { white-space: nowrap; }
.main .table-dark, .main .table-dark th, .main .table-dark td { background: var(--sm-bg-raised); color: var(--sm-text); border-color: var(--sm-bg-deep); }

/* A two-column label/value list, at every width.

   The label takes what it needs and wraps; the figure is right-aligned and
   does not, so a panel of them reads as a column of numbers rather than as
   ragged text. `tabular-nums` keeps those numbers in line with each other,
   which is most of why a list of figures looks deliberate.

   The label is capped rather than left to share the width evenly: with two
   free columns a long value pushes the label narrow and a long label pushes
   the value into two lines, and which of the two happened depended on the
   member. See the mobile block for why this table opts out of the scroll
   floor. */

/* A group heading inside a form.

   Where a panel used to carry a paragraph introducing three fields, it now
   carries one of these and the paragraph is in the help modal. So it has to do
   the paragraph's job of separating groups, without being a heading that
   competes with the panel title above it: small, muted, letterspaced, and
   spaced generously above but tightly below, so it reads as belonging to the
   fields under it rather than floating between the two groups.

   The first one in a form gets no top margin — there is nothing above it to be
   separated from. */
.main .sm-field-group {
  margin: 1.25rem 0 0.5rem; font-size: 0.75rem; font-weight: 700;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--sm-text-muted);
}
.main .sm-field-group:first-child { margin-top: 0; }
/*
  THE GROUP'S LABEL IS A TITLE; EVERYTHING ELSE IN IT IS A SENTENCE.

  The uppercase above is set on the whole block so the label above a field
  needs no class of its own — which also caught every sentence that happens to
  sit inside one. "LET PORTAL VISITORS SWITCH BETWEEN LIGHT AND DARK" is a
  switch's label, not a field title, and shouting it made a settings panel read
  as a warning.

  So the three things that are prose opt back out: the muted note, a field
  hint, and any checkbox or radio label.
*/
.main .sm-field-group .muted,
.main .sm-field-group .field-hint,
.main .sm-field-group .form-check-label {
  font-weight: 400; letter-spacing: 0; text-transform: none;
}
.main .sm-kv-table td:first-child { width: 58%; color: var(--sm-text-muted); }
/* Wrapping, deliberately, rather than nowrap. A figure has no break
   opportunity in it, so "1,204" cannot wrap whatever this says; what CAN wrap is
   a value that is words — a country, a timezone, a list of languages — and those
   should. nowrap here would have pushed exactly those past the right edge of a
   table that no longer has a scroller to catch them. */
.main .sm-kv-table td:last-child {
  text-align: right; white-space: normal; overflow-wrap: anywhere;
  font-variant-numeric: tabular-nums;
}


/* The verified mark on a member's demographics: the member typed this field,
   as opposed to the intro scan having parsed it. An icon rather than a bare
   check character so it sits in the same family as every other glyph in the
   pane, and in the success colour because it is a positive assertion about
   the data rather than a warning about it. Sized down and nudged off the
   value so it reads as an annotation, not as part of the text. */
.main .sm-verified {
  color: var(--sm-success);
  font-size: 0.78em;
  margin-left: 0.3em;
  vertical-align: baseline;
}
/* The other end of the same idea: this value is a model's guess. Same size
   and offset as the verified mark so the two read as one annotation slot,
   and in the muted text colour rather than a semantic one — an inferred
   location is not a warning, it is a value to weigh. Never both on one
   field: stated and inferred are mutually exclusive by construction. */
.main .sm-inferred {
  color: var(--sm-text-dim);
  font-size: 0.78em;
  margin-left: 0.3em;
  vertical-align: baseline;
}
/* Apply row-hover to every dark table on the dashboard, even ones that didn't
   pick up Bootstrap's `table-hover` class explicitly. Keeps the hover-tinted
   row pattern (e.g. on the archive page) consistent across all pages. */
.main .table-hover tbody tr:hover,
.main .table-dark tbody tr:hover { background: var(--sm-bg-hover); }
.main .table-dark tbody tr:hover td { background: var(--sm-bg-hover); }
.main .table-striped tbody tr:nth-of-type(odd) { background: var(--sm-wash-1); }
.main .table-sm th, .main .table-sm td { padding: 0.3rem 0.5rem; }
.main .text-secondary { color: var(--sm-text-muted) !important; }

.main a.member-row { color: inherit; text-decoration: none; }
.main a.member-row:hover, .main a.member-info:hover { color: inherit; text-decoration: none; }
.main tr.member-row-clickable:hover a.member-row, .main tr.member-row-clickable:hover a.member-info { color: inherit; text-decoration: none; }
.main tr.member-row-clickable, .main tr.archive-row-clickable, .main tr.demographics-group-row { cursor: pointer; }
.member-list-table .member-cell { padding: 0.5rem 0.75rem; }
.main .member-info { display: flex; align-items: center; gap: 0.75rem; color: inherit; text-decoration: none; }
.main .member-info:hover { color: inherit; }
.main .member-avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.main .member-name-block { display: flex; flex-direction: column; gap: 0; line-height: 1.35; min-width: 0; }
.main .member-display-line { display: flex; align-items: center; gap: 0.35rem; flex-wrap: nowrap; }
.main .member-display-name { font-weight: 500; color: var(--sm-text); font-size: 0.95rem; }
.main .member-username { font-size: 0.8rem; color: var(--sm-text-muted); margin-top: 0.1rem; }
.main .bot-tag { align-items: center; display: inline-flex; flex-shrink: 0; font-size: 0.625rem; text-indent: 0; text-transform: uppercase; vertical-align: middle; border-radius: 0; height: 16px; padding: 0 4px; background: var(--sm-accent); color: var(--sm-accent-fg); font-weight: 600; line-height: 1; }
.main .bot-tag .bot-tag-text { line-height: 16px; }
.member-list-table .member-id { color: var(--sm-text-muted); font-size: 0.85rem; font-family: var(--sm-font-mono); }
.member-list-table .member-joined,
.member-list-table .member-discord-joined { color: var(--sm-text-muted); font-size: 0.9rem; }
/* Same reasoning as the Cases table below: four columns is one or two too many
   on a phone. "Joined Discord" goes first (rarely what you came for), then the
   account ID, which is a 18-digit mono string that alone eats half the row.
   Every row clicks through to the member profile, where both are shown.
   Header and body cells carry different class names, so both are listed. */
@media (max-width: 768px) {
  .main .member-list-table .member-col-discord,
  .main .member-list-table .member-discord-joined { display: none; }
}
@media (max-width: 480px) {
  .main .member-list-table .member-col-id,
  .main .member-list-table .member-id { display: none; }
}

/* Shared search toolbar for the Members and Cases lists.
   Sizing lives here rather than in inline styles so the mobile rules further
   down can override it — inline `flex`/`width` on the controls used to win over
   the media query and the row stayed squeezed. */
/* Filter toolbars hold their size.
   These used to be `flex: 1 1 auto`, so the controls grew into whatever space
   the rest of the row was not using — and the rest of the row includes the
   pagination, which disappears the moment a filter narrows the results to a
   single page. Choosing a value from a dropdown therefore resized the dropdown
   you had just used, and moved everything beside it. The search box now has a
   fixed basis and the trailing cluster takes the slack instead. */
.main .members-search-form { flex: 0 1 auto; min-width: 0; flex-wrap: nowrap; }
.main .members-search-input { width: 22rem; max-width: 100%; flex: 0 1 22rem; }
.main .members-search-form .members-bots-filter { width: auto; flex-shrink: 0; }
.main .members-search-form .btn { flex-shrink: 0; }
/* Let the search box take the slack instead of leaving it at the end of the row.

   The 22rem basis above is right for Members, which has one filter beside it.
   Cases has four, and ModMail's log has two — so on a wide screen the controls
   ended in the middle of the panel with a stretch of empty card to their right,
   which reads as a toolbar that failed to load rather than as one that fits.
   Opt-in, so Members keeps the fixed box it was tuned for, and only above the
   breakpoint: the mobile rules further down already let it shrink. */
@media (min-width: 769px) {
  .main .members-search-form.sm-search-grow { flex: 1 1 auto; }
  .main .members-search-form.sm-search-grow .members-search-input { flex: 1 1 22rem; width: auto; }
  .main .sm-field-lg.sm-search-grow { max-width: none; width: 100%; }
}
/* The Search button and the pager are one flex child of the form.
   They used to be siblings — the form, then a trailing div holding the pager —
   and on a phone that meant the pager dropped to a row of its own beneath a
   form which had already wrapped into three. Nested, they share a row at every
   width. Members and Cases both use this. The form grows here so the slack
   lands inside .members-search-actions
   and margin-left:auto can hold the pager hard right; the controls themselves
   keep the fixed bases set above, so a pager that comes and goes with the
   filter still does not resize the dropdown you just used. */
.main .members-toolbar .members-search-form { flex: 1 1 auto; }
.main .members-search-actions { display: flex; align-items: center; gap: 0.5rem; flex: 1 1 auto; min-width: 0; }
.main .members-search-actions .members-pagination { margin-left: auto; }
.main .member-roles-wrap { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }

.main tr.case-row-clickable { cursor: pointer; white-space: nowrap; }
.main .cases-table .case-col-type { padding-left: 2ch; padding-right: 2ch; }
.main .cases-table .case-col-created { white-space: nowrap; }

/* Six columns (ID, Type, User, Moderator, Reason, Timestamp) do not fit a phone.
   Held at six they either overflow into a horizontal scroll that hides the
   Reason — the column people actually scan — or crush every cell to two or three
   characters.
   Moderator and Timestamp are dropped first: they are the least useful when
   skimming, and every row is clickable through to the case detail where the full
   record is shown. Narrower still, the Type label goes but its coloured icon
   stays, which is what distinguishes a ban from a note at a glance. */
@media (max-width: 768px) {
  .main .cases-table .case-col-author,
  .main .cases-table .case-col-created { display: none; }
  .main .cases-table .case-col-type { padding-left: 0.5ch; padding-right: 0.5ch; }
  .main .cases-table .case-col-reason { white-space: normal; }
}
@media (max-width: 480px) {
  .main .cases-table .case-col-type .small { display: none; }
}
/* No colour of its own — the type's colour is set inline by caseIcon() so the
   glyph carries the same red/orange/yellow/grey/green as everywhere else. */
.main .case-type-icon { font-size: 1.1rem; }

/* Case detail: labels in a column exactly as wide as the longest of them, with
   every value starting immediately to their right.
   It was a 12-column grid, so a two-character label like "ID" reserved a
   quarter of the card and the values sat marooned a long way from what they
   were labelling. max-content closes that gap and still keeps the values on one
   aligned edge, which is what makes the block scannable. */
.main .case-detail-dl {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr);
  column-gap: 1.25rem;
  row-gap: 0.55rem;
  align-items: baseline;
}
.main .case-detail-dl > dt {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0;
  font-weight: 500;
  color: var(--sm-text-muted);
  white-space: nowrap;
}
.main .case-detail-dl > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.main .case-detail-dl > dd .cases-avatar { vertical-align: middle; }

@media (max-width: 575.98px) {
  /* Below this the label column and a long username cannot share a line, so the
     pairs stack — value tight under its own label, spacing moved to between
     fields rather than inside them. */
  .main .case-detail-dl { grid-template-columns: minmax(0, 1fr); row-gap: 0; }
  .main .case-detail-dl > dt { margin-bottom: 0.15rem; }
  .main .case-detail-dl > dd { margin-bottom: 0.9rem; }
  .main .case-detail-dl > dd:last-child { margin-bottom: 0; }
}

.main a { color: var(--sm-text-muted); text-decoration: none; }
.main a:hover, .main a:active { color: var(--sm-accent-cyan); }
.main a.case-user-link { color: var(--sm-text-muted); text-decoration: none; }
.main a.case-user-link:hover, .main a.case-user-link:active { color: inherit; text-decoration: none; }
.main tr.case-row-clickable:hover a.case-user-link { color: inherit; text-decoration: none; }

/* margin-top: 0 belongs here, not on the pages. The heading-size reset lives on
   the `.h1`–`.h6` *classes*, so a panel written as `<h4 class="card-heading">`
   — no `.h6` alongside it — kept the browser's default 1.33em top margin and
   opened with a blank row above its own title. Every panel gets the same
   rhythm now whether or not it spells the size class out. */
.main .card-heading { display: flex; align-items: center; gap: 0.5rem; margin-top: 0; margin-bottom: 0.5rem; flex-wrap: nowrap; font-size: 0.9rem; font-weight: 600; letter-spacing: 0.01em; color: var(--sm-text-muted); text-transform: none; min-width: 0; }
/* A heading row must be able to lose its own text before it widens the card.
   `flex-wrap: nowrap` is right — a two-line panel title reads as a mistake —
   but with nothing allowed to shrink, a heading carrying a labelled button had
   no way to fit on a phone and pushed the whole card past the viewport. That
   was one of the two causes of Levels scrolling sideways, and it is a property
   of the rule rather than of that page: any heading given a long title and a
   control would have done the same.
   The title is what gives way, because the control is the part you cannot use
   if it is clipped. */
.main .card-heading > span:not(.badge):not([class*="sm-status"]) { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main .card-heading .card-heading-icon { width: 1.1rem; flex-shrink: 0; text-align: center; color: var(--sm-text-muted); opacity: 0.85; }
/* A button inside a card heading must not change the heading's height.
   .sm-icon-btn is taller than the heading's own line box, so a panel carrying
   one (Intro post) sat noticeably lower than every panel that does not, and the
   headings stopped lining up across a row of cards. The negative block margin
   lets the button overflow its line instead of stretching it. */
.main .card-heading .card-heading-action { margin-block: -0.3rem; line-height: 1; }

/* Panel description — the one sanctioned way to explain what a panel does.
   Sits directly under .card-heading, then a clear gap before the controls, so
   every panel opens with the same heading → blurb → content rhythm. Capped at
   a readable measure; a description that overflows it wants to be shorter, not
   wider. See docs/UI_REFERENCE.md. */
.main .card-desc { margin: 0 0 1rem; max-width: 78ch; color: var(--sm-text-muted); font-size: 0.8125rem; line-height: 1.5; }
.main .card-heading + .card-desc { margin-top: -0.15rem; }
.main .card-desc:last-child { margin-bottom: 0; }
/* Helper text bound to a single field, not to the panel. */
.main .field-hint { display: block; margin: 0.3rem 0 0; color: var(--sm-text-dim); font-size: 0.78rem; line-height: 1.45; }

/*
  THE BARE HELP GLYPH, for a `?` that annotates text rather than sitting in a
  toolbar. See helpButton({ inline: true }) in dashboard/pages/helpModal.js.

  Everything here is about disappearing into the line it follows: the font size
  is inherited, the colour is the dim one a hint uses, and there is no box. The
  0.35em of left margin is the space after a word rather than button padding,
  so "Pre-screening ?" reads as one phrase.
*/
.main .sm-help-inline {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; margin-left: 0.35em; border: 0; background: none;
  font-size: inherit; line-height: 1; color: var(--sm-text-dim);
  cursor: pointer; vertical-align: baseline;
}
.main .sm-help-inline:hover { color: var(--sm-text); }
.main .sm-help-inline:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }
/* INSIDE A PANEL OR CARD HEADING, every help control is the plain glyph — the
   bordered square is for a page header, where it sits among real buttons.
   Whatever classes the call site passed, so a panel added later cannot come
   out boxed by mistake. */
.main .sm-panel-actions .sm-help-open, .main .sm-card-head .sm-help-open, .main .sm-card-head > div > .sm-help-open {
  display: inline-flex; align-items: center; justify-content: center;
  padding: 0; margin: 0 0 0 0.35em; border: 0; background: none; box-shadow: none; border-radius: 0;
  min-width: 0; min-height: 0; width: auto; height: auto;
  font-size: 1.05rem; line-height: 1; color: var(--sm-text-dim);
}
.main .sm-panel-actions .sm-help-open i, .main .sm-card-head .sm-help-open i { font-weight: 400; }
.main .sm-panel-actions .sm-help-open:hover, .main .sm-card-head .sm-help-open:hover { color: var(--sm-text); background: none; }
/* Existing pages write the description a dozen different ways; normalise the
   spacing centrally so they inherit the rhythm without 80-odd markup edits. */
.main .card > p.muted:not(:last-child),
.main .card > p.text-muted:not(:last-child) { margin-top: 0; margin-bottom: 1rem; max-width: 78ch; }

/* Nested panel: a sub-section inside a card drops to the deep surface so the
   hierarchy reads without a second border weight. */
.main .card .card { background: var(--sm-bg-deep); padding: 0.9rem; margin-bottom: 0.75rem; }
.main .card .card:last-child { margin-bottom: 0; }

/* Icon-only control (export, row actions). Square, so it can't inherit the
   5.5rem min-width every other button carries for cluster alignment. */
/* Square, and it has to WIN. `.btn-sm` / `.btn-outline-secondary` also set
   min-width: 5.5rem and appear later in this file at equal specificity, so the
   plain `.main .sm-icon-btn` selector lost and the icon button rendered as a
   5.5rem-wide rectangle with a lonely icon floating in the middle. The
   `.btn.sm-icon-btn` form outranks them wherever they sit. */
.main .sm-icon-btn,
.main .btn.sm-icon-btn { min-width: 2.25rem; width: 2.25rem; padding: 0.35rem; flex-shrink: 0; }
.main .sm-icon-btn i,
.main .btn.sm-icon-btn i { margin: 0; }

/* Capped, internally-scrolling panel. `.main` is the only page scroller, so any
   unbounded list must bound itself here instead of growing the page.
   Three sanctioned heights — see docs/UI_REFERENCE.md §8. */
.main .sm-scroll { overflow-y: auto; overscroll-behavior: contain; }
.main .sm-scroll-sm { max-height: 320px; }
.main .sm-scroll-md { max-height: 380px; }
.main .sm-scroll-lg { max-height: 440px; }
/* Two panels sitting side by side in one row need the same list height, or the
   cards finish at different points and the row looks broken. Used in place of
   sm-scroll-md/-lg, which differ by 60px and were doing exactly that on the
   subscriptions page. flex:1 lets the list absorb whatever space is left after
   each card's own heading and controls, so the CARDS match even when the
   preamble above the list does not. */
.main .sm-panel-list { max-height: 440px; flex: 1 1 auto; min-height: 0; }
.main .card:has(> .sm-panel-list) { display: flex; flex-direction: column; }
/* Sticky header so column names survive scrolling in a capped table. */
.main .sm-scroll table.table thead th { position: sticky; top: 0; z-index: 1; background: var(--sm-bg-raised); }

/* Dense timestamped feed row (subscriptions, activity logs). */
.main .sm-feed-item { display: flex; align-items: baseline; gap: 0.6rem; padding: 0.35rem 0; border-bottom: 1px solid var(--sm-border); font-size: 0.875rem; }
.main .sm-feed-item:last-child { border-bottom: 0; }
.main .sm-feed-time { font-family: var(--sm-font-mono); font-size: 0.78rem; color: var(--sm-text-dim); flex-shrink: 0; }
.main .sm-feed-text { min-width: 0; }
/* Day separator inside a feed. */
.main .sm-feed-day { position: sticky; top: 0; background: var(--sm-bg-raised); font-family: var(--sm-font-mono); font-size: 0.68rem; letter-spacing: 0.12em; text-transform: uppercase; color: var(--sm-text-dim); padding: 0.5rem 0 0.25rem; border-bottom: 1px solid var(--sm-border-strong); z-index: 1; }
.main .sm-feed-day:first-child { padding-top: 0; }
/* Inline icon before a label or button word.
   width was a hard 0.9rem, which is narrower than several glyphs actually render
   (fa-terminal, fa-sliders) — the overflow ran straight into the following text
   with no gap. min-width reserves the column for alignment while letting a wider
   glyph take the room it needs, and the margin guarantees a real gap either way. */
.main .label-icon {
  display: inline-block;
  min-width: 0.9rem;
  margin-right: 0.45rem;
  text-align: center;
  opacity: 0.85;
  vertical-align: middle;
}
.main .form-label { display: block; margin-bottom: 0.35rem; color: var(--sm-text-muted); font-size: 0.85rem; font-weight: 500; }

/* Read-only value block: a setting shown where an input used to be, because it
   is owned somewhere else. Deliberately not a disabled <textarea> — that reads
   as "you may edit this later", where this is "this lives on another page".
   pre-wrap so a multi-line template keeps its line breaks without a <pre>. */
.main .sm-readonly-block { white-space: pre-wrap; font-family: var(--sm-font-mono); color: var(--sm-text); background: var(--sm-bg-deep); border: 1px solid var(--sm-border); border-radius: 0; padding: 0.6rem 0.75rem; line-height: 1.5; }

.main .logs-badge { display: inline-flex; align-items: center; font-size: 0.75rem; font-weight: 500; color: var(--sm-text); background: var(--sm-pill-bg); border-radius: 0; padding: 0.2rem 0.45rem; line-height: 1.25; }

/* ============== Pills / chips / role tags ============== */
.main .immune-badge-pill,
.main .ai-badge-pill,
.main .perm-role-tag:not(.perm-role-tag-readonly),
.main .settings-pill,
.main .stats-exclusion-tag {
  display: inline-flex !important;
  align-items: center;
  gap: 0.35rem;
  font-size: 0.75rem !important;
  font-weight: 500;
  line-height: 1.25;
  color: var(--sm-text) !important;
  background: var(--sm-pill-bg) !important;
  border-radius: 0 !important;
  padding: 0.2rem 0.45rem 0.2rem 0.5rem !important;
  margin: 0.15rem 0.5rem 0.15rem 0 !important;
  vertical-align: middle;
  box-sizing: border-box;
}

.main .ai-watchers-table .ai-watcher-name { font-size: 0.72rem; font-weight: 600; color: var(--sm-text-muted); letter-spacing: 0.02em; vertical-align: middle; }

/* The master switch sits on the same line as the alert channel it governs —
   one row that reads "alerts go here, and they're on/off". */
.main .ai-watchers-controls { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 1rem; }
.main .ai-watchers-field { display: flex; flex-direction: column; gap: 0.35rem; }
.main .ai-watchers-label { margin: 0; font-size: 0.72rem; font-weight: 600; color: var(--sm-text-muted); letter-spacing: 0.02em; text-transform: uppercase; }
.main .ai-watchers-switch { display: inline-flex; align-items: center; gap: 0.55rem; margin: 0 0 0.15rem; cursor: pointer; user-select: none; }
.main .ai-watchers-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
.main .ai-watchers-switch-track {
  position: relative;
  width: 2.25rem;
  height: 1.25rem;
  flex-shrink: 0;
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border);
  border-radius: 0;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.main .ai-watchers-switch-thumb {
  position: absolute;
  top: 50%;
  left: 0.175rem;
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 0;
  background: var(--sm-text-muted);
  transform: translateY(-50%);
  transition: transform 0.15s ease, background 0.15s ease;
}
.main .ai-watchers-switch input:checked + .ai-watchers-switch-track { background: var(--sm-accent); border-color: var(--sm-accent); }
.main .ai-watchers-switch input:checked + .ai-watchers-switch-track .ai-watchers-switch-thumb { transform: translate(1rem, -50%); background: var(--sm-accent-fg); }
.main .ai-watchers-switch input:focus-visible + .ai-watchers-switch-track { outline: 2px solid var(--sm-accent); outline-offset: 2px; }
.main .ai-watchers-switch input:disabled + .ai-watchers-switch-track { opacity: 0.5; cursor: not-allowed; }
.main .ai-watchers-switch-label { font-size: 0.85rem; font-weight: 500; }

.main .member-role-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  font-size: 0.78rem;
  font-weight: 400;
  line-height: 1.3;
  border-radius: 0;
  padding: 0.28rem 0.55rem;
  vertical-align: middle;
  box-sizing: border-box;
  max-width: 100%;
}

.main .settings-pill.text-warning { color: var(--sm-warning-text) !important; background: var(--sm-warning-soft) !important; }
.main .settings-pill .settings-pill-id { font-size: 0.7rem !important; color: var(--sm-text) !important; background: transparent !important; border: none !important; padding: 0 0.1rem !important; }
.main .perm-role-tag-readonly { background: var(--sm-wash-2) !important; color: var(--sm-text) !important; }
.main .perm-role-tag-readonly .perm-role-tag-label { color: var(--sm-text) !important; }
.main .perm-role-tag-label,
.main .stats-exclusion-tag-label { color: var(--sm-text) !important; }
.main .immune-badge-pill form,
.main .ai-badge-pill form,
.main .stats-exclusion-tag form { display: inline; margin: 0; padding: 0; }
.main .perm-role-tag form.perm-role-tag-remove { display: inline-flex !important; margin: 0 !important; padding: 0 !important; align-items: center; }
.main .immune-badge-pill button.badge-pill-remove,
.main .ai-badge-pill button.ai-badge-remove,
.main .stats-exclusion-tag button.stats-exclusion-tag-x,
.main .perm-role-tag button.perm-role-tag-x,
.main .settings-pill button.settings-pill-remove {
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  width: 1.05em !important;
  height: 1.05em !important;
  min-width: 1.05em !important;
  min-height: 1.05em !important;
  margin: 0 0 0 0.1rem !important;
  padding: 0 !important;
  background: transparent !important;
  border: none !important;
  color: var(--sm-text) !important;
  cursor: pointer;
  font-size: 0.95rem !important;
  line-height: 1;
  font-weight: 600;
  border-radius: 0;
  box-shadow: none !important;
}
.main .immune-badge-pill button.badge-pill-remove:hover,
.main .ai-badge-pill button.ai-badge-remove:hover,
.main .stats-exclusion-tag button.stats-exclusion-tag-x:hover,
.main .perm-role-tag button.perm-role-tag-x:hover,
.main .settings-pill button.settings-pill-remove:hover { color: var(--sm-text) !important; background: var(--sm-wash-3) !important; }

.main .badge { display: inline-flex; align-items: center; padding: 0.25em 0.55em; font-size: 0.75rem; font-weight: 500; line-height: 1.25; border-radius: 0; background: var(--sm-pill-bg); color: var(--sm-text); }
.main .badge.dash-tag-flat { border-radius: 0 !important; font-size: 0.75rem; font-weight: 500; padding: 0.25em 0.55em; }

/* Pills and badges are fill-only. A hairline around a 0.75rem chip inside a
   table cell reads as a second, competing table border; the fill alone is
   enough to separate the label from the cell. Fill is bumped a shade to carry
   the definition the border used to. */
.main .badge,
.main .logs-badge,
.main .immune-badge-pill,
.main .ai-badge-pill,
.main .perm-role-tag,
.main .perm-role-tag-readonly,
.main .settings-pill,
.main .stats-exclusion-tag,
.main .automod-state-pill,
.main .ov-kind-tag,
.ov-kind-tag,
.ov-report-badge { border: none !important; }

/* ============== Forms ============== */
.main input[type="text"], .main input[type="url"], .main input[type="number"], .main input[type="search"], .main input[type="email"], .main input[type="password"], .main input[type="datetime-local"], .main input[type="date"], .main input[type="time"], .main select, .main textarea,
.main .form-control, .main .form-select { background: var(--sm-bg-deep); border: 1px solid var(--sm-border); color: var(--sm-text) !important; padding: 0.4rem 0.55rem; border-radius: 0; font-family: inherit; font-size: 0.9rem; width: 100%; max-width: 100%; box-sizing: border-box; min-height: 2.25rem; transition: border-color 0.12s ease; }
/* Inputs share the button's 2.25rem min-height so a label/input/button row
   lines up on one baseline instead of stair-stepping. */
/* A native select paints its arrow inside the padding box, so 0.55rem of
   right padding leaves the option text running underneath it — "All types" and
   "Bot Only" both collided with their own chevron. Reserve the arrow's column. */
.main select, .main .form-select { padding-right: 1.9rem; }
/* Same for the multi-select case, which has no arrow and so needs none. */
.main select[multiple], .main select[size]:not([size="1"]) { padding-right: 0.55rem; }

.main input:hover:not(:disabled), .main select:hover:not(:disabled), .main textarea:hover:not(:disabled),
.main .form-control:hover:not(:disabled), .main .form-select:hover:not(:disabled) { border-color: var(--sm-border-strong); }
.main input:disabled, .main select:disabled, .main textarea:disabled,
.main .form-control:disabled, .main .form-select:disabled { opacity: 0.55; cursor: not-allowed; }
.main textarea, .main textarea.form-control { min-height: 4.5rem; line-height: 1.5; padding-top: 0.45rem; }
/* Checkboxes/radios are sized by .form-check-input, not the text-input ramp. */
.main input[type="checkbox"], .main input[type="radio"] { min-height: 0; width: auto; }
/* A colour swatch input is a fixed chip, not a full-width field. */
.main input[type="color"] { width: 2.75rem; padding: 0.15rem; min-height: 2.25rem; cursor: pointer; }
.main input::placeholder, .main textarea::placeholder { color: var(--sm-text-faint); opacity: 1; }
.main input::-webkit-input-placeholder, .main textarea::-webkit-input-placeholder { color: var(--sm-text-faint); opacity: 1; }
.main input::-moz-placeholder, .main textarea::-moz-placeholder { color: var(--sm-text-faint); opacity: 1; }
.main input:-webkit-autofill, .main input:-webkit-autofill:hover, .main input:-webkit-autofill:focus { -webkit-text-fill-color: var(--sm-text); -webkit-box-shadow: 0 0 0 1000px var(--sm-bg-deep) inset; caret-color: var(--sm-text); }
.main input:focus, .main select:focus, .main textarea:focus, .main .form-control:focus, .main .form-select:focus { outline: 1px solid var(--sm-accent); outline-offset: -1px; }

.main .form-check { display: flex; align-items: center; gap: 0.5rem; padding-left: 0; }
.main .form-check-input { width: 1rem; height: 1rem; margin: 0; }
.main .form-check-label { color: var(--sm-text); font-size: 0.9rem; cursor: pointer; }
/* Squared, like everything else. Toggles were the last rounded exception in the
   stylesheet: the track was a 999px pill and the thumb a circle. The affordance
   survives without the rounding — it is the two-position travel and the accent
   fill that say "switch", not the pill shape. */
.main .form-switch .form-check-input { width: 2rem; height: 1rem; appearance: none; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); border-radius: 0; position: relative; cursor: pointer; }
.main .form-switch .form-check-input::after { content: ""; position: absolute; top: 1px; left: 1px; width: 0.8rem; height: 0.8rem; background: var(--sm-text-muted); border-radius: 0; transition: transform 0.15s ease, background 0.15s ease; }
.main .form-switch .form-check-input:checked { background: var(--sm-accent); border-color: var(--sm-accent); }
.main .form-switch .form-check-input:checked::after { transform: translateX(1rem); background: var(--sm-accent-fg); }

/* ============== Buttons ============== */
.main button, .main .btn, .main .btn-sm { background: var(--sm-btn-bg); color: var(--sm-btn-fg); border: none; padding: 0.4rem 0.75rem; border-radius: 0; cursor: pointer; font-size: 0.9rem; min-width: 5.5rem; min-height: 2.25rem; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; font-family: inherit; transition: background 0.12s ease, transform 0.06s ease; }
.main button:hover, .main .btn:hover, .main .btn-sm:hover { background: var(--sm-btn-bg-hover); }
.main button:active, .main .btn:active, .main .btn-sm:active { transform: translateY(1px); }
.main button:focus-visible, .main .btn:focus-visible { outline: 2px solid var(--sm-accent-cyan); outline-offset: 2px; }
.main button:disabled, .main .btn:disabled { opacity: 0.6; cursor: not-allowed; }
/* Anchors styled as buttons: kill the link colour and underline.
   `:not([class*="btn-outline"])` matters. This rule is (0,2,1) — the element
   selector gives it a tiebreaker over `.main .btn-outline-secondary` at (0,2,0)
   — so without the exclusion it repainted every outline button in
   `--sm-accent-fg` too. That token is derived from the guild's brand colour by
   luminance, so it is DARK whenever the accent is light. An outline button has
   a transparent background over the dark page, so a light brand colour turned
   every link-button's text and icon dark-on-dark and they disappeared: Prev,
   Next and the CSV export icon all vanished when the accent was set to white.
   Filled buttons are unaffected — there the accent IS the background, and a
   dark foreground on it is exactly right. */
.main a.btn:not([class*="btn-outline"]),
.main a.btn-sm:not([class*="btn-outline"]) { text-decoration: none; color: var(--sm-btn-fg); }
.main a.btn:hover:not([class*="btn-outline"]),
.main a.btn-sm:hover:not([class*="btn-outline"]) { text-decoration: none; color: var(--sm-btn-fg); }
.main a.btn[class*="btn-outline"],
.main a.btn-sm[class*="btn-outline"] { text-decoration: none; }
.main .btn-sm { padding: 0.35rem 0.65rem; font-size: 0.875rem; min-width: 5.5rem; min-height: 2.25rem; }
.main .btn-lg { padding: 0.6rem 1.1rem; font-size: 1rem; min-height: 2.75rem; }
/* Drops the 5.5rem floor so a button sitting against a select is only as wide
   as its own label. The floor exists so a row of standalone buttons reads as a
   row rather than as ragged chips; a Save welded to the picker it saves is the
   opposite case, where the extra width reads as a second, chunkier control. */
.main .btn.sm-btn-fit, .main .btn-sm.sm-btn-fit { min-width: 0; }
/* Filled in the brand colour, no border. Same tokens as the base button — the
   distinction from a secondary action is the fill itself. */
.main .btn-primary { background: var(--sm-btn-bg); color: var(--sm-btn-fg); border: none; }
.main .btn-primary:hover { background: var(--sm-btn-bg-hover); }
.main .btn-secondary { background: var(--sm-neutral); color: var(--sm-on-solid); }
.main .btn-secondary:hover { background: var(--sm-neutral-hover); }
/* The hover states are the semantic colour taken one step further from the
   page rather than a second hex: on slate that is brighter, on paper it is
   darker, and the hand-written hovers here only ever did the first. */
.main .btn-danger { background: var(--sm-danger); color: var(--sm-on-solid); }
.main .btn-danger:hover { background: color-mix(in srgb, var(--sm-danger) 82%, var(--sm-text)); }
.main .btn-success { background: var(--sm-success); color: var(--sm-on-solid); }
.main .btn-success:hover { background: color-mix(in srgb, var(--sm-success) 82%, var(--sm-text)); }
/* Amber is the one fill white type does not belong on — in dark mode. The
   token flips with the theme; see --sm-on-warning. */
.main .btn-warning { background: var(--sm-warning); color: var(--sm-on-warning); }
.main .btn-warning:hover { background: color-mix(in srgb, var(--sm-warning) 82%, #ffffff); }
.main .btn-info { background: var(--sm-info); color: var(--sm-on-solid); }
.main .btn-info:hover { background: color-mix(in srgb, var(--sm-info) 82%, var(--sm-text)); }
.main .btn-outline-primary { background: transparent; border: 1px solid var(--sm-accent); color: var(--sm-accent); border-radius: 0; }
.main .btn-outline-primary:hover { background: var(--sm-accent-ghost); color: var(--sm-text); }
/* --sm-text, not --sm-text-muted. These are buttons, not captions: the muted
   token is for supporting copy sitting beside something louder, and on a 36px
   icon-only button it left the glyph as a grey smudge with nothing next to it
   to be quiet relative to. Deliberately a fixed token rather than anything
   derived from the guild's brand colour, so this stays legible whatever accent
   is set. */
.main .btn-outline-secondary { min-width: 5.5rem; min-height: 2.25rem; background: transparent; border: 1px solid var(--sm-border-strong); color: var(--sm-text); border-radius: 0; }
.main .btn-outline-secondary:hover { background: var(--sm-bg-hover); color: var(--sm-text); border-color: var(--sm-border-strong); }
.main .btn-outline-danger { min-width: 5.5rem; min-height: 2.25rem; background: transparent; border: 1px solid var(--sm-danger); color: var(--sm-danger); border-radius: 0; }
.main .btn-outline-danger:hover { background: var(--sm-danger-soft); color: var(--sm-danger-text); border-color: var(--sm-danger); }
.main .btn-outline-success { background: transparent; border: 1px solid var(--sm-success); color: var(--sm-success); }
.main .btn-outline-success:hover { background: var(--sm-success-soft); color: var(--sm-success-text); }
.main .d-flex.gap-2 button, .main .d-flex.gap-2 .btn-sm, .main .d-flex.gap-2 .btn { min-height: 2.25rem; }

/* ============== Alerts ============== */
.main .alert { padding: 0.75rem 1rem; border-radius: 0; border: 1px solid var(--sm-border); margin-bottom: 1rem; color: var(--sm-text); }
.main .alert-primary { background: var(--sm-accent-ghost); border-color: var(--sm-accent); }
.main .alert-secondary { background: var(--sm-bg-raised); border-color: var(--sm-border-strong); }
/* Tint, line and type all derived from the one semantic token, so an alert
   reads as pale-on-slate or deep-on-paper without either being written out.
   The old pale pinks and mints were dark-mode text colours: on white they
   were a coloured smudge on a coloured ground. */
.main .alert-danger { background: var(--sm-danger-soft); border-color: var(--sm-danger); color: var(--sm-danger-text); }
.main .alert-success { background: var(--sm-success-soft); border-color: var(--sm-success); color: var(--sm-success-text); }
.main .alert-warning { background: var(--sm-warning-soft); border-color: var(--sm-warning); color: var(--sm-warning-text); }
.main .alert-info { background: var(--sm-info-soft); border-color: var(--sm-info); color: var(--sm-info-text); }

/* ============== AutoMod cards ============== */
.main .automod-rule-card { background: var(--sm-bg-raised); border: 1px solid var(--sm-border); border-radius: 0; padding: 1.25rem; margin-bottom: 1rem; position: relative; }
.main .automod-rule-card .automod-conditions-box { background: var(--sm-bg-deep); border-radius: 0; padding: 0.75rem 1rem; margin-bottom: 0.75rem; }
.main .automod-rule-card .automod-condition-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; padding: 0.35rem 0; border-bottom: 1px solid var(--sm-border); }
.main .automod-rule-card .automod-condition-row:last-child { border-bottom: none; }
.main .automod-rule-card .automod-condition-remove { color: var(--sm-danger); background: none; border: none; cursor: pointer; padding: 0.25rem; font-size: 1rem; line-height: 1; opacity: 0.8; min-width: 0; min-height: 0; }
.main .automod-rule-card .automod-condition-remove:hover { opacity: 1; background: none; }
/* At-a-glance on/off for a detector card, so a wall of eight cards reads
   without opening each one. Squared to match the rest of the dashboard. */
.main .automod-state-pill { flex-shrink: 0; font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; padding: 0.12rem 0.45rem; border-radius: 0; }
.main .automod-state-pill.is-on { color: var(--sm-success-text); background: var(--sm-success-soft); }
.main .automod-state-pill.is-off { color: var(--sm-text-dim); background: var(--sm-bg-deep); }
/* A third state, for a module that is running but deliberately not acting -
   Join Guard's test mode. It cannot borrow is-on: the whole point of the pill
   is that a reader who never opens the panel knows whether anything is being
   enforced, and green over a module doing nothing is the one misreading that
   leaves a server unguarded while somebody believes it is guarded. */
.main .automod-state-pill.is-warn { color: var(--sm-warning-text); background: var(--sm-warning-soft); }
.main .automod-detector-card select[multiple] { min-height: 4.5rem; }

/* ---- AutoMod: Immune channels panel ----
   Two columns on a desktop (log channels, then the picker), stacked below
   992px. Log rows are a name and ONE summary line, left-aligned, so a phone
   never wraps a right-aligned run of reasons into a column of text. The picker
   is a checklist (see dashboard/pages/channelPicker.js — shared by every
   multi-channel control on the dashboard): every row is
   at least 40px tall so it is a finger target, not a cursor target. */
.main .automod-immune-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem 1.5rem; }
@media (min-width: 992px) {
  .main .automod-immune-grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: start; }
}
.main .automod-immune-col { min-width: 0; }
.main .automod-immune-col > .sm-field-group:first-child { margin-top: 0; }

/* ---- Mode choice: one setting with a few named states, as a row of squared
   cards over real radios. The Join Guard mode (off / test / live) is the first;
   anything that was two switches where the second only counts once the first
   is on should be one of these instead. Stacks below 768px. ---- */
.main .sm-mode-choice { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0.5rem; }
@media (min-width: 768px) {
  .main .sm-mode-choice { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.main .sm-mode-opt {
  position: relative; display: flex; align-items: flex-start; gap: 0.6rem; margin: 0; padding: 0.65rem 0.8rem;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border); cursor: pointer; min-width: 0;
}
.main .sm-mode-opt:hover { border-color: var(--sm-border-strong); background: var(--sm-bg-hover); }
.main .sm-mode-opt input { flex-shrink: 0; margin: 0.15rem 0 0; }
.main .sm-mode-opt:has(input:checked) { border-color: var(--sm-accent); box-shadow: inset 0 0 0 1px var(--sm-accent); }
.main .sm-mode-opt:has(input:disabled) { cursor: default; opacity: 0.7; }
.main .sm-mode-opt:has(input:focus-visible) { outline: 1px solid var(--sm-accent); outline-offset: 1px; }
.main .sm-mode-opt-body { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }
.main .sm-mode-opt-title { display: inline-flex; align-items: center; gap: 0.45rem; font-size: 0.88rem; font-weight: 600; color: var(--sm-text); }
.main .sm-mode-opt-title > i { width: 1rem; text-align: center; color: var(--sm-text-dim); }
.main .sm-mode-opt:has(input:checked) .sm-mode-opt-title > i { color: var(--sm-accent); }
.main .sm-mode-opt-blurb { font-size: 0.78rem; line-height: 1.4; color: var(--sm-text-dim); }

/* ---- Join Guard settings: the mode on the left, the log channel on the
   right, Save under both. One column below 992px. ---- */
.main .jg-settings { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1.25rem 1.5rem; }
@media (min-width: 992px) {
  .main .jg-settings { grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); align-items: start; }
}
.main .jg-settings-col { min-width: 0; }
.main .jg-settings-col > .sm-field-group:first-child { margin-top: 0; }

.main .automod-log-list { list-style: none; margin: 0 0 0.5rem; padding: 0; border: 1px solid var(--sm-border); background: var(--sm-bg-deep); }
.main .automod-log-row {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.15rem 0.75rem;
  padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--sm-border); font-size: 0.86rem;
}
.main .automod-log-row:last-child { border-bottom: none; }
.main .automod-log-name { display: inline-flex; align-items: baseline; gap: 0.45rem; flex: 1 1 10rem; min-width: 0; color: var(--sm-text); overflow-wrap: anywhere; }
.main .automod-log-name > i { color: var(--sm-text-dim); width: 1rem; text-align: center; flex-shrink: 0; }
.main .automod-log-name.is-missing { color: var(--sm-text-dim); }
.main .automod-log-summary,
.main .automod-log-reasons { flex: 1 1 12rem; min-width: 0; color: var(--sm-text-muted); text-align: left; overflow-wrap: anywhere; }
.main .automod-log-reasons > summary { cursor: pointer; list-style: none; }
.main .automod-log-reasons > summary::-webkit-details-marker { display: none; }
.main .automod-log-reasons > summary::after { content: "\25BE"; margin-left: 0.35rem; color: var(--sm-text-dim); }
.main .automod-log-reasons[open] > summary::after { content: "\25B4"; }
.main .automod-log-reasons > summary:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 1px; }
.main .automod-log-reasons > ul { margin: 0.35rem 0 0; padding-left: 1.1rem; color: var(--sm-text-dim); font-size: 0.8rem; }

.main .sm-chanpick-chips { margin-bottom: 0.5rem; }
.main .sm-chanpick-chip span { color: var(--sm-text); background: var(--sm-pill-bg); border-color: var(--sm-pill-border); cursor: pointer; }
.main .sm-chanpick-chip span > .fa-xmark { color: var(--sm-text-dim); }
.main .sm-chanpick-chip:hover span { border-color: var(--sm-border-strong); }
.main .sm-chanpick-none { font-size: 0.85rem; color: var(--sm-text-dim); align-self: center; }
.main .sm-chanpick-filter { margin-bottom: 0.5rem; }
.main .sm-chanpick-list { border: 1px solid var(--sm-border); background: var(--sm-bg-deep); }
.main .sm-chanpick-group[hidden],
.main .sm-chanpick-check[hidden] { display: none; }
.main .sm-chanpick-cat {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0 0.75rem;
  padding: 0 0.75rem; background: var(--sm-bg-raised); border-bottom: 1px solid var(--sm-border);
  position: sticky; top: 0; z-index: 1;
}
.main .sm-chanpick-cat-name { display: inline-flex; align-items: center; gap: 0.45rem; min-width: 0; min-height: 2.5rem; font-size: 0.8rem; font-weight: 600; color: var(--sm-text-muted); overflow-wrap: anywhere; }
.main .sm-chanpick-cat-name > i { color: var(--sm-text-dim); }
.main .sm-chanpick-check {
  display: flex; align-items: center; gap: 0.6rem; min-height: 2.5rem; margin: 0;
  padding: 0 0.75rem; cursor: pointer; font-size: 0.86rem; color: var(--sm-text);
}
.main .sm-chanpick-channel { border-bottom: 1px solid var(--sm-border); }
.main .sm-chanpick-channel:hover { background: var(--sm-bg-hover); }
.main .sm-chanpick-whole { padding: 0; font-size: 0.8rem; color: var(--sm-text-muted); }
.main .sm-chanpick-check input[type="checkbox"] { width: 1.1rem; height: 1.1rem; margin: 0; flex-shrink: 0; accent-color: var(--sm-accent); cursor: pointer; }
.main .sm-chanpick-channel-name { min-width: 0; overflow-wrap: anywhere; }
.main .sm-chanpick-via { display: none; margin-left: auto; font-size: 0.75rem; color: var(--sm-text-dim); white-space: nowrap; }
.main .sm-chanpick-check input:disabled { cursor: not-allowed; }
.main .sm-chanpick-check input:disabled ~ .sm-chanpick-channel-name { color: var(--sm-text-muted); }
.main .sm-chanpick-check input:disabled ~ .sm-chanpick-via { display: inline; }
.main .sm-chanpick-nomatch { padding: 0.6rem 0.75rem; }
/* A short list, for a dialog or a card that holds several pickers. */
.main .sm-chanpick-sm .sm-chanpick-list { max-height: 13rem; }
.main .sm-chanpick-list { max-height: 22rem; overflow-y: auto; }
@media (pointer: coarse) {
  .main .sm-chanpick-check,
  .main .sm-chanpick-cat-name { min-height: 2.75rem; }
  .main .sm-chanpick-check input[type="checkbox"] { width: 1.35rem; height: 1.35rem; }
  .main .automod-log-reasons > summary { padding: 0.35rem 0; }
}
/* Panel heading row with a control on the right — used for a module's on/off
   switch. The switch belongs beside the panel's name because it governs the
   whole panel; buried among the fields it reads as one setting among many. */
.main .sm-card-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; margin-bottom: 0.5rem;
  /*
    WRAPS, because the thing opposite the heading is sometimes four tabs.

    Leaderboard puts XP / Messages / Voice / Listens plus a range switch in
    this row. With no wrap they had nowhere to go on a narrow screen and
    overlapped the word "Leaderboard" — two controls drawn on top of a
    title. Wrapping drops them to their own line, which is what the eye
    expects of a tab strip anyway.

    min-width: 0 so the heading beside them can still ellipsise rather than
    setting a floor for the row.
  */
  flex-wrap: wrap; min-width: 0;
}
/* The group opposite the heading wraps within itself too, and stays
   right-aligned when it does, so a wrapped second line does not start under
   the title. */
.main .sm-card-head > div {
  display: flex; flex-wrap: wrap; justify-content: flex-end; min-width: 0;
}
.main .sm-card-head-toggle { flex-shrink: 0; margin-bottom: 0; padding-left: 2.4rem; }
/* A help icon in a card head sits opposite a heading or an intro sentence, and
   that sentence is long enough on some panels to squeeze a shrinkable button
   into an ellipse. */
.main .sm-card-head > .sm-help-open,
.main .sm-card-head > div > .sm-help-open { flex-shrink: 0; }
.main .sm-card-head-toggle .form-check-label { color: var(--sm-text-muted); }

/* Active-hours heatmap (Stats → Status). A table, not a grid of divs: it is
   tabular data with row and column headers, so a screen reader gets something
   usable and the headers stay attached to the cells they label.
   `table-layout: fixed` is what keeps 24 equal columns — auto layout sizes them
   by content, and the content is empty, so they collapse unevenly. */
.main .ah-heat { border-collapse: separate; border-spacing: 2px; table-layout: fixed; width: 100%; min-width: 34rem; }
.main .ah-heat .ah-rowhead {
  width: 2.6rem; padding: 0 0.4rem 0 0; text-align: right;
  font-size: 0.72rem; font-weight: 600; color: var(--sm-text-muted); white-space: nowrap;
}
.main .ah-heat .ah-colhead {
  height: 1.1rem; padding: 0; font-family: var(--sm-font-mono);
  font-size: 0.62rem; font-weight: 400; color: var(--sm-text-dim); text-align: left;
}
/* A visible floor on empty cells, so the grid still reads as a grid at 3am
   rather than dissolving into the card background. */
.main .ah-heat .ah-cell {
  height: 1.35rem; padding: 0; border-radius: 0;
  background: var(--sm-wash-1);
}

/* Emoji and sticker rankings (Stats → Messages).
   A ranked-table cell holding a glyph and a name. The glyph leads at a size
   you can actually recognise an emoji at — 1.15rem for unicode, 24px for a
   custom image — with the name beside it in the body size, so a row scans as
   "picture, then what it is called" rather than as two equal columns.
   `vertical-align: middle` on the images because a bare inline <img> sits on
   the text baseline and hangs the row an extra few pixels deep. */
.main .sm-emoji-glyph { font-size: 1.15rem; line-height: 1; margin-right: 0.45rem; }
.main .sm-emoji-img {
  width: 24px; height: 24px; object-fit: contain;
  vertical-align: middle; margin-right: 0.45rem;
}
/* A sticker is a picture rather than a glyph, so it gets the room to be one —
   at 24px most stickers are an unidentifiable smudge. */
.main .sm-emoji-img-sticker { width: 32px; height: 32px; }
.main .sm-emoji-name { vertical-align: middle; }
/* The identifier beside the glyph, as a code chip.
   The label used to be the emoji's own key, which for a unicode emoji IS the
   glyph — so the row drew the same character twice and read as a rendering
   bug. It is now the codepoint sequence for a unicode emoji and `:name:` for a
   custom one, and a chip is what says "this is a value, not prose": it is read
   character by character, copied, and pasted into a command.
   No border, because a ranked table already has enough lines in it; the
   monospace face and the recessed ground carry it. `white-space: nowrap` so a
   codepoint sequence breaks the column rather than itself. */
.main .sm-emoji-id {
  font-family: var(--sm-font-mono); font-size: 0.76rem;
  color: var(--sm-text-muted); background: var(--sm-bg-deep);
  padding: 0.05rem 0.3rem; white-space: nowrap;
}
/* Stickers and reactions under a logged message (Server > Message Log).
   A second line under the text rather than a column of their own: most rows
   have neither, and two mostly-empty columns would cost every row width to
   serve a minority of them. The leading glyph says which of the two a line is,
   so a row with both does not read as one long list.
   `line-height` matches the small text above so a row with chips grows by a
   line rather than by a line and a half. */
.main .msglog-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; margin-top: 0.2rem; line-height: 1.35; }
.main .msglog-chips > i { font-size: 0.7rem; color: var(--sm-text-dim); margin-right: 0.1rem; }
.main .msglog-chip {
  font-family: var(--sm-font-mono); font-size: 0.68rem;
  color: var(--sm-text-muted); background: var(--sm-bg-deep);
  padding: 0.02rem 0.28rem; white-space: nowrap;
}

/* Selected range / All time, above the emoji figures.
   Sits at the top of its block rather than in the page header, because it
   scopes ONE panel and the header's date picker scopes the page — putting them
   together would read as two controls for the same thing. The margin is what
   keeps it from touching the figures strip below it. */
.main .sm-emoji-scope { margin-bottom: 0.75rem; }

/* Month-on-month change in the Usage pane's month table.
   Green and red for direction and nothing else — no arrow glyph, because the
   sign is already in the number and a second indicator of the same fact just
   makes the column wider. Deliberately NOT the semantic success/danger pair
   at full strength: a month that is 4% down is not an error, so both are the
   token at a reduced weight, which reads as annotation rather than as alarm. */
.main .sm-trend-up { color: var(--sm-success); }
.main .sm-trend-down { color: var(--sm-danger); }

/* ---- Date range picker ---------------------------------------------------
   One button in the header, a panel underneath holding the presets and both
   dates. It replaces a strip of five controls in two forms that ran ~34rem
   wide, which on the stats pages left Refresh and Export nowhere to sit.

   Built on <details> so it opens with no JavaScript; the panel is absolutely
   positioned so opening it does not reflow the header row. */
.main .sm-range { position: relative; display: inline-block; }
.main .sm-range > summary {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
}
.main .sm-range > summary::-webkit-details-marker { display: none; }
/* The caret is drawn rather than inherited from the marker, so it can sit after
   the label and flip when the panel opens. */
.main .sm-range > summary::after {
  content: "";
  margin-left: 0.4rem;
  border: 0.3rem solid transparent;
  border-top-color: currentColor;
  transform: translateY(0.15rem);
}
.main .sm-range[open] > summary::after { transform: translateY(-0.15rem) rotate(180deg); }
.main .sm-range-face { font-variant-numeric: tabular-nums; }
/* The phone-only alternative to .sm-range-face: "30d" where the full label
   would take the header cluster onto a second row. Hidden everywhere except
   the under-768px header rule that swaps the pair. */
.main .sm-range-face-short { display: none; font-variant-numeric: tabular-nums; }

.main .sm-range-panel {
  position: absolute;
  z-index: 30;
  top: calc(100% + 0.35rem);
  right: 0;
  min-width: 22rem;
  padding: 0.75rem;
  background: var(--sm-bg-raised);
  border: 1px solid var(--sm-border-strong);
  border-radius: 0;
  box-shadow: 0 0.75rem 1.75rem var(--sm-shadow-3);
  text-align: left;
}
/* Two columns of presets: six of them in one column made the panel taller than
   the header it hangs from, and the list is short enough to scan either way. */
.main .sm-range-presets {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.25rem;
  margin-bottom: 0.6rem;
}
.main .sm-range-preset {
  display: block;
  padding: 0.3rem 0.5rem;
  font-size: 0.82rem;
  border-radius: 0;
  color: var(--sm-text);
  text-decoration: none;
  border: 1px solid transparent;
}
.main .sm-range-preset:hover { background: var(--sm-wash-2); }
.main .sm-range-preset.is-active {
  background: var(--sm-wash-3);
  border-color: var(--sm-border-strong);
  font-weight: 600;
}
.main .sm-range-dates { display: flex; gap: 0.5rem; }
.main .sm-range-field { flex: 1 1 0; min-width: 0; }
.main .sm-range-field > span {
  display: block;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--sm-text-muted);
  margin-bottom: 0.15rem;
}
.main .sm-range-field input[type="date"] { width: 100%; }
.main .sm-range-actions { display: flex; justify-content: flex-end; margin-top: 0.6rem; }

/* Available to assistive tech, invisible on screen. Not `display: none` and not
   `.invisible` — both take the element out of the tab order, and the one use
   for this is a submit button that keeps a JS-auto-submitting form working for
   keyboard and no-JS users. */
.sm-sr-only,
.main .sm-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  /* min-* as well as width/height: the one caller is a <button>, and
     `.main button` sets a 5.5rem x 2.25rem minimum that would otherwise win on
     specificity and leave a clipped-but-full-size element in the corner. */
  min-width: 0; min-height: 0;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Ten rows, then scroll in place. The header stays put so the columns are still
   labelled halfway down a long history. */
.main .cases-scroll { max-height: 21rem; overflow-y: auto; }
.main .cases-scroll thead th {
  position: sticky; top: 0; z-index: 1;
  background: var(--sm-bg-raised);
}

/* The add form sits above the list it adds to, so it needs a closing edge —
   without one the form's last row and the table header run together and the
   button looks like it belongs to the list. */
.main .automod-escalation-add { padding-bottom: 1rem; border-bottom: 1px solid var(--sm-border); }

/* ============== Mobile menu ============== */
.menu-toggle { display: none; background: none; border: none; color: var(--sm-text); padding: 0.5rem; cursor: pointer; border-radius: 0; min-width: 0; min-height: 0; }
.menu-toggle:hover { background: var(--sm-bg-raised); }
.menu-toggle svg { width: 24px; height: 24px; }
.sidebar-backdrop { display: none; position: fixed; inset: 0; background: var(--sm-backdrop); z-index: 99; }

/* ============== Bootstrap-compatible utility classes ============== */

/* Display */
.d-none { display: none !important; }
.d-block { display: block !important; }
.d-inline { display: inline !important; }
.d-inline-block { display: inline-block !important; }
.d-inline-flex { display: inline-flex !important; }
.d-flex { display: flex !important; }
.d-grid { display: grid !important; }

/* Flex */
.flex-row { flex-direction: row !important; }
.flex-row-reverse { flex-direction: row-reverse !important; }
.flex-column { flex-direction: column !important; }
.flex-column-reverse { flex-direction: column-reverse !important; }
.flex-wrap { flex-wrap: wrap !important; }
.flex-nowrap { flex-wrap: nowrap !important; }
.flex-grow-0 { flex-grow: 0 !important; }
.flex-grow-1 { flex-grow: 1 !important; }
.flex-shrink-0 { flex-shrink: 0 !important; }
.flex-shrink-1 { flex-shrink: 1 !important; }
.flex-fill { flex: 1 1 auto !important; }

.justify-content-start { justify-content: flex-start !important; }
.justify-content-center { justify-content: center !important; }
.justify-content-end { justify-content: flex-end !important; }
.justify-content-between { justify-content: space-between !important; }
.justify-content-around { justify-content: space-around !important; }
.justify-content-evenly { justify-content: space-evenly !important; }

.align-items-start { align-items: flex-start !important; }
.align-items-center { align-items: center !important; }
.align-items-end { align-items: flex-end !important; }
.align-items-baseline { align-items: baseline !important; }
.align-items-stretch { align-items: stretch !important; }

.align-self-start { align-self: flex-start !important; }
.align-self-center { align-self: center !important; }
.align-self-end { align-self: flex-end !important; }

/* Spacing — m/p, all/x/y/t/b/s/e × 0..5 */
.m-0 { margin: 0 !important; } .m-1 { margin: 0.25rem !important; } .m-2 { margin: 0.5rem !important; } .m-3 { margin: 1rem !important; } .m-4 { margin: 1.5rem !important; } .m-5 { margin: 3rem !important; } .m-auto { margin: auto !important; }
.mt-0 { margin-top: 0 !important; } .mt-1 { margin-top: 0.25rem !important; } .mt-2 { margin-top: 0.5rem !important; } .mt-3 { margin-top: 1rem !important; } .mt-4 { margin-top: 1.5rem !important; } .mt-5 { margin-top: 3rem !important; } .mt-auto { margin-top: auto !important; }
.mb-0 { margin-bottom: 0 !important; } .mb-1 { margin-bottom: 0.25rem !important; } .mb-2 { margin-bottom: 0.5rem !important; } .mb-3 { margin-bottom: 1rem !important; } .mb-4 { margin-bottom: 1.5rem !important; } .mb-5 { margin-bottom: 3rem !important; } .mb-auto { margin-bottom: auto !important; }
.ms-0 { margin-left: 0 !important; } .ms-1 { margin-left: 0.25rem !important; } .ms-2 { margin-left: 0.5rem !important; } .ms-3 { margin-left: 1rem !important; } .ms-4 { margin-left: 1.5rem !important; } .ms-5 { margin-left: 3rem !important; } .ms-auto { margin-left: auto !important; }
.me-0 { margin-right: 0 !important; } .me-1 { margin-right: 0.25rem !important; } .me-2 { margin-right: 0.5rem !important; } .me-3 { margin-right: 1rem !important; } .me-4 { margin-right: 1.5rem !important; } .me-5 { margin-right: 3rem !important; } .me-auto { margin-right: auto !important; }
.mx-0 { margin-left: 0 !important; margin-right: 0 !important; } .mx-1 { margin-left: 0.25rem !important; margin-right: 0.25rem !important; } .mx-2 { margin-left: 0.5rem !important; margin-right: 0.5rem !important; } .mx-3 { margin-left: 1rem !important; margin-right: 1rem !important; } .mx-4 { margin-left: 1.5rem !important; margin-right: 1.5rem !important; } .mx-5 { margin-left: 3rem !important; margin-right: 3rem !important; } .mx-auto { margin-left: auto !important; margin-right: auto !important; }
.my-0 { margin-top: 0 !important; margin-bottom: 0 !important; } .my-1 { margin-top: 0.25rem !important; margin-bottom: 0.25rem !important; } .my-2 { margin-top: 0.5rem !important; margin-bottom: 0.5rem !important; } .my-3 { margin-top: 1rem !important; margin-bottom: 1rem !important; } .my-4 { margin-top: 1.5rem !important; margin-bottom: 1.5rem !important; } .my-5 { margin-top: 3rem !important; margin-bottom: 3rem !important; }

.p-0 { padding: 0 !important; } .p-1 { padding: 0.25rem !important; } .p-2 { padding: 0.5rem !important; } .p-3 { padding: 1rem !important; } .p-4 { padding: 1.5rem !important; } .p-5 { padding: 3rem !important; }
.pt-0 { padding-top: 0 !important; } .pt-1 { padding-top: 0.25rem !important; } .pt-2 { padding-top: 0.5rem !important; } .pt-3 { padding-top: 1rem !important; } .pt-4 { padding-top: 1.5rem !important; } .pt-5 { padding-top: 3rem !important; }
.pb-0 { padding-bottom: 0 !important; } .pb-1 { padding-bottom: 0.25rem !important; } .pb-2 { padding-bottom: 0.5rem !important; } .pb-3 { padding-bottom: 1rem !important; } .pb-4 { padding-bottom: 1.5rem !important; } .pb-5 { padding-bottom: 3rem !important; }
.ps-0 { padding-left: 0 !important; } .ps-1 { padding-left: 0.25rem !important; } .ps-2 { padding-left: 0.5rem !important; } .ps-3 { padding-left: 1rem !important; } .ps-4 { padding-left: 1.5rem !important; } .ps-5 { padding-left: 3rem !important; }
.pe-0 { padding-right: 0 !important; } .pe-1 { padding-right: 0.25rem !important; } .pe-2 { padding-right: 0.5rem !important; } .pe-3 { padding-right: 1rem !important; } .pe-4 { padding-right: 1.5rem !important; } .pe-5 { padding-right: 3rem !important; }
.px-0 { padding-left: 0 !important; padding-right: 0 !important; } .px-1 { padding-left: 0.25rem !important; padding-right: 0.25rem !important; } .px-2 { padding-left: 0.5rem !important; padding-right: 0.5rem !important; } .px-3 { padding-left: 1rem !important; padding-right: 1rem !important; } .px-4 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; } .px-5 { padding-left: 3rem !important; padding-right: 3rem !important; }
.py-0 { padding-top: 0 !important; padding-bottom: 0 !important; } .py-1 { padding-top: 0.25rem !important; padding-bottom: 0.25rem !important; } .py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } .py-3 { padding-top: 1rem !important; padding-bottom: 1rem !important; } .py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } .py-5 { padding-top: 3rem !important; padding-bottom: 3rem !important; }

/* Gap */
.gap-0 { gap: 0 !important; }
.gap-1 { gap: 0.25rem !important; }
.gap-2 { gap: 0.5rem !important; }
.gap-3 { gap: 1rem !important; }
.gap-4 { gap: 1.5rem !important; }
.gap-5 { gap: 3rem !important; }

/* Text */
.text-start { text-align: left !important; }
.text-center { text-align: center !important; }
.text-end { text-align: right !important; }
.text-nowrap { white-space: nowrap !important; }
.text-wrap { white-space: normal !important; }
.text-break { word-wrap: break-word !important; word-break: break-word !important; }
.text-truncate { overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; }
.text-uppercase { text-transform: uppercase !important; }
.text-lowercase { text-transform: lowercase !important; }
.text-capitalize { text-transform: capitalize !important; }
.text-decoration-none { text-decoration: none !important; }
.text-decoration-underline { text-decoration: underline !important; }
.fw-light { font-weight: 300 !important; }
.fw-normal { font-weight: 400 !important; }
.fw-medium { font-weight: 500 !important; }
.fw-semibold { font-weight: 600 !important; }
.fw-bold { font-weight: 700 !important; }
.fst-italic { font-style: italic !important; }
.fs-1 { font-size: 2rem !important; }
.fs-2 { font-size: 1.5rem !important; }
.fs-3 { font-size: 1.25rem !important; }
.fs-4 { font-size: 1.1rem !important; }
.fs-5 { font-size: 1rem !important; }
.fs-6 { font-size: 0.9rem !important; }
.small { font-size: 0.875rem; }
.lh-1 { line-height: 1 !important; }
.lh-sm { line-height: 1.25 !important; }
.lh-base { line-height: 1.5 !important; }
.lh-lg { line-height: 1.75 !important; }

.text-primary { color: var(--sm-accent) !important; }
.text-info { color: var(--sm-info) !important; }
.text-success { color: var(--sm-success) !important; }
.text-warning { color: var(--sm-warning) !important; }
.text-danger { color: var(--sm-danger) !important; }
.text-light { color: var(--sm-text) !important; }
.text-dark { color: #1e1f22 !important; }
.text-white { color: #fff !important; }
.text-body { color: var(--sm-text) !important; }
.text-reset { color: inherit !important; }

/* Background */
.bg-primary { background: var(--sm-accent) !important; color: var(--sm-accent-fg); }
.bg-secondary { background: var(--sm-neutral) !important; color: var(--sm-on-solid); }
.bg-success { background: var(--sm-success) !important; color: var(--sm-on-solid); }
.bg-danger { background: var(--sm-danger) !important; color: var(--sm-on-solid); }
.bg-warning { background: var(--sm-warning) !important; color: var(--sm-on-warning); }
.bg-info { background: var(--sm-info) !important; color: var(--sm-on-solid); }
.bg-light { background: var(--sm-bg-raised) !important; }
.bg-dark { background: var(--sm-bg-deep) !important; }
.bg-transparent { background: transparent !important; }

/* Borders */
.border { border: 1px solid var(--sm-border) !important; }
.border-0 { border: 0 !important; }
.border-top { border-top: 1px solid var(--sm-border) !important; }
.border-bottom { border-bottom: 1px solid var(--sm-border) !important; }
.border-start { border-left: 1px solid var(--sm-border) !important; }
.border-end { border-right: 1px solid var(--sm-border) !important; }
.border-top-0 { border-top: 0 !important; }
.border-bottom-0 { border-bottom: 0 !important; }
.border-primary { border-color: var(--sm-accent) !important; }
.border-danger { border-color: var(--sm-danger) !important; }
.border-success { border-color: var(--sm-success) !important; }

.rounded { border-radius: 0.25rem !important; }
.rounded-0 { border-radius: 0 !important; }
.rounded-1 { border-radius: 0.2rem !important; }
.rounded-2 { border-radius: 0.25rem !important; }
.rounded-3 { border-radius: 0.3rem !important; }
.rounded-circle { border-radius: 50% !important; }
.rounded-pill { border-radius: 999px !important; }

/* Sizing */
.w-25 { width: 25% !important; }
.w-50 { width: 50% !important; }
.w-75 { width: 75% !important; }
.w-100 { width: 100% !important; }
.w-auto { width: auto !important; }
.h-25 { height: 25% !important; }
.h-50 { height: 50% !important; }
.h-75 { height: 75% !important; }
.h-100 { height: 100% !important; }
.h-auto { height: auto !important; }
.mw-100 { max-width: 100% !important; }
.mh-100 { max-height: 100% !important; }
.vw-100 { width: 100vw !important; }
.vh-100 { height: 100vh !important; }
.min-vh-100 { min-height: 100vh !important; }

/* Position */
.position-static { position: static !important; }
.position-relative { position: relative !important; }
.position-absolute { position: absolute !important; }
.position-fixed { position: fixed !important; }
.position-sticky { position: sticky !important; }

/* Visibility */
.invisible { visibility: hidden !important; }
.visible { visibility: visible !important; }

/* Overflow */
.overflow-auto { overflow: auto !important; }
.overflow-hidden { overflow: hidden !important; }
.overflow-visible { overflow: visible !important; }
.overflow-scroll { overflow: scroll !important; }
.overflow-x-auto { overflow-x: auto !important; }
.overflow-y-auto { overflow-y: auto !important; }

/* Float */
.float-start { float: left !important; }
.float-end { float: right !important; }
.float-none { float: none !important; }

/* Shadows */
.shadow-none { box-shadow: none !important; }
.shadow-sm { box-shadow: 0 1px 2px var(--sm-shadow-1) !important; }
.shadow { box-shadow: 0 2px 6px var(--sm-shadow-2) !important; }
.shadow-lg { box-shadow: 0 6px 18px var(--sm-shadow-3) !important; }

/* Opacity */
.opacity-25 { opacity: 0.25 !important; }
.opacity-50 { opacity: 0.5 !important; }
.opacity-75 { opacity: 0.75 !important; }
.opacity-100 { opacity: 1 !important; }

/* ============== Grid (rows / columns) ============== */
.container { width: 100%; max-width: 1320px; margin: 0 auto; padding: 0 0.75rem; }
.container-fluid { width: 100%; padding: 0 0.75rem; }
/* A row's outdent and its columns' padding are ONE number, and they have to
   stay one number.

   This grid is hand-rolled rather than Bootstrap's, and the two halves had
   drifted apart: `.row` outdented a fixed -0.75rem while `.row.gN > *` set the
   padding independently, so only g-4 (0.75rem) actually cancelled. Every other
   gutter left a residue — g-3, which is what nearly the whole dashboard uses,
   padded 0.5rem against a 0.75rem outdent and so pushed its cards 0.25rem
   further out on each side than any card not in a row.

   The visible result was that a three-across row, a two-across row and a
   full-width card in the same column of the page did not share a left or right
   edge, by four pixels each side — small enough to read as "slightly wrong" and
   never as a specific bug, and impossible to fix at a call site because the
   call sites were all correct.

   One custom property drives both now, so a gutter class sets a single value
   and the arithmetic cannot come apart again. */
.row { --sm-gutter: 0.75rem; display: flex; flex-wrap: wrap; margin-right: calc(-1 * var(--sm-gutter)); margin-left: calc(-1 * var(--sm-gutter)); }
.row > * { flex: 1 0 0%; padding-right: var(--sm-gutter); padding-left: var(--sm-gutter); max-width: 100%; }
.col { flex: 1 0 0%; }
.col-auto { flex: 0 0 auto; width: auto; }
.col-1  { flex: 0 0 auto; width: 8.3333%; }
.col-2  { flex: 0 0 auto; width: 16.6667%; }
.col-3  { flex: 0 0 auto; width: 25%; }
.col-4  { flex: 0 0 auto; width: 33.3333%; }
.col-5  { flex: 0 0 auto; width: 41.6667%; }
.col-6  { flex: 0 0 auto; width: 50%; }
.col-7  { flex: 0 0 auto; width: 58.3333%; }
.col-8  { flex: 0 0 auto; width: 66.6667%; }
.col-9  { flex: 0 0 auto; width: 75%; }
.col-10 { flex: 0 0 auto; width: 83.3333%; }
.col-11 { flex: 0 0 auto; width: 91.6667%; }
.col-12 { flex: 0 0 auto; width: 100%; }
/* Gutter classes set the one number. The margin follows from it, so a row's
   cards always end up flush with the content around it whatever the gap
   between them. g-0's explicit margin reset is gone — it falls out now. */
.row.g-0 { --sm-gutter: 0; }
.row.g-1 { --sm-gutter: 0.125rem; }
.row.g-2 { --sm-gutter: 0.25rem; }
.row.g-3 { --sm-gutter: 0.5rem; }
.row.g-4 { --sm-gutter: 0.75rem; }
.row.g-5 { --sm-gutter: 1.5rem; }

/* Heading utilities */
.h1, .h2, .h3, .h4, .h5, .h6 { color: var(--sm-text); margin-top: 0; margin-bottom: 0.5rem; line-height: 1.2; }
.h1 { font-size: 2rem; } .h2 { font-size: 1.5rem; } .h3 { font-size: 1.25rem; } .h4 { font-size: 1.1rem; } .h5 { font-size: 1rem; } .h6 { font-size: 0.9rem; }

/* ============== Responsive breakpoints ============== */
/* Bootstrap-style sm/md/lg/xl mirrors. Only the most-used utilities are exposed responsively. */

/* ≥576px (sm) */
@media (min-width: 576px) {
  .d-sm-none { display: none !important; }
  .d-sm-block { display: block !important; }
  .d-sm-inline { display: inline !important; }
  .d-sm-inline-block { display: inline-block !important; }
  .d-sm-flex { display: flex !important; }
  .d-sm-inline-flex { display: inline-flex !important; }
  .text-sm-start { text-align: left !important; }
  .text-sm-center { text-align: center !important; }
  .text-sm-end { text-align: right !important; }
  .col-sm-1  { flex: 0 0 auto; width: 8.3333%; }
  .col-sm-2  { flex: 0 0 auto; width: 16.6667%; }
  .col-sm-3  { flex: 0 0 auto; width: 25%; }
  .col-sm-4  { flex: 0 0 auto; width: 33.3333%; }
  .col-sm-5  { flex: 0 0 auto; width: 41.6667%; }
  .col-sm-6  { flex: 0 0 auto; width: 50%; }
  .col-sm-7  { flex: 0 0 auto; width: 58.3333%; }
  .col-sm-8  { flex: 0 0 auto; width: 66.6667%; }
  .col-sm-9  { flex: 0 0 auto; width: 75%; }
  .col-sm-10 { flex: 0 0 auto; width: 83.3333%; }
  .col-sm-11 { flex: 0 0 auto; width: 91.6667%; }
  .col-sm-12 { flex: 0 0 auto; width: 100%; }
}

/* ≥768px (md) */
@media (min-width: 768px) {
  .d-md-none { display: none !important; }
  .d-md-block { display: block !important; }
  .d-md-inline { display: inline !important; }
  .d-md-inline-block { display: inline-block !important; }
  .d-md-flex { display: flex !important; }
  .d-md-inline-flex { display: inline-flex !important; }
  .text-md-start { text-align: left !important; }
  .text-md-center { text-align: center !important; }
  .text-md-end { text-align: right !important; }
  .col-md-1  { flex: 0 0 auto; width: 8.3333%; }
  .col-md-2  { flex: 0 0 auto; width: 16.6667%; }
  .col-md-3  { flex: 0 0 auto; width: 25%; }
  .col-md-4  { flex: 0 0 auto; width: 33.3333%; }
  .col-md-5  { flex: 0 0 auto; width: 41.6667%; }
  .col-md-6  { flex: 0 0 auto; width: 50%; }
  .col-md-7  { flex: 0 0 auto; width: 58.3333%; }
  .col-md-8  { flex: 0 0 auto; width: 66.6667%; }
  .col-md-9  { flex: 0 0 auto; width: 75%; }
  .col-md-10 { flex: 0 0 auto; width: 83.3333%; }
  .col-md-11 { flex: 0 0 auto; width: 91.6667%; }
  .col-md-12 { flex: 0 0 auto; width: 100%; }
}

/* ≥992px (lg) */
@media (min-width: 992px) {
  .d-lg-none { display: none !important; }
  .d-lg-block { display: block !important; }
  .d-lg-inline { display: inline !important; }
  .d-lg-inline-block { display: inline-block !important; }
  .d-lg-flex { display: flex !important; }
  .d-lg-inline-flex { display: inline-flex !important; }
  .col-lg-1  { flex: 0 0 auto; width: 8.3333%; }
  .col-lg-2  { flex: 0 0 auto; width: 16.6667%; }
  .col-lg-3  { flex: 0 0 auto; width: 25%; }
  .col-lg-4  { flex: 0 0 auto; width: 33.3333%; }
  .col-lg-5  { flex: 0 0 auto; width: 41.6667%; }
  .col-lg-6  { flex: 0 0 auto; width: 50%; }
  .col-lg-7  { flex: 0 0 auto; width: 58.3333%; }
  .col-lg-8  { flex: 0 0 auto; width: 66.6667%; }
  .col-lg-9  { flex: 0 0 auto; width: 75%; }
  .col-lg-10 { flex: 0 0 auto; width: 83.3333%; }
  .col-lg-11 { flex: 0 0 auto; width: 91.6667%; }
  .col-lg-12 { flex: 0 0 auto; width: 100%; }
}

/* ≥1200px (xl) */
@media (min-width: 1200px) {
  .d-xl-none { display: none !important; }
  .d-xl-block { display: block !important; }
  .d-xl-flex { display: flex !important; }
}

/* ============== Mobile (<768px) — sidebar drawer + tighter UI ============== */
@media (max-width: 768px) {
  .menu-toggle { display: block; }
  .topbar { padding: 0.6rem 0.9rem; }
  /* .brand-text is not hidden here any more — the whole .brand is, up with the
     .topbar-server-home rule that replaces it. */
  .wrap { position: relative; }
  .sidebar {
    position: fixed; top: 0; left: 0; bottom: 0; z-index: 100;
    width: 280px; transform: translateX(-100%);
    transition: transform 0.2s ease;
    box-shadow: 4px 0 12px var(--sm-shadow-2);
  }
  body.sidebar-open .sidebar { transform: translateX(0); }
  body.sidebar-open .sidebar-backdrop { display: block; }
  /* As a drawer the nav has the width to itself and is driven by thumb rather
     than cursor. The desktop sizes — 0.9rem items, 0.8rem uppercase group
     headers — read as fine print at arm's length, so the whole nav steps up a
     notch and the rows grow into proper tap targets. */
  .nav-item { font-size: 1rem; padding: 0.7rem 1rem; }
  .nav-item-sub { font-size: 0.95rem; padding: 0.65rem 1rem 0.65rem 2rem; }
  .nav-group-toggle { font-size: 0.85rem; padding: 0.7rem 1rem; }
  .sidebar-section-title { font-size: 0.78rem; }
  /* Relative to whichever step layout.js picked, rather than a flat override —
     a flat 1.05rem would undo the shrink and put a long name back to wrapping
     over three lines on the narrowest screens. */
  .server-name { font-size: calc(var(--sm-name-size, 0.95rem) * 1.1); }
  /* The horizontal gutter matches the desktop 0.75rem rather than halving it.
     At 0.5rem a right-aligned header action, and every card edge under it, sat
     about six pixels off the glass — which reads as the layout having run out
     of room rather than as a margin. The vertical padding still tightens. */
  .main { padding: 1rem 0.75rem; min-width: 0; }
  .main h2 { font-size: 1.15rem; }
  .main .card { padding: 1rem; }
  .main .table-responsive { width: 100%; }
  /* The Members and Cases toolbars share these classes. Held on one nowrap row
     they squeeze a search box, one or two filter selects and a Search button
     into the viewport width — on Cases that is four controls, and the search
     input ends up narrower than its own placeholder.
     Wrapping gives search its own full-width row and lets the filters share the
     next, which is the layout those controls actually need on a phone. */
  /* The type filter belongs beside the search box, not under it. A 100% basis
     on the input forced a line break before the select had a chance to fit, so
     the three controls stacked into three rows on a phone. An 8rem basis lets
     the input take whatever is left after the select and the button. */
  .main .members-search-form { flex-wrap: wrap; row-gap: 0.5rem; }
  /* width:auto here, because the desktop rule pins a 22rem basis that must not
     survive into the wrapped phone layout. */
  .main .members-search-form .members-search-input { min-width: 0; width: auto; flex: 1 1 8rem; }
  .main .members-search-form .members-bots-filter { flex: 0 1 auto; width: auto; min-width: 0; }
  .main .members-search-form .btn { flex: 0 0 auto; }
  /* Pagination standing alone still drops below the toolbar. */
  .main .members-pagination { width: 100%; justify-content: flex-start; }
  /* The actions cluster takes the whole of its own wrapped row, which puts the
     pager to the right of the Search button instead of under it — the one row
     the toolbar was still spending on navigation alone. */
  .main .members-search-actions { flex: 1 0 100%; width: 100%; }
  .main .members-search-actions .members-pagination { width: auto; margin-left: auto; justify-content: flex-end; }
  /* One gutter for every row on a phone, whatever gN it carries — the
     difference between a 0.125rem and a 1.5rem gap is not worth spending
     narrow-screen width on. Set through the variable rather than by
     overriding margin and padding separately, so this cannot drift out of
     step the way the desktop rules had. */
  .main .row { --sm-gutter: 0.5rem; }
  /* Buttons full-width inside d-grid, normal inline elsewhere */
  .main .d-grid > .btn, .main .d-grid > button { width: 100%; }
  /* Forms: every form control goes full width on mobile.
     `!important` is load-bearing, not laziness. Around thirty controls across
     the pages carry an inline `style="width:170px"` or `min-width:220px`, and
     an inline declaration outranks any external stylesheet rule regardless of
     selector strength. Without this the rule below silently lost every time and
     those fields stayed at their desktop width on a phone. min-width has to be
     zeroed too — it beats width, so a 220px floor would still overflow. */
  .main input[type="text"], .main input[type="url"], .main input[type="number"],
  .main input[type="search"], .main input[type="email"], .main input[type="password"],
  .main input[type="datetime-local"], .main input[type="date"], .main input[type="time"],
  .main select, .main textarea, .main .form-control, .main .form-select {
    width: 100% !important;
    min-width: 0 !important;
    max-width: 100% !important;
  }
}

/* Panels go one-up on anything narrower than a laptop.
   Bootstrap's `col-md-*` only collapses below 768px, so a landscape phone or a
   small tablet (768–991px) still renders two or three panels side by side. The
   width each one gets there is not enough for the tables and label/value pairs
   they hold, which is what makes those pages read as squished. Below 992px a
   grid column takes the whole row.
   `col-auto` is exempt — it sizes to its content and is used for inline
   controls sitting next to a field, not for panels. Bootstrap 5 sets column
   width via `width`, so that is the property to override. */
@media (max-width: 991.98px) {
  /*
    A COLUMN THAT NAMED A NARROW-SCREEN WIDTH ALREADY DECIDED THIS.

    This forced EVERY column to 100% below 992px, which is right for the panels
    the rule was written for — a table or a label/value pair in a third of a
    tablet is the squashed look it exists to fix. It was wrong for the columns
    that had already answered the question: `col-6 col-md-4` says "half a row
    on a phone, a third on a laptop", and overriding the first half made the
    Bootstrap xs classes mean nothing below 992px. Two short numeric fields
    written to sit side by side stacked instead, on 61 columns across the
    dashboard.

    So the override now skips any column carrying an explicit xs width. Naming
    `col-1` to `col-12` rather than matching a pattern, because `col-md-4` also
    contains "col-" and a looser selector would exempt the panels this rule is
    for. Verbose and exact beats clever and wrong here.

    A column with only `col-md-*` and up is untouched and still goes full width,
    which is every panel the original comment describes.
  */
  .main .row > [class*="col-"]:not([class*="col-auto"]):not(.col-1):not(.col-2):not(.col-3):not(.col-4):not(.col-5):not(.col-6):not(.col-7):not(.col-8):not(.col-9):not(.col-10):not(.col-11):not(.col-12) { flex: 0 0 auto; width: 100%; }

  /* Flex rows wrap instead of compressing. Toolbars across the pages are built
     as `d-flex` with children carrying inline `flex:1;min-width:240px` — held
     on one line those children shrink past their floor and the row overflows.
     Allowed to wrap, each drops to the next line at a usable width instead.
     Bootstrap's own .flex-nowrap is `!important`, so anywhere a single line is
     genuinely required it still wins.

     ROWS ONLY. A `d-flex flex-column` (a settings form, a stacked panel) must
     never wrap: a wrapping flex container sizes each line to its widest item's
     content rather than to the container, so one long unbreakable line — the
     Levels "XP curve" summary — made every field, row and toggle in the form
     as wide as it and pushed them all past the card's right edge. A column has
     nothing to wrap onto anyway. */
  .main .d-flex:not(.flex-column) { flex-wrap: wrap; }
  .main .flex-column { flex-wrap: nowrap; }

  /* Fields keep their designed width here — a tablet has room for a 170px date
     picker — but they may not exceed the space available or hold a floor wider
     than their container. Same inline-style problem as the phone rule above. */
  .main input, .main select, .main textarea,
  .main .form-control, .main .form-select { max-width: 100% !important; }

  /* A Discord snowflake is eighteen unbroken digits and appears in table cells
     and detail rows all over. Left unbreakable it sets the minimum width of
     whatever contains it and pushes the layout sideways. */
  .main code { overflow-wrap: anywhere; }

  /* And so does a URL, for exactly the same reason and with less excuse — the
     dashboard prints full addresses in prose all over: the portal's own
     address on Levels, a resource link, a feed's source. A bare
     `https://servermod.app/atc/leaderboard` is one unbreakable token that sets
     the minimum width of the paragraph holding it.

     `anywhere` rather than `break-word`: only `anywhere` lets the long token
     affect the element's min-content width, which is the whole point — with
     `break-word` the paragraph still reports the URL's full width as its
     minimum and still pushes its container wide. */
  .main a, .main .field-hint, .main .card-desc { overflow-wrap: anywhere; }

  /* Tables scroll sideways inside their own box rather than widening the page.
     A .table-responsive can only do that if it is permitted to be narrower than
     the table it holds — as a flex or grid child it inherits `min-width: auto`,
     which resolves to the table's min-content width, so it grows the container
     and the whole page scrolls instead. Zeroing the floor on the wrapper and on
     the boxes it sits inside is what makes the overflow land in the right
     place. */
  .main .table-responsive { min-width: 0; overflow-x: auto; -webkit-overflow-scrolling: touch; }
  .main .card, .main .row > [class*="col-"] { min-width: 0; }

  /* ...and the table needs a floor, or it never actually scrolls. Bootstrap's
     .table is width:100%, so inside an auto-overflow box it simply shrinks to
     whatever it is given and wraps every cell to shreds — the squish, not a
     scroll. A minimum wider than a phone makes the overflow real. Reports
     carries nine columns; 44rem is about where they stay legible.
     Cases and Members opt out: they hide columns on mobile instead, so they
     already fit and a floor would give them a pointless sideways scroll.

     So does any `.sm-kv-table` — a two-column label/value list, of which the
     member's Member Stats panel is the one that was reported. Two columns
     need about twenty rem, so the floor made it 704px wide inside a 360px
     phone: readable only by scrolling right, which took the labels off the
     left. The floor exists to make a WIDE table scroll honestly; a narrow one
     has nothing to scroll to.

     The opt-out list keeps growing, and it should be read as "tables that
     already fit" rather than as exceptions. An opt-IN would be the tidier
     shape and would mean touching every wide table on the dashboard to say
     something they currently get for free. */
  .main .table-responsive > .table:not(.cases-table):not(.member-list-table):not(.sm-kv-table) { min-width: 44rem; }

}

/* Very narrow phones */
@media (max-width: 480px) {
  .topbar { padding: 0.5rem 0.75rem; }
  .user-bar .username { display: none; }
  .main { padding: 0.85rem 0.75rem; }
  .main .btn, .main button { font-size: 0.85rem; padding: 0.4rem 0.6rem; }
  /* Search + Prev + "Page 3 of 40" + Next does not fit across 360px. The page
     count is the one part that is repeated verbatim under the table, so it is
     the part that goes. */
  .main .members-search-actions .members-page-info { display: none; }
}

/* ============== Overview (redesign) ============== */
/* `center`, not `flex-start`. The title block is three stacked things — a 38px
   icon tile beside an eyebrow and an h1 — while the action cluster is a single
   2.25rem row. Top-aligned, the buttons sat level with the eyebrow and read as
   floating above the title they belong to. Centred, the whole row lines up on
   one axis, which is what the mobile breakpoint already did. */
.ov-head { display: flex; align-items: center; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.ov-title { display: flex; align-items: center; gap: 0.8rem; min-width: 0; }
.ov-title-text { min-width: 0; }
.ov-tile { width: 38px; height: 38px; display: grid; place-items: center; flex-shrink: 0; background: var(--sm-accent-ghost); border: 1px solid var(--sm-pill-border); color: var(--sm-accent-cyan); }
.ov-tile i { font-size: 1.05rem; }
.ov-crumb { font-family: var(--sm-font-mono); font-size: 0.66rem; letter-spacing: 0.13em; text-transform: uppercase; color: var(--sm-text-dim); margin-bottom: 2px; }
.main .ov-head h1 { margin: 0; font-size: 1.45rem; font-weight: 700; letter-spacing: -0.02em; color: var(--sm-text); overflow-wrap: anywhere; }
/* The action cluster must be allowed to shrink and to wrap onto its own line.
   Without `min-width: 0` a wide cluster — Stats carries two date inputs, Apply
   range, Refresh and Export CSV — refuses to shrink below its content width and
   overflows past the right edge of the page, taking the last button off-screen.
   `flex-wrap` on .ov-head is what lets it drop under the title instead. */
.ov-head-actions {
  margin-left: auto; display: flex; align-items: center; gap: 0.6rem;
  flex-wrap: wrap; justify-content: flex-end; min-width: 0; max-width: 100%;
}
/* Back sits outside .ov-head-actions so mobile can split the two apart. On
   desktop it takes the auto margin instead, which keeps the rendered order
   identical to when it lived inside the cluster. */
.ov-head > .sm-back-btn { margin-left: auto; flex-shrink: 0; }
.ov-head > .sm-back-btn + .ov-head-actions { margin-left: 0.6rem; }

/* Export menu — one fa-file-csv icon that opens a named list, for pages with
   more than one exportable dataset. See exportHeaderControl() in csvKit.js.

   A <details> rather than a scripted dropdown: keyboard-accessible for free, and
   the header stays usable if page JS has not run. The summary is styled as the
   same icon button every single-dataset export uses, so the two read as one
   control at a glance. */
.sm-export-menu { position: relative; display: inline-block; }
/* The marker is the default disclosure triangle; the icon is the affordance. */
.sm-export-menu > summary { list-style: none; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; }
.sm-export-menu > summary::-webkit-details-marker { display: none; }
.sm-export-menu > summary::marker { content: ""; }
.sm-export-menu[open] > summary { background: var(--sm-bg-raised); border-color: var(--sm-border-strong); }
/* Right-aligned: the control sits at the right edge of the header, so a
   left-aligned panel would hang off the page on a narrow viewport. */
.sm-export-menu-list {
  position: absolute; z-index: 40; top: calc(100% + 0.25rem); right: 0;
  min-width: 15rem; max-width: min(22rem, calc(100vw - 2rem));
  background: var(--sm-bg-raised); border: 1px solid var(--sm-border-strong);
  box-shadow: 0 8px 24px var(--sm-shadow-3); padding: 0.25rem;
  display: flex; flex-direction: column;
}
.sm-export-menu-list > a {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0.6rem; color: var(--sm-text); text-decoration: none;
  font-size: 0.85rem; line-height: 1.3;
}
.sm-export-menu-list > a:hover,
.sm-export-menu-list > a:focus-visible { background: var(--sm-bg-deep); color: var(--sm-text); outline: none; }
.sm-export-menu-list > a > i { color: var(--sm-text-muted); flex-shrink: 0; }

.ov-kpis { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 1rem; }
.ov-kpi { position: relative; background: var(--sm-bg-raised); border: 1px solid var(--sm-border); padding: 1rem 1.1rem; overflow: hidden; }
.ov-kpi.accent::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--sm-accent); }
.ov-kpi-top { display: flex; align-items: center; gap: 0.5rem; color: var(--sm-text-muted); font-size: 0.82rem; font-weight: 600; }
.ov-kpi-top i { font-size: 0.85rem; }
.ov-kpi-val { font-family: var(--sm-font-mono); font-size: 1.9rem; font-weight: 700; letter-spacing: -0.03em; line-height: 1; margin: 0.6rem 0 0.5rem; color: var(--sm-text); }
.ov-kpi-sub { font-size: 0.78rem; color: var(--sm-text-dim); }
/* A unit trailing the number — "30.6 mo", "9.3 %". Smaller, unbolded and in the
   body face, so the figure is the number and the unit is a caption on it rather
   than three more monospace characters of the same size. */
.ov-kpi-unit { font-family: var(--sm-font-ui); font-size: 0.9rem; font-weight: 400; color: var(--sm-text-muted); margin-left: 0.25rem; }
/* The value is a word, not a number. 1.9rem of monospace turns "Local lexicon"
   into two wrapped lines; at this size and in the body face it is a value you
   can read. */
.ov-kpi-val.is-text { font-family: var(--sm-font-ui); font-size: 1.05rem; font-weight: 600; line-height: 1.3; }

/* The box every chart on Stats, Overview and Subscribers is drawn into.

   Chart.js needs a sized parent — `responsive` with `maintainAspectRatio:false`
   measures the container, not the canvas — so the height is set per chart with
   an inline style and the width comes from here. It lived in two inline
   <style> blocks, one per page, which is how Subscribers came to have its own
   copy under a different name. See docs/UI_REFERENCE.md, Charts. */
.overview-chart-wrap { position: relative; width: 100%; min-height: 120px; }
.overview-chart-wrap canvas { max-width: 100%; }

/* ============== Stats panes ==============
   A pane is a run of blocks — one chart, one ranked table, one group of
   figures each. See dashboard/pages/stats/paneKit.js for what a block is.

   EVERY BLOCK IS ITS OWN PANEL, AT EVERY WIDTH.

   This used to be a phone-only treatment: above 768px a block was invisible and
   contributed only spacing, so a pane rendered as one tall card holding a dozen
   charts. The reasoning was that the two-column grid did the separating on a
   desktop, and it does not — a grid puts things beside each other, it does not
   tell you where one figure stops and the next begins. Ten charts and six
   ranked tables inside one border reads as a wall on a 27" monitor for exactly
   the reason it did on a phone, and every other module in the dashboard is
   already a stack of panels. So the rule that was correct below 768px is now
   the rule everywhere, and the desktop-only special case is gone.

   The pane's own card goes flat in its place, so the panels do not nest. */
/* Three of the nine panes wrap their whole content in this and the other six
   do not — and it has never had a rule, so it was decoration that looked like
   structure. `display: contents` makes it structurally absent rather than
   deleting six matched tags across three files: its children become direct
   children of .stats-pane, which is what the rules below expect, and all nine
   panes end up the same shape. */
.main .stats-pane-content { display: contents; }
/* A full block's worth of gap, because the strip is now a row of bordered tiles
   rather than loose text — 0.25rem was right when it had no edges of its own. */
/* The figure tiles: one track width, not a track count.

   This was Bootstrap `row-cols-2 / md-3 / xl-{item count}`, so the number of
   columns came from how many figures a pane happened to have and the last row
   was the remainder. Five tiles gave 3+2 with the pair stretched wider than the
   three above them; six gave 4+2, and Moderation's "Total cases" wrapped its
   label AND its "last 30 days" sub-line, making one tile taller than the rest
   of its row.

   `auto-fill` with a floor fixes all of it at once: every tile is the same
   width whatever the count, a short last row leaves space rather than
   stretching into it, and 13rem is wider than the longest label these panes
   use ("Active channels", "Total cases") so none of them wrap. 1fr as the max
   lets a row of three share the full width rather than leaving a gutter on the
   right.

   `auto-fill` and not `auto-fit`: auto-fit collapses the empty tracks, which
   re-stretches a short final row and puts the original bug back. */
.main .stats-figures {
  display: grid;
  /* auto-fit, not auto-fill: a row of four tiles takes the whole width
     rather than leaving a fifth track empty at the end. Every row of the
     same grid still shares one set of tracks, so a strip that wraps keeps
     its tiles equal — the remainder-row stretching the note in paneKit.js
     describes was Bootstrap's, and a grid does not do it. */
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  gap: 0.75rem;
  margin-bottom: 1rem;
}
/* The cell is a plain grid child now. It carried Bootstrap's `.col`, which
   brings its own flex basis and padding and fought the grid it sits in. */
.main .stats-figure-cell { min-width: 0; }
/* The sub-line stays on one line: it is a qualifier ("last 30 days"), and
   wrapping it was what made one tile taller than its neighbours. */
.main .ov-kpi-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* THE OLD BARE FIGURE CELL. The stats panes draw `.ov-kpi` now (see
   statsFigures in dashboard/pages/stats/paneKit.js); this survives for the one
   remaining hand-rolled strip, the sentiment status row in settings.js. Convert
   that and these three rules go. */
.main .stats-figure { display: flex; flex-direction: column; gap: 0.15rem; }
.main .stats-figure-label { font-size: 0.78rem; color: var(--sm-text-muted); }
.main .stats-figure-value { font-size: 1.05rem; font-weight: 700; color: var(--sm-text); }

/* `:has(.stats-block)` is a guard, not decoration. A pane that has not been
   converted to blocks yet still needs its card — stripping the chrome from a
   pane with nothing inside to carry it would leave its content floating on the
   page background. So the flat treatment applies exactly where there is
   something to replace it with, and an unconverted pane is merely unimproved
   rather than broken. */
.main .stats-pane:has(.stats-block) { background: none; border: none; padding: 0; margin-bottom: 0; }
.main .stats-block {
  background: var(--sm-bg-raised);
  border: 1px solid var(--sm-border);
  /* Was 1rem all round, which on top of the heading row's own 0.5rem put about
     a line and a half above the first thing worth looking at. The top is the
     edge that was wrong — the sides and bottom were fine — so only it comes
     down. */
  padding: 0.6rem 1rem 1rem;
  margin-bottom: 1rem;
}
.main .stats-block + .stats-block { margin-top: 0; }
.main .stats-block > :last-child { margin-bottom: 0; }
/* The pane heading is not a block and must not be boxed like one — it is the
   title of the stack, so it sits above the first card. */
.main .stats-pane > .card-heading { margin-bottom: 0.75rem; }
/* A pair is two blocks sharing a row. The row's own gutter is the gap between
   the two panels, so the blocks inside must not add a bottom margin on top of
   it — and both columns stretch so two panels of unequal content still square
   off against each other rather than leaving one short. */
.main .stats-pair { margin-bottom: 1rem; }
.main .stats-pair > [class*="col-"] { display: flex; }
.main .stats-pair > [class*="col-"] > .stats-block { flex: 1 1 auto; margin-bottom: 0; }

@media (max-width: 768px) {
  /* A pair is two blocks that happened to share a row; on a phone they are just
     two blocks stacked, and the row must not add its own gutters on top of the
     margin each panel already carries. */
  .main .stats-pair { --sm-gutter: 0; margin: 0; }
  .main .stats-pair > [class*="col-"] { padding: 0; display: block; }
  .main .stats-pair > [class*="col-"] > .stats-block { margin-bottom: 1rem; }
  .main .stats-block { padding: 0.55rem 0.9rem 0.85rem; }
}

/* ---- Stats: the pane while it is still loading ----
   The Stats pages ship a shell and fetch the pane after it, so between the two
   there is a placeholder standing in the pane's place. It reuses the theme's
   own busy vocabulary — .sm-progress for the bar, .sm-skeleton for the boxes,
   .sm-spinner for the line above them — and only adds the sizes that make those
   pieces the shape of a stats pane rather than the shape of a list row.

   The silhouette matters as much as the shimmer: the placeholder emits real
   .stats-block panels in the real grid, so when the figures land the boxes are
   already where they will end up and the page does not jump under a reader who
   has started scrolling. See statsSkeleton in dashboard/pages/stats/paneKit.js.
*/
.main .stats-loading { margin-bottom: 1rem; }
.main .stats-loading-line {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
  color: var(--sm-text-muted);
}
/* Pushes the elapsed count to the far end, so the wording can grow and shrink
   as the wait gets longer without the number moving around with it. */
.main .stats-loading-elapsed,
.main .stats-loading-line .sm-progress-pct { margin-left: auto; }

/* `.sm-skeleton:last-child` narrows the final bar to 60%, which is right for a
   stack of text lines and wrong for anything that is standing in for a solid
   shape. These say which they are rather than relying on their position. */
.main .sm-skeleton-fill { width: 100%; height: 100%; margin-bottom: 0; }
.main .sm-skeleton-line { display: block; width: 100%; }
.main .sm-skeleton-line:last-child { width: 55%; }
.main .sm-skeleton-label { display: block; width: 9rem; height: 0.7rem; margin-bottom: 0; }
.main .sm-skeleton-figure { display: block; width: 4.5rem; height: 1.05rem; margin-bottom: 0; }

/* Matches the height statsChart gives a real chart wrapper, so the swap does
   not resize the box it happens in. */
.main .stats-skeleton-chart { height: 180px; }

.ov-warn { color: var(--sm-warning); font-weight: 600; }

/* minmax(0, …) on both tracks, not a bare `1.55fr 1fr`.
   An `fr` track's floor is the item's min-content width, and the review queue
   holds single-line-clamped rows — a watcher summary is one long unbreakable
   line as far as intrinsic sizing is concerned, `overflow: hidden` or not. So
   the right track claimed the width of the longest alert sentence and squeezed
   the activity feed down to its own min-content: a ~200px column with the panel
   title wrapped over three lines. A zero floor lets the ratio hold and lets the
   rows' own ellipsis do the clamping. */
/* `stretch`, not `start`. On `start` each panel was only as tall as its own
   content, so the two ended the row at different heights — and because the left
   head carries a "View Cases" button and the right one only a badge, they were
   never going to agree even with identical bodies. Stretch makes the shorter one
   match the taller, and the min-height below gives them a floor so an empty
   Overview does not collapse to two shallow strips. */
.ov-cols { display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: 1rem; align-items: stretch; }
.ov-side { display: flex; flex-direction: column; gap: 1rem; min-width: 0; }
/* The side column stretches with the row; its panel has to grow to fill it, or
   the card sits at its content height inside a full-height column. */
.ov-side > .ov-panel { flex: 1 1 auto; }
/* A column, so the body can take the space the head does not. */
.ov-panel { padding: 0; margin-bottom: 0; overflow: hidden; min-width: 0; display: flex; flex-direction: column; }
/* Both panels cap at six rows, which comes out near 24rem with the head. Setting
   the floor there means the empty state occupies the same box the populated one
   will, so the row does not resize as cases and alerts arrive. */
.ov-cols .ov-panel { min-height: 24rem; }
/* Fill the panel under the head. Flex columns so an .ov-empty child can take the
   remaining height and centre itself in it rather than sitting at the top. */
.ov-feed, .ov-reports { flex: 1 1 auto; display: flex; flex-direction: column; min-height: 0; }
.ov-feed > .ov-empty, .ov-reports > .ov-empty { flex: 1 1 auto; }
.ov-panel-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.9rem 1.1rem; border-bottom: 1px solid var(--sm-border); }
.ov-panel-title { display: flex; align-items: center; gap: 0.55rem; font-size: 0.92rem; font-weight: 600; color: var(--sm-text); }
.ov-panel-title i { color: var(--sm-text-muted); }
/* Recent mod activity carries a View cases link in its head and Review queue
   does not, so the two heads sat at different heights — the titles beside each
   other were about 14px out of line, which reads as one panel having slipped.
   Giving the title the same 2.25rem floor every labelled button already has
   makes both heads resolve to padding + 2.25rem, with or without a control in
   them. Tied to the button height rather than a measured total so the two stay
   together if the head's padding is ever changed. It still holds now the
   control is a text link, which is shorter than the button it replaced.
   Scoped to .ov-cols because .ov-panel-head is also borrowed by Social Media's
   per-network rows, which are a list and should stay at their content height. */
.ov-cols .ov-panel-title { min-height: 2.25rem; }
.ov-panel-head .btn { margin-left: auto; flex-shrink: 0; }
/* Text style, not a button: the head already has a title and a border, and an
   outlined box for "go and look at the full list" competed with the rows below
   it for the eye. Underlined only on hover so it does not read as body copy. */
.ov-panel-link {
  margin-left: auto; flex-shrink: 0;
  font-size: 0.82rem; font-weight: 500;
  color: var(--sm-text-muted); text-decoration: none;
}
.ov-panel-link:hover, .ov-panel-link:focus-visible { color: var(--sm-accent); text-decoration: underline; }
.ov-report-badge { margin-left: auto; font-family: var(--sm-font-mono); font-size: 0.72rem; color: var(--sm-danger-text); background: var(--sm-danger-soft); border: 1px solid var(--sm-danger-line); padding: 0.1rem 0.45rem; }

.main .ov-feed-row { display: flex; align-items: center; gap: 0.8rem; padding: 0.8rem 1.1rem; border-bottom: 1px solid var(--sm-border); color: inherit; text-decoration: none; }
.main .ov-feed-row:last-child { border-bottom: 0; }
.main .ov-feed-row:hover { background: var(--sm-bg-hover); color: inherit; }
.ov-feed-ic { width: 34px; height: 34px; display: grid; place-items: center; flex-shrink: 0; font-size: 0.9rem; }
.ov-feed-body { flex: 1; min-width: 0; }
.ov-feed-top { display: flex; align-items: center; gap: 0.5rem; }
.ov-kind { font-size: 0.7rem; font-weight: 600; padding: 0.1rem 0.45rem; text-transform: capitalize; }
.ov-feed-who { font-weight: 600; color: var(--sm-text); font-size: 0.9rem; }
.ov-feed-by { color: var(--sm-text-muted); font-weight: 400; font-size: 0.82rem; }
.ov-feed-reason { font-size: 0.83rem; color: var(--sm-text-muted); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ov-feed-time { font-family: var(--sm-font-mono); font-size: 0.74rem; color: var(--sm-text-dim); flex-shrink: 0; }

/* On a phone the row is a summary, and "by <moderator>" is not part of the
   summary — it is the second-least useful thing on a line that already has to
   fit an icon, a type pill, a member name, a reason and a timestamp. It was
   what pushed the row past the edge. The case page carries the moderator, and
   the row links straight to it.

   `min-width: 0` on the name goes with it: without the by-line to blame, a long
   display name becomes the next thing that will not fit, and a flex item does
   not shrink below its content unless it is told it may. */
@media (max-width: 768px) {
  .ov-feed-by { display: none; }
  .ov-feed-top { min-width: 0; }
  .main .ov-feed-who {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* Rows are click targets for the detail modal, not links, so they need the
   cursor and the focus ring a link would have brought with it. align-items is
   flex-start rather than center: a two-line row centred its avatar against the
   whole block, which read as misaligned next to the badge on line one. */
.main .ov-report-row { display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.55rem 0.9rem; border-bottom: 1px solid var(--sm-border); color: inherit; text-decoration: none; cursor: pointer; }
.main .ov-report-row:last-child { border-bottom: 0; }
.main .ov-report-row:hover { background: var(--sm-bg-hover); color: inherit; }
.main .ov-report-row:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: -2px; background: var(--sm-bg-hover); }
.ov-ava { width: 26px; height: 26px; border-radius: 50%; display: grid; place-items: center; flex-shrink: 0; color: var(--sm-on-solid); font-size: 0.66rem; font-weight: 700; margin-top: 1px; }
.ov-ava i { font-size: 0.62rem; }
/* Real Discord avatar. Same box as the generated tile so a queue of mixed rows
   keeps one left edge, and object-fit stops a non-square upload distorting. */
.ov-ava-img { width: 26px; height: 26px; border-radius: 50%; flex-shrink: 0; margin-top: 1px; object-fit: cover; background: var(--sm-bg-hover); }

/* Settings → Audit activity tag. Same shape as .ov-kind-tag; colour comes from
   the action's tone, applied inline so one rule covers all five. */
.sm-audit-tag { display: inline-flex; align-items: center; gap: 0.3rem; white-space: nowrap; font-size: 0.64rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; padding: 0.1rem 0.4rem; line-height: 1.45; }
.sm-audit-tag i { font-size: 0.85em; }
.ov-report-body { flex: 1; min-width: 0; }
/* One line, never two. This used to wrap, because a badge + a name + a watcher
   tag did not always fit at the width this panel gets on a laptop — but the
   watcher tag moved into the summary line below (see overview.js), so the row
   now holds a name and a badge and nothing else. With the name truncating
   instead of pushing, there is nothing left to wrap, and allowing it meant a
   badge could drop to a line of its own and sit hard right, which reads worse
   than the raggedness below. */
.ov-report-top { display: flex; align-items: center; gap: 0.35rem 0.45rem; min-width: 0; flex-wrap: nowrap; }
/* The badge right-aligns instead of trailing the handle.
   Following the name put its left edge wherever that name happened to stop, so
   a queue of five rows had five different badge positions — @reif a few
   characters in, a twenty-character handle most of the way across. The eye
   reads a queue down the right edge as much as the left, and a ragged column of
   identical badges is the kind of untidy that looks like a bug.
   .ov-report-body is flex: 1, so its right edge is the same x on every row: one
   column, landing under the panel's own count badge and above the chevron.
   Scoped to this row rather than set on .ov-kind-tag, because Surveys reuses
   that class for its Draft/Active/Closed pills, where an auto margin would
   shove the pill to the far side of whatever holds it. */
.ov-report-top .ov-kind-tag { margin-left: auto; }
/* A watcher alert falls back to a raw user id when the member isn't resolvable,
   which is long enough to shove the meta tag out of the row without this. */
.ov-report-name { font-weight: 600; color: var(--sm-text); font-size: 0.82rem; min-width: 0; max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ov-report-count { font-size: 0.64rem; font-weight: 600; color: var(--sm-warning-text); background: var(--sm-warning-soft); border: 1px solid var(--sm-warning-line); padding: 0.05rem 0.35rem; white-space: nowrap; }
/* Marks whether a queue row came from a member pressing Report or from a
   watcher. The two need different follow-up, so the distinction has to survive
   being interleaved in one list.

   Sized to the name it sits beside rather than to a table cell: the tag's box
   is the same height as .ov-report-name's line, so the two share a baseline
   strip instead of the tag standing proud of it. Red for a report (a person
   said something is wrong), orange for an alert (a watcher thinks it might be)
   — the same pairing the queue's own status pills use. */
.ov-kind-tag { display: inline-flex; align-items: center; gap: 0.22rem; flex-shrink: 0; font-size: 0.58rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.03em; padding: 0 0.3rem; border-radius: 0; height: 1.23rem; line-height: 1; }
.ov-kind-tag i { font-size: 0.72em; }
.ov-kind-tag.report { color: var(--sm-danger-text); background: var(--sm-danger-soft); border: 1px solid var(--sm-danger-line); }
.ov-kind-tag.alert { color: var(--sm-warning-text); background: var(--sm-warning-soft); }
/* A flagged join. Blue, not red or orange: nothing has happened yet — the
   watcher only saw how the account arrived — and colouring it like a sanction
   would put a raid and a false positive on the same footing. */
.ov-kind-tag.flag { color: var(--sm-info-text); background: var(--sm-info-soft); border: 1px solid var(--sm-info-line); }
.ov-report-reason { font-size: 0.78rem; line-height: 1.35; color: var(--sm-text-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ov-chev { color: var(--sm-text-dim); flex-shrink: 0; font-size: 0.75rem; align-self: center; }

/* Moderation → Risks.

   Fixed layout, because one of the five columns is prose and `auto` let it
   decide the split: a long model reason squeezed the member column down to a
   40px avatar beside a name broken over four lines, and a short one left the
   reason column stranded at a third of the table. The percentages are declared
   here so the shape is the same whatever the queue happens to hold. */
.main .risks-table { table-layout: fixed; }
.main .risks-table th, .main .risks-table td { vertical-align: top; }
/* The reason column was `auto`, which on a wide screen meant it took everything
   the other four did not — a one-line sentence with 600px of empty cell after
   it, and the member column squeezed to fit around it. Both are declared now:
   the member column is the one carrying an avatar, a name, a badge, a handle
   and an id, so it gets the larger share. */
.main .risks-table .risks-col-member { width: 40%; }
.main .risks-table .risks-col-why { width: 30%; }
.main .risks-table .risks-col-confidence { width: 7rem; white-space: nowrap; }
.main .risks-table .risks-col-flagged { width: 10rem; }
/* sm-col-actions is `width: 1%` — a shrink-to-fit hint that an auto layout
   honours and a fixed one takes literally, which left the dismiss button in a
   column narrower than itself. */
.main .risks-table .sm-col-actions { width: 3.5rem; }
.main .risks-table .member-info { align-items: flex-start; }
/* The badge wraps under the name here. In the member list the name column is
   most of the row so nowrap costs nothing; here it competes with a column of
   prose, and "Possible Impersonator" beside a display name is wider than the
   column at any split worth having. */
.main .risks-table .member-display-line { flex-wrap: wrap; }
/* A model reason can be one unbroken URL or username. Without this the fixed
   layout is overruled by the longest word and the table overflows its wrapper. */
.main .risks-table .risks-reason { overflow-wrap: anywhere; }
/* The account id beside the handle. Dimmer and smaller than the handle itself —
   it is there to be copied, not read, and at the same weight it competed with
   the name above it. Breaks anywhere, because eighteen unbroken digits would
   otherwise set the minimum width of the column. */
.main .member-username .member-id-code {
  margin-left: 0.4rem; font-size: 0.72rem; color: var(--sm-text-dim);
  background: none; padding: 0; overflow-wrap: anywhere;
}
/* Desktop carries these as columns, so the inline copy in the reason cell stays
   out of the way until the columns go. */
.main .risks-table .risks-narrow-meta { display: none; }

/* Under 992px the two narrow columns are dropped and their values reappear
   inline under the reason — the same trade the member list makes, except the
   facts are not repeated on the member page, so they are repeated here instead
   of lost. The member column widens to take the freed space. */
@media (max-width: 992px) {
  .main .risks-table .risks-col-confidence,
  .main .risks-table .risks-col-flagged { display: none; }
  .main .risks-table .risks-narrow-meta { display: block; }
  .main .risks-table .risks-col-member { width: 42%; }
}

/* On a phone the avatar and the username go too: two columns of real content,
   a name, a badge and a reason. Everything dropped is on the member page, which
   the name still links to. */
@media (max-width: 576px) {
  .main .risks-table .risks-col-member { width: 40%; }
  .main .risks-table .member-avatar { display: none; }
  .main .risks-table .member-cell { padding-left: 0.5rem; padding-right: 0.5rem; }
}

/* Suspicious-join flag. Amber rather than red: it is "look at this", not "this
   member did something" — the watcher only saw how they arrived. */
.main .badge.sm-flag-suspicious {
  display: inline-flex; align-items: center; gap: 0.3rem;
  color: var(--sm-warning-text); background: var(--sm-warning-soft);
  border: 1px solid var(--sm-warning-line);
  font-size: 0.66rem; font-weight: 600; padding: 0.1rem 0.4rem; white-space: nowrap;
}
.main .badge.sm-flag-suspicious i { font-size: 0.9em; }

/* The same badge as a link, on the Risks wave strip. An <a> inherits the
   accent colour and an underline on hover from .main a, both of which fight
   the amber the badge is deliberately painted in — so the colour is restated
   and the hover moves the surface instead. */
.main a.badge.sm-flag-suspicious { color: var(--sm-warning); text-decoration: none; }
.main a.badge.sm-flag-suspicious:hover,
.main a.badge.sm-flag-suspicious:focus {
  color: var(--sm-warning); text-decoration: none;
  background: color-mix(in srgb, var(--sm-warning) 22%, transparent); border-color: color-mix(in srgb, var(--sm-warning) 55%, transparent);
}
/* Sits directly above the reason sentence it summarises, so it needs to break
   the line rather than sit inline with it. */
.main .risks-posing { display: block; }

/* Tap-to-reveal tooltip.
   `title` is a desktop-only affordance: a touch device has no hover, so the
   whole explanation of why an account was flagged was unreadable on a phone —
   the badge said "Suspicious" and there was no way to find out why short of
   opening a laptop. Any element carrying `data-tip` gets a real bubble that
   opens on tap, click or keyboard focus, and `title` is left in place so the
   native tooltip still serves a mouse. */
.sm-tip { position: relative; cursor: pointer; }
.sm-tip::after {
  content: attr(data-tip);
  position: absolute; left: 0; top: calc(100% + 0.35rem); z-index: 40;
  display: block; width: max-content; max-width: min(20rem, 78vw);
  padding: 0.4rem 0.55rem;
  background: var(--sm-bg-deep); color: var(--sm-text);
  border: 1px solid var(--sm-border); border-radius: 4px;
  font-size: 0.72rem; font-weight: 400; line-height: 1.35;
  white-space: normal; text-transform: none; letter-spacing: 0;
  box-shadow: 0 6px 18px var(--sm-shadow-3);
  /* Hidden by visibility rather than display so it is not read out to a screen
     reader while closed, and can still be measured when it opens.

     Collapsed to nothing while closed as well, because an absolutely positioned
     box still counts towards its scroll container's overflow even when it is
     invisible. A badge anywhere near the right of a card therefore reserved up
     to 20rem of bubble to the right of itself, and the page gained a horizontal
     scrollbar that nothing visible explained — which is what a member profile
     did the moment the member carried a suspicious or impersonator flag.
     Zero-sized and clipped it reserves nothing; the size comes back with
     .is-open, and the opacity still fades. */
  visibility: hidden; opacity: 0;
  width: 0; height: 0; padding: 0; border-width: 0; overflow: hidden;
  transition: opacity 120ms ease;
}
.sm-tip.is-open::after {
  visibility: visible; opacity: 1;
  width: max-content; height: auto; padding: 0.4rem 0.55rem;
  border-width: 1px; overflow: visible;
}
/* A badge near the right edge would push its own bubble off screen. */
.sm-tip.sm-tip-end::after { left: auto; right: 0; }

/* `align-content: center` as well as `place-items`: the latter centres an item
   inside its own track, and with the panel now taller than this content the
   track itself has to be centred or the message sits at the top of the space. */
/* The panel-body empty state — panelEmpty() in dashboard/pages/panelKit.js.
   Centred and dimmed so it reads as a state rather than as content, with enough
   padding that an empty panel keeps roughly the height of a full one and a row
   of three does not go ragged. Distinct from .ov-empty, which is the larger
   icon-over-text version for a whole feed panel. */
.sm-panel-empty {
  margin: 0; padding: 0.9rem 1rem; text-align: center;
  font-size: 0.86rem; color: var(--sm-text-dim);
}
.ov-empty { display: grid; place-items: center; align-content: center; gap: 0.5rem; padding: 2.2rem 1rem; color: var(--sm-text-dim); text-align: center; font-size: 0.86rem; }
.ov-empty i { font-size: 1.4rem; }

@media (max-width: 992px) {
  .ov-kpis { grid-template-columns: repeat(2, 1fr); }
  .ov-cols { grid-template-columns: 1fr; }
  /* Stacked, the two floors add up: 24rem each is 48rem of mostly empty screen
     to scroll past on a phone. They are no longer side by side, so there is
     nothing left to line up. */
  .ov-cols .ov-panel { min-height: 0; }
}

/* Header action clusters collapse to icons on narrow screens.
   A member profile carries five header controls (AI, Demographics, Add case,
   Privacy, Back). At full width with labels they wrap onto three rows and each
   button ends up a few characters wide — unreadable and hard to hit. Dropping
   to icons keeps them on one row at a comfortable tap size.
   Back is the exception: it keeps its label, because an unlabelled arrow in a
   row of unlabelled glyphs is the one control you cannot afford to guess at.
   Every collapsed button carries its own aria-label and title, so the accessible
   name survives the span being hidden. */
@media (max-width: 768px) {
  /* ONE RULE, and the content decides — not a count of children.

     This was five rules: the cluster took a row of its own, and then four
     `:has()` exceptions pulled it back beside the title depending on whether
     there was a Back button, whether the cluster had fewer than two or three
     children, and whether any child was a form or a `.form-control`. The
     result was that the same control sat on the title row on one page and a
     row of its own on the next, and a reader could not learn where to look.
     Worse, the counts went by DOM position, so a hidden count badge still
     counted and a cluster of three could be one visible button.

     Now: the cluster is `flex: 0 0 auto` and right-aligned, and `.ov-head`
     still wraps. So it sits on the title row whenever it fits and drops to a
     row of its own — still right-aligned — when it genuinely does not. That is
     the same outcome the exceptions were reaching for, decided by measurement
     rather than by counting, and it stays correct when a page gains a control.

     What makes the wrap happen is the floor on the title: without it the title
     shrinks to nothing and the cluster never wraps, so a long page name would
     be crushed to a few characters rather than the buttons moving down. */
  .ov-head { gap: 0.6rem 0.7rem; flex-wrap: wrap; align-items: center; }
  .ov-title { flex: 1 1 auto; min-width: min(5rem, 100%); }
  /* Back is never part of the action cluster and never moves: it is the way
     out, and the way out should be in the same place on every page. */
  .ov-head > .sm-back-btn { order: 1; margin-left: auto; white-space: nowrap; flex-shrink: 0; }
  /* `flex: 1 1 auto` rather than `0 0 auto`: growing does not change WHETHER
     the cluster wraps — that is decided from its max-content base size before
     any growing happens — but it does decide where it sits once it has. At
     `0 0 auto` the cluster is only as wide as its buttons, so on a row it
     shares with the Back button the free space lands between them and the
     buttons read as left-aligned under the title. Filling the row and
     justifying to the end puts them against the right edge in every
     arrangement, which is where the eye goes for actions. */
  .ov-head-actions { order: 2; flex: 1 1 auto; margin-left: auto; gap: 0.4rem; justify-content: flex-end; }
  .ov-head > .sm-back-btn + .ov-head-actions { margin-left: 0.4rem; }
  /* Count pills in a page header are desktop-only.
     Unlike a .btn they have no icon to collapse to — the text IS the badge — so
     they keep their full width and push the cluster onto its own row.

     display:none, not a narrower badge: "487 members · 12 apps" is context, not
     a control, and a phone reading a member list has the list itself. */
  .ov-head-actions .sm-head-count { display: none; }
  /* A field in a header cluster is capped rather than special-cased.
     `.sm-field-md` is 15rem, which is a whole phone line — and the old rule
     answered that by giving any cluster containing one a full row of its own.
     Capping it keeps the one rule above true for every page. */
  .ov-head-actions .form-control,
  .ov-head-actions .form-select { max-width: 6rem; }
  /*
    THE TOOLBAR SHARES THE TITLE'S ROW, which is arithmetic rather than taste.

    `.ov-head` wraps when the title's floor plus the cluster's width exceed the
    line, and on a 360px phone the Subscribers header did: a 9rem select plus
    three 2.25rem tiles is about 270px against a 12rem title floor. So the
    toolbar dropped to a row of its own and the first figure started a third of
    a screen further down — on every stats page, since they all carry a range
    control and an export.

    Three changes buy the row back, in the order they cost the least:

      the tile     hidden. It is a decorative glyph beside a heading that
                   already names the page, and it is 38px plus its gap — the
                   single largest thing on that row carrying no information.
      the select   6rem. Enough for "30 days" and its caret; the buttons beside
                   it collapsed to icons long ago for the same reason.
      the title    a 5rem floor and a smaller face, so it gives way before the
                   controls do.

    `flex-wrap` is untouched, so a genuinely long page name still drops the
    cluster to its own row rather than crushing the heading. This makes the row
    fit where it nearly did, not force it everywhere.
  */
  .ov-head .ov-tile { display: none; }
  .ov-title { gap: 0; }
  .main .ov-head h1 { font-size: 1.15rem; }
  /* A collapsed header action is a square glyph, not a short pill. Padding-only
     shrinking left buttons at inconsistent widths — a wand is narrower than a
     calendar — so a row of them stair-stepped. Fixing both axes to the 2.25rem
     control height makes the cluster read as one strip of equal tiles. */
  .ov-head-actions .btn > span { display: none; }
  .ov-head-actions .btn {
    min-width: 2.25rem;
    width: 2.25rem;
    padding: 0;
    justify-content: center;
  }
  .ov-head-actions .btn .label-icon { margin-right: 0; }
  /* The range picker keeps a label where the other header buttons collapse to
     icons: a calendar glyph alone would say "there is a date control here" and
     not which range is showing, which is the only thing it is for.

     It keeps the SHORT one. "Last 30 days" beside three 2.25rem tiles is what
     pushed the whole cluster onto a row of its own; "30d" fits beside them on
     the title row, which is where the cluster belongs. Swapped rather than
     truncated — see shortFace in dateRange.js. The long label stays in the DOM
     and the summary carries its own aria-label, so nothing is lost to a reader.
     The panel drops to full width so two date inputs still fit side by side. */
  .ov-head-actions .sm-range > summary { min-width: 0; width: auto; padding: 0 0.6rem; }
  .ov-head-actions .sm-range > summary > span { display: inline; }
  .ov-head-actions .sm-range .sm-range-face { display: none; }
  .ov-head-actions .sm-range .sm-range-face-short { display: inline; }
  .main .sm-range-panel { min-width: min(22rem, calc(100vw - 2rem)); }
  /* Filter/action strips that live in a page header block get the same
     treatment — a select keeps its label, a button becomes its icon. */
  .sm-head-filters .btn > span { display: none; }
  .sm-head-filters .btn { min-width: 2.25rem; width: 2.25rem; padding: 0; justify-content: center; }
  .sm-head-filters .btn .label-icon { margin-right: 0; }
}
@media (max-width: 560px) {
  /* Stays 2x2 rather than collapsing to a single column: four stacked KPIs push
     the actual page content below the fold on a phone, and the figures are short
     enough to read at half width. */
  .ov-kpis { gap: 0.6rem; }
  .ov-kpi { padding: 0.8rem 0.85rem; }
  .ov-kpi-val { font-size: 1.5rem; margin: 0.45rem 0 0.35rem; }

  /*
    THE STATS FIGURES PACK ON A PHONE INSTEAD OF STACKING.

    The desktop rule is `auto-fill, minmax(13rem, 1fr)`, and 13rem is wider than
    half a phone — so exactly one track ever fitted and every pane rendered its
    figures one per row. Subscribers draws six, which was six full-width tiles
    pushing the first chart two screens down; Voice draws two and Messages three,
    and all of them looked like a list rather than a summary.

    A 6rem floor fits three across at 360px, which is what these figures need:
    they are short numbers with two-word labels, and the value is the thing being
    read. Wider phones simply fit more.

    `auto-fit` HERE, where the desktop rule says `auto-fill`, and the difference
    is deliberate. auto-fill keeps empty tracks, which is right on a wide screen
    because it stops a short final row stretching into a row of six. On a phone
    it is wrong: a two-figure pane would sit in the left two thirds with a third
    of the width blank beside it. auto-fit collapses what is unused, so two
    figures take half each and three take a third each.
  */
  .main .stats-figures {
    grid-template-columns: repeat(auto-fit, minmax(6rem, 1fr));
    gap: 0.5rem;
  }
  /* Three-up is narrower than "Total voice minutes" on one line, so the label
     is allowed the second line it now needs. `nowrap` here was protecting the
     equal-height rule that `align-items` already gives the grid. */
  .main .stats-figures .ov-kpi-top { flex-wrap: wrap; row-gap: 0.1rem; font-size: 0.75rem; }
  .main .stats-figures .ov-kpi { padding: 0.65rem 0.7rem; }
  .main .stats-figures .ov-kpi-val { font-size: 1.15rem; margin: 0.35rem 0 0.2rem; }
  .main .stats-figures .ov-kpi-val.is-text { font-size: 0.9rem; }
  /* The glyph goes where the label needs the room. Three-up leaves about 70px
     of content, and "Members with intro data" beside a 14px icon wraps to four
     lines; without it, two. The label is the identification here — the icon
     was reinforcing something the words already say. */
  .main .stats-figures .ov-kpi-top i { display: none; }
}

/* ============== AI Copilot (redesign) ============== */
/* The Copilot header is pageHeader() now — see dashboard/pages/copilot.js.
   What is left here is the wrapper the chat layout needs: the panel below is
   a flex column filling the viewport, and this keeps the header out of it.
   The tile, eyebrow and title rules that used to sit here were a duplicate
   of the .ov-* ones and are gone. */
/*
  TIGHT UNDER THE HEADER, because everything below it is the point.

  1.25rem here plus 1.4rem of thread padding put about 45px of nothing between
  the title and the first message, on the one page in the dashboard whose
  content is a column that wants to be as tall as the window. The panel takes
  whatever is left after this margin (see the :has() rule below), so every
  pixel spent here is a pixel off the conversation.
*/
.cp-head { margin-bottom: 0.6rem; }

/* Fills the main column rather than a guessed offset. The old
   calc(100vh - 230px) assumed a fixed amount of chrome above it, so on a short
   viewport the panel ended up a stub with two lines of thread visible. `.main`
   is already the scroll container with known padding, so 100% of it is both
   taller and correct at every size. */
/* Base/fallback height for browsers without :has(). Any fixed value here is a
   guess at how much chrome sits above the panel, which is why the rule below
   supersedes it. */
.cp-panel { display: flex; flex-direction: column; height: calc(100vh - 15rem); min-height: 320px; background: var(--sm-bg-raised); border: 1px solid var(--sm-border); overflow: hidden; }

/* The correct version: make .main a flex column on the Copilot page only, and
   let the panel take whatever is left after the page header. No arithmetic, no
   assumption about header height — it is exact at every viewport size, which
   calc() against 100vh never is (it overflowed the fold, cutting off the
   composer). Scoped with :has() so no other page's layout changes. */
.main:has(.cp-panel) { display: flex; flex-direction: column; }
.main:has(.cp-panel) .cp-panel { flex: 1 1 auto; height: auto; min-height: 0; }
.main:has(.cp-panel) .cp-head { flex: 0 0 auto; }

/* Less at the top and bottom than at the sides: the sides are the reading
   measure and want the room, the ends are just the first and last bubble's
   clearance from a border. */
.cp-thread { flex: 1; overflow-y: auto; padding: 0.9rem 1.4rem; display: flex; flex-direction: column; gap: 0.9rem; min-height: 0; }
/* Readability: answers are prose and lists, so they get a longer line-height, a
   capped measure, and preserved newlines — the model's paragraph and bullet
   breaks were previously collapsed into one wall of text.
   break-word, not break-all: only over-long tokens (ids, URLs) should break. */
.cp-bubble {
  max-width: min(76%, 68ch);
  padding: 0.85rem 1.05rem;
  font-size: 0.92rem;
  line-height: 1.65;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: normal;
}
.cp-bubble + .cp-bubble { margin-top: 0.1rem; }
.cp-bubble.bot { align-self: flex-start; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); color: var(--sm-text-muted); }
.cp-bubble.bot strong, .cp-bubble.bot b { color: var(--sm-text); }
.cp-bubble.me { align-self: flex-end; background: var(--sm-accent); color: var(--sm-accent-fg); font-weight: 500; white-space: pre-wrap; }
.cp-bubble.err { align-self: flex-start; background: var(--sm-danger-soft); border: 1px solid var(--sm-danger-line); color: var(--sm-danger-text); white-space: pre-wrap; }
.cp-list { margin: 0.5rem 0 0.1rem; padding-left: 0; list-style: none; display: grid; gap: 0.4rem; }
.cp-list li { padding-left: 1.1rem; position: relative; color: var(--sm-text-muted); }
.cp-list li::before { content: ""; position: absolute; left: 2px; top: 0.55em; width: 6px; height: 6px; background: var(--sm-accent); }
.cp-list li strong { color: var(--sm-text); }

.cp-foot { border-top: 1px solid var(--sm-border); padding: 1rem 1.1rem; display: flex; flex-direction: column; gap: 0.8rem; }
.cp-locked { margin: 0; font-size: 0.82rem; }
/* Suggestion chips: even columns that collapse to one on narrow screens,
   left-aligned so the question text lines up with the composer below. Under
   768px the media query further down turns them into a scrolled strip, which
   is why these must stay above it (a copy after it once undid the strip). */
.cp-suggests { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 0.5rem; }
.main .cp-suggest { text-align: left; justify-content: flex-start; width: 100%; padding: 0.65rem 0.85rem; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); color: var(--sm-text-muted); font-size: 0.82rem; font-weight: 500; min-width: 0; min-height: 0; }
.main .cp-suggest:hover { background: var(--sm-bg-hover); border-color: var(--sm-pill-border); color: var(--sm-text); }
.main .cp-suggest:disabled { opacity: 0.5; }
.cp-composer { display: flex; gap: 0.6rem; align-items: stretch; }
.main .cp-composer textarea.cp-input { flex: 1; resize: none; min-height: 2.4rem; }
/* Match the textarea exactly. The button carried the global 2.25rem button
   minimum while the textarea sets 2.4rem, so it sat a few pixels short of the
   field it belongs to. align-self: stretch keeps them level as the textarea
   grows with a longer question. */
.main .cp-composer .btn { flex-shrink: 0; min-height: 2.4rem; align-self: stretch; }

@media (max-width: 768px) {
  /* The composer is the one control that must always be reachable, so on a
     phone everything above it gives ground first.
     Suggestions become a single horizontally-scrolled strip rather than a
     stacked column: four full-width chips were ~9rem of vertical space in the
     foot, which on a 640px-tall viewport left the thread showing two messages
     and pushed Send toward the fold. */
  .cp-head { margin-bottom: 0.75rem; }
  .cp-panel { min-height: 0; }
  .cp-thread { padding: 0.9rem; gap: 0.7rem; }
  .cp-bubble { max-width: 90%; }
  .cp-foot { padding: 0.7rem 0.75rem; gap: 0.6rem; }
  .cp-suggests {
    display: flex;
    grid-template-columns: none;
    gap: 0.45rem;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    padding-bottom: 0.15rem;
  }
  .cp-suggests::-webkit-scrollbar { display: none; }
  .main .cp-suggest { flex: 0 0 auto; width: auto; white-space: nowrap; padding: 0.45rem 0.7rem; font-size: 0.78rem; }
  /* Send collapses to its glyph — the label is 4rem of a 360px row that the
     question field needs more than the word "Send" does. */
  .main .cp-composer .btn { min-width: 2.6rem; width: 2.6rem; padding: 0; justify-content: center; }
  .main .cp-composer .btn > span { display: none; }
}

/* ============== Progress, spinners and busy states ==============
   Anything that takes long enough to notice says so in one of three ways:

     .sm-spinner   a control is working — replaces the button's own icon
     .sm-progress  a job with a known size — determinate bar plus percentage
     .sm-skeleton  a panel whose content has not arrived yet

   Squared like everything else: the bar has no radius, and the spinner is the
   only rotating element in the theme because a static "working…" is
   indistinguishable from a wedged request. */

.sm-spinner {
  display: inline-block;
  width: 0.85em;
  height: 0.85em;
  /* Never shrink. Every button in the theme is `display: inline-flex` (see the
     .main button rule above), which makes this a FLEX ITEM — and a flex item
     with `content: ""` has a min-content width of zero, so the browser is free
     to squeeze it flat while leaving the height alone. What you get is the 2px
     left border and nothing else: the "thin vertical line" instead of a
     spinner. `flex: 0 0 auto` is what forbids that. */
  flex: 0 0 auto;
  border: 2px solid currentColor;
  /* One transparent edge is what makes the rotation legible; a full ring reads
     as a static circle no matter how fast it spins. */
  border-right-color: transparent;
  border-radius: 50%;
  animation: sm-spin 0.6s linear infinite;
  vertical-align: -0.1em;
}
@keyframes sm-spin { to { transform: rotate(360deg); } }

/* A control mid-submit: dimmed, unclickable, and its icon swapped for a
   spinner. Applied by the layout script on submit, so every form gets it
   without opting in. */
.main .btn.is-busy { opacity: 0.7; pointer-events: none; }
.main .btn.is-busy > i { display: none; }
.main .btn.is-busy::before {
  content: "";
  display: inline-block;
  /*
    A SQUARE, in the same units on both axes, and the same footprint as the icon
    it replaces.

    Both halves of that matter and the second one broke the first once already:

      1. `border-radius: 50%` on a box that is not square draws an ELLIPSE, and
         an ellipse rotating on its centre wobbles. This was 0.85em tall with a
         0.9rem min-width - 12px by 14px - so it span as an oval. If these two
         values ever stop matching, that is the symptom.
      2. The size is the icon column's, not the glyph's. `.label-icon` reserves
         `min-width: 0.9rem`, so a spinner narrower than that makes the whole
         content group shrink the moment a button goes busy, and on a centred
         `min-width: 5.5rem` button the label visibly jumps.

    No margin either. Buttons carry `gap: 0.45rem` and `.label-icon` has its own
    margin zeroed there precisely so the two do not add up; this pseudo-element
    was the last thing still adding one on top of the gap.
  */
  width: 0.9rem;
  height: 0.9rem;
  /* Same trap as .sm-spinner above: the button is inline-flex, so this is a
     flex item, and a flex item with `content: ""` has a min-content width of
     zero - the browser will squeeze it flat and leave the height alone, which
     draws the 2px left border and nothing else. */
  flex: 0 0 auto;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: sm-spin 0.6s linear infinite;
  vertical-align: -0.1em;
}

/* No icon-only override any more: the spinner is a fixed square, so there is
   no reserved column to undo. An icon-only button gets the same circle. */

.sm-progress {
  height: 6px;
  background: var(--sm-bg-deep);
  overflow: hidden;
  position: relative;
}
.sm-progress-bar {
  height: 100%;
  width: 0;
  background: var(--sm-accent);
  transition: width 0.4s ease;
}
/* Size not yet known — a job still counting what it has to do. The stripe
   sweeps rather than filling, so it cannot be misread as "40% done". */
.sm-progress.is-indeterminate .sm-progress-bar {
  width: 35%;
  animation: sm-sweep 1.2s ease-in-out infinite;
}
@keyframes sm-sweep {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(320%); }
}
.sm-progress-meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.4rem;
  font-size: 0.8rem;
  color: var(--sm-text-dim);
}
.sm-progress-pct { font-family: var(--sm-font-mono); color: var(--sm-text-muted); }

/* Placeholder blocks for a panel whose data is still loading. */
.sm-skeleton {
  background: linear-gradient(90deg, var(--sm-bg-deep) 25%, var(--sm-bg-hover) 37%, var(--sm-bg-deep) 63%);
  background-size: 400% 100%;
  animation: sm-shimmer 1.4s ease infinite;
  height: 0.85rem;
  margin-bottom: 0.5rem;
}
.sm-skeleton:last-child { margin-bottom: 0; width: 60%; }
@keyframes sm-shimmer {
  0% { background-position: 100% 50%; }
  100% { background-position: 0 50%; }
}

/* Respect a stated preference for less motion: the spinner and sweep still
   need to say "working", so they slow to a crawl rather than stopping dead. */
@media (prefers-reduced-motion: reduce) {
  .sm-spinner,
  .main .btn.is-busy::before { animation-duration: 2s; }
  .sm-progress.is-indeterminate .sm-progress-bar { animation-duration: 3s; }
  .sm-skeleton { animation: none; }
}

/* ============== Navigating ==============

   A nav click is a FULL PAGE LOAD, and some pages take seconds to build.

   Settings is the worst of them: it loads the data for all four of its panes on
   every visit. Between the click and the new document arriving the browser
   paints nothing at all — the old page just sits there, the tab throbber being
   the only sign anything happened, and on a wide monitor that is off in the
   corner. So the click looked like it had missed, and the honest response to
   that is to click again, which starts the whole render a second time.

   Nothing here makes the page faster. It makes the click ANSWERED: the item
   goes active immediately, its icon becomes a spinner, and a stripe sweeps
   along the bottom of the top bar until the document is replaced. All three are
   undone by the arriving page, which is a fresh document — there is nothing to
   clean up on the success path, only on the ones where no navigation happens
   after all (a cancelled load, or coming back through the bfcache). */

/* Pending, not active. It reads as active because that is what the reader is
   being told — you are going here — but it keeps its own class so the arriving
   page's genuinely-active item is not competing with a stale one if both are
   ever on screen at once. */
.nav-item.is-navigating {
  color: var(--sm-accent-cyan);
  background: var(--sm-accent-ghost);
  border-left-color: var(--sm-accent);
  font-weight: 500;
}
/* The icon BECOMES the spinner rather than sitting beside one: the sidebar is
   240px with a fixed icon column, and adding a second glyph would shift the
   label sideways at the moment of the click. `visibility` rather than
   `display`, so the column keeps its width and nothing reflows. */
.nav-item.is-navigating .nav-icon { position: relative; visibility: hidden; }
.nav-item.is-navigating .nav-icon::after {
  content: "";
  visibility: visible;
  position: absolute;
  inset: 0;
  margin: auto;
  width: 0.8rem;
  height: 0.8rem;
  border: 2px solid var(--sm-accent-ghost);
  border-top-color: var(--sm-accent);
  border-radius: 50%;
  animation: sm-spin 0.7s linear infinite;
}

/* The stripe, on the bottom edge of the top bar.

   Inside .topbar (which is already `position: relative`) rather than in .main.
   .main is the scroller on this dashboard, so a bar placed in it would scroll
   away from a reader halfway down a long page — which is exactly where somebody
   reaching for a nav item usually is. The top bar never moves.

   `bottom: -1px` puts it over the bar's own border rather than above it, so
   nothing on the page shifts by three pixels when it appears. */
.topbar .sm-nav-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 3px;
  background: var(--sm-bg-deep);
  overflow: hidden;
  pointer-events: none;
}
.sm-nav-progress[hidden] { display: none; }
.sm-nav-progress::after {
  content: "";
  display: block;
  height: 100%;
  width: 35%;
  background: var(--sm-accent);
  animation: sm-sweep 1.2s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .nav-item.is-navigating .nav-icon::after { animation-duration: 2s; }
  .sm-nav-progress::after { animation-duration: 3s; }
}

/* ============== Toasts ==============
   Bottom-right, stacked newest-last, auto-dismissing. Lives outside .main so
   the `.main button` reset doesn't style the close control. Driven by the
   smToast() API defined in dashboard/layout.js. */
.sm-toasts {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 1080;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  align-items: stretch;
  width: min(23rem, calc(100vw - 2rem));
  /* The container spans a column the toasts sit in; only the toasts themselves
     should intercept clicks, or it would blanket the corner of the page. */
  pointer-events: none;
}
.sm-toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: 0.6rem;
  background: var(--sm-bg-raised);
  border: 1px solid var(--sm-border-strong);
  border-left: 3px solid var(--sm-info);
  color: var(--sm-text);
  padding: 0.7rem 0.75rem;
  box-shadow: 0 10px 28px var(--sm-shadow-3);
  font-size: 0.875rem;
  line-height: 1.45;
  animation: sm-toast-in 0.18s ease-out;
}
.sm-toast-icon { flex-shrink: 0; margin-top: 0.1rem; color: var(--sm-info); }
.sm-toast-body { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.sm-toast-title { display: block; font-weight: 600; margin-bottom: 0.1rem; }
.sm-toast-msg { display: block; color: var(--sm-text-muted); }
.sm-toast-close {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--sm-text-dim);
  cursor: pointer;
  padding: 0 0.15rem;
  font-size: 0.9rem;
  line-height: 1.4;
}
.sm-toast-close:hover { color: var(--sm-text); }
.sm-toast-close:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }

.sm-toast-success { border-left-color: var(--sm-success); }
.sm-toast-success .sm-toast-icon { color: var(--sm-success); }
.sm-toast-error { border-left-color: var(--sm-danger); }
.sm-toast-error .sm-toast-icon { color: var(--sm-danger); }
.sm-toast-warning { border-left-color: var(--sm-warning); }
.sm-toast-warning .sm-toast-icon { color: var(--sm-warning); }
.sm-toast-info { border-left-color: var(--sm-info); }
.sm-toast-info .sm-toast-icon { color: var(--sm-info); }

.sm-toast-hiding { animation: sm-toast-out 0.16s ease-in forwards; }

@keyframes sm-toast-in {
  from { opacity: 0; transform: translateX(12px); }
  to { opacity: 1; transform: none; }
}
@keyframes sm-toast-out {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateX(12px); }
}

@media (prefers-reduced-motion: reduce) {
  .sm-toast, .sm-toast-hiding { animation: none; }
}

@media (max-width: 560px) {
  .sm-toasts { right: 0.5rem; left: 0.5rem; bottom: 0.5rem; width: auto; }
}

/* ============== Detail modal + snippet cell ==============
   A row shows a one-line extract; the magnify control opens the full text plus
   every field behind it, with the row's actions repeated inside so a moderator
   can read and act without leaving the list. Shared by Alerts and Reports —
   see dashboard/pages/detailModal.js. */
.sm-snip { display: flex; align-items: flex-start; gap: 0.4rem; min-width: 0; }
.sm-snip-text {
  /* Block, not the default inline: `overflow`/`text-overflow` do nothing on a
     non-replaced inline box, so as a span in a bare table cell this clamped
     nothing and a long reason set the width of the whole table. Still a valid
     flex child inside .sm-snip. */
  display: block;
  min-width: 0;
  flex: 1;
  color: var(--sm-text-muted);
  /* One line only: the list stays scannable, the modal carries the detail. */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.sm-snip-text.is-empty { color: var(--sm-text-faint); font-style: italic; }

/* ---- Review queue rows (Alerts + Reports) ----
   The row is the click target for the detail modal; the cursor and the hover
   tint are what say so. Interactive children keep their own cursor because the
   handler lets their clicks through — see dashboard/pages/detailModal.js. */
.main .sm-detail-row { cursor: pointer; }
.main .sm-detail-row:hover > td { background: var(--sm-bg-hover); }
.main .sm-detail-row a,
.main .sm-detail-row button { cursor: pointer; }

/* Width caps, applied to the cell's content rather than the cell: `max-width`
   on a `<td>` is advisory under auto table layout, so a capped block inside it
   is what actually holds the column still. */
.sm-col-content .sm-snip-text { max-width: 38rem; }
.sm-cell-clip { max-width: 15rem; }
.sm-cell-clip > a,
.sm-cell-clip > span:not(.badge) { display: inline-block; max-width: 15rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
@media (max-width: 1400px) {
  .sm-col-content .sm-snip-text { max-width: 24rem; }
}

.sm-modal-backdrop { position: fixed; inset: 0; background: var(--sm-backdrop); z-index: 1070; display: flex; align-items: center; justify-content: center; padding: 1rem; }
.sm-modal-backdrop[hidden] { display: none; }
.sm-modal-card { width: min(720px, 100%); max-height: 88vh; display: flex; flex-direction: column; background: var(--sm-bg-raised); border: 1px solid var(--sm-border-strong); border-radius: 0; }
.sm-modal-head { display: flex; align-items: center; gap: 0.6rem; padding: 0.85rem 1rem; border-bottom: 1px solid var(--sm-border); flex-shrink: 0; }
.sm-modal-title { margin: 0; font-size: 0.95rem; font-weight: 600; color: var(--sm-text); flex: 1; min-width: 0; }
/* Sits before the heading rather than inside it — see formModal's titleIcon.
   Dimmed and non-shrinking, matching the panel summary icon it echoes. */
.sm-modal-title-icon { color: var(--sm-text-muted); flex-shrink: 0; font-size: 1rem; line-height: 1; }

/* A labelled button that becomes icon-only on a phone.

   For a header control whose word is worth having where there is room and is
   the first thing to cost you the row where there is not. The glyph loses its
   label gutter at the same breakpoint, so the button squares off rather than
   keeping padding for text that is no longer there. */
@media (max-width: 576px) {
  .main .btn.sm-hide-narrow > span { display: none; }
  .main .btn.sm-hide-narrow > .label-icon { margin-right: 0; }
}

/* A field and its button as one control.

   Joined rather than spaced: a search box and the button that runs it are one
   thing, and a gap between them made the button read as belonging to whatever
   sat next in the toolbar. The shared edge is collapsed so the pair has one
   outline rather than two touching ones. */
.main .sm-input-group { display: flex; align-items: stretch; width: 100%; }
.main .sm-input-group > .form-control { flex: 1 1 auto; min-width: 0; }
.main .sm-input-group > .form-control:not(:last-child) { border-right: 0; }
.main .sm-input-group > .btn { flex: 0 0 auto; }
/* The focus ring has to sit above the neighbour it shares an edge with, or the
   collapsed border hides half of it. */
.main .sm-input-group > :focus-visible { position: relative; z-index: 1; }

/* The leaderboard toolbar: the lookup takes a full row, Cleanup takes the next.
   Cleanup keeps its word — on its own row there is nothing for the label to
   cost, which is why it no longer carries sm-hide-narrow. */
.main .levels-board-tools { width: 100%; }
.main .levels-lookup-form { width: 100%; min-width: 0; max-width: 26rem; }
.main .levels-lookup-input { min-width: 0; }

.sm-modal-body { padding: 1rem; overflow-y: auto; }
.sm-modal-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem; border-top: 1px solid var(--sm-border); flex-shrink: 0; }
.sm-modal-foot:empty { display: none; }

/* Multi-section help: one modal covering a page whose panels each have their own
   `?`. The section is the scroll target, so it needs a rule that separates one
   from the next — otherwise a jump lands mid-prose with no visible boundary and
   reads as having scrolled to the wrong place. `scroll-margin-top` keeps a
   little of the previous section visible above the one jumped to, which is what
   makes it obvious the reader is partway down a longer document. */
.sm-help-section { scroll-margin-top: 0.5rem; }
.sm-help-section:focus { outline: none; }
.sm-help-section + .sm-help-section { margin-top: 1.1rem; padding-top: 1.1rem; border-top: 1px solid var(--sm-border); }
.sm-help-section-title {
  margin: 0 0 0.4rem; font-size: 0.78rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.05em; color: var(--sm-text-dim);
}
.sm-help-section > p:last-child { margin-bottom: 0; }

/* A form modal wraps head/body/foot in the <form> itself, so the form has to
   become the flex column the card would otherwise be. Without this the body
   never gets a bounded height, `overflow-y: auto` has nothing to scroll
   against, and a twelve-field form grows the card past the viewport and takes
   Save with it. */
.sm-modal-card > .sm-form-modal-form { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
/* Only for forms that genuinely run two columns; the 720px default is right
   for the single-column majority and a wider dialog reads as a page. */
.sm-modal-card-wide { width: min(940px, 100%); }

/* The unit half of a number-plus-unit control — "6 [Hours v]".

   Inside a Bootstrap .input-group every child is `flex: 1 1 auto`, so a select
   sitting beside a number input takes half the width and the number it
   qualifies gets squeezed into the other half. The unit is one short word and
   should be sized by its content; the number is the field being filled in and
   keeps the room. */
.main .input-group > .form-control.sm-input-unit {
  flex: 0 0 auto;
  width: auto;
  min-width: 5.5rem;
}

/* A field showing a value the deployment owns. Dimmed and not-allowed rather
   than hidden: what your feeds are actually using is worth being able to read,
   and a box you cannot type into should look like one before you try. */
.main .form-control.sm-input-locked {
  background: var(--sm-bg-deep);
  color: var(--sm-text-muted) !important;
  cursor: not-allowed;
  opacity: 0.85;
}

/* An image that opens the file itself, in a new tab — the member avatar and
   banner. It replaced a lightbox whose entire content was one `<img>`, which
   the browser's own image view does better; see the note in member.js.

   An anchor rather than the old button, so it is a link and behaves like one:
   middle-click, ctrl-click and "copy link address" all work without any script.
   The reset is still needed because the anchor must look exactly like the image
   it wraps, with only the focus ring and the pointer saying it is a control. */
.main .sm-image-link,
.sm-image-link {
  display: inline-block;
  padding: 0;
  border: 0;
  background: none;
  line-height: 0;
  cursor: zoom-in;
  max-width: 100%;
}
.sm-image-link:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }
.sm-image-link > img { display: block; }

/* Full-bleed variant, for an image meant to span its container rather than sit
   at its own size — the member banner. The wrapper is what carries the width,
   so the link between it and the image has to stop being a shrink-to-fit inline
   box and pass the width through. */
.main .sm-image-link-block,
.sm-image-link-block { display: block; width: 100%; }

/* Social Media: one Platforms panel holding every network.
   Each network is a collapsible panel now rather than a bordered row — the
   .sm-net-row rules that drew those separators are gone with them, because a
   panel brings its own chrome and the two together drew a box inside a box.
   What is kept is the styling of the followed-account list inside one. */
.main .sm-net-feed-actions { display: inline-flex; align-items: center; gap: 0.25rem; flex-shrink: 0; }
/* Icon buttons in a network box are 2rem, not the 5.5rem labelled-button floor
   — a row holding two of them at full width leaves no room for the feed name. */
.main .sm-net-feed-actions .btn.sm-icon-btn { min-width: 2rem; width: 2rem; height: 2rem; min-height: 2rem; padding: 0; }
.main .sm-net-feeds { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.main .sm-net-feed {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 0.45rem 0; border-bottom: 1px solid var(--sm-border);
  font-size: 0.85rem; min-width: 0;
}
.main .sm-net-feed:last-child { border-bottom: none; }
.main .sm-net-feed > i { flex-shrink: 0; font-size: 0.8rem; }
.main .sm-net-feed-ok > i { color: var(--sm-success); }
.main .sm-net-feed-err > i { color: var(--sm-danger); }
.main .sm-net-feed-muted > i { color: var(--sm-text-dim); }
/* The name takes the slack so the channel and the edit control stay put; a long
   feed name truncates rather than pushing the button off the card. */
.main .sm-net-feed-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--sm-text); }
.main .sm-net-feed-ch { flex-shrink: 0; color: var(--sm-text-dim); font-family: var(--sm-font-mono); font-size: 0.78rem; }
.main .sm-net-feed .btn { flex-shrink: 0; }

/* The locked screen. A denied page used to look like a failed one — same red,
   same lone sentence — so "not yours" and "broken" were indistinguishable. This
   is deliberately calm: dim rather than red, centred, and built around the
   permission key, because the only useful next step is asking someone for it. */
.main .sm-locked {
  display: flex; flex-direction: column; align-items: center; text-align: center;
  gap: 0.5rem; padding: 3rem 1.5rem; margin: 0 auto; max-width: 34rem;
}
.main .sm-locked-tile {
  width: 64px; height: 64px; display: grid; place-items: center; flex-shrink: 0;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border-strong);
  color: var(--sm-text-dim); margin-bottom: 0.5rem;
}
.main .sm-locked-tile i { font-size: 1.6rem; }
.main .sm-locked-title { margin: 0; font-size: 1.15rem; font-weight: 600; color: var(--sm-text); }
.main .sm-locked-body { margin: 0; font-size: 0.9rem; color: var(--sm-text-muted); }
.main .sm-locked-keys { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 0.4rem; margin-top: 0.5rem; }
.main .sm-locked-or { font-size: 0.78rem; color: var(--sm-text-dim); }
.main .sm-locked-hint { margin: 0.35rem 0 0; font-size: 0.8rem; color: var(--sm-text-dim); }
/* The read-only strip: an icon and a line, aligned rather than stacked. */
.main .alert.sm-locked-strip { display: flex; align-items: flex-start; gap: 0.6rem; }
.main .alert.sm-locked-strip > i { margin-top: 0.15rem; color: var(--sm-text-dim); flex-shrink: 0; }

/* Multi-select over a handful of options — the content types a feed announces.
   Deliberately not a <select multiple>: it hides the current state behind a
   scroll box, needs shift-click to discover, and is close to unusable on a
   phone. As checkboxes-as-chips every choice and its state is visible at once,
   which for three options is the whole point. */
.main .sm-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.main .sm-chip { position: relative; display: inline-flex; }
.main .sm-chip input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
.main .sm-chip span {
  display: inline-flex; align-items: center; gap: 0.4rem;
  padding: 0.35rem 0.7rem; min-height: 2.25rem;
  font-size: 0.85rem; color: var(--sm-text-muted);
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border);
  user-select: none;
}
.main .sm-chip input:checked + span { color: var(--sm-text); background: var(--sm-pill-bg); border-color: var(--sm-pill-border); }
.main .sm-chip input:focus-visible + span { outline: 1px solid var(--sm-accent); outline-offset: -1px; }
.main .sm-chip input:disabled + span { opacity: 0.55; cursor: not-allowed; }
.main .sm-chip input:not(:disabled):hover + span { border-color: var(--sm-border-strong); }

/* The quoted message/trigger text. Pre-wrap because Discord content is
   newline-significant, and a scroll cap so a wall of text can't push the
   actions off-screen. */
.sm-modal-quote {
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border);
  border-left: 3px solid var(--sm-accent);
  padding: 0.7rem 0.8rem;
  margin: 0 0 1rem;
  font-size: 0.875rem;
  line-height: 1.55;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 40vh;
  overflow-y: auto;
}
.sm-modal-quote.is-empty { color: var(--sm-text-faint); font-style: italic; border-left-color: var(--sm-border-strong); white-space: normal; }

/* ============== Emoji picker ==============
   Opens from the button beside any emoji field. See dashboard/pages/emojiPicker.js.

   `position: fixed` and script-set coordinates, not absolute: these buttons sit
   inside modals and scrolling cards, and an absolute panel would be clipped by
   whichever ancestor has overflow hidden. z-index above the modal backdrop for
   the same reason — the starboard and report fields are inside one. */
.sm-emoji-panel {
  position: fixed; z-index: 1200;
  width: min(21rem, calc(100vw - 1rem));
  background: var(--sm-bg-raised); border: 1px solid var(--sm-border-strong);
  box-shadow: 0 12px 32px var(--sm-shadow-3);
  display: flex; flex-direction: column;
  max-height: min(60vh, 24rem);
}
.sm-emoji-panel[hidden] { display: none; }
/* Phone layout: the script docks the panel to the bottom of the visible area
   and sets its width inline, because there is no room beside a button for it
   and an anchored panel moved every time the keyboard or address bar did. Taller
   than the anchored one — a docked sheet has the screen to itself, and 60vh of a
   phone with the keyboard up is about four rows of emoji. */
.sm-emoji-panel.sm-emoji-docked { max-height: min(70vh, 26rem); }
.sm-emoji-panel.sm-emoji-docked .sm-emoji-item { width: 2.4rem; height: 2.4rem; font-size: 1.25rem; }
.sm-emoji-head {
  display: flex; align-items: center; gap: 0.4rem;
  padding: 0.5rem; border-bottom: 1px solid var(--sm-border); flex-shrink: 0;
}
.sm-emoji-head .form-control { flex: 1 1 auto; min-width: 0; }
.sm-emoji-body { overflow-y: auto; overscroll-behavior: contain; padding: 0.5rem; }
.sm-emoji-group + .sm-emoji-group { margin-top: 0.6rem; }
.sm-emoji-group[hidden] { display: none; }
.sm-emoji-group-label {
  font-size: 0.68rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em;
  color: var(--sm-text-dim); margin-bottom: 0.3rem;
}
/* How many are actually on screen, which the script keeps in step with the
   search. It exists because "the picker is not showing all our emoji" is only
   answerable if the panel states a number — a grid that simply ends looks the
   same whether it is complete or truncated. */
.sm-emoji-count {
  margin-left: 0.35rem; padding: 0 0.3rem;
  background: var(--sm-bg-hover); border: 1px solid var(--sm-border);
  color: var(--sm-text-dim); font-weight: 500; letter-spacing: 0;
}
/* auto-fill rather than fixed columns, so the grid reflows in the narrow panel
   instead of overflowing it. */
.sm-emoji-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(2rem, 1fr)); gap: 2px; }
/* Scoped under .main is not enough — the panel is appended in page content but
   these are bare buttons, so they carry their own reset. */
.sm-emoji-panel .sm-emoji-item {
  background: none; border: 1px solid transparent; border-radius: 0;
  min-width: 0; min-height: 0; width: 2rem; height: 2rem; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 1.1rem; line-height: 1; cursor: pointer; color: inherit;
}
.sm-emoji-panel .sm-emoji-item[hidden] { display: none; }
.sm-emoji-panel .sm-emoji-item:hover { background: var(--sm-bg-hover); border-color: var(--sm-border-strong); }
.sm-emoji-panel .sm-emoji-item:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: -1px; }
.sm-emoji-panel .sm-emoji-item img { width: 22px; height: 22px; object-fit: contain; }
.sm-emoji-empty { margin: 0; padding: 0.4rem 0.1rem; font-size: 0.8rem; color: var(--sm-text-dim); }
/* Field plus its picker button as one unit, so the button cannot wrap away from
   the input it writes into. */
.sm-emoji-field { display: flex; align-items: center; gap: 0.4rem; }
.sm-emoji-field > .form-control { flex: 1 1 auto; min-width: 0; }

/* ============== Alert modal: attached media ==============
   The thing the watcher flagged, shown in the modal so reviewing a media alert
   does not mean opening the message in Discord — where it is not blurred at all.

   BLURRED IS THE DEFAULT, AND IT IS THE UNMODIFIED STATE. `.is-blurred` is
   present on first render and script removes it; the rule is not "add blur on
   load". That ordering matters: if the modal script fails, the media is never
   inserted, and if this class were inverted a script error would leave flagged
   media on screen unblurred.

   Some categories never reach here at all — see NEVER_RENDER_CATEGORY_TERMS in
   dashboard/pages/detailModal.js. No <img> is created for those, so nothing is
   fetched and there is no reveal control. */
.sm-modal-media { margin-bottom: 0.75rem; }
.sm-modal-media[hidden] { display: none; }
.sm-modal-media-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  margin-bottom: 0.4rem;
  font-size: 0.78rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em;
  color: var(--sm-text-dim);
}
.sm-modal-media-grid {
  display: flex; flex-wrap: wrap; gap: 0.5rem;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border); padding: 0.5rem;
}
.sm-modal-media-grid img {
  max-width: 100%; max-height: 15rem; width: auto; object-fit: contain;
  background: var(--sm-bg-raised);
}
/* `filter` alone leaves the edge pixels legible, so the image is scaled up
   slightly and clipped — otherwise a thumbnail's outer band stays readable
   however much blur is applied. */
.sm-modal-media-grid.is-blurred { overflow: hidden; }
.sm-modal-media-grid.is-blurred img {
  filter: blur(22px);
  transform: scale(1.08);
  /* Stops a click landing on the image (and its context menu) while hidden. */
  pointer-events: none;
}
.sm-modal-media-link { font-size: 0.85rem; }
.sm-modal-media-grid.is-blurred .sm-modal-media-link { filter: none; }
.sm-modal-media-blocked {
  margin: 0; padding: 0.6rem 0.75rem;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-danger);
  border-left-width: 3px;
  font-size: 0.82rem; color: var(--sm-text-muted);
}

/* Field list: label column sized to content, values wrap. */
.sm-modal-fields { display: grid; grid-template-columns: minmax(6rem, max-content) 1fr; gap: 0.4rem 1rem; margin: 0; font-size: 0.85rem; align-items: baseline; }
.sm-modal-fields dt { color: var(--sm-text-dim); font-weight: 500; display: flex; align-items: baseline; gap: 0.4rem; }
.sm-modal-fields dt .label-icon { width: 1rem; flex-shrink: 0; text-align: center; opacity: 0.8; }
/* Separates what the watcher saw from what has been done about it. Spans both
   grid columns, since the pair either side of it are unrelated. */
.sm-modal-fields-break { grid-column: 1 / -1; height: 1px; background: var(--sm-border); margin: 0.5rem 0; }
.sm-modal-quote[hidden] { display: none; }
.sm-modal-fields dd { margin: 0; color: var(--sm-text); min-width: 0; overflow-wrap: anywhere; }

/* ---- Rich field values ---------------------------------------------------
   A "who" row: avatar, name, id. The avatar is what makes the row answer the
   question at a glance — an 18-digit snowflake does not. */
.sm-modal-fields dd.sm-modal-user {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  flex-wrap: wrap;
  /* The row sits on the field grid's baseline alignment, which puts a 22px
     avatar's baseline below the label's. Centre it against its own line box
     instead, so the label and the name read as one row. */
  align-self: center;
}
.sm-modal-avatar {
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  flex-shrink: 0;
  object-fit: cover;
  background: var(--sm-bg-hover);
}
.sm-modal-user-name { font-weight: 600; }

/* Ids and quoted content, fenced. Both are read verbatim or copied, and as
   prose an id wrapped mid-number and a quote was indistinguishable from the
   label beside it. Tight vertical padding: these sit in a field grid, and the
   default pre/code box was tall enough to break the row rhythm. */
.sm-modal-code-inline {
  padding: 0.05rem 0.3rem;
  border-radius: 0;
  background: var(--sm-pill-bg);
  color: var(--sm-text-dim);
  font-size: 0.8em;
  line-height: 1.4;
  white-space: nowrap;
}
.sm-modal-code {
  margin: 0;
  padding: 0.4rem 0.55rem;
  max-height: 14rem;
  overflow: auto;
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border);
  border-radius: 0;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  font-size: 0.82rem;
  line-height: 1.45;
}
.sm-modal-code code { background: none; padding: 0; color: var(--sm-text); }

/* Pills and tags inside the modal inherit the page's badge rules, which are
   sized for a table cell with more room above and below than a field row has.
   Without this a single tag makes its row noticeably taller than its
   neighbours, and a body of six fields stair-steps. */
.sm-modal-fields dd .badge,
.sm-modal-foot .badge {
  padding: 0.1rem 0.4rem;
  font-size: 0.75rem;
  line-height: 1.35;
  vertical-align: baseline;
}

@media (max-width: 560px) {
  .sm-modal-fields { grid-template-columns: 1fr; gap: 0.15rem; }
  .sm-modal-fields dd { margin-bottom: 0.5rem; }
}

/* Value carrying extra precision in its title attribute (rounded months → exact
   days, truncated counts → totals). The dotted underline is the only signal a
   hover is worth trying. */
.main .sm-help { border-bottom: 1px dotted var(--sm-text-dim); cursor: help; }

/* ============== Copilot drawer ==============
   Reachable from the topbar on every page. A right-hand drawer rather than a
   modal: the point is to ask about what is on screen, so the page stays visible
   and unblocked behind it. The full-page Copilot remains for longer sessions. */
/* The notification bell and its panel.
   Anchored to the button rather than the topbar so it stays under the bell at
   every width, and capped in height because a hundred kept rows would otherwise
   run off the bottom of the screen with no way to scroll them. */
.sm-notif-wrap { position: relative; display: inline-flex; }
.topbar .sm-notif-open { position: relative; }
.sm-notif-badge {
  position: absolute; top: 0; right: 0;
  min-width: 15px; height: 15px; padding: 0 3px;
  display: grid; place-items: center;
  background: var(--sm-danger); color: var(--sm-on-solid);
  font-family: var(--sm-font-mono); font-size: 0.6rem; font-weight: 600; line-height: 1;
}
/* Anchored to .topbar, which is already the positioning context the account
   menu uses. It used to be anchored to .sm-notif-wrap — i.e. to the bell — and
   that is what put the panel INSIDE .user-menu-panel, a dropdown that is
   display:none below 768px until it is opened. A display:none ancestor hides
   everything under it, position:fixed included, so the mobile modal could never
   be shown; and the bell's handler closes that menu on the way to opening the
   panel, which guaranteed it. The panel is now a sibling of the menu and hangs
   off the bar instead. 1.5rem is .topbar's own padding, so its right edge lines
   up with the controls above it rather than with the window. */
.sm-notif-panel {
  position: absolute; top: calc(100% + 0.4rem); right: 1.5rem; z-index: 1090;
  width: min(24rem, calc(100vw - 1.5rem));
  background: var(--sm-bg-raised); border: 1px solid var(--sm-border-strong);
  display: flex; flex-direction: column; max-height: min(70vh, 30rem);
}
.sm-notif-panel[hidden] { display: none; }
.sm-notif-head {
  display: flex; align-items: center; justify-content: space-between; gap: 0.5rem;
  padding: 0.6rem 0.75rem; border-bottom: 1px solid var(--sm-border);
  font-size: 0.85rem; font-weight: 600; color: var(--sm-text-muted); flex-shrink: 0;
}
.sm-notif-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; overscroll-behavior: contain; }
/* Row = the link (or static text) plus its own dismiss form beside it. The
   divider lives on the row rather than the link, so it spans the dismiss button
   too instead of stopping short of it. */
.sm-notif { display: flex; align-items: stretch; border-bottom: 1px solid var(--sm-border); }
.sm-notif:last-child { border-bottom: none; }
.sm-notif-main { display: flex; gap: 0.6rem; padding: 0.6rem 0.75rem; text-decoration: none; flex: 1 1 auto; min-width: 0; }
.sm-notif a.sm-notif-main:hover { background: var(--sm-bg-hover); text-decoration: none; }
.sm-notif a.sm-notif-main:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: -1px; }
/* A tile rather than a bare glyph: at 0.85rem the four level icons were hard to
   tell apart at a glance, which is the only job they have. The tinted square
   gives the colour enough area to register. */
.sm-notif-icon {
  flex-shrink: 0; width: 1.6rem; height: 1.6rem; margin-top: 0.1rem;
  display: grid; place-items: center; font-size: 0.8rem; line-height: 1;
  border: 1px solid currentColor; background: var(--sm-bg-deep);
}
.sm-notif-error { color: var(--sm-danger); }
.sm-notif-warning { color: var(--sm-warning); }
.sm-notif-success { color: var(--sm-success); }
.sm-notif-info { color: var(--sm-info); }
.sm-notif-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.sm-notif-title { font-size: 0.85rem; color: var(--sm-text); }
.sm-notif-count { font-family: var(--sm-font-mono); font-size: 0.7rem; color: var(--sm-text-dim); }
.sm-notif-detail { font-size: 0.78rem; color: var(--sm-text-muted); overflow-wrap: anywhere; }
.sm-notif-time { font-family: var(--sm-font-mono); font-size: 0.68rem; color: var(--sm-text-dim); }

/* Per-row dismiss. The panel lives outside .main, and every button rule in this
   stylesheet is scoped under .main, so this control has to carry its own
   styling — unstyled it rendered as a default grey browser button. */
.sm-notif-dismiss-form { margin: 0; display: flex; flex-shrink: 0; }
.sm-notif-dismiss {
  background: none; border: none; border-left: 1px solid var(--sm-border);
  color: var(--sm-text-faint); cursor: pointer;
  width: 2.25rem; padding: 0; font-size: 0.8rem; line-height: 1;
  display: flex; align-items: center; justify-content: center;
}
/* Danger on hover, because it deletes the row for everyone — see
   dismissNotification() in util/notifications.js. */
.sm-notif-dismiss:hover { background: var(--sm-bg-hover); color: var(--sm-danger); }
.sm-notif-dismiss:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: -1px; color: var(--sm-danger); }

.sm-notif-empty {
  display: flex; align-items: center; gap: 0.5rem;
  padding: 1.25rem 0.75rem; justify-content: center;
  font-size: 0.85rem; color: var(--sm-text-dim);
}
/* The head's "Mark all read" is in the same position: outside .main, so it needs
   its own box or it inherits the browser default. */
.sm-notif-head form { margin: 0; }
.sm-notif-head .btn {
  background: none; border: 1px solid var(--sm-border-strong); color: var(--sm-text-muted);
  font-size: 0.72rem; font-weight: 500; padding: 0.2rem 0.5rem; border-radius: 0;
  cursor: pointer; line-height: 1.3;
}
.sm-notif-head .btn:hover { background: var(--sm-bg-hover); color: var(--sm-text); border-color: var(--sm-accent); }
.sm-notif-head .btn:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 1px; }

/* The dimmed sheet behind the notification modal. Inert above 768px, where
   the panel is still a dropdown hanging off the bell and a backdrop would be
   dimming the page for a menu. */
.sm-notif-backdrop { display: none; }

/* MOBILE: notifications are a modal, not a dropdown.

   As a dropdown on a phone it had nowhere good to hang. Anchored to the bell it
   started at a negative x — the bell is not the last thing in the bar — and
   anchored to the topbar it spanned the gutters but pushed the page content
   down behind it. It is now behind the account menu as well, which is a
   dropdown opening out of a dropdown.

   A modal instead: centred, over a dimmed page, dismissed by the backdrop, by
   Escape, or by the bell. Same surface treatment as the detail modal
   (.sm-modal-card), so the dashboard has one dialog look rather than two. */
@media (max-width: 768px) {
  .sm-notif-backdrop {
    display: block; position: fixed; inset: 0; z-index: 1085;
    background: var(--sm-backdrop);
  }
  .sm-notif-backdrop[hidden] { display: none; }
  .sm-notif-wrap { position: static; }
  .sm-notif-panel {
    position: fixed; z-index: 1090;
    top: 50%; left: 50%; right: auto; transform: translate(-50%, -50%);
    width: min(26rem, calc(100vw - 1.5rem)); max-width: none;
    max-height: min(80vh, 34rem);
    border: 1px solid var(--sm-border-strong);
    box-shadow: 0 18px 48px var(--sm-shadow-3);
  }
  /* Comfortably tappable, and the head stops being a thin strip of small type
     when it is the title bar of a dialog rather than the lip of a menu. */
  .sm-notif-head { padding: 0.75rem 0.9rem; font-size: 0.9rem; }
  .sm-notif-main { padding: 0.7rem 0.85rem; }
  .sm-notif-dismiss { width: 2.75rem; }
}

/* Colour mode. Sized and coloured exactly like the Copilot launcher next to it
   — in the bar these are a row of glyphs and one of them looking different
   would read as a different kind of thing.

   The icon itself is swapped in the markup (moon, sun, monitor) rather than by
   CSS, because the server has to render the right one for a reader with no
   scripting at all. */
.topbar .sm-theme-toggle { background: none; border: none; color: var(--sm-text-muted); padding: 0.35rem 0.5rem; border-radius: 0; font-size: 1.05rem; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; }
.topbar .sm-theme-toggle:hover { color: var(--sm-accent-cyan); background: var(--sm-bg-raised); }
.topbar .sm-theme-toggle:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }
/* The form contributes no box of its own in the bar either. */
.user-bar .sm-theme-form { display: contents; }

.topbar .copilot-launch { background: none; border: none; color: var(--sm-text-muted); padding: 0.35rem 0.5rem; border-radius: 0; font-size: 1.05rem; line-height: 1; cursor: pointer; display: inline-flex; align-items: center; }
.topbar .copilot-launch:hover { color: var(--sm-accent-cyan); background: var(--sm-bg-raised); }
.topbar .copilot-launch:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }

.sm-copilot { position: fixed; top: 0; right: 0; bottom: 0; width: min(420px, 100vw); background: var(--sm-bg-raised); border-left: 1px solid var(--sm-border-strong); box-shadow: -12px 0 32px var(--sm-shadow-3); z-index: 1075; display: flex; flex-direction: column; }
.sm-copilot[hidden] { display: none; }
.sm-copilot-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem 0.9rem; border-bottom: 1px solid var(--sm-border); flex-shrink: 0; }
.sm-copilot-head h5 { margin: 0; font-size: 0.9rem; font-weight: 600; }
/* The drawer lives outside .main, and every button rule in this stylesheet is
   scoped under .main — so these two controls have to carry their own styling or
   they fall back to browser defaults (a white box) and a bare underlined link. */
.sm-copilot-action {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  height: 1.75rem;
  padding: 0 0.55rem;
  background: transparent;
  border: 1px solid var(--sm-border-strong);
  border-radius: 0;
  color: var(--sm-text-muted);
  font-family: inherit;
  font-size: 0.78rem;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
}
.sm-copilot-action:hover,
.sm-copilot-action:active { color: var(--sm-text); background: var(--sm-bg-hover); text-decoration: none; }
.sm-copilot-action:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: 2px; }
.sm-copilot-close { width: 1.75rem; padding: 0; font-size: 0.85rem; }
.sm-copilot-scope { font-size: 0.72rem; color: var(--sm-text-dim); display: block; margin-top: 1px; }
.sm-copilot-thread { flex: 1; overflow-y: auto; padding: 0.9rem; display: flex; flex-direction: column; gap: 0.55rem; }
.sm-copilot-bubble { max-width: 92%; padding: 0.5rem 0.65rem; font-size: 0.85rem; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; border: 1px solid var(--sm-border); background: var(--sm-bg-deep); }
.sm-copilot-bubble.me { align-self: flex-end; background: var(--sm-accent); color: var(--sm-accent-fg); border-color: transparent; }
.sm-copilot-bubble.err { border-color: var(--sm-danger); color: var(--sm-danger); }
.sm-copilot-form { display: flex; gap: 0.5rem; padding: 0.75rem; border-top: 1px solid var(--sm-border); flex-shrink: 0; }
.sm-copilot-form textarea { flex: 1; min-height: 2.4rem; max-height: 7rem; resize: none; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); color: var(--sm-text); font-family: inherit; font-size: 0.875rem; padding: 0.45rem 0.55rem; }
.sm-copilot-form textarea:focus { outline: 1px solid var(--sm-accent); outline-offset: -1px; }
/* The Copilot send button lives outside `.main`, so it misses the button rules
   above. Pointed at the same tokens so it cannot drift from them again. */
.sm-copilot-form button { flex-shrink: 0; min-width: 3.5rem; min-height: 2.25rem; background: var(--sm-btn-bg); color: var(--sm-btn-fg); border: none; cursor: pointer; font-family: inherit; font-size: 0.875rem; }
.sm-copilot-form button:hover { background: var(--sm-btn-bg-hover); }
.sm-copilot-form button:disabled { opacity: 0.6; cursor: not-allowed; }

@media (max-width: 560px) {
  .sm-copilot { width: 100vw; }
}

/* Screen-reader-only. The Copilot composer used this class for its input label
   and it was never defined, so "Your question" rendered as visible text sitting
   on top of the placeholder. */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* ---- Copilot page tidy-up ---- */
/* The head reserved a wide empty middle and let the model/read-only chips run
   off the right edge, where the panel's overflow:hidden clipped them. Title
   takes the slack, chips sit tight to the right and wrap instead of clipping. */

/* Answers are prose with bullet lists; the markers were sitting outside the
   bubble's padding and the rows were cramped. */
.cp-bubble ul, .cp-bubble ol { margin: 0.4rem 0 0.4rem 0; padding-left: 1.15rem; }
.cp-bubble li { margin: 0.15rem 0; }
.cp-bubble li::marker { color: var(--sm-text-dim); }
.cp-bubble p { margin: 0 0 0.5rem; }
.cp-bubble p:last-child, .cp-bubble ul:last-child, .cp-bubble ol:last-child { margin-bottom: 0; }
.cp-bubble code { background: var(--sm-bg-base); padding: 0.1em 0.35em; font-size: 0.875em; }


/* Row action cluster: inspect / approve / discard as square icon buttons in a
   table's actions column. Keeps them from wrapping mid-row and gives a
   consistent gap on Alerts and Reports, which share the same three controls. */
.main .sm-row-actions { display: inline-flex; align-items: center; gap: 0.35rem; flex-wrap: nowrap; }
.main .sm-row-actions form { margin: 0; display: inline-flex; }
/* The actions column never gives ground: it holds three fixed-width icon
   buttons, and letting it shrink is what pushed them onto two lines. */
.main .sm-col-actions { width: 1%; white-space: nowrap; }

/* A figure the reader might compare or copy.
   §9 has said "mono for numbers" since the first draft, but there was no way to
   say it — KPI values, feed times and member ids each carried their own
   font-family rule, and a number in a table cell simply got the UI face. That
   is what left ranked tables reading as ragged columns: proportional digits are
   different widths, so 1,024 and 998 do not line up under each other however
   they are aligned. `tabular-nums` fixes the width even where the mono face has
   not loaded yet. */
.main .sm-num { font-family: var(--sm-font-mono); font-variant-numeric: tabular-nums; }
/* The rank itself is scaffolding — it orders the rows, it is not the figure you
   came to read. Dimmed and narrowed so the measure column stays the loud one. */
.main .sm-rank { width: 1%; white-space: nowrap; color: var(--sm-text-dim); }

/* A quoted excerpt of user-written text in a table cell: message content, a
   monitor match, a filter pattern. `pre-wrap` because the line breaks the
   author typed are part of what you are reading, `break-word` because a URL or
   an unbroken keysmash would otherwise widen the whole table.

   The cap is a measure, not a layout: past ~60ch the eye loses the line on the
   way back, and one long message was making every other column narrow. It was
   four inline copies before this — 520px on Monitor's hits table and its
   pattern cell, no cap at all on ModMail's snippet or the Monitor pane, so the
   same kind of cell wrapped differently on each. */
.main .sm-excerpt {
  display: inline-block;
  white-space: pre-wrap;
  word-break: break-word;
  max-width: 32rem;
}

/* Review-queue status pills. Open is the only one that should catch the eye —
   the other two are records of work already done. */
.main .badge.sm-status-open { color: var(--sm-warning-text); background: var(--sm-warning-soft); }
.main .badge.sm-status-done { color: var(--sm-success-text); background: var(--sm-success-soft); }
.main .badge.sm-status-muted { color: var(--sm-text-dim); background: var(--sm-wash-2); }
/* The other two states the Discord alert embed already had, so the queue and
   the channel describe the same item the same way: blue for "a case came out
   of this", red for an open alert the watcher judged urgent. */
.main .badge.sm-status-actioned { color: var(--sm-info-text); background: var(--sm-info-soft); }
.main .badge.sm-status-critical { color: var(--sm-danger-text); background: var(--sm-danger-soft); }
/* Broken, as distinct from merely unresolved: a feed that is failing needs a
   different colour from one that is simply waiting for its first poll. */
.main .badge.sm-status-err { color: var(--sm-danger-text); background: var(--sm-danger-soft); }
.main a.badge.sm-status-done { text-decoration: none; }

/* Every button that pairs an icon with a word gets a real gap between them.
   Buttons are already inline-flex with centred content, so a gap here is
   reliable regardless of how the page wrote the markup — some use
   `<i class="label-icon">`, some a bare `<i>`, some put the icon after the
   text. `gap` handles all three without touching a single page.
   Icon-only buttons are excluded: with no text there is nothing to space, and
   a gap would push the glyph off centre. */
.main button:not(.sm-icon-btn),
.main .btn:not(.sm-icon-btn),
.main .btn-sm:not(.sm-icon-btn) { gap: 0.45rem; }
/* The icons' own margins would then double the gap. */
.main button:not(.sm-icon-btn) > .label-icon,
.main .btn:not(.sm-icon-btn) > .label-icon,
.main .btn-sm:not(.sm-icon-btn) > .label-icon { margin-right: 0; }

/* ---- Copilot rich answers ----
   Bars and tables rendered from the model's own text (see format() in
   dashboard/pages/copilot.js). Squared and on theme tokens, following the chart
   rules in docs/UI_REFERENCE.md — a bar here is a chart, just drawn in HTML. */
.cp-bubble .cp-h { font-weight: 600; color: var(--sm-text); margin: 0.6rem 0 0.3rem; }
.cp-bubble .cp-h:first-child { margin-top: 0; }
.cp-bubble .cp-gap { height: 0.5rem; }

.cp-bars { display: flex; flex-direction: column; gap: 0.3rem; margin: 0.5rem 0; }
.cp-bar-row { display: grid; grid-template-columns: minmax(5rem, 10rem) 1fr auto; align-items: center; gap: 0.6rem; font-size: 0.85rem; }
.cp-bar-label { color: var(--sm-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-bar-track { background: var(--sm-bg-base); height: 0.85rem; border: 1px solid var(--sm-border); }
/* Squared fill, no radius — same rule as the Chart.js bars. */
.cp-bar-fill { display: block; height: 100%; background: var(--sm-accent); }
.cp-bar-value { font-family: var(--sm-font-mono); font-size: 0.78rem; color: var(--sm-text); white-space: nowrap; }

.cp-table-wrap { overflow-x: auto; margin: 0.5rem 0; }
.cp-table { border-collapse: collapse; width: 100%; font-size: 0.85rem; }
.cp-table th, .cp-table td { border: 1px solid var(--sm-border); padding: 0.35rem 0.55rem; text-align: left; vertical-align: top; }
.cp-table th { background: var(--sm-bg-deep); color: var(--sm-text-muted); font-weight: 600; white-space: nowrap; }

/* The bubble sets pre-wrap for plain prose; block children must not inherit it
   or the generated markup gains phantom blank lines. */
.cp-bubble .cp-bars, .cp-bubble .cp-table-wrap, .cp-bubble .cp-list { white-space: normal; }

/* What a level GRANTS, under what the role IS, in the Levels reward table.
   Two lines and a count — the full list is in the row's dialog, and a cell
   holding eight bullets makes every other row in the table unreadable. */
.main .sm-perk-list {
  list-style: none;
  margin: 0.2rem 0 0;
  padding: 0;
  font-size: 0.76rem;
  color: var(--sm-text-dim);
}
.main .sm-perk-list li { position: relative; padding-left: 0.7rem; line-height: 1.35; }
/* A middot rather than a disc: the rows are short and a bullet glyph at this
   size reads as a smudge next to the role pill above it. */
.main .sm-perk-list li::before { content: "\00b7"; position: absolute; left: 0.15rem; }

/* ---- Field width scale ----
   Pages had 38 inline max-widths across 12 arbitrary values (120px…520px), so
   two selects doing the same job on different pages — or in the same row — were
   rarely the same size. Four sanctioned widths replace them; anything needing a
   different one is a sign the layout, not the field, is wrong.

   rem, not px, so fields scale with the user's text size. */
/*
  WHY THESE CARRY `!important` AND A `min()`.

  Two blanket rules above force `max-width: 100% !important` onto every control
  under `.main` below 992px. They exist to defeat the inline `style="width:
  170px"` attributes scattered across the pages, and an inline declaration only
  loses to `!important` — so the blanket had to have it. The cost was that it
  also outranked this scale, which is a class and not an inline style: on any
  phone or tablet all ninety fields carrying one of these lost their cap and
  stretched the full width of their column. A two-digit XP floor rendered as a
  box the width of the screen, which is what "the input fields are too wide"
  looked like on the Levels page.

  So the scale says `!important` back, and wins by being later in the file.

  `min(Xrem, 100%)` rather than a bare `Xrem`, because the blanket rules were
  also doing something genuinely necessary: keeping a field from being wider
  than the space it sits in. A 32rem `sm-field-xl` in a narrow column would
  overflow the card on a phone. `min()` keeps the designed width where there is
  room and the container's width where there is not — which is what the
  blanket was for, without discarding the four sanctioned sizes to get it.
*/
.main .sm-field-sm { max-width: min(10rem, 100%) !important; }   /* short: counts, minutes, hex */
.main .sm-field-md { max-width: min(15rem, 100%) !important; }   /* default: most selects */
.main .sm-field-lg { max-width: min(20rem, 100%) !important; }   /* channel / role pickers */
.main .sm-field-xl { max-width: min(32rem, 100%) !important; }   /* long text, URLs */

/* `.form-control-sm` and `.form-select-sm` are used on well over a hundred
   controls but were never defined, so "small" rendered identically to normal.
   Given every input already shares the 2.25rem button-matched height, defining
   them as genuinely smaller now would resize half the dashboard at once. They
   are instead made explicit no-ops, so the next person greps this and finds an
   answer rather than assuming the styling went missing. */
.main .form-control-sm, .main .form-select-sm { /* intentionally identical to .form-control */ }

/* Typing indicator while the Copilot is working.
   A tool-calling turn can take several seconds, and a motionless "…" reads as a
   stalled request — the movement is what says "still going". Used by both the
   full page and the navbar drawer, so the two behave the same. */
.cp-typing, .sm-copilot-bubble.cp-typing {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  min-height: 1.4rem;
}
.cp-typing span {
  width: 0.4rem;
  height: 0.4rem;
  background: var(--sm-text-dim);
  /* Round here is deliberate and sanctioned — Copilot's typing dots are one of
     the three documented exceptions to the squared rule (avatars, spinners,
     these dots). */
  border-radius: 50%;
  animation: cp-typing-bounce 1.1s infinite ease-in-out;
}
.cp-typing span:nth-child(2) { animation-delay: 0.15s; }
.cp-typing span:nth-child(3) { animation-delay: 0.3s; }

@keyframes cp-typing-bounce {
  0%, 70%, 100% { opacity: 0.32; transform: translateY(0); }
  35%           { opacity: 1;    transform: translateY(-0.22rem); }
}

/* Respect a reduced-motion preference: keep the "still working" signal as a
   pulse rather than removing the feedback entirely. */
@media (prefers-reduced-motion: reduce) {
  .cp-typing span { animation: cp-typing-fade 1.4s infinite ease-in-out; }
  @keyframes cp-typing-fade { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
}

/* ============== Collapsible panels (sm-panel) ==============
   The shared disclosure panel — see dashboard/pages/panelKit.js for when to
   collapse something and why it is built on <details>.

   Four pages had already grown their own version of this and each hid the
   browser's disclosure triangle a slightly different way. One rule set now,
   with a chevron that rotates instead: the native marker cannot be styled
   consistently across engines, and a triangle that sits outside the padding is
   the reason every one of those pages overrode it.

   The summary is a flex row rather than a heading with things floated onto it,
   because the note in the middle is the part that has to give way first. On a
   narrow screen the title and the chevron keep their size and the note
   truncates — the opposite arrangement wraps a panel header onto three lines
   and buries the control it is meant to label. */
.main .sm-panel > summary {
  display: flex; align-items: center; gap: 0.5rem;
  cursor: pointer; list-style: none;
  /*
    THE FLOOR THAT LETS THE ELLIPSIS ACTUALLY HAPPEN.

    `.sm-panel-note` has had `min-width: 0` and `text-overflow: ellipsis` for as
    long as it has existed, and on the Levels page it still refused to truncate:
    the XP curve note printed "5·level² + 50·level + 100 XP per level
    (MEE6-compatible)" in full on a phone and took the card sideways with it.

    A flex ITEM cannot shrink below its content unless it says so — which the
    note does — but only within a container that is itself allowed to be
    narrower than its contents. `summary` and the `details` around it are block
    boxes whose default `min-width: auto` resolves to min-content, so the row
    had a floor set by the longest thing in it and the note never reached the
    width where an ellipsis applies. Zeroing the floor here and on the panel is
    what makes the note's own shrink rule take effect.
  */
  min-width: 0;
  font-size: 0.9rem; font-weight: 600; letter-spacing: 0.01em;
  color: var(--sm-text-muted);
  /* Comfortably past the 44px touch target once the card's own padding is in. */
  min-height: 1.9rem;
}
.main .sm-panel > summary::-webkit-details-marker { display: none; }
.main .sm-panel > summary::marker { content: ""; }
.main .sm-panel > summary:hover { color: var(--sm-text); }
/* Focus lands on the summary, and it is the only way to reach the panel by
   keyboard, so the ring has to be obvious rather than inherited from a link. */
.main .sm-panel > summary:focus-visible {
  outline: 2px solid var(--sm-accent); outline-offset: 2px; color: var(--sm-text);
}
.main .sm-panel > summary > .sm-panel-icon {
  width: 1.1rem; flex-shrink: 0; text-align: center; opacity: 0.85;
}
/* Shrinks only once the note has already given up everything it has.
   `flex-shrink: 0` was absolute, so a summary carrying a long title, a note, a
   badge and an action had no slack left anywhere and grew past its card — the
   other half of the Levels sideways scroll. The note is still the first to go
   (it is `flex: 1 1 auto` with an ellipsis); this is the second, and only at
   widths where there is genuinely nothing else to give. */
/*
  SHRINKS AT EVERY WIDTH NOW, not only below 576px.

  The gate was the phone breakpoint, which assumed the squeeze only happens
  on a phone. It does not: a tablet, a split window, and a phone asking for
  the desktop site all land between 577 and 991px — where the Bootstrap `md`
  columns are still in force and these rows are at their most crowded. There
  the title could not give way at all.

  The note is still the first thing to go (it is `flex: 1 1 auto` with an
  ellipsis) and the actions never shrink; this is second in line, and gives up
  width only once the note has none left.
*/
.main .sm-panel-title {
  flex-shrink: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The current state, not the purpose — "Everyone, anywhere", "3 roles". It is
   what makes a closed panel readable, so it is the one part allowed to shrink
   and clip rather than push the chevron off the row. */
.main .sm-panel-note {
  min-width: 0; flex: 1 1 auto;
  font-weight: 400; font-size: 0.85rem; color: var(--sm-text-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.main .sm-panel { min-width: 0; }
.main .sm-panel-badge { flex-shrink: 0; margin-left: auto; }
.main .sm-panel-note + .sm-panel-badge { margin-left: 0; }
/* Icon buttons pinned to the right of the summary, before the chevron. Never
   shrink and never wrap: these are the panel's actions, and a control that has
   been squeezed to half a button is worse than a clipped note beside it — which
   is why .sm-panel-note is the one that gives way. Clicking one does not toggle
   the panel; that is panelActionsScript() in panelKit.js. */
.main .sm-panel-actions {
  display: inline-flex; align-items: center; gap: 0.3rem;
  flex-shrink: 0; margin-left: auto;
}
.main .sm-panel-note + .sm-panel-actions,
.main .sm-panel-badge + .sm-panel-actions { margin-left: 0.5rem; }
/* The summary is a pointer target as a whole, so a button inside it has to say
   for itself that it is one — otherwise the only cue is the hover background. */
.main .sm-panel-actions .btn { cursor: pointer; }
/* Disabled means "connect the platform first", not "this does nothing". Keep it
   legible rather than ghosted away: the tooltip on it is the explanation, and
   nobody hovers something they read as absent. */
.main .sm-panel-actions .btn:disabled { opacity: 0.45; cursor: not-allowed; }
/* …and the tooltip has to hang off this wrapper rather than the button,
   because a disabled control never receives the hover that renders one. The
   span is the hover target; the button inside it keeps looking disabled. */
.main .sm-disabled-wrap { display: inline-flex; cursor: not-allowed; }
.main .sm-panel-caret {
  flex-shrink: 0; margin-left: auto; font-size: 0.75rem; color: var(--sm-text-dim);
  transition: transform 0.15s ease;
}
.main .sm-panel-note ~ .sm-panel-caret,
.main .sm-panel-actions + .sm-panel-caret,
.main .sm-panel-badge + .sm-panel-caret { margin-left: 0.5rem; }
.main .sm-panel[open] > summary > .sm-panel-caret { transform: rotate(180deg); }
.main .sm-panel[open] > summary { color: var(--sm-text); }
/* An open panel drops its note: the contents say it better than a summary can,
   and leaving it there reads as a stale duplicate of what is on screen. */
.main .sm-panel[open] > summary > .sm-panel-note { display: none; }
/* …which strands whatever followed it on the left.
   `.sm-panel-note + .sm-panel-badge` drops the badge's `margin-left: auto`,
   because with a note present the note is the flexible element doing the
   pushing. An adjacent-sibling selector still matches a `display: none`
   sibling, so the moment a panel opened the note vanished and the badge — the
   status pill, or a help button — slid up against the title instead of staying
   at the right-hand end. Give the spacing back once there is no note to do it. */
.main .sm-panel[open] > summary > .sm-panel-badge,
.main .sm-panel[open] > summary > .sm-panel-actions { margin-left: auto; }
.main .sm-panel[open] > summary > .sm-panel-badge + .sm-panel-actions { margin-left: 0.5rem; }
.main .sm-panel-body { padding-top: 0.85rem; }
.main .sm-panel-body > :last-child { margin-bottom: 0; }

/* A panel nested inside another: no card chrome of its own, just a rule to
   separate it from the one above. Nesting cards inside cards produces a stack
   of boxes with three different border colours and no obvious hierarchy. */
/* A read-only block of template text — the intro template as another panel
   shows it. A <pre>, so the lines stay lines, in the page face rather than
   the mono one: it is prose a member fills in, not code. */
.main .sm-template-preview {
  margin: 0; padding: 0.6rem 0.8rem; white-space: pre-wrap; overflow-wrap: anywhere;
  font-family: inherit; font-size: 0.86rem; line-height: 1.5; color: var(--sm-text);
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border);
}
/* A fieldset used for its one disabled attribute and nothing else: the
   browser's default border, padding and indent drew a box inside the panel. */
.main .sm-fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
/* A panel's own Save, under a rule, so it reads as the panel's foot and not
   as one more field. */
.main .sm-panel-foot { margin-top: 1rem; padding-top: 0.85rem; border-top: 1px solid var(--sm-border); }
/* A switch standing in for the date picker in a Stats page header. */
.main .stats-header-switch { display: inline-flex; align-items: center; gap: 0.4rem; padding-left: 2.4rem; min-height: 2.1rem; }
.main .stats-header-switch .form-check-label { color: var(--sm-text-muted); }
.main .sm-panel-sub { border-top: 1px solid var(--sm-border); }
.main .sm-panel-sub:first-of-type { border-top: 0; }
.main .sm-panel-sub > summary { padding: 0.55rem 0; }
.main .sm-panel-sub > .sm-panel-body { padding-bottom: 0.85rem; }

@media (prefers-reduced-motion: reduce) {
  .main .sm-panel-caret { transition: none; }
}

/* ============== Discord composer ==============
   The toolbar, the source field and the preview beside it.
   See dashboard/pages/markdownEditor.js.

   UNSCOPED, like the emoji picker and for the same reason: these live inside a
   modal as often as inside a card, so `.main` is not a reliable ancestor. The
   buttons carry their own reset rather than inheriting one.

   EVERY COLOUR IS A TOKEN. The preview is a rendering of a Discord message and
   the temptation is to paint it in Discord's own greys, which would then be a
   dark patch on a light dashboard. It is drawn in this dashboard's palette
   instead: what is being previewed is the STRUCTURE — which words are bold,
   where the header falls, what the spoiler hides — and that reads in any
   theme. */
.sm-md { display: block; }

.sm-md-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.15rem;
  padding: 0.3rem; margin-bottom: -1px;
  background: var(--sm-bg-deep);
  border: 1px solid var(--sm-border-strong);
}
/* Groups, separated by a rule rather than by space: the four families —
   character styles, headers, blocks, links and code — are what makes a strip of
   fourteen buttons scannable instead of a wall. */
.sm-md-group { display: flex; align-items: center; gap: 0.1rem; }
.sm-md-group + .sm-md-group {
  margin-left: 0.3rem; padding-left: 0.4rem;
  border-left: 1px solid var(--sm-border);
}
/* Scoped under .sm-md, and that is load-bearing. A toolbar button is a bare
   <button>, and `.main button` (0,1,1) paints every bare button inside .main as
   a primary: the brand fill, a 5.5rem minimum width and a 2.25rem minimum
   height. A plain `.sm-md-btn` (0,1,0) lost to it, so on any page where the
   composer sat in .main rather than a modal the toolbar became a wrapping grid
   of solid accent blocks (red on atc). `.sm-md .sm-md-btn` (0,2,0) outranks it,
   and every property that rule sets is set again here. */
.sm-md .sm-md-btn {
  background: transparent; border: 1px solid transparent; border-radius: 0;
  min-width: 1.9rem; min-height: 0; height: 1.9rem; padding: 0 0.35rem; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center; gap: 0.3rem;
  color: var(--sm-text-muted); font-family: inherit; font-size: 0.82rem; line-height: 1; cursor: pointer;
}
.sm-md .sm-md-btn:hover { background: transparent; }
.sm-md .sm-md-btn:hover:not(:disabled) { background: var(--sm-bg-hover); border-color: var(--sm-border-strong); color: var(--sm-text); }
.sm-md .sm-md-btn:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: -1px; }
.sm-md .sm-md-btn:disabled { opacity: 0.4; cursor: not-allowed; }
/* H1/H2/H3 and -# have no icon that says what they are, so they are set in the
   mono face at the size of the syntax they insert — the button IS the markdown. */
.sm-md-btn-text { font-family: var(--sm-font-mono); font-size: 0.72rem; font-weight: 600; letter-spacing: -0.02em; }
.sm-md .sm-md-toggle { margin-left: auto; color: var(--sm-text-dim); }
.sm-md .sm-md-toggle.is-on, .sm-md .sm-md-aa.is-on { color: var(--sm-accent-cyan); box-shadow: inset 0 -2px 0 var(--sm-accent); }
/* The emoji button is a plain toolbar button here, not the bordered field
   button it is beside a one-line input. Three classes, to outrank
   `.main .btn.sm-icon-btn` and `.main .btn-sm` it also carries. */
.sm-md .sm-md-toolbar .sm-emoji-open {
  background: transparent; border: 1px solid transparent; border-radius: 0;
  min-width: 1.9rem; width: auto; min-height: 0; height: 1.9rem; padding: 0 0.35rem; color: var(--sm-text-muted);
}
.sm-md .sm-md-toolbar .sm-emoji-open:hover:not(:disabled) { background: var(--sm-bg-hover); border-color: var(--sm-border-strong); color: var(--sm-text); }

/* ---- The compact composer (markdownEditorField({ compact: true }), the Inbox
   reply box). One slim row that scrolls sideways rather than wrapping into a
   block, 36px ghost buttons, the preview under the text and closed until asked
   for, and a field about four lines tall that grows. ---- */
.sm-md-compact .sm-md-toolbar { flex-wrap: nowrap; overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; gap: 0.1rem; padding: 0.15rem 0.25rem; }
.sm-md-compact .sm-md-toolbar::-webkit-scrollbar { display: none; }
.sm-md.sm-md-compact .sm-md-btn,
.sm-md.sm-md-compact .sm-md-toolbar .sm-emoji-open { min-width: 2.25rem; height: 2.25rem; }
.sm-md-compact .sm-md-format { display: flex; align-items: center; flex-shrink: 0; }
.sm-md-compact .sm-md-group + .sm-md-group { margin-left: 0.2rem; padding-left: 0.2rem; }
.sm-md-compact .sm-md-group + .sm-md-format { margin-left: 0.2rem; padding-left: 0.2rem; border-left: 1px solid var(--sm-border); }
.sm-md-compact .sm-md-group { flex-shrink: 0; }
.sm-md.sm-md-compact .sm-md-toggle { margin-left: 0; }
.sm-md.sm-md-compact .sm-md-aa { display: none; }
.sm-md-compact .sm-md-tools select { width: auto; max-width: 11rem; min-height: 2rem; padding-top: 0.2rem; padding-bottom: 0.2rem; font-size: 0.8rem; }
.sm-md-compact .sm-md-panes,
.sm-md-compact.is-preview-off .sm-md-panes { grid-template-columns: minmax(0, 1fr); }
.sm-md-compact .sm-md-input { min-height: 6.5rem; max-height: 18rem; field-sizing: content; resize: none; font-size: 0.85rem; }
.sm-md-compact .sm-md-preview { border-left: 1px solid var(--sm-border-strong); border-top: 1px dashed var(--sm-border-strong); min-height: 0; max-height: 14rem; }
.sm-md-compact .sm-md-foot { margin-top: 0.25rem; justify-content: flex-end; }
@media (max-width: 576px) {
  /* Formatting behind "Aa"; emoji, snippets and the preview stay out. */
  .sm-md.sm-md-compact .sm-md-aa { display: inline-flex; }
  .sm-md-compact:not(.is-format-open) .sm-md-format { display: none; }
  .sm-md-compact .sm-md-aa + .sm-md-group { margin-left: 0.2rem; padding-left: 0.2rem; border-left: 1px solid var(--sm-border); }
  .sm-md-compact .sm-md-toggle .sm-md-btn-text { display: none; }
}

/* Source and preview side by side on a desktop, stacked on anything narrower.
   Side by side is the arrangement that makes the preview do its job: the edit
   and its effect are on screen together, so a wrong `__` is seen while the
   caret is still in it rather than after a scroll. */
.sm-md-panes { display: grid; grid-template-columns: 1fr 1fr; gap: 0; align-items: stretch; }
.sm-md.is-preview-off .sm-md-panes { grid-template-columns: 1fr; }
.sm-md.is-preview-off .sm-md-preview { display: none; }
@media (max-width: 991.98px) {
  .sm-md-panes { grid-template-columns: 1fr; }
}
/* Mono, because this is source: the reader is looking at ** and __ and needs to
   see how many of each there are. The preview is the proportional half. */
.sm-md-input {
  border-radius: 0;
  font-family: var(--sm-font-mono); font-size: 0.82rem; line-height: 1.55;
  resize: vertical; min-height: 9rem;
}
.sm-md-preview {
  border: 1px solid var(--sm-border-strong); border-left: 0;
  background: var(--sm-bg-raised);
  padding: 0.5rem 0.7rem 0.7rem;
  overflow: auto; max-height: 26rem; min-height: 9rem;
}
@media (max-width: 991.98px) {
  .sm-md-preview { border-left: 1px solid var(--sm-border-strong); border-top: 0; max-height: 18rem; }
}
.sm-md-preview-label {
  font-size: 0.62rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--sm-text-faint); margin-bottom: 0.4rem;
}
.sm-md-preview-body { font-size: 0.88rem; line-height: 1.45; color: var(--sm-text); overflow-wrap: anywhere; }
.sm-md-empty { color: var(--sm-text-faint); font-style: italic; margin: 0; font-size: 0.82rem; }

.sm-md-foot {
  display: flex; align-items: baseline; justify-content: space-between; gap: 0.75rem;
  margin-top: 0.3rem;
}
.sm-md-count { font-family: var(--sm-font-mono); font-size: 0.72rem; color: var(--sm-text-faint); flex-shrink: 0; }
/* Warned before it bites, refused at the wall. The maxlength stops the typing;
   this is what explains why. */
.sm-md-count.is-near { color: var(--sm-warning); }
.sm-md-count.is-full { color: var(--sm-danger); font-weight: 600; }

/* ---- the rendered message ----
   Sized in relation to the body text rather than in Discord's pixels: this is
   the dashboard's type ramp, showing Discord's hierarchy. */
.sm-md-line { min-height: 1.1em; }
.sm-md-blank { height: 0.6em; }
.sm-md-h1 { font-size: 1.35rem; font-weight: 700; line-height: 1.25; margin: 0.5rem 0 0.2rem; }
.sm-md-h2 { font-size: 1.15rem; font-weight: 700; line-height: 1.3; margin: 0.45rem 0 0.2rem; }
.sm-md-h3 { font-size: 1rem; font-weight: 700; line-height: 1.35; margin: 0.4rem 0 0.2rem; }
.sm-md-h1:first-child, .sm-md-h2:first-child, .sm-md-h3:first-child { margin-top: 0; }
.sm-md-subtext { font-size: 0.76rem; color: var(--sm-text-dim); line-height: 1.4; }
.sm-md-list { margin: 0.1rem 0 0.1rem 1.15rem; padding: 0; }
.sm-md-list .sm-md-list { margin-bottom: 0; }
.sm-md-list li { margin: 0.05rem 0; }
.sm-md-quote {
  margin: 0.2rem 0; padding: 0.05rem 0 0.05rem 0.6rem;
  border-left: 3px solid var(--sm-border-strong);
  color: var(--sm-text-muted);
}
.sm-md-code {
  font-family: var(--sm-font-mono); font-size: 0.85em;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border);
  padding: 0.05rem 0.25rem; color: var(--sm-text);
}
.sm-md-pre {
  font-family: var(--sm-font-mono); font-size: 0.78rem; line-height: 1.45;
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border);
  padding: 0.45rem 0.6rem; margin: 0.35rem 0; overflow-x: auto;
  white-space: pre; color: var(--sm-text);
}
.sm-md-pre code { background: none; border: 0; padding: 0; font-size: inherit; }
.sm-md-link { color: var(--sm-accent-cyan); text-decoration: none; }
.sm-md-link:hover { text-decoration: underline; }
/* A mention is drawn as a chip with the last four digits of the id rather than
   as a resolved name. Resolving it would mean a lookup per keystroke, and the
   thing the writer needs to see is that the mention IS one — that they typed
   the id form correctly — not who it points at. */
.sm-md-mention {
  background: var(--sm-pill-bg); border: 1px solid var(--sm-pill-border);
  color: var(--sm-accent-cyan); padding: 0 0.25rem; font-size: 0.85em; white-space: nowrap;
}
.sm-md-emoji { width: 1.35em; height: 1.35em; object-fit: contain; vertical-align: -0.25em; }
/* Covered until clicked, exactly as the reader will find it. A preview that
   showed the spoiler would be answering the wrong question. */
.sm-md-spoiler {
  background: var(--sm-wash-3); color: transparent; cursor: pointer;
  border-radius: 0; padding: 0 0.15rem;
}
.sm-md-spoiler > * { visibility: hidden; }
.sm-md-spoiler.is-open { background: var(--sm-wash-1); color: inherit; }
.sm-md-spoiler.is-open > * { visibility: visible; }
.sm-md-spoiler:focus-visible { outline: 1px solid var(--sm-accent); outline-offset: 1px; }

/* ==========================================================================
   MODERATION INBOX
   dashboard/pages/inbox.js, inboxControls.js, and Overview's Needs attention
   box. Alerts, report groups, ModMail tickets and appeals as one queue: list, detail
   pane and member rail. Tokens only; squared like the rest of the theme.
   ========================================================================== */

/* The sidebar count beside the Inbox entry. */
.nav-item .nav-count {
  margin-left: auto; flex-shrink: 0; min-width: 1.4rem; padding: 0 0.35rem;
  font-family: var(--sm-font-mono); font-size: 0.7rem; line-height: 1.15rem; text-align: center;
  color: var(--sm-warning-text); background: var(--sm-warning-soft);
}

/* ---- The split view ----
   Desktop (1200px and up): list | item | member rail, each its own column.
   992 to 1199px: list | item, the rail under the item.
   Under 992px (tablets and phones): one column, the list OR the item with a
   Back control, never both, and the rail folded under the item. */
.main .inbox {
  display: grid; gap: 0.75rem;
  /* The columns share one bottom edge: every pane is the same height, sized
     from the viewport below, so the split reads as a mail client rather than
     as three cards of different lengths with the page's background between
     them. The list column is wide enough for its five tabs on one line — at
     22rem the last tab was cut off with nothing to say so. */
  align-items: stretch;
  grid-template-columns: minmax(19rem, 26rem) minmax(0, 1fr) minmax(15rem, 18rem);
}
.main .inbox > .card { margin-bottom: 0; min-width: 0; padding: 0; }
.main .inbox:not(:has(.inbox-rail)) { grid-template-columns: minmax(20rem, 28rem) minmax(0, 1fr); }
/* One height for every pane, from the viewport: what is left under the top
   bar and the page header. Each pane scrolls INSIDE itself — the list and the
   item body — rather than the page scrolling, so the tabs, the filters and
   the item's head stay put while a long thread moves. */
.main .inbox-list-pane, .main .inbox-detail, .main .inbox-rail {
  min-height: calc(100dvh - 13.5rem); max-height: calc(100dvh - 13.5rem);
}

/*
  THE INBOX FILLS THE WINDOW.

  Every other page is a column of cards with the main region's 1.5rem of
  breathing room around it, which is right for reading and wrong for a mail
  client: the three panes already scroll inside themselves, so that padding
  was a border drawn around a thing that wants the glass, and the page header
  above it took three and a half rem more to name a page its own tabs caption.

  `13.5rem` in the rule above is the sum of what USED to sit above and below —
  top bar, page header, main padding, gaps. With the header gone and the
  padding dropped, the only chrome left is the top bar, so the panes take
  everything under it. `100dvh` rather than `100vh` deliberately: on a phone
  the dynamic unit follows the address bar, and `vh` there leaves the last
  rows of the queue under browser furniture.

  Scoped with :has() so it reaches only the page that opts in by rendering
  .inbox-full — the padding on .main is shared by every other page and must
  stay where it is.
*/
.main:has(.inbox-full) { padding: 0; }
.main .inbox-full { gap: 1px; }
.main .inbox-full > .card { border-top: 0; border-bottom: 0; }
.main .inbox-full .inbox-list-pane,
.main .inbox-full .inbox-detail,
.main .inbox-full .inbox-rail {
  min-height: calc(100dvh - var(--sm-topbar-h, 3.5rem));
  max-height: calc(100dvh - var(--sm-topbar-h, 3.5rem));
}
/* The tab row gains the help button the page header used to carry. The tabs
   keep scrolling horizontally on a narrow screen; the button does not move. */
.main .inbox-tabs-row { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.main .inbox-tabs-row > .inbox-tabs { flex: 1; min-width: 0; }
.main .inbox-help { flex-shrink: 0; padding-right: 0.65rem; display: flex; align-items: center; }
.main .inbox-rail { overflow-y: auto; overscroll-behavior: contain; }
@media (max-width: 1199.98px) {
  .main .inbox, .main .inbox:not(:has(.inbox-rail)) { grid-template-columns: minmax(18rem, 24rem) minmax(0, 1fr); }
  .main .inbox .inbox-rail { grid-column: 2; min-height: 0; max-height: none; overflow: visible; }
  /* Same release for the full-bleed variant: below 1200px the rail drops under
     the detail pane and a fixed height would leave it scrolling inside a box
     the page has already stopped sizing. */
  .main .inbox-full .inbox-rail { min-height: 0; max-height: none; overflow: visible; }
}

/* ---- List pane ----
   Tabs and filter chips are each ONE row that scrolls sideways rather than
   wrapping; the sort is a square toggle at the end of the chips. */
.main .inbox-list-pane { display: flex; flex-direction: column; }
.main .inbox-tabs {
  display: flex; flex-wrap: nowrap; gap: 0.85rem; padding: 0 1rem; border-bottom: 1px solid var(--sm-border);
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none;
}
.main .inbox-tabs::-webkit-scrollbar { display: none; }
.main .inbox-tab {
  display: inline-flex; align-items: center; gap: 0.4rem; min-height: 2.5rem; flex-shrink: 0; white-space: nowrap;
  color: var(--sm-text-muted); border-bottom: 2px solid transparent; text-decoration: none; font-weight: 500;
}
.main .inbox-tab:hover { color: var(--sm-text); }
.main .inbox-tab.is-active { color: var(--sm-text); border-bottom-color: var(--sm-accent); }
.main .inbox-tab-count { font-family: var(--sm-font-mono); font-size: 0.72rem; color: var(--sm-text-dim); }
.main .inbox-list-tools { display: flex; align-items: center; gap: 0.5rem; min-width: 0; padding: 0.6rem 1rem 0; }
.main .inbox-chips {
  display: flex; flex-wrap: nowrap; gap: 0.35rem; flex: 1 1 auto; min-width: 0;
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none;
}
.main .inbox-chips::-webkit-scrollbar { display: none; }
.main .inbox-chip {
  display: inline-flex; align-items: center; flex-shrink: 0; white-space: nowrap; min-height: 2rem; padding: 0 0.7rem;
  border: 1px solid var(--sm-border-strong); color: var(--sm-text-muted); font-size: 0.8rem; text-decoration: none;
}
.main .inbox-chip:hover { color: var(--sm-text); background: var(--sm-bg-hover); }
.main .inbox-chip.is-on { color: var(--sm-text); background: var(--sm-pill-bg); border-color: var(--sm-pill-border); }
.main .inbox-sort {
  display: inline-grid; place-items: center; flex-shrink: 0; width: 2rem; height: 2rem;
  border: 1px solid var(--sm-border-strong); color: var(--sm-text-muted); text-decoration: none;
}
.main .inbox-sort:hover { color: var(--sm-text); background: var(--sm-bg-hover); }
.main .inbox-sort.is-on { color: var(--sm-text); background: var(--sm-pill-bg); border-color: var(--sm-pill-border); }
.main .inbox-list-count { margin: 0; padding: 0.45rem 1rem 0.55rem; font-size: 0.78rem; color: var(--sm-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.main .inbox-arrived {
  display: block; padding: 0.45rem 1rem; text-align: center; font-size: 0.82rem; font-weight: 500;
  color: var(--sm-accent-cyan); background: var(--sm-accent-ghost);
  border-top: 1px solid var(--sm-border); border-bottom: 1px solid var(--sm-border); text-decoration: none;
}
/* The list takes whatever height the tabs, filters and footer leave, and
   scrolls inside that; the pane's own height is set with the grid above. */
.main .inbox-list {
  display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain; border-top: 1px solid var(--sm-border);
}
.main .inbox-rows { display: flex; flex-direction: column; flex: 1 1 auto; }
/* An empty list says so in the middle of the space, not on its first line. */
.main .inbox-rows > .sm-panel-empty { margin: auto 1rem; }
.main .inbox-list > .sm-panel-sub { padding: 0 1rem; }
.main .inbox-keys { padding: 0.5rem 1rem; border-top: 1px solid var(--sm-border); }
.main .inbox kbd, .sm-modal-body kbd {
  display: inline-block; min-width: 1.1rem; padding: 0 0.3rem; text-align: center;
  font-family: var(--sm-font-mono); font-size: 0.7rem; color: var(--sm-text-muted);
  background: var(--sm-bg-deep); border: 1px solid var(--sm-border-strong); border-radius: 0; box-shadow: none;
}
/* Touch and narrow screens: 40px chips, and no keyboard footer. */
@media (hover: none), (max-width: 991.98px) {
  .main .inbox-chip { min-height: 2.5rem; }
  .main .inbox-sort { width: 2.5rem; height: 2.5rem; }
  .main .inbox-keys { display: none; }
}

/* ---- A row: a link that selects, and its quick controls beside it ----
   The link's hit area is stretched over the whole row (::after), and the
   controls sit above it, so a tap anywhere but a button opens the item. */
.main .inbox-row { position: relative; display: flex; align-items: center; border-bottom: 1px solid var(--sm-border); }
.main .inbox-row:hover { background: var(--sm-bg-hover); }
.main .inbox-row.is-critical { box-shadow: inset 2px 0 0 var(--sm-danger); }
.main .inbox-row.is-selected { background: var(--sm-accent-ghost); box-shadow: inset 2px 0 0 var(--sm-accent); }
.main .inbox-row-link { flex: 1 1 auto; min-width: 0; display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.65rem 0.5rem 0.65rem 1rem; color: var(--sm-text); text-decoration: none; }
.main .inbox-row-link::after { content: ""; position: absolute; inset: 0; }
.main .inbox-row-link:hover { color: var(--sm-text); }
.main .inbox-row-link:focus-visible { outline: none; }
.main .inbox-row-link:focus-visible::after { outline: 2px solid var(--sm-accent); outline-offset: -2px; }
.main .inbox-row-icon { width: 1rem; margin-top: 0.15rem; text-align: center; flex-shrink: 0; }
.main .inbox-kind-alert { color: var(--sm-warning); }
.main .inbox-kind-report { color: var(--sm-danger); }
.main .inbox-kind-ticket { color: var(--sm-info); }
.main .inbox-kind-appeal { color: var(--sm-accent-cyan); }
.main .inbox-row-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.3rem; }
.main .inbox-row-top { display: flex; align-items: baseline; gap: 0.5rem; min-width: 0; }
.main .inbox-row-who { flex-shrink: 0; max-width: 60%; font-weight: 600; font-size: 0.88rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.main .inbox-row-title { flex: 1 1 auto; min-width: 0; font-size: 0.85rem; color: var(--sm-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main .inbox-row-age { margin-left: auto; flex-shrink: 0; font-family: var(--sm-font-mono); font-size: 0.7rem; color: var(--sm-text-dim); white-space: nowrap; }
.main .inbox-row-meta { display: flex; align-items: center; gap: 0.45rem; min-width: 0; }
/* A status pill never wraps and never gives up width to the text beside it. */
.main .inbox-pill { white-space: nowrap; flex-shrink: 0; }
.main .inbox-row-line { flex: 1 1 auto; min-width: 0; font-size: 0.78rem; color: var(--sm-text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main .inbox-row-ai { color: var(--sm-text-muted); }
.main .inbox-row-ai .label-icon, .main .inbox-attn-line .label-icon, .main .inbox-summary-note .label-icon { color: var(--sm-accent-cyan); }
/* Quick controls: out of the way until the row is pointed at or focused on a
   desktop, always there on touch, where there is no hover to reveal them. The
   claimer's avatar is never hidden: who holds it is not an action. */
.main .inbox-row-actions { position: relative; z-index: 1; display: flex; align-items: center; flex-shrink: 0; padding: 0 0.75rem 0 0; }
.main .inbox-row-actions form, .main .inbox-row-actions .sm-form-open { opacity: 0; transition: opacity 0.12s ease; }
.main .inbox-row:hover .inbox-row-actions form, .main .inbox-row:hover .inbox-row-actions .sm-form-open,
.main .inbox-row:focus-within .inbox-row-actions form, .main .inbox-row:focus-within .inbox-row-actions .sm-form-open { opacity: 1; }
@media (hover: none) { .main .inbox-row-actions form, .main .inbox-row-actions .sm-form-open { opacity: 1; } }

/* ---- Claim and resolve controls (inboxControls.js) ---- */
.main .inbox-controls { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; }
.main .inbox-controls form { margin: 0; display: inline-flex; }
.main .inbox-controls.is-compact { flex-wrap: nowrap; gap: 0.25rem; }
/* Compact quick controls are 40px squares: a thumb, on a phone. */
.main .inbox-controls.is-compact .btn.sm-icon-btn { width: 2.5rem; min-width: 2.5rem; height: 2.5rem; min-height: 2.5rem; padding: 0; }
/* A compact Resolve is soft success, not a solid green block. */
.main .btn.inbox-resolve { background: var(--sm-success-soft); color: var(--sm-success-text); border: 1px solid var(--sm-success-line); }
.main .btn.inbox-resolve:hover { background: var(--sm-success); color: var(--sm-on-solid); }
/* The claimer's avatar, standing in for Claim. Round: it is an avatar (§1). */
.main .inbox-ava {
  display: inline-grid; place-items: center; flex-shrink: 0; width: 1.75rem; height: 1.75rem; border-radius: 50%; object-fit: cover;
  font-size: 0.72rem; font-weight: 700; color: var(--sm-text-muted); background: var(--sm-bg-deep); border: 1px solid var(--sm-border-strong);
}
.main .inbox-ava.is-empty { color: var(--sm-text-dim); font-size: 0.68rem; border-style: dashed; }
.main .inbox-controls.is-compact .inbox-ava { margin: 0 0.375rem; }

/* ---- Detail pane ----
   The head is compact and NEVER sticky: bar (Back, who, Kind #id, overflow),
   status pill and one meta line, the title, one row of actions. */
.main .inbox-detail { display: flex; flex-direction: column; min-height: 20rem; min-width: 0; }
/* Nothing chosen: the prompt sits in the middle of the pane, as the list's
   empty line does, rather than at the top of a tall blank box. */
.main .inbox-detail > .ov-empty, .main .inbox-detail > .sm-panel-empty { flex: 1 1 auto; display: grid; place-items: center; align-content: center; margin: 0; }
.main .inbox-back { display: none; }
.main .inbox-detail > .inbox-back { padding: 0.6rem 1rem; font-size: 0.85rem; border-bottom: 1px solid var(--sm-border); }
.main .inbox-detail-head { display: flex; flex-direction: column; gap: 0.4rem; padding: 0.6rem 1rem 0.85rem; border-bottom: 1px solid var(--sm-border); background: var(--sm-bg-raised); }
.main .inbox-detail-bar { display: flex; align-items: center; gap: 0.5rem; min-width: 0; min-height: 2.25rem; }
.main .inbox-detail-bar .inbox-back { place-items: center; flex-shrink: 0; width: 2.5rem; height: 2.5rem; margin-left: -0.6rem; color: var(--sm-text-muted); text-decoration: none; }
.main .inbox-detail-bar .inbox-back:hover { color: var(--sm-text); background: var(--sm-bg-hover); }
.main .inbox-detail-ref { flex: 1 1 auto; min-width: 0; display: flex; align-items: baseline; gap: 0.45rem; white-space: nowrap; overflow: hidden; }
.main .inbox-detail-who { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-weight: 600; font-size: 0.9rem; }
.main .inbox-detail-id { flex-shrink: 0; font-family: var(--sm-font-mono); font-size: 0.72rem; color: var(--sm-text-dim); }
.main .inbox-detail-status { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.main .inbox-detail-sub { min-width: 0; font-size: 0.8rem; color: var(--sm-text-dim); overflow-wrap: anywhere; }
.main h2.inbox-detail-title { display: block; margin: 0; font-size: 1.15rem; font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; }
.main .inbox-detail-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; margin-top: 0.3rem; }
.main .inbox-detail-actions form { margin: 0; display: inline-flex; }
.main .inbox-claim-banner { margin-top: 0.3rem; font-size: 0.85rem; }
/* The overflow menu: a <details> popover. A floating layer, so it may cast a shadow (§1). */
.main .inbox-more { position: relative; flex-shrink: 0; margin-left: auto; }
.main .inbox-more > summary { list-style: none; }
.main .inbox-more > summary::-webkit-details-marker { display: none; }
.main .inbox-more[open] > summary { background: var(--sm-bg-hover); }
.main .inbox-more-menu {
  position: absolute; right: 0; top: calc(100% + 0.25rem); z-index: 30; min-width: 15rem; padding: 0.25rem 0;
  background: var(--sm-bg-raised); border: 1px solid var(--sm-border-strong); box-shadow: 0 8px 24px var(--sm-shadow-2);
}
.main .inbox-more-item { display: flex; align-items: center; gap: 0.5rem; min-height: 2.5rem; padding: 0 0.9rem; font-size: 0.88rem; color: var(--sm-text); text-decoration: none; white-space: nowrap; }
.main .inbox-more-item:hover, .main .inbox-more-item:focus-visible { background: var(--sm-bg-hover); color: var(--sm-text); }
.main .inbox-more-out { margin-left: auto; padding-left: 0.75rem; font-size: 0.72rem; color: var(--sm-text-dim); }
.main .inbox-detail-body { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; gap: 1rem; padding: 1rem; overflow-y: auto; overscroll-behavior: contain; }
.main .inbox-section .sm-field-group { margin-top: 0; }
.main .inbox-context { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.35rem; font-size: 0.86rem; color: var(--sm-text-muted); }
.main .inbox-context li { padding-left: 0.6rem; border-left: 2px solid var(--sm-border-strong); white-space: pre-wrap; overflow-wrap: anywhere; }
.main .inbox-context li.is-anchor { color: var(--sm-text); background: var(--sm-warning-soft); border-left-color: var(--sm-warning); padding: 0.35rem 0.6rem; }
.main .inbox-message { padding: 0.8rem 0.9rem; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); }
.main .inbox-message.is-flagged { border-color: var(--sm-danger-line); }
.main .inbox-message-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.45rem; margin-bottom: 0.3rem; }
.main .inbox-message-author { font-weight: 600; font-size: 0.86rem; color: var(--sm-text); }
.main .inbox-message-time { font-size: 0.75rem; color: var(--sm-text-dim); }
.main .inbox-message-body { font-size: 0.9rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.main .inbox-message-links { display: flex; flex-wrap: wrap; gap: 1rem; margin-top: 0.6rem; font-size: 0.82rem; }
.main .inbox-media { margin: 0.6rem 0 0; }
/* The item pane's facts (the AI assessment, an appeal's sanction): the
   rail's list at the pane's text size, with a fixed label column so the
   values line up down the block and a sentence wraps under itself. */
.main .inbox-facts-wide { grid-template-columns: 7rem minmax(0, 1fr); row-gap: 0.45rem; font-size: 0.88rem; }
.main .inbox-facts-wide dt { color: var(--sm-text-muted); }
.main .inbox-facts-wide dd { line-height: 1.45; }
.main .inbox-confidence { display: inline-flex; align-items: center; gap: 0.6rem; }
.main .inbox-confidence progress {
  appearance: none; -webkit-appearance: none; width: 9rem; height: 0.4rem; border: 0; border-radius: 0;
  background: var(--sm-bg-deep); color: var(--sm-accent);
}
.main .inbox-confidence progress::-webkit-progress-bar { background: var(--sm-bg-deep); border-radius: 0; }
.main .inbox-confidence progress::-webkit-progress-value { background: var(--sm-accent); border-radius: 0; }
.main .inbox-confidence progress::-moz-progress-bar { background: var(--sm-accent); border-radius: 0; }
.main .inbox-actionbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; padding: 0.75rem 1rem;
  background: var(--sm-bg-deep); border-top: 1px solid var(--sm-border);
}
.main .inbox-actionbar form { margin: 0; display: inline-flex; }
.main .inbox-reporters { list-style: none; margin: 0; padding: 0; }
.main .inbox-reporter { padding: 0.6rem 0; border-bottom: 1px solid var(--sm-border); }
.main .inbox-reporter:last-child { border-bottom: 0; }
.main .inbox-reporter-head { display: flex; align-items: baseline; gap: 0.45rem; }
.main .inbox-reporter-head .inbox-message-time { margin-left: auto; font-family: var(--sm-font-mono); }
.main .inbox-reporter-words { margin-top: 0.2rem; font-size: 0.86rem; color: var(--sm-text-muted); white-space: pre-wrap; overflow-wrap: anywhere; }

/* ---- Ticket: the claim and summary card, conversation, composer ---- */
.main .inbox-scheduled { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin: 0; }
.main .inbox-scheduled form { margin: 0 0 0 auto; }
.main .inbox-ticket-card { background: var(--sm-bg-deep); border: 1px solid var(--sm-pill-border); }
.main .inbox-claim-line { display: flex; align-items: center; gap: 0.55rem; min-height: 2.75rem; padding: 0.3rem 0.5rem 0.3rem 0.75rem; font-size: 0.86rem; }
.main .inbox-claim-line + .inbox-summary { border-top: 1px solid var(--sm-border); }
.main .inbox-claim-who { flex: 1 1 auto; min-width: 0; color: var(--sm-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.main .inbox-claim-who strong { color: var(--sm-text); font-weight: 600; }
.main .inbox-claim-when { color: var(--sm-text-dim); font-size: 0.78rem; }
.main .inbox-claim-line .inbox-controls { flex-shrink: 0; flex-wrap: nowrap; }
/* The small text-weight buttons on the card: Claim / Release / Take over, Refresh. */
.main .inbox-claim-line .inbox-controls .btn,
.main .btn.inbox-quiet-btn { min-width: 0; min-height: 2rem; padding: 0.2rem 0.6rem; font-size: 0.8rem; }
.main .btn.inbox-quiet-btn { border-color: transparent; color: var(--sm-text-muted); }
.main .btn.inbox-quiet-btn:hover { color: var(--sm-text); background: var(--sm-bg-hover); border-color: var(--sm-border-strong); }
.main .inbox-summary { padding: 0.65rem 0.75rem 0.45rem; }
.main .inbox-summary-text { margin: 0; font-size: 0.9rem; line-height: 1.5; text-align: left; overflow-wrap: anywhere; }
/* An old five-section summary: label over value, left-aligned, never a table. */
.main .inbox-summary-stack { margin: 0; text-align: left; }
.main .inbox-summary-stack dt { margin-top: 0.45rem; font-size: 0.75rem; font-weight: 600; color: var(--sm-text-dim); }
.main .inbox-summary-stack dt:first-child { margin-top: 0; }
.main .inbox-summary-stack dd { margin: 0.1rem 0 0; font-size: 0.88rem; line-height: 1.45; overflow-wrap: anywhere; }
.main .inbox-summary-foot { display: flex; align-items: center; gap: 0.5rem; min-width: 0; margin-top: 0.35rem; }
.main .inbox-summary-note { flex: 1 1 auto; min-width: 0; font-size: 0.75rem; color: var(--sm-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.main .inbox-summary-refresh { flex-shrink: 0; margin: 0; }
.main .inbox-thread { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.5rem; }
.main .inbox-msg { padding: 0.55rem 0.75rem 0.6rem; border: 1px solid var(--sm-border); background: var(--sm-bg-deep); }
.main .inbox-msg.is-mod { background: var(--sm-accent-ghost); border-color: var(--sm-pill-border); }
.main .inbox-msg.is-note { background: var(--sm-warning-soft); border: 1px dashed var(--sm-warning-line); }
.main .inbox-msg.is-system { background: transparent; border-style: dashed; color: var(--sm-text-dim); }
.main .inbox-msg.is-retracted .inbox-message-body { color: var(--sm-text-dim); text-decoration: line-through; }
.main .inbox-msg .inbox-message-head { gap: 0.4rem; margin-bottom: 0.2rem; }
.main .inbox-msg-marker { font-size: 0.72rem; color: var(--sm-text-dim); }
.main .inbox-msg .inbox-message-time { margin-left: auto; font-family: var(--sm-font-mono); font-size: 0.7rem; white-space: nowrap; }
/* What a message was: To member, or Note. A member's own words are untagged. */
.main .inbox-msg-tag { display: inline-flex; align-items: center; align-self: center; height: 1.15rem; padding: 0 0.4rem; font-size: 0.68rem; font-weight: 600; white-space: nowrap; }
.main .inbox-msg-tag.is-reply { background: var(--sm-pill-bg); color: var(--sm-text); }
.main .inbox-msg-tag.is-note { background: var(--sm-warning-soft); color: var(--sm-warning-text); }
.main .inbox-msg-via { margin-top: 0.3rem; font-size: 0.74rem; color: var(--sm-text-dim); }
.main .inbox-composer { padding: 0.75rem 1rem 0.85rem; background: var(--sm-bg-deep); border-top: 1px solid var(--sm-border); }
/* Reply | Anonymous | Note: a segmented control over real radios. */
.main .inbox-seg { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); max-width: 26rem; margin-bottom: 0.5rem; border: 1px solid var(--sm-border-strong); }
.main .inbox-seg-opt { position: relative; display: flex; min-width: 0; margin: 0; }
.main .inbox-seg-opt + .inbox-seg-opt { border-left: 1px solid var(--sm-border-strong); }
.main .inbox-seg-opt input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.main .inbox-seg-opt span {
  flex: 1 1 auto; display: flex; align-items: center; justify-content: center; min-width: 0; min-height: 2.35rem; padding: 0 0.5rem;
  font-size: 0.84rem; color: var(--sm-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.main .inbox-seg-opt:hover span { color: var(--sm-text); background: var(--sm-bg-hover); }
.main .inbox-seg-opt input:checked + span { color: var(--sm-text); background: var(--sm-bg-hover); box-shadow: inset 0 -2px 0 var(--sm-accent); }
.main .inbox-seg-opt input[value="note"]:checked + span { box-shadow: inset 0 -2px 0 var(--sm-warning); }
.main .inbox-seg-opt input:focus-visible + span { outline: 1px solid var(--sm-accent); outline-offset: -1px; }
.main .inbox-composer:has(input[data-inbox-mode][value="note"]:checked) .sm-md-input { border-style: dashed; border-color: var(--sm-warning-line); }
.main .inbox-composer-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 0.5rem; margin-top: 0.5rem; }
/* The send button and the signing hint follow the mode, in CSS. */
.main .inbox-sign { margin-right: auto; min-width: 0; }
.main .inbox-sign-for, .main .inbox-send-note { display: none; }
.main .inbox-composer:has(input[data-inbox-mode][value="reply"]:checked) .inbox-sign-for[data-sign="reply"],
.main .inbox-composer:has(input[data-inbox-mode][value="anonymous"]:checked) .inbox-sign-for[data-sign="anonymous"],
.main .inbox-composer:has(input[data-inbox-mode][value="note"]:checked) .inbox-sign-for[data-sign="note"],
.main .inbox-composer:has(input[data-inbox-mode][value="note"]:checked) .inbox-send-note { display: inline; }
.main .inbox-composer:has(input[data-inbox-mode][value="note"]:checked) .inbox-send-reply { display: none; }

/* ---- Appeal: what they wrote, earlier attempts, the outcome ---- */
.main .inbox-appeal-outcome { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.35rem; font-size: 0.86rem; }
.main .inbox-appeal-said { border-left: 2px solid var(--sm-border-strong); }
.main .inbox-appeal-cell { white-space: pre-wrap; overflow-wrap: anywhere; }
.main .inbox-appeal-answers { margin: 0; display: flex; flex-direction: column; gap: 0.75rem; }
.main .inbox-appeal-answers dt { margin-bottom: 0.2rem; font-size: 0.8rem; font-weight: 500; color: var(--sm-text-dim); }
.main .inbox-appeal-answers dd { margin: 0; padding: 0.6rem 0.8rem; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); }
.main .inbox-appeal-history { list-style: none; margin: 0; padding: 0; }
.main .inbox-appeal-history li { padding: 0.5rem 0; border-bottom: 1px solid var(--sm-border); }
.main .inbox-appeal-history li:last-child { border-bottom: 0; }

/* ---- Appeal decision panel: the detail pane and the Resolve dialog share it
   (appealDecisionFields in inboxControls.js). ---- */
.main .inbox-appeal-form { padding: 0.9rem; background: var(--sm-bg-deep); border: 1px solid var(--sm-pill-border); }
.main .inbox-appeal-form .inbox-composer-foot { margin-top: 0.9rem; }
.main .inbox-appeal-decide { display: flex; flex-direction: column; gap: 0.9rem; }
.main .inbox-appeal-choices { margin: 0; padding: 0; border: 0; display: flex; flex-direction: column; gap: 1px; }
.main .inbox-appeal-choices legend { float: none; width: auto; margin-bottom: 0.4rem; }
.main .inbox-appeal-choice { position: relative; display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.55rem 0.7rem; margin: 0; background: var(--sm-bg-raised); border: 1px solid var(--sm-border); cursor: pointer; }
.main .inbox-appeal-choice:hover { background: var(--sm-bg-hover); }
.main .inbox-appeal-choice input { margin-top: 0.2rem; flex-shrink: 0; accent-color: var(--sm-accent); }
.main .inbox-appeal-choice:has(input:checked) { background: var(--sm-accent-ghost); border-color: var(--sm-pill-border); box-shadow: inset 2px 0 0 var(--sm-accent); }
.main .inbox-appeal-choice:has(input:focus-visible) { outline: 1px solid var(--sm-accent); outline-offset: -1px; }
.main .inbox-appeal-choice-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; font-size: 0.86rem; }
.main .inbox-appeal-effect { font-size: 0.8rem; color: var(--sm-text-muted); }
/* Reduce's days only matter for Reduce. */
.main .inbox-appeal-days { display: none; }
.main .inbox-appeal-decide:has(input[data-appeal-decision][value="reduce"]:checked) .inbox-appeal-days { display: block; }
.main .inbox-appeal-shown { font-size: 0.78rem; font-weight: 500; color: var(--sm-warning-text); }
.main .inbox-appeal-count { display: flex; align-items: baseline; gap: 0.75rem; margin-top: 0.3rem; }
.main .inbox-appeal-count [data-appeal-count] { margin-left: auto; flex-shrink: 0; font-size: 0.72rem; color: var(--sm-text-dim); }
.main .inbox-appeal-preview .sm-field-group { margin-top: 0; }
.main .inbox-appeal-preview-card { padding: 0.7rem 0.8rem; background: var(--sm-bg-base); border: 1px solid var(--sm-border); }
.main .inbox-appeal-preview-outcome { font-weight: 600; font-size: 0.88rem; }
.main .inbox-appeal-preview-outcome [data-appeal-outcome] { display: none; }
.main .inbox-appeal-decide:not(:has(input[data-appeal-decision]:checked)) [data-appeal-outcome=""] { display: inline; color: var(--sm-text-dim); }
.main .inbox-appeal-decide:has(input[data-appeal-decision][value="approve"]:checked) [data-appeal-outcome="approve"] { display: inline; color: var(--sm-success-text); }
.main .inbox-appeal-decide:has(input[data-appeal-decision][value="reduce"]:checked) [data-appeal-outcome="reduce"] { display: inline; color: var(--sm-info-text); }
.main .inbox-appeal-decide:has(input[data-appeal-decision][value="reject"]:checked) [data-appeal-outcome="reject"] { display: inline; color: var(--sm-text-muted); }
.main .inbox-appeal-preview-text { margin-top: 0.3rem; font-size: 0.88rem; white-space: pre-wrap; overflow-wrap: anywhere; }
.main .inbox-appeal-preview-text.is-empty { color: var(--sm-text-dim); font-style: italic; }

/* ---- Member rail ----
   A <details>: at 992px and up the page script opens it and its summary line
   is hidden, so it reads as a plain column; narrower, it is one "Member" line
   under the item until tapped. */
.main .inbox-rail { padding: 0; }
.main .inbox-rail-summary {
  display: none; align-items: center; gap: 0.5rem; min-height: 3rem; padding: 0 1rem;
  font-weight: 600; font-size: 0.9rem; cursor: pointer; list-style: none;
}
.main .inbox-rail-summary::-webkit-details-marker { display: none; }
.main .inbox-rail-summary::after {
  content: ""; flex-shrink: 0; width: 0.45rem; height: 0.45rem; margin: -0.2rem 0.2rem 0 0.25rem;
  border-right: 2px solid var(--sm-text-dim); border-bottom: 2px solid var(--sm-text-dim); transform: rotate(45deg);
}
.main .inbox-rail-fold[open] > .inbox-rail-summary::after { margin-top: 0.2rem; transform: rotate(-135deg); }
.main .inbox-rail-fold[open] > .inbox-rail-summary { border-bottom: 1px solid var(--sm-border); }
.main .inbox-rail-summary:focus-visible { outline: 2px solid var(--sm-accent); outline-offset: -2px; }
.main .inbox-rail-summary-name { flex: 1 1 auto; min-width: 0; font-weight: 400; color: var(--sm-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.main .inbox-rail-summary-note { flex-shrink: 0; font-weight: 400; font-size: 0.78rem; color: var(--sm-text-dim); }
.main .inbox-rail-body { display: flex; flex-direction: column; gap: 1rem; padding: 1rem; }
.main .inbox-rail-who { display: flex; align-items: center; gap: 0.7rem; min-width: 0; }
.main .inbox-rail-avatar { width: 2.75rem; height: 2.75rem; border-radius: 50%; flex-shrink: 0; object-fit: cover; }
.main .inbox-rail-avatar.is-blank { display: grid; place-items: center; background: var(--sm-bg-deep); color: var(--sm-text-dim); }
.main .inbox-rail-name { display: flex; flex-direction: column; align-items: flex-start; gap: 0.1rem; min-width: 0; }
.main .inbox-rail-name strong { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The id: dim mono text that copies on a tap, not a coloured chip. */
.main .inbox-rail .inbox-copy {
  display: inline; min-width: 0; min-height: 0; padding: 0; border: 0; background: transparent;
  font-family: var(--sm-font-mono); font-size: 0.72rem; color: var(--sm-text-dim); cursor: copy;
}
.main .inbox-rail .inbox-copy:hover { background: transparent; color: var(--sm-text-muted); }
.main .inbox-rail .inbox-copy:active { transform: none; }
/* Facts: a left-aligned definition list, label beside value. */
.main .inbox-facts { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 0.75rem; row-gap: 0.3rem; margin: 0; font-size: 0.84rem; text-align: left; }
.main .inbox-facts dt { font-weight: 400; color: var(--sm-text-dim); white-space: nowrap; }
.main .inbox-facts dd { margin: 0; color: var(--sm-text); overflow-wrap: anywhere; }
.main .inbox-rail-section .sm-field-group { margin-top: 0; }
.main .inbox-rail-tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.35rem; }
.main .inbox-rail-tile { display: flex; flex-direction: column; min-width: 0; padding: 0.35rem 0.55rem; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); font-size: 0.72rem; color: var(--sm-text-dim); }
.main .inbox-rail-tile .sm-num { font-size: 1.05rem; font-weight: 600; line-height: 1.25; color: var(--sm-text); }
.main .inbox-rail-tile.is-zero .sm-num { color: var(--sm-text-dim); font-weight: 500; }
.main .inbox-rail-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 1px; }
.main .inbox-rail-item { display: flex; align-items: center; gap: 0.45rem; min-height: 2.5rem; padding: 0.3rem 0.55rem; background: var(--sm-bg-deep); font-size: 0.82rem; }
.main .inbox-rail-item.is-open-item { padding: 0.25rem 0.25rem 0.25rem 0.55rem; }
.main .inbox-rail-item.is-current { box-shadow: inset 2px 0 0 var(--sm-accent); }
.main .inbox-rail-item-label { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main .inbox-rail-item .inbox-controls { flex-shrink: 0; }
.main .inbox-rail-sanction { padding: 0.6rem; background: var(--sm-bg-deep); border: 1px solid var(--sm-border); font-size: 0.85rem; }
.main .inbox-rail-member { align-self: flex-start; font-size: 0.85rem; }
@media (min-width: 992px) {
  /* Where the browser allows it, a closed fold still shows its content at this
     width, so the column is never empty before the script has run. */
  .main .inbox-rail-fold::details-content { content-visibility: visible; }
}
@media (max-width: 991.98px) {
  .main .inbox-rail-summary { display: flex; }
}

/* ---- Overview: Needs attention ----
   One row of counts at every width (a cell per visible tab), the unclaimed
   count and oldest wait under the heading, and rows the whole of which open
   the item, with 40px Claim (or the claimer's avatar) and Resolve. */
.main .inbox-attn .ov-panel-link { margin-left: auto; flex-shrink: 0; }
.main .inbox-attn-heading { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.main .inbox-attn-sub { font-size: 0.78rem; color: var(--sm-text-dim); }
.main .inbox-attn-figures { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); border-bottom: 1px solid var(--sm-border); }
.main .inbox-attn-figure {
  display: flex; flex-direction: column; align-items: center; gap: 0.1rem; min-width: 0; padding: 0.6rem 0.25rem 0.55rem;
  text-align: center; color: var(--sm-text); text-decoration: none; border-left: 1px solid var(--sm-border);
}
.main .inbox-attn-figure:first-child { border-left: 0; }
.main .inbox-attn-figure:hover { background: var(--sm-bg-hover); color: var(--sm-text); }
.main .inbox-attn-figure .sm-num { font-size: 1.35rem; font-weight: 600; line-height: 1.15; }
.main .inbox-attn-figure.is-zero .sm-num { color: var(--sm-text-dim); font-weight: 500; }
.main .inbox-attn-figure.is-hot .sm-num { color: var(--sm-warning-text); }
.main .inbox-attn-figure-label { max-width: 100%; font-size: 0.72rem; color: var(--sm-text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.main .inbox-attn-row { position: relative; display: flex; align-items: center; gap: 0.5rem; min-height: 4rem; padding-right: 0.75rem; border-bottom: 1px solid var(--sm-border); }
.main .inbox-attn-row:hover { background: var(--sm-bg-hover); }
.main .inbox-attn-row.is-critical { box-shadow: inset 2px 0 0 var(--sm-danger); }
.main .inbox-attn-link { flex: 1 1 auto; min-width: 0; display: flex; align-items: flex-start; gap: 0.6rem; padding: 0.6rem 0 0.6rem 1.1rem; color: var(--sm-text); text-decoration: none; }
.main .inbox-attn-link::after { content: ""; position: absolute; inset: 0; }
.main .inbox-attn-link:hover { color: var(--sm-text); }
.main .inbox-attn-link:focus-visible { outline: none; }
.main .inbox-attn-link:focus-visible::after { outline: 2px solid var(--sm-accent); outline-offset: -2px; }
.main .inbox-attn-main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.main .inbox-attn-kind { flex-shrink: 0; font-size: 0.8rem; color: var(--sm-text-dim); white-space: nowrap; }
/* Here the name gives way, not the pill or the age. */
.main .inbox-attn-main .inbox-row-who { flex: 0 1 auto; min-width: 2.5rem; max-width: none; }
.main .inbox-attn-line { display: block; font-size: 0.8rem; color: var(--sm-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.main .inbox-attn-actions { position: relative; z-index: 1; flex-shrink: 0; }
.main .inbox-attn-row form { margin: 0; display: inline-flex; }
.main .inbox-attn-foot { display: flex; flex-wrap: wrap; gap: 0.5rem; padding: 0.6rem 1.1rem; font-size: 0.8rem; color: var(--sm-text-dim); }
.main .inbox-attn-foot a { margin-left: auto; }
@media (max-width: 576px) {
  .main .inbox-attn-figure .sm-num { font-size: 1.15rem; }
  .main .inbox-attn-figure-label { font-size: 0.68rem; }
  .main .inbox-attn-kind { display: none; }
  .main .inbox-attn-link { padding-left: 1rem; }
}

/* ---- Under 992px (tablets and phones): one column. The list with nothing
   selected; the item, a Back control in its head, and the member rail folded
   under it, with one. Nothing is sticky. ---- */
@media (max-width: 991.98px) {
  .main .inbox, .main .inbox:not(:has(.inbox-rail)) { grid-template-columns: minmax(0, 1fr); }
  .main .inbox .inbox-rail { grid-column: auto; }
  .main .inbox[data-has-item="1"] .inbox-list-pane { display: none; }
  .main .inbox[data-has-item="0"] .inbox-detail,
  .main .inbox[data-has-item="0"] .inbox-rail { display: none; }
  .main .inbox-detail > .inbox-back { display: block; }
  .main .inbox-detail-bar .inbox-back { display: inline-grid; }
  .main .inbox-list-pane, .main .inbox-detail, .main .inbox-rail { min-height: 0; max-height: none; overflow: visible; }
  .main .inbox-list, .main .inbox-detail-body { max-height: none; overflow: visible; }
  .main .inbox-rows > .sm-panel-empty { margin: 1rem; }
  .main .inbox-row-actions form, .main .inbox-row-actions .sm-form-open { opacity: 1; }
}
/* ---- Phones: the ticket's two actions share the width, as do the composer's
   two send buttons; the signing hint takes its own line above them. ---- */
@media (max-width: 576px) {
  .main .inbox-detail-head { padding: 0.4rem 1rem 0.75rem; }
  .main h2.inbox-detail-title { font-size: 1.05rem; }
  .main .inbox-detail-body { gap: 0.75rem; padding: 0.75rem 1rem; }
  .main .inbox-detail-actions.is-split { flex-wrap: nowrap; }
  .main .inbox-detail-actions.is-split > .inbox-controls,
  .main .inbox-detail-actions.is-split > .btn { flex: 1 1 0; min-width: 0; }
  .main .inbox-detail-actions.is-split > .inbox-controls { display: flex; }
  .main .inbox-detail-actions.is-split .inbox-controls form { flex: 1 1 auto; }
  .main .inbox-detail-actions.is-split .btn { width: 100%; min-height: 2.5rem; }
  .main .inbox-composer { padding: 0.75rem 1rem; }
  .main .inbox-seg { max-width: none; }
  .main .inbox-composer-foot .inbox-sign { flex-basis: 100%; }
  .main .inbox-composer-foot > .btn { flex: 1 1 0; min-width: 0; min-height: 2.5rem; }
  /* Refresh keeps its glyph and its aria-label; the word gives the footer back. */
  .main .inbox-summary-refresh .btn > span { display: none; }
}

/* A row of filter buttons that stays one row.
   Compact enough that the usual four or five fit on a phone; if they still do
   not, the row scrolls sideways within itself rather than wrapping onto a second
   line. Its own class rather than .d-flex, because the phone rule wraps every
   .d-flex row. */
.main .sm-filter-row {
  display: flex; flex-wrap: nowrap; gap: 0.3rem;
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none;
}
.main .sm-filter-row::-webkit-scrollbar { display: none; }
.main .sm-filter-row > .btn { flex: 0 0 auto; white-space: nowrap; }
@media (max-width: 575.98px) {
  .main .sm-filter-row { gap: 0.2rem; }
  .main .sm-filter-row > .btn { font-size: 0.78rem; padding: 0.3rem 0.5rem; }
}

/* ===================== Progression tiers ============================== */
/*
  A rung's rank, drawn the same way wherever it appears: the tier's icon,
  then one to four stars. The stars are the tier counted, so the rank reads
  without knowing what the colours mean — which is the state somebody is in
  the first time they see another member's badge.
*/
.sm-tier { display: inline-flex; align-items: center; gap: 0.3rem; line-height: 1; }
.sm-tier-icon { font-size: 0.95em; }
.sm-tier-stars { display: inline-flex; gap: 0.08rem; font-size: 0.55em; }
.sm-tier-bronze { color: var(--sm-tier-bronze); }
.sm-tier-silver { color: var(--sm-tier-silver); }
.sm-tier-gold { color: var(--sm-tier-gold); }
.sm-tier-diamond { color: var(--sm-tier-diamond); }
/* Announced once, as the tier's name, instead of four star glyphs read out. */
.sm-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
}

/*
  LIGHT MODE. Silver and diamond are near-white and vanish on a light
  surface, so those two darken; bronze and gold already carry enough
  contrast. Guarded the same way every other light override in this file is
  — see the colour-mode block above.
*/
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --sm-tier-silver: #7b8798;
    --sm-tier-diamond: #2a93ac;
  }
}
:root[data-theme="light"] {
  --sm-tier-silver: #7b8798;
  --sm-tier-diamond: #2a93ac;
}
