/*
 * app.css  |  Denizen
 *
 * Monochrome. Greyscale only, no hues anywhere, so the interface stays out
 * of the way of the thing being edited. Discord servers are full of colour
 * already, and role swatches are the one place colour carries meaning.
 *
 * Light and dark are the same design with the ramp inverted. Every value
 * below is a shade of grey defined once as a variable, so a theme swap is
 * a change of variables rather than a second stylesheet.
 *
 * THEME SWITCHING
 * Set data-theme="light" or data-theme="dark" on <html>. With no attribute
 * the OS preference applies via prefers-color-scheme. theme.js writes the
 * attribute and remembers the choice.
 *
 * THEME SWAP
 * No property transitions. Cross-fading two inverted ramps makes text and
 * background pass through mid-grey simultaneously, contrast collapses, and
 * it reads as a flash. The swap is done with the View Transitions API
 * instead, which cross-fades a snapshot. See theme.js for the full note.
 */

/* Palette ------------------------------------------------------------- */

:root,
:root[data-theme="dark"] {
    --bg:         #000000;
    --bg-raised:  #0a0a0a;
    --bg-card:    #101010;
    --bg-hover:   #1a1a1a;
    --bg-active:  #242424;

    --line:       #262626;
    --line-soft:  #171717;

    --text:       #fafafa;
    --text-dim:   #a3a3a3;
    /* 8a8a8a, not 6b6b6b. The old value measured 3.94:1 against black,
       under the 4.5:1 needed for body text. */
    --text-mute:  #8a8a8a;

    /* Inverted pair for primary buttons: light block, dark label. */
    --invert-bg:  #fafafa;
    --invert-fg:  #000000;

    --focus:      #ffffff;

    /* What makes a button look pressable without adding a colour: a hairline
       highlight on the top edge and a shadow underneath, both the same
       neutral as the rest of the palette at low alpha. Dark surfaces need a
       brighter sheen and a heavier shadow than light ones to show at all. */
    --btn-sheen:  rgba(255, 255, 255, .06);
    --btn-shadow: rgba(0, 0, 0, .45);

    /* The sweep that crosses a "soon" badge. Light on dark, dark on light,
       so it reads as a highlight either way rather than a smear. */
    --shine:      rgba(255, 255, 255, .14);

    color-scheme: dark;
}

:root[data-theme="light"] {
    --bg:         #ffffff;
    --bg-raised:  #fafafa;
    --bg-card:    #ffffff;
    --bg-hover:   #f4f4f4;
    --bg-active:  #ebebeb;

    --line:       #e0e0e0;
    --line-soft:  #ededed;

    --text:       #0a0a0a;
    --text-dim:   #4a4a4a;
    /* 6b6b6b gives 5.33:1 on white. The previous 8a8a8a measured 3.45:1,
       which is why the faded feature text and the badges were hard to
       read in light mode. Greyscale has no hue to lean on, so the ramp
       has to carry the contrast by itself. */
    --text-mute:  #6b6b6b;

    --invert-bg:  #0a0a0a;
    --invert-fg:  #ffffff;

    --focus:      #000000;

    --btn-sheen:  rgba(255, 255, 255, .75);
    --btn-shadow: rgba(0, 0, 0, .08);
    --shine:      rgba(0, 0, 0, .10);

    color-scheme: light;
}

/* Follow the OS only when the user has not chosen explicitly. */
@media (prefers-color-scheme: light) {
    :root:not([data-theme]) {
        --bg:         #ffffff;
        --bg-raised:  #fafafa;
        --bg-card:    #ffffff;
        --bg-hover:   #f4f4f4;
        --bg-active:  #ebebeb;

        --line:       #e0e0e0;
        --line-soft:  #ededed;

        --text:       #0a0a0a;
        --text-dim:   #4a4a4a;
        --text-mute:  #6b6b6b;

        --invert-bg:  #0a0a0a;
        --invert-fg:  #ffffff;

        --focus:      #000000;

        --btn-sheen:  rgba(255, 255, 255, .75);
        --btn-shadow: rgba(0, 0, 0, .08);
        --shine:      rgba(0, 0, 0, .10);

        color-scheme: light;
    }
}

:root {
    --radius:    10px;
    --radius-sm: 6px;

    --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, sans-serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

    --ease: cubic-bezier(.4, 0, .2, 1);
}

/*
 * View Transitions cross-fade a snapshot of the whole page rather than
 * animating individual properties, so text never passes through a
 * low-contrast midpoint. See the long note at the top of theme.js.
 *
 * The default cross-fade is a little quick for a full palette inversion,
 * so both snapshots are slowed and overlapped.
 */
::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: .32s;
    animation-timing-function: var(--ease);
}

@media (prefers-reduced-motion: reduce) {
    ::view-transition-old(root),
    ::view-transition-new(root) {
        animation: none;
    }
}

* { box-sizing: border-box; }

/*
 * Make the hidden attribute actually win.
 *
 * The browser hides [hidden] elements with `[hidden] { display: none }` in
 * its own stylesheet. That selector has the same specificity as a single
 * class, and author styles come later, so ANY rule like
 * `.drawer { display: flex }` silently defeats it. The element keeps the
 * attribute, JavaScript keeps setting it, and nothing happens.
 *
 * That is exactly what stranded the permissions drawer: it could be opened
 * and never closed, because .drawer set display: flex.
 *
 * This one line makes `hidden` reliable everywhere and stops the same bug
 * appearing every time a hidden element is given a display value.
 */
[hidden] { display: none !important; }

html, body {
    margin: 0;
    padding: 0;
    /* iOS reflows text on rotation without this, which on a fixed-width
       layout means the page changes size when the phone turns. */
    -webkit-text-size-adjust: 100%;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 15px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

h1, h2, h3 { margin: 0; font-weight: 650; letter-spacing: -0.01em; }

:focus-visible {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
    border-radius: 3px;
}

/* Wordmark ------------------------------------------------------------ */

.wordmark {
    font-weight: 800;
    letter-spacing: 0.18em;
    font-size: 14px;
    color: var(--text);
}

/* Theme toggle -------------------------------------------------------- */

.theme-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-dim);
    cursor: pointer;
}
.theme-toggle:hover { background: var(--bg-hover); color: var(--text); }
.theme-toggle svg { display: block; width: 15px; height: 15px; }

/* Show the icon for the mode you would switch TO, not the current one. */
.theme-toggle .icon-moon { display: none; }
.theme-toggle .icon-sun  { display: block; }
:root[data-theme="light"] .theme-toggle .icon-moon { display: block; }
:root[data-theme="light"] .theme-toggle .icon-sun  { display: none; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .theme-toggle .icon-moon { display: block; }
    :root:not([data-theme]) .theme-toggle .icon-sun  { display: none; }
}

/* Buttons ------------------------------------------------------------- */

/*
 * Buttons.
 *
 * The flat bordered rectangle this replaced was correct and lifeless: no
 * response to a press beyond a background swap, so a click felt like nothing
 * happened. Three cheap additions fix that without adding a colour to a
 * monochrome design: a hairline highlight along the top edge to lift the
 * surface, a one-pixel drop on press, and a transition so both are felt
 * rather than merely true.
 *
 * Everything is greyscale on purpose. See the note on .btn-discord.
 */
.btn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 9px;
    padding: 10px 18px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text);
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    letter-spacing: .01em;
    cursor: pointer;
    box-shadow: inset 0 1px 0 var(--btn-sheen), 0 1px 2px var(--btn-shadow);
    transition: background-color .14s var(--ease), border-color .14s var(--ease),
                box-shadow .14s var(--ease), transform .06s var(--ease);
}

.btn:hover {
    background: var(--bg-hover);
    border-color: var(--text-mute);
}

/* Pressed, not just re-coloured. One pixel is enough to read as a press and
   small enough not to shift the layout around it. */
.btn:active {
    background: var(--bg-active);
    transform: translateY(1px);
    box-shadow: inset 0 1px 2px var(--btn-shadow);
}

.btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

.btn-sm  { padding: 6px 12px; font-size: 13px; }
.btn-lg  { padding: 14px 26px; font-size: 16px; }
.btn-block { width: 100%; }

.btn-primary {
    background: var(--invert-bg);
    color: var(--invert-fg);
    border-color: var(--invert-bg);
}
.btn-primary:hover {
    background: var(--invert-bg);
    border-color: var(--invert-bg);
    opacity: .88;
}

/*
 * The Discord sign-in button stays monochrome deliberately. Borrowing
 * Discord's blurple would put the only hue on the page and imply an
 * affiliation that does not exist.
 */
.btn-discord {
    background: var(--invert-bg);
    color: var(--invert-fg);
    border-color: var(--invert-bg);
}
.btn-discord:hover { opacity: .85; }

/*
 * Discord's mark, on a button that is inverted against the page.
 *
 * A light button in dark mode, a dark button in light mode, which means the
 * icon needs the OPPOSITE pairing to the wordmark and the watermark. Getting
 * that backwards produces a black mark on a black button, which is what the
 * previous hand-drawn path effectively did.
 *
 * Decided in CSS rather than by script, so it is correct on the first paint.
 */
.discord-mark {
    flex: 0 0 auto;
    width: 20px;
    height: 20px;
    /* Fitted rather than stretched: the two files are not the same aspect
       ratio, and letting either squash is more obvious at 20px than at any
       size where it would not matter. */
    object-fit: contain;
}

.discord-mark.is-black { display: block; }
.discord-mark.is-white { display: none; }

:root[data-theme="light"] .discord-mark.is-black { display: none; }
:root[data-theme="light"] .discord-mark.is-white { display: block; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .discord-mark.is-black { display: none; }
    :root:not([data-theme]) .discord-mark.is-white { display: block; }
}

.btn-ghost {
    background: transparent;
    border-color: var(--line);
    color: var(--text-dim);
}
.btn-ghost:hover { background: var(--bg-hover); color: var(--text); }

/* A disabled button is flat: no sheen, no shadow, nothing to press. The
   sheen is what makes the others look raised, so removing it is most of the
   signal. */
.btn:disabled,
.btn[disabled] {
    opacity: .38;
    cursor: not-allowed;
    box-shadow: none;
}
.btn:disabled:hover {
    opacity: .38;
    border-color: var(--line);
}
.btn:disabled:active { transform: none; }

/* Alerts and banners -------------------------------------------------- */

.alert {
    padding: 13px 16px;
    border: 1px solid var(--line);
    border-left: 3px solid var(--text);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-dim);
    margin: 0 0 22px;
    font-size: 14px;
}
.alert strong { color: var(--text); }
.alert-warn { border-left-color: var(--text-dim); }

.banner {
    padding: 12px 16px;
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-left: 3px solid var(--line);
    color: var(--text-dim);
    font-size: 14px;
    margin: 0 0 18px;
}
.banner strong { color: var(--text); }
.banner-info { border-left-color: var(--text-mute); }
.banner-warn { border-left-color: var(--text); }

/* Landing ------------------------------------------------------------- */

/*
 * Three bands, each doing one job: the hero states what Denizen is and
 * offers the only button; the capability grid shows breadth honestly,
 * including what is not built yet; the closing section argues the single
 * idea that separates it from every other bot.
 *
 * The earlier version sold a server editor. That is one feature, so the
 * page now leads with the product and lets the editor be the first item
 * in a list rather than the whole pitch.
 */

.page-landing { min-height: 100vh; background: var(--bg); }

.landing {
    max-width: 860px;
    margin: 0 auto;
    padding: 22px 22px 40px;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
}

.landing-head {
    display: flex;
    align-items: center;
    gap: 11px;
}
.landing-head .theme-toggle { margin-left: auto; }

.landing-body { flex: 1; }

.pill {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--bg-hover);
    color: var(--text-dim);
    border: 1px solid var(--line);
}

/* Hero ---------------------------------------------------------------- */

.hero {
    padding: 84px 0 68px;
    max-width: 620px;
}

.hero h1 {
    font-size: clamp(30px, 5vw, 44px);
    line-height: 1.1;
    letter-spacing: -0.025em;
    margin-bottom: 18px;
}

.lede {
    color: var(--text-dim);
    font-size: 17px;
    line-height: 1.6;
    max-width: 52ch;
    margin: 0 0 30px;
}

.hero .alert { max-width: 460px; }

.fineprint {
    color: var(--text-mute);
    font-size: 12.5px;
    line-height: 1.5;
    margin: 16px 0 0;
    /* One line where there is room for one. The 46ch cap was inherited from
       when this sat in a narrow column and broke a single short sentence
       across two lines for no reason. */
    max-width: 100%;
}

/* Capabilities -------------------------------------------------------- */

.capabilities {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 1px;
    background: var(--line-soft);
    border: 1px solid var(--line-soft);
    border-radius: var(--radius);
    overflow: hidden;
}

/*
 * Hairline dividers rather than gaps between cards. Six separate boxes
 * looked like six unrelated products; a single divided block reads as one
 * thing with six parts, which is what it is.
 */
.cap {
    background: var(--bg);
    padding: 20px 20px 22px;
}

.cap-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 6px;
}

.cap-name {
    font-size: 14.5px;
    font-weight: 650;
    color: var(--text);
}

/* Saying "building" outright is better than implying everything ships
   today and losing the reader on their second visit. */
.cap-status {
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-dim);
    background: var(--bg-hover);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 1px 6px;
}

/* The badge says 'building'. Fading the name as well made the
   card look disabled rather than forthcoming. */
.cap.is-building .cap-name { color: var(--text); }

.cap p {
    margin: 0;
    font-size: 13px;
    line-height: 1.55;
    color: var(--text-dim);
    max-width: 34ch;
}

/* Principle ----------------------------------------------------------- */

.principle {
    margin-top: 68px;
    padding-top: 40px;
    border-top: 1px solid var(--line-soft);
    max-width: 560px;
}

.principle h2 {
    font-size: clamp(20px, 3vw, 26px);
    line-height: 1.2;
    letter-spacing: -0.02em;
    margin-bottom: 12px;
}

.principle p {
    margin: 0;
    color: var(--text-dim);
    font-size: 15px;
    line-height: 1.65;
}

.landing-foot {
    margin-top: 72px;
    color: var(--text-mute);
    font-size: 11.5px;
}

@media (max-width: 560px) {
    .hero { padding: 48px 0 44px; }
    .capabilities { grid-template-columns: 1fr; }
    .principle { margin-top: 48px; padding-top: 30px; }
    .landing-foot { margin-top: 48px; }
}

/* Top bar ------------------------------------------------------------- */

/*
 * Full-bleed background, constrained contents. Letting the bar's contents
 * run to the window edges while the page body sits in a centred column
 * reads as two unrelated layouts, which is what it looked like before.
 */
.topbar {
    background: var(--bg-raised);
    border-bottom: 1px solid var(--line);
    position: sticky;
    top: 0;
    z-index: 20;
}

.topbar-inner {
    display: flex;
    align-items: center;
    gap: 18px;
    height: 56px;
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 22px;
    /* Nothing here may push the bar wider than the screen. Without this the
       rigid wordmark and the rigid right-hand group added up to about 460px
       inside a 346px phone, widening the whole document and giving every
       page a horizontal scrollbar. */
    min-width: 0;
}

/* The editor is a full-width workspace, so its bar should be too. */
.page-editor .topbar-inner { max-width: none; }

.topbar-right {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 12px;
}

.who {
    display: flex;
    align-items: center;
    gap: 9px;
    color: var(--text-dim);
    font-size: 14px;
    /* A long Discord name was pushing the bar off screen: this element had no
       minimum and nothing to truncate against, so it simply grew. */
    min-width: 0;
    overflow: hidden;
}
.who img { border-radius: 50%; }

.crumbs {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 14px;
    color: var(--text-mute);
    min-width: 0;
}
.crumbs a:hover { color: var(--text); }
.crumb-sep { color: var(--line); }
.crumb-current {
    color: var(--text);
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 40vw;
}

/* Shell --------------------------------------------------------------- */

.shell {
    max-width: 1200px;
    margin: 0 auto;
    padding: 34px 22px 80px;
}

.page-head { margin-bottom: 30px; }
.page-head h1 { font-size: 26px; margin-bottom: 5px; }
.sub { color: var(--text-dim); margin: 0; font-size: 14.5px; }

.empty {
    padding: 44px 30px;
    text-align: center;
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
}
.empty h2 { font-size: 18px; margin-bottom: 8px; }
.empty p { color: var(--text-dim); max-width: 52ch; margin: 0 auto; font-size: 14px; }

/* Server grid --------------------------------------------------------- */

.guild-section { margin-bottom: 42px; }

.section-title {
    display: flex;
    align-items: center;
    gap: 9px;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: .07em;
    color: var(--text-mute);
    margin-bottom: 6px;
}

.count {
    background: var(--bg-hover);
    color: var(--text-dim);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 1px 8px;
    font-size: 11px;
    letter-spacing: 0;
}

.section-note {
    color: var(--text-mute);
    font-size: 13.5px;
    margin: 0 0 16px;
}

.guild-grid {
    list-style: none;
    padding: 0;
    margin: 12px 0 0;
    display: grid;
    gap: 10px;
    /* 250px rather than 290px: with 28 servers the previous three-column
       layout ran ten rows deep. This gives four or five columns on a wide
       screen without the cards becoming cramped. */
    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}

.guild-card.is-hidden { display: none; }

.guild-card a {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 11px 13px;
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: var(--radius);
    transition: background-color .13s var(--ease),
                border-color .13s var(--ease),
                transform .13s var(--ease);
}

.guild-card.is-active a:hover {
    background: var(--bg-hover);
    border-color: var(--text-mute);
    transform: translateY(-1px);
}

/*
 * Faded, not disabled. These are the servers you invite the bot into, so
 * they must stay legible and obviously clickable. Raised from .42 to .55
 * for the light theme, where the lower value dropped body text below a
 * comfortable contrast ratio against white.
 */
.guild-card.is-absent a { opacity: .55; }
.guild-card.is-absent a:hover { opacity: 1; background: var(--bg-hover); }

/*
 * ONE size for both, defined once.
 *
 * The image was sized by width/height attributes in dashboard.php (56px) and
 * the initials fallback by CSS (44px), so a server with an icon rendered a
 * taller card than one without and the grid came out ragged. Two places
 * owning the same measurement is how they drift.
 *
 * The variable is what stops it happening again: the img rule and the
 * initials rule now read the same value, so changing it changes both.
 */
:root { --guild-icon: 52px; }

.guild-icon { flex: 0 0 auto; }

.guild-icon img,
.guild-initials {
    width: var(--guild-icon);
    height: var(--guild-icon);
    border-radius: 26%;
    display: block;

    /* Discord icons are square, but a cached or malformed one need not be.
       object-fit keeps a wrong aspect ratio inside the box rather than
       stretching the card around it. */
    object-fit: cover;
}

.guild-initials {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--bg-hover);
    border: 1px solid var(--line);
    color: var(--text-dim);
    font-weight: 700;
    font-size: 16px;

    /* The 1px border sits INSIDE the box, so this stays the same size as the
       borderless image. That comes from the global `* { box-sizing:
       border-box }` at the top of this file; without it the fallback would
       be 2px larger and the ragged grid would be back. */
}

/* The editor sidebar uses a smaller pair, and they must match each other
   for the same reason. */
.side-guild img,
.guild-initials.sm {
    width: 40px;
    height: 40px;
    font-size: 14px;
}

.guild-meta { min-width: 0; flex: 1; }

.guild-name {
    display: block;
    font-weight: 620;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.guild-role {
    display: block;
    font-size: 12.5px;
    color: var(--text-mute);
}

.guild-go {
    font-size: 12.5px;
    color: var(--text-mute);
    white-space: nowrap;
}
.guild-card.is-active a:hover .guild-go { color: var(--text); }

/* Editor -------------------------------------------------------------- */

/*
 * Three columns: navigation, workspace, members. The member rail fills what
 * was dead space on wide screens and, more usefully, gives you a live view
 * of who is actually in the server while you restructure it.
 */
.editor {
    display: grid;
    grid-template-columns: 248px minmax(0, 1fr) 260px;
    /* dvh, with vh behind it for anything that does not know dvh. On iOS
       100vh is the LARGE viewport, the one you get with the toolbars hidden,
       so a full-height column has its bottom underneath the browser chrome
       until you scroll. That bottom is where Publish lives. */
    min-height: calc(100vh - 56px);
    min-height: calc(100dvh - 56px);
}

@media (max-width: 1180px) {
    /* The rail is the first thing to go: it is context, not the task. */
    .editor { grid-template-columns: 248px minmax(0, 1fr); }
    .editor-members { display: none; }
}

.editor-side {
    background: var(--bg-raised);
    border-right: 1px solid var(--line);
    padding: 20px 16px;
    display: flex;
    flex-direction: column;
    gap: 22px;
    position: sticky;
    top: 56px;
    height: calc(100vh - 56px);
    height: calc(100dvh - 56px);
}

.side-guild {
    display: flex;
    align-items: center;
    gap: 11px;
    min-width: 0;
}
.side-guild img { border-radius: 26%; }

.side-name {
    font-weight: 650;
    font-size: 14.5px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 160px;
}
.side-sub { font-size: 12.5px; color: var(--text-mute); }

/* The module list is the part that scrolls.
 *
 * .editor-side is a fixed-height column, so anything past the bottom was
 * simply clipped: on a 900px window the Publish button sat below the edge
 * with no way to reach it, because nothing in the column could scroll. Making
 * the nav the flexible, scrolling child keeps the footer pinned and visible
 * whatever the window height. min-height: 0 is load-bearing, a flex child
 * will not shrink below its content without it. */
.side-nav {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* Room for the scrollbar so it does not sit on the tab labels. */
    margin-right: -6px;
    padding-right: 6px;
}

.side-group-title {
    padding: 0 12px 8px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-mute);
}

.side-tab {
    text-align: left;
    padding: 9px 12px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-dim);
    font: inherit;
    font-size: 14px;
    cursor: pointer;
}
.side-tab:hover { background: var(--bg-hover); color: var(--text); }

/* Section links inside a module read as links, not as blue text. */
a.side-tab { text-decoration: none; display: block; }

/*
 * Unbuilt modules.
 *
 * Dimmed AND inert, rather than dimmed alone. A greyed row that still
 * highlights on hover reads as a working control that failed, which is the
 * confusion the "Edit permissions" bug caused for real. No pointer, no hover
 * state, cursor stays default.
 */
.side-tab.is-soon {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    color: var(--text-mute);
    cursor: default;
}
.side-tab.is-soon:hover { background: transparent; color: var(--text-mute); }

/*
 * The "soon" badge, with a shine that passes across it now and then.
 *
 * STAGGERED, and that is the whole point. Four badges pulsing in unison
 * reads as one flashing element and pulls the eye hard; the same sweep at
 * different offsets reads as something quietly alive. The offsets are
 * deliberately not multiples of each other, so they drift apart rather than
 * resynchronising every few cycles.
 *
 * A long cycle with a short sweep. Most of the animation is the badge
 * sitting still, which is what keeps it from becoming the loudest thing in
 * a sidebar of otherwise static text.
 */
.side-soon {
    position: relative;
    flex: 0 0 auto;
    padding: 1px 6px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-mute);
    overflow: hidden;
    /* The sweep is drawn with a pseudo-element that leaves the layout
       alone. Clipping to the pill is what makes it read as light crossing
       the badge rather than a rectangle sliding past it. */
    isolation: isolate;
}

.side-soon::after {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    width: 55%;
    /* Starts fully outside, so nothing is visible between passes. */
    left: -70%;
    background: linear-gradient(
        100deg,
        transparent,
        var(--shine) 45%,
        var(--shine) 55%,
        transparent
    );
    pointer-events: none;
    animation: soon-shine 9s ease-in-out infinite;
}

/*
 * Four offsets, none a multiple of another. Equal spacing would have them
 * fire in a visible rotation, which is its own kind of pattern.
 */
.side-tab:nth-of-type(1) .side-soon::after { animation-delay: 0s; }
.side-tab:nth-of-type(2) .side-soon::after { animation-delay: 2.3s; }
.side-tab:nth-of-type(3) .side-soon::after { animation-delay: 5.1s; }
.side-tab:nth-of-type(4) .side-soon::after { animation-delay: 7.4s; }

@keyframes soon-shine {
    /* The sweep occupies the first fifth of the cycle. The rest is the
       badge at rest, which is what makes it occasional rather than busy. */
    0%   { left: -70%; }
    18%  { left: 115%; }
    100% { left: 115%; }
}

@media (prefers-reduced-motion: reduce) {
    .side-soon::after { animation: none; opacity: 0; }
}

.side-tab.is-current {
    background: var(--bg-hover);
    color: var(--text);
    font-weight: 600;
    box-shadow: inset 2px 0 0 var(--text);
}

.side-foot { margin-top: auto; flex: 0 0 auto; }

.staged-summary {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    color: var(--text-mute);
    margin-bottom: 10px;
}

.staged-count {
    background: var(--bg-hover);
    border: 1px solid var(--line);
    color: var(--text-dim);
    border-radius: 999px;
    padding: 1px 9px;
    font-weight: 700;
    font-family: var(--mono);
}

.side-hint {
    font-size: 12px;
    color: var(--text-mute);
    margin: 9px 0 0;
    line-height: 1.45;
}

.editor-main {
    padding: 26px 30px 70px;
    max-width: 900px;
    min-width: 0;
}

.panel { display: none; }
.panel.is-current { display: block; }

.panel-head { margin-bottom: 18px; }
.panel-head h2 { font-size: 19px; margin-bottom: 4px; }
.panel-sub { color: var(--text-dim); font-size: 13.5px; margin: 0; max-width: 68ch; }

/* Live indicator ------------------------------------------------------ */

.live-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--text-mute);
    display: inline-block;
}

/*
 * Monochrome removes colour as a status signal, so state is carried by
 * fill and ring instead: solid with a halo when live, hollow when
 * reconnecting, flat grey when offline. Shape reads at a glance and
 * survives colour blindness, which the old green/amber pair did not.
 */
.live-dot.is-ok {
    background: var(--text);
    box-shadow: 0 0 0 3px var(--bg-hover);
    animation: live-pulse 2s ease-in-out infinite;
}

/*
 * Only the live state animates. A dot that pulses whether or not anything
 * is arriving is decoration; one that stops when the connection goes stale
 * is information, and the absence of movement is then meaningful on its own.
 *
 * Opacity and halo, not size. Animating width or box-shadow spread forces
 * layout or paint work every frame next to a list that re-renders on a
 * timer; opacity and transform stay on the compositor.
 */
@keyframes live-pulse {
    0%, 100% { opacity: 1;   box-shadow: 0 0 0 3px var(--bg-hover); }
    50%      { opacity: .45; box-shadow: 0 0 0 5px transparent; }
}

/* Someone who set "reduce motion" asked for exactly that. The dot still
   reads as live through its solid fill and halo. */
@media (prefers-reduced-motion: reduce) {
    .live-dot.is-ok { animation: none; }
}
.live-dot.is-stale {
    background: transparent;
    border: 2px solid var(--text-dim);
    width: 10px;
    height: 10px;
}
.live-dot.is-off { background: var(--text-mute); }

.live-label {
    font-size: 12px;
    color: var(--text-mute);
}

/* Tree ---------------------------------------------------------------- */

.tree { display: flex; flex-direction: column; gap: 16px; }

.tree-group {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-card);
}

.tree-cat {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 10px 14px;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--line-soft);
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-dim);
}
.tree-cat-none { color: var(--text-mute); font-style: italic; text-transform: none; letter-spacing: 0; }
.tree-cat-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.tree-cat-count {
    background: var(--bg-hover);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 0 7px;
    font-size: 11px;
    letter-spacing: 0;
    color: var(--text-mute);
}

.tree-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 14px;
    border-bottom: 1px solid var(--line-soft);
    font-size: 14px;
}
.tree-row:last-child { border-bottom: 0; }
.tree-row:hover { background: var(--bg-hover); }

.tree-glyph {
    color: var(--text-mute);
    width: 17px;
    text-align: center;
    flex: 0 0 auto;
    font-size: 13px;
}

.tree-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.tree-empty {
    padding: 12px 14px;
    color: var(--text-mute);
    font-size: 13px;
    font-style: italic;
}

.tag {
    margin-left: auto;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    padding: 2px 7px;
    border-radius: 999px;
    background: var(--bg-hover);
    border: 1px solid var(--line);
    color: var(--text-mute);
    flex: 0 0 auto;
}
.tag + .tag { margin-left: 6px; }

/* Emphasis by weight and border style, not hue. */
.tag-nsfw { color: var(--text); border-color: var(--text-mute); }
.tag-perm { font-family: var(--mono); color: var(--text-dim); }
.tag-lock { color: var(--text-dim); border-style: dashed; }

/* Roles --------------------------------------------------------------- */

.role-list {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-card);
}

.role-row {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 9px 14px;
    border-bottom: 1px solid var(--line-soft);
    font-size: 14px;
}
.role-row:last-child { border-bottom: 0; }
.role-row:hover { background: var(--bg-hover); }
.role-row.is-locked { opacity: .55; }

/*
 * The one intentional exception to monochrome. A role's colour is data the
 * user chose and needs to recognise, so the swatch shows it as-is. The
 * ring keeps low-contrast colours visible against both backgrounds.
 */
.role-swatch {
    width: 11px;
    height: 11px;
    border-radius: 50%;
    flex: 0 0 auto;
    box-shadow: 0 0 0 1px var(--line);
}

.role-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.role-pos {
    margin-left: auto;
    font-family: var(--mono);
    font-size: 12px;
    color: var(--text-mute);
    flex: 0 0 auto;
}
.role-row .tag { margin-left: 0; }

/* Overwrite summary --------------------------------------------------- */

.overwrite-summary {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-card);
}

.ow-total {
    padding: 12px 14px;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--line-soft);
    font-size: 13.5px;
    color: var(--text-dim);
}
.ow-total strong { color: var(--text); font-family: var(--mono); }

/* A button now, not a div: the row expands, so it has to be reachable by
   keyboard and announced as a control. */
.ow-row {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 8px 14px;
    border: 0;
    border-bottom: 1px solid var(--line-soft);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 14px;
    text-align: left;
    cursor: pointer;
}
.ow-row:last-child { border-bottom: 0; }
.ow-row:hover { background: var(--bg-hover); }

.ow-caret { flex: 0 0 auto; color: var(--text-mute); font-size: 11px; }

.ow-detail {
    border-bottom: 1px solid var(--line-soft);
    background: var(--bg);
}

.ow-place {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 7px 14px 7px 34px;
    border: 0;
    background: transparent;
    color: var(--text-dim);
    font: inherit;
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
}
.ow-place:hover { background: var(--bg-hover); color: var(--text); }

.ow-place-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ow-place-note { margin-left: auto; font-size: 12px; color: var(--text-mute); }

.ow-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ow-count { margin-left: auto; font-size: 12.5px; color: var(--text-mute); font-family: var(--mono); }

/* Responsive ---------------------------------------------------------- */

/* ── Small screens: the sidebar becomes a drawer ────────────────────────
 *
 * WHAT WAS HERE BEFORE, AND WHY IT STOPPED WORKING
 *
 * This block used to say `.side-nav { flex-direction: row }`, which turned
 * the column of tabs into a horizontal strip. That was right when the nav
 * was a flat run of buttons. It is not a flat run any more: each child is
 * now a .tree-module containing a head and an indented list of children, so
 * turning the container into a row laid whole MODULES side by side and
 * produced a sideways-scrolling forest of little vertical trees.
 *
 * It also quietly disabled the nav's own scrolling, because `height: auto`
 * on .editor-side left the flexible child nothing to fill.
 *
 * So the nav slides in over the page instead, which is what a navigation
 * list of this shape wants on a phone, and the footer holding the staged
 * count and Publish stays where it is inside it.
 */
@media (max-width: 820px) {
    .editor { grid-template-columns: minmax(0, 1fr); }

    .editor-side {
        position: fixed;
        top: 56px;
        left: 0;
        bottom: 0;
        z-index: 150;
        width: min(300px, 86vw);
        border-right: 1px solid var(--line);
        border-bottom: 0;
        /* Off screen, not display:none, so it animates and so the controls
           inside it keep their state while hidden. */
        transform: translateX(-100%);
        transition: transform .22s ease;
        box-shadow: 0 0 40px var(--btn-shadow);
    }
    .editor.is-nav-open .editor-side { transform: translateX(0); }

    /* Only present below the breakpoint, and only once the nav is open. */
    .nav-scrim {
        position: fixed;
        inset: 56px 0 0;
        z-index: 140;
        background: rgba(0, 0, 0, .5);
        opacity: 0;
        pointer-events: none;
        transition: opacity .22s ease;
    }
    .editor.is-nav-open .nav-scrim { opacity: 1; pointer-events: auto; }

    .nav-toggle { display: inline-flex; }

    .editor-main { padding: 18px 14px 60px; }
    .crumb-current { max-width: 30vw; }
}

/* The toggle exists only where the drawer does. Above the breakpoint the
   sidebar is always on screen and a button to reveal it would do nothing. */
.nav-toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    flex: 0 0 auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text);
    cursor: pointer;
    font-size: 15px;
    line-height: 1;
}
.nav-toggle:hover { background: var(--bg-hover); }

@media (min-width: 821px) {
    /* Never rendered above the breakpoint, but the scrim is created once and
       reused, so it has to be inert here rather than merely transparent. */
    .nav-scrim { display: none; }
}

/* ── Phones ────────────────────────────────────────────────────────────
 *
 * Everything below is a width problem, not a taste problem: each rule fixes
 * something that overflowed the screen or could not be read.
 */
@media (max-width: 620px) {
    /* The topbar. Its two rigid ends added up to more than a phone is wide. */
    .topbar-inner { gap: 10px; padding: 0 12px; }
    .wordmark span:last-child { display: none; }   /* the mark alone is enough */
    .wordmark.is-double .mark img:last-child,
    .wordmark.is-double .mark img:first-child { width: 24px; height: 24px; }
    .crumbs { font-size: 13px; gap: 6px; }
    .crumb-current { max-width: 38vw; }
    .live-label { display: none; }                 /* the dot carries it */
    .who span { display: none; }                   /* avatar only */
    .topbar-right { gap: 8px; }

    .shell { padding: 22px 14px 60px; }

    /* Card padding was eating an eighth of the screen. */
    .am-section { padding: 15px; }
    .am-master { padding: 14px 15px; }

    /*
     * The command body was inset 46px by margin and 16px by padding: 62px of
     * a 358px screen spent on indentation. The old 760 rule trimmed the
     * padding by 2px and left the margin alone.
     */
    .cmd-body { margin-left: 14px; padding-left: 12px; }
    .cmd-head { padding: 12px 12px; gap: 10px; }
    .pick-list { padding-left: 0; }

    /* Two of three tracks were unusably narrow rather than overflowing. */
    .reason-row { grid-template-columns: minmax(0, 1fr); }
    .sub { grid-template-columns: minmax(0, 1fr) auto; row-gap: 8px; }
    .sub-alias { width: 100%; grid-column: 1 / -1; }

    /* Discord collapses inline embed fields on a phone. So does the preview,
       or it would be showing something that will not happen. */
    .dp-fields { grid-template-columns: minmax(0, 1fr); }
    .dp-field.is-inline { grid-column: 1 / -1; }

    /* Below 1100 the preview sits under the form rather than beside it, and a
       sticky element in a full-width column pins itself under the topbar and
       stays there while the whole form scrolls past. */
    .util-preview-inner { position: static; }

    .rule-action .select { min-width: 0; }
    .drawer { width: 100vw; border-left: 0; }
}

/* ── Touch ─────────────────────────────────────────────────────────────
 *
 * Keyed on the POINTER, not the width. A 1024px tablet has the room for
 * small controls and not the precision, and a narrow desktop window has the
 * precision without the room. Width is the wrong question for a tap target.
 */
@media (pointer: coarse) {
    /*
     * THE ONE THAT MATTERS.
     *
     * Deny, inherit and allow, three 34x26 buttons with no gap between them,
     * 34px apart centre to centre. A mis-tap here does not do nothing: it
     * sets the opposite of what was intended, on a permission, for every
     * role selected. Everything else in this block is comfort.
     */
    .tri-btn { width: 46px; height: 40px; font-size: 15px; }

    .icon-btn { width: 40px; height: 40px; }
    .drawer-close { width: 40px; height: 40px; font-size: 22px; }
    .banner-close { width: 36px; height: 36px; }
    .filter-clear { width: 32px; height: 32px; }
    .theme-toggle { width: 40px; height: 40px; }
    .row-menu { width: 36px; height: 36px; }
    .ctx-item { padding: 11px 12px; }

    /* A 16px thumb on a 22px track, dragged with a fingertip. */
    .slider { height: 40px; }
    .slider::-webkit-slider-thumb { width: 26px; height: 26px; margin-top: -11px; }
    .slider::-moz-range-thumb { width: 26px; height: 26px; }
    /* Without this the page claims any drag that starts even slightly off
       the horizontal, and the slider never moves. */
    .slider { touch-action: none; }

    /*
     * 16px, and not for looks.
     *
     * iOS zooms the page whenever a focused control is under 16px. Every
     * input, select and textarea here was 12 to 14, so tapping any field
     * zoomed the layout and left the reader in a horizontally scrolling page
     * they then had to pinch back out of. Raising the size removes the whole
     * behaviour at the source.
     */
    .settings-input,
    .select,
    .ms-search,
    .filter input,
    .edit-field,
    .sub-alias,
    .tree-cat .edit-field { font-size: 16px; }

    .side-tab, .tree-head { padding-top: 11px; padding-bottom: 11px; }
}

/* Toolbar and filter -------------------------------------------------- */

/*
 * Added once the server list got long enough to scroll. With 28 servers a
 * grid alone means hunting by eye through ten rows; typing three letters
 * is faster than any amount of visual scanning.
 */
.toolbar {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 26px;
    flex-wrap: wrap;
}

.filter {
    position: relative;
    flex: 1 1 280px;
    max-width: 360px;
}

.filter svg {
    position: absolute;
    left: 11px;
    top: 50%;
    transform: translateY(-50%);
    width: 14px;
    height: 14px;
    color: var(--text-mute);
    pointer-events: none;
}

.filter input {
    width: 100%;
    padding: 9px 12px 9px 33px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text);
    font: inherit;
    font-size: 14px;
}
.filter input::placeholder { color: var(--text-mute); }
.filter input:focus {
    outline: none;
    border-color: var(--text-mute);
}

.filter-clear {
    position: absolute;
    right: 6px;
    top: 50%;
    transform: translateY(-50%);
    width: 22px;
    height: 22px;
    display: none;
    align-items: center;
    justify-content: center;
    border: 0;
    border-radius: 4px;
    background: transparent;
    color: var(--text-mute);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
}
.filter-clear:hover { background: var(--bg-hover); color: var(--text); }
.filter.has-value .filter-clear { display: flex; }

.filter-count {
    font-size: 13px;
    color: var(--text-mute);
    white-space: nowrap;
}

/* Shown only when a filter matches nothing at all. */
.no-matches {
    display: none;
    padding: 30px 20px;
    text-align: center;
    color: var(--text-mute);
    font-size: 14px;
    border: 1px dashed var(--line);
    border-radius: var(--radius);
}
.no-matches.is-shown { display: block; }

/* A section whose every card is filtered out hides its heading too,
   rather than leaving a title floating above nothing. */
.guild-section.is-empty { display: none; }

/* Editable fields ----------------------------------------------------- */

/*
 * Inputs that do not look like inputs until you interact with them. A tree
 * of thirty boxed text fields reads as a form to fill in; the point here is
 * a server you can touch, so the affordance stays quiet until hover.
 */
.edit-field {
    flex: 1;
    min-width: 0;
    padding: 3px 6px;
    margin: -3px 0;
    border: 1px solid transparent;
    border-radius: 4px;
    background: transparent;
    color: inherit;
    font: inherit;
    font-size: inherit;
}
.edit-field:hover { border-color: var(--line); }
.edit-field:focus {
    outline: none;
    border-color: var(--text-mute);
    background: var(--bg);
}

/* Categories are uppercase labels, so their input has to match. */
.tree-cat .edit-field {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
}

/*
 * A changed field is marked by a bar in the gutter rather than a colour,
 * since the palette is monochrome and colour is not available as a signal.
 * Weight change alone was too subtle to catch while scanning.
 */
.edit-field.is-changed { font-weight: 650; }

.tree-row.is-changed,
.tree-cat.is-changed,
.role-row.is-changed {
    box-shadow: inset 2px 0 0 var(--text);
    background: var(--bg-hover);
}

.edit-check {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font-size: 11px;
    color: var(--text-mute);
    cursor: pointer;
    flex: 0 0 auto;
    user-select: none;
}
.edit-check input { margin: 0; cursor: pointer; accent-color: var(--text); }
.edit-check:hover { color: var(--text-dim); }

.link-btn {
    margin-left: auto;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text-mute);
    font: inherit;
    font-size: 12px;
    text-decoration: underline;
    cursor: pointer;
}
.link-btn:hover { color: var(--text); }

.side-foot .btn + .btn { margin-top: 8px; }

/* Plan preview -------------------------------------------------------- */

.plan-panel {
    margin-top: 26px;
    padding-top: 22px;
    border-top: 1px solid var(--line);
}

.plan-head { margin-bottom: 16px; }
.plan-head h3 { font-size: 17px; margin-bottom: 3px; }
.plan-sub { color: var(--text-dim); font-size: 13.5px; margin: 0; }

.plan-note {
    padding: 11px 14px;
    margin-bottom: 12px;
    border: 1px solid var(--line);
    border-left: 3px solid var(--text-mute);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-dim);
    font-size: 13px;
    line-height: 1.55;
}
.plan-note strong { color: var(--text); }
.plan-note p { margin: 7px 0 0; }
.plan-note-warn { border-left-color: var(--text); }
.plan-note-skip { border-left-style: dashed; }

.plan-saving {
    font-family: var(--mono);
    font-size: 11.5px;
    color: var(--text-mute);
    white-space: nowrap;
}

.plan-phase {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
    margin-bottom: 12px;
    overflow: hidden;
}

.plan-phase-head {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 9px 14px;
    background: var(--bg-raised);
    border-bottom: 1px solid var(--line-soft);
}

.plan-phase-title {
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--text-dim);
}

.plan-phase-count {
    margin-left: auto;
    font-family: var(--mono);
    font-size: 11.5px;
    color: var(--text-mute);
}

.plan-phase-note {
    margin: 0;
    padding: 9px 14px 0;
    font-size: 12px;
    color: var(--text-mute);
    line-height: 1.5;
}

.plan-ops {
    list-style: none;
    margin: 0;
    padding: 8px 0;
}

.plan-ops li {
    padding: 5px 14px;
    font-size: 13.5px;
    color: var(--text-dim);
}
.plan-ops li:hover { background: var(--bg-hover); color: var(--text); }

/* Destructive operations in the plan preview --------------------------- */

.plan-danger {
    border: 1px solid var(--text-mute);
    border-radius: var(--radius);
    background: var(--bg-card);
    padding: 14px 16px;
    margin-bottom: 16px;
}

.plan-danger h4 {
    margin: 0 0 4px;
    font-size: 14px;
    font-weight: 700;
}

.plan-danger p { margin: 0 0 10px; font-size: 13px; color: var(--text-dim); }

.plan-danger ul { list-style: none; margin: 0; padding: 0; }

.plan-danger li {
    padding: 9px 0;
    border-top: 1px solid var(--line-soft);
    font-size: 13.5px;
}
.plan-danger li strong { display: block; color: var(--text); }
.plan-danger li span {
    display: block;
    margin-top: 3px;
    color: var(--text-mute);
    font-size: 12.5px;
    line-height: 1.5;
}

/* Row controls -------------------------------------------------------- */

/*
 * Rows are draggable and carry a hover-revealed three-dot control. Nothing
 * is permanently visible except the name, which keeps a forty-channel tree
 * readable rather than turning it into a wall of buttons.
 */
.tree-row[draggable="true"],
.tree-cat[draggable="true"] { cursor: grab; }
.tree-row.is-dragging,
.tree-cat.is-dragging { opacity: .4; cursor: grabbing; }

/* Drop indicators are lines rather than highlights, so it is unambiguous
   whether the channel lands above or below the row under the cursor. */
.is-drop-before { box-shadow: inset 0 2px 0 var(--text); }
.is-drop-after  { box-shadow: inset 0 -2px 0 var(--text); }
.is-drop-into   { background: var(--bg-active); box-shadow: inset 0 0 0 1px var(--text); }

.row-menu {
    flex: 0 0 auto;
    width: 22px;
    height: 22px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    border-radius: 4px;
    background: transparent;
    color: var(--text-mute);
    font-size: 15px;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
}
.tree-row:hover .row-menu,
.tree-cat:hover .row-menu,
.role-row:hover .row-menu,
.row-menu:focus-visible { opacity: 1; }
.row-menu:hover { background: var(--bg-active); border-color: var(--line); color: var(--text); }

.tree-add {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    padding: 10px 2px 0;
}

.tree-add-btn {
    padding: 5px 10px;
    border: 1px dashed var(--line);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-mute);
    font: inherit;
    font-size: 12.5px;
    cursor: pointer;
}
.tree-add-btn:hover { border-style: solid; color: var(--text); background: var(--bg-hover); }

.tree-row.is-deleted,
.tree-cat.is-deleted,
.role-row.is-deleted { opacity: .45; }
.tree-row.is-deleted .edit-field,
.tree-cat.is-deleted .edit-field,
.role-row.is-deleted .edit-field,
.role-row.is-deleted .role-name { text-decoration: line-through; }

.tree-row.is-new,
.tree-cat.is-new,
.role-row.is-new { box-shadow: inset 2px 0 0 var(--text-dim); }

.tag-new { color: var(--text); border-style: dashed; }
.tag-del { color: var(--text); border-color: var(--text-mute); }

/* Context menu -------------------------------------------------------- */

.ctx {
    position: fixed;
    z-index: 200;
    min-width: 190px;
    /* It now carries Move to <category> for every category on the server, so
       it can be taller than the screen. And on a narrow screen 190px of menu
       placed near the right edge needs somewhere to go. */
    max-width: calc(100vw - 24px);
    max-height: calc(100dvh - 24px);
    overflow-y: auto;
    padding: 5px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    box-shadow: 0 8px 28px -8px rgba(0, 0, 0, .7);
}

.ctx-item {
    display: block;
    width: 100%;
    padding: 7px 10px;
    border: 0;
    border-radius: 4px;
    background: none;
    color: var(--text-dim);
    font: inherit;
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
}

/*
 * Full strength, not --text-dim.
 *
 * Dimmed text is how the rest of this interface says "you cannot use this",
 * and these items are perfectly usable. Sitting next to the destructive
 * item, which is deliberately brighter and heavier, made a working control
 * read as disabled.
 */
.ctx-item { color: var(--text); }
.ctx-item:hover { background: var(--bg-hover); }

/* Monochrome removes red as a warning, so the destructive item is set
   apart by weight and a rule above it instead. */
.ctx-item.is-danger { font-weight: 650; color: var(--text); }
.ctx-item.is-danger:hover { background: var(--bg-active); }

.ctx-sep { height: 1px; margin: 5px 4px; background: var(--line); }

/* Permission drawer --------------------------------------------------- */

.drawer-backdrop {
    position: fixed;
    inset: 0;
    z-index: 190;
    background: rgba(0, 0, 0, .5);
}

.drawer {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    z-index: 195;
    width: min(560px, 100vw);
    display: flex;
    flex-direction: column;
    background: var(--bg);
    border-left: 1px solid var(--line);
}

.drawer-head {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    padding: 18px 20px;
    border-bottom: 1px solid var(--line);
}

.drawer-title { font-size: 16px; font-weight: 650; }
.drawer-sub { font-size: 12.5px; color: var(--text-mute); margin-top: 3px; }

.drawer-close {
    margin-left: auto;
    width: 28px;
    height: 28px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-dim);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
}
.drawer-close:hover { background: var(--bg-hover); color: var(--text); }

/* overscroll-behavior: reaching the end of this list used to hand the scroll
   to the editor underneath, so the page moved behind an open drawer. */
.drawer-body {
    flex: 1;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 16px 20px calc(40px + env(safe-area-inset-bottom, 0px));
}

.perm-roles {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    padding-bottom: 16px;
    margin-bottom: 16px;
    border-bottom: 1px solid var(--line);
}

.perm-role {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 5px 10px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: transparent;
    color: var(--text-dim);
    font: inherit;
    font-size: 12.5px;
    cursor: pointer;
}
.perm-role:hover { background: var(--bg-hover); color: var(--text); }
.perm-role.is-current {
    background: var(--invert-bg);
    color: var(--invert-fg);
    border-color: var(--invert-bg);
}

.perm-role-count {
    font-family: var(--mono);
    font-size: 10.5px;
    opacity: .7;
}

.perm-group { margin-bottom: 22px; }

.perm-group-title {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--text-mute);
    margin-bottom: 8px;
}

.perm-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 7px 0;
    border-bottom: 1px solid var(--line-soft);
    font-size: 13.5px;
}
.perm-row:last-child { border-bottom: 0; }
.perm-row > span:first-child { flex: 1; min-width: 0; }

/*
 * Deny / inherit / allow, left to right, matching Discord's own control.
 * Keeping their order and their wording means an admin does not have to
 * work out which end is which.
 */
.tri {
    display: inline-flex;
    flex: 0 0 auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    overflow: hidden;
}

.tri-btn {
    width: 34px;
    height: 26px;
    border: 0;
    background: transparent;
    color: var(--text-mute);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
}
.tri-btn + .tri-btn { border-left: 1px solid var(--line); }
.tri-btn:hover { background: var(--bg-hover); color: var(--text); }
.tri-btn.is-on { background: var(--invert-bg); color: var(--invert-fg); font-weight: 700; }

/* Member rail --------------------------------------------------------- */

.editor-members {
    border-left: 1px solid var(--line);
    background: var(--bg-raised);
    position: sticky;
    top: 56px;
    height: calc(100vh - 56px);
    display: flex;
    flex-direction: column;
}

.members-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 16px 16px 10px;
    border-bottom: 1px solid var(--line);
}

.members-title {
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-mute);
}

.members-count {
    margin-left: auto;
    font-family: var(--mono);
    font-size: 11.5px;
    color: var(--text-mute);
}

.members-body { flex: 1; overflow-y: auto; padding: 10px 8px 24px; }

.members-empty {
    padding: 14px 8px;
    font-size: 12.5px;
    color: var(--text-mute);
}

.mgroup { margin-bottom: 14px; }

.mgroup-head {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-mute);
    padding: 0 8px 6px;
}
.mgroup-count { opacity: .6; font-family: var(--mono); }

.mrow {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 13px;
}
.mrow:hover { background: var(--bg-hover); }

.mrow img,
.mavatar {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    flex: 0 0 auto;
    background: var(--bg-active);
}

.mname {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-dim);
}

.mbot {
    font-size: 9px;
    font-weight: 700;
    letter-spacing: .05em;
    padding: 1px 4px;
    border-radius: 3px;
    background: var(--bg-active);
    color: var(--text-mute);
    flex: 0 0 auto;
}

/* Dismissible banners -------------------------------------------------- */

/*
 * The close control is revealed on hover rather than always shown. A
 * permanent X on an informational banner adds a second thing to look at
 * before you have read the first.
 */
.banner.is-dismissible {
    position: relative;
    padding-right: 42px;
}

.banner-close {
    position: absolute;
    top: 8px;
    right: 8px;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: 1px solid transparent;
    border-radius: 4px;
    background: transparent;
    color: var(--text-mute);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
    opacity: 0;
}

.banner.is-dismissible:hover .banner-close,
.banner-close:focus-visible { opacity: 1; }

.banner-close:hover {
    background: var(--bg-active);
    border-color: var(--line);
    color: var(--text);
}

/* Touch devices have no hover, so the control has to be permanent there. */
@media (hover: none) {
    .banner-close { opacity: 1; }
}

/* Role hierarchy ------------------------------------------------------- */

/*
 * The list IS the hierarchy. Highest role at the top, drag to change rank.
 * A grip makes it obvious the row can be moved, since a draggable row with
 * no affordance is a feature nobody finds.
 */
.role-grip {
    flex: 0 0 auto;
    width: 14px;
    color: var(--text-mute);
    font-size: 11px;
    letter-spacing: -2px;
    cursor: grab;
    opacity: 0;
    user-select: none;
}
.role-row:hover .role-grip { opacity: 1; }
.role-row.is-dragging { opacity: .4; }
.role-row.is-dragging .role-grip { cursor: grabbing; }

/* Locked roles cannot be reordered, so their grip is inert and stays dim. */
.role-grip.is-locked { cursor: default; opacity: .35; }
.role-row:hover .role-grip.is-locked { opacity: .35; }

/*
 * The native colour input is hidden behind the swatch. It gives us the
 * platform picker for free, which is better than anything hand-rolled and
 * already familiar on every OS.
 */
.role-colour {
    position: relative;
    display: inline-flex;
    flex: 0 0 auto;
    cursor: pointer;
    line-height: 0;
}

.role-colour .colour-input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    border: 0;
    padding: 0;
    cursor: pointer;
}

.role-colour .role-swatch { width: 13px; height: 13px; }
.role-colour:hover .role-swatch { box-shadow: 0 0 0 2px var(--text-mute); }

/* Publish progress ----------------------------------------------------- */

.job { padding-top: 4px; }

.job-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 10px;
}
.job-head h3 { font-size: 17px; }

.job-count {
    margin-left: auto;
    font-family: var(--mono);
    font-size: 12.5px;
    color: var(--text-mute);
}

.job-bar {
    height: 3px;
    border-radius: 2px;
    background: var(--bg-active);
    overflow: hidden;
    margin-bottom: 16px;
}
.job-bar span {
    display: block;
    height: 100%;
    background: var(--text);
    transition: width .3s var(--ease);
}

.job-steps {
    list-style: none;
    margin: 14px 0 0;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
    overflow: hidden;
}

.job-step {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 7px 14px;
    border-bottom: 1px solid var(--line-soft);
    font-size: 13.5px;
    color: var(--text-mute);
}
.job-step:last-child { border-bottom: 0; }

.job-mark {
    flex: 0 0 auto;
    width: 14px;
    font-family: var(--mono);
    text-align: center;
}

.job-step.is-done { color: var(--text-dim); }
.job-step.is-running { color: var(--text); background: var(--bg-hover); }

/* Monochrome, so a failure is marked by weight and a gutter bar rather
   than by colour. */
.job-step.is-failed {
    color: var(--text);
    font-weight: 600;
    background: var(--bg-hover);
    box-shadow: inset 3px 0 0 var(--text);
}
.job-step.is-destructive .job-mark { font-weight: 700; color: var(--text); }

.job-error {
    display: block;
    width: 100%;
    padding-left: 24px;
    margin-top: 3px;
    font-size: 12.5px;
    font-weight: 400;
    color: var(--text-mute);
}

/* Mark ----------------------------------------------------------------- */

/*
 * Two images rather than one plus a CSS filter.
 *
 * `filter: invert(1)` on a black PNG produces white, but it also inverts
 * any antialiasing at the edges, so the mark picks up a faint halo against
 * the opposite background. Shipping both artworks costs a few KB and is
 * simply correct.
 *
 * Which one shows is decided in CSS, not JavaScript, so the right mark is
 * painted on the very first frame and never swaps visibly after load.
 */
.wordmark {
    display: inline-flex;
    align-items: center;
    gap: 9px;
}

.mark {
    display: inline-flex;
    flex: 0 0 auto;
    line-height: 0;
}

.mark img {
    display: block;
    width: 26px;
    height: 26px;
}

/*
 * `.mark img.mark-black`, not `.mark-black`.
 *
 * `.mark img` is specificity (0,1,1) and a bare `.mark-black` is (0,1,0),
 * so the sizing rule above beat the hiding rule and BOTH marks rendered.
 * The theme swap was never running at all. Exactly the same trap as
 * `.drawer { display: flex }` beating `[hidden]`, and worth noticing that
 * it caught me twice in one file.
 *
 * Matching the element as well puts these at (0,2,1) and they win properly.
 */
.mark img.mark-black { display: none; }
.mark img.mark-white { display: block; }

:root[data-theme="light"] .mark img.mark-black { display: block; }
:root[data-theme="light"] .mark img.mark-white { display: none; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .mark img.mark-black { display: block; }
    :root:not([data-theme]) .mark img.mark-white { display: none; }
}

/*
 * Both marks at once, on purpose.
 *
 * This started as the specificity bug above, kev liked how it looked, and
 * it stays as an explicit opt-in rather than an accident that a future
 * tidy-up would silently "fix". Remove `is-double` from the wordmark in
 * the PHP to go back to one.
 */
.wordmark.is-double .mark img.mark-black,
.wordmark.is-double .mark img.mark-white { display: block; }

.wordmark.is-double .mark { gap: 4px; }

/* The editor's bar is tighter than the landing header. */
.topbar .mark img { width: 22px; height: 22px; }

/* Multi-select permissions --------------------------------------------- */

.perm-bar {
    display: flex;
    align-items: center;
    gap: 12px;
    padding-bottom: 14px;
    margin-bottom: 16px;
    border-bottom: 1px solid var(--line);
    font-size: 12.5px;
}

.perm-scope {
    color: var(--text);
    font-weight: 600;
}
.perm-bar .segmented { margin-left: auto; }

/* Segmented control ---------------------------------------------------- */

/*
 * A pill with a thumb that slides between the two options.
 *
 * inline-grid with two equal columns, not flex. The thumb is positioned as a
 * fraction of the container, so the columns have to be exactly half each;
 * with flex they would size to their own labels, "All roles" and "One role"
 * would come out slightly different, and the thumb would land a few pixels
 * off the segment it is meant to sit under.
 */
.segmented {
    position: relative;
    display: inline-grid;
    grid-template-columns: 1fr 1fr;
    padding: 3px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg-hover);
}

.segmented-thumb {
    position: absolute;
    top: 3px;
    bottom: 3px;
    left: 3px;
    width: calc(50% - 3px);
    border-radius: 999px;
    background: var(--invert-bg);
    transition: transform .22s var(--ease), opacity .16s var(--ease);
}

.segmented-thumb[data-at="1"] { transform: translateX(100%); }

/*
 * Hidden when the selection is neither all nor one.
 *
 * Any number of roles can be selected, so "3 of 5" is a real state and this
 * control has no segment for it. Lighting one anyway would claim a selection
 * the drawer is not in, and this pill is the only summary of it on screen.
 */
.segmented.is-custom .segmented-thumb { opacity: 0; }

.segmented-btn {
    position: relative;
    z-index: 1;
    padding: 5px 15px;
    border: 0;
    border-radius: 999px;
    background: transparent;
    color: var(--text-dim);
    font: inherit;
    font-size: 12.5px;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
    transition: color .16s var(--ease);
}

.segmented-btn:hover { color: var(--text); }

/* Sits on the thumb, so it takes the inverted foreground. */
.segmented-btn.is-on,
.segmented-btn.is-on:hover { color: var(--invert-fg); }

@media (prefers-reduced-motion: reduce) {
    .segmented-thumb,
    .segmented-btn { transition: none; }
}

/*
 * A mixed control means the selected roles disagree about this permission.
 * Monochrome has no colour to spend on it, so it is marked with a dashed
 * rule and an inline word rather than a tint nobody would decode.
 */
.perm-row.is-mixed { border-bottom-style: dashed; }

.perm-mixed {
    margin-left: 8px;
    font-style: normal;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-mute);
}

/* Nothing is "on" when the roles disagree, so no segment is filled. That
   is the honest rendering: picking one would imply a value that is not
   actually set. */
.perm-row.is-mixed .tri { border-style: dashed; }

.perm-role[aria-pressed="true"] .perm-role-count { opacity: .85; }


/* Hero actions -------------------------------------------------------- */

/*
 * Wraps rather than shrinking. Two long labels side by side overflow a phone,
 * and a button that has been squeezed until its text truncates is worse than
 * one that has moved to its own line.
 */
/*
 * flex-start, matching the rest of the hero.
 *
 * This was centred, which put the two buttons in the middle of a 620px block
 * while the heading, lede and fineprint all started hard left. The result was
 * a visible void to the right of the buttons and an edge that lined up with
 * nothing. Everything in the hero shares one left edge now.
 */
.hero-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 12px;
    margin-bottom: 18px;
}

@media (max-width: 460px) {
    .hero-actions { flex-direction: column; align-items: stretch; }
}


/* Toast --------------------------------------------------------------- */

/*
 * Bottom centre, above everything, and never interactive. pointer-events is
 * none so a toast appearing over a control cannot swallow the click that was
 * already on its way to it.
 */
.toast {
    position: fixed;
    left: 50%;
    /* Clear of the home indicator and Safari's bottom bar. */
    bottom: calc(28px + env(safe-area-inset-bottom, 0px));
    /* The base rule had no maximum. Centred by a -50% translate, a long
       message overflowed symmetrically off both sides of a phone. */
    max-width: calc(100vw - 32px);
    transform: translateX(-50%) translateY(8px);
    z-index: 300;
    padding: 10px 16px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text);
    font-size: 13.5px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
    opacity: 0;
    pointer-events: none;
    transition: opacity .16s var(--ease), transform .16s var(--ease);
}

.toast.is-on {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

@media (prefers-reduced-motion: reduce) {
    .toast { transition: none; }
}

/* Stopped job --------------------------------------------------------- */

/*
 * Resume sits beside the explanation rather than beneath it. The action
 * belongs with the sentence telling you to take it.
 */
.job-stopped {
    display: flex;
    align-items: flex-start;
    gap: 16px;
}

.job-stopped-text { flex: 1; min-width: 0; }

.job-stopped-action { flex: 0 0 auto; margin-top: 2px; }

@media (max-width: 620px) {
    .job-stopped { flex-direction: column; }
    .job-stopped-action { align-self: flex-start; }
}

/* Blocked add buttons ------------------------------------------------- */

.tree-add-btn.is-blocked {
    opacity: .5;
    cursor: default;
}
.tree-add-btn.is-blocked:hover { background: transparent; }

.tree-note {
    margin: 10px 0 0;
    font-size: 12.5px;
    color: var(--text-mute);
    max-width: 62ch;
}


/* Channel settings ---------------------------------------------------- */

.settings { display: flex; flex-direction: column; gap: 20px; }

.settings-row {
    display: flex;
    flex-direction: column;
    gap: 7px;
}

/* The toggle reads as a row, not a stack: the control belongs beside its
   label rather than under it. */
.settings-toggle {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    cursor: pointer;
}

.settings-label {
    display: flex;
    flex-direction: column;
    gap: 2px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-dim);
}

.settings-label em {
    font-style: normal;
    font-size: 12.5px;
    font-weight: 400;
    letter-spacing: 0;
    text-transform: none;
    color: var(--text-mute);
}

.settings-input {
    width: 100%;
    padding: 9px 11px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 14px;
}

.settings-input:focus-visible { border-color: var(--text-dim); }

textarea.settings-input { resize: vertical; min-height: 66px; }

/* Only the plain inputs need pinning now. The switch sizes itself, and
   the rule that used to size a raw checkbox here beat `.switch input` on
   specificity and squashed the hit area to 18px. */
.settings-toggle .settings-input {
    width: auto;
    flex: 0 0 auto;
}


/* Audit log ----------------------------------------------------------- */

.audit-list { display: flex; flex-direction: column; gap: 12px; }

.audit-empty {
    padding: 22px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
    color: var(--text-mute);
    font-size: 14px;
}

.audit-job {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
    overflow: hidden;
}

/*
 * A staff change is marked with a rule down the edge, not a colour.
 *
 * The palette is greyscale, so weight and shape carry meaning here. This is
 * the one thing on the page that must be noticeable at a glance: somebody
 * outside your server changed it.
 */
.audit-job.is-staff { box-shadow: inset 3px 0 0 var(--text); }

.audit-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 11px 14px;
    border-bottom: 1px solid var(--line-soft);
}

.audit-actor {
    display: flex;
    align-items: center;
    gap: 8px;
    font-weight: 650;
    font-size: 14px;
}

.audit-badge {
    padding: 1px 7px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-dim);
}

.audit-when {
    margin-left: auto;
    font-size: 12.5px;
    color: var(--text-mute);
    white-space: nowrap;
}

.audit-items { margin: 0; padding: 8px 14px 10px 30px; }

.audit-item {
    font-size: 13.5px;
    color: var(--text-dim);
    line-height: 1.7;
}
.audit-item.is-destructive { color: var(--text); font-weight: 600; }

.audit-note {
    padding: 8px 14px;
    border-top: 1px solid var(--line-soft);
    font-size: 12.5px;
    color: var(--text-mute);
}


/* Staff access -------------------------------------------------------- */

/*
 * Heavier than the other banners on purpose. Everything else on this page
 * describes the server; this describes the person reading it, and it is the
 * one notice that must not fade into the layout.
 */
.banner-staff {
    border-left-width: 4px;
    border-left-color: var(--text);
    background: var(--bg-hover);
    color: var(--text);
}

/* Staff lookup on the dashboard --------------------------------------- */

.staff-panel {
    margin: 0 0 30px;
    padding: 18px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
}

.staff-panel h2 { font-size: 15px; margin-bottom: 4px; }

.staff-panel p {
    margin: 0 0 14px;
    font-size: 13.5px;
    color: var(--text-mute);
    max-width: 68ch;
}

.staff-search { position: relative; display: flex; gap: 10px; }

.staff-input {
    flex: 1;
    min-width: 0;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-family: var(--mono);
    font-size: 14px;
}

/* Recent searches, shown under the field while it has focus. */
.staff-recent {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    right: 0;
    z-index: 40;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    box-shadow: 0 10px 30px rgba(0, 0, 0, .35);
    overflow: hidden;
}

.staff-recent-head {
    padding: 8px 12px;
    border-bottom: 1px solid var(--line-soft);
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--text-mute);
}

.staff-recent-item {
    display: flex;
    align-items: baseline;
    gap: 10px;
    width: 100%;
    padding: 9px 12px;
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
}
.staff-recent-item:hover { background: var(--bg-hover); }

.staff-recent-id {
    margin-left: auto;
    font-family: var(--mono);
    font-size: 12px;
    color: var(--text-mute);
}

.staff-result {
    margin-top: 14px;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    font-size: 14px;
}

.staff-result.is-error { color: var(--text-mute); }


/* Toast with choices --------------------------------------------------- */

/*
 * Wider than the plain toast, and interactive, so pointer-events come back.
 * The plain variant sets them to none so a passing confirmation cannot
 * swallow a click; this one IS the thing being clicked.
 */
.toast.has-actions {
    pointer-events: auto;
    max-width: min(560px, calc(100vw - 40px));
    text-align: left;
    padding: 14px 16px;
}

.toast-text { margin-bottom: 10px; line-height: 1.5; }

.toast-actions { display: flex; flex-wrap: wrap; gap: 8px; }

.toast-btn {
    padding: 6px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
}
.toast-btn:hover { background: var(--bg-hover); }

/* The first action is the recommended one. */
.toast-btn:first-child {
    background: var(--invert-bg);
    color: var(--invert-fg);
    border-color: var(--invert-bg);
}
.toast-btn:first-child:hover { opacity: .85; background: var(--invert-bg); }


/* Moderation ---------------------------------------------------------- */

/* An anchor styled as a tab. Same shape as the buttons beside it, but a real
   link, so it opens in a new tab on middle click like anything else. */
a.side-tab { display: block; text-decoration: none; }

.reason-group {
    margin-bottom: 26px;
    padding-bottom: 22px;
    border-bottom: 1px solid var(--line-soft);
}
.reason-group:last-child { border-bottom: 0; margin-bottom: 0; }

.reason-head {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 12px;
}

.reason-head h3 { font-size: 15px; }

.reason-count {
    padding: 1px 8px;
    border-radius: 999px;
    background: var(--bg-hover);
    border: 1px solid var(--line);
    font-size: 11px;
    font-family: var(--mono);
    color: var(--text-mute);
}

.reason-head .btn { margin-left: auto; }

.reason-list { display: flex; flex-direction: column; gap: 8px; }

/*
 * Label and text side by side, label narrower.
 *
 * The label is what staff scan in a dropdown, the text is what the member
 * reads. Giving the text more room reflects which one people actually write
 * at length.
 */
.reason-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr) auto;
    gap: 8px;
    align-items: center;
}

.reason-empty {
    margin: 0;
    font-size: 13px;
    color: var(--text-mute);
}

.icon-btn {
    flex: 0 0 auto;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-mute);
    font-size: 12px;
    cursor: pointer;
}
.icon-btn:hover { background: var(--bg-hover); color: var(--text); }

/* Dimmed, not hidden, when the feature above it is off. The settings stay
   readable so someone can see what enabling it would do. */
.automod-body { margin-top: 20px; }
.automod-body.is-off { opacity: .45; }

.slider-read {
    display: block;
    margin-top: 6px;
    font-size: 12.5px;
    color: var(--text-mute);
}
.slider-read strong { color: var(--text); font-family: var(--mono); }

/* ── Commands ──────────────────────────────────────────────────────────
 *
 * One row per command, its settings revealed only when expanded. Sixteen
 * commands with their settings all showing is a page nobody can scan.
 */

.cmd-summary {
    margin-bottom: 16px;
    padding: 11px 14px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    font-size: 13px;
    color: var(--text-mute);
}
.cmd-summary strong { color: var(--text); font-family: var(--mono); }

.cmd-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
    overflow: hidden;
}

.cmd + .cmd { border-top: 1px solid var(--line-soft); }

/* Marked on the EDGE rather than filled in. A filled row would compete with
   the expanded body below it, and neither would read as "this one". */
.cmd.is-on { box-shadow: inset 3px 0 0 var(--text); }

.cmd-head {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 12px 16px;
}

/* The name is the click target for expanding, so it takes the row. */
.cmd-name {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.cmd-name:focus-visible { outline: 2px solid var(--focus); outline-offset: 3px; }

.cmd-caret {
    flex: 0 0 auto;
    width: 10px;
    color: var(--text-mute);
    font-size: 11px;
}

.cmd-name code {
    font-family: var(--mono);
    font-size: 13px;
    color: var(--text);
}

.cmd-label { font-size: 13px; color: var(--text-mute); }

/* What it used to be called, so a renamed command is still findable by the
   name the documentation uses. */
.cmd-was {
    flex: 0 0 auto;
    padding: 1px 7px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10.5px;
    color: var(--text-mute);
    white-space: nowrap;
}

.cmd-meta {
    flex: 0 0 auto;
    font-size: 12px;
    color: var(--text-mute);
    white-space: nowrap;
}

/* Indented under a hairline, so the settings visibly belong to the row above
   rather than floating between two commands. */
.cmd-body {
    display: flex;
    flex-direction: column;
    gap: 20px;
    margin: 0 16px 18px 46px;
    padding: 16px 0 0 16px;
    border-left: 1px solid var(--line);
}

/* ── Automations ───────────────────────────────────────────────────────
 *
 * An automation reads as a sentence: when this, and this, then that. The
 * layout exists to keep that readable, which is why each step is its own row
 * with the connecting word on the left.
 */

.auto-empty {
    padding: 26px;
    border: 1px dashed var(--line);
    border-radius: var(--radius);
    text-align: center;
}
.auto-empty h3 { font-size: 15px; margin-bottom: 6px; }
.auto-empty p {
    max-width: 52ch;
    margin: 0 auto 18px;
    font-size: 13.5px;
    color: var(--text-mute);
}

.auto-list { display: flex; flex-direction: column; gap: 12px; }

.auto {
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
    overflow: hidden;
}
.auto.is-on { box-shadow: inset 3px 0 0 var(--text); }

.auto-head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
}

.auto-name { flex: 1; font-weight: 600; }

.auto-body {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 0 16px 16px;
}

.auto-step {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border: 1px solid var(--line-soft);
    border-radius: var(--radius-sm);
    background: var(--bg);
}

/* The action steps are what actually happens, so they carry the weight. */
.auto-step.is-action { box-shadow: inset 2px 0 0 var(--text-dim); }

/* Fixed width so "When", "and" and "then" line up down the left edge and the
   sentence reads as one thing rather than three rows. */
.auto-when {
    flex: 0 0 40px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--text-mute);
}

.auto-step .select-wrap { flex: 0 1 240px; }
.auto-step .settings-input { flex: 1 1 160px; width: auto; }
.auto-emoji { flex: 0 0 90px; text-align: center; }

/* ── Escalation ────────────────────────────────────────────────────────
 *
 * One rule per line, written as a sentence for the same reason.
 */

.ladder { display: flex; flex-direction: column; gap: 8px; }

.ladder-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 9px;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
}

.ladder-at { font-size: 13px; color: var(--text-mute); }
.ladder-num { width: 74px; flex: 0 0 auto; text-align: center; }
.ladder-row .select-wrap { flex: 0 1 190px; }

/* ── Cases ─────────────────────────────────────────────────────────────
 *
 * A log, so it is a table in everything but markup: fixed columns, aligned
 * down the page, scannable without reading every row.
 */

.case-list {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
    overflow: hidden;
}

.case-row {
    display: grid;
    grid-template-columns: 56px 96px minmax(120px, 1fr) minmax(140px, 2fr) auto;
    align-items: center;
    gap: 14px;
    padding: 11px 16px;
    font-size: 13px;
}
.case-row + .case-row { border-top: 1px solid var(--line-soft); }

.case-num { font-family: var(--mono); font-size: 12px; color: var(--text-mute); }

.case-action {
    padding: 2px 8px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    text-align: center;
    color: var(--text-dim);
}

/* The irreversible ones are marked. Monochrome, so weight and a filled
   background carry it rather than the red every other bot reaches for. */
.case-action.is-kick,
.case-action.is-ban {
    background: var(--invert-bg);
    color: var(--invert-fg);
    border-color: var(--invert-bg);
}

.case-target { font-weight: 600; }
.case-reason,
.case-actor {
    color: var(--text-mute);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 760px) {
    .case-row { grid-template-columns: 48px 1fr; row-gap: 4px; }
    .case-reason, .case-actor { grid-column: 2; }
}

/* ── The module tree ───────────────────────────────────────────────────
 *
 * A module, and its features beneath it. Not <details>: that element cannot
 * express "open and not closeable", and the module you are working in must
 * never be foldable away.
 */

.tree-module { display: flex; flex-direction: column; gap: 2px; }

.tree-head {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 9px 12px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-dim);
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}
.tree-head:hover { background: var(--bg-hover); color: var(--text); }
.tree-head:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px; }

/* Marked on the edge, so it does not compete with the current SECTION
   inside it. Two solid highlights in one column and neither reads as
   "you are here". */
.tree-module.is-current > .tree-head {
    color: var(--text);
    box-shadow: inset 2px 0 0 var(--text);
}

/* A triangle drawn in CSS, rotated open. One element, no icon font, and it
   inherits colour from the row. */
.tree-caret {
    flex: 0 0 auto;
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform .16s var(--ease);
}
.tree-head[aria-expanded="true"] .tree-caret { transform: rotate(90deg); }

/* Scoped to the head. `.tree-name` also belongs to the editor's channel
   tree, which is a different control with different needs. */
.tree-head .tree-name { flex: 1; min-width: 0; }

.tree-children {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 2px 0 8px 18px;
    padding-left: 10px;
    border-left: 1px solid var(--line);
}
.tree-children .side-tab { font-size: 13.5px; padding: 7px 11px; }

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

/* ── Permission grid ───────────────────────────────────────────────────
 *
 * Grouped the way Discord groups them, because the whole design principle
 * here is that nobody should have to relearn where a permission lives.
 */

.perm-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
    gap: 22px;
}

.perm-role-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pick-list { flex: 1 1 100%; padding-left: 50px; }

.auto-add { align-self: flex-start; }

/* The "Add automation" button sits below the list, not against it. */
.auto-foot { margin-top: 14px; }

@media (max-width: 760px) {
    .cmd-body { padding-left: 14px; }
    .auto-step .pick-list { padding-left: 0; }
}


/* ── Modern form controls ───────────────────────────────────────────────
 *
 * Switches, selects and sliders, styled once and used everywhere.
 *
 * All three keep the NATIVE input underneath. The visible part is a sibling
 * or a pseudo-element, so keyboard focus, form semantics, screen reader
 * announcements and mobile pickers all keep working. A hand-rolled div that
 * looks like a switch is a div, and behaves like one for anyone not using a
 * mouse.
 */

/* Switch --------------------------------------------------------------- */

.switch {
    position: relative;
    flex: 0 0 auto;
    display: inline-block;
    width: 42px;
    height: 24px;
}

/* Not display:none. A hidden input is unfocusable and leaves the control
   unreachable by keyboard; this keeps it in the tree and invisible. */
.switch input {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    opacity: 0;
    cursor: pointer;
    z-index: 1;
}

.switch-track {
    position: absolute;
    inset: 0;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg-hover);
    transition: background-color .18s var(--ease), border-color .18s var(--ease);
    pointer-events: none;
}

.switch-track::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--text-mute);
    transition: transform .18s var(--ease), background-color .18s var(--ease);
}

.switch input:checked + .switch-track {
    background: var(--invert-bg);
    border-color: var(--invert-bg);
}
.switch input:checked + .switch-track::after {
    transform: translateX(18px);
    background: var(--invert-fg);
}

.switch input:focus-visible + .switch-track {
    outline: 2px solid var(--focus);
    outline-offset: 2px;
}

.switch input:disabled + .switch-track { opacity: .4; }
.switch input:disabled { cursor: default; }

/* Select --------------------------------------------------------------- */

.select-wrap { position: relative; display: block; }

.select {
    appearance: none;
    -webkit-appearance: none;
    width: 100%;
    padding: 10px 34px 10px 13px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 14px;
    cursor: pointer;
}
.select:hover { border-color: var(--text-mute); }
.select:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }

/* The caret the native control no longer draws. pointer-events none so it
   cannot swallow the click that opens the menu. */
.select-caret {
    position: absolute;
    right: 13px;
    top: 50%;
    width: 0;
    height: 0;
    margin-top: -2px;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid var(--text-mute);
    pointer-events: none;
}

.select-wrap:hover .select-caret { border-top-color: var(--text); }

/* Slider --------------------------------------------------------------- */

.slider {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 22px;
    background: transparent;
    cursor: pointer;
}

.slider::-webkit-slider-runnable-track {
    height: 4px;
    border-radius: 999px;
    background: var(--bg-active);
}
.slider::-moz-range-track {
    height: 4px;
    border-radius: 999px;
    background: var(--bg-active);
}

.slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 16px;
    height: 16px;
    margin-top: -6px;
    border-radius: 50%;
    background: var(--text);
    border: 3px solid var(--bg);
    box-shadow: 0 0 0 1px var(--line);
    transition: transform .12s var(--ease);
}
.slider::-moz-range-thumb {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    background: var(--text);
    border: 3px solid var(--bg);
    box-shadow: 0 0 0 1px var(--line);
}

.slider:hover::-webkit-slider-thumb { transform: scale(1.15); }
.slider:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 2px var(--focus); }

/* Ticks under the track, so the scale reads as a range rather than a
   number nobody has a reference for. */
.slider-scale {
    display: flex;
    justify-content: space-between;
    margin-top: 2px;
    font-size: 11px;
    color: var(--text-mute);
}

/* Module picker -------------------------------------------------------- */

.module-picker { margin-bottom: 18px; }

.module-label {
    display: block;
    margin-bottom: 6px;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-mute);
}

/* Automod sections ----------------------------------------------------- */

/*
 * Grouped into cards with room to breathe. The first version was one
 * unbroken column of settings rows, which made a long page of unrelated
 * decisions look like one undifferentiated form.
 */
.am-section {
    margin-bottom: 18px;
    padding: 20px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
}

.am-section > h3 {
    font-size: 14px;
    margin-bottom: 4px;
}

.am-section > p {
    margin: 0 0 18px;
    font-size: 13px;
    color: var(--text-mute);
    max-width: 66ch;
}

.am-section .settings-row + .settings-row { margin-top: 20px; }

/* The enable row sits above the sections and reads as the master switch. */
.am-master {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 18px 20px;
    margin-bottom: 18px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg-card);
}
.am-master .settings-label { flex: 1; }
.am-master.is-on { box-shadow: inset 3px 0 0 var(--text); }


/* ── Multi-select ───────────────────────────────────────────────────────
 *
 * A closed control that says what is chosen, and opens to a searchable list.
 *
 * This replaced a row of chips. Chips are fine for four roles and unusable
 * for forty: a real server's role list wrapped into six lines of buttons and
 * pushed everything below it off the screen, and there was no way to find one
 * by name. Closed, this takes one line whatever the server looks like.
 *
 * Selecting does NOT re-render the panel. The menu stays open so several can
 * be picked in one go, which is the whole reason it is a multi-select.
 */

.ms { position: relative; }

.ms-btn {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 10px 13px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    color: var(--text);
    font: inherit;
    font-size: 14px;
    text-align: left;
    cursor: pointer;
    transition: border-color .14s var(--ease);
}
.ms-btn:hover { border-color: var(--text-mute); }
.ms-btn:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.ms-btn[aria-expanded="true"] { border-color: var(--text-dim); }

.ms-value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Nothing chosen is a real state with a real meaning, not an empty field. */
.ms-value.is-empty { color: var(--text-mute); }

.ms-count {
    flex: 0 0 auto;
    padding: 1px 7px;
    border-radius: 999px;
    background: var(--invert-bg);
    color: var(--invert-fg);
    font-size: 11px;
    font-weight: 700;
    font-family: var(--mono);
}

.ms-caret {
    flex: 0 0 auto;
    width: 0;
    height: 0;
    border-left: 4px solid transparent;
    border-right: 4px solid transparent;
    border-top: 5px solid var(--text-mute);
    transition: transform .16s var(--ease);
}
.ms-btn[aria-expanded="true"] .ms-caret { transform: rotate(180deg); }

.ms-menu {
    position: absolute;
    z-index: 40;
    left: 0;
    right: 0;
    top: calc(100% + 4px);
    display: flex;
    flex-direction: column;
    max-height: 280px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
    box-shadow: 0 8px 24px var(--btn-shadow);
    overflow: hidden;
}

.ms-search {
    flex: 0 0 auto;
    padding: 10px 13px;
    border: 0;
    border-bottom: 1px solid var(--line);
    background: transparent;
    color: var(--text);
    font: inherit;
    font-size: 13.5px;
}
.ms-search:focus { outline: none; }
.ms-search::placeholder { color: var(--text-mute); }

.ms-options { overflow-y: auto; padding: 5px; }

.ms-opt {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 8px 10px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-dim);
    font: inherit;
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
}
.ms-opt:hover { background: var(--bg-hover); color: var(--text); }
.ms-opt.is-on { color: var(--text); font-weight: 600; }

/* The tick occupies its space whether or not it is shown, so rows do not
   shift sideways as they are selected. */
.ms-tick {
    flex: 0 0 auto;
    width: 14px;
    text-align: center;
    color: var(--text);
    opacity: 0;
}
.ms-opt.is-on .ms-tick { opacity: 1; }

.ms-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.ms-empty, .ms-none {
    padding: 12px 14px;
    font-size: 13px;
    color: var(--text-mute);
}

.ms-foot {
    flex: 0 0 auto;
    display: flex;
    justify-content: flex-end;
    padding: 6px;
    border-top: 1px solid var(--line);
}


/* ── Aliases and subcommands ────────────────────────────────────────────
 *
 * A server can rename a command. The leading slash is drawn as part of the
 * field rather than typed into it, so what you are editing is unmistakably a
 * command name and nobody types "/warn" and ends up with "//warn".
 */

.alias-row {
    display: flex;
    align-items: center;
    gap: 0;
}

.alias-slash {
    flex: 0 0 auto;
    padding: 9px 2px 9px 11px;
    border: 1px solid var(--line);
    border-right: 0;
    border-radius: var(--radius-sm) 0 0 var(--radius-sm);
    background: var(--bg-hover);
    color: var(--text-mute);
    font-family: var(--mono);
    font-size: 14px;
}

.alias-row .settings-input {
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    font-family: var(--mono);
}

/* What it used to be called, so a renamed command is still findable by the
   name the documentation uses. */
.cmd-was {
    padding: 1px 7px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10.5px;
    color: var(--text-mute);
    white-space: nowrap;
}

.sub-list { display: flex; flex-direction: column; gap: 6px; }

.sub {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
}
.sub.is-on { box-shadow: inset 2px 0 0 var(--text); }

.sub-name {
    grid-column: 1;
    font-family: var(--mono);
    font-size: 12.5px;
    color: var(--text);
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The description drops out first when there is no room. The name and the
   switch are what the row is FOR; the help is a nicety. */
.sub-help {
    grid-column: 1;
    grid-row: 2;
    font-size: 12px;
    color: var(--text-mute);
}

.sub-alias {
    grid-column: 2;
    grid-row: 1 / span 2;
    width: 130px;
    font-family: var(--mono);
    font-size: 12.5px;
}

.sub .switch { grid-column: 3; grid-row: 1 / span 2; }

/* ── Automod filters ────────────────────────────────────────────────────
 *
 * One card per filter, its settings revealed only once it is on. A filter
 * nobody enabled does not need to show its word list, and eight expanded
 * cards is a wall.
 *
 * PREFIXED `amf`, NOT `filter`. `.filter` is the dashboard's server search
 * box, and reusing the name inherited `flex: 1 1 280px` and
 * `max-width: 360px` from it. In a column flex container the basis applies to
 * the MAIN axis, so every card came out 280px TALL and 360px wide: enormous,
 * half width, and almost entirely empty. One collision, both symptoms.
 */

.amf-list { display: flex; flex-direction: column; gap: 8px; }

.amf {
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg);
    overflow: hidden;
}
.amf.is-on { box-shadow: inset 2px 0 0 var(--text); }

/* Not merely dim. A filter that cannot be turned on gets no switch at all,
   because a greyed control that still responds is the failure this project
   keeps rediscovering. */
.amf.is-blocked { opacity: .6; }

.amf-head {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 13px 15px;
}

.amf-name { flex: 1; display: flex; flex-direction: column; gap: 3px; }
.amf-name strong { font-size: 13.5px; }
.amf-name em {
    font-style: normal;
    font-size: 12.5px;
    color: var(--text-mute);
}

.amf-soon {
    flex: 0 0 auto;
    padding: 3px 9px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--text-mute);
}

.amf-body {
    display: flex;
    flex-wrap: wrap;
    gap: 16px;
    padding: 0 15px 15px;
    border-top: 1px solid var(--line-soft);
    padding-top: 15px;
}

/* One filter field. */
.ff { display: flex; flex-direction: column; gap: 6px; font-size: 12.5px; }
.ff > span {
    display: flex;
    align-items: baseline;
    gap: 6px;
    font-weight: 600;
    color: var(--text-dim);
}
.ff em { font-style: normal; font-weight: 400; color: var(--text-mute); }

.ff-count {
    padding: 0 6px;
    border-radius: 999px;
    background: var(--bg-active);
    font-family: var(--mono);
    font-size: 11px;
}

.ff-wide { flex: 1 0 100%; }
.ff-num { width: 90px; }
.ff-unit { color: var(--text-mute); }

/* A number and its unit read as one phrase, so they sit on one line. */
.ff:not(.ff-wide):not(.ff-toggle) {
    flex-direction: row;
    align-items: center;
    gap: 8px;
}

.ff-toggle {
    flex: 1 0 100%;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
}
.ff-toggle > span { flex-direction: column; align-items: flex-start; gap: 2px; }


/* ── Watermark ──────────────────────────────────────────────────────────
 *
 * The mark, tiled, angled, and falling straight down forever.
 *
 * A single centred logo read as a logo somebody left there. Tiling turns it
 * into texture, which is what a watermark should be: noticed only if you go
 * looking for it.
 *
 * WHY THE ANGLE IS 18.435 AND NOT 18
 *
 * The tiles are rotated, but the motion has to be vertical on SCREEN. Those
 * two facts fight each other, because moving "down" through a rotated field
 * displaces the pattern along both of its own axes at once. The loop is only
 * seamless if that displacement is a whole number of tiles in each.
 *
 * That constrains the angle: tan(angle) has to be a simple ratio. At
 * atan(1/3) = 18.435 degrees, falling exactly 132 x sqrt(10) = 417.42px
 * moves the field by precisely 1 tile across and 3 tiles down, so the frame
 * at 100% is pixel-identical to the frame at 0%.
 *
 * Get this wrong and the whole background twitches once per cycle, which is
 * far more noticeable than the drift itself and reads as a rendering bug.
 *
 * WHY TWO ELEMENTS
 *
 * transform functions apply right to left, so the rotation has to live on a
 * CHILD for the parent's translate to happen in screen space. Rotating and
 * translating the same element would send it diagonally.
 */

.editor-main { position: relative; }

.watermark {
    position: fixed;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    user-select: none;
}

/*
 * The falling layer. Oversized well beyond the viewport so the top edge is
 * never in view at the start of a cycle nor the bottom at the end.
 */
.watermark-drift {
    position: absolute;
    top: -150%;
    left: -50%;
    width: 200%;
    height: 400%;
    animation: wm-fall 46s linear infinite;
    will-change: transform;
}

/* The tiled field, rotated. Inset further so the corners of a rotated
   rectangle never expose an edge. */
.watermark-drift::before {
    content: "";
    position: absolute;
    inset: -30%;
    background-repeat: repeat;
    background-size: 132px;
    transform: rotate(-18.435deg);
}

.wm-black::before { background-image: url("/assets/BlackLogo.png"); }
.wm-white::before { background-image: url("/assets/WhiteLogo.png"); }

@keyframes wm-fall {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(0, 417.42px, 0); }
}

/*
 * Faint enough to be texture rather than content.
 *
 * Light mode is NOT simply the same number. Dark artwork on white reads
 * differently from light artwork on black at equal alpha, so the two are
 * tuned by eye rather than matched arithmetically.
 */
.wm-white { opacity: .028; }
.wm-black { opacity: 0; }

:root[data-theme="light"] .wm-white { opacity: 0; }
:root[data-theme="light"] .wm-black { opacity: .038; }

@media (prefers-color-scheme: light) {
    :root:not([data-theme]) .wm-white { opacity: 0; }
    :root:not([data-theme]) .wm-black { opacity: .038; }
}

/* Everything the page renders sits above it. Without this the watermark is
   painted over the panels rather than behind them. */
.editor-main > .panel,
.editor-main > .banner { position: relative; z-index: 1; }

/*
 * Motion is the first thing to go.
 *
 * Not slowed, STOPPED. Continuous peripheral movement is exactly what
 * triggers vestibular symptoms, and this carries no information, so holding
 * it still costs nothing.
 */
@media (prefers-reduced-motion: reduce) {
    .watermark-drift { animation: none; }
}

/* Narrow screens have no room to spare, and texture behind body text on a
   phone is just reduced contrast. */
@media (max-width: 760px) {
    .watermark { display: none; }
}

/* ── Unread badge ───────────────────────────────────────────────────────
 *
 * How many audit entries have appeared since this browser last looked at the
 * tab. Discord's own unread pip in everything but colour, because that is
 * the thing every person using this already knows how to read.
 */

.side-tab:has(.tab-badge) {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.tab-badge {
    flex: 0 0 auto;
    min-width: 18px;
    padding: 1px 6px;
    border-radius: 999px;
    background: var(--invert-bg);
    color: var(--invert-fg);
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 700;
    line-height: 16px;
    text-align: center;
    /* Announced when it appears, so it is not purely visual. */
    animation: badge-in .22s var(--ease);
}

@keyframes badge-in {
    from { transform: scale(.4); opacity: 0; }
    to   { transform: scale(1); opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .tab-badge { animation: none; }
}


/* One rule's action, inside its card. */
.rule-action {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: 16px;
    flex: 1 0 100%;
}

.rule-action .ff { flex-direction: column; align-items: flex-start; }
.rule-action .select { min-width: 190px; }

/* The unit sits on the line with its number, not under it. */
.rule-action .ff:has(.ff-num) {
    flex-direction: row;
    align-items: center;
    gap: 8px;
}


/* ── Templates ──────────────────────────────────────────────────────────
 *
 * One card per saved template. The two apply buttons sit side by side and do
 * very different things, so they are deliberately not styled alike: the safe
 * one is solid, the destructive one is a ghost, and the wording carries the
 * rest.
 */

.tpl-list { display: flex; flex-direction: column; gap: 10px; }

.tpl {
    padding: 14px 16px;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--bg);
}

.tpl-head {
    display: flex;
    align-items: flex-start;
    gap: 12px;
}

.tpl-name { flex: 1; display: flex; flex-direction: column; gap: 3px; }
.tpl-name strong { font-size: 14px; }
.tpl-name em {
    font-style: normal;
    font-size: 12px;
    color: var(--text-mute);
}

/* What is in it, at a glance. Counts rather than a list: a template with
   forty channels is not a thing to enumerate on a card. */
.tpl-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 12px 0;
}

.tpl-meta span {
    padding: 2px 8px;
    border: 1px solid var(--line);
    border-radius: 999px;
    font-size: 11px;
    color: var(--text-mute);
}

.tpl-actions { display: flex; flex-wrap: wrap; gap: 8px; }


/* Said when the cap is reached, next to the button that just stopped
   working, rather than left for the refusal to explain. */
.tpl-full {
    margin: 10px 0 0;
    font-size: 12.5px;
    color: var(--text-mute);
}

/* ── Utility: composing a message ──────────────────────────────────────
 *
 * Editor on the left, preview on the right. Below 1100px the preview drops
 * underneath rather than shrinking, because a preview narrower than Discord
 * renders it is a preview that lies about where the text wraps.
 */
.util {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 420px);
    gap: 20px;
    align-items: start;
}
@media (max-width: 1100px) {
    .util { grid-template-columns: minmax(0, 1fr); }
}
.util-edit { min-width: 0; }
.util-preview { min-width: 0; }
.util-preview-inner {
    position: sticky;
    top: 84px;
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 16px;
}
.util-preview-inner h3 {
    margin: 0 0 12px;
    font-size: 12px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-mute);
}
.util-row {
    border: 1px solid var(--line);
    border-radius: 10px;
    padding: 12px;
    margin: 0 0 10px;
}
.util-row-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    font-size: 12px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-mute);
    margin: 0 0 10px;
}

/* A pair of fields side by side. Anything that needs the full width says so
   with .ff-wide rather than living in its own row. */
.ff-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 12px;
    margin: 12px 0 0;
}
.ff-grid .ff-wide { grid-column: 1 / -1; }

.tpl-colour {
    padding: 2px;
    height: 36px;
    cursor: pointer;
}

/* The character counter. Neutral until it is over, then it inverts, because
   a red that only appears once is a red nobody has learnt to read. */
.cc {
    margin-left: 8px;
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    color: var(--text-mute);
}
.cc.is-over {
    background: var(--invert-bg);
    color: var(--invert-fg);
    border-radius: 4px;
    padding: 1px 5px;
}

/* ── The preview itself ────────────────────────────────────────────────
 *
 * THE ONE PLACE COLOUR IS ALLOWED.
 *
 * The dashboard is monochrome throughout. This is not dashboard chrome, it
 * is a facsimile of what members will see in Discord, and a preview that
 * rendered a red button grey would be showing something that will not happen.
 * Every colour below is Discord's, used only inside .dp.
 */
.dp {
    font-size: 14px;
    line-height: 1.4;
}
.dp-msg { display: flex; gap: 12px; }
.dp-avatar {
    width: 38px;
    height: 38px;
    border-radius: 50%;
    flex: 0 0 38px;
    object-fit: cover;
}
.dp-blank { background: var(--bg-active); display: block; }
.dp-body { min-width: 0; flex: 1 1 auto; }
.dp-head {
    display: flex;
    align-items: baseline;
    gap: 6px;
    margin: 0 0 2px;
}
.dp-head strong { color: var(--text); font-size: 14px; }
.dp-tag {
    background: #5865f2;
    color: #fff;
    font-size: 10px;
    font-weight: 600;
    border-radius: 3px;
    padding: 1px 4px;
    letter-spacing: .02em;
}
.dp-when { font-size: 11px; color: var(--text-mute); }
.dp-content { color: var(--text-dim); word-wrap: break-word; }
.dp-content a, .dp-desc a, .dp-field-value a { color: #00a8fc; }
.dp-content pre, .dp-desc pre {
    background: var(--bg-hover);
    border: 1px solid var(--line);
    border-radius: 4px;
    padding: 8px;
    white-space: pre-wrap;
    font-size: 12.5px;
    margin: 4px 0;
}
.dp-content code, .dp-desc code, .dp-field-value code {
    background: var(--bg-hover);
    border-radius: 3px;
    padding: 1px 3px;
    font-size: 12.5px;
}
.dp-empty {
    color: var(--text-mute);
    font-size: 12.5px;
    font-style: italic;
}
.dp-embed {
    background: var(--bg-hover);
    border-left: 4px solid var(--line);
    border-radius: 4px;
    padding: 10px 14px 12px;
    margin: 6px 0 0;
    max-width: 100%;
}
.dp-author {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text);
    margin: 0 0 6px;
}
.dp-author img, .dp-footer img {
    width: 20px;
    height: 20px;
    border-radius: 50%;
}
.dp-title { font-weight: 600; color: var(--text); margin: 0 0 4px; }
.dp-title a { color: #00a8fc; text-decoration: none; }
.dp-desc { color: var(--text-dim); word-wrap: break-word; }
.dp-fields {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 8px;
    margin: 8px 0 0;
}
/* Discord's default is a full-width field; only inline ones share a row. */
.dp-field { grid-column: 1 / -1; min-width: 0; }
.dp-field.is-inline { grid-column: span 1; }
.dp-field-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--text);
    margin: 0 0 2px;
}
.dp-field-value { font-size: 13px; color: var(--text-dim); word-wrap: break-word; }
.dp-image {
    display: block;
    max-width: 100%;
    border-radius: 4px;
    margin: 10px 0 0;
}
.dp-thumb {
    display: block;
    max-width: 80px;
    border-radius: 4px;
    margin: 10px 0 0;
}
.dp-footer {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 11.5px;
    color: var(--text-mute);
    margin: 8px 0 0;
}
.dp-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 8px 0 0;
}
.dp-btn {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 13px;
    font-weight: 500;
    border-radius: 4px;
    padding: 6px 14px;
    color: #fff;
    cursor: default;
}
.dp-style-1 { background: #5865f2; }
.dp-style-2 { background: #4e5058; }
.dp-style-3 { background: #248046; }
.dp-style-4 { background: #da373c; }
.dp-style-5 { background: #4e5058; }
.dp-ext { font-size: 11px; opacity: .7; }

/* ── A wider main, for pages that earn it ──────────────────────────────
 *
 * 900px is right for a settings page: a column of rows, read top to bottom.
 * The composer is two panes side by side, and at 900px each got 420, which
 * is narrower than the Discord client it is previewing. Opt-in rather than
 * global, because widening the settings pages would leave their labels
 * stranded from their controls.
 */
.editor-main.is-wide { max-width: 1500px; }

/* Inside a field grid the label sits ABOVE its control.
 *
 * The global .ff rule puts them side by side, which reads well for "Percent
 * [70]" and badly for a text box: the label eats a third of the width and
 * the input that is left is too short to see what you typed. */
.ff-grid .ff,
.ff-grid .ff:not(.ff-wide):not(.ff-toggle) {
    flex-direction: column;
    align-items: stretch;
    gap: 6px;
}
.ff-grid .ff-toggle { flex-direction: row; align-items: center; }

/* ── Who the message comes from ────────────────────────────────────────
 *
 * Two choices, both visible, one selected. A switch would have made the
 * second option a state of the first rather than its equal, and this is a
 * genuine either/or.
 */
.ident {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 10px;
    margin: 12px 0 0;
}
.ident-opt {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    text-align: left;
    padding: 12px 14px;
    border: 1px solid var(--line);
    border-radius: 10px;
    background: var(--bg-raised);
    color: var(--text-dim);
    cursor: pointer;
    font: inherit;
    transition: border-color .12s ease, background .12s ease;
}
.ident-opt:hover { background: var(--bg-hover); }
.ident-opt.is-on {
    border-color: var(--text);
    background: var(--bg-active);
    color: var(--text);
}
.ident-mark {
    flex: 0 0 16px;
    width: 16px;
    height: 16px;
    margin-top: 2px;
    border: 1px solid var(--line);
    border-radius: 50%;
    background: var(--bg);
}
.ident-opt.is-on .ident-mark {
    border-color: var(--text);
    /* The filled centre, drawn rather than nested, so the dot cannot drift
       out of the ring at any zoom level. */
    box-shadow: inset 0 0 0 4px var(--bg), inset 0 0 0 9px var(--text);
}
.ident-text { min-width: 0; }
.ident-text strong { display: block; font-size: 13px; color: inherit; }
.ident-text em {
    display: block;
    font-style: normal;
    font-size: 12px;
    color: var(--text-mute);
    margin: 3px 0 0;
}

/* ── The walkthrough ───────────────────────────────────────────────────
 *
 * A cursor that moves to each control and says what it does.
 *
 * NOT MODAL. The backdrop dims rather than blocks, and every step is
 * escapable, because a tutorial you cannot leave is worse than none. The
 * whole thing is pointer-events: none apart from its own two buttons, so
 * the page underneath stays usable while it runs.
 */
.tut {
    position: fixed;
    inset: 0;
    /* Above the permission drawer (195) and the context menu (200), because
       the tour has to explain those and a tooltip hidden behind the thing it
       describes is worse than no tooltip. Below the toast at 300, which is
       how the page talks about something going wrong. */
    z-index: 240;
    pointer-events: none;
}
.tut-veil {
    position: absolute;
    inset: 0;
    background: var(--bg);
    opacity: .55;
    transition: opacity .3s ease;
}
/* The ring around whatever is being pointed at. Drawn with a huge spread
   shadow rather than four dimmed panels, so it moves as one object. */
.tut-ring {
    position: absolute;
    border: 2px solid var(--text);
    border-radius: 10px;
    box-shadow: 0 0 0 9999px rgba(0, 0, 0, .01);
    transition: top .45s cubic-bezier(.4, 0, .2, 1),
                left .45s cubic-bezier(.4, 0, .2, 1),
                width .45s cubic-bezier(.4, 0, .2, 1),
                height .45s cubic-bezier(.4, 0, .2, 1);
}
.tut-cursor {
    position: absolute;
    width: 26px;
    height: 26px;
    /* The hotspot is the tip, at the top left of the image, so no offset is
       applied: the image's own corner IS the point. */
    transition: transform .45s cubic-bezier(.4, 0, .2, 1);
    filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .5));
}
.tut-tip {
    position: absolute;
    width: min(320px, 80vw);
    background: var(--bg-card);
    border: 1px solid var(--line);
    border-radius: 12px;
    padding: 14px 16px;
    box-shadow: 0 12px 40px var(--btn-shadow);
    pointer-events: auto;
    transition: top .45s cubic-bezier(.4, 0, .2, 1),
                left .45s cubic-bezier(.4, 0, .2, 1);
}
.tut-step {
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-mute);
}
.tut-tip h4 { margin: 4px 0 6px; font-size: 14px; color: var(--text); }
.tut-tip p { margin: 0; font-size: 13px; color: var(--text-dim); line-height: 1.45; }
.tut-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin: 14px 0 0;
}
.tut-dots { display: flex; gap: 5px; }
.tut-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--line);
}
.tut-dot.is-on { background: var(--text); }
.tut-acts { display: flex; gap: 8px; }

/* The offer, shown once, sitting quietly above the composer rather than
   interrupting with a dialog. */
.tut-offer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 12px;
    border: 1px solid var(--line);
    border-radius: 12px;
    background: var(--bg-raised);
    padding: 12px 16px;
    margin: 0 0 16px;
}
.tut-offer p { margin: 0; font-size: 13px; color: var(--text-dim); }
.tut-offer strong { color: var(--text); }

/* Anyone who has asked their system not to animate gets the tour without
   the travel: the cursor and ring jump straight to each target. */
@media (prefers-reduced-motion: reduce) {
    .tut-ring, .tut-cursor, .tut-tip { transition: none; }
}

/* The example server notice.
 *
 * Deliberately not styled as an error or a warning. It is neither: it states
 * a fact about what is on screen, and it has to stay readable for the whole
 * tour without becoming something the eye learns to skip. */
.banner-demo {
    border-color: var(--text-dim);
    background: var(--bg-active);
    color: var(--text);
}

/* Taking it again.
 *
 * Quiet, and only shown once the tour has been finished or declined. Nobody
 * needs a Replay button next to an offer to watch the thing for the first
 * time. */
.tut-replay {
    font-size: 12.5px;
    color: var(--text-mute);
    background: none;
    border: 0;
    padding: 0;
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
}
.tut-replay:hover { color: var(--text); }
.tut-replay-row {
    display: flex;
    justify-content: flex-end;
    margin: 0 0 10px;
}

/* ── The walkthrough on a small screen ──────────────────────────────────
 *
 * A 320px card floated beside its target assumes there is room beside the
 * target. On a phone there is not: everything is full width, so "beside"
 * means on top of the thing being explained.
 *
 * So the card docks to the bottom instead, the ring still marks the target,
 * and the pointer is dropped. The pointer exists to imitate a mouse, and
 * imitating a mouse for somebody using their finger explains nothing.
 */
/* Touch only, and no longer phones: the tour is not offered below 820px at
   all, so a rule for 620 would be styling something that never appears. What
   remains is a tablet or a touchscreen laptop, which has the room for the
   tour and still wants the card docked under a finger. */
@media (pointer: coarse) {
    .tut-tip {
        /* !important because the JS writes top and left on this element every
           step. Those coordinates are correct for a floating card and wrong
           for a docked one, and the alternative is teaching the positioning
           code about breakpoints, which would put the layout in two places. */
        position: fixed !important;
        left: 0 !important;
        right: 0 !important;
        top: auto !important;
        bottom: 0 !important;
        width: auto !important;
        border-radius: 14px 14px 0 0;
        border-bottom: 0;
        padding: 16px 16px calc(16px + env(safe-area-inset-bottom, 0px));
    }

    .tut-cursor { display: none; }

    /* Room for the sheet, so a step never rings something underneath it. */
    .tut-ring { scroll-margin-bottom: 220px; }

    .tut-foot { margin-top: 16px; }
    .tut-acts .btn { padding: 10px 16px; }
    .tut-offer { flex-direction: column; align-items: stretch; }
    .tut-offer .tut-acts { display: flex; gap: 8px; }
    .tut-offer .tut-acts .btn { flex: 1 1 auto; }
}

/* ── The DM a server writes ─────────────────────────────────────────────
 *
 * A message box, the fields it can use, and what the member would actually
 * receive. The preview is the reason the whole thing works: a template is
 * otherwise written blind and only seen once it has been sent to somebody.
 */
.dm-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 8px 0 0;
}
.dm-chip {
    font-family: var(--mono);
    font-size: 11.5px;
    padding: 4px 8px;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--bg-raised);
    color: var(--text-dim);
    cursor: pointer;
}
.dm-chip:hover { background: var(--bg-hover); color: var(--text); }

.dm-preview {
    margin: 10px 0 0;
    padding: 10px 12px;
    border: 1px solid var(--line);
    border-left: 3px solid var(--text-dim);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}
.dm-preview-tag {
    display: block;
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--text-mute);
    margin: 0 0 5px;
}
.dm-preview p {
    margin: 0;
    font-size: 13px;
    color: var(--text-dim);
    /* The member sees line breaks exactly as they were typed. */
    white-space: pre-wrap;
}

/* ── The message a rule sends ───────────────────────────────────────────
 *
 * Collapsed by default. There are fifteen of these on the page once every
 * filter and category has one, and fifteen open editors would bury the
 * settings people actually came for.
 */
.dm {
    margin: 12px 0 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-raised);
}
.dm-head {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 9px 12px;
    background: none;
    border: 0;
    color: var(--text-dim);
    font: inherit;
    font-size: 12.5px;
    cursor: pointer;
    text-align: left;
}
.dm-head:hover { color: var(--text); }
.dm-caret { flex: 0 0 auto; font-size: 10px; }
/* Visible while the editor is CLOSED, so a customised rule can be found
   without opening all fifteen. */
.dm-mark {
    margin-left: auto;
    font-size: 10.5px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-mute);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 1px 7px;
}
.dm-body { padding: 0 12px 12px; }

.dm-styles { display: flex; gap: 6px; margin: 0 0 10px; }
.dm-style {
    flex: 1 1 auto;
    padding: 7px 10px;
    font: inherit;
    font-size: 12.5px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-dim);
    cursor: pointer;
}
.dm-style.is-on {
    border-color: var(--text);
    background: var(--bg-active);
    color: var(--text);
}

.dm-note { margin: 8px 0 0; font-size: 12px; color: var(--text-mute); }
.dm-default { margin: 0; font-size: 13px; color: var(--text-mute); font-style: italic; }

/* The preview, drawn as text or as an embed to match the style chosen. A
   preview that always drew a plain line while the rule was set to send an
   embed would be showing something that does not happen. */
.dm-embed {
    border-left: 3px solid var(--text-dim);
    border-radius: 3px;
    background: var(--bg-hover);
    padding: 8px 12px;
}
.dm-embed strong { display: block; font-size: 13px; color: var(--text); }
.dm-embed p { margin: 4px 0 0; }
.dm-embed em {
    display: block;
    font-style: normal;
    font-size: 11.5px;
    color: var(--text-mute);
    margin: 6px 0 0;
}

/* ── The full embed a server can design ─────────────────────────────────
 *
 * The preview's own parts, drawn close to how Discord lays them out so the
 * "They receive" box reads as the real thing: an author line, a thumbnail
 * floated to the right, fields that can sit inline, an image, a footer with
 * an optional timestamp, and a row of link buttons underneath.
 */
.dm-embed-author {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 12px;
    font-weight: 600;
    color: var(--text);
    margin: 0 0 4px;
}
.dm-embed-author-icon {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    object-fit: cover;
}
.dm-embed-title { display: block; font-size: 13px; color: var(--text); }
.dm-embed-thumb {
    float: right;
    width: 48px;
    height: 48px;
    margin: 0 0 6px 8px;
    border-radius: 4px;
    object-fit: cover;
}
.dm-embed-image {
    display: block;
    max-width: 100%;
    margin: 8px 0 0;
    border-radius: 4px;
}
.dm-fields {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin: 8px 0 0;
}
.dm-field {
    flex: 1 1 100%;
    min-width: 0;
    font-size: 12px;
}
.dm-field.is-inline { flex: 1 1 40%; }
.dm-field strong { display: block; font-size: 12px; color: var(--text); }
.dm-field span { color: var(--text-dim); white-space: pre-wrap; }
.dm-embed-footer {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 8px 0 0;
    font-size: 11.5px;
    color: var(--text-mute);
}
.dm-embed-footer-icon {
    width: 16px;
    height: 16px;
    border-radius: 50%;
    object-fit: cover;
}
.dm-embed-time::before { content: "•"; margin: 0 4px 0 0; }

/* Link buttons under the message, in the preview. */
.dm-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 8px 0 0;
    clear: both;
}
.dm-btn {
    font-size: 12px;
    padding: 4px 10px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-dim);
}

/* The editor's own controls: the field and button repeaters, mirroring the
   saved-reasons list above them so the page has one way to add a row. */
.dm-repeater { margin: 10px 0 0; }
.dm-sub-label {
    display: block;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-dim);
    margin: 0 0 6px;
}
.dm-sub-label em {
    font-style: normal;
    font-weight: 400;
    color: var(--text-mute);
    margin-left: 6px;
}
.dm-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    margin: 0 0 6px;
}
.dm-row .dm-text-field,
.dm-row input[type="url"] {
    flex: 1 1 40%;
    min-width: 0;
}
.dm-field-row .dm-text-field:first-child,
.dm-button-row .dm-text-field:first-child { flex-basis: 30%; }
.dm-text-field { min-width: 0; }
.dm-inline-toggle {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 12px;
    color: var(--text-dim);
    white-space: nowrap;
}
.dm-row-del {
    flex: 0 0 auto;
    width: 26px;
    height: 26px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--bg-card);
    color: var(--text-mute);
    cursor: pointer;
    font-size: 15px;
    line-height: 1;
}
.dm-row-del:hover { color: var(--text); border-color: var(--text-dim); }
.dm-add {
    font: inherit;
    font-size: 12px;
    padding: 6px 12px;
    border: 1px dashed var(--line);
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text-dim);
    cursor: pointer;
}
.dm-add:hover { color: var(--text); border-color: var(--text-dim); }
.dm-ts-row { margin: 10px 0 0; }
