/* ---------------------------------------------------------------------------
   The scales.

   Defined in docs/design/2026-08-06-the-design-system.md. Colour was already
   tokenised and stayed coherent through five design passes; space and type were
   not, and drifted to 131 distinct padding values and 46 distinct font sizes.
   Nobody chose 0.82rem over 0.83rem for a reason — both exist because two
   screens were built on different days, and that is the difference a person
   reads as "unfinished" without being able to name it.

   `tests/design-audit.js` counts what is still off these scales.
   --------------------------------------------------------------------------- */

:root {
    /* Space, on a 4px grid. Anything off it wants a comment saying why —
       optical corrections are real, unexamined values are not. */
    --s-1: 0.25rem;   /*  4 */
    --s-2: 0.5rem;    /*  8 */
    --s-3: 0.75rem;   /* 12 */
    --s-4: 1rem;      /* 16 — the default */
    --s-5: 1.25rem;   /* 20 */
    --s-6: 1.5rem;    /* 24 */
    --s-7: 2rem;      /* 32 */
    --s-8: 2.5rem;    /* 40 */
    --s-9: 3rem;      /* 48 */
    --s-10: 4rem;     /* 64 */

    /* Type, named by role, so a template asks for what a thing *is* rather
       than how big somebody thought it should be. */
    --t-micro: 0.7rem;
    --t-small: 0.8rem;
    --t-body: 0.9rem;
    --t-read: 1rem;
    --t-lead: 1.15rem;
    --t-h3: 1.25rem;
    --t-h2: 1.6rem;
    --t-h1: 2.1rem;

    /* Three weights. Regular, emphasis, and the one number or title carrying
       a screen. */
    --w-body: 400;
    --w-strong: 600;
    --w-loud: 750;
}

:root {
    --c-header-bg: #13202e;
    /* The admin's dark rail. Chrome, not surface: it stays dark in light mode
       on purpose, the way the site header does, so the two products read as
       one. Named rather than inherited from a separate palette. */
    --c-chrome-bg: #16222f;
    --c-chrome-ink: rgba(255, 255, 255, 0.7);
    --c-primary: #1d74b8;
    --c-primary-hover: #175f97;
    --c-primary-light: rgba(29, 116, 184, 0.08);
    --c-primary-border: rgba(29, 116, 184, 0.28);
    --c-bg: #f8f9fa;
    --c-surface: #ffffff;
    --c-hover: #f3f4f6;
    --c-code-bg: #1e1e2e;
    --c-code-text: #cdd6f4;
    --c-text: #1a1a2e;
    --c-text-muted: #6b7280;
    --c-border: #e5e7eb;
    --c-border-strong: #d1d5db;
    --c-success: #059669;
    --c-danger: #dc2626;
    --c-warning: #d97706;
    --radius: 8px;
    --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
    --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);

    /* The semantic tints, in their light values.

       🚨 These are fallbacks in the same sense as the ones above: the real
       pair comes from StyleSheet::semantic(), which emits light here and dark
       inside a prefers-color-scheme query so System works. But the admin
       layout loads this file and renders no token block at all, so for admin
       pages these are not a fallback — they are the values. An undefined
       custom property is not an error, it is an invalid declaration that
       silently drops, which on an alert means no background at all. */
    --c-danger-soft: #fef2f2;
    --c-danger-line: #fecaca;
    --c-danger-ink: #991b1b;
    --c-danger-chip: #fef2f2;
    --c-danger-chip-ink: #991b1b;
    --c-success-soft: #f0fdf4;
    --c-success-line: #bbf7d0;
    --c-success-ink: #059669;
    --c-success-chip: #dcfce7;
    --c-success-chip-ink: #166534;
    --c-warning-soft: #fffbeb;
    --c-warning-line: #fde68a;
    --c-warning-ink: #d97706;
    --c-warning-chip: #fef3c7;
    --c-warning-chip-ink: #92400e;
    --c-warning-edge: #f59e0b;
    --c-info-chip: #dbeafe;
    --c-info-chip-ink: #1d4ed8;
    --c-kind-topic-ink: #059669;
    --c-kind-post-ink: #1d4ed8;
    --c-control-bg: transparent;
    --c-control-ink: #1a1a2e;
    --c-control-danger-ink: #dc2626;
    --c-control-danger-line: #fecaca;
    --c-inset: #f9fafb;
    --lift: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -12px rgba(15, 23, 42, 0.18);
    --lift-hover: 0 2px 4px rgba(15, 23, 42, 0.05), 0 16px 32px -16px rgba(15, 23, 42, 0.25);
    --select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

*, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

body {
    /* The fallback repeats the default stack so the page still reads
       correctly if the theme's <style> block is missing for any reason. */
    font-family: var(--font-body, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif);
    line-height: 1.6;
    color: var(--c-text);
    background: var(--c-bg);
    -webkit-font-smoothing: antialiased;
}

a {
    color: var(--c-primary);
    text-decoration: none;
}

a:hover {
    text-decoration: underline;
}

.container {
    /* Set by the theme. The fallback is the shipped default rather than the
       old 980px, so a page still looks right if the token block is missing. */
    max-width: var(--container, 1440px);
    margin: 0 auto;
    /* Enough that the full-width setting still breathes rather than sitting
       against the glass. */
    padding: 0 1.5rem;
}

/* Header */
.site-header {
    /* A deep blue-slate in the logo's own hue family, exposed as a variable
       so the sticky header's translucent version reads the same colour. */
    background: var(--c-header-bg, #13202e);
    color: #fff;
    padding: 0;
    box-shadow: 0 2px 8px rgba(0,0,0,0.15);
    position: sticky;
    top: 0;
    z-index: 100;
}

/*
   104px. The bar was originally sized to a 30px mark, which is how a header
   ends up dictating the logo instead of holding it — see the note on
   .site-logo-img further down. An 80px logo needs room above and below or it
   reads as jammed in.

   🚨 This is close to the ceiling. Much taller and the bar starts eating the
   first screenful on a laptop, which costs more than a larger mark is worth.
*/
.header-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    height: 104px;
    gap: 1rem;
}

.site-logo {
    font-size: 1.2rem;
    font-weight: 700;
    color: #fff;
    text-decoration: none;
    letter-spacing: -0.02em;
    white-space: nowrap;
    flex-shrink: 0;
}

.site-logo:hover {
    text-decoration: none;
    opacity: 0.9;
}

.header-nav {
    display: flex;
    align-items: center;
    gap: 0.25rem;
}

.header-nav a {
    color: rgba(255,255,255,0.8);
    padding: 0.4rem 0.75rem;
    border-radius: 6px;
    font-size: 0.9rem;
    font-weight: 500;
    transition: background 0.15s, color 0.15s;
    text-decoration: none;
}

.header-nav a:hover {
    color: #fff;
    background: rgba(255,255,255,0.1);
    text-decoration: none;
}

.header-nav .btn-sm {
    background: var(--c-primary);
    color: #fff;
    padding: 0.35rem 0.9rem;
    border-radius: 6px;
}

.header-nav .btn-sm:hover {
    background: var(--c-primary-hover);
}

.header-user {
    display: flex;
    align-items: center;
    gap: 0.75rem;
}

.header-username {
    color: rgba(255,255,255,0.9);
    font-weight: 500;
    font-size: 0.9rem;
}

/* Main */
.site-main {
    /* 🚨 padding-block, never the shorthand. `.site-main` is also `.container`,
       and `padding: 1.5rem 0 3rem` sets the inline padding to zero — same
       specificity, later in the file, so it silently won and every page lost
       its side gutter. It went unnoticed for as long as the container was
       narrower than the window, because `margin: 0 auto` was supplying gutters
       that were never the container's to supply; the moment the layout went
       full width the cards were against the glass. */
    padding-block: 1.5rem 3rem;
    min-height: calc(100vh - 56px - 60px);
}

/* Footer */
.site-footer {
    padding: 1.25rem 0;
    text-align: center;
    color: var(--c-text-muted);
    font-size: 0.82rem;
    border-top: 1px solid var(--c-border);
}

/* Page header */
.page-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 1.25rem;
    flex-wrap: wrap;
    gap: 0.75rem;
}

.page-header h2 {
    font-size: 1.5rem;
    font-weight: 700;
    letter-spacing: -0.02em;
}

/* Breadcrumb */
.breadcrumb {
    font-size: 0.85rem;
    color: var(--c-text-muted);
    margin-bottom: 1rem;
}

.breadcrumb a {
    color: var(--c-text-muted);
}

.breadcrumb a:hover {
    color: var(--c-primary);
}

/* Buttons */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: 0.55rem 1.25rem;
    border: none;
    border-radius: 6px;
    font-size: 0.9rem;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, box-shadow 0.15s;
    text-decoration: none;
    line-height: 1.4;
}

.btn:hover {
    text-decoration: none;
}

.btn-primary {
    background: var(--c-primary);
    color: #fff;
}

.btn-primary:hover {
    background: var(--c-primary-hover);
    color: #fff;
}

.btn-danger {
    background: var(--c-danger);
    color: #fff;
}

.btn-outline {
    background: var(--c-control-bg);
    color: var(--c-control-ink);
    border: 1px solid var(--c-border-strong);
}

.btn-outline:hover {
    background: var(--c-bg);
}

/* Forms */
.form-group {
    margin-bottom: 1.25rem;
}

.form-group label {
    display: block;
    margin-bottom: 0.35rem;
    font-weight: 600;
    font-size: 0.9rem;
    color: var(--c-text);
}

.form-group input[type="text"],
.form-group input[type="email"],
.form-group input[type="password"],
.form-group textarea,
.form-group select {
    width: 100%;
    padding: 0.6rem 0.85rem;
    border: 1px solid var(--c-border-strong);
    border-radius: 6px;
    font-size: 0.95rem;
    font-family: inherit;
    color: var(--c-text);
    /* background-COLOR: this group includes a select, and the shorthand would
       take the chevron's repeat, position and size with it. */
    background-color: var(--c-surface);
    transition: border-color 0.15s, box-shadow 0.15s;
}

.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
    outline: none;
    border-color: var(--c-primary);
    box-shadow: 0 0 0 3px var(--c-primary-light);
}

/* Alerts */
.alert {
    padding: 0.75rem 1rem;
    border-radius: var(--radius);
    margin-bottom: 1.25rem;
    font-size: 0.9rem;
    font-weight: 500;
}

.alert-error {
    background: var(--c-danger-soft);
    color: var(--c-danger-ink);
    border: 1px solid var(--c-danger-line);
}

.alert-success {
    background: var(--c-success-soft);
    color: var(--c-success-ink);
    border: 1px solid var(--c-success-line);
}

.alert-warning {
    background: var(--c-warning-soft);
    color: var(--c-warning-ink);
    border: 1px solid var(--c-warning-line);
}

/* Forum index */
.forum-category {
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    margin-bottom: 1.25rem;
    overflow: hidden;
}

.category-header {
    padding: 0.75rem 1rem;
    background: var(--c-hover);
    border-bottom: 1px solid var(--c-border);
}

.category-header h3 {
    font-size: 0.8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--c-text-muted);
}

.forum-row {
    display: flex;
    align-items: center;
    padding: 0.85rem 1rem;
    border-bottom: 1px solid var(--c-border);
    color: var(--c-text);
    text-decoration: none;
    transition: background 0.1s;
    gap: 0.85rem;
}

.forum-row:last-child {
    border-bottom: none;
}

.forum-row:hover {
    background: var(--c-hover);
    text-decoration: none;
}

.forum-icon {
    flex-shrink: 0;
    width: 40px;
    height: 40px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--c-primary-light);
    border-radius: 8px;
    color: var(--c-primary);
}

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

.forum-title {
    font-weight: 600;
    font-size: 0.95rem;
}

.forum-desc {
    font-size: 0.82rem;
    color: var(--c-text-muted);
    margin-top: 0.15rem;
}

.forum-stats {
    display: flex;
    gap: 1rem;
    font-size: 0.82rem;
    color: var(--c-text-muted);
    flex-shrink: 0;
}

/* Topic list */
.topic-list {
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    overflow: hidden;
}

.topic-row {
    display: flex;
    align-items: center;
    padding: 0.85rem 1rem;
    border-bottom: 1px solid var(--c-border);
    color: var(--c-text);
    text-decoration: none;
    transition: background 0.1s;
    gap: 0.75rem;
}

.topic-row:last-child {
    border-bottom: none;
}

.topic-row:hover {
    background: var(--c-hover);
    text-decoration: none;
}

.topic-meta {
    display: flex;
    gap: 0.35rem;
    flex-shrink: 0;
}

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

.topic-title {
    font-weight: 600;
    font-size: 0.95rem;
}

.topic-starter {
    font-size: 0.82rem;
    color: var(--c-text-muted);
    margin-top: 0.1rem;
}

.topic-stats {
    display: flex;
    gap: 1rem;
    font-size: 0.82rem;
    color: var(--c-text-muted);
    flex-shrink: 0;
}

.topic-meta-bar {
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

/* Badges */
.badge {
    display: inline-block;
    padding: 0.15em 0.55em;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-radius: 4px;
}

.badge-pin {
    background: var(--c-info-chip);
    color: var(--c-info-chip-ink);
}

.badge-lock {
    background: var(--c-warning-chip);
    color: var(--c-warning-chip-ink);
}

/* Post list */
.post-list {
    margin-bottom: 1.5rem;
}

.post {
    display: flex;
    gap: 1rem;
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.25rem;
    margin-bottom: 0.75rem;
}

.post-sidebar {
    flex-shrink: 0;
    width: 100px;
    text-align: center;
}

.post-avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: var(--c-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 1.2rem;
    font-weight: 700;
    margin: 0 auto 0.5rem;
}

.post-author {
    font-weight: 600;
    font-size: 0.9rem;
    display: block;
    color: var(--c-text);
}

.post-author:hover {
    color: var(--c-primary);
}

.post-author-stats {
    font-size: 0.75rem;
    color: var(--c-text-muted);
    margin-top: 0.15rem;
}

.post-content {
    flex: 1;
    min-width: 0;
}

.post-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 0.75rem;
    font-size: 0.82rem;
    color: var(--c-text-muted);
}

.post-number {
    color: var(--c-text-muted);
    font-weight: 500;
}

.post-number:hover {
    color: var(--c-primary);
}

.post-body {
    font-size: 0.95rem;
    line-height: 1.7;
    overflow-wrap: break-word;
}

.post-body p {
    margin-bottom: 0.75rem;
}

.post-body p:last-child {
    margin-bottom: 0;
}

.post-body h2 {
    font-size: 1.4rem;
    font-weight: 700;
    margin: 1.25rem 0 0.75rem;
}

.post-body h3 {
    font-size: 1.2rem;
    font-weight: 600;
    margin: 1rem 0 0.5rem;
}

.post-body ul, .post-body ol {
    padding-left: 1.5rem;
    margin-bottom: 0.75rem;
}

.post-body blockquote {
    border-left: 3px solid var(--c-border-strong);
    padding-left: 1rem;
    margin: 0.75rem 0;
    color: var(--c-text-muted);
}

.post-body pre {
    background: var(--c-code-bg);
    color: var(--c-code-text);
    padding: 1rem;
    border-radius: 6px;
    overflow-x: auto;
    margin: 0.75rem 0;
    font-family: 'SF Mono', 'Fira Code', monospace;
    font-size: 0.85rem;
    line-height: 1.6;
}

.post-body code {
    background: var(--c-hover);
    padding: 0.15em 0.4em;
    border-radius: 3px;
    font-family: 'SF Mono', 'Fira Code', monospace;
    font-size: 0.875em;
}

.post-body pre code {
    background: transparent;
    padding: 0;
    color: inherit;
}

.post-body img {
    max-width: 100%;
    height: auto;
    border-radius: 6px;
}

.post-body hr {
    border: none;
    border-top: 1px solid var(--c-border);
    margin: 1.5rem 0;
}

.post-edited {
    margin-top: 0.75rem;
    font-size: 0.78rem;
    color: var(--c-text-muted);
    font-style: italic;
}

/* Reply form */
.reply-form-wrapper {
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.25rem;
}

.reply-form-wrapper h3 {
    font-size: 1.1rem;
    font-weight: 600;
    margin-bottom: 1rem;
}

/* Topic form */
.topic-form {
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 1.5rem;
}

/* Pagination */
.pagination {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    margin-top: 1.25rem;
    flex-wrap: wrap;
}

.page-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 36px;
    height: 36px;
    padding: 0 0.5rem;
    border-radius: 6px;
    font-size: 0.85rem;
    font-weight: 500;
    color: var(--c-text);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    text-decoration: none;
    transition: background 0.15s, border-color 0.15s;
}

.page-link:hover {
    background: var(--c-hover);
    text-decoration: none;
}

.page-link.active {
    background: var(--c-primary);
    color: #fff;
    border-color: var(--c-primary);
}

/* Empty state */
.empty-state {
    text-align: center;
    padding: 3rem 1rem;
    color: var(--c-text-muted);
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

/* Profile */
.profile-card {
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 2rem;
    text-align: center;
}

.profile-avatar {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    background: var(--c-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 2rem;
    font-weight: 700;
    margin: 0 auto 1rem;
}

.profile-username {
    font-size: 1.5rem;
    font-weight: 700;
}

.profile-title {
    color: var(--c-text-muted);
    font-size: 0.9rem;
    margin-top: 0.25rem;
}

.profile-stats {
    display: flex;
    justify-content: center;
    gap: 2rem;
    margin-top: 1.25rem;
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

.profile-stats strong {
    display: block;
    font-size: 1.1rem;
    color: var(--c-text);
}

/* Auth forms */
.auth-form {
    max-width: 400px;
    margin: 2rem auto;
    background: var(--c-surface);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 2rem;
}

.auth-form h2 {
    text-align: center;
    margin-bottom: 1.5rem;
    font-size: 1.5rem;
}

.auth-form .btn {
    width: 100%;
    margin-top: 0.5rem;
}

.auth-links {
    text-align: center;
    margin-top: 1rem;
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

/* Forum description */
.forum-description {
    color: var(--c-text-muted);
    font-size: 0.9rem;
    margin-bottom: 1.25rem;
}

/* Tag badges */
.tag-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3em;
    padding: 0.2em 0.65em;
    border-radius: 20px;
    font-size: 0.8rem;
    font-weight: 600;
    white-space: nowrap;
    line-height: 1.5;
}

.tag-badge i {
    font-size: 0.75em;
}

.tag-badge-sm {
    font-size: 0.72rem;
    padding: 0.15em 0.5em;
}

.topic-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-top: 0.5rem;
}

.topic-tags-inline {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin-top: 0.2rem;
}

/* Tag picker */
.tag-picker {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
}

.tag-picker-item {
    cursor: pointer;
    display: inline-flex;
}

.tag-picker-item input[type="checkbox"] {
    display: none;
}

.tag-picker-item .tag-badge {
    cursor: pointer;
    opacity: 0.55;
    transition: opacity 0.15s, box-shadow 0.15s;
}

.tag-picker-item input[type="checkbox"]:checked + .tag-badge {
    opacity: 1;
    box-shadow: 0 0 0 2px currentColor;
}

.tag-picker-item:hover .tag-badge {
    opacity: 0.85;
}

/* Responsive */
@media (max-width: 640px) {
    .post {
        flex-direction: column;
    }

    .post-sidebar {
        width: 100%;
        display: flex;
        align-items: center;
        gap: 0.75rem;
        text-align: left;
    }

    .post-avatar {
        width: 36px;
        height: 36px;
        font-size: 0.9rem;
        margin: 0;
    }

    .post-author-stats {
        display: none;
    }

    .forum-stats,
    .topic-stats {
        display: none;
    }

    .page-header {
        flex-direction: column;
        align-items: flex-start;
    }
}

/* Mod toolbar */
.mod-toolbar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.6rem 1rem;
    background: var(--c-warning-soft);
    border: 1px solid var(--c-warning-line);
    border-radius: 6px;
    margin-bottom: 1rem;
    flex-wrap: wrap;
}

.mod-toolbar-label {
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--c-warning-chip-ink);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.mod-btn {
    display: inline-flex;
    align-items: center;
    padding: 0.3rem 0.6rem;
    border: 1px solid var(--c-border);
    border-radius: 4px;
    background: var(--c-surface);
    font-size: 0.8rem;
    cursor: pointer;
    color: var(--c-text);
    transition: background 0.15s;
}

.mod-btn:hover {
    background: var(--c-hover);
}

.mod-btn-danger {
    color: var(--c-control-danger-ink);
    border-color: var(--c-control-danger-line);
}

.mod-btn-danger:hover {
    background: var(--c-danger-soft);
}

.mod-btn-sm {
    padding: 0.2rem 0.45rem;
    font-size: 0.75rem;
}

.mod-indicator {
    display: inline-block;
    padding: 0.15em 0.5em;
    font-size: 0.7rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    border-radius: 4px;
}

.mod-locked { background: var(--c-warning-chip); color: var(--c-warning-chip-ink); }
.mod-pinned { background: var(--c-info-chip); color: var(--c-info-chip-ink); }
.mod-hidden { background: var(--c-danger-chip); color: var(--c-danger-chip-ink); }

.post-actions {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.75rem;
    padding-top: 0.5rem;
    border-top: 1px solid var(--c-border);
}

/* Report modal */
.report-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
}

.report-modal-overlay {
    position: absolute;
    inset: 0;
    background: rgba(0,0,0,0.4);
}

.report-modal-box {
    position: relative;
    background: var(--c-surface);
    border-radius: 8px;
    padding: 1.5rem;
    width: 90%;
    max-width: 440px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.15);
}

.report-modal-box h3 {
    margin: 0 0 1rem;
    font-size: 1.1rem;
}

.btn-outline {
    background: var(--c-surface);
    color: var(--c-text);
    border: 1px solid var(--c-border);
    padding: 0.5rem 1rem;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.875rem;
}

.btn-outline:hover {
    background: var(--c-hover);
}

/* Header search */
.header-search {
    flex: 0 1 280px;
    margin-left: auto;
    margin-right: 1rem;
}

.header-search input {
    width: 100%;
    padding: 0.4rem 0.75rem;
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 6px;
    background: rgba(255,255,255,0.08);
    color: #fff;
    font-size: 0.875rem;
}

.header-search input::placeholder {
    color: rgba(255,255,255,0.45);
}

.header-search input:focus {
    outline: none;
    background: rgba(255,255,255,0.14);
    border-color: rgba(255,255,255,0.3);
}

/* Search page */
.search-panel {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    padding: 1.25rem;
    margin-bottom: 1.5rem;
}

.search-main-row {
    display: flex;
    gap: 0.75rem;
    margin-bottom: 1rem;
}

.search-input {
    flex: 1;
    font-size: 1rem;
}

.search-filters {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 1rem;
}

.search-filter label {
    display: block;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--c-text-muted);
    margin-bottom: 0.3rem;
}

.search-results-header {
    font-size: 0.9rem;
    color: var(--c-text-muted);
    margin-bottom: 1rem;
}

.search-result {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    padding: 1rem 1.25rem;
    margin-bottom: 0.75rem;
}

.search-result-title {
    display: block;
    font-size: 1.05rem;
    font-weight: 600;
    color: var(--c-primary);
    text-decoration: none;
    margin-bottom: 0.35rem;
}

.search-result-title:hover {
    text-decoration: underline;
}

.search-result-excerpt {
    font-size: 0.9rem;
    color: var(--c-text);
    line-height: 1.55;
    margin-bottom: 0.5rem;
}

.search-result-meta {
    font-size: 0.82rem;
    color: var(--c-text-muted);
    display: flex;
    gap: 0.35rem;
    flex-wrap: wrap;
}

.search-result-meta a {
    color: var(--c-text-muted);
}

.search-empty {
    background: var(--c-inset);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    padding: 3rem 1rem;
    text-align: center;
    color: var(--c-text-muted);
}

/* Notification bell */
.header-bell {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    border-radius: 6px;
    background: rgba(255,255,255,0.08);
    text-decoration: none;
    color: rgba(255,255,255,0.8);
}

.header-bell:hover {
    background: rgba(255,255,255,0.16);
}

.bell-icon {
    font-size: 0.7rem;
}

.bell-badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 16px;
    height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: #ef4444;
    color: #fff;
    font-size: 0.65rem;
    font-weight: 700;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Notification list */
.notification {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.9rem 1.25rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    margin-bottom: 0.5rem;
    text-decoration: none;
    color: var(--c-text);
    transition: background 0.15s;
}

.notification:hover {
    background: var(--c-hover);
}

.notification-unread {
    border-left: 3px solid var(--c-primary);
    background: var(--c-primary-light);
}

.notification-avatar {
    width: 38px;
    height: 38px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--c-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
}

.notification-body {
    flex: 1;
    min-width: 0;
}

.notification-text {
    font-size: 0.92rem;
    line-height: 1.45;
}

.notification-time {
    font-size: 0.8rem;
    color: var(--c-text-muted);
    margin-top: 0.15rem;
}

.notification-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--c-primary);
    flex-shrink: 0;
}

/* ---- The messenger ------------------------------------------------------

   🚨 A rewrite, not a restyle. What was here rendered a conversation as a page
   header, a participants strip, a vertical list of identical .message blocks
   and a "Reply" heading over a form — the topic page with the word Message in
   it. Reported as "the Messages component looks basic as can be".

   Three panes: choose on the left, read in the middle, see who is in it on the
   right. Reasoning in docs/design/2026-08-08-the-messenger.md.
*/

/* A page that takes the whole width has no sidebar to leave room for. */
.site-main.is-wide { display: block; }

.pm {
    display: grid;
    grid-template-columns: 320px minmax(0, 1fr);
    /* Fills what is left of the viewport rather than growing the page: a
       conversation scrolls inside itself, the way every messenger does.

       🚨 min-height is the floor that keeps the composer reachable. On a short
       window `calc()` alone produced a pane too small to hold the message list
       AND the composer, and it was the composer that lost. */
    height: calc(100vh - 190px);
    /* 🚨 A floor low enough to still FIT. 34rem pushed the composer past the
       bottom of a 720px window, which is the fault this was meant to fix. */
    min-height: 26rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    overflow: hidden;
    position: relative;
}

.pm.is-open {
    grid-template-columns: 320px minmax(0, 1fr) 260px;
}

/* ---- the conversations ---- */

.pm-list {
    display: flex;
    flex-direction: column;
    min-height: 0;
    border-right: 1px solid var(--c-border);
    background: var(--c-bg);
}

.pm-list-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 1rem 1.1rem;
    border-bottom: 1px solid var(--c-border);
}

.pm-list-head h2 { font-size: 1.15rem; margin: 0; }

.pm-new {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.2rem;
    height: 2.2rem;
    border-radius: 10px;
    color: var(--c-primary);
    background: var(--c-primary-light);
    transition: background 0.15s ease;
}

.pm-new:hover { background: var(--c-primary); color: #fff; }

.pm-list-empty {
    padding: 2rem 1.1rem;
    text-align: center;
    color: var(--c-text-muted);
    font-size: 0.9rem;
}

.pm-threads {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}

.pm-thread {
    position: relative;
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.75rem;
    padding: 0.8rem 1.1rem;
    border-bottom: 1px solid var(--c-border);
    color: var(--c-text);
    transition: background 0.12s ease;
}

.pm-thread:hover { background: var(--c-hover); text-decoration: none; }

/* The one that is open, marked down its edge rather than by a background —
   which would fight with the hover state on the row under the pointer. */
.pm-thread.is-on {
    background: var(--c-primary-light);
    box-shadow: inset 3px 0 0 var(--c-primary);
}

.pm-thread-avatar { position: relative; display: block; }

/* How many other people are in it, when it is more than one. */
.pm-thread-count {
    position: absolute;
    right: -4px;
    bottom: -4px;
    min-width: 1.15rem;
    height: 1.15rem;
    padding: 0 0.25rem;
    border-radius: 999px;
    background: var(--c-primary);
    color: #fff;
    font-size: 0.68rem;
    font-weight: 700;
    line-height: 1.15rem;
    text-align: center;
    border: 2px solid var(--c-bg);
}

.pm-thread-body { display: block; min-width: 0; }

.pm-thread-top {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
}

.pm-thread-who {
    font-weight: 600;
    font-size: 0.94rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pm-thread-when {
    flex: 0 0 auto;
    font-size: 0.74rem;
    color: var(--c-text-muted);
}

.pm-thread-subject,
.pm-thread-preview {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pm-thread-subject { font-size: 0.85rem; color: var(--c-text); }
.pm-thread-preview { font-size: 0.82rem; color: var(--c-text-muted); }

/* Unread is carried by weight and a dot, not by colour alone. */
.pm-thread.is-unread .pm-thread-who,
.pm-thread.is-unread .pm-thread-subject { font-weight: 700; }
.pm-thread.is-unread .pm-thread-preview { color: var(--c-text); }

.pm-thread-dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--c-primary);
    flex-shrink: 0;
}

/* ---- the conversation ---- */

.pm-thread-view {
    display: flex;
    flex-direction: column;
    min-width: 0;
    min-height: 0;
}

.pm-nothing {
    margin: auto;
    padding: 2rem;
    text-align: center;
    color: var(--c-text-muted);
}

.pm-head {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.85rem 1.2rem;
    border-bottom: 1px solid var(--c-border);
    background: var(--c-surface);
}

.pm-head-text { min-width: 0; flex: 1; }
.pm-head-text h3 {
    margin: 0;
    font-size: 1.02rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.pm-head-with {
    margin: 0.1rem 0 0;
    font-size: 0.8rem;
    color: var(--c-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Back to the list. Only means anything where the list is not on screen. */
.pm-back { display: none; color: var(--c-text-muted); }

.pm-people-toggle {
    display: none;
    background: none;
    border: 0;
    padding: 0.4rem;
    border-radius: 8px;
    color: var(--c-text-muted);
    cursor: pointer;
}

.pm-people-toggle:hover { background: var(--c-hover); color: var(--c-text); }

.pm-alert { margin: 0.8rem 1.2rem 0; }

.pm-messages {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 1.2rem;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

/* The day changed. A rule with the day sitting on it. */
.pm-day {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin: 1.1rem 0 0.6rem;
    color: var(--c-text-muted);
    font-size: 0.75rem;
}

.pm-day::before,
.pm-day::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--c-border);
}

.pm-day span { flex: 0 0 auto; }

/* ---- a message ---- */

.pm-msg {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.6rem;
    max-width: min(78%, 44rem);
    margin-top: 0.55rem;
}

/* 🚨 Yours on the right. The single most useful fact on this screen is who
   said which line, and it used to be carried only by a name in small text. */
.pm-msg.is-mine {
    grid-template-columns: minmax(0, 1fr);
    margin-left: auto;
    justify-items: end;
}

.pm-msg.is-mine .pm-msg-avatar { display: none; }

/* A run from the same person collapses: no repeated avatar, name or time. */
.pm-msg.is-grouped { margin-top: 0.15rem; }

.pm-msg-avatar { width: 2.6rem; flex-shrink: 0; }

.pm-msg-main { min-width: 0; display: flex; flex-direction: column; }
.pm-msg.is-mine .pm-msg-main { align-items: flex-end; }

.pm-msg-who {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    margin-bottom: 0.2rem;
    font-size: 0.76rem;
    color: var(--c-text-muted);
}

.pm-msg-name { font-weight: 650; color: var(--c-text); }

.pm-bubble {
    padding: 0.6rem 0.9rem;
    border-radius: 14px;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    line-height: 1.55;
    overflow-wrap: anywhere;
}

/* The tail, on the first bubble of a run only — a corner squared off towards
   whoever wrote it. Cheaper and steadier than a pseudo-element triangle. */
.pm-msg:not(.is-grouped) .pm-bubble { border-top-left-radius: 4px; }
.pm-msg.is-mine:not(.is-grouped) .pm-bubble {
    border-top-left-radius: 14px;
    border-top-right-radius: 4px;
}

.pm-msg.is-mine .pm-bubble {
    background: var(--c-primary);
    border-color: var(--c-primary);
    color: #fff;
}

/* Inside your own bubble the accent is the background, so anything that would
   have been drawn in it has to become the foreground. */
.pm-msg.is-mine .pm-bubble a { color: #fff; text-decoration: underline; }
.pm-msg.is-mine .pm-bubble blockquote {
    border-left-color: rgba(255, 255, 255, 0.55);
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
}
.pm-msg.is-mine .pm-bubble code {
    background: rgba(255, 255, 255, 0.16);
    color: #fff;
}

.pm-bubble p:first-child { margin-top: 0; }
.pm-bubble p:last-child { margin-bottom: 0; }
.pm-bubble img { max-width: 100%; border-radius: 8px; }

/* ---- the composer ---- */

.pm-composer {
    display: flex;
    align-items: flex-end;
    gap: 0.6rem;
    padding: 0.8rem 1.2rem;
    border-top: 1px solid var(--c-border);
    background: var(--c-surface);
}

/* 🚨 Convoro's editor, docked and RESIZED.

   The editor ships with `min-height: 200px` on its content area, which is right
   for writing a post and wrong for a chat composer: 200px of empty box plus a
   toolbar came to more than the space left under the conversation, so the place
   you type was pushed off the bottom of the screen entirely — reported as "no
   way to get to the editor to type a message".

   Two or three lines to start, growing to a point and then scrolling, so the
   conversation above is never squeezed to nothing by a long message. */
/* 🚨 The wrapper is what flexes, not the editor.

   The editor inserts its toolbar as a sibling of itself, so whatever contains
   the editor also ends up containing the toolbar. Putting the toolbar above
   the input rather than beside it is entirely a question of what that
   container is. */
.pm-editor-wrap {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    max-height: 14rem;
    border: 1px solid var(--c-border);
    border-radius: 12px;
    background: var(--c-bg);
    overflow: hidden;
}

.pm-editor {
    min-width: 0;
    overflow-y: auto;
}

.pm-editor-wrap .convoro-editor-content {
    min-height: 3.2rem;
    max-height: 10rem;
    overflow-y: auto;
    border: 0;
    background: transparent;
}

/* The toolbar is a chat toolbar: one quiet row, not a wall of buttons over a
   two-line box. */
.pm-editor-wrap .editor-toolbar {
    border: 0;
    border-bottom: 1px solid var(--c-border);
    padding: 0.3rem 0.4rem;
    background: transparent;
}

.pm-send {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.6rem;
    height: 2.6rem;
    flex-shrink: 0;
    border: 0;
    border-radius: 50%;
    background: var(--c-primary);
    color: #fff;
    cursor: pointer;
    transition: filter 0.15s ease;
}

.pm-send:hover { filter: brightness(1.08); }

/* ---- who is in it ---- */

.pm-people {
    display: flex;
    flex-direction: column;
    min-height: 0;
    border-left: 1px solid var(--c-border);
    background: var(--c-bg);
    overflow-y: auto;
}

.pm-people-head {
    padding: 0.95rem 1rem;
    border-bottom: 1px solid var(--c-border);
    font-weight: 650;
    font-size: 0.9rem;
}

.pm-people-list { padding: 0.5rem; }

.pm-person {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.45rem 0.55rem;
    border-radius: 8px;
    color: var(--c-text);
    font-size: 0.88rem;
}

.pm-person:hover { background: var(--c-hover); text-decoration: none; }

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

.pm-person-you {
    margin-left: 0.3rem;
    font-size: 0.72rem;
    color: var(--c-text-muted);
}

.pm-invite {
    padding: 0.9rem 1rem;
    border-top: 1px solid var(--c-border);
}

.pm-invite label {
    display: block;
    font-size: 0.82rem;
    font-weight: 600;
    margin-bottom: 0.4rem;
}

.pm-invite-row { display: flex; gap: 0.4rem; }
.pm-invite-row .form-input { min-width: 0; }

.pm-leave {
    margin-top: auto;
    padding: 0.9rem 1rem;
    border-top: 1px solid var(--c-border);
}

/* ---- narrow ---- */

@media (max-width: 1100px) {
    /* The people panel stops being a column and becomes a panel over the
       conversation: a third of a screen each for three things is three things
       nobody can use. */
    .pm.is-open { grid-template-columns: 300px minmax(0, 1fr); }

    .pm-people-toggle { display: inline-flex; }

    .pm-people {
        position: absolute;
        top: 0;
        right: 0;
        bottom: 0;
        width: 280px;
        transform: translateX(100%);
        transition: transform 0.18s ease;
        box-shadow: var(--lift-hover);
        z-index: 20;
    }

    .pm-people.is-open { transform: translateX(0); }
}

@media (max-width: 760px) {
    /*
        🚨 The list OR the conversation, never a sliver of each. Which one is
        decided by whether one is open, which the server already knows — so
        this needs no JavaScript and cannot end up showing neither.
    */
    .pm,
    .pm.is-open { grid-template-columns: minmax(0, 1fr); }

    .pm.is-open .pm-list { display: none; }
    .pm:not(.is-open) .pm-thread-view { display: none; }

    .pm-back { display: inline-flex; }

    .pm-people { width: min(300px, 88vw); }

    .pm-msg { max-width: 88%; }

    /* The browser chrome on a phone eats more of the viewport. */
    .pm { height: calc(100vh - 150px); }
}

.alert {
    padding: 0.75rem 1rem;
    border-radius: 6px;
    margin-bottom: 1rem;
    font-size: 0.9rem;
}

/* A second .alert-error, which has been quietly winning over the one at the top
   of this file. Identical now that both are tokens; left in place rather than
   deleted so the two can be reconciled in a pass that is looking at alerts. */
.alert-error {
    background: var(--c-danger-soft);
    color: var(--c-danger-ink);
    border: 1px solid var(--c-danger-line);
}

/* Mentions */
.mention {
    display: inline-block;
    padding: 0 0.25em;
    border-radius: 3px;
    background: var(--c-primary-light);
    color: var(--c-primary);
    font-weight: 500;
    text-decoration: none;
}

.mention:hover {
    background: rgba(29, 116, 184, 0.16);
}

.mention-dropdown {
    position: absolute;
    z-index: 1000;
    min-width: 180px;
    max-height: 240px;
    overflow-y: auto;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
    padding: 0.25rem;
}

.mention-item {
    display: block;
    width: 100%;
    text-align: left;
    padding: 0.4rem 0.6rem;
    border: none;
    background: none;
    border-radius: 4px;
    font-size: 0.9rem;
    color: var(--c-text);
    cursor: pointer;
}

.mention-item:hover,
.mention-item.active {
    background: var(--c-primary);
    color: #fff;
}

/* Quoted content inside posts */
.post-body blockquote,
.pm-bubble blockquote {
    margin: 0.75rem 0;
    padding: 0.6rem 1rem;
    border-left: 3px solid var(--c-primary);
    background: var(--c-bg);
    border-radius: 0 6px 6px 0;
    color: var(--c-text);
}

.post-body blockquote p:first-child,
.pm-bubble blockquote p:first-child {
    margin-top: 0;
}

.post-body blockquote p:last-child,
.pm-bubble blockquote p:last-child {
    margin-bottom: 0;
}

/* Reactions */
.reaction-bar {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.75rem;
}

.reaction-counts {
    display: flex;
    gap: 0.3rem;
    flex-wrap: wrap;
}

.reaction-count {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.15rem 0.5rem;
    border-radius: 12px;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    font-size: 0.8rem;
    color: var(--c-text-muted);
}

.reaction-emoji {
    font-size: 0.9rem;
    line-height: 1;
}

.reaction-picker {
    position: relative;
}

.reaction-trigger {
    padding: 0.2rem 0.55rem;
    border: 1px solid var(--c-border);
    border-radius: 12px;
    background: var(--c-surface);
    font-size: 0.78rem;
    color: var(--c-text-muted);
    cursor: pointer;
}

.reaction-trigger:hover {
    background: var(--c-primary-light);
    border-color: var(--c-primary);
    color: var(--c-primary);
}

.reaction-menu {
    display: none;
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 100;
    gap: 0.15rem;
    padding: 0.3rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.12);
}

.reaction-menu.open {
    display: flex;
}

.reaction-option {
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: none;
    border-radius: 6px;
    font-size: 1.05rem;
    cursor: pointer;
    transition: background 0.12s, transform 0.12s;
}

.reaction-option:hover {
    background: var(--c-primary-light);
    transform: scale(1.15);
}

.reaction-option.selected {
    background: var(--c-primary);
}

.profile-actions {
    display: flex;
    justify-content: center;
    gap: 0.75rem;
    margin-top: 1.25rem;
    flex-wrap: wrap;
}

/* Activity stream */
.activity-filters {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.activity-filters form {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.activity-filters label {
    font-size: 0.85rem;
    font-weight: 600;
    color: var(--c-text-muted);
}

.activity-filters select {
    max-width: 240px;
}

.activity-total {
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

.activity-item {
    display: flex;
    gap: 0.85rem;
    padding: 0.9rem 1.25rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 8px;
    margin-bottom: 0.5rem;
}

.activity-avatar {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--c-primary);
    color: #fff;
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 600;
    font-size: 0.9rem;
}

.activity-body {
    flex: 1;
    min-width: 0;
}

.activity-headline {
    font-size: 0.92rem;
    margin-bottom: 0.25rem;
}

.activity-headline a {
    font-weight: 600;
    text-decoration: none;
    color: var(--c-primary);
}

.activity-headline a:hover {
    text-decoration: underline;
}

.activity-verb {
    display: inline-block;
    padding: 0.05em 0.4em;
    margin: 0 0.15em;
    border-radius: 4px;
    font-size: 0.72rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
}

.activity-verb-topic { background: var(--c-success-chip); color: var(--c-success-chip-ink); }
.activity-verb-reply { background: var(--c-info-chip); color: var(--c-info-chip-ink); }

.activity-excerpt {
    font-size: 0.88rem;
    color: var(--c-text-muted);
    line-height: 1.5;
    margin-bottom: 0.3rem;
}

.activity-meta {
    font-size: 0.8rem;
    color: var(--c-text-muted);
}

.activity-meta a {
    color: var(--c-text-muted);
}

/* Attachments — composer */
.attach-btn {
    margin-top: 0.5rem;
    padding: 0.35rem 0.7rem;
    border: 1px solid var(--c-border);
    border-radius: 6px;
    background: var(--c-surface);
    font-size: 0.83rem;
    color: var(--c-text-muted);
    cursor: pointer;
}

.attach-btn:hover {
    background: var(--c-primary-light);
    border-color: var(--c-primary);
    color: var(--c-primary);
}

.drop-active {
    outline: 2px dashed var(--c-primary);
    outline-offset: 3px;
}

.attachment-strip {
    margin-top: 0.5rem;
}

.attachment-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0.6rem;
    margin-bottom: 0.25rem;
    border: 1px solid var(--c-border);
    border-radius: 6px;
    background: var(--c-surface);
    font-size: 0.85rem;
}

.attachment-row.pending {
    color: var(--c-text-muted);
    font-style: italic;
}

.attachment-row.error {
    background: var(--c-danger-soft);
    border-color: var(--c-danger-line);
    color: var(--c-danger-ink);
}

/* Attachments — rendered on posts */
.post-attachments {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.85rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--c-border);
}

.post-attachments-label {
    width: 100%;
    font-size: 0.75rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
    margin-bottom: 0.15rem;
}

.attachment-thumb {
    display: block;
    width: 108px;
    height: 108px;
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid var(--c-border);
}

.attachment-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 0.18s;
}

.attachment-thumb:hover img {
    transform: scale(1.06);
}

.attachment-file {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.7rem;
    border: 1px solid var(--c-border);
    border-radius: 6px;
    background: var(--c-bg);
    text-decoration: none;
    color: var(--c-text);
    font-size: 0.85rem;
}

.attachment-file:hover {
    border-color: var(--c-primary);
    color: var(--c-primary);
}

.attachment-size {
    font-size: 0.78rem;
    color: var(--c-text-muted);
    margin-left: auto;
}

/* Inline images inside posts */
.post-body img,
.pm-bubble img {
    max-width: 100%;
    height: auto;
    border-radius: 6px;
    cursor: zoom-in;
}

/* Lightbox */
body.lightbox-open {
    overflow: hidden;
}

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.85);
    padding: 2rem;
}

.lightbox img {
    max-width: 100%;
    max-height: 85vh;
    object-fit: contain;
    border-radius: 6px;
    cursor: zoom-out;
}

.lightbox-close {
    position: absolute;
    top: 1rem;
    right: 1.25rem;
    width: 40px;
    height: 40px;
    border: none;
    border-radius: 50%;
    background: rgba(255, 255, 255, 0.15);
    color: #fff;
    font-size: 1.6rem;
    line-height: 1;
    cursor: pointer;
}

.lightbox-close:hover {
    background: rgba(255, 255, 255, 0.28);
}

.lightbox-caption {
    position: absolute;
    bottom: 1.25rem;
    left: 50%;
    transform: translateX(-50%);
    color: rgba(255, 255, 255, 0.85);
    font-size: 0.88rem;
    max-width: 80%;
    text-align: center;
}

/* Theme toggle */
/* Light, Dark, Match my device — one segmented control, three states, so the
   one you are on is visible rather than inferred from what the button offers
   to do next. */
.scheme-switch {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    padding: 2px;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.08);
}

.scheme-switch-option {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border: none;
    border-radius: var(--radius-pill);
    background: none;
    color: rgba(255, 255, 255, 0.6);
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
    /* The visible segment is 30px; the tap target is the 44px the hand needs.
       Grown with a pseudo-element rather than padding so three of them still
       fit the header bar. */
    position: relative;
}

.scheme-switch-option::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}

.scheme-switch-option:hover { color: #fff; }

.scheme-switch-option.is-on {
    background: rgba(255, 255, 255, 0.9);
    color: var(--c-header-bg);
}

@media (max-width: 640px) {
    .header-nav a { padding: 0.4rem 0.5rem; font-size: 0.85rem; }
}

/* The semantic tints that used to be re-stated for dark live in the token
   block now — see StyleSheet::semantic(). They were written as
   :root[data-theme="dark"] rules, which match nothing when the visitor is on
   System, so a dark machine got light-mode alert tints on dark surfaces. */


.header-nav form button,
.header-user form button {
    white-space: nowrap;
}

.header-user {
    flex-shrink: 0;
}

/* Who's online */
.online-block {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    margin-top: 1.5rem;
    overflow: hidden;
}

.online-header {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.7rem 1rem;
    background: var(--c-hover);
    border-bottom: 1px solid var(--c-border);
    font-size: 0.8rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--c-text-muted);
}

.online-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--c-success);
    flex-shrink: 0;
}

.online-body {
    padding: 0.9rem 1rem;
}

.online-counts {
    font-size: 0.9rem;
    color: var(--c-text-muted);
}

.online-counts strong {
    color: var(--c-text);
}

.online-members {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin-top: 0.6rem;
}

.online-member {
    display: inline-block;
    padding: 0.15rem 0.5rem;
    border-radius: 4px;
    background: var(--c-primary-light);
    color: var(--c-primary);
    font-size: 0.82rem;
    font-weight: 500;
    text-decoration: none;
}

.online-member:hover {
    text-decoration: none;
    background: var(--c-primary);
    color: #fff;
}

.online-total {
    margin-top: 0.6rem;
    padding-top: 0.6rem;
    border-top: 1px solid var(--c-border);
    font-size: 0.82rem;
    color: var(--c-text-muted);
}

/* Topic prefixes */
.prefix-badge {
    display: inline-block;
    padding: 0.1em 0.5em;
    border-radius: 4px;
    font-size: 0.75rem;
    font-weight: 700;
    line-height: 1.6;
    color: #fff;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    vertical-align: middle;
    white-space: nowrap;
}

.page-header h2 .prefix-badge {
    font-size: 0.7rem;
    position: relative;
    top: -2px;
}

.prefix-filter {
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin-bottom: 1rem;
}

.prefix-chip {
    --chip: var(--c-text-muted);
    display: inline-block;
    padding: 0.25rem 0.7rem;
    border-radius: 14px;
    border: 1px solid var(--c-border);
    background: var(--c-surface);
    font-size: 0.82rem;
    font-weight: 500;
    color: var(--c-text-muted);
    text-decoration: none;
}

.prefix-chip:hover {
    text-decoration: none;
    border-color: var(--chip);
    color: var(--chip);
}

.prefix-chip.active {
    background: var(--chip);
    border-color: var(--chip);
    color: #fff;
}

/* ---- the site's own mark ------------------------------------------------ */

/*
   🚨 This was 30px, and 30px is the Flarum/Convoro-1 habit rather than a
   decision: a small mark tucked at the left of a thin bar, sized so as not to
   disturb the row of controls beside it. It makes every install of the product
   look like the same install of the product.

   80px, and the header grows to hold it. A logo is the one piece of the page
   that is the operator's rather than ours, and the layout should be arranged
   around it rather than the other way round.

   30px → 48 → 64 → 80 over 2026-08-08, each step because it still read as small
   with a real logo in it. That is the only test that counts here, and it is
   worth recording that the first number was inherited rather than chosen.
*/
.site-logo {
    display: inline-flex;
    align-items: center;
    /* Reserves the height even before the image decodes, so the header does
       not jolt downward on first paint. */
    min-height: 80px;
}

.site-logo-img {
    height: 80px;
    width: auto;
    max-width: min(480px, 48vw);
    object-fit: contain;
    display: block;
}

/*
   The wordmark: what an operator sees when they have uploaded nothing.

   It has to hold the same visual weight as a logo would, because for most new
   installs this *is* the logo. Sized and spaced to sit on the same baseline as
   an image of the same height.
*/
.site-wordmark {
    font-size: 2rem;
    font-weight: 700;
    letter-spacing: -0.025em;
    line-height: 1.1;
    color: var(--c-header-ink, #fff);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: min(480px, 48vw);
}

/*
   Light and dark marks. Both are in the DOM and CSS picks, because theme mode
   may be `auto` — resolved by the visitor's operating system, which the server
   does not know when it renders.
*/
/*
   ---- What colour the BROWSER draws its own controls ----------------------

   🚨 `color-scheme`, and it has to be said in all three forms below.

   A checkbox, a radio, a scrollbar, a date picker and the inside of a
   <select> are drawn by the browser, not by this stylesheet. Left unsaid,
   the browser draws them light — so on a dark page the checkboxes down the
   right of the topic list were white squares, and the split-post bar had a
   white input sitting in a dark panel. Both were reported as display bugs and
   neither could be fixed with `background`: there is no element to style.

   Three declarations because the theme has three states, and the middle one
   is the one that gets forgotten:

     - dark, chosen explicitly
     - auto, where the OPERATING SYSTEM decides and the server cannot know
     - light, stated rather than left to inherit, so a dark OS does not drag a
       deliberately light site's controls dark with it

   This is exactly the mistake the semantic tints made before: dark rules
   written only as [data-theme="dark"], matching nothing for a visitor on
   `auto` with a dark machine.
*/
:root[data-theme="dark"] { color-scheme: dark; }
:root[data-theme="light"] { color-scheme: light; }

@media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] { color-scheme: dark; }
}

@media (prefers-color-scheme: light) {
    :root[data-theme="auto"] { color-scheme: light; }
}

.site-logo-dark { display: none; }

:root[data-theme="dark"] .site-logo-light { display: none; }
:root[data-theme="dark"] .site-logo-dark { display: block; }

@media (prefers-color-scheme: dark) {
    :root[data-theme="auto"] .site-logo-light { display: none; }
    :root[data-theme="auto"] .site-logo-dark { display: block; }
}

@media (max-width: 640px) {
    /* Still far larger than the 26px it was, and a phone screen is the one
       place where a tall bar genuinely costs reading room. */
    .site-logo { min-height: 52px; }
    .site-logo-img { height: 52px; }
    .site-wordmark { font-size: 1.45rem; }
}


/* Search trigger */
.search-trigger {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    margin-left: auto;
    border: none;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.08);
    color: rgba(255, 255, 255, 0.8);
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}

.search-trigger:hover {
    background: rgba(255, 255, 255, 0.18);
    color: #fff;
}

/* Search modal */
body.search-open {
    overflow: hidden;
}

.search-modal {
    position: fixed;
    inset: 0;
    z-index: 1500;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 12vh 1rem 1rem;
}

.search-modal[hidden] {
    display: none;
}

.search-modal-backdrop {
    position: absolute;
    inset: 0;
    background: rgba(10, 11, 16, 0.55);
    backdrop-filter: blur(3px);
}

.search-modal-panel {
    position: relative;
    width: 100%;
    /*
     * 🚨 Wider than it was. At 580px a result's title and its excerpt were
     * both truncated, so the modal answered "there are seven results" and made
     * you open one to find out what any of them were — which is the job the
     * modal exists to save you.
     */
    max-width: 760px;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 14px;
    box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
    overflow: hidden;
    animation: search-in 0.14s ease-out;
}

@keyframes search-in {
    from { opacity: 0; transform: translateY(-8px) scale(0.99); }
    to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
    .search-modal-panel { animation: none; }
}

.search-modal-head {
    display: flex;
    align-items: center;
    gap: 0.65rem;
    padding: 0.9rem 1rem;
    border-bottom: 1px solid var(--c-border);
}

.search-modal-icon {
    flex-shrink: 0;
    color: var(--c-text-muted);
}

.search-modal-head input {
    flex: 1;
    border: none;
    background: none;
    font-size: 1rem;
    font-family: inherit;
    color: var(--c-text);
    padding: 0;
}

.search-modal-head input:focus {
    outline: none;
}

.search-modal-esc {
    flex-shrink: 0;
    padding: 0.2rem 0.45rem;
    border: 1px solid var(--c-border);
    border-radius: 5px;
    background: var(--c-bg);
    font-size: 0.7rem;
    font-weight: 600;
    color: var(--c-text-muted);
    cursor: pointer;
}

.search-modal-esc:hover {
    color: var(--c-text);
}

.search-modal-results {
    /*
     * Taller too. 46vh on a laptop is about five results, and the sixth is the
     * one you wanted often enough to be annoying — widening the panel without
     * this just makes a short list wider.
     */
    max-height: 60vh;
    overflow-y: auto;
}

.search-modal-note {
    padding: 1.4rem 1rem;
    text-align: center;
    font-size: 0.88rem;
    color: var(--c-text-muted);
}

.search-result-item {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.6rem 1rem;
    text-decoration: none;
    color: var(--c-text);
    border-left: 2px solid transparent;
}

.search-result-item:hover,
.search-result-item.active {
    background: var(--c-hover);
    border-left-color: var(--c-primary);
    text-decoration: none;
}

.search-result-kind {
    flex-shrink: 0;
    padding: 0.1em 0.4em;
    border-radius: 4px;
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.kind-topic { background: rgba(52, 211, 153, 0.18); color: var(--c-kind-topic-ink); }
.kind-post  { background: rgba(96, 165, 250, 0.18); color: var(--c-kind-post-ink); }

.search-result-main {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
}

.search-result-heading {
    font-size: 0.92rem;
    font-weight: 600;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.search-result-sub {
    font-size: 0.8rem;
    color: var(--c-text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.search-result-by {
    flex-shrink: 0;
    font-size: 0.78rem;
    color: var(--c-text-muted);
}

.search-modal-foot {
    display: flex;
    gap: 1rem;
    padding: 0.55rem 1rem;
    border-top: 1px solid var(--c-border);
    background: var(--c-bg);
    font-size: 0.72rem;
    color: var(--c-text-muted);
}

.search-modal-foot kbd {
    display: inline-block;
    min-width: 16px;
    padding: 0 0.2em;
    margin-right: 0.15em;
    border: 1px solid var(--c-border-strong);
    border-radius: 3px;
    background: var(--c-surface);
    font-family: inherit;
    font-size: 0.7rem;
    text-align: center;
}

@media (max-width: 640px) {
    .search-modal { padding-top: 6vh; }
    .search-modal-foot { display: none; }
    .search-result-by { display: none; }
}

/* Menus
   ------------------------------------------------------------------------
   The header links are admin-managed (Admin -> Menus). .menu-link inherits
   the .header-nav a rules above; only the dropdown needs anything new.

   Submenus open on :hover and on :focus-within, with no JavaScript. That
   means they work before scripts load, and tabbing into the parent reveals
   the children without a roving-tabindex implementation to get wrong.      */

.menu-parent {
    position: relative;
    display: inline-flex;
    align-items: center;
}

/* Labels are admin-written and can be long. Wrapping one across two lines
   inside a single-row header is worse than letting the row scroll. */
.menu-link { white-space: nowrap; }

.menu-caret {
    margin-left: 0.3rem;
    font-size: 0.7em;
    opacity: 0.75;
}

.menu-submenu {
    position: absolute;
    top: 100%;
    left: 0;
    z-index: 60;
    min-width: 12rem;
    padding: 0.35rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius, 10px);
    box-shadow: var(--shadow, 0 10px 30px rgba(0,0,0,0.12));

    /* Hidden with opacity and pointer-events rather than display:none, so
       the panel keeps a size and the transition has something to animate. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity 0.14s ease, transform 0.14s ease, visibility 0.14s;
}

.menu-parent:hover .menu-submenu,
.menu-parent:focus-within .menu-submenu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

/* A small bridge across the gap between the trigger and the panel, so the
   pointer does not fall through it on the way down and close the menu. */
.menu-parent::after {
    content: "";
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    height: 0.5rem;
}

.menu-sublink {
    display: block;
    padding: 0.45rem 0.6rem;
    border-radius: 6px;
    font-size: 0.88rem;
    color: var(--c-text);
    text-decoration: none;
    white-space: nowrap;
}

.menu-sublink:hover {
    background: var(--c-hover);
    color: var(--c-text);
    text-decoration: none;
}

.footer-links {
    display: inline-flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.25rem 1rem;
    margin-right: 1rem;
}

.footer-link {
    color: var(--c-text-muted);
    text-decoration: none;
}

.footer-link:hover {
    color: var(--c-text);
    text-decoration: underline;
}

@media (max-width: 640px) {
    /* No hover on a touch screen, so the dropdown becomes a plain indented
       list rather than a panel that can never be opened. */
    .menu-submenu {
        position: static;
        opacity: 1;
        visibility: visible;
        transform: none;
        min-width: 0;
        padding: 0;
        background: none;
        border: 0;
        box-shadow: none;
    }

    .menu-parent { display: block; }
    .menu-caret { display: none; }
    .menu-sublink { color: rgba(255,255,255,0.7); padding-left: 1.25rem; }
}


/* Headings follow their own token, so a theme can pair a display face with a
   readable body face — the one typographic choice worth exposing. */
h1, h2, h3, h4, h5, h6 {
    font-family: var(--font-heading, inherit);
}

/* Preview banner. Fixed, so it is visible wherever the previewer scrolls to,
   and unmistakable — being unsure whether you are looking at the live site is
   the failure mode this exists to prevent. */
.theme-preview-bar {
    position: fixed;
    inset: 0 0 auto 0;
    z-index: 200;
    padding: 0.5rem 1rem;
    background: #78350f;
    color: #fef3c7;
    font-size: 0.85rem;
    text-align: center;
}

.theme-preview-bar a { color: #fde68a; text-decoration: underline; }
.theme-preview-bar + * { margin-top: 2.25rem; }

/* Per-member theme choice
   ------------------------------------------------------------------------ */

.theme-choices {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(210px, 1fr));
    gap: 0.85rem;
    margin-bottom: 1.25rem;
}

.theme-choice {
    display: grid;
    grid-template-rows: auto auto auto;
    gap: 0.15rem;
    padding: 0.85rem;
    border: 2px solid var(--c-border);
    border-radius: var(--radius);
    background: var(--c-surface);
    cursor: pointer;
}

.theme-choice:hover { border-color: var(--c-border-strong); }
.theme-choice.is-chosen { border-color: var(--c-primary); }

/* The radio stays in the DOM for keyboard and screen readers; the card is
   the visible control. */
.theme-choice input {
    position: absolute;
    opacity: 0;
    pointer-events: none;
}

.theme-choice:focus-within { outline: 2px solid var(--c-primary); outline-offset: 2px; }

.theme-choice-preview {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 64px;
    margin-bottom: 0.5rem;
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius) - 2px);
}

.theme-choice-default {
    background: linear-gradient(135deg, var(--c-surface) 50%, var(--c-bg) 50%);
}

.theme-choice-dot {
    width: 26px;
    height: 26px;
    border-radius: 50%;
    background: var(--c-primary);
}

.theme-choice-name { font-weight: 600; font-size: 0.92rem; }
.theme-choice-note { font-size: 0.8rem; color: var(--c-text-muted); }

.theme-choice-footnote {
    margin-top: 1.25rem;
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

/* Pages
   ------------------------------------------------------------------------ */

.page-body { max-width: 100%; }
.page-width-narrow { max-width: 42rem; margin: 0 auto; }
.page-width-wide { max-width: none; }

.page-title { font-size: 1.9rem; margin-bottom: 1rem; }

.page-notice {
    padding: 0.75rem 1rem;
    margin-bottom: 1.25rem;
    border-radius: var(--radius);
    background: var(--c-primary-light);
    border: 1px solid var(--c-primary);
    font-size: 0.88rem;
}

.page-block-heading { margin: 1.75rem 0 0.6rem; }
.page-block-rich { margin-bottom: 1rem; line-height: 1.7; }
.page-block-rich p { margin-bottom: 0.85rem; }
.page-block-html { margin-bottom: 1rem; }

.page-block-image { margin: 1.25rem 0; }
.page-block-image img { max-width: 100%; height: auto; border-radius: var(--radius); }
.page-block-image figcaption {
    margin-top: 0.4rem;
    font-size: 0.82rem;
    color: var(--c-text-muted);
}

/* A 16:9 box, so the page does not jump as the iframe loads. */
.page-block-embed {
    position: relative;
    margin: 1.25rem 0;
    padding-bottom: 56.25%;
    border-radius: var(--radius);
    overflow: hidden;
}

.page-block-embed iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: 0;
}

.page-block-topics {
    display: grid;
    gap: 0.5rem;
    margin: 1.25rem 0;
}

.page-topic {
    display: block;
    padding: 0.7rem 0.9rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    background: var(--c-surface);
    text-decoration: none;
    color: inherit;
}

.page-topic:hover { border-color: var(--c-border-strong); text-decoration: none; }
.page-topic-title { display: block; font-weight: 600; }
.page-topic-meta { display: block; font-size: 0.8rem; color: var(--c-text-muted); }

.post-edit-actions {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin-top: 1rem;
}

/* ============================================================================
   Modern pass — 2026-08-04
   ============================================================================

   A refresh toward something less flat: cards that sit on the page rather than
   being drawn on it, larger radii, more air, and a stronger typographic
   hierarchy. Direction only — the layout metaphors, palette and spacing are
   Convoro's own, per ROADMAP.md's originality rules.

   Everything here still reads the theme tokens, so a site that has picked its
   own accent, radius or neutrals keeps them. Nothing is hardcoded.

   Written as a final section that refines the base rules above rather than a
   rewrite of them: the base is what a theme with everything turned down still
   gets, and it stays intact.
   ========================================================================= */

:root {
    /* Derived from the theme's radius so the control still means something —
       a site that sets 0 gets square corners everywhere, not square inputs
       inside rounded cards. */
    --radius-card: calc(var(--radius, 12px) * 1.35);
    --radius-pill: 999px;
}


/* ---- Header ------------------------------------------------------------ */

.site-header {
    padding: 0.35rem 0;
    /* A hairline rather than a hard edge, so the header reads as a surface
       above the page instead of a band drawn across it. */
    box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.06);
}

.header-inner { gap: 0.75rem; }

.header-nav { gap: 0.15rem; }

.header-nav a,
.menu-link {
    padding: 0.45rem 0.85rem;
    border-radius: var(--radius-pill);
    font-weight: 500;
}

.header-nav .btn-sm {
    padding: 0.5rem 1.1rem;
    border-radius: var(--radius-pill);
    font-weight: 600;
}

.search-trigger {
    border-radius: var(--radius-pill);
    width: 36px;
    height: 36px;
}

/* ---- Page rhythm ------------------------------------------------------- */

.site-main { padding-top: 2rem; padding-bottom: 3rem; }

.page-header {
    margin-bottom: 1.5rem;
    align-items: flex-end;
}

.page-header h2 {
    font-size: 1.75rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    font-family: var(--font-heading, inherit);
}

/* ---- Forum list -------------------------------------------------------- */

/* The category is now a grouping, not a container: its forums are cards in
   their own right, and wrapping them in another card was a box inside a box. */
.forum-category {
    margin-bottom: 2rem;
    background: none;
    box-shadow: none;
    border-radius: 0;
    overflow: visible;
}

/*
   The category name becomes a label sitting above its forums rather than a
   filled bar inside the card. It groups without competing with the forum
   titles, which are the thing somebody is actually scanning for.
*/
.category-header {
    background: none;
    border: 0;
    padding: 0 0.25rem 0.6rem;
}

.category-header h3 {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* Each forum is its own card, so the list reads as a set of destinations
   rather than rows in a table. */
.forum-row {
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    background: var(--c-surface);
    box-shadow: var(--lift);
    padding: 1.15rem 1.25rem;
    margin-bottom: 0.75rem;
    gap: 1.1rem;
    transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}

.forum-row:last-child { margin-bottom: 0; }

.forum-row:hover {
    transform: translateY(-2px);
    box-shadow: var(--lift-hover);
    text-decoration: none;
}

.forum-icon {
    width: 46px;
    height: 46px;
    border-radius: calc(var(--radius-card) * 0.7);
    background: var(--c-primary-light);
    color: var(--c-primary);
    flex-shrink: 0;
}

.forum-title {
    font-size: 1.05rem;
    font-weight: 650;
    letter-spacing: -0.01em;
}

.forum-desc { margin-top: 0.15rem; }

/* Stats stack into a tidy column instead of sitting in a row, which stops
   them colliding with the description on a narrow screen. */
.forum-stats {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.1rem;
    font-variant-numeric: tabular-nums;
    font-size: 0.82rem;
}

/* ---- Topic list -------------------------------------------------------- */

/*
    Cards sit on the page, not inside a panel. The original list was a single
    bordered surface with rows in it; once the rows became cards the wrapper
    was left showing through the gaps as a second, pointless surface.
*/
.topic-list {
    gap: 0.6rem;
    display: flex;
    flex-direction: column;
    background: none;
    box-shadow: none;
    border-radius: 0;
    overflow: visible;
}

.topic-row {
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    background: var(--c-surface);
    box-shadow: var(--lift);
    padding: 1rem 1.15rem;
    transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}

.topic-row:hover {
    transform: translateY(-2px);
    box-shadow: var(--lift-hover);
    border-color: var(--c-primary);
    text-decoration: none;
}

.topic-title {
    font-size: 1rem;
    font-weight: 650;
    letter-spacing: -0.01em;
}

/*
   The opening line of the post, under the title.

   Deliberately quieter than the title and clamped to two lines: it is there to
   help somebody decide, not to be read in the listing. A three-line excerpt
   turns twenty topics into a wall and the titles stop being scannable, which
   is the thing the list is actually for.
*/
.topic-excerpt {
    margin-top: 0.3rem;
    font-size: 0.875rem;
    line-height: 1.5;
    color: var(--c-text-muted);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

@media (max-width: 640px) {
    /* One line on a phone, where two is most of the row. */
    .topic-excerpt { -webkit-line-clamp: 1; font-size: 0.84rem; }
}

.topic-stats { font-variant-numeric: tabular-nums; }

/* ---- Posts ------------------------------------------------------------- */

.post {
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    overflow: hidden;
}

.post-avatar {
    border-radius: calc(var(--radius-card) * 0.65);
    font-weight: 700;
}

/* ---- Blocks and controls ----------------------------------------------- */

.online-block,
.empty-state,
.reply-form-wrapper,
.profile-card,
.auth-form,
.alert {
    border-radius: var(--radius-card);
}

.online-block,
.profile-card,
.auth-form { box-shadow: var(--lift); }

.online-header {
    background: none;
    border-bottom: 1px solid var(--c-border);
    font-size: 0.75rem;
    letter-spacing: 0.09em;
}

.btn,
.btn-primary,
.btn-outline {
    border-radius: var(--radius-pill);
    padding: 0.6rem 1.35rem;
    font-weight: 600;
    letter-spacing: -0.005em;
    transition: transform 0.14s ease, box-shadow 0.14s ease, background 0.14s ease;
}

.btn-primary:hover {
    transform: translateY(-1px);
    box-shadow: var(--lift);
}

.badge,
.tag-badge,
.prefix-badge,
.page-link { border-radius: var(--radius-pill); }

.page-link { padding: 0.45rem 0.9rem; }

.form-input,
.form-textarea,
.form-select {
    border-radius: calc(var(--radius, 12px) * 0.9);
    padding: 0.7rem 0.9rem;
}

/* ---- Search palette ----------------------------------------------------
   The roadmap names this as the reference point for Convoro's visual
   language, so it gets the same treatment rather than being left behind. */

.search-modal-panel {
    border-radius: var(--radius-card);
    box-shadow: var(--lift-hover);
}

@media (max-width: 640px) {
    /* The lift is a pointer affordance. On a touch screen it costs paint and
       buys nothing, and the tighter spacing matters more. */
    .forum-row:hover,
    .topic-row:hover { transform: none; }

    .forum-row { padding: 0.95rem 1rem; }
    .page-header h2 { font-size: 1.45rem; }
}

/* ============================================================================
   Second pass — content, not just surfaces
   ========================================================================= */

/* ---- Avatars ------------------------------------------------------------
   The fallback colour comes from the username, so it is set inline; only the
   shape and size live here. */

.avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    overflow: hidden;
    color: #fff;
    font-weight: 700;
    line-height: 1;
    letter-spacing: -0.02em;
    /* Circles. A squircle was tried, on the argument that it matched the
       cards — it read as a design choice nobody asked for, and a face is a
       circle everywhere else on the internet. */
    border-radius: 50%;
}

.avatar img { width: 100%; height: 100%; object-fit: cover; }

/*
   The avatar scale, raised across the board on 2026-08-08.

   26/40/64/104 was small enough that a person read as a decoration beside
   their name rather than as the point of the row — the same inherited habit
   that had the logo at 30px. A forum is people talking, and the faces should
   be big enough to recognise at a glance.

   🚨 The ceiling is real: AvatarStore keeps one 256px square, so the largest
   size must stay at or under 128 to remain sharp on a 2x display. Every step
   here doubles to 256 or less. Going bigger means storing bigger first.
*/
.avatar-sm { width: 34px; height: 34px; font-size: 0.82rem; }
.avatar-md { width: 52px; height: 52px; font-size: 1.15rem; }
.avatar-lg { width: 80px; height: 80px; font-size: 1.8rem; }

/* ---- The stats band ----------------------------------------------------- */

.stat-band {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 0.75rem;
    margin-bottom: 2rem;
}

.stat-tile {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    padding: 1rem 1.15rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
}

.stat-value {
    font-size: 1.6rem;
    font-weight: 700;
    letter-spacing: -0.03em;
    line-height: 1.1;
    /* Tabular, so a row of tiles does not jitter as the numbers change. */
    font-variant-numeric: tabular-nums;
    font-family: var(--font-heading, inherit);
}

.stat-label {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.stat-tile-person {
    flex-direction: row;
    align-items: center;
    gap: 0.7rem;
    text-decoration: none;
    color: inherit;
    transition: border-color 0.16s ease, transform 0.16s ease;
}

.stat-tile-person:hover {
    border-color: var(--c-primary);
    transform: translateY(-2px);
    text-decoration: none;
}

.stat-person { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

.stat-person-name {
    font-weight: 650;
    font-size: 0.95rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---- Row stats ----------------------------------------------------------
   A number and its label as one unit, rather than loose text at the end of a
   row. The value leads; the label is support. */

.stat-pair {
    display: flex;
    flex-direction: column;
    align-items: center;
    min-width: 3.25rem;
}

.stat-pair-value {
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.2;
    font-variant-numeric: tabular-nums;
}

.stat-pair-label {
    font-size: 0.68rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* ---- Forum rows --------------------------------------------------------- */

.forum-row {
    display: grid;
    grid-template-columns: auto 1fr auto auto;
    align-items: center;
    gap: 1.1rem;
}

.forum-stats {
    flex-direction: row;
    gap: 0.35rem;
    align-items: stretch;
}

.forum-latest {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 15rem;
    padding-left: 1.1rem;
    border-left: 1px solid var(--c-border);
    min-height: 2.4rem;
}

.forum-latest-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

.forum-latest-title {
    font-size: 0.86rem;
    font-weight: 600;
    color: var(--c-text);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.forum-latest-meta,
.forum-latest-empty {
    font-size: 0.76rem;
    color: var(--c-text-muted);
}

/* ---- Topic rows --------------------------------------------------------- */

.topic-row {
    display: grid;
    /*
       🚨 THREE columns: avatar, everything about the topic, the counts.

       There was a fourth between the avatar and the title, holding the Pinned
       and Locked badges. It was set to collapse when empty — which is most
       rows — so it looked fine until a topic was actually pinned, and then
       that ONE row's title, prefix, excerpt and byline all shifted sideways
       while every row above and below stayed where it was. The badges read
       better after the title anyway; they are facts about the topic, not a
       column of the list.
    */
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: 0.9rem;
}

/*
   The title line, with its prefix and badges on it.

   `align-items: baseline` rather than center: a badge is smaller than the
   title, and centring makes it float in the middle of the cap height instead
   of sitting on the same line as the words.
*/
.topic-title {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.4rem;
}

/* Long titles wrap and take the badges with them, rather than pushing the
   counts off the end of the row. */
.topic-title-text { min-width: 0; }

/* The badges do not stretch on the wrap, and never break across lines. */
.topic-title .badge { flex: 0 0 auto; white-space: nowrap; align-self: center; }
.topic-replies-label { font-weight: 600; opacity: 0.85; }

.topic-starter {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
}

/* Replies is the number people judge a thread by, so it is a chip. Views are
   context, so they stay quiet underneath. */
.topic-stats {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.25rem;
}

.topic-replies {
    display: inline-flex;
    align-items: baseline;
    gap: 0.3rem;
    padding: 0.3rem 0.65rem;
    border-radius: var(--radius-pill);
    background: var(--c-primary-light);
    color: var(--c-primary);
    font-size: 0.8rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
}

.topic-views { font-size: 0.72rem; color: var(--c-text-muted); }

/* ---- Sticky header ------------------------------------------------------ */

.site-header {
    position: sticky;
    top: 0;
    z-index: 100;
}

@supports (backdrop-filter: blur(10px)) {
    /* Content passing underneath should read as passing underneath, not as
       being clipped by a solid bar. */
    .site-header {
        backdrop-filter: blur(12px) saturate(140%);
        background-color: color-mix(in srgb, var(--c-header-bg, #16171f) 88%, transparent);
    }
}

/* ---- Empty states ------------------------------------------------------- */

.empty-state {
    padding: 3rem 1.5rem;
    text-align: center;
    background: var(--c-surface);
    border: 1px dashed var(--c-border-strong);
}

.empty-state p { color: var(--c-text-muted); margin-bottom: 1rem; }

@media (max-width: 900px) {
    /* The last-post column is the first thing to go: it is the widest and the
       least essential of the four. */
    .forum-latest { display: none; }
    .forum-row { grid-template-columns: auto 1fr auto; }
}

@media (max-width: 640px) {
    .forum-stats { display: none; }
    .forum-row { grid-template-columns: auto 1fr; }
    .stat-band { grid-template-columns: repeat(2, 1fr); }
    .stat-value { font-size: 1.35rem; }
}

/* ============================================================================
   Third pass — the topic view
   ========================================================================= */

/* The gutter is gone: post text gets the full width, which is what people
   came for. The identity moved into the header where it reads as a sentence. */
.post {
    display: block;
    padding: 1.25rem 1.4rem;
    margin-bottom: 0.85rem;
}

.post-sidebar { display: none; }

.post-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding-bottom: 0.9rem;
    margin-bottom: 1rem;
    border-bottom: 1px solid var(--c-border);
}

.post-identity {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    text-decoration: none;
    color: inherit;
    min-width: 0;
}

.post-identity:hover { text-decoration: none; }
.post-identity:hover .post-author { color: var(--c-primary); }

.post-identity-text { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

.post-author {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-weight: 650;
    font-size: 0.95rem;
    color: var(--c-text);
    transition: color 0.14s ease;
}

.post-author-stats { font-size: 0.76rem; color: var(--c-text-muted); }

.post-header-right { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; }

.post-number {
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--c-text-muted);
    padding: 0.2rem 0.55rem;
    border-radius: var(--radius-pill);
    background: var(--c-hover);
    text-decoration: none;
    transition: background 0.14s ease, color 0.14s ease;
}

.post-number:hover {
    background: var(--c-primary-light);
    color: var(--c-primary);
    text-decoration: none;
}

.post-number.is-copied { background: var(--c-success); color: #fff; }

.post-body { font-size: 0.98rem; line-height: 1.75; }

/* ---- Role badges --------------------------------------------------------
   Only admin and moderator. A badge for every group is noise; these two are
   the ones that change how an answer reads. */

.role-badge {
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    padding: 0.15em 0.5em;
    border-radius: var(--radius-pill);
    line-height: 1.5;
}

.role-admin { background: var(--c-primary-light); color: var(--c-primary); }

.role-moderator {
    background: color-mix(in srgb, var(--c-success) 16%, transparent);
    color: var(--c-success);
}

/* ---- Post actions -------------------------------------------------------
   Faded until wanted, so they stop competing with the text — but only where
   there is a pointer to hover with. A hover-only control does not exist on a
   phone, and it must never hide from the keyboard. */

@media (hover: hover) and (pointer: fine) {
    .post-actions {
        opacity: 0;
        transition: opacity 0.16s ease;
    }

    .post:hover .post-actions,
    .post:focus-within .post-actions { opacity: 1; }
}

.post-actions { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }

.reaction-bar { margin-top: 0.85rem; }

/* ---- Profile ------------------------------------------------------------ */

.profile-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35rem;
    padding: 2.25rem 1.5rem 1.75rem;
}

.profile-username {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1.5rem;
    letter-spacing: -0.02em;
}

/* The member's numbers should look like the site's numbers. */
.profile-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
    gap: 0.75rem;
    width: 100%;
    margin: 1.5rem 0 0.5rem;
    border: 0;
    padding: 0;
}

.profile-stats > div {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.15rem;
    padding: 0.85rem 0.5rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.75);
}

.profile-stats strong {
    font-size: 1.25rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.profile-stats > div { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--c-text-muted); font-weight: 700; }
.profile-stats strong { color: var(--c-text); text-transform: none; letter-spacing: -0.02em; }

.profile-actions { margin-top: 1.25rem; display: flex; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }

@media (max-width: 640px) {
    .post { padding: 1rem; }
    .post-header { flex-wrap: wrap; gap: 0.5rem; }
}

/* ============================================================================
   Widgets
   ========================================================================= */

/* The sidebar is a grid column that only exists when the markup is there, so
   a site with no sidebar widgets gets the same full-width page it had. */
.site-main.has-sidebar {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 22rem;
    gap: 2rem;
    align-items: start;
}

.site-sidebar { display: flex; flex-direction: column; gap: 1rem; }

.widget-area { display: flex; flex-direction: column; gap: 1rem; }
.widget-area-index\.top, .widget-area-index\.bottom,
.widget-area-forum\.top, .widget-area-topic\.top { margin-bottom: 1.75rem; }

.widget {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    padding: 1.1rem 1.2rem;
}

/* A content widget with no title is bare markup on purpose — that is how a
   banner goes into an area without a panel wrapped round it. */
.widget-bare {
    background: none;
    border: 0;
    box-shadow: none;
    padding: 0;
}

.widget-title {
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-text-muted);
    margin-bottom: 0.85rem;
}

.widget-empty { font-size: 0.85rem; color: var(--c-text-muted); }

/* ---- Statistics --------------------------------------------------------- */

/* Named for the container, not the widget type: the wrapper already carries
   .widget-stats from its type, and a shared name made the whole panel a grid. */
.widget-stat-grid {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.5rem;
    text-align: center;
}

.widget-stat { display: flex; flex-direction: column; gap: 0.1rem; }

.widget-stat-value {
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.widget-stat-label {
    font-size: 0.64rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.widget-newest {
    margin-top: 0.9rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--c-border);
    font-size: 0.82rem;
    color: var(--c-text-muted);
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

/* ---- People ------------------------------------------------------------- */

.widget-people { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.widget-person {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.25rem 0.6rem 0.25rem 0.25rem;
    border-radius: var(--radius-pill);
    background: var(--c-hover);
    font-size: 0.8rem;
    color: var(--c-text);
    text-decoration: none;
}

.widget-person:hover { background: var(--c-primary-light); color: var(--c-primary); text-decoration: none; }
.widget-person-name { font-weight: 600; }

.widget-ranked { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }

.widget-ranked li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
}

.widget-ranked-count {
    font-size: 0.78rem;
    font-weight: 700;
    font-variant-numeric: tabular-nums;
    color: var(--c-text-muted);
}

/* ---- Topics ------------------------------------------------------------- */

/* Named for the container, never after a widget type: the wrapper already
   carries .widget-<type>, so .widget-topics here zeroed the panel's own
   padding and put the text against the edge. See widget-stat-grid above —
   this is the second instance, which is why there is now a test for it. */
.widget-topic-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }

.widget-topic-list li + li { border-top: 1px solid var(--c-border); }

.widget-topic-list a {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.55rem 0;
    text-decoration: none;
    color: inherit;
}

.widget-topic-list a:hover .widget-topic-title { color: var(--c-primary); }

.widget-topic-title {
    font-size: 0.86rem;
    font-weight: 600;
    line-height: 1.35;
    transition: color 0.14s ease;
}

.widget-topic-meta { font-size: 0.74rem; color: var(--c-text-muted); }

/* ---- Sign-up prompt ----------------------------------------------------- */

.widget-welcome-text { font-size: 0.88rem; line-height: 1.6; margin-bottom: 0.85rem; }
.widget-welcome-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.widget-welcome-actions .btn { padding: 0.5rem 1rem; font-size: 0.85rem; }

@media (max-width: 900px) {
    /* The sidebar drops under the content rather than squeezing it. */
    .site-main.has-sidebar { grid-template-columns: minmax(0, 1fr); }
    .site-sidebar { margin-top: 1.75rem; }
}

/* ============================================================================
   Traditional post layout, drawn modern
   ============================================================================

   Who is speaking down the left, what they said on the right — the shape
   people know from every forum. The modernising is in how it is drawn: a
   large squircle avatar, a quiet fact list rather than a stack of labels, and
   a hairline rule instead of a filled column.
   ========================================================================= */

.post {
    display: grid;
    /* Wide enough that an ordinary username and a "Aug 2026" both sit on one
       line. At 10.5rem "DemoUser" broke across two, which is the kind of
       detail that makes a layout look unfinished. */
    grid-template-columns: 12.5rem minmax(0, 1fr);
    gap: 0;
    padding: 0;
    overflow: hidden;
}

.post-sidebar {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.45rem;
    /* The original stylesheet pins this to 100px for the old flex layout.
       As a grid item that width wins over the track, which is why the name
       was breaking mid-word in a column that looked wide enough. */
    width: auto;
    padding: 1.35rem 0.9rem;
    text-align: center;
    /* A rule rather than a filled panel: the column should read as part of
       the card, not as a second card stuck to its side. */
    border-right: 1px solid var(--c-border);
}

.post-identity {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.5rem;
    text-decoration: none;
    color: inherit;
    max-width: 100%;
}

.post-identity:hover { text-decoration: none; }
.post-identity:hover .post-author { color: var(--c-primary); }

.post-author {
    display: block;
    font-weight: 650;
    font-size: 0.92rem;
    line-height: 1.3;
    color: var(--c-text);
    /* break-word, not anywhere: a long name should break only when it truly
       cannot fit, never in the middle of one that would have. */
    overflow-wrap: break-word;
    hyphens: none;
    max-width: 100%;
    transition: color 0.14s ease;
}

.post-author-facts {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    margin-top: 0.35rem;
    width: 100%;
}

.post-author-facts > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem;
    font-size: 0.72rem;
}

.post-author-facts dt {
    color: var(--c-text-muted);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    font-weight: 700;
    font-size: 0.62rem;
}

.post-author-facts dd {
    margin: 0;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
    /* "Aug 2026" is one value, not two words to be split up. */
    white-space: nowrap;
}

.post-content { padding: 1.35rem 1.4rem; min-width: 0; }

.post-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding-bottom: 0.85rem;
    margin-bottom: 1rem;
    border-bottom: 1px solid var(--c-border);
}

.post-when { font-size: 0.78rem; color: var(--c-text-muted); }

@media (max-width: 640px) {
    /* The column becomes a row above the post: a 10rem gutter on a phone is
       most of the screen. */
    .post { grid-template-columns: minmax(0, 1fr); }

    .post-sidebar {
        flex-direction: row;
        align-items: center;
        justify-content: flex-start;
        /* 🚨 It wraps. A member with a rank, a badge and three facts is not an
           edge case, and without this the strip is one unbreakable row that
           squeezes the name until it renders one letter per line. */
        flex-wrap: wrap;
        gap: 0.7rem;
        text-align: left;
        padding: 0.9rem 1rem;
        border-right: 0;
        border-bottom: 1px solid var(--c-border);
    }

    .post-identity {
        flex-direction: row;
        align-items: center;
        gap: 0.6rem;
        /* The name is the thing you are least willing to lose, so it does not
           shrink and it does not break mid-word. */
        flex: 1 0 auto;
        min-width: 0;
    }

    .post-identity .post-author { white-space: nowrap; }
    .post-identity .avatar-lg { width: 38px; height: 38px; font-size: 1rem; }

    .post-author-facts {
        flex-direction: row;
        gap: 0.9rem;
        margin: 0 0 0 auto;
        width: auto;
        flex-wrap: wrap;
    }

    .post-author-facts > div { flex-direction: column; align-items: flex-end; gap: 0; }

    /* The rank and badges sit with the name rather than fighting the facts
       for the end of the row. */
    .post-rank, .post-badges { flex: 0 0 auto; }
    .post-content { padding: 1rem; }
}


/* ---- Sub-header --------------------------------------------------------
   The breadcrumb gets a bar of its own under the header, so it reads as part
   of the chrome rather than floating on the page background. Sticky under the
   header for the same reason the header is: on a long topic, knowing where
   you are should not need a scroll back to the top. */

.site-subheader {
    position: sticky;
    top: var(--header-height, 60px);
    z-index: 90;
    background: var(--c-surface);
    border-bottom: 1px solid var(--c-border);
}

.site-subheader .breadcrumb {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.15rem 0.35rem;
    margin: 0;
    padding: 0.7rem 0;
    font-size: 0.82rem;
    color: var(--c-text-muted);
    min-height: 1rem;
}

.site-subheader .breadcrumb a {
    color: var(--c-text-muted);
    font-weight: 500;
    padding: 0.15rem 0.35rem;
    border-radius: 6px;
    transition: background 0.14s ease, color 0.14s ease;
}

.site-subheader .breadcrumb a:hover {
    background: var(--c-hover);
    color: var(--c-text);
    text-decoration: none;
}

/* The last crumb is where you are, so it reads as the current place rather
   than as one more link that happens not to work. */
.site-subheader .breadcrumb span {
    color: var(--c-text);
    font-weight: 600;
    padding: 0.15rem 0.35rem;
}

/* The page content no longer needs to clear a floating breadcrumb. */
.site-main { padding-top: 1.75rem; }

/* ---- A plain content card ----------------------------------------------
   The front end had no generic card class — every panel rolled its own. This
   is the shared one, matching the widget and forum-row treatment. */

.card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    padding: 1.4rem 1.5rem;
    margin-bottom: 1.25rem;
}

.card-title {
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-text-muted);
    margin-bottom: 1.1rem;
}

/* ---- API tokens --------------------------------------------------------- */

/* Loud on purpose: this is the only moment the token is readable. */
.token-reveal {
    background: var(--c-primary-light);
    border: 1px solid var(--c-primary);
    border-radius: var(--radius-card);
    padding: 1.2rem 1.4rem;
    margin-bottom: 1.25rem;
}

.token-reveal p { font-size: 0.86rem; color: var(--c-text-muted); margin: 0.4rem 0 0.9rem; }

.token-value {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}

.token-value code {
    flex: 1;
    min-width: 14rem;
    padding: 0.6rem 0.8rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius) * 0.9);
    font-size: 0.85rem;
    overflow-wrap: anywhere;
}

.token-abilities { display: flex; flex-direction: column; gap: 0.5rem; }

.token-ability {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    cursor: pointer;
}

.token-ability code { font-size: 0.82rem; font-weight: 700; }
.token-ability-note { display: block; font-size: 0.82rem; color: var(--c-text-muted); }

.token-list { display: flex; flex-direction: column; gap: 0.6rem; }

.token-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.85rem 1rem;
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.8);
}

/* A revoked token stays listed — "this was used at 3am and is now revoked"
   should remain answerable — but it should not read as usable. */
.token-row.is-revoked { opacity: 0.6; }

.token-name { font-weight: 650; display: flex; align-items: center; gap: 0.5rem; }
.token-meta { font-size: 0.78rem; color: var(--c-text-muted); margin-top: 0.15rem; overflow-wrap: anywhere; }
.token-empty, .token-docs { font-size: 0.86rem; color: var(--c-text-muted); }
.token-docs { margin-top: 0.5rem; }

/* ---- Security and two-step verification --------------------------------- */

.code-input {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 1.15rem;
    letter-spacing: 0.25em;
    max-width: 12rem;
}

.auth-intro { font-size: 0.9rem; color: var(--c-text-muted); margin-bottom: 1.25rem; }

.recovery-codes {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: 0.4rem;
    margin-top: 0.5rem;
}

.recovery-codes code {
    padding: 0.5rem 0.6rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius) * 0.8);
    font-size: 0.88rem;
    text-align: center;
    letter-spacing: 0.05em;
}

.security-state { font-size: 0.95rem; margin-bottom: 0.75rem; }
.security-on strong { color: var(--c-success); }

.security-key {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1rem;
    padding: 1rem;
    margin: 1rem 0 1.25rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.8);
}

.security-key-manual code {
    display: block;
    margin-top: 0.2rem;
    font-size: 1rem;
    letter-spacing: 0.12em;
}

/*
    The inputs are narrow, not the form: constraining the whole thing squeezed
    the explanatory text under the code field into three short lines.
*/
.security-form .form-input { max-width: 22rem; }
.security-form .form-hint { max-width: 34rem; }
.security-divider { border: 0; border-top: 1px solid var(--c-border); margin: 1.5rem 0; }

/* Settings sub-navigation, shared by every settings page. */
.settings-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin: -0.25rem 0 1.5rem;
    border-bottom: 1px solid var(--c-border);
}

.settings-nav-item {
    padding: 0.55rem 0.9rem;
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--c-text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    transition: color 0.15s ease, border-color 0.15s ease;
}

.settings-nav-item:hover { color: var(--c-text); text-decoration: none; }

.settings-nav-item.is-active {
    color: var(--c-primary);
    border-bottom-color: var(--c-primary);
}

.security-heading {
    font-size: 0.95rem;
    font-weight: 600;
    margin: 0 0 0.4rem;
    color: var(--c-text);
}

.security-form .form-group { margin-bottom: 1rem; }
.security-form + .security-divider { margin-top: 1.5rem; }

/*
    Corrections from looking at the rendered page:
    - the count sat flush against the label under it;
    - `.security-form .form-input` out-specified `.code-input`, so the code
      box rendered at full width instead of the width of a code;
    - hints inside and outside the forms wrapped at different widths.
*/
.security-form .form-input.code-input { max-width: 12rem; }

.card > .form-hint,
.security-heading + .form-hint { max-width: 34rem; }

.security-heading + .form-hint,
.security-form .form-group > .form-hint { margin-bottom: 0.9rem; }

/* ---- Bulk moderation ---------------------------------------------------- */

/*
    The row is a link, so the checkbox has to sit beside it rather than inside
    it — ticking a box must not open the topic.
*/
.topic-item { display: flex; align-items: stretch; position: relative; }
.topic-item > .topic-row { flex: 1; min-width: 0; }

/*
   The select box, laid over the right of the card.

   It used to be a 2.25rem gutter to the *left* of the row, so it floated in
   the page margin outside the card — visibly detached from the thing it
   selects, and the first thing the eye hit on every row whether or not
   anybody was moderating.

   🚨 It cannot be moved into the markup: the row is an anchor, so a label
   inside it would open the topic when ticked. Absolute positioning puts it
   where it belongs without nesting it, and the row gets padding on that side
   so the reply and view counts never sit underneath it.
*/
.topic-select {
    position: absolute;
    top: 50%;
    right: 0.9rem;
    transform: translateY(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    /* A real target, not a 13px square. */
    width: 2rem;
    height: 2rem;
    border-radius: 8px;
    cursor: pointer;
}

.topic-select:hover { background: var(--c-hover); }

.topic-select input {
    cursor: pointer;
    width: 17px;
    height: 17px;
    accent-color: var(--c-primary);
}

.topic-item.is-selectable > .topic-row { padding-right: 3.2rem; }

.topic-item.is-selected > .topic-row {
    background: var(--c-primary-light);
    border-color: var(--c-primary-border);
}

/*
    Pinned to the bottom of the viewport: a moderator scrolling a long list to
    pick the last few topics should not have to scroll back to act on them.
*/
.bulk-bar {
    position: sticky;
    bottom: 0;
    z-index: 20;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem 1rem;
    padding: 0.7rem 1rem;
    background: var(--c-surface);
    border: 1px solid var(--c-primary-border);
    border-radius: 0 0 var(--radius-card) var(--radius-card);
    box-shadow: var(--lift-hover, 0 6px 20px rgba(0, 0, 0, 0.1));
}

.bulk-bar[hidden] { display: none; }

.bulk-count {
    font-size: 0.86rem;
    font-weight: 600;
    white-space: nowrap;
}

.bulk-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
}

.bulk-sep {
    width: 1px;
    align-self: stretch;
    margin: 0 0.25rem;
    background: var(--c-border);
}

.bulk-forum {
    width: auto;
    max-width: 14rem;
    padding: 0.35rem 0.6rem;
    font-size: 0.85rem;
}

.bulk-actions button[disabled] { opacity: 0.45; cursor: not-allowed; }
.bulk-actions button[disabled]:hover { background: var(--c-surface); }

/* ---- Signatures --------------------------------------------------------- */

.post-signature {
    margin-top: 1rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--c-border);
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--c-text-muted);
}

.post-signature p { margin: 0 0 0.35rem; }
.post-signature p:last-child { margin-bottom: 0; }
.post-signature a { color: var(--c-primary); }

/* ---- Profile settings --------------------------------------------------- */

.avatar-editor {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 1.25rem;
}

.avatar-editor-actions {
    flex: 1;
    min-width: 15rem;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
}

.avatar-upload {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/* A file input is as wide as its own control; padding it out to 20rem just
   pushed the Upload button away from it. */
.avatar-upload input[type="file"] {
    width: auto;
    max-width: 100%;
    border: 0;
    padding: 0;
    background: none;
    font-size: 0.88rem;
}

.btn-quiet { align-self: flex-start; font-size: 0.85rem; padding: 0.35rem 0.75rem; }

.timezone-select { max-width: 24rem; }

.account-form .form-input { max-width: 22rem; }
.account-form .form-hint { max-width: 34rem; }

/* Beats the composer's 200px: a signature is two lines, and a box the size of
   a post invites one. */
.convoro-editor-content.signature-editor-content { min-height: 5rem; }

.signature-preview {
    margin-top: 1rem;
    padding: 0.75rem 1rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.8);
}

.signature-preview-label {
    display: block;
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--c-text-muted);
}

.signature-preview .post-signature { margin-top: 0.25rem; padding-top: 0; border-top: 0; }

.inline-form { display: inline; }

.link-button {
    background: none;
    border: 0;
    padding: 0;
    font: inherit;
    color: var(--c-primary);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 2px;
}

/* ---- Following ---------------------------------------------------------- */

.page-header-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
}

.follow-control { display: inline-flex; align-items: center; gap: 0.4rem; }

.follow-select {
    padding: 0.4rem 1.9rem 0.4rem 0.7rem;
    font-size: 0.85rem;
    font-weight: 500;
    color: var(--c-text);
    /*
       🚨 background-COLOR, not the `background` shorthand.
       The chevron on a select is drawn with background-image plus repeat,
       position and size — and a shorthand here resets all three to their
       initial values while leaving the image in place. The result is a
       select wallpapered in tiled chevrons, which is what this was.
    */
    background-color: var(--c-surface);
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius-pill);
    cursor: pointer;
}

.follow-select:hover { background-color: var(--c-hover); }

/*
    Following is the ordinary state for a thread somebody has replied to, so
    it is marked rather than shouted about.
*/
.follow-select:not([data-idle]) { border-color: var(--c-border-strong); }

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.follow-list { list-style: none; margin: 0; padding: 0; }

.follow-list-item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem 0.9rem;
    padding: 0.6rem 0;
    border-bottom: 1px solid var(--c-border);
}

.follow-list-item:last-child { border-bottom: 0; }
.follow-list-title { flex: 1; min-width: 12rem; font-weight: 500; }

.follow-list-mode {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--c-text-muted);
}

/* ---- Read state --------------------------------------------------------- */

/*
    An unread thread is marked, not shouted about: the title carries most of
    the signal and a row of loud badges makes a busy forum unreadable.
*/
.topic-row.is-unread .topic-title { font-weight: 700; }

/*
    A fixed gutter, present whether or not there is anything in it, so the
    rows line up instead of shuffling sideways as threads are read.
*/
.topic-unread {
    flex: 0 0 1.6rem;
    display: flex;
    align-items: center;
    justify-content: center;
    align-self: stretch;
}

.topic-unread::before {
    content: '';
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--c-primary);
}

.topic-unread.is-read::before { background: none; }

.topic-unread:hover::before { transform: scale(1.25); }
.topic-unread::before { transition: transform 0.12s ease; }

.forum-row.is-unread .forum-title { font-weight: 700; }

.read-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-bottom: 1rem;
}

.forum-unread-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 0.4rem;
    border-radius: 50%;
    background: var(--c-primary);
    vertical-align: 0.1em;
}

/* A stream spans forums, so the row says which one it is in. */
.topic-forum { color: var(--c-text-muted); font-weight: 600; }

/* ---- Activity streams --------------------------------------------------- */

.stream-nav {
    display: flex;
    flex-wrap: wrap;
    gap: 0.25rem;
    margin: -0.25rem 0 0.75rem;
    border-bottom: 1px solid var(--c-border);
}

.stream-nav-item {
    padding: 0.55rem 0.9rem;
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--c-text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    transition: color 0.15s ease, border-color 0.15s ease;
}

.stream-nav-item:hover { color: var(--c-text); text-decoration: none; }

.stream-nav-item.is-active {
    color: var(--c-primary);
    border-bottom-color: var(--c-primary);
}

.stream-hint {
    margin: 0 0 1.25rem;
    font-size: 0.88rem;
    color: var(--c-text-muted);
}

/*
    Marked, not alarming. A destructive area people arrive at deliberately
    should be recognisable without the whole page shouting.
*/
.card-danger { border-color: var(--c-danger-border, rgba(179, 38, 30, 0.35)); }
.card-danger .card-title { color: var(--c-danger, #b3261e); }

.consent-block {
    margin: 1rem 0 1.25rem;
    padding: 0.85rem 1rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.8);
    font-size: 0.88rem;
}

.consent-block .admin-checkbox-label { margin-bottom: 0.5rem; }
.consent-block .admin-checkbox-label:last-child { margin-bottom: 0; }

/* ---- Polls -------------------------------------------------------------- */

.poll {
    padding: 1.1rem 1.25rem;
    margin-bottom: 1rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
}

.poll.is-closed { background: var(--c-bg); }

.poll-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.5rem 1rem;
    margin-bottom: 0.9rem;
}

.poll-question { margin: 0; font-size: 1.05rem; font-weight: 700; }

.poll-meta {
    font-size: 0.78rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--c-text-muted);
    white-space: nowrap;
}

.poll-results { list-style: none; margin: 0 0 1rem; padding: 0; }
.poll-result + .poll-result { margin-top: 0.7rem; }

.poll-result-head {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    font-size: 0.9rem;
    margin-bottom: 0.25rem;
}

.poll-result.is-mine .poll-label { font-weight: 700; }
.poll-count { color: var(--c-text-muted); white-space: nowrap; }

/* A bar, because a column of numbers is not a result anybody reads. */
.poll-bar {
    height: 8px;
    background: var(--c-border);
    border-radius: var(--radius-pill);
    overflow: hidden;
}

.poll-bar span {
    display: block;
    height: 100%;
    background: var(--c-primary);
    border-radius: var(--radius-pill);
    transition: width 0.3s ease;
}

.poll-result.is-mine .poll-bar span { background: var(--c-primary); }

.poll-voters { margin-top: 0.3rem; font-size: 0.78rem; }
.poll-voter { color: var(--c-text-muted); }
.poll-voter::after { content: ','; }
.poll-voter:last-child::after { content: ''; }

.poll-hidden {
    margin: 0 0 1rem;
    font-size: 0.88rem;
    color: var(--c-text-muted);
    font-style: italic;
}

.poll-choices { list-style: none; margin: 0 0 0.85rem; padding: 0; }
.poll-choices li + li { margin-top: 0.35rem; }

.poll-choice {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.92rem;
    cursor: pointer;
}

.poll-public-note { display: inline-block; margin-left: 0.6rem; }

.poll-admin {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem;
    margin-top: 0.9rem;
    padding-top: 0.75rem;
    border-top: 1px solid var(--c-border);
    font-size: 0.85rem;
}

.poll-builder {
    margin-bottom: 1.25rem;
    padding: 0.75rem 1rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.8);
}

.poll-builder summary { cursor: pointer; font-weight: 600; font-size: 0.92rem; }
.poll-builder[open] summary { margin-bottom: 0.9rem; }
.poll-option-input + .poll-option-input { margin-top: 0.35rem; }

/* ---- Merging and splitting ---------------------------------------------- */

.mod-surgery {
    margin: -0.5rem 0 1rem;
    padding: 0.6rem 0.9rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.8);
    font-size: 0.88rem;
}

.mod-surgery summary { cursor: pointer; font-weight: 600; }
.mod-surgery[open] summary { margin-bottom: 0.75rem; }
.mod-surgery .form-input { max-width: 26rem; }
.mod-surgery .form-hint { max-width: 38rem; }

.post-list.is-selectable .post { position: relative; }

/*
   🚨 Sat on top of the post number.
   
   Both were pinned to the top-right corner, so the checkbox covered the "#2"
   that tells you which reply you are looking at — and the number is also the
   permalink, which the box was intercepting clicks on.

   It moves to the left edge of the post, beside the author column, where there
   is nothing to collide with. It only exists while a moderator is selecting
   posts to split, so it is out of everybody else's way entirely.
*/
.post-select {
    position: absolute;
    top: 0.6rem;
    left: 0.6rem;
    z-index: 2;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.9rem;
    height: 1.9rem;
    border-radius: 7px;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    cursor: pointer;
}

.post-select:hover { border-color: var(--c-primary); }

.post-select input {
    width: 16px;
    height: 16px;
    accent-color: var(--c-primary);
    cursor: pointer;
}

.post.is-selected { outline: 2px solid var(--c-primary); outline-offset: -2px; }

.split-bar {
    position: sticky;
    bottom: 0;
    z-index: 20;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.6rem;
    padding: 0.7rem 1rem;
    margin-top: 0.6rem;
    background: var(--c-surface);
    border: 1px solid var(--c-primary-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift-hover, 0 6px 20px rgba(0, 0, 0, 0.1));
}

.split-bar[hidden] { display: none; }
.split-count { font-size: 0.86rem; font-weight: 600; white-space: nowrap; }
.split-title { flex: 1; min-width: 14rem; max-width: 28rem; }

.field-required {
    font-size: 0.7rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--c-text-muted);
}

/* ---- Profile fields ----------------------------------------------------- */

.profile-fields {
    margin: 1rem 0 0;
    padding: 1rem 0 0;
    border-top: 1px solid var(--c-border);
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 0.35rem 1rem;
    font-size: 0.9rem;
}

.profile-fields dt { font-weight: 600; color: var(--c-text-muted); }
.profile-fields dd { margin: 0; }

/*
    Quieter than the post stats above them: these are read on every post of
    every thread, so they must not compete with what somebody wrote.
*/
.post-fields {
    margin: 0.6rem 0 0;
    padding-top: 0.6rem;
    border-top: 1px solid var(--c-border);
    font-size: 0.72rem;
    line-height: 1.4;
}

.post-fields dt {
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
}

.post-fields dd { margin: 0 0 0.35rem; color: var(--c-text); }
.post-fields dd:last-child { margin-bottom: 0; }

/* ---- Points, ranks and the leaderboard ---------------------------------- */

.rank-badge {
    display: inline-block;
    padding: 0.1em 0.5em;
    font-size: 0.68rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--rank, var(--c-text-muted));
    background: color-mix(in srgb, var(--rank, #888) 12%, transparent);
    border-radius: var(--radius-pill);
    white-space: nowrap;
}

.leaderboard { list-style: none; margin: 0; padding: 0; }

.leader {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    padding: 0.7rem 1rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    margin-bottom: 0.5rem;
}

.leader.is-top { border-color: var(--c-primary-border); }

.leader-place {
    min-width: 1.75rem;
    font-size: 1rem;
    font-weight: 700;
    color: var(--c-text-muted);
    text-align: right;
}

.leader.is-top .leader-place { color: var(--c-primary); }

.leader-who { flex: 1; min-width: 0; display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; }
.leader-name { font-weight: 600; }

.leader-points {
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    font-size: 1.05rem;
}

.post-points {
    display: flex;
    justify-content: space-between;
    gap: 0.5rem;
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
}

.post-rank { margin: 0.5rem 0 0.35rem; }

.profile-points {
    margin: 1rem 0 0;
    padding: 0.85rem 1rem;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: calc(var(--radius-card) * 0.8);
}

.profile-points-total {
    display: flex;
    align-items: baseline;
    gap: 0.45rem;
    font-size: 0.9rem;
    color: var(--c-text-muted);
}

.profile-points-total strong { font-size: 1.3rem; color: var(--c-text); }

.profile-points-why {
    list-style: none;
    margin: 0.6rem 0 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1.25rem;
    font-size: 0.82rem;
    color: var(--c-text-muted);
}

/* ---- Badges ------------------------------------------------------------- */

.badge-list {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin: 1rem 0 0;
    padding: 0;
}

.badge-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.6rem;
    font-size: 0.8rem;
    font-weight: 600;
    color: var(--badge, var(--c-text-muted));
    background: color-mix(in srgb, var(--badge, #888) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--badge, #888) 30%, transparent);
    border-radius: var(--radius-pill);
    cursor: default;
}

.badge-icon { font-size: 0.95em; line-height: 1; }

/* Beside posts: the glyph alone, because the sidebar is read on every post. */
.post-badges {
    list-style: none;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 0.25rem;
    margin: 0.5rem 0 0;
    padding: 0;
}

.badge-chip-sm { padding: 0.15rem 0.35rem; font-size: 0.9rem; }

/* ============================================================================
   Small screens
   ============================================================================

   Everything above is written for a screen with room. This is the layer that
   says what happens when there isn't any. It comes last on purpose: it is the
   authority, and it should not have to fight an ordering it cannot see.

   There are two widths in the whole stylesheet and both are here:

     640px  a phone. One column, the header collapses, controls go full width.
     900px  narrower than a laptop. Side columns fold under the main one.

   Reasoning is in docs/design/2026-08-05-mobile.md.
   ========================================================================= */

/* ---- The header menu ---------------------------------------------------- */

/*
   One set of markup in two shapes. On a wide screen the summary is hidden and
   the panel is given a `display` of its own — which overrides a closed
   <details> and leaves the ordinary horizontal bar. On a phone the element is
   left alone to be what it already is: a disclosure that opens, closes, takes
   Escape, and reads correctly aloud, with no script anywhere near it.
*/
.header-menu {
    display: flex;
    align-items: center;
    min-width: 0;
}

.header-menu > .header-nav {
    display: flex;
    align-items: center;
    gap: 0.15rem;
}

/*
   The checkbox is never seen and never removed from the page: `display: none`
   would take it out of the tab order, and it being reachable by keyboard is
   the whole reason the panel needs no script. Clipped to nothing instead, the
   way a skip link is.
*/
.header-menu-check {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    border: 0;
    clip-path: inset(50%);
    overflow: hidden;
    white-space: nowrap;
}

/* Focus has to land somewhere visible, and the checkbox is not visible — so
   it lands on the button beside it. */
.header-menu-check:focus-visible + .header-menu-button {
    outline: 2px solid rgba(255, 255, 255, 0.7);
    outline-offset: 2px;
}

.header-menu-button { display: none; }

.header-link {
    color: rgba(255, 255, 255, 0.7);
    font-size: 0.85rem;
}

.header-logout-form { display: inline-flex; }

.header-logout {
    background: rgba(255, 255, 255, 0.1);
    border: none;
    color: rgba(255, 255, 255, 0.8);
    cursor: pointer;
    padding: 0.35rem 0.75rem;
    border-radius: var(--radius-pill, 6px);
    font-size: 0.85rem;
}

.header-logout:hover { background: rgba(255, 255, 255, 0.2); color: #fff; }

/*
   The bell lives in the bar beside search at every width, not inside the
   panel. It was hidden above the phone breakpoint at first, which quietly took
   notifications off the desktop header altogether — the count is the whole
   point of the thing, and a count nobody can see is not a notification.
*/
.header-bell-standalone {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: relative;
    flex: none;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-pill, 999px);
    background: rgba(255, 255, 255, 0.08);
    color: rgba(255, 255, 255, 0.8);
}

.header-bell-standalone:hover {
    background: rgba(255, 255, 255, 0.18);
    color: #fff;
    text-decoration: none;
}

/* The scheme control keeps its shape everywhere: three icons, one lit. There
   is no labelled variant to hide, because the label was only ever needed to
   say which way the single button was about to flip. */

@media (max-width: 640px) {
    /*
       A closed <details> hides its children — unless a child has a `display`
       of its own, which overrides it. `.header-nav` has had `display: flex`
       since long before there was a panel, so the closed panel has to be
       closed explicitly. Without this it renders inside the header bar,
       invisible but tabbable, and a keyboard walks through eight links that
       are not on the screen.
    */
    .header-menu-check:not(:checked) ~ .header-menu { display: none; }

    .header-inner {
        gap: 0.35rem;
        /* The logo takes the space, the controls take what they need. */
        justify-content: flex-start;
    }

    /* The logo is the way home, so it is a target like any other. */
    .site-logo {
        display: inline-flex;
        align-items: center;
        min-height: 44px;
        margin-right: auto;
    }

    /* The bar shrinks on a phone, but not back to the old 56px. */
    .header-inner { height: 74px; }

    .header-bell-standalone { width: 44px; height: 44px; }

    .search-trigger {
        width: 44px;
        height: 44px;
        margin-left: 0;
        flex: none;
    }

    /* 🚨 The panel is the dropdown, not each nav inside it.

       Each .header-nav used to be `position: absolute; top: 100%` in its own
       right. That was correct when the header had one nav; when it became two —
       what the site is, and who you are — both were placed at the same
       coordinates and the session nav painted straight over the site nav. On a
       phone that looked like a menu with "Activity" half-buried under the
       Register button, which is what a real person reported.

       The mobile audit missed it because it measured `.header-nav` and found
       the first one, open and the right size. It was the second one on top of
       it that was the bug. */
    .header-menu {
        position: absolute;
        left: 0;
        right: 0;
        top: 100%;
        z-index: 200;
        flex-direction: column;
        align-items: stretch;
        padding: var(--s-2) 0;
        background: var(--c-header-bg);
        box-shadow: 0 12px 24px -8px rgba(0, 0, 0, 0.5);
        /* Long menus scroll rather than run off the bottom of the screen. */
        max-height: calc(100dvh - 56px);
        overflow-y: auto;
        overscroll-behavior: contain;
    }

    .header-menu-button {
        display: inline-flex;
        align-items: center;
        gap: 0.4rem;
        height: 44px;
        padding: 0 0.75rem 0 0.65rem;
        border-radius: var(--radius-pill, 999px);
        background: rgba(255, 255, 255, 0.08);
        color: rgba(255, 255, 255, 0.85);
        font-size: 0.85rem;
        font-weight: 600;
        cursor: pointer;
        list-style: none;
        user-select: none;
    }

    .header-menu-button:hover { background: rgba(255, 255, 255, 0.18); }

    .header-menu-button:focus-visible {
        outline: 2px solid rgba(255, 255, 255, 0.7);
        outline-offset: 2px;
    }

    /*
       Three bars drawn in one element. The middle is the element's own
       background; the outer two are its shadows. No extra markup, and it
       cannot get out of step with the label beside it.
    */
    .header-menu-bars {
        width: 16px;
        height: 2px;
        background: currentColor;
        border-radius: 2px;
        box-shadow: 0 -5px 0 currentColor, 0 5px 0 currentColor;
        transition: box-shadow 0.15s, background 0.15s, transform 0.15s;
    }

    /* Open, it becomes a cross — the same three bars, rearranged. */
    .header-menu-check:checked ~ .header-menu-button .header-menu-bars {
        background: transparent;
        box-shadow: 0 0 0 currentColor;
        transform: rotate(45deg);
    }

    .header-menu-check:checked ~ .header-menu-button .header-menu-bars::before,
    .header-menu-check:checked ~ .header-menu-button .header-menu-bars::after {
        content: '';
        position: absolute;
        width: 16px;
        height: 2px;
        background: currentColor;
        border-radius: 2px;
    }

    .header-menu-check:checked ~ .header-menu-button .header-menu-bars::after { transform: rotate(90deg); }

    .header-menu-bars { position: relative; }

    /*
       The panel hangs from the header across the full width. Positioned
       against the header rather than the button so it lines up with the bar
       above it whatever the button's own width turns out to be.
    */
    /* Ordinary stacked children of the panel. What the site is comes first,
       because that is what somebody opened the menu for; who you are follows. */
    .header-menu-check:checked ~ .header-menu > .header-nav {
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        width: 100%;
    }

    .header-menu-check:checked ~ .header-menu > .header-nav-primary { order: 1; }

    .header-menu-check:checked ~ .header-menu > .header-nav-session {
        order: 2;
        margin-top: var(--s-2);
        padding-top: var(--s-2);
        border-top: 1px solid rgba(255, 255, 255, 0.1);
    }

    /* Every row in the panel is a row: full width, and tall enough to hit. */
    .header-menu-check:checked ~ .header-menu > .header-nav a,
    .header-menu-check:checked ~ .header-menu > .header-nav .menu-link,
    .header-menu-check:checked ~ .header-menu > .header-nav button {
        display: flex;
        align-items: center;
        width: 100%;
        min-height: 48px;
        padding: 0 1.1rem;
        border-radius: 0;
        font-size: 1rem;
        text-align: left;
        justify-content: flex-start;
    }

    .header-menu-check:checked ~ .header-menu > .header-nav .header-user {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        /* A rule above the account rows, so the panel reads as two groups:
           where you can go, and who you are. */
        border-top: 1px solid rgba(255, 255, 255, 0.12);
        margin-top: 0.4rem;
        padding-top: 0.4rem;
    }

    .header-menu-check:checked ~ .header-menu .header-logout-form { display: block; width: 100%; }

    /* In the panel it centres on its own row rather than stretching: a
       segmented control that spans the sheet reads as three buttons, which is
       the opposite of what it is. */
    .header-menu-check:checked ~ .header-menu .scheme-switch {
        display: inline-flex;
        align-self: center;
        margin: 0.35rem auto;
        background: rgba(255, 255, 255, 0.1);
    }

    /* 🚨 "Every row in the panel is a row" above matches any button inside the
       nav, which is right for a logout and wrong for three segments of one
       control: they came out 48px tall, square-cornered and full width, so the
       lit one was a white block hanging out of its own pill. The switch is not
       a row, it is a thing on a row. */
    .header-menu-check:checked ~ .header-menu > .header-nav .scheme-switch-option {
        width: 34px;
        min-height: 34px;
        padding: 0;
        border-radius: var(--radius-pill);
        justify-content: center;
    }

    .header-menu-check:checked ~ .header-menu .header-logout {
        background: none;
        font-size: 1rem;
        font-weight: 500;
    }


    .header-menu-check:checked ~ .header-menu > .header-nav .btn-sm {
        /* Register keeps its colour: it is the one thing in the panel that is
           an action rather than a destination. */
        margin: 0.4rem 1.1rem;
        width: auto;
        justify-content: center;
        border-radius: var(--radius-pill, 999px);
    }

    /* The username is a row like the others rather than a bare word. */
    .header-menu-check:checked ~ .header-menu .header-username { font-weight: 600; }

    /*
       Submenus open on hover on a desktop, which a touch screen does not have.
       Inside the panel they are simply always shown, indented under
       their parent — a phone has vertical room and no hover.
    */
    .header-menu-check:checked ~ .header-menu .menu-parent { display: block; width: 100%; }

    .header-menu-check:checked ~ .header-menu .menu-dropdown {
        position: static;
        display: block;
        box-shadow: none;
        background: none;
        border: 0;
        padding: 0;
        min-width: 0;
        opacity: 1;
        visibility: visible;
        transform: none;
    }

    .header-menu-check:checked ~ .header-menu .menu-dropdown a { padding-left: 2.1rem; font-size: 0.95rem; }
}

/* ---- Anything you press is big enough to press ------------------------- */

/*
   44px, not WCAG's 24px: 24px is the size below which a target is a failure,
   which is a different question from the size a target ought to be.

   A `min-height` rather than padding on each control, so something added next
   year inherits it. Inline links inside prose are exempt — the rule is for
   controls, and blocking out every link in a sentence would wreck the
   sentence.
*/
@media (max-width: 640px) {
    .btn,
    .btn-sm,
    .btn-secondary,
    .btn-danger,
    button[type="submit"],
    input[type="submit"],
    .pagination a,
    .pagination span,
    .tab-link,
    .settings-nav a,
    .filter-link,
    .prefix-chip,
    .stream-tab,
    .period-link {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
        justify-content: center;
    }

    /* Text inputs get the same floor: a 30px field is a fiddly field, and
       under 16px iOS zooms the page on focus and never zooms back. */
    input[type="text"],
    input[type="email"],
    input[type="password"],
    input[type="url"],
    input[type="number"],
    input[type="search"],
    input[type="date"],
    select,
    textarea {
        min-height: 44px;
        font-size: 16px;
    }

    textarea { min-height: 7rem; }
}

/* ---- The row is the target --------------------------------------------- */

/*
   A 17px title link inside a 70px row means the row is mostly dead space, and
   on a phone the row is what the thumb is aiming at. The link is stretched
   over its row with a pseudo-element, so the whole row is pressable while the
   markup stays a heading containing a link — which is what it is, and what a
   screen reader should hear.

   Anything else in the row that is itself a link needs to sit above the
   stretched one, or it becomes unreachable.
*/
@media (max-width: 640px) {
    .topic-row,
    .forum-row,
    .leader-row,
    .stream-row,
    .activity-item,
    .search-result {
        position: relative;
    }

    .topic-title-link::after,
    .forum-name-link::after,
    .leader-name::after,
    .activity-topic::after,
    .search-result-title::after {
        content: '';
        position: absolute;
        inset: 0;
        z-index: 1;
    }

    /*
       Anything else in the row that is itself a link has to sit above the
       stretched one, or it becomes unreachable — the author's name in an
       activity line goes to their profile, and it should still go there.
    */
    .topic-row a:not(.topic-title-link),
    .topic-row button,
    .topic-row form,
    .forum-row a:not(.forum-name-link),
    .leader-row a:not(.leader-name),
    .activity-item a:not(.activity-topic),
    .search-result a:not(.search-result-title) {
        position: relative;
        z-index: 2;
    }
}

/* ---- One column -------------------------------------------------------- */

@media (max-width: 640px) {
    .container { padding-left: 0.85rem; padding-right: 0.85rem; }

    /* Page headings and their action button stack rather than fight. The
       stretch matters: with flex-start the controls shrink to their text and
       the `width: 100%` below resolves against nothing. */
    .page-header {
        flex-direction: column;
        align-items: stretch;
        gap: 0.75rem;
    }

    .page-header h2 { font-size: 1.5rem; }
    .page-header .btn { width: 100%; }

    /* Two-and-three-column grids become one. Written once here rather than
       per-feature, which is how there came to be seven breakpoints. */
    .form-row,
    .settings-grid,
    .profile-grid,
    .admin-grid { grid-template-columns: minmax(0, 1fr); }

    /* A table that will not fit scrolls inside itself rather than taking the
       page with it. */
    .table-wrap,
    .admin-table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

    /*
       🚨 …and nothing was ever wrapped in one.

       `.admin-table-wrap` was written and then used by none of the thirty-one
       admin templates that draw a table, so on a phone seven admin screens —
       Members, Modules, Themes, Downloads, the mod log, the dashboard — pushed
       the whole page sideways by up to 360px. The header went with it.

       Rather than editing thirty-one templates, the container scrolls: a card
       body or a card that *directly contains* a table, which is the shape every
       one of them has. `:has()` keeps it to those, so nothing else on the page
       gains a scrollbar it did not ask for.
    */
    .admin-card-body:has(> .admin-table),
    .admin-card:has(> .admin-table),
    .admin-content:has(> .admin-table),
    .panel:has(> .table),
    .panel-body:has(> .table) {
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
    }

    /*
       The admin header is a title and its buttons on one row. At 375px the
       buttons run off the side of a bar that is `position: sticky`, so the
       overflow follows you down the page.
    */
    .admin-header {
        flex-wrap: wrap;
        gap: var(--s-2, 0.5rem);
    }

    /*
       Full width, so the buttons drop to their own line instead of sitting
       beside the title and running off the end of it.
    */
    .admin-header-actions {
        flex-wrap: wrap;
        width: 100%;
        margin-left: 0;
    }

    .admin-header-actions form { flex-wrap: wrap; }

    /*
       🚨 `.admin-input.input-lg`, not `.input-lg`.

       The caps are declared further down this file, so an equally specific
       override up here would lose to them however correct it is — the last
       equal-specificity rule wins, media query or not. Two classes settles it.

       20rem is a comfortable field on a desktop and wider than the phone it is
       being drawn on once a button sits beside it. On a phone the field takes
       the room it has.
    */
    .admin-input.input-lg,
    .form-input.input-lg,
    .admin-input.input-sm,
    .form-input.input-sm,
    .admin-input.input-xs,
    .form-input.input-xs { max-width: 100%; }

    /* Long unbroken strings — URLs, tokens, a pasted stack trace — are the
       other way a page ends up wider than the screen. */
    .post-content,
    .post-body,
    .pm-bubble {
        overflow-wrap: anywhere;
    }

    .post-content pre,
    .post-body pre {
        overflow-x: auto;
        max-width: 100%;
    }

    /* Toolbars wrap instead of scrolling off the edge. */
    .topic-actions,
    .post-actions,
    .moderation-bar,
    .editor-toolbar {
        flex-wrap: wrap;
    }
}

/* ---- The rest of what the check found ---------------------------------- */

@media (max-width: 640px) {
    /*
       These are all the same shape of mistake: a control drawn as a line of
       small text, sized by its font rather than by the thumb aiming at it.
       Listed rather than caught by a blanket rule, because a blanket rule
       over every `a` would take the prose with it.
    */
    .breadcrumb a,
    .breadcrumb span,
    /* "Forgot your password?" is a link alone on its own line under a form —
       standalone, so a target, not prose. */
    .auth-links a,
    .widget-person,
    .stream-nav-item,
    .settings-nav a,
    .profile-tab,
    .toolbar-btn {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    /* The breadcrumb becomes a row of targets, so it needs room to be one. */
    .breadcrumb { flex-wrap: wrap; row-gap: 0; }

    /* The editor toolbar: square buttons, so width matters as much as height. */
    .toolbar-btn { min-width: 44px; justify-content: center; }

    /* A person chip in a sidebar widget is a link to a profile. */
    .widget-person { padding-top: 0.35rem; padding-bottom: 0.35rem; }
}

/* ---- The topic row on a phone ------------------------------------------ */

/*
   Wide, the row is one line: avatar, badges, title and author, replies and
   views, all beside each other. At 375px that same line gives the title about
   90px, and "Hello Convoro World" comes out as three words on three lines
   with the reply count squeezed against the edge.

   Narrow, it becomes a block: the avatar down the left, and everything else
   stacked beside it in the order it is read — badges, title, who and when,
   then the numbers.
*/
@media (max-width: 640px) {
    .topic-row {
        display: grid;
        grid-template-columns: auto minmax(0, 1fr);
        align-items: start;
        column-gap: 0.8rem;
        row-gap: 0;
        padding: 0.9rem 1rem;
    }

    /* The avatar runs down the side of everything stacked beside it. */
    .topic-row > .avatar,
    .topic-row > .avatar-md,
    .topic-row > .avatar-sm {
        grid-column: 1;
        grid-row: 1 / span 3;
    }

    .topic-row .topic-meta,
    .topic-row .topic-info,
    .topic-row .topic-stats {
        grid-column: 2;
        min-width: 0;
    }

    /* The badge strip is often empty, and an empty grid row must not leave a
       gap behind it — hence row-gap: 0 above and margins here instead. */
    .topic-row .topic-meta:not(:empty) { margin-bottom: 0.3rem; }

    .topic-row .topic-title { font-size: 1.02rem; line-height: 1.3; }

    /* The numbers go under the title as a quiet line rather than a column
       fighting it for width. */
    .topic-row .topic-stats {
        display: flex;
        flex-direction: row;
        align-items: baseline;
        gap: 0.75rem;
        margin-top: 0.35rem;
        text-align: left;
    }

    /* The selection gutter is the moderator's, and on a phone it is taking
       width from everybody. It keeps a target but stops keeping a column. */
    .topic-select { flex: 0 0 1.75rem; }

    .forum-row .forum-desc { font-size: 0.85rem; }
}

/* ---- Two corrections the screenshots found ----------------------------- */

@media (max-width: 640px) {
    /*
       Log out is a .btn-sm, so it was picking up the pill treatment meant for
       Register and sitting 1.1rem further in than every row above it. In the
       panel it is a row like the others; Register is the exception because it
       is an action rather than a destination.
    */
    .header-menu-check:checked ~ .header-menu > .header-nav .btn-sm.header-logout {
        margin: 0;
        border-radius: 0;
        width: 100%;
        justify-content: flex-start;
    }

    /*
       The moon and sun glyph is what the toggle *is* when it is a 44px button
       in the bar. In the panel the row says "Dark mode" in words, and the
       glyph only pushes that text out of line with every label above it.
    */

    /* The forum's follow control and its New Topic button are a pair, so they
       are the same width rather than one full and one not. */
    .page-header select,
    .page-header .follow-form,
    .page-header form { width: 100%; }
    .page-header select { max-width: none; }
}

/* ============================================================================
   Realtime
   ============================================================================

   Things arriving without a refresh. The rule the whole design turns on is
   that nothing moves under the person reading: replies wait behind a bar
   unless the reader is already at the bottom of the thread, and a list never
   reorders itself.

   Reasoning is in docs/design/2026-08-05-realtime.md.
   ========================================================================= */

/*
   The bar sits above the page rather than in it, so appearing does not push
   anything down — which would be the exact thing it exists to avoid.
*/
.realtime-bar {
    position: fixed;
    left: 50%;
    bottom: 1.5rem;
    transform: translateX(-50%);
    z-index: 300;

    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    min-height: 44px;
    padding: 0 1.25rem;

    border: 0;
    border-radius: var(--radius-pill, 999px);
    background: var(--c-primary);
    color: #fff;
    font-size: 0.92rem;
    font-weight: 600;
    font-family: inherit;
    cursor: pointer;

    box-shadow: 0 6px 24px -6px rgba(0, 0, 0, 0.45);
}

.realtime-bar:hover { background: var(--c-primary-hover); }

.realtime-bar:focus-visible {
    outline: 2px solid var(--c-primary);
    outline-offset: 3px;
}

.realtime-bar[hidden] { display: none; }

@media (max-width: 640px) {
    /* Above the thumb, not under it: the bottom of a phone screen belongs to
       the reply box and to the browser's own chrome. */
    .realtime-bar {
        left: 0.85rem;
        right: 0.85rem;
        bottom: 1rem;
        transform: none;
        justify-content: center;
    }
}

/*
   Something that has just arrived is marked for a moment and then stops being
   new. A tint rather than a movement — the point is to be findable, not to
   catch the eye of somebody reading something else.
*/
.is-arriving {
    animation: realtime-arrive 2s ease-out;
}

@keyframes realtime-arrive {
    from { background-color: color-mix(in srgb, var(--c-primary) 12%, transparent); }
    to { background-color: transparent; }
}

@media (prefers-reduced-motion: reduce) {
    /* No fade, but still marked: somebody who has asked for less movement has
       not asked to be unable to find the new reply. */
    .is-arriving {
        animation: none;
        box-shadow: inset 3px 0 0 var(--c-primary);
    }
}

/*
   Typing, as a pill in the bottom-left corner.

   Out of the reading path entirely. In the page it had to hold its height
   whether or not anybody was writing, or the composer jumped every time
   somebody started and stopped — which is the same sin as moving the page.
   Over the page it cannot reflow anything, it costs no layout, and it is
   visible whether you are at the composer or twenty replies up the thread.
*/
.typing-line {
    position: fixed;
    left: 1.1rem;
    bottom: 1.1rem;
    z-index: 250;

    display: inline-flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.4rem 0.9rem 0.4rem 0.45rem;

    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    box-shadow: var(--lift);

    font-size: 0.85rem;
    color: var(--c-text-muted);
    white-space: nowrap;
    max-width: calc(100vw - 2.2rem);

    /* Arrives from below rather than appearing, which reads as somebody
       leaning in rather than as the page changing. */
    animation: typing-in 0.22s cubic-bezier(0.2, 0.8, 0.3, 1);
}

.typing-line[hidden] { display: none; }

.typing-line .who { font-weight: 650; color: var(--c-text); }

/* The faces of whoever is writing, stacked when there is more than one. */
.typing-faces { display: inline-flex; flex: none; }

.typing-faces .avatar {
    width: 26px; height: 26px;
    border-radius: 50%;
    font-size: 0.7rem;
}

.typing-faces .avatar + .avatar {
    margin-left: -9px;
    box-shadow: 0 0 0 2px var(--c-surface);
}

.typing-dots { display: inline-flex; align-items: flex-end; gap: 3px; height: 12px; flex: none; }

.typing-dots i {
    width: 5px; height: 5px;
    border-radius: 50%;
    background: var(--c-text-muted);
    animation: typing-dance 1.25s ease-in-out infinite;
}

.typing-dots i:nth-child(2) { animation-delay: 0.16s; }
.typing-dots i:nth-child(3) { animation-delay: 0.32s; }

@keyframes typing-dance {
    0%, 65%, 100% { transform: translateY(0); opacity: 0.4; }
    32% { transform: translateY(-5px); opacity: 1; }
}

@keyframes typing-in {
    from { opacity: 0; transform: translateY(8px); }
    to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    /* Still alive, just not bouncing. */
    .typing-line { animation: none; }
    .typing-dots i { animation: typing-fade 1.6s ease-in-out infinite; }
    @keyframes typing-fade { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
}

@media (max-width: 640px) {
    /* Clear of the "new replies" bar, which owns the bottom of a phone. */
    .typing-line { bottom: 4.2rem; left: 0.85rem; }
}

/* ============================================================================
   Forums as cards
   ============================================================================

   The alternative to the row list, chosen per category. Same facts, laid out
   for browsing rather than for scanning.

   Reasoning is in docs/design/2026-08-05-forum-cards-and-covers.md.
   ========================================================================= */

.forum-grid {
    display: grid;
    /* Equal cards, as many as fit. No masonry and no varying sizes: those
       encode an importance the data does not have, and masonry stops the
       reading order matching the DOM order.

       340px, not 290: at the narrower width a two-line title was the common
       case rather than the exception, and a grid of wrapped titles reads as
       accidental. An index holds eight to twenty forums, so fewer and larger
       is also the honest shape. */
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 1.25rem;
}

.forum-card {
    display: flex;
    flex-direction: column;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    overflow: hidden;
    color: var(--c-text);
    text-decoration: none;
    transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}

/* No border-color here. The "hover, quietly" block further down this file
   resets it for rows and cards together, deliberately — an accent frame is
   louder than whatever the card points at. Setting it here only looked like
   it did something. */
.forum-card:hover {
    transform: translateY(-3px);
    box-shadow: var(--lift-hover);
    text-decoration: none;
}

/*
   🚨 The card was an <a> until 2026-08-10 and is now an <article>, because it
   carries two topic links and an anchor inside an anchor makes the parser
   close the outer one early. The title is a stretched link instead: it covers
   the card, so the whole tile is still one press.

   Anything else clickable inside a card needs `position: relative` and
   `z-index: 2`, or this pseudo-element sits on top of it and swallows the
   click. The topic links below do exactly that.
*/
.forum-card { position: relative; }

.forum-card-link::after {
    content: "";
    position: absolute;
    inset: 0;
    /* Below the topic links, above everything that is not a link. */
    z-index: 1;
}

/* The focus ring follows the link now, since that is what takes focus. */
.forum-card:has(.forum-card-link:focus-visible) {
    outline: 2px solid var(--c-primary);
    outline-offset: 2px;
}

.forum-card-link:focus-visible { outline: none; }

/*
   🚨 No fixed aspect ratio when there is a picture. The card takes its height
   from the image instead.

   Two wrong answers were tried first. `cover` fills the box and crops whatever
   does not fit — which slices the ends off a banner, and a forum banner is
   mostly lettering. `contain` keeps the whole picture and leaves bands of
   colour above and below it. Ernest saw both: "This image is still getting cut
   off. Can we not make the images automatically resize to fit the space?"

   Letting the cover be as tall as the image needs means no crop and no bands.
   Cards in a row still line up, because the row stretches to the tallest and
   the slack goes to the card body — which already takes it, so the foot stays
   on the bottom edge.
*/
.forum-card-cover {
    position: relative;
    display: block;
    background: var(--c-hover);
    overflow: hidden;
}

/*
   With no picture there is nothing to take a height from, so the generated
   tile keeps a shape of its own.

   A cover whose dimensions are known carries its own `aspect-ratio` inline —
   see the card partial — which reserves the exact space before the image
   loads. This is the fallback for one whose file went missing before the
   backfill could measure it.
*/
.forum-card-cover:not(:has(img)) {
    aspect-ratio: 16 / 9;
}

/*
   Fills the box, and crops whatever does not fit.
   
   🚨 Tried `contain` on 2026-08-08 and it was worse: an image that does not
   match the card's shape leaves bands of colour above and below it, and a grid
   of cards with different-shaped pictures in them looks broken rather than
   deliberate. Ernest, seeing it: "Can we not have the cover images just fill
   the space? That way there is no color above and below the images?"

   The answer to losing the wrong part of a picture is the focal point on the
   forum, which already exists and moves the crop, rather than refusing to crop
   at all.
*/
.forum-card-cover img {
    width: 100%;
    /* The whole picture, at the card's width. */
    height: auto;
    display: block;

    /*
       One guard: a portrait photograph uploaded as a cover would otherwise make
       a card several screens tall. Past this it is cropped, because at that
       point something has to give and a very tall card is worse than a crop.
    */
    max-height: 360px;
    object-fit: cover;

    /* Fills the reserved box exactly when the ratio is known, so the colour
       behind it is never visible around the edges. */
    height: 100%;
}

/* A scrim under the badge, so "New posts" is readable on a pale photograph.
   Only where there is a photograph: the generated placeholder below already
   has a known contrast against white and does not need darkening. */
.forum-card-cover:has(img)::after {
    content: '';
    position: absolute;
    inset: 0 0 auto 0;
    height: 4.5rem;
    background: linear-gradient(to bottom, rgba(15, 23, 42, 0.42), transparent);
    pointer-events: none;
}

/* No cover: the forum's initial on the colour its name generates. Stable, so
   a section is the same colour every visit and the grid stays recognisable —
   the same reasoning as avatar colours. */
.forum-card-mark {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 3.4rem;
    font-weight: 750;
    letter-spacing: -0.04em;
    color: rgba(255, 255, 255, 0.9);
}

.forum-card-new {
    position: absolute;
    top: 0.75rem;
    left: 0.75rem;
    /* Above the scrim, which is drawn on the cover's own ::after. */
    z-index: 1;
    padding: 0.2rem 0.65rem;
    border-radius: var(--radius-pill);
    background: var(--c-primary);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.03em;
}

.forum-card-body {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 1.15rem 1.25rem 1rem;
    /* Takes the slack, so the last-post foot sits on the bottom edge of every
       card in the row however long the descriptions are. */
    flex: 1;
}

.forum-card-title {
    font-size: 1.15rem;
    font-weight: 650;
    letter-spacing: -0.015em;
    line-height: 1.25;
}

.forum-card-desc {
    font-size: 0.875rem;
    line-height: 1.5;
    color: var(--c-text-muted);
    /* Two lines, then stop. A long description must not make one card in a row
       twice the height of its neighbours. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* Chips rather than two grey clauses. Unbordered, they read as a continuation
   of the description; bordered, they are obviously counts. */
.forum-card-counts {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: auto;
    padding-top: 0.75rem;
    font-size: 0.76rem;
    color: var(--c-text-muted);
    font-variant-numeric: tabular-nums;
}

.forum-card-counts > span {
    display: inline-flex;
    align-items: baseline;
    gap: 0.28rem;
    padding: 0.18rem 0.55rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    background: var(--c-bg);
    white-space: nowrap;
}

.forum-card-counts strong { color: var(--c-text); font-weight: 650; }

/*
   The foot holds two topics now, stacked, so it is a column of rows rather
   than one row. One topic said nothing about whether a forum was busy.
*/
.forum-card-foot {
    display: flex;
    flex-direction: column;
    gap: var(--s-2);
    padding: 0.85rem 1.25rem;
    border-top: 1px solid var(--c-border);
    background: var(--c-hover);
    min-width: 0;
}

/* One topic: avatar beside the two lines of text. */
.forum-card-recent {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    min-width: 0;
}

.forum-card-last { display: flex; flex-direction: column; min-width: 0; }

/*
   🚨 Above the stretched title link, or the card swallows the click and every
   topic here goes to the forum instead of to the topic.
*/
.forum-card-last .t {
    position: relative;
    z-index: 2;
}

.forum-card-last .t:hover { text-decoration: underline; }

.forum-card-last .t {
    font-size: 0.85rem;
    font-weight: 600;
    line-height: 1.3;
    /* One line: the foot is a footnote, not a second title. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.forum-card-last .w { font-size: 0.75rem; color: var(--c-text-muted); }

/* 🚨 A ring, not a border colour. `border-color` here lost to the "hover,
   quietly" block further down the file, so hovering an unread forum cleared
   the only thing marking it unread. A box-shadow ring is not in that fight,
   and it sits inside the radius because it is inset. */
.forum-card.is-unread {
    box-shadow: var(--lift), inset 0 0 0 1px var(--c-primary-border);
}

.forum-card.is-unread:hover {
    box-shadow: var(--lift-hover), inset 0 0 0 1px var(--c-primary-border);
}

@media (hover: hover) and (pointer: fine) {
    .forum-card-cover img { transition: transform 0.4s ease; }
    .forum-card:hover .forum-card-cover img { transform: scale(1.03); }
}

@media (prefers-reduced-motion: reduce) {
    .forum-card, .forum-card-cover img { transition: none; }
    .forum-card:hover { transform: none; }
    .forum-card:hover .forum-card-cover img { transform: none; }
}

/* Between one column and a comfortable two, 340px minimums would leave a
   single very wide card. Below the point where two fit, stop asking. */
@media (max-width: 780px) {
    .forum-grid { grid-template-columns: minmax(0, 1fr); gap: 1rem; }
}

@media (max-width: 640px) {
    /* The hover lift is meaningless on a touch screen, and a full-bleed cover
       at 16/9 is a lot of screen for a heading — pull it back a little. */
    .forum-grid { gap: 0.85rem; }
    .forum-card:hover { transform: none; }
    .forum-card-cover:not(:has(img)) { aspect-ratio: 16 / 7; }
    .forum-card-cover img { max-height: 260px; }
    .forum-card-body { padding: 1rem 1.05rem 0.9rem; }
    .forum-card-foot { padding: 0.75rem 1.05rem; }
}

/* ============================================================================
   The index, rebuilt
   ============================================================================

   What is happening, above what exists. The page used to open with a list of
   rooms all at the same weight, so the first thing a returning member read was
   furniture.

   Reasoning is in docs/design/2026-08-05-mobile.md's sibling, the design
   directions artifact, and in the forum cards note.
   ========================================================================= */

.pulse {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.35rem 1rem;
    padding: 0.7rem 1.2rem;
    margin-bottom: 1.5rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    box-shadow: var(--lift);
    font-size: 0.88rem;
    color: var(--c-text-muted);
}

.pulse-live {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    font-weight: 650;
    color: var(--c-success);
}

.pulse-live i {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: currentColor;
    animation: pulse-beat 2.4s ease-in-out infinite;
}

@keyframes pulse-beat { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

@media (prefers-reduced-motion: reduce) {
    .pulse-live i { animation: none; }
}

.pulse-fact strong { color: var(--c-text); font-weight: 650; font-variant-numeric: tabular-nums; }
.pulse-sep { opacity: 0.5; }

.pulse-link { color: var(--c-text-muted); }
.pulse-link:hover { color: var(--c-primary); text-decoration: none; }
.pulse-link:hover strong { color: var(--c-primary); }

.pulse-faces { display: flex; align-items: center; margin-left: auto; }

.pulse-faces .avatar {
    width: 25px;
    height: 25px;
    font-size: 0.66rem;
    margin-left: -7px;
    box-shadow: 0 0 0 2px var(--c-surface);
}

.pulse-faces .avatar:first-child { margin-left: 0; }

.pulse-more {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    height: 25px;
    padding: 0 0.45rem;
    margin-left: 0.3rem;
    border-radius: var(--radius-pill);
    background: var(--c-hover);
    color: var(--c-text-muted);
    font-size: 0.7rem;
    font-weight: 650;
    font-variant-numeric: tabular-nums;
}

/* ---- rows or cards, the reader's own switch ---------------------------- */

.view-switch {
    display: inline-flex;
    gap: 2px;
    padding: 3px;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    background: var(--c-surface);
}

.view-switch-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    height: 32px;
    padding: 0 0.75rem;
    border: 0;
    border-radius: var(--radius-pill);
    background: none;
    color: var(--c-text-muted);
    font-family: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    cursor: pointer;
}

.view-switch-btn:hover { background: var(--c-hover); color: var(--c-text); }

.view-switch-btn.is-on {
    background: var(--c-primary-light);
    color: var(--c-primary);
    box-shadow: inset 0 0 0 1px var(--c-primary-border);
}

.view-switch-btn:focus-visible { outline: 2px solid var(--c-primary); outline-offset: 1px; }

/* ---- categories are rules, not cards ----------------------------------- */

.category-header {
    display: flex;
    align-items: baseline;
    gap: 0.8rem;
    margin: 1.6rem 0 0.7rem;
    /* Whatever the old block gave it: a heading beside a hairline reads as
       grouping, and a panel reads as another thing to click. */
    background: none;
    border: 0;
    padding: 0;
}

.forum-category:first-of-type .category-header { margin-top: 0; }

.category-header h3 {
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* The rule that finishes the heading. */
.category-header::after {
    content: "";
    flex: 1;
    height: 1px;
    background: var(--c-border);
}

/* ---- the row --------------------------------------------------------- */

/*
   Unread belongs to the whole row, not to a dot beside the title: a stripe
   down the edge survives a fast scan, and a 6px dot does not.
*/
.forum-row { position: relative; overflow: hidden; }

.forum-row.is-unread::before {
    content: "";
    position: absolute;
    inset: 0 auto 0 0;
    width: 3px;
    background: var(--c-primary);
}

.forum-row.is-unread .forum-unread-dot { display: none; }

@media (max-width: 640px) {
    .view-switch { width: 100%; justify-content: center; }
    .view-switch-btn { flex: 1; justify-content: center; min-height: 40px; }
    .pulse { border-radius: var(--radius-card); }
    .pulse-faces { margin-left: 0; }
}

/* ============================================================================
   The thread, rebuilt
   ========================================================================= */

/*
   The gutter loses a centimetre it did not need. At 12.5rem the sidebar was
   routinely taller than a two-line reply, so every short post sat in a card
   with a hole in the right of it.
*/
/* 🚨 Guarded by the breakpoint, not stated bare.

   Bare, this sat a thousand lines below the `@media (max-width: 640px)` block
   that collapses the gutter into a strip — same specificity, later in the
   file, so it won at every width. On a phone the gutter stayed a 11.5rem
   column inside a 375px screen, which left the reading column about 90px wide
   and wrapped every username one letter per line, with the badges and the
   post counts piled on top of the avatar.

   It reached a real person before anybody here noticed. The mobile audit did
   not catch it because nothing overflowed and every tap target was big
   enough — a name rendering vertically is not a thing it knew to look at. */
@media (min-width: 641px) {
    .post { grid-template-columns: 11.5rem minmax(0, 1fr); }
    .post-sidebar { padding: 1.15rem 0.95rem; gap: 0.5rem; }
}

.post-author { font-size: 0.93rem; }

/*
   The post opens with a sentence rather than a bare time. The name is in the
   gutter as well, and that is deliberate: below the phone breakpoint the
   gutter becomes a strip and this line is what carries the identity.
*/
.post-header { gap: 0.5rem; justify-content: flex-start; flex-wrap: wrap; }

.post-header-name {
    color: var(--c-text);
    font-weight: 650;
    font-size: 0.88rem;
    letter-spacing: -0.01em;
}

.post-header-name:hover { color: var(--c-primary); text-decoration: none; }
.post-header-sep { color: var(--c-text-muted); opacity: 0.6; }
.post-when { font-size: 0.82rem; color: var(--c-text-muted); }

.role-badge-sm { font-size: 0.62rem; padding: 0.08rem 0.4rem; }

/* Pushed to the end of the line whatever else is in it. */
.post-header-right { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }

.post-number {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.74rem;
    padding: 0.1rem 0.42rem;
    border-radius: 6px;
    background: var(--c-hover);
    color: var(--c-text-muted);
}

.post-number:hover { color: var(--c-primary); text-decoration: none; }

@media (max-width: 640px) {
    /* The gutter is already a row above the post here; the identity in the
       header would then say the same thing twice, a centimetre apart. */
    .post-header-name, .post-header .role-badge { display: none; }
    .post-header-sep { display: none; }
}

/* ---- dropping onto the composer ---------------------------------------- */

/*
   The whole composer is the target, and it says what dropping will do. A
   dashed outline says "something is happening here" but not what — and on a
   box that already has a bordered editor inside it, one more border is not a
   message.
*/
.drop-overlay {
    position: absolute;
    inset: 0;
    z-index: 5;
    display: none;
    align-items: center;
    justify-content: center;
    border-radius: inherit;
    background: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface));
    border: 2px dashed var(--c-primary);
    pointer-events: none;
}

.drop-active .drop-overlay { display: flex; }

/* The old outline is replaced by the overlay, not joined by it. */
.drop-active { outline: none; }

.drop-overlay-face {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.4rem;
    color: var(--c-primary);
}

.drop-overlay-face strong { font-size: 0.95rem; font-weight: 650; }

.attachment-row.pending { align-items: center; }
.attachment-name { font-size: 0.85rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.attachment-progress {
    flex: 1;
    height: 4px;
    min-width: 60px;
    border-radius: var(--radius-pill);
    background: var(--c-border);
    overflow: hidden;
}

.attachment-progress-fill {
    display: block;
    width: 0;
    height: 100%;
    border-radius: inherit;
    background: var(--c-primary);
    transition: width 0.15s linear;
}

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

/* ============================================================================
   The profile, rebuilt
   ========================================================================= */

/* 128 exactly, because AvatarStore keeps 256px and 2x of 128 is 256. */
.avatar-xl { width: 128px; height: 128px; font-size: 3rem; }

.profile-card {
    /*
       The old card was a centred flex column with its own padding — every
       child shrank to its own content, so the cover came out 420px wide in an
       850px card with white either side of it. This one is a block with
       edge-to-edge parts and the padding moved inward, so `display` has to be
       said explicitly rather than inherited from the rule above.
    */
    display: block;
    align-items: initial;
    gap: 0;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    overflow: hidden;
    padding: 0;
    text-align: left;
}

.profile-cover {
    position: relative;
    height: 210px;
    background: var(--c-hover);
    overflow: hidden;
}

.profile-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% var(--focus, 50%);
    display: block;
    user-select: none;
    -webkit-user-drag: none;
}

/* A scrim only where the buttons sit, so the picture is not dimmed for
   nothing. */
.profile-cover::after {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 40%;
    background: linear-gradient(to bottom, rgba(6, 10, 16, 0.45), transparent);
    pointer-events: none;
}

.profile-cover.is-blank { height: 150px; }
.profile-cover.is-blank::after { display: none; }

.cover-tools {
    position: absolute;
    top: 0.75rem;
    right: 0.75rem;
    z-index: 2;
    display: flex;
    gap: 0.4rem;
}

/* A class-level display outranks the attribute, so this has to say so. */
.cover-tools[hidden],
.cover-hint[hidden] { display: none; }

.cover-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    height: 34px;
    padding: 0 0.8rem;
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: var(--radius-pill);
    background: rgba(10, 14, 20, 0.55);
    backdrop-filter: blur(8px);
    color: #fff;
    font-family: inherit;
    font-size: 0.79rem;
    font-weight: 600;
    cursor: pointer;
    white-space: nowrap;
}

.cover-btn:hover { background: rgba(10, 14, 20, 0.78); color: #fff; text-decoration: none; }
.cover-btn:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }
.cover-btn.is-primary { background: var(--c-primary); border-color: transparent; }
.cover-btn.is-primary:hover { background: var(--c-primary-hover); }

/* Repositioning: the image becomes the control. */
.profile-cover.is-editing { cursor: grab; touch-action: none; }
.profile-cover.is-dragging { cursor: grabbing; }

.profile-cover.is-editing::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    box-shadow: inset 0 0 0 2px var(--c-primary);
    /* Guides at the thirds, so "level" is something you can see rather than
       guess at. */
    background:
        linear-gradient(to bottom, transparent calc(33.33% - 1px), rgba(255,255,255,0.34) calc(33.33% - 1px), rgba(255,255,255,0.34) 33.33%, transparent 33.33%),
        linear-gradient(to bottom, transparent calc(66.66% - 1px), rgba(255,255,255,0.34) calc(66.66% - 1px), rgba(255,255,255,0.34) 66.66%, transparent 66.66%);
    pointer-events: none;
}

.cover-hint {
    position: absolute;
    left: 50%;
    bottom: 0.85rem;
    transform: translateX(-50%);
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.4rem 0.9rem;
    border-radius: var(--radius-pill);
    background: rgba(10, 14, 20, 0.72);
    backdrop-filter: blur(8px);
    color: #fff;
    font-size: 0.79rem;
    white-space: nowrap;
}

.cover-hint b { font-variant-numeric: tabular-nums; }
.cover-hint.is-bad { background: var(--c-danger); }

/* ---- who this is ------------------------------------------------------- */

.profile-identity {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: end;
    gap: 1.1rem;
    padding: 0.9rem 1.4rem 1.1rem;
}

.profile-identity .avatar {
    /*
       The avatar alone rises into the cover. A negative margin on the row
       would take the name up there with it, against the scrim.
    */
    margin-top: -62px;
    margin-bottom: -4px;
    border: 4px solid var(--c-surface);
    box-shadow: var(--lift);

    /*
       Positioned, and above. The cover is `position: relative` for its own
       buttons, which makes it a stacking context that paints over a static
       sibling — so the half of the avatar that overlapped it was drawn and
       then covered up, and the avatar looked cropped square along the cover's
       bottom edge.
    */
    position: relative;
    z-index: 2;
}

.profile-names { padding-bottom: 0.2rem; min-width: 0; }

.profile-names h2 {
    font-size: 1.6rem;
    font-weight: 750;
    letter-spacing: -0.03em;
    line-height: 1.15;
}

.profile-sub {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.45rem;
    margin-top: 0.35rem;
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

.profile-title { font-style: italic; }

.profile-acts { display: flex; gap: 0.45rem; padding-bottom: 0.25rem; flex-wrap: wrap; }

.profile-strip {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    border-top: 1px solid var(--c-border);
}

.profile-strip div { padding: 0.85rem 1.4rem; }
.profile-strip div + div { border-left: 1px solid var(--c-border); }

.profile-strip b {
    display: block;
    font-size: 1.15rem;
    font-weight: 750;
    letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
}

.profile-strip span {
    font-size: 0.63rem;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* ---- what is under it -------------------------------------------------- */

.profile-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 22rem;
    gap: 1.6rem;
    align-items: start;
    margin-top: 1.4rem;
}

.profile-main, .profile-aside { display: flex; flex-direction: column; gap: 1rem; }

.panel {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    padding: 1.1rem 1.2rem;
}

.panel-label {
    display: block;
    margin-bottom: 0.7rem;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.profile-fields { display: flex; flex-direction: column; gap: 0.55rem; }

.profile-fields > div {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.8rem;
}

.profile-fields dt {
    font-size: 0.64rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--c-text-muted);
    white-space: nowrap;
}

.profile-fields dd { font-size: 0.87rem; font-weight: 600; text-align: right; min-width: 0; overflow-wrap: anywhere; }

@media (max-width: 900px) {
    .profile-body { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 640px) {
    .profile-cover { height: 150px; }
    .profile-identity { grid-template-columns: minmax(0, 1fr); gap: 0.7rem; }
    .profile-identity .avatar { width: 84px; height: 84px; font-size: 2rem; margin-top: -50px; }
    .profile-acts { width: 100%; }
    .profile-acts .btn { flex: 1; justify-content: center; }
    .profile-strip { grid-template-columns: repeat(2, 1fr); }
    .profile-strip div:nth-child(3) { border-left: 0; }
    .profile-strip div:nth-child(n+3) { border-top: 1px solid var(--c-border); }
}

/* ============================================================================
   Drop zones
   ============================================================================

   Shared with the admin panel — the same component, so a cover uploads the
   same way whoever is uploading it.

   A label wrapped round a file input. The input is the control and is never
   removed — clicking the zone opens the picker because that is what a label
   does, tabbing reaches it, and a screen reader announces it as a file field.
   Drag, drop and paste are additions on top.
   ========================================================================= */

.drop-zone {
    position: relative;
    display: block;
    min-height: 150px;
    padding: 1.25rem;
    border: 2px dashed var(--c-border-strong, #d1d5db);
    border-radius: 12px;
    background: var(--c-hover, #f3f4f6);
    cursor: pointer;
    overflow: hidden;
    transition: border-color 0.15s, background 0.15s;
}

.drop-zone:hover { border-color: var(--c-primary, #1d74b8); }

/* The input keeps its size and its place in the tab order; it is only
   invisible. `display: none` would take it out of the tab order and out of the
   accessibility tree, which is the whole thing this is trying to preserve. */
.drop-input {
    position: absolute;
    inset: 0;
    opacity: 0;
    cursor: pointer;
}

.drop-zone:focus-within {
    border-color: var(--c-primary, #1d74b8);
    box-shadow: 0 0 0 3px var(--c-primary-light, rgba(29, 116, 184, 0.18));
}

.drop-zone.is-over {
    border-color: var(--c-primary, #1d74b8);
    border-style: solid;
    background: var(--c-primary-light, rgba(29, 116, 184, 0.08));
}

.drop-zone.is-bad { border-color: var(--c-danger, #dc2626); }

.drop-face {
    position: relative;
    z-index: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.3rem;
    min-height: 118px;
    text-align: center;
    color: var(--c-text-muted, #6b7280);
    pointer-events: none;
}

.drop-face strong { color: var(--c-text, #1a1a2e); font-size: 0.95rem; font-weight: 650; }
.drop-sub { font-size: 0.8rem; }

.drop-preview {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: 50% var(--focus, 50%);
}

/* With a picture behind it the prompt needs its own ground to be read on. */
.drop-zone.has-file .drop-face,
.drop-zone:has(.drop-preview:not([hidden])) .drop-face {
    background: rgba(8, 12, 18, 0.55);
    backdrop-filter: blur(3px);
    border-radius: 10px;
    padding: 0.7rem 1rem;
    min-height: 0;
    margin: auto;
    max-width: max-content;
    color: rgba(255, 255, 255, 0.85);
}

.drop-zone.has-file .drop-face strong,
.drop-zone:has(.drop-preview:not([hidden])) .drop-face strong { color: #fff; }

/* ---- the profile's tabs and timeline ----------------------------------- */

.profile-tabs {
    display: flex;
    gap: 0.2rem;
    padding: 0 1rem;
    border-top: 1px solid var(--c-border);
    overflow-x: auto;
    /*
     * 🚨 `overflow-y: hidden` is not tidying up — it is the fix for a visible
     * bug, and it must not be removed as redundant.
     *
     * Per spec, when one axis is not `visible` the other computes to `auto`.
     * So `overflow-x: auto` alone quietly made this scrollable vertically too,
     * and the `margin-bottom: -1px` on the tabs below overflows by exactly one
     * pixel — enough for a full vertical scrollbar to appear at the right-hand
     * end of the row. It was reported as "a white spot with arrows" on
     * profiles, which is precisely what a scrollbar looks like when you were
     * not expecting one.
     */
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
}

.profile-tabs a {
    padding: 0.75rem 0.85rem;
    font-size: 0.88rem;
    font-weight: 600;
    color: var(--c-text-muted);
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
}

.profile-tabs a:hover { color: var(--c-text); text-decoration: none; }
.profile-tabs a.is-on { color: var(--c-primary); border-bottom-color: var(--c-primary); }

.timeline { display: flex; flex-direction: column; }

.timeline-event {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 0.9rem;
    padding: 0.8rem 0;
    border-bottom: 1px solid var(--c-border);
}

.timeline-event:last-child { border-bottom: 0; padding-bottom: 0; }
.timeline-event:first-child { padding-top: 0; }

/*
   A rail with a node, not the same avatar six times: every event on a profile
   is by the person whose profile it is, so the face is not news. The node's
   colour is the only thing carrying which kind of event it was.
*/
.timeline-node {
    position: relative;
    width: 9px;
    display: flex;
    justify-content: center;
    padding-top: 0.4rem;
}

.timeline-node i {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--c-surface);
    box-shadow: 0 0 0 2px var(--c-border-strong);
    z-index: 1;
}

.timeline-node::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: -0.9rem;
    width: 1px;
    background: var(--c-border);
}

.timeline-event:first-child .timeline-node::before { top: 0.4rem; }
.timeline-event:last-child .timeline-node::before { display: none; }

/* Starting a thread is a different thing from answering in one. */
.timeline-event.is-topic .timeline-node i { box-shadow: 0 0 0 2px var(--c-primary); }

.timeline-verb {
    font-size: 0.65rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.timeline-title {
    display: block;
    margin-top: 0.12rem;
    font-size: 0.98rem;
    font-weight: 650;
    letter-spacing: -0.01em;
    line-height: 1.3;
    color: var(--c-text);
}

.timeline-title:hover { color: var(--c-primary); text-decoration: none; }

.timeline-when { margin-top: 0.15rem; font-size: 0.77rem; color: var(--c-text-muted); }
.timeline-when a { color: inherit; }
.timeline-when a:hover { color: var(--c-primary); }

.cv-quiet { color: var(--c-text-muted); font-size: 0.9rem; }

/* ============================================================================
   Selects
   ============================================================================

   One treatment for every drop-down on the site.

   They were left native. On a light page that reads as plain and slightly
   unfinished; on a dark one it is a white box with black text and the
   platform's own stepper arrows sitting in the middle of a dark page, which
   looks like a bug because it is one.
   ========================================================================= */

/*
   `select` on its own is one type selector, which any single class outranks.
   Doubling it costs nothing, changes no behaviour, and means a `.something`
   rule written next year cannot silently take the chevron's arrangement with
   it — the failure this exists to prevent looked like a rendering bug and was
   a cascade one.
*/
select,
select:where(:not(#\9)):where(:not(#\9)),
.form-group select,
.form-select,
.admin-input[size],
select.admin-input {
    appearance: none;
    -webkit-appearance: none;

    padding: 0.55rem 2.3rem 0.55rem 0.85rem;
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius, 8px);
    background-color: var(--c-surface);
    color: var(--c-text);
    font-family: inherit;
    font-size: 0.92rem;
    line-height: 1.4;
    cursor: pointer;

    /* One chevron, not the platform's two arrows: this opens a list, it does
       not step through values. */
    background-image: var(--select-chevron);
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 14px;

    transition: border-color 0.15s, box-shadow 0.15s;
}

select:hover { border-color: var(--c-primary-border); }

select:focus,
select:focus-visible {
    outline: none;
    border-color: var(--c-primary);
    box-shadow: 0 0 0 3px var(--c-primary-light);
}

select:disabled {
    opacity: 0.6;
    cursor: not-allowed;
    background-color: var(--c-hover);
}

/*
   The open list is drawn by the operating system, and on Windows and Linux it
   takes these two properties from the element rather than from the page. Left
   alone, a dark site opens a white menu.
*/
select option {
    background: var(--c-surface);
    color: var(--c-text);
}

/* A multi-select is a list, not a drop-down: no chevron, no pointer. */
select[multiple],
select[size]:not([size="1"]) {
    background-image: none;
    padding-right: 0.85rem;
    cursor: default;
}

/* Full-width where it sits in a stacked form; its own width in a toolbar. */
.form-group select { width: 100%; }

@media (max-width: 640px) {
    /* 16px, or iOS zooms the page on focus and never zooms back. */
    select { font-size: 16px; min-height: 44px; }
}

/* ============================================================================
   Error pages
   ============================================================================

   The one screen nobody designs and everybody eventually reads.

   Reasoning is in docs/design/2026-08-05-error-pages.md.
   ========================================================================= */

.error-page {
    max-width: 34rem;
    margin: clamp(2rem, 8vw, 5rem) auto;
    padding: 2.5rem 2rem;
    text-align: center;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
}

.error-code {
    display: inline-block;
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--c-primary);
}

.error-page h2 {
    margin: 0.5rem 0 0.65rem;
    font-size: 1.6rem;
    font-weight: 750;
    letter-spacing: -0.028em;
    line-height: 1.2;
    text-wrap: balance;
}

.error-body {
    color: var(--c-text-muted);
    /* Comfortable reading measure, centred — this is the one paragraph on the
       page and it should not run the full width of the card. */
    max-width: 28rem;
    margin: 0 auto;
}

.error-search {
    display: flex;
    gap: 0.5rem;
    margin: 1.6rem auto 0;
    max-width: 26rem;
}

.error-search input {
    flex: 1;
    min-width: 0;
    min-height: 44px;
    padding: 0 0.9rem;
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius-pill);
    background: var(--c-bg);
    color: var(--c-text);
    font-family: inherit;
    font-size: 0.95rem;
}

.error-search input:focus {
    outline: none;
    border-color: var(--c-primary);
    box-shadow: 0 0 0 3px var(--c-primary-light);
}

.error-search .btn { border-radius: var(--radius-pill); }

.error-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.55rem;
    justify-content: center;
    margin-top: 1.6rem;
}

.error-reference {
    margin-top: 1.6rem;
    padding-top: 1.2rem;
    border-top: 1px solid var(--c-border);
    font-size: 0.82rem;
    color: var(--c-text-muted);
}

.error-reference code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    padding: 0.1rem 0.4rem;
    border-radius: 5px;
    background: var(--c-hover);
    color: var(--c-text);
}

@media (max-width: 640px) {
    .error-page { padding: 2rem 1.25rem; }
    .error-search { flex-direction: column; }
    .error-actions .btn { width: 100%; justify-content: center; }
}


/* ============================================================================
   The account menu
   ============================================================================

   Everything about you, in one place. There was previously no way to reach
   your own settings from anywhere on the site: a member could not change their
   password without knowing the URL.

   Opens on hover and on focus-within, like the admin-managed menus beside it —
   no script, so it works before anything has loaded, and tabbing into it
   reveals it without a roving-tabindex implementation to get wrong.
   ========================================================================= */

.account-menu { position: relative; display: inline-flex; align-items: center; }

.account-trigger {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.25rem 0.55rem 0.25rem 0.3rem;
    border-radius: var(--radius-pill);
    color: rgba(255, 255, 255, 0.85);
}

.account-trigger:hover,
.account-menu:focus-within .account-trigger {
    background: rgba(255, 255, 255, 0.12);
    color: #fff;
    text-decoration: none;
}

.account-trigger .avatar { width: 26px; height: 26px; font-size: 0.68rem; }
.account-chevron { opacity: 0.7; transition: transform 0.15s; }
.account-menu:hover .account-chevron { transform: rotate(180deg); }

.account-dropdown {
    position: absolute;
    top: calc(100% + 0.4rem);
    right: 0;
    z-index: 200;

    display: flex;
    flex-direction: column;
    min-width: 13.5rem;
    padding: 0.35rem;

    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift-hover);

    /* Hidden without being gone: visibility keeps it out of the tab order
       while opacity gives it something to animate, and a `display: none`
       cannot transition. */
    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
}

.account-menu:hover .account-dropdown,
.account-menu:focus-within .account-dropdown {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.account-dropdown a,
.account-dropdown .header-logout {
    display: flex;
    align-items: center;
    width: 100%;
    min-height: 38px;
    padding: 0 0.7rem;
    border: 0;
    border-radius: calc(var(--radius, 8px) * 0.8);
    background: none;
    color: var(--c-text);
    font-family: inherit;
    font-size: 0.88rem;
    font-weight: 500;
    text-align: left;
    cursor: pointer;
}

.account-dropdown a:hover,
.account-dropdown .header-logout:hover {
    background: var(--c-hover);
    color: var(--c-primary);
    text-decoration: none;
}

.account-dropdown .header-logout-form { width: 100%; }

.account-rule {
    display: block;
    height: 1px;
    margin: 0.3rem 0.4rem;
    background: var(--c-border);
}

@media (max-width: 640px) {
    /*
       Inside the phone panel there is nothing to hover and nowhere to hang a
       dropdown, so the menu flattens into the rows it is made of.
    */
    .header-menu-check:checked ~ .header-menu .account-menu { display: block; width: 100%; }
    .header-menu-check:checked ~ .header-menu .account-trigger { display: none; }

    .header-menu-check:checked ~ .header-menu .account-dropdown {
        position: static;
        opacity: 1;
        visibility: visible;
        transform: none;
        min-width: 0;
        padding: 0;
        border: 0;
        background: none;
        box-shadow: none;
    }

    .header-menu-check:checked ~ .header-menu .account-dropdown a,
    .header-menu-check:checked ~ .header-menu .account-dropdown .header-logout {
        min-height: 48px;
        padding: 0 1.1rem;
        border-radius: 0;
        font-size: 1rem;
        color: rgba(255, 255, 255, 0.85);
    }

    .header-menu-check:checked ~ .header-menu .account-rule {
        margin: 0.4rem 0;
        background: rgba(255, 255, 255, 0.12);
    }
}


/* ---- hover, quietly ----------------------------------------------------- */

/*
   A row lifts and lightens on hover. It does not draw a coloured frame round
   itself: an accent border on a card is louder than whatever the card is
   pointing at, and on an index of eight forums it turns pointing at one into
   an event.
*/
.forum-row:hover,
.forum-card:hover,
.topic-row:hover {
    border-color: var(--c-border);
}

/* ---- dropdowns, opened by pressing them --------------------------------- */

.menu-parent > .menu-link-parent,
.account-trigger {
    /* A real button now, so it needs saying that it is not a form control. */
    border: 0;
    background: none;
    font: inherit;
    cursor: pointer;
}

.menu-chevron, .account-chevron {
    margin-left: 0.25rem;
    opacity: 0.65;
    transition: transform 0.15s;
}

.menu-parent.is-open .menu-chevron,
.menu-parent.is-open .account-chevron { transform: rotate(180deg); }

/*
   Hover no longer opens anything. `:focus-within` still does, so tabbing to a
   parent reveals its children before any script has loaded.
*/
.menu-parent:hover > .menu-submenu { opacity: 0; visibility: hidden; }

.menu-parent.is-open > .menu-submenu,
.menu-parent:focus-within > .menu-submenu {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.account-menu:hover .account-dropdown { opacity: 0; visibility: hidden; transform: translateY(-4px); }

.account-menu.is-open .account-dropdown,
.account-menu:focus-within .account-dropdown {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.account-menu:hover .account-chevron { transform: none; }
.account-menu.is-open .account-chevron { transform: rotate(180deg); }

/* The bell is an outline now, so it needs a colour rather than a glyph size. */
.bell-icon { display: block; }

/* ---- a parent that is both a destination and a menu --------------------- */

/*
   The label goes where it says; the chevron beside it opens what is under it.
   Making the whole parent a button takes its destination away — nest Unread
   and Activity under Forums and "Forums" stops going to the forums.
*/
.menu-parent { display: inline-flex; align-items: center; gap: 0; }

.menu-parent > .menu-link-parent { padding-right: 0.4rem; }

.menu-chevron-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 30px;
    padding: 0;
    border: 0;
    border-radius: var(--radius-pill);
    background: none;
    color: rgba(255, 255, 255, 0.7);
    cursor: pointer;
}

.menu-chevron-btn:hover { color: #fff; background: rgba(255, 255, 255, 0.1); }

.menu-chevron-btn:focus-visible {
    outline: 2px solid rgba(255, 255, 255, 0.7);
    outline-offset: 1px;
}

.menu-parent.is-open .menu-chevron-btn { color: #fff; background: rgba(255, 255, 255, 0.12); }

@media (max-width: 640px) {
    /*
       In the phone panel there is nothing to hover and nowhere to hang a
       submenu, so children sit indented under their parent and the chevron
       has nothing left to do.
    */
    .header-menu-check:checked ~ .header-menu .menu-chevron-btn { display: none; }
    .header-menu-check:checked ~ .header-menu .menu-parent > .menu-link-parent { padding-right: 1.1rem; }
}


/* ---- submenu links, on the panel rather than on the header -------------- */

/*
   🚨 `.header-nav a` sets white-on-dark for the bar, and it outranks
   `.menu-sublink` — so a submenu, whose panel is a light surface, rendered
   white text on white and looked like an empty box. The panel's own links
   have to say so with at least the same specificity.
*/
.header-nav .menu-submenu a,
.header-nav .menu-sublink {
    display: block;
    width: 100%;
    padding: 0.5rem 0.65rem;
    border-radius: calc(var(--radius, 8px) * 0.7);
    color: var(--c-text);
    font-size: 0.88rem;
    font-weight: 500;
    white-space: nowrap;
}

.header-nav .menu-submenu a:hover,
.header-nav .menu-sublink:hover {
    background: var(--c-hover);
    color: var(--c-primary);
    text-decoration: none;
}

@media (max-width: 640px) {
    /* Inside the phone panel the submenu is part of the dark sheet again,
       indented under its parent rather than floating over anything. */
    .header-menu-check:checked ~ .header-menu .menu-submenu a,
    .header-menu-check:checked ~ .header-menu .menu-sublink {
        padding-left: 2.1rem;
        border-radius: 0;
        color: rgba(255, 255, 255, 0.78);
        font-size: 0.95rem;
    }

    .header-menu-check:checked ~ .header-menu .menu-submenu a:hover,
    .header-menu-check:checked ~ .header-menu .menu-sublink:hover {
        background: rgba(255, 255, 255, 0.08);
        color: #fff;
    }
}

/* ============================================================================
   The notifications panel
   ============================================================================

   Being told you have three and then being taken to a page to find out what
   they are is two steps for something that is one.
   ========================================================================= */

.notif-menu { position: relative; display: inline-flex; }

.header-bell-standalone { border: 0; cursor: pointer; font: inherit; }

.notif-panel {
    position: absolute;
    top: calc(100% + 0.5rem);
    right: 0;
    z-index: 200;

    display: flex;
    flex-direction: column;
    width: 23rem;
    max-width: calc(100vw - 1.7rem);

    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift-hover);
    overflow: hidden;

    opacity: 0;
    visibility: hidden;
    transform: translateY(-4px);
    transition: opacity 0.15s, transform 0.15s, visibility 0.15s;
}

.notif-menu.is-open .notif-panel,
.notif-menu:focus-within .notif-panel {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
}

.notif-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.7rem 0.9rem;
    border-bottom: 1px solid var(--c-border);
}

.notif-title {
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.notif-clear {
    border: 0;
    background: none;
    font-family: inherit;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--c-text-muted);
    cursor: pointer;
    padding: 0.2rem 0.3rem;
    border-radius: 5px;
}

.notif-clear:hover { color: var(--c-danger); background: var(--c-hover); }

/* Eight rows fit; past that it scrolls rather than running off the screen. */
.notif-scroll { max-height: 24rem; overflow-y: auto; overscroll-behavior: contain; }

/*
   A row is a link with a button beside it, not a button inside a link — an
   anchor cannot contain one, and nesting it is how a row swallows the press
   meant for the cross.
*/
.notif-row {
    display: flex;
    align-items: stretch;
    border-bottom: 1px solid var(--c-border);
    transition: opacity 0.15s;
}

.notif-row:last-child { border-bottom: 0; }
.notif-row:hover { background: var(--c-hover); }
.notif-row.is-going { opacity: 0.35; pointer-events: none; }

.notif-link {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    flex: 1;
    min-width: 0;
    padding: 0.7rem 0.4rem 0.7rem 0.9rem;
    color: var(--c-text);
}

.notif-link:hover { text-decoration: none; }

.notif-body { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }

.notif-text {
    font-size: 0.85rem;
    line-height: 1.4;
    color: var(--c-text-muted);
    /* Two lines of a long topic title, then stop. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.notif-text strong { color: var(--c-text); font-weight: 650; }
.notif-when { font-size: 0.73rem; color: var(--c-text-muted); }

/* Unread carries a stripe rather than a dot: it belongs to the whole row. */
.notif-row.is-unread { box-shadow: inset 3px 0 0 var(--c-primary); }
.notif-row.is-unread .notif-text strong:first-child { font-weight: 700; }

.notif-remove {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    flex: none;
    border: 0;
    background: none;
    color: var(--c-text-muted);
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.12s, color 0.12s;
}

/* Shown on hover on a pointer, always on a touch screen — where there is no
   hover to reveal it with. */
.notif-row:hover .notif-remove,
.notif-remove:focus-visible { opacity: 1; }

@media (hover: none) {
    .notif-remove { opacity: 1; }
}

.notif-remove:hover { color: var(--c-danger); }

.notif-empty {
    padding: 1.4rem 1rem;
    text-align: center;
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

.notif-all {
    display: block;
    padding: 0.65rem;
    text-align: center;
    border-top: 1px solid var(--c-border);
    background: var(--c-hover);
    font-size: 0.83rem;
    font-weight: 600;
    color: var(--c-primary);
}

.notif-all:hover { text-decoration: none; background: var(--c-primary-light); }

/* ---- an icon link in the bar -------------------------------------------- */

.header-icon-link {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.08);
    color: rgba(255, 255, 255, 0.8);
}

.header-icon-link:hover { background: rgba(255, 255, 255, 0.18); color: #fff; text-decoration: none; }
.header-icon-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }

@media (max-width: 640px) {
    /*
       In the phone panel an icon on its own is a mystery, so the label comes
       back and the row reads like every other row in the sheet.
    */
    .header-menu-check:checked ~ .header-menu .header-icon-link {
        width: 100%;
        height: auto;
        justify-content: flex-start;
        gap: 0.6rem;
        background: none;
        border-radius: 0;
    }

    .header-menu-check:checked ~ .header-menu .header-icon-label {
        position: static;
        width: auto;
        height: auto;
        clip-path: none;
    }

    /* The panel hangs from the header, not from a bell that is 40px wide. */
    .notif-menu { position: static; }
    .notif-panel { right: 0.85rem; left: 0.85rem; width: auto; max-width: none; }
}

/* The footer's one line. The link is the entity, so it reads as one. */
.site-copyright a { color: inherit; font-weight: 600; }
.site-copyright a:hover { color: var(--c-primary); text-decoration: none; }

/* ---- the icons in the bar are one set ----------------------------------- */

/*
   🚨 `flex: none` on the glyph.
   The envelope measured 9×21 in a 36px button: it is a flex item, its width
   was allowed to shrink, and it came out squashed to less than half of what it
   was drawn at. It read as an afterthought beside the bell, which was the
   complaint.
*/
.header-icon-link svg,
.header-bell-standalone svg,
.search-trigger svg {
    flex: none;
    width: 19px;
    height: 19px;
}

/* Slightly larger buttons, so a 19px glyph has room to breathe in one. */
.header-icon-link,
.header-bell-standalone,
.search-trigger { width: 38px; height: 38px; }

@media (max-width: 640px) {
    /* Still 44 on a phone, where they are targets before they are icons. */
    .header-bell-standalone, .search-trigger { width: 44px; height: 44px; }
}

/* ============================================================================
   Arranging widgets on the page
   ============================================================================

   Reasoning is in docs/design/2026-08-06-live-widget-editing.md.
   ========================================================================= */

.arrange-rail {
    position: sticky;
    top: 0;
    z-index: 300;
    background: var(--c-header-bg);
    color: #fff;
    box-shadow: 0 8px 24px -12px rgba(0, 0, 0, 0.6);
    border-bottom: 2px solid var(--c-primary);
}

.arrange-rail.is-bad { border-bottom-color: var(--c-danger); }

.arrange-inner {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    padding-top: 0.7rem;
    padding-bottom: 0.7rem;
    flex-wrap: wrap;
}

.arrange-state { display: flex; align-items: center; gap: 0.6rem; }
.arrange-state strong { display: block; font-size: 0.92rem; }

.arrange-dot {
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--c-primary);
    flex: none;
    animation: arrange-beat 2.2s ease-in-out infinite;
}

@keyframes arrange-beat { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }

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

.arrange-note { font-size: 0.78rem; color: rgba(255, 255, 255, 0.6); }

/* The tray of what is not on the page. Dragging one out drops it here. */
.arrange-tray {
    flex: 1;
    min-width: 14rem;
    padding: 0.45rem 0.6rem;
    border: 1px dashed rgba(255, 255, 255, 0.28);
    border-radius: var(--radius-card);
}

.arrange-tray.is-target { border-style: solid; border-color: var(--c-primary); background: rgba(29, 116, 184, 0.18); }

.arrange-tray-label {
    display: block;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.5);
    margin-bottom: 0.3rem;
}

.arrange-tray-items { display: flex; flex-wrap: wrap; gap: 0.35rem; min-height: 28px; }
.arrange-tray-empty { font-size: 0.78rem; color: rgba(255, 255, 255, 0.45); }

.arrange-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.6rem;
    border-radius: var(--radius-pill);
    background: rgba(255, 255, 255, 0.12);
    font-size: 0.8rem;
    font-weight: 600;
    cursor: grab;
    touch-action: none;
}

.arrange-actions { display: flex; gap: 0.5rem; margin-left: auto; }

/* ---- the areas, while arranging ---------------------------------------- */

.widget-area.is-arrangeable {
    position: relative;
    min-height: 4.5rem;
    padding: 1.4rem 0.5rem 0.5rem;
    border: 1px dashed var(--c-border-strong);
    border-radius: var(--radius-card);
    background: color-mix(in srgb, var(--c-primary) 4%, transparent);
}

.widget-area.is-arrangeable.is-target {
    border-style: solid;
    border-color: var(--c-primary);
    background: var(--c-primary-light);
}

.widget-area-name {
    position: absolute;
    top: 0.35rem;
    left: 0.6rem;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

/* Only shown when the area has nothing in it — an empty area is invisible the
   rest of the time and is exactly where somebody wants to put something. */
.widget-area-empty {
    display: none;
    padding: 0.9rem 0.6rem;
    text-align: center;
    font-size: 0.83rem;
    color: var(--c-text-muted);
}

.widget-area.is-arrangeable:not(:has([data-widget])) .widget-area-empty { display: block; }

/* ---- the widget itself -------------------------------------------------- */

.widget-area.is-arrangeable .widget {
    position: relative;
    cursor: default;
}

/*
   A grip, not the whole widget. A widget can contain links and a poll, and
   making all of it a drag handle makes those unusable.
*/
/*
   🚨 Not positioned.
   The grip used to be the only tool on a widget and pinned itself to the
   corner. It now sits inside `.widget-tools`, which is pinned there instead —
   and leaving this absolute laid the grip directly on top of the settings cog,
   so the two icons stacked.
*/
.widget-grip { cursor: grab; touch-action: none; }

.arrange-placeholder {
    border: 2px dashed var(--c-primary);
    border-radius: var(--radius-card);
    background: var(--c-primary-light);
    margin-bottom: 1rem;
}

.arrange-ghost {
    position: fixed;
    z-index: 400;
    pointer-events: none;
    opacity: 0.9;
    transform: rotate(1.2deg);
    box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.45);
}

body.is-arranging-drag { cursor: grabbing; user-select: none; }

@media (max-width: 640px) {
    .arrange-inner { gap: 0.7rem; }
    .arrange-actions { margin-left: 0; width: 100%; }
    .arrange-actions .btn { flex: 1; justify-content: center; }
}

/* ============================================================================
   The header bar, laid out
   ============================================================================

   What the site is, beside the logo. Who you are, over on the right.
   ========================================================================= */

@media (min-width: 641px) {
    /*
       The panel wrapper stops being a box and its two navs become direct
       children of the bar. On a phone it goes back to being the sheet they
       both live in.
    */
    .header-menu { display: contents; }

    /*
       🚨 `display: contents` does not change document order — the navs come
       out where they already were, which is after the search and bell. The
       order is stated, and stating it for every item is what stops one of
       them landing somewhere nobody meant.
    */
    .site-logo { order: 0; margin-right: 0.6rem; flex: none; }

    /*
       🚨 `min-width: 0`, and the items that do not fit go into a "More" menu.

       A flex item will not shrink below its content, so once the menu is long
       enough — eight items and a signed-in member's name is enough at 1280 —
       the nav pushed straight through the container and took the page with it.
       The header is `position: sticky`, so the horizontal scrollbar followed
       you down every page.

       This was a scrolling nav for about a day. A scrollbar in a navigation bar
       is a workaround wearing a control's clothes; header-nav-overflow.js
       moves the overflow into a dropdown instead, which is what the header
       actually wants. NO overflow property here on purpose — `auto` would make
       this a scroll container, and a scroll container clips the submenus that
       drop out of it.
    */
    .header-nav-primary {
        order: 1;
        margin-right: auto;
        min-width: 0;
        flex: 1 1 auto;
    }

    .header-nav-primary a { flex: none; }

    .search-trigger { order: 2; flex: none; }
    .notif-menu { order: 3; flex: none; }

    /* The session controls never shrink: a squashed username or a clipped
       avatar is worse than a menu you have to scroll. */
    .header-nav-session { order: 4; gap: 0.35rem; flex: none; }
    .header-inner > [data-hook], .header-inner > *:not([class]) { order: 5; }
}

/* ---- a parent and its chevron are one control -------------------------- */

/*
   They were two: a pill for the label and a separate round button for the
   chevron beside it, so "Forums ⌄" read as a word next to an unrelated knob.
   The pill is now the pair, and the chevron sits inside it.
*/
.menu-parent {
    position: relative;
    display: inline-flex;
    align-items: center;
    border-radius: var(--radius-pill);
    transition: background 0.15s;
}

.menu-parent:hover,
.menu-parent.is-open { background: rgba(255, 255, 255, 0.1); }

.header-nav .menu-parent > .menu-link-parent {
    padding-right: 0.3rem;
    background: none;
}

.header-nav .menu-parent > .menu-link-parent:hover { background: none; }

.menu-chevron-btn {
    width: 22px;
    height: 30px;
    padding: 0 0.35rem 0 0;
    background: none;
    color: inherit;
    opacity: 0.7;
}

.menu-chevron-btn:hover { background: none; opacity: 1; }
.menu-parent.is-open .menu-chevron-btn { background: none; opacity: 1; }

.menu-chevron { margin: 0; transition: transform 0.15s; }
.menu-parent.is-open .menu-chevron { transform: rotate(180deg); }

@media (max-width: 640px) {
    /* In the sheet it is a row again, full width, with no pill round it. */
    .header-menu-check:checked ~ .header-menu .menu-parent { background: none; border-radius: 0; }
}

/* ---- the palette: kinds you can add ------------------------------------- */

/*
   Separate from the tray, and the distinction matters: the tray holds widgets
   that exist and are not placed; this holds kinds you can make a new one of.
   Mixing them would mean dragging something out and not knowing whether you
   had removed it or destroyed it.
*/
/*
   The dock: everything not on the page, down the left, open the whole time.

   It was a collapsed "Add a widget" disclosure in the rail. Somebody who never
   opened it had no way of knowing there were eleven kinds, which is the whole
   of the complaint — see the 2026-08-08 section of the note.
*/
.arrange-dock {
    position: fixed;
    left: 0;
    /* Under the rail, which is sticky at the top and 2px of accent border. */
    top: var(--arrange-rail-h, 4.25rem);
    bottom: 0;
    z-index: 290;
    width: var(--arrange-dock-w, 15.5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 1rem 0.9rem 2rem;
    background: var(--c-surface);
    border-right: 1px solid var(--c-border);
    box-shadow: 6px 0 20px -18px rgba(0, 0, 0, 0.5);
}

/* The page moves over rather than being covered. A dock that sits on top of
   the sidebar would hide the area you are dragging into. */
body.is-arranging-dock { padding-left: var(--arrange-dock-w, 15.5rem); }

.arrange-dock-section + .arrange-dock-section {
    margin-top: 1.1rem;
    padding-top: 1.1rem;
    border-top: 1px solid var(--c-border);
}

.arrange-dock-heading {
    margin: 0 0 0.5rem;
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.arrange-dock-hint {
    margin: -0.25rem 0 0.6rem;
    font-size: 0.76rem;
    color: var(--c-text-muted);
}

/* In the dock the tray is on the surface, not on the rail's dark ground, so
   its own colours have to come back to the ordinary text tokens. */
.arrange-dock .arrange-tray {
    flex: none;
    min-width: 0;
    padding: 0;
    border: 0;
}

.arrange-dock .arrange-tray.is-target {
    border: 1px dashed var(--c-primary);
    border-radius: var(--radius-card);
    padding: 0.5rem;
    background: var(--c-primary-light);
}

.arrange-dock .arrange-tray-items { flex-direction: column; align-items: stretch; }
.arrange-dock .arrange-tray-empty { color: var(--c-text-muted); }

.arrange-dock .arrange-chip {
    background: var(--c-hover);
    color: var(--c-text);
    justify-content: flex-start;
}

.arrange-dock .arrange-palette-items { flex-direction: column; align-items: stretch; }

.arrange-palette-group {
    display: block;
    margin: 0.6rem 0 0.35rem;
    font-size: 0.62rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--c-text-muted);
}

.arrange-palette-group:first-child { margin-top: 0; }

.arrange-palette-items { display: flex; flex-wrap: wrap; gap: 0.35rem; }

.arrange-chip.arrange-new {
    background: var(--c-hover);
    border: 1px dashed var(--c-border-strong);
    color: var(--c-text);
}

.arrange-chip.arrange-new:hover { border-color: var(--c-primary); color: var(--c-primary); }

/*
   Below 1100px there is not enough width for the dock and the page both, so it
   becomes a strip under the rail — still open, still grouped, scrolled
   sideways. Folding it away again on a laptop would undo the whole change.
*/
@media (max-width: 1100px) {
    .arrange-dock {
        position: static;
        width: auto;
        max-height: none;
        padding: 0.75rem 1rem;
        border-right: 0;
        border-bottom: 1px solid var(--c-border);
        box-shadow: none;
        display: flex;
        gap: 1.5rem;
        overflow-x: auto;
        overflow-y: hidden;
    }

    body.is-arranging-dock { padding-left: 0; }

    .arrange-dock-section { flex: none; }

    .arrange-dock-section + .arrange-dock-section {
        margin-top: 0;
        padding-top: 0;
        padding-left: 1.5rem;
        border-top: 0;
        border-left: 1px solid var(--c-border);
    }

    .arrange-dock .arrange-tray-items,
    .arrange-dock .arrange-palette-items { flex-direction: row; }

    .arrange-dock-hint { display: none; }
}

/* ---- widget tools and the settings sheet -------------------------------- */

.widget-tools {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 0.3rem;
}

.widget-tool {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    border: 0;
    border-radius: 7px;
    background: var(--c-hover);
    color: var(--c-text-muted);
    cursor: pointer;
}

.widget-tool:hover { background: var(--c-primary-light); color: var(--c-primary); }
.widget-grip { cursor: grab; touch-action: none; }

/*
   The X reads as the others until it is pointed at, then as what it is. A
   permanently red control in the corner of every widget makes the page look
   like it is full of errors; a quiet one that turns red on hover says the same
   thing at the moment it matters.
*/
.widget-tool-remove:hover,
.widget-tool-remove:focus-visible {
    background: var(--c-danger-soft);
    color: var(--c-danger);
}

/*
   The sign-up prompt is only shown to signed-out visitors, so an administrator
   arranging the page cannot see it and has no way to know it is already there.
   Every widget with an audience or a condition has the same trap, so they are
   drawn while arranging and marked instead.
*/
.widget-hidden-note {
    padding: 0.12rem 0.5rem;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--c-warning) 16%, transparent);
    color: var(--c-warning);
    font-size: 0.63rem;
    font-weight: 700;
    letter-spacing: 0.03em;
    white-space: nowrap;
}

.widget-area.is-arrangeable [data-widget-hidden] {
    /* Present but not yours: enough to read, obviously not the live state. */
    opacity: 0.72;
    outline: 1px dashed var(--c-warning);
    outline-offset: -1px;
}

.widget-sheet {
    position: fixed;
    inset: 0;
    z-index: 500;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 1.5rem;
    background: rgba(8, 12, 18, 0.5);
    backdrop-filter: blur(2px);
}

.widget-sheet-panel {
    display: flex;
    flex-direction: column;
    width: 32rem;
    max-width: 100%;
    max-height: 88vh;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift-hover);
    overflow: hidden;
}

.widget-sheet-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.85rem 1rem;
    border-bottom: 1px solid var(--c-border);
}

.widget-sheet-title { font-weight: 650; letter-spacing: -0.01em; }

.widget-sheet-close {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border: 0;
    border-radius: 8px;
    background: none;
    color: var(--c-text-muted);
    cursor: pointer;
}

.widget-sheet-close:hover { background: var(--c-hover); color: var(--c-text); }

.widget-sheet-body { padding: 1rem; overflow-y: auto; }
.widget-sheet-loading { padding: 2rem 1rem; text-align: center; color: var(--c-text-muted); }

.widget-sheet-foot {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    padding: 0.8rem 1rem;
    border-top: 1px solid var(--c-border);
    background: var(--c-hover);
}

/* The admin form's own controls, on a front-end surface. */
.widget-sheet-body .admin-input,
.widget-sheet-body .admin-form-group input[type="text"],
.widget-sheet-body .admin-form-group input[type="number"],
.widget-sheet-body textarea {
    width: 100%;
    min-height: 40px;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--c-border-strong);
    border-radius: var(--radius, 8px);
    background: var(--c-bg);
    color: var(--c-text);
    font-family: inherit;
    font-size: 0.92rem;
}

.widget-sheet-body .admin-form-group { margin-bottom: 1rem; }
.widget-sheet-body label { display: block; font-size: 0.82rem; font-weight: 650; margin-bottom: 0.3rem; }
.widget-sheet-body .form-hint { font-size: 0.78rem; color: var(--c-text-muted); margin-top: 0.3rem; }
.widget-sheet-body .admin-checkbox-label { display: flex; align-items: center; gap: 0.5rem; font-weight: 500; }
.widget-sheet-body .admin-checkbox-label input { width: auto; min-height: 0; }

/*
   The admin form brings its own submit. In the sheet the footer owns that, and
   two Save buttons a centimetre apart is a question nobody should have to
   answer.
*/
.widget-sheet-body button[type="submit"],
.widget-sheet-body .theme-actions { display: none; }

/* Remove sits apart from Cancel and Apply: it is the one control here that
   ends with something not existing. */
.widget-sheet-spacer { flex: 1; }

.btn-quiet-danger {
    background: none;
    border: 1px solid transparent;
    color: var(--c-danger);
}

.btn-quiet-danger:hover {
    background: color-mix(in srgb, var(--c-danger) 10%, transparent);
    border-color: color-mix(in srgb, var(--c-danger) 30%, transparent);
}

.widget-sheet-foot { justify-content: flex-start; }

/* A widget that has nothing to say, said while arranging. */
.widget-nothing {
    padding: 0.4rem 0;
    font-size: 0.85rem;
    font-style: italic;
    color: var(--c-text-muted);
}

/* ============================================================================
   Downloads
   ============================================================================

   Reasoning is in docs/design/2026-08-06-downloads-and-the-marketplace.md.
   ========================================================================= */

.cat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: 1rem;
    margin-bottom: 2rem;
}

.cat-card {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    padding: 1.1rem 1.2rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    color: var(--c-text);
}

.cat-card:hover { box-shadow: var(--lift-hover); text-decoration: none; transform: translateY(-2px); }
.cat-card { transition: transform 0.16s ease, box-shadow 0.16s ease; }

.cat-name { font-size: 1.02rem; font-weight: 650; letter-spacing: -0.01em; }
.cat-desc { font-size: 0.85rem; line-height: 1.45; color: var(--c-text-muted); }
.cat-count { margin-top: 0.35rem; font-size: 0.75rem; color: var(--c-text-muted); }

.cat-heading { display: flex; align-items: center; gap: 0.8rem; margin: 1.6rem 0 0.7rem; }
.cat-heading .rule { flex: 1; height: 1px; background: var(--c-border); }

/* ---- a file in a list --------------------------------------------------- */

.file-list { display: flex; flex-direction: column; gap: 0.6rem; }

.file-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.25rem;
    padding: 0.95rem 1.15rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    color: var(--c-text);
}

.file-row:hover { box-shadow: var(--lift-hover); text-decoration: none; }

.file-main { display: flex; flex-direction: column; gap: 0.15rem; min-width: 0; }

.file-title {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    font-size: 1rem;
    font-weight: 650;
    letter-spacing: -0.01em;
}

.file-paid { background: color-mix(in srgb, var(--c-warning) 16%, transparent); color: var(--c-warning); }
.file-desc { font-size: 0.86rem; line-height: 1.4; color: var(--c-text-muted); }
.file-by { font-size: 0.77rem; color: var(--c-text-muted); }

.file-side { display: flex; flex-direction: column; align-items: flex-end; gap: 0.15rem; flex: none; }

/*
   The version leads. On a downloads page it is what somebody is checking — a
   file they already have is only interesting if the number changed.
*/
.file-version {
    padding: 0.12rem 0.55rem;
    border-radius: var(--radius-pill);
    background: var(--c-primary-light);
    color: var(--c-primary);
    font-size: 0.8rem;
    font-weight: 700;
}

.file-count { font-size: 0.73rem; color: var(--c-text-muted); }

/* ---- the file's own page ------------------------------------------------ */

.file-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1.5rem;
    flex-wrap: wrap;
    padding: 1.4rem 1.5rem;
    margin-bottom: 1.4rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
}

.file-head h2 { font-size: 1.6rem; font-weight: 750; letter-spacing: -0.03em; line-height: 1.2; }

.file-head-by {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    margin-top: 0.5rem;
    font-size: 0.87rem;
    color: var(--c-text-muted);
}

.file-head-get { display: flex; flex-direction: column; align-items: flex-end; gap: 0.4rem; }

/* The gap is the button's, not the icon's — an svg with a margin only works
   until somebody uses the button without one. */
.btn-get { font-size: 1rem; padding: 0 1.5rem; min-height: 46px; gap: 0.5rem; }
.btn-get svg { flex: none; }
.file-size { font-size: 0.77rem; color: var(--c-text-muted); }

.file-locked { text-align: right; max-width: 22rem; }
.file-price { display: block; font-size: 1.3rem; font-weight: 750; letter-spacing: -0.02em; }
.file-locked p { margin: 0.3rem 0 0.6rem; font-size: 0.85rem; }

/* ---- versions ----------------------------------------------------------- */

.version-list { display: flex; flex-direction: column; }

.version-row { padding: 0.85rem 0; border-bottom: 1px solid var(--c-border); }
.version-row:last-child { border-bottom: 0; padding-bottom: 0; }
.version-row:first-child { padding-top: 0; }

.version-head { display: flex; align-items: baseline; gap: 0.7rem; flex-wrap: wrap; }

.version-number {
    padding: 0.1rem 0.5rem;
    border-radius: var(--radius-pill);
    background: var(--c-hover);
    font-size: 0.83rem;
    font-weight: 700;
}

.version-row:first-child .version-number { background: var(--c-primary-light); color: var(--c-primary); }

.version-when, .version-size { font-size: 0.78rem; color: var(--c-text-muted); }
.version-get { margin-left: auto; font-size: 0.83rem; font-weight: 650; }
.version-changelog { margin-top: 0.5rem; font-size: 0.88rem; color: var(--c-text-muted); }

.checksum { font-size: 0.78rem; color: var(--c-text-muted); line-height: 1.7; }

.checksum code {
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: 0.72rem;
    overflow-wrap: anywhere;
    color: var(--c-text);
}

@media (max-width: 640px) {
    .file-row { flex-direction: column; align-items: stretch; gap: 0.6rem; }
    .file-side { flex-direction: row; align-items: center; justify-content: space-between; }
    .file-head-get { align-items: stretch; width: 100%; }
    .file-locked { text-align: left; max-width: none; }
    .btn-get { justify-content: center; }
}

/* ===========================================================================
   The control vocabulary.

   Step two of docs/design/2026-08-06-the-design-system.md: one button, one
   input, one table, one panel, one empty state — defined here, once, and used
   by the front end and the admin alike.

   🚨 Each selector carries both names. The admin's nineteen templates say
   `.admin-btn` and `.admin-card`; rather than churn every one of them to prove
   a point, the shared rule answers to both. What matters is that there is one
   definition, not that there is one spelling. The duplicates that used to live
   in admin.css are deleted — a second definition loaded afterwards is not an
   alias, it is an override, and that is how the two drifted apart.

   Everything here is on the space, type and radius scales.
   =========================================================================== */

.btn,
.admin-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    padding: var(--s-2) var(--s-5);
    border: 1px solid transparent;
    border-radius: var(--radius);
    font-family: inherit;
    font-size: var(--t-body);
    font-weight: var(--w-strong);
    line-height: 1.4;
    cursor: pointer;
    text-decoration: none;
    white-space: nowrap;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.btn:hover,
.admin-btn:hover { text-decoration: none; }

/* A control nobody can use should look like one, everywhere. */
.btn:disabled,
.admin-btn:disabled,
.btn[aria-disabled="true"] {
    opacity: 0.55;
    cursor: not-allowed;
}

.btn-primary,
.admin-btn-primary {
    background: var(--c-primary);
    border-color: var(--c-primary);
    color: #fff;
}

.btn-primary:hover,
.admin-btn-primary:hover {
    background: var(--c-primary-hover);
    border-color: var(--c-primary-hover);
    color: #fff;
}

/* The default: present, but not competing with the primary action. */
.btn-default,
.admin-btn-default,
/* The admin's old fourth button. It baked in #fff and #f9fafb, so it was one
   more white box waiting for a dark page; it is the default emphasis. */
.admin-btn-outline {
    background: var(--c-surface);
    border-color: var(--c-border-strong);
    color: var(--c-text);
}

.btn-default:hover,
.admin-btn-default:hover,
.admin-btn-outline:hover { background: var(--c-hover); }

/* Quiet: a real control that gets out of the way until wanted. */
.btn-quiet,
.admin-btn-quiet {
    background: none;
    border-color: transparent;
    color: var(--c-text-muted);
}

.btn-quiet:hover,
.admin-btn-quiet:hover {
    background: var(--c-hover);
    color: var(--c-text);
}

.btn-danger,
.admin-btn-danger {
    background: var(--c-danger);
    border-color: var(--c-danger);
    color: #fff;
}

.btn-danger:hover,
.admin-btn-danger:hover { filter: brightness(0.92); color: #fff; }

.btn-sm,
.admin-btn-sm {
    padding: var(--s-1) var(--s-3);
    font-size: var(--t-small);
}

.btn-lg {
    padding: var(--s-3) var(--s-6);
    font-size: var(--t-read);
}

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

/*
   🚨 `.form-input` belongs on this list and was missing from it.
   
   The front end's own input class only ever set padding and a radius, so every
   text field using it fell through to the browser's default styling: a white
   box with a grey border, which was invisible while the site was light and
   became a white slab on a dark page the moment it was not. The split-posts bar
   is where it showed up — a bright white field in a dark sticky bar — and
   fourteen templates use the class.

   The same mistake the comment below describes, in the same file, made twice.
*/
.input,
.admin-input,
.form-input,
.form-textarea,
.form-control {
    width: 100%;
    padding: var(--s-2) var(--s-3);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    font-family: inherit;
    font-size: var(--t-body);
    /* 🚨 --c-surface, not #fff. The admin hardcoded white here, which was
       invisible for as long as the admin was light-only and became a white
       box on a dark page the moment it was not. */
    background: var(--c-surface);
    color: var(--c-text);
    transition: border-color 0.15s, box-shadow 0.15s;
}

.input:focus,
.admin-input:focus,
.form-input:focus,
.form-textarea:focus,
.form-control:focus {
    outline: none;
    border-color: var(--c-primary);
    box-shadow: 0 0 0 3px var(--c-primary-light);
}

.input::placeholder,
.admin-input::placeholder,
.form-input::placeholder,
.form-textarea::placeholder { color: var(--c-text-muted); }

.input:disabled,
.admin-input:disabled,
.form-input:disabled,
.form-textarea:disabled {
    background: var(--c-inset);
    color: var(--c-text-muted);
    cursor: not-allowed;
}

/* ---- tables ------------------------------------------------------------- */

.table,
.admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--t-body);
}

.table th,
.table td,
.admin-table th,
.admin-table td {
    padding: var(--s-3) var(--s-4);
    text-align: left;
    border-bottom: 1px solid var(--c-border);
}

.table th,
.admin-table th {
    font-size: var(--t-micro);
    font-weight: var(--w-strong);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
    background: var(--c-inset);
    white-space: nowrap;
}

.table tr:last-child td,
.admin-table tr:last-child td { border-bottom: none; }

.table tbody tr:hover td,
.admin-table tbody tr:hover td { background: var(--c-hover); }

/* Digits that line up, wherever a column of them appears. */
.table .num,
.admin-table .num {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* ---- panels ------------------------------------------------------------- */

.panel,
.admin-card {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    margin-bottom: var(--s-6);
}

.panel-header,
.admin-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s-3);
    padding: var(--s-4) var(--s-5);
    border-bottom: 1px solid var(--c-border);
    font-size: var(--t-body);
    font-weight: var(--w-strong);
}

.panel-body,
.admin-card-body { padding: var(--s-5); }

/* A table inside a panel already has the panel's edge; it does not need its
   own, and it should reach the panel's corners rather than stop short. */
.panel > .table,
.admin-card > .admin-table { margin-bottom: 0; }

.panel > .table tr:last-child td,
.admin-card > .admin-table tr:last-child td { border-bottom: none; }

/* ---- the empty state ---------------------------------------------------- */

/* 🚨 Every list ships one. "No results" is a dead end; an empty state says what
   would be here and offers the thing that would put it there. This is the item
   most often skipped and the one that most reads as finished software. */
.empty,
.admin-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s-3);
    padding: var(--s-9) var(--s-6);
    text-align: center;
    color: var(--c-text-muted);
}

.empty-title,
.admin-empty-title {
    font-size: var(--t-lead);
    font-weight: var(--w-strong);
    color: var(--c-text);
}

.empty-note,
.admin-empty-note {
    max-width: 34rem;
    font-size: var(--t-body);
    line-height: 1.6;
}

/* A label that only a screen reader needs. Clipped rather than display:none,
   which would take it out of the accessibility tree along with the layout. */
.visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* ---- tabs ---------------------------------------------------------------
   One choice with several positions, not several buttons in a row. Used
   wherever a list is filtered rather than acted on.
   ------------------------------------------------------------------------- */

.admin-tabs {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-1);
    margin-bottom: var(--s-6);
    border-bottom: 1px solid var(--c-border);
}

.admin-tab {
    display: inline-flex;
    align-items: center;
    gap: var(--s-2);
    padding: var(--s-3) var(--s-4);
    /* The lit tab sits on the rule rather than above it, so the strip reads as
       one control. The transparent border keeps the others from moving. */
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    color: var(--c-text-muted);
    font-size: var(--t-body);
    font-weight: var(--w-strong);
    text-decoration: none;
}

.admin-tab:hover { color: var(--c-text); text-decoration: none; }

.admin-tab.is-on {
    color: var(--c-primary);
    border-bottom-color: var(--c-primary);
}

.admin-tab-count {
    padding: 0 var(--s-2);
    border-radius: var(--radius-pill);
    background: var(--c-hover);
    font-size: var(--t-micro);
    font-weight: var(--w-strong);
}

.admin-tab.is-on .admin-tab-count {
    background: var(--c-primary-light);
    color: var(--c-primary);
}

/* A value stored lower-case that should read as a word: a mode, a type, a
   state. Named rather than inline, because it is a presentation rule about a
   stored value and there are a dozen of them. */
.cv-capitalise { text-transform: capitalize; }

/* ---- putting a file up --------------------------------------------------- */

.submit-form .form-group:last-child { margin-bottom: 0; }

.submit-actions {
    display: flex;
    gap: var(--s-3);
    border-top: 1px solid var(--c-border);
}

/* Adding a version: closed until wanted, because the page is for reading and
   this is for the one person in a hundred who owns the file. */
.version-add { padding: 0; }

.version-add-summary {
    padding: var(--s-4) var(--s-5);
    font-weight: var(--w-strong);
    cursor: pointer;
    list-style: none;
}

.version-add-summary::-webkit-details-marker { display: none; }

.version-add-summary::before {
    content: '+';
    display: inline-block;
    width: 1.1em;
    color: var(--c-text-muted);
}

.version-add[open] .version-add-summary {
    border-bottom: 1px solid var(--c-border);
}

.version-add[open] .version-add-summary::before { content: '\2212'; }


/* ===========================================================================
   Shared utilities.

   🚨 These live here, not in admin.css, because the front end does not load
   admin.css. They were written during the admin pass and left there, and one
   of them — .is-hidden — was the only thing keeping the report dialog closed
   on every topic page. Undefined class, no display:none, dialog open over the
   thread for every visitor.

   If a utility can be used on both surfaces, it belongs in this file. The
   design audit now fails when one does not.
   =========================================================================== */

/* A form that is a control rather than a section of a page. */
.inline-form { display: inline; }

/* Column widths are a property of what the column holds, not of the data. */
.table .col-tiny,   .admin-table .col-tiny   { width: 3rem; }
.table .col-narrow, .admin-table .col-narrow { width: 5rem; }
.table .col-mid,    .admin-table .col-mid    { width: 9rem; }
.table .col-wide,   .admin-table .col-wide   { width: 12rem; }

/* The row of buttons that ends a form. */
.form-actions {
    display: flex;
    align-items: center;
    gap: var(--s-3);
    flex-wrap: wrap;
    margin-top: var(--s-5);
}

/* A field that is not shown yet — a section revealed by a choice above it.
   The attribute would do, but a class survives a script that toggles it. */
.is-hidden { display: none; }

/* A block that needs air above it because it starts a new thought. */
.stack-top { margin-top: var(--s-6); }

/* A large glyph standing in for an image: a badge icon, an emoji marker. */
.glyph-lg { font-size: var(--t-h3); line-height: 1; }

/* Air under one block in a stack. */
.stack-bottom { margin-bottom: var(--s-2); }

/* The largest of the shared field sizes. This was .admin-input-sm at 320px —
   a name that said "small" for the widest field in the admin, which is how it
   ended up applied to seventy-five fields that had nothing in common. */
/* 🚨 A field sized by what it holds, not by the width of the window. A port
   number is four characters wide whatever the screen is, and a text input
   stretched across a 1440px page invites somebody to type a paragraph into a
   field that takes a number.

   Three sizes, because the admin had seven hand-written pixel widths — 110,
   120, 130, 160, 200, 220 and 260 — none of which meant anything different
   from its neighbour. */
.input-xs { max-width: 8rem; }   /* a count, a port, a version */
.input-sm { max-width: 12rem; }  /* a short value, a colour */
.input-md { max-width: 16rem; }  /* a name, a slug, a title */

.input-lg { max-width: 20rem; }


/* ---------------------------------------------------------------------------
   Widget settings fields.

   🚨 These were in admin.css, and the widget settings partial is rendered by
   BOTH the admin screen and the front-end widget editor — so on the front end
   they resolved to nothing and the fields came out unstyled. Found by the
   admin-only-class check, not by looking; the same root cause as the report
   dialog, sitting there the whole time.
   --------------------------------------------------------------------------- */

.menu-optional {
    font-weight: var(--w-body);
    font-size: var(--t-small);
    color: var(--c-text-muted);
}

.page-block-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--s-3);
    align-items: start;
}

.theme-code {
    width: 100%;
    font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
    font-size: var(--t-small);
    line-height: 1.6;
    resize: vertical;
    tab-size: 2;
}

/* 🚨 The ink moves a shade: #78350f becomes --c-warning-chip-ink (#92400e).
   Both are dark amber and the difference is barely visible in light mode,
   which is the price of the note being readable in dark mode at all — it was
   near-black-brown text on a hardcoded cream box. */
.ext-trust-note {
    background: var(--c-warning-soft);
    border: 1px solid var(--c-warning-line);
    border-left: 3px solid var(--c-warning-ink);
    border-radius: var(--radius);
    padding: var(--s-3) var(--s-4);
    margin-bottom: var(--s-5);
    font-size: var(--t-body);
    line-height: 1.65;
    color: var(--c-warning-chip-ink);
}

.ext-trust-note strong { color: var(--c-warning-chip-ink); }

.menu-group-grid {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-2) var(--s-6);
    margin: var(--s-3) 0;
}

/* ---------------------------------------------------------------- file field

   The browser's own file input is a grey "Choose File" button with a
   filename beside it, styled by the platform and by nothing else. Next to
   Convoro's controls it reads as a hole in the page — which is what it was
   doing on the avatar form.

   It cannot be styled directly, so the real input is made invisible and
   stretched over a label that can be. Not `display:none`: that takes the
   input out of the tab order and the control stops being reachable by
   keyboard. Invisible but present keeps focus, drag-and-drop and the file
   picker all working, and the label is a click target for the whole control.
*/
.file-field {
    display: inline-flex;
    align-items: center;
    gap: 0.7rem;
    position: relative;
    max-width: 100%;
    cursor: pointer;
}

.file-field input[type="file"] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    cursor: pointer;
}

.file-field-button {
    display: inline-flex;
    align-items: center;
    padding: 0.45rem 0.95rem;
    border: 1px solid var(--c-border-strong);
    border-radius: 6px;
    background: var(--c-control-bg);
    color: var(--c-control-ink);
    font-size: 0.87rem;
    font-weight: 600;
    white-space: nowrap;
    transition: background 0.15s, border-color 0.15s;
}

.file-field:hover .file-field-button {
    border-color: var(--c-primary);
}

/* The ring goes on the label, because the thing being focused is invisible. */
.file-field input[type="file"]:focus-visible + .file-field-button {
    outline: 2px solid var(--c-primary);
    outline-offset: 2px;
}

.file-field-name {
    color: var(--c-text-2);
    font-size: 0.87rem;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    min-width: 0;
}

.file-field-name:empty::before {
    content: attr(data-empty);
}

/* ============================================================================
   Custom database records
   ============================================================================

   Reasoning is in docs/design/2026-08-08-custom-databases.md.
   ========================================================================= */

.db-intro { color: var(--c-text-muted); margin-bottom: 1.25rem; }

.db-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: 1rem;
}

.db-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1.1rem 1.25rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    color: var(--c-text);
    text-decoration: none;
    transition: transform 0.16s ease, box-shadow 0.16s ease;
}

.db-card:hover { transform: translateY(-2px); box-shadow: var(--lift-hover); text-decoration: none; }

.db-card-title { font-size: 1.05rem; font-weight: 650; letter-spacing: -0.01em; }

.db-card-fields { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.db-field {
    display: inline-flex;
    align-items: baseline;
    gap: 0.3rem;
    font-size: 0.78rem;
    color: var(--c-text-muted);
}

.db-field-label { font-weight: 600; }

/*
   The value can be a paragraph of a long-text field, so it is clamped to one
   line here — a card whose height depends on how much somebody typed makes a
   grid look broken.
*/
.db-field-value {
    max-width: 18rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ---- one record ---- */

.db-record {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift);
    overflow: hidden;
}

.db-row {
    display: grid;
    grid-template-columns: minmax(8rem, 20%) 1fr;
    gap: 1rem;
    padding: 0.9rem 1.25rem;
    border-bottom: 1px solid var(--c-border);
}

.db-row:last-child { border-bottom: none; }

.db-row-label { font-weight: 650; color: var(--c-text-muted); font-size: 0.88rem; }
.db-row-value { min-width: 0; overflow-wrap: anywhere; }

.db-byline {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin-top: 1rem;
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

@media (max-width: 640px) {
    .db-list { grid-template-columns: minmax(0, 1fr); }
    /* Label above value: two columns at 320px leaves neither enough room. */
    .db-row { grid-template-columns: minmax(0, 1fr); gap: 0.25rem; }
}

/* ============================================================================
   Knowledge Base
   ============================================================================

   Reasoning is in docs/design/2026-08-08-the-knowledge-base.md.
   ========================================================================= */

.kb-intro { color: var(--c-text-muted); margin-bottom: 1.25rem; }

/* Audience is a filter across every category rather than three sections. */
.kb-audience {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-bottom: 1.5rem;
}

.kb-audience-pill {
    padding: 0.3rem 0.8rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    background: var(--c-surface);
    color: var(--c-text-muted);
    font-size: 0.83rem;
    font-weight: 600;
    text-decoration: none;
}

.kb-audience-pill:hover { border-color: var(--c-primary); color: var(--c-primary); text-decoration: none; }

.kb-audience-pill.is-on {
    background: var(--c-primary);
    border-color: var(--c-primary);
    color: #fff;
}

.kb-section { margin-bottom: 2rem; }

.kb-section-head h3 { margin: 0 0 0.2rem; font-size: 1.15rem; letter-spacing: -0.01em; }
.kb-section-head p { margin: 0 0 0.9rem; color: var(--c-text-muted); font-size: 0.88rem; }

.kb-articles {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(330px, 1fr));
    gap: 0.85rem;
}

.kb-article-card {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 1rem 1.15rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    color: var(--c-text);
    text-decoration: none;
    transition: border-color 0.16s ease, transform 0.16s ease;
}

.kb-article-card:hover {
    border-color: var(--c-primary);
    transform: translateY(-2px);
    text-decoration: none;
}

/* The question, not the title: it is what somebody would actually ask, and it
   is why they are on this page. */
.kb-article-title { font-weight: 650; line-height: 1.35; }

.kb-article-summary {
    font-size: 0.85rem;
    line-height: 1.5;
    color: var(--c-text-muted);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.kb-audience-tag {
    align-self: flex-start;
    margin-top: 0.15rem;
    padding: 0.1rem 0.5rem;
    border-radius: var(--radius-pill);
    background: var(--c-hover);
    color: var(--c-text-muted);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.03em;
}

/* ---- one article ---- */

/*
   The short answer, before the long one. Somebody who arrived from a search
   result should be able to stop reading after this paragraph — which is the
   same reason the column exists for the assistant.
*/
.kb-summary {
    padding: 1rem 1.25rem;
    margin-bottom: 1.5rem;
    border-left: 3px solid var(--c-primary);
    border-radius: 0 var(--radius-card) var(--radius-card) 0;
    background: var(--c-primary-light);
    font-size: 1.02rem;
    line-height: 1.6;
}

.kb-body { line-height: 1.7; }
.kb-body p { margin: 0 0 1rem; }
.kb-body h2, .kb-body h3 { margin: 1.75rem 0 0.6rem; letter-spacing: -0.01em; }
.kb-body ul, .kb-body ol { margin: 0 0 1rem 1.25rem; }
.kb-body li { margin-bottom: 0.35rem; }

.kb-body code {
    padding: 0.1rem 0.35rem;
    border-radius: 5px;
    background: var(--c-hover);
    font-size: 0.9em;
}

.kb-helpful {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.75rem;
    margin-top: 2rem;
    padding: 1rem 1.25rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    background: var(--c-surface);
}

.kb-helpful-ask { font-size: 0.9rem; color: var(--c-text-muted); }
.kb-helpful-form { display: flex; gap: 0.4rem; }

@media (max-width: 640px) {
    .kb-articles { grid-template-columns: minmax(0, 1fr); }
    .kb-summary { font-size: 0.95rem; padding: 0.85rem 1rem; }
}

/* ============================================================================
   The moderation menu
   ============================================================================

   Was `.mod-toolbar`: four buttons and a warning colour across the top of every
   topic a moderator opened. On a site where most topics never need moderating,
   that competed with the title and crowded the follow control above it.

   A menu instead, opened when it is wanted. Built on <details>, like the merge
   panel beside it — works with scripts off, keyboard-reachable for free.
   ========================================================================= */

.mod-menu {
    position: relative;
    /* Clears the follow control above it, which was almost touching. */
    margin: 0.75rem 0 1rem;
}

.mod-menu-open {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.4rem 0.85rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    background: var(--c-surface);
    color: var(--c-text-muted);
    font-size: 0.85rem;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.mod-menu-open::-webkit-details-marker { display: none; }
.mod-menu-open::marker { content: ''; }

.mod-menu-open:hover { border-color: var(--c-primary); color: var(--c-primary); }

.mod-menu[open] .mod-menu-open {
    border-color: var(--c-warning-line);
    background: var(--c-warning-soft);
    color: var(--c-warning-chip-ink);
}

/*
   The panel floats rather than pushing the thread down: opening it should not
   move the post you were about to read.
*/
.mod-menu-panel {
    position: absolute;
    top: calc(100% + 0.4rem);
    left: 0;
    z-index: 30;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    padding: 0.6rem 0.75rem;
    min-width: 18rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-card);
    box-shadow: var(--lift-hover);
}

/* Badges live on the summary, so a locked or pinned topic still says so
   without anybody opening anything. */
.mod-menu-open .mod-indicator { margin-left: 0.15rem; }

@media (max-width: 640px) {
    /* No room to float on a phone; it opens in place instead. */
    .mod-menu-panel {
        position: static;
        min-width: 0;
        margin-top: 0.4rem;
        box-shadow: none;
    }
}

/* ---- Blogs ---------------------------------------------------------------

   The fourth application, and the first honest test of whether the registries
   earn their keep — it renders entries in six places and none of them is a
   forum. Reasoning in docs/design/2026-08-08-the-blog.md.
*/

.blog-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: 1.5rem;
    align-items: start;
}

.blog-main { min-width: 0; }
.blog-side { min-width: 0; }

.blog-description {
    color: var(--c-text-muted);
    margin: -0.4rem 0 1.2rem;
}

.blog-list { list-style: none; margin: 0; padding: 0; }

.blog-list li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.6rem;
    padding: 0.4rem 0;
}

.blog-list-count { font-size: 0.78rem; color: var(--c-text-muted); }

/* ---- an entry in a list ---- */

.blog-card {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 1.1rem;
    padding: 1.1rem;
    margin-bottom: 1rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.blog-card:hover { border-color: var(--c-primary); box-shadow: var(--lift-hover); }

/* 🚨 Width and height come from the stored dimensions, so the card does not
   jump when the picture arrives — the same reasoning as the forum covers,
   which was arrived at the hard way. */
.blog-card-cover {
    display: block;
    width: 190px;
    flex-shrink: 0;
    border-radius: 10px;
    overflow: hidden;
    background: var(--c-bg);
}

.blog-card-cover img {
    display: block;
    width: 100%;
    height: auto;
}

.blog-card-body { min-width: 0; }

.blog-card-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.4rem;
    font-size: 0.78rem;
    color: var(--c-text-muted);
    margin-bottom: 0.35rem;
}

.blog-card-blog { color: var(--c-primary); font-weight: 600; }

.blog-card-title {
    margin: 0 0 0.4rem;
    font-size: 1.12rem;
    line-height: 1.3;
}

.blog-card-title a { color: var(--c-text); }
.blog-card-title a:hover { color: var(--c-primary); text-decoration: none; }

.blog-card-excerpt {
    margin: 0 0 0.6rem;
    color: var(--c-text-muted);
    font-size: 0.9rem;
    line-height: 1.5;
    /* Two lines: enough to decide by, not enough to turn a list into a wall. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.blog-card-foot {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.8rem;
    color: var(--c-text-muted);
}

.blog-card-author { font-weight: 600; color: var(--c-text); }
.blog-card-comments { margin-left: auto; }

/* Drafts and scheduled entries, shown only to whoever may write here. */
.blog-unpublished {
    padding: 1rem 1.1rem 0.2rem;
    margin-bottom: 1.5rem;
    border: 1px dashed var(--c-border);
    border-radius: var(--radius);
    background: var(--c-bg);
}

.blog-unpublished h3 { margin: 0; font-size: 1rem; }
.blog-unpublished > .form-hint { margin-bottom: 0.9rem; }

/* ---- one entry ---- */

.blog-entry {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    overflow: hidden;
    margin-bottom: 1.5rem;
}

.blog-entry-cover img { display: block; width: 100%; height: auto; }

.blog-entry-head { padding: 1.4rem 1.5rem 0; }
.blog-entry-head h1 { margin: 0 0 0.9rem; font-size: 1.7rem; line-height: 1.25; }

.blog-entry-meta {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding-bottom: 1.1rem;
    border-bottom: 1px solid var(--c-border);
}

.blog-entry-author { font-weight: 650; color: var(--c-text); }
.blog-entry-when { font-size: 0.8rem; color: var(--c-text-muted); }
.blog-entry-actions { margin-left: auto; display: flex; gap: 0.4rem; align-items: center; }

.blog-entry-body { padding: 1.3rem 1.5rem 1.6rem; line-height: 1.7; }

/* ---- comments ---- */

.blog-comments {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    padding: 1.2rem 1.4rem;
}

.blog-comments > h3 { margin: 0 0 1rem; font-size: 1.05rem; }

.blog-comment {
    display: flex;
    gap: 0.8rem;
    padding: 0.9rem 0;
    border-top: 1px solid var(--c-border);
}

.blog-comment-main { min-width: 0; flex: 1; }

.blog-comment-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.8rem;
    color: var(--c-text-muted);
    margin-bottom: 0.25rem;
}

.blog-comment-author { font-weight: 650; color: var(--c-text); }
.blog-comment-body { line-height: 1.6; }

.blog-comment-form { margin-top: 1.2rem; }

.blog-compose .form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: 1rem;
}

@media (max-width: 900px) {
    .blog-layout { grid-template-columns: minmax(0, 1fr); }

    /* The picture goes above the words rather than shrinking to a thumbnail
       nobody can make anything out in. */
    .blog-card { grid-template-columns: minmax(0, 1fr); }
    .blog-card-cover { width: 100%; }
}

/* ---- Events --------------------------------------------------------------

   🚨 The date block on a card leads, because an event is chosen by WHEN before
   it is chosen by what. Reasoning in docs/design/2026-08-08-events.md.
*/

.event-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: 1.5rem;
    align-items: start;
}

.event-main, .event-side { min-width: 0; }

.event-description { color: var(--c-text-muted); margin: -0.4rem 0 1.2rem; }

.event-calendar-list { list-style: none; margin: 0; padding: 0; }

.event-calendar-list li {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0;
}

.event-calendar-dot {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    flex-shrink: 0;
    display: inline-block;
}

/* ---- a card ---- */

.event-card {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    gap: 1.1rem;
    padding: 1.1rem;
    margin-bottom: 1rem;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.event-card:hover { border-color: var(--c-primary); box-shadow: var(--lift-hover); }

.event-date {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    width: 4.2rem;
    padding: 0.55rem 0;
    border-radius: 12px;
    background: var(--c-primary-light);
    color: var(--c-primary);
    flex-shrink: 0;
    line-height: 1.1;
}

.event-date:hover { text-decoration: none; }
.event-date-month { font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 700; }
.event-date-day { font-size: 1.6rem; font-weight: 700; }

.event-card-body { min-width: 0; }

.event-card-meta {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.78rem;
    color: var(--c-text-muted);
    margin-bottom: 0.25rem;
}

.event-card-calendar { color: var(--c-primary); font-weight: 600; }

.event-card-title { margin: 0 0 0.3rem; font-size: 1.1rem; line-height: 1.3; }
.event-card-title a { color: var(--c-text); }
.event-card-title a:hover { color: var(--c-primary); text-decoration: none; }

.event-card-when { font-size: 0.88rem; font-weight: 600; }
.event-card-where, .event-card-going { font-size: 0.82rem; color: var(--c-text-muted); }
.event-card-going { margin-top: 0.35rem; }

.event-card-excerpt {
    margin: 0.4rem 0 0;
    color: var(--c-text-muted);
    font-size: 0.88rem;
    line-height: 1.5;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* The zone the event is in, named next to its time. Quiet, but never absent —
   an event without its zone is an event somebody turns up an hour late to. */
.event-zone {
    font-size: 0.74rem;
    font-weight: 600;
    color: var(--c-text-muted);
    margin-left: 0.25rem;
}

.event-drafts {
    padding: 1rem 1.1rem 0.2rem;
    margin-bottom: 1.5rem;
    border: 1px dashed var(--c-border);
    border-radius: var(--radius);
    background: var(--c-bg);
}

.event-drafts h3 { margin: 0; font-size: 1rem; }
.event-drafts > .form-hint { margin-bottom: 0.9rem; }

/* ---- one event ---- */

.event-page, .event-rsvp, .event-comments {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    margin-bottom: 1.5rem;
    overflow: hidden;
}

.event-cover img { display: block; width: 100%; height: auto; }
.event-head { padding: 1.4rem 1.5rem; }
.event-head h1 { margin: 0 0 0.4rem; font-size: 1.7rem; line-height: 1.25; }

.event-over {
    margin: 0 0 0.9rem;
    font-size: 0.85rem;
    color: var(--c-text-muted);
}

.event-facts { display: grid; gap: 0.7rem; margin-top: 1rem; }

.event-fact {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr);
    gap: 0.6rem;
    align-items: baseline;
}

.event-fact-label {
    font-size: 0.76rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--c-text-muted);
    font-weight: 650;
}

.event-fact-value { font-size: 0.95rem; }

/* The reader's own time, under the event's. Quieter, because the event's zone
   is the one that decides when to turn up. */
.event-fact-yours {
    grid-column: 2;
    font-size: 0.8rem;
    color: var(--c-text-muted);
}

.event-actions { display: flex; gap: 0.5rem; align-items: center; margin-top: 1.1rem; }
.event-body { padding: 0 1.5rem 1.5rem; line-height: 1.7; }

/* ---- coming ---- */

.event-rsvp { padding: 1.2rem 1.4rem; }

.event-rsvp-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 1rem;
    margin-bottom: 0.9rem;
}

.event-rsvp-head h3 { margin: 0; font-size: 1.05rem; }
.event-rsvp-count { font-size: 0.85rem; color: var(--c-text-muted); }
.event-rsvp-form { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1rem; }

.event-attendees { margin-top: 1rem; }
.event-attendees h4 { margin: 0 0 0.5rem; font-size: 0.85rem; color: var(--c-text-muted); font-weight: 650; }
.event-attendee-count { color: var(--c-text); }

.event-attendee-list { display: flex; flex-wrap: wrap; gap: 0.5rem; }

.event-attendee {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.3rem 0.6rem 0.3rem 0.3rem;
    border-radius: 999px;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    font-size: 0.82rem;
    color: var(--c-text);
}

.event-attendee:hover { border-color: var(--c-primary); text-decoration: none; }

/* ---- comments ---- */

.event-comments { padding: 1.2rem 1.4rem; }
.event-comments > h3 { margin: 0 0 1rem; font-size: 1.05rem; }

.event-comment {
    display: flex;
    gap: 0.8rem;
    padding: 0.9rem 0;
    border-top: 1px solid var(--c-border);
}

.event-comment-main { min-width: 0; flex: 1; }

.event-comment-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.8rem;
    color: var(--c-text-muted);
    margin-bottom: 0.25rem;
}

.event-comment-author { font-weight: 650; color: var(--c-text); }
.event-comment-body { line-height: 1.6; }
.event-comment-form { margin-top: 1.2rem; }

.event-compose .form-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
    gap: 1rem;
}

@media (max-width: 900px) {
    .event-layout { grid-template-columns: minmax(0, 1fr); }
    .event-fact { grid-template-columns: minmax(0, 1fr); gap: 0.15rem; }
    .event-fact-yours { grid-column: 1; }
}

/* ---- Custom database layouts ---------------------------------------------

   🚨 Shapes chosen from a list, not templates typed into a form. Convoro will
   not ship operator-authored server templates: a template is code, and code
   entered through an admin panel makes a compromised admin account a shell.
   Reasoning in docs/design/2026-08-08-pages-as-a-cms.md.
*/

/* Cards rather than rows, when the operator picked that shape. */
.db-list-cards {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 1rem;
}

.db-list-cards .db-card {
    flex-direction: column;
    align-items: flex-start;
}

/* A table scrolls inside its own container. A page that scrolls sideways
   because one column is wide is a page nobody can read on a phone. */
.db-table-wrap { overflow-x: auto; }

.db-table {
    width: 100%;
    border-collapse: collapse;
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    overflow: hidden;
}

.db-table th,
.db-table td {
    padding: 0.7rem 0.9rem;
    text-align: left;
    border-bottom: 1px solid var(--c-border);
    font-size: 0.9rem;
}

.db-table th {
    background: var(--c-bg);
    font-weight: 650;
    font-size: 0.8rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
    white-space: nowrap;
}

.db-table tr:last-child td { border-bottom: 0; }

/* ---- one record ---- */

/* The article layout: body first, facts beside it. */
.db-record-article {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 260px;
    gap: 1.5rem;
    align-items: start;
}

/* The profile layout: facts first, body underneath. Same two blocks, one
   column, and the include order in the template is what puts them that way. */
.db-record-profile,
.db-record-plain {
    display: block;
}

.db-record-profile .db-facts { margin-bottom: 1.2rem; }

.db-record-body { min-width: 0; }

.db-facts {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    padding: 1rem 1.1rem;
    display: grid;
    gap: 0.7rem;
}

.db-fact { display: grid; gap: 0.1rem; }

.db-fact-label {
    font-size: 0.74rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--c-text-muted);
    font-weight: 650;
}

.db-fact-value { font-size: 0.92rem; overflow-wrap: anywhere; }

/* ---- record comments ---- */

.db-comments {
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    padding: 1.2rem 1.4rem;
    margin-top: 1.5rem;
}

.db-comments > h3 { margin: 0 0 1rem; font-size: 1.05rem; }

.db-comment {
    display: flex;
    gap: 0.8rem;
    padding: 0.9rem 0;
    border-top: 1px solid var(--c-border);
}

.db-comment-main { min-width: 0; flex: 1; }

.db-comment-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.8rem;
    color: var(--c-text-muted);
    margin-bottom: 0.25rem;
}

.db-comment-author { font-weight: 650; color: var(--c-text); }
.db-comment-body { line-height: 1.6; }
.db-comment-form { margin-top: 1.2rem; }

/* ---- categories ---- */

.db-categories {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-bottom: 1.2rem;
}

.db-category {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.35rem 0.8rem;
    border-radius: 999px;
    border: 1px solid var(--c-border);
    background: var(--c-surface);
    color: var(--c-text);
    font-size: 0.85rem;
}

.db-category:hover { border-color: var(--c-primary); text-decoration: none; }

.db-category.is-on {
    background: var(--c-primary);
    border-color: var(--c-primary);
    color: #fff;
}

.db-category-count { font-size: 0.74rem; opacity: 0.75; }
.db-category-description { color: var(--c-text-muted); margin: -0.6rem 0 1.2rem; }

.db-category-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.5rem;
    flex-wrap: wrap;
}

@media (max-width: 900px) {
    /* The facts panel goes under the body rather than squeezing it. */
    .db-record-article { grid-template-columns: minmax(0, 1fr); }
}

/* ---- Gallery -------------------------------------------------------------

   The seventh application, and the only one whose content is files. Almost
   everything below is about one problem the other six do not have: a page that
   asks for forty pictures at once must not shift under somebody while they
   arrive. Reasoning in docs/design/2026-08-08-the-gallery.md.
*/

.gallery-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 280px;
    gap: 1.5rem;
    align-items: start;
}

.gallery-main { min-width: 0; }
.gallery-side { min-width: 0; }

.gallery-description {
    color: var(--c-text-muted);
    margin: -0.4rem 0 1.2rem;
}

.gallery-section-heading {
    font-size: 0.82rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
    margin: 1.6rem 0 0.7rem;
}

/* Albums and images share the grid so a page of both reads as one thing. */
.gallery-albums,
.gallery-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
    gap: 0.9rem;
}

.gallery-albums { margin-bottom: 0.6rem; }

.gallery-tile,
.gallery-album-card {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    text-decoration: none;
    color: inherit;
    min-width: 0;
}

/*
   🚨 The frame holds the space before the picture arrives.

   A fixed aspect ratio plus object-fit is what stops a grid of forty
   thumbnails reflowing forty times as they load. The <img> also carries its
   real width and height, so the reservation is right even with CSS disabled.
*/
.gallery-tile-frame,
.gallery-album-frame {
    display: block;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: var(--radius);
    border: 1px solid var(--c-border);
    background: var(--c-bg);
}

.gallery-tile-frame img,
.gallery-album-frame img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform 0.18s ease;
}

.gallery-tile:hover .gallery-tile-frame img,
.gallery-album-card:hover .gallery-album-frame img { transform: scale(1.03); }

.gallery-tile:hover .gallery-tile-title,
.gallery-album-card:hover .gallery-album-title { color: var(--c-primary); }

/* An album with nothing in it yet, rather than a broken picture. */
.gallery-album-blank {
    display: block;
    width: 100%;
    height: 100%;
    background: repeating-linear-gradient(
        45deg,
        var(--c-bg),
        var(--c-bg) 8px,
        var(--c-surface) 8px,
        var(--c-surface) 16px
    );
}

.gallery-tile-title,
.gallery-album-title {
    font-size: 0.86rem;
    font-weight: 600;
    line-height: 1.3;
    /* One line, cut cleanly — a long filename must not push the grid about. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.gallery-tile-meta,
.gallery-album-count {
    font-size: 0.75rem;
    color: var(--c-text-muted);
}

/* ---- one picture ---- */

.gallery-view-frame {
    border-radius: var(--radius);
    border: 1px solid var(--c-border);
    background: var(--c-bg);
    overflow: hidden;
    text-align: center;
}

.gallery-view-frame img {
    /*
       Bounded by the viewport height as well as the width, so a tall portrait
       photograph does not need scrolling past to reach anything under it.
    */
    max-width: 100%;
    max-height: 78vh;
    width: auto;
    height: auto;
    display: block;
    margin: 0 auto;
}

.gallery-view-head { margin: 1rem 0 0.4rem; }
.gallery-view-head h1 { margin: 0 0 0.6rem; font-size: 1.4rem; }

.gallery-view-meta {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    flex-wrap: wrap;
}

.gallery-view-author { font-weight: 600; text-decoration: none; }
.gallery-view-when { font-size: 0.78rem; color: var(--c-text-muted); }

.gallery-view-actions {
    margin-left: auto;
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.gallery-view-caption {
    margin: 0.8rem 0 0;
    color: var(--c-text);
    line-height: 1.6;
}

.gallery-view-edit { margin-top: 1rem; }
.gallery-view-edit summary {
    cursor: pointer;
    font-size: 0.82rem;
    color: var(--c-text-muted);
}
.gallery-view-edit form { margin-top: 0.8rem; }

.gallery-neighbours {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    margin: 1.2rem 0;
    flex-wrap: wrap;
}

.gallery-upload { max-width: 640px; }

/* ---- comments ---- */

.gallery-comments { margin-top: 2rem; }

.gallery-comment {
    display: flex;
    gap: 0.8rem;
    padding: 0.9rem 0;
    border-top: 1px solid var(--c-border);
}

.gallery-comment-main { min-width: 0; flex: 1; }

.gallery-comment-head {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    font-size: 0.8rem;
    color: var(--c-text-muted);
    margin-bottom: 0.3rem;
}

.gallery-comment-author { font-weight: 600; color: var(--c-text); }
.gallery-comment-form { margin-top: 1.2rem; }

/* ---- the widgets ---- */

.widget-thumbs {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.35rem;
}

.widget-thumb {
    display: block;
    aspect-ratio: 1 / 1;
    overflow: hidden;
    border-radius: calc(var(--radius) / 2);
}

.widget-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }

@media (max-width: 900px) {
    .gallery-layout { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 560px) {
    /* Two across rather than one: a phone gallery of one picture per screen
       is a gallery nobody scrolls to the end of. */
    .gallery-albums,
    .gallery-grid { grid-template-columns: repeat(2, 1fr); gap: 0.6rem; }

    .gallery-view-actions { margin-left: 0; width: 100%; }
}

/* ---- Clubs ---------------------------------------------------------------

   The eighth application. A club is a group — see
   docs/design/2026-08-08-clubs.md — so almost nothing here is about
   permissions; that is all handled a layer down. This is a directory, a
   member list, and the panels a leader runs it from.
*/

.club-layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 300px;
    gap: 1.5rem;
    align-items: start;
}

.club-main { min-width: 0; }
.club-side { min-width: 0; }

.club-description {
    color: var(--c-text-muted);
    margin: -0.4rem 0 1.2rem;
}

.club-section-heading {
    font-size: 0.82rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
    margin: 1.4rem 0 0.7rem;
}

.club-section-heading:first-child { margin-top: 0; }

.club-grid {
    display: grid;
    /*
     * 🚨 260px, not 220. A club card carries a cover, an icon, a description
     * and a row of faces — at 220px that is five things in a column the width
     * of a phone, on a desktop. The card has to be big enough to be worth
     * having a picture on.
     */
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 0.9rem;
}

/*
 * 🚨 The card is no longer an <a>. It contains one — the cover and title — and
 * a row of member avatars, and a link inside a link is invalid HTML that every
 * browser handles differently. The whole tile still looks and hovers as one.
 */
.club-card {
    position: relative;
    display: flex;
    flex-direction: column;
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    background: var(--c-surface);
    color: inherit;
    min-width: 0;
    overflow: hidden;
    transition: border-color 0.15s ease;
}

.club-card:hover { border-color: var(--c-border-strong); }
.club-card:hover .club-card-name { text-decoration: underline; }

/* ---- the cover, and the title over it ---- */

.club-card-head {
    position: relative;
    display: block;
    min-height: 96px;
    padding: 0.75rem 0.9rem 0.6rem;
    background-color: var(--c-surface-alt, #2f3a45);
    background-size: cover;
    background-position: center;
    text-decoration: none;
    color: #fff;
}

/*
 * 🚨 Always present, never conditional on the image being "dark enough". A
 * title over somebody's holiday photograph is unreadable about half the time
 * and which half depends on the photograph, so there is always a gradient.
 */
.club-card-scrim {
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgb(0 0 0 / 72%) 0%, rgb(0 0 0 / 28%) 55%, rgb(0 0 0 / 8%) 100%);
    pointer-events: none;
}

/* No cover: a flat surface rather than a gradient over nothing. */
.club-card-plain .club-card-head { background-image: none; }
.club-card-plain .club-card-scrim { background: linear-gradient(to top, rgb(0 0 0 / 45%), rgb(0 0 0 / 10%)); }

.club-card-icon {
    position: relative;
    display: block;
    width: 46px;
    height: 46px;
    border-radius: 12px;
    overflow: hidden;
    /* Reads as an object sitting on the cover rather than a hole cut in it. */
    box-shadow: 0 0 0 2px rgb(255 255 255 / 85%), 0 1px 4px rgb(0 0 0 / 35%);
    background: var(--c-surface);
}

.club-card-icon img { width: 100%; height: 100%; object-fit: cover; display: block; }

.club-card-initials {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-weight: 700;
    font-size: 1.05rem;
    color: #fff;
}

.club-card-name {
    position: relative;
    display: block;
    margin-top: 0.55rem;
    font-weight: 650;
    font-size: 1.02rem;
    line-height: 1.25;
    color: #fff;
    /* One more layer of separation from a busy photograph. */
    text-shadow: 0 1px 3px rgb(0 0 0 / 55%);
    overflow-wrap: anywhere;
}

.club-card-body { padding: 0.7rem 0.9rem 0.75rem; display: flex; flex-direction: column; gap: 0.35rem; }

.club-card-type { font-weight: 600; }
.club-card-type-private { color: var(--c-danger, #b4443a); }
.club-card-type-closed { color: var(--c-warning, #9a6b1f); }

.club-card-dot { opacity: 0.5; }

/* ---- who is in it ---- */

.club-card-faces {
    display: flex;
    align-items: center;
    padding: 0 0.9rem 0.85rem;
    min-height: 1.5rem;
}

/* Overlapped, so ten faces read as a group rather than as a list. */
.club-card-faces .avatar { margin-right: -6px; box-shadow: 0 0 0 2px var(--c-surface); }

.club-card-more {
    margin-left: 12px;
    font-size: 0.74rem;
    color: var(--c-text-muted);
}

.club-card-badge {
    position: absolute;
    top: 0.55rem;
    right: 0.6rem;
    width: 22px;
    height: 22px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 0.78rem;
    font-weight: 700;
    color: #fff;
    background: var(--c-success, #3f8f4f);
    box-shadow: 0 0 0 2px rgb(255 255 255 / 80%);
}

.club-card-description {
    font-size: 0.84rem;
    color: var(--c-text-muted);
    /* Two lines, so one long description cannot make its card twice the
       height of everything beside it. */
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.club-card-meta {
    font-size: 0.76rem;
    color: var(--c-text-muted);
    display: flex;
    align-items: center;
    gap: 0.3rem;
    flex-wrap: wrap;
}

.club-card-description-empty { font-style: italic; opacity: 0.75; }

.club-card-mine { border-color: var(--c-primary); }

/* ---- editing a club's pictures ---- */

.club-image-field { display: flex; align-items: center; gap: 0.7rem; }
.club-image-preview { border-radius: 6px; border: 1px solid var(--c-border); display: block; }
.club-image-preview-icon { width: 44px; height: 44px; object-fit: cover; }
.club-image-preview-cover { width: 110px; height: 38px; object-fit: cover; }
.club-image-remove { display: block; margin-top: 0.3rem; }

/* ---- inside a club ---- */

.club-contents { list-style: none; margin: 0; padding: 0; }

.club-contents li {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.6rem;
    padding: 0.65rem 0;
    border-bottom: 1px solid var(--c-border);
}

.club-contents li:last-child { border-bottom: 0; }
.club-contents-kind { font-size: 0.74rem; color: var(--c-text-muted); }

.club-add { margin-top: 1.2rem; }
.club-add summary { cursor: pointer; font-size: 0.84rem; color: var(--c-text-muted); }
.club-add form { margin-top: 0.9rem; max-width: 420px; }

/* ---- the people ---- */

.club-members { list-style: none; margin: 0; padding: 0; }

.club-members li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.4rem 0;
    flex-wrap: wrap;
}

.club-member-name {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
    min-width: 0;
}

.club-member-actions,
.club-request-actions {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    flex-wrap: wrap;
}

.club-request {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.5rem;
    padding: 0.4rem 0;
    flex-wrap: wrap;
}

@media (max-width: 900px) {
    .club-layout { grid-template-columns: minmax(0, 1fr); }
}

@media (max-width: 560px) {
    .club-grid { grid-template-columns: minmax(0, 1fr); }
}

/* ============================================================================
   AI Assistant
   ============================================================================

   Reasoning is in docs/design/2026-08-08-the-assistant.md.
   ========================================================================= */

.assistant-intro {
    color: var(--c-text-muted);
    margin-bottom: 1.25rem;
    max-width: 60ch;
}

.assistant-signin {
    padding: 1.25rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-md);
    background: var(--c-surface);
}

.assistant-signin p { margin: 0 0 0.75rem; color: var(--c-text-muted); }

.assistant-form {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.6rem;
    margin-bottom: 1.5rem;
}

.assistant-form textarea {
    width: 100%;
    padding: 0.7rem 0.85rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-md);
    background: var(--c-surface);
    color: var(--c-text);
    font: inherit;
    resize: vertical;
}

.assistant-form textarea:focus {
    outline: none;
    border-color: var(--c-primary);
}

.assistant-thread {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
}

.assistant-asked {
    align-self: flex-end;
    max-width: 85%;
    padding: 0.6rem 0.9rem;
    border-radius: var(--radius-md);
    background: var(--c-primary);
    color: #fff;
}

.assistant-waiting { color: var(--c-text-muted); font-size: 0.9rem; }

.assistant-answer {
    padding: 0.9rem 1rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-md);
    background: var(--c-surface);
    /*
     * The answer arrives as plain text and is inserted as text, never as
     * markup — so line breaks have to survive in CSS rather than in HTML.
     * That is also what keeps a model's output from being able to write
     * anything into this page.
     */
    white-space: pre-wrap;
    line-height: 1.6;
}

.assistant-problem {
    padding: 0.7rem 0.9rem;
    border: 1px solid var(--c-danger, #c0392b);
    border-radius: var(--radius-md);
    color: var(--c-danger, #c0392b);
    font-size: 0.9rem;
}

.assistant-sources {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    font-size: 0.85rem;
}

.assistant-sources-label { color: var(--c-text-muted); }

.assistant-sources a {
    padding: 0.25rem 0.7rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-pill);
    background: var(--c-surface);
    text-decoration: none;
}

.assistant-sources a:hover { border-color: var(--c-primary); text-decoration: none; }


/* ============================================================================
   Convoro's own marketing home page
   ============================================================================

   Built by `homepage:seed` as html blocks. Everything is prefixed `cv-` so it
   cannot collide with markup an operator writes into a page of their own.

   🚨 No core variable is redefined here. Every colour is derived from the
   theme's own `--c-*` values, so this page follows the site into light or dark
   rather than forcing a look. A landing page that ignores the theme is the
   first thing anybody notices is bolted on.
   ========================================================================= */

.cv-hero {
    position: relative;
    padding: 4.5rem 1.5rem 2.5rem;
    text-align: center;
    overflow: hidden;
}

/* The light behind the headline. Decoration only, hence aria-hidden. */
.cv-glow {
    position: absolute;
    inset: -30% -10% auto -10%;
    height: 42rem;
    pointer-events: none;
    background:
        radial-gradient(45rem 24rem at 50% 18%, color-mix(in oklab, var(--c-primary) 26%, transparent), transparent 70%),
        radial-gradient(30rem 20rem at 18% 8%, color-mix(in oklab, var(--c-primary) 14%, transparent), transparent 70%),
        radial-gradient(26rem 18rem at 84% 4%, color-mix(in oklab, var(--c-primary) 12%, transparent), transparent 70%);
    filter: blur(6px);
}

.cv-hero-inner { position: relative; max-width: 52rem; margin: 0 auto; }

.cv-eyebrow {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.32rem 0.85rem;
    margin: 0 0 1.4rem;
    border: 1px solid var(--c-border);
    border-radius: 999px;
    background: color-mix(in oklab, var(--c-surface) 70%, transparent);
    font-size: 0.76rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--c-text-muted);
}

.cv-dot {
    width: 0.45rem;
    height: 0.45rem;
    border-radius: 50%;
    background: #2ecc71;
    box-shadow: 0 0 0 3px color-mix(in oklab, #2ecc71 25%, transparent);
}

.cv-hero h1 {
    margin: 0 0 1.1rem;
    font-size: clamp(2.3rem, 6.2vw, 4rem);
    line-height: 1.04;
    letter-spacing: -0.04em;
    font-weight: 800;
}

.cv-grad {
    background: linear-gradient(100deg, var(--c-primary), color-mix(in oklab, var(--c-primary) 45%, #b06ab3));
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.cv-lede {
    margin: 0 auto 1.9rem;
    max-width: 40rem;
    font-size: 1.09rem;
    line-height: 1.68;
    color: var(--c-text-muted);
}

.cv-cta { display: flex; flex-wrap: wrap; gap: 0.65rem; justify-content: center; }

.cv-btn {
    display: inline-block;
    padding: 0.78rem 1.55rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-md);
    font-weight: 650;
    text-decoration: none;
    color: var(--c-text);
    transition: transform 0.16s ease, box-shadow 0.16s ease, border-color 0.16s ease;
}

.cv-btn:hover { transform: translateY(-1px); border-color: var(--c-primary); text-decoration: none; }

.cv-btn-primary {
    background: var(--c-primary);
    border-color: var(--c-primary);
    color: #fff;
    box-shadow: 0 8px 24px -10px color-mix(in oklab, var(--c-primary) 70%, transparent);
}

.cv-btn-primary:hover { color: #fff; box-shadow: 0 12px 30px -10px color-mix(in oklab, var(--c-primary) 80%, transparent); }
.cv-btn-ghost { background: transparent; }

/*
 * The placeholder download: looks like a button, and is honest about not
 * being one yet. A dead link that looks alive costs trust the first time
 * somebody clicks it.
 */
.cv-btn.is-waiting { cursor: default; opacity: 0.72; box-shadow: none; }
.cv-btn.is-waiting:hover { transform: none; border-color: var(--c-primary); }

/* ---- the product window ---- */

.cv-window {
    max-width: 46rem;
    margin: 3rem auto 0;
    border: 1px solid var(--c-border);
    border-radius: 14px;
    overflow: hidden;
    background: var(--c-surface);
    box-shadow: 0 40px 80px -40px rgba(0, 0, 0, 0.55), 0 2px 8px rgba(0, 0, 0, 0.12);
    text-align: left;
}

.cv-window-bar {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.6rem 0.85rem;
    border-bottom: 1px solid var(--c-border);
    background: color-mix(in oklab, var(--c-surface) 80%, var(--c-bg));
}

.cv-dotb { width: 0.62rem; height: 0.62rem; border-radius: 50%; background: var(--c-border); }

.cv-window-url {
    margin-left: 0.7rem;
    padding: 0.15rem 0.7rem;
    border-radius: 999px;
    background: var(--c-bg);
    font-size: 0.72rem;
    color: var(--c-text-muted);
}

.cv-window-body { padding: 0.9rem; }

.cv-skel-nav {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding-bottom: 0.85rem;
    margin-bottom: 0.5rem;
    border-bottom: 1px solid var(--c-border);
}

.cv-skel { display: inline-block; height: 0.55rem; border-radius: 999px; background: var(--c-border); }
.cv-skel-logo { width: 4.2rem; height: 0.85rem; background: var(--c-primary); opacity: 0.85; }
.cv-skel.w-30 { width: 2rem; }
.cv-skel.w-40 { width: 2.7rem; }
.cv-skel.w-50 { width: 3.4rem; }
.cv-skel.w-60 { width: 40%; }
.cv-skel.w-70 { width: 55%; }
.cv-skel.w-90 { width: 72%; }
.cv-skel-av { width: 1.6rem; height: 1.6rem; border-radius: 50%; background: var(--c-border); margin-left: auto; }

.cv-skel-row { display: flex; align-items: center; gap: 0.7rem; padding: 0.6rem 0.2rem; }
.cv-skel-row .cv-skel-av { margin-left: 0; flex: 0 0 auto; }
.cv-skel-row > span:nth-child(2) { flex: 1; display: flex; flex-direction: column; gap: 0.35rem; }
.cv-skel-row b, .cv-skel-row i { display: block; height: 0.5rem; border-radius: 999px; background: var(--c-border); }
.cv-skel-row i { opacity: 0.55; height: 0.4rem; }

.cv-skel-pill {
    padding: 0.15rem 0.5rem;
    border-radius: 999px;
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    font-size: 0.7rem;
    color: var(--c-text-muted);
}

.cv-hero-note {
    margin: 1.8rem auto 0;
    max-width: 38rem;
    font-size: 0.86rem;
    color: var(--c-text-muted);
}

/* ---- the numbers ---- */

.cv-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 1px;
    max-width: 64rem;
    margin: 2.5rem auto 0;
    background: var(--c-border);
    border: 1px solid var(--c-border);
    border-radius: var(--radius-md);
    overflow: hidden;
}

.cv-stats > div { padding: 1.15rem 1rem; text-align: center; background: var(--c-bg); }
.cv-stats b { display: block; font-size: 1.7rem; font-weight: 800; letter-spacing: -0.03em; }
.cv-stats span { font-size: 0.82rem; color: var(--c-text-muted); }

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

.cv-section { padding: 3.5rem 1.5rem; }
.cv-section-alt { background: var(--c-surface); border-block: 1px solid var(--c-border); }

.cv-section h2 {
    margin: 0 0 0.65rem;
    text-align: center;
    font-size: clamp(1.6rem, 3.6vw, 2.35rem);
    letter-spacing: -0.03em;
    font-weight: 800;
}

.cv-section-lede {
    margin: 0 auto 2.2rem;
    max-width: 40rem;
    text-align: center;
    color: var(--c-text-muted);
    line-height: 1.65;
}

.cv-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(245px, 1fr));
    gap: 1rem;
    max-width: 68rem;
    margin: 0 auto;
}

.cv-grid-3 { max-width: 64rem; }

.cv-card {
    position: relative;
    padding: 1.35rem;
    border: 1px solid var(--c-border);
    border-radius: var(--radius-md);
    background: var(--c-bg);
    transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease;
}

.cv-card:hover {
    transform: translateY(-3px);
    border-color: color-mix(in oklab, var(--c-primary) 55%, var(--c-border));
    box-shadow: 0 18px 34px -22px rgba(0, 0, 0, 0.5);
}

.cv-card h3 { margin: 0 0 0.45rem; font-size: 1.04rem; letter-spacing: -0.01em; }
.cv-card p { margin: 0; font-size: 0.92rem; line-height: 1.62; color: var(--c-text-muted); }

/* ---- the closing band ---- */

.cv-final {
    position: relative;
    padding: 4rem 1.5rem 4.5rem;
    text-align: center;
    overflow: hidden;
    background:
        radial-gradient(40rem 20rem at 50% 120%, color-mix(in oklab, var(--c-primary) 20%, transparent), transparent 70%),
        var(--c-surface);
    border-top: 1px solid var(--c-border);
}

.cv-final-inner { position: relative; max-width: 44rem; margin: 0 auto; }

.cv-final h2 {
    margin: 0 0 0.6rem;
    font-size: clamp(1.7rem, 4vw, 2.5rem);
    letter-spacing: -0.03em;
    font-weight: 800;
}

/* ---- smaller screens ---- */

@media (max-width: 640px) {
    .cv-hero { padding: 2.75rem 1rem 1.5rem; }
    .cv-section, .cv-final { padding: 2.25rem 1rem; }
    .cv-window { margin-top: 2rem; }
    .cv-stats { margin-top: 1.75rem; }
}

/*
 * 🚨 Somebody who has asked their machine for less movement gets none. Every
 * lift and shadow here is decoration, and decoration is precisely what that
 * setting is about.
 */
@media (prefers-reduced-motion: reduce) {
    .cv-btn, .cv-card { transition: none; }
    .cv-btn:hover, .cv-card:hover { transform: none; }
}

/* =========================================================================
   Installed as an app: pull to refresh, and the offline page.
   Added by the Pwa module. Both are invisible in a normal browser tab.
   ========================================================================= */

/*
 * 🚨 The indicator is fixed and starts ABOVE the viewport, then is translated
 * down by the finger. Animating `top` instead would lay out the page on every
 * touchmove — sixty times a second, on a phone, during the one interaction
 * where jank is unmissable.
 */
.pwa-pull {
    position: fixed;
    top: -44px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 900;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--cv-surface, #fff);
    box-shadow: 0 2px 10px rgb(0 0 0 / 18%);
    opacity: 0;
    pointer-events: none;
}

.pwa-pull.is-pulling { opacity: 1; }

.pwa-pull-spinner {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 2px solid var(--cv-border, #d9dee5);
    border-top-color: var(--cv-accent, #2c5aa0);
}

/* Armed: let go and it reloads. The colour change is the only feedback a
   finger covering the indicator can still see at the edges. */
.pwa-pull.is-armed .pwa-pull-spinner { border-color: var(--cv-accent, #2c5aa0); border-top-color: transparent; }

.pwa-pull.is-refreshing .pwa-pull-spinner { animation: pwa-spin 0.7s linear infinite; }

@keyframes pwa-spin { to { transform: rotate(360deg); } }

/* ---- the offline page ---- */

.cv-offline {
    max-width: 26rem;
    margin: 4rem auto;
    padding: 0 1.25rem;
    text-align: center;
}

.cv-offline-mark { color: var(--cv-text-muted, #6b7683); margin-bottom: 1rem; }
.cv-offline h1 { font-size: 1.5rem; margin: 0 0 0.75rem; }
.cv-offline p { color: var(--cv-text-muted, #6b7683); margin: 0 0 1.25rem; }

@media (prefers-reduced-motion: reduce) {
    .pwa-pull.is-refreshing .pwa-pull-spinner { animation: none; }
}

/* =========================================================================
   Back to top / bottom, and the emoji picker.
   ========================================================================= */

.scroll-jump {
    position: fixed;
    right: 1rem;
    bottom: 1rem;
    z-index: 60;
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
}

/* Both buttons hidden: the container must not sit there catching clicks. */
.scroll-jump.is-empty { pointer-events: none; }

.scroll-jump-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    /* 44px, which is the smallest thing a thumb reliably hits. */
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--c-border);
    background: var(--c-surface);
    color: var(--c-text-muted);
    cursor: pointer;
    box-shadow: 0 2px 10px rgb(0 0 0 / 14%);
    transition: color 0.12s ease, border-color 0.12s ease;
}

.scroll-jump-btn:hover { color: var(--c-primary); border-color: var(--c-primary); }
.scroll-jump-btn[hidden] { display: none; }

/*
 * 🚨 Out of the way of the typing pill, which is fixed to the bottom LEFT —
 * they would otherwise overlap on a narrow screen, and the pill is the one
 * carrying information.
 */
@media (max-width: 640px) {
    .scroll-jump { right: 0.6rem; bottom: 0.6rem; }
    .scroll-jump-btn { width: 40px; height: 40px; }
}

/* ---- emoji picker ---- */

.emoji-panel {
    position: absolute;
    z-index: 300;
    width: 310px;
    max-width: calc(100vw - 16px);
    background: var(--c-surface);
    border: 1px solid var(--c-border);
    border-radius: 10px;
    box-shadow: 0 12px 34px rgb(0 0 0 / 22%);
    padding: 0.55rem;
}

.emoji-panel[hidden] { display: none; }

.emoji-search {
    width: 100%;
    padding: 0.4rem 0.55rem;
    font: inherit;
    font-size: 0.86rem;
    color: var(--c-text);
    background: var(--c-bg);
    border: 1px solid var(--c-border);
    border-radius: 6px;
    margin-bottom: 0.5rem;
}

.emoji-body { max-height: 260px; overflow-y: auto; }

.emoji-group {
    font-size: 0.68rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--c-text-muted);
    margin: 0.35rem 0 0.25rem;
}

.emoji-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 1px; }

.emoji-btn {
    /* No border and no background until hovered: 200 bordered boxes is a
       wall, and the emoji themselves are the only thing worth looking at. */
    border: 0;
    background: none;
    font-size: 1.15rem;
    line-height: 1;
    padding: 0.24rem 0;
    border-radius: 5px;
    cursor: pointer;
}

.emoji-btn:hover, .emoji-btn:focus-visible { background: var(--c-hover); }

.emoji-none {
    font-size: 0.82rem;
    color: var(--c-text-muted);
    padding: 0.6rem 0.2rem;
    margin: 0;
}

/* =========================================================================
   Modal dialogs, and the wider club grid they make room for.
   ========================================================================= */

.cv-dialog {
    width: min(30rem, calc(100vw - 2rem));
    padding: 0;
    border: 1px solid var(--c-border);
    border-radius: 12px;
    background: var(--c-surface);
    color: var(--c-text);
    box-shadow: 0 24px 60px rgb(0 0 0 / 30%);
}

/*
 * 🚨 Styled, because the default is a flat black wash that looks like a bug in
 * dark mode — and ::backdrop does not inherit anything from the page.
 */
.cv-dialog::backdrop { background: rgb(0 0 0 / 45%); }

.cv-dialog-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.9rem 1.1rem;
    border-bottom: 1px solid var(--c-border);
}

.cv-dialog-head h3 { margin: 0; font-size: 1.05rem; }

.cv-dialog-close {
    border: 0;
    background: none;
    color: var(--c-text-muted);
    font-size: 1.5rem;
    line-height: 1;
    padding: 0 0.2rem;
    cursor: pointer;
}

.cv-dialog-close:hover { color: var(--c-text); }

.cv-dialog-body { padding: 1.1rem; }

.cv-dialog-foot {
    display: flex;
    justify-content: flex-end;
    gap: 0.5rem;
    padding: 0.85rem 1.1rem;
    border-top: 1px solid var(--c-border);
    background: var(--c-bg);
}

/*
 * The whole point of moving "Start a club" into a dialog: with no sidebar the
 * grid gets the page, and the cards get room to be cards.
 */
.club-layout.is-wide { display: block; }

/*
 * 🚨 380px, which is three columns on a normal desktop and two on a laptop —
 * not the five that a smaller minimum produces. Reported twice as "still
 * small": removing the sidebar gave the grid the page, and then the grid spent
 * it on more columns instead of bigger cards, which is the same problem
 * wearing a different hat.
 */
.club-layout.is-wide .club-grid { grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 1.1rem; }
.club-layout.is-wide .club-card-head { min-height: 168px; padding: 1rem 1.1rem 0.85rem; }
.club-layout.is-wide .club-card-icon { width: 68px; height: 68px; border-radius: 14px; }
.club-layout.is-wide .club-card-name { font-size: 1.32rem; margin-top: 0.7rem; }
.club-layout.is-wide .club-card-body { padding: 0.9rem 1.1rem 0.8rem; }
.club-layout.is-wide .club-card-description { font-size: 0.9rem; }
.club-layout.is-wide .club-card-faces { padding: 0 1.1rem 1rem; }
.club-layout.is-wide .club-card-faces .avatar { margin-right: -5px; }

/* One column on a phone, where 380px is the whole screen. */
@media (max-width: 560px) {
    .club-layout.is-wide .club-grid { grid-template-columns: minmax(0, 1fr); }
}

/* =========================================================================
   Events: the month, week and day views.
   ========================================================================= */

.event-views { margin-bottom: 1rem; }

.event-view-tabs {
    display: flex;
    gap: 0.15rem;
    border-bottom: 1px solid var(--c-border);
    margin-bottom: 0.75rem;
    overflow-x: auto;
}

.event-view-tab {
    padding: 0.45rem 0.85rem;
    font-size: 0.88rem;
    color: var(--c-text-muted);
    text-decoration: none;
    border-bottom: 2px solid transparent;
    white-space: nowrap;
}

.event-view-tab:hover { color: var(--c-text); }
.event-view-tab.is-active { color: var(--c-primary); border-bottom-color: var(--c-primary); font-weight: 600; }

.event-view-nav { display: flex; align-items: center; gap: 0.5rem; }

.event-nav-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    border: 1px solid var(--c-border);
    border-radius: 6px;
    color: var(--c-text);
    text-decoration: none;
    font-size: 1.1rem;
    line-height: 1;
}

.event-nav-btn:hover { border-color: var(--c-primary); color: var(--c-primary); }

.event-view-heading { font-weight: 600; min-width: 11rem; }
.event-today { font-size: 0.84rem; }

/* ---- month ---- */

.event-month { border: 1px solid var(--c-border); border-radius: var(--radius); overflow: hidden; }

.event-month-head, .event-month-week { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }

.event-month-dow {
    padding: 0.45rem 0.5rem;
    font-size: 0.74rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
    background: var(--c-bg);
    border-bottom: 1px solid var(--c-border);
}

.event-month-cell {
    min-height: 104px;
    padding: 0.3rem;
    border-right: 1px solid var(--c-border);
    border-bottom: 1px solid var(--c-border);
    min-width: 0;
}

.event-month-week:last-child .event-month-cell { border-bottom: 0; }
.event-month-cell:last-child { border-right: 0; }

/* Days from the neighbouring months: drawn, but clearly not this month. */
.event-month-cell.is-outside { background: var(--c-bg); }
.event-month-cell.is-outside .event-month-date { opacity: 0.45; }

.event-month-cell.is-today { box-shadow: inset 0 0 0 2px var(--c-primary); }

.event-month-date {
    display: inline-block;
    font-size: 0.8rem;
    color: var(--c-text-muted);
    text-decoration: none;
    padding: 0.1rem 0.25rem;
    border-radius: 4px;
}

.event-month-date:hover { background: var(--c-hover); color: var(--c-text); }

/* ---- a single event, in a grid ---- */

.event-chip {
    display: block;
    margin-top: 0.15rem;
    padding: 0.14rem 0.3rem 0.14rem 0.4rem;
    /* The calendar's own colour, set inline because it is data. */
    border-left: 3px solid var(--c-primary);
    border-radius: 3px;
    background: var(--c-bg);
    font-size: 0.76rem;
    line-height: 1.3;
    color: var(--c-text);
    text-decoration: none;
    /* 🚨 One line. A long title must not make one cell taller than the six
       beside it, which is what turns a month grid into a staircase. */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.event-chip:hover { background: var(--c-hover); }
.event-chip-time { color: var(--c-text-muted); margin-right: 0.25rem; }

/* ---- week ---- */

.event-week {
    display: grid;
    grid-template-columns: repeat(7, minmax(0, 1fr));
    gap: 0.4rem;
}

.event-week-day {
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    padding: 0.4rem;
    min-height: 130px;
    min-width: 0;
}

.event-week-day.is-today { border-color: var(--c-primary); }

.event-week-head {
    display: block;
    font-size: 0.78rem;
    font-weight: 600;
    color: var(--c-text-muted);
    text-decoration: none;
    margin-bottom: 0.3rem;
}

.event-week-head:hover { color: var(--c-primary); }
.event-week-empty { color: var(--c-text-muted); font-size: 0.8rem; margin: 0; }

/*
 * 🚨 Both grids scroll sideways rather than squashing on a phone. Seven
 * columns in 360px is seven columns of nothing — an unreadable grid is worse
 * than one you have to push.
 */
@media (max-width: 700px) {
    .event-month, .event-week { overflow-x: auto; }
    .event-month-head, .event-month-week { min-width: 640px; }
    .event-week { min-width: 640px; }
    .event-view-heading { min-width: 0; font-size: 0.9rem; }
}

/* =========================================================================
   Chat.
   ========================================================================= */

.chat {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    gap: 1rem;
    /*
     * 🚨 A fixed height, and the LOG scrolls inside it — not the page. A chat
     * that grows the document pushes the composer off the bottom of the
     * screen, and on a phone that means the keyboard covers the box you are
     * typing into.
     */
    height: calc(100vh - 220px);
    min-height: 420px;
}

.chat-channels {
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    background: var(--c-surface);
    overflow-y: auto;
    padding: 0.4rem;
}

.chat-channels-head {
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--c-text-muted);
    padding: 0.4rem 0.5rem;
}

.chat-channel {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.35rem 0.5rem;
    border-radius: 6px;
    color: var(--c-text);
    text-decoration: none;
    font-size: 0.9rem;
}

.chat-channel:hover { background: var(--c-hover); }
.chat-channel.is-active { background: var(--c-hover); font-weight: 600; color: var(--c-primary); }
.chat-channel-hash { color: var(--c-text-muted); }
.chat-channel-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chat-channel-lock { margin-left: auto; font-size: 0.75rem; opacity: 0.6; }

.chat-main {
    display: flex;
    flex-direction: column;
    min-width: 0;
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    background: var(--c-surface);
    overflow: hidden;
}

.chat-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.7rem 0.9rem;
    border-bottom: 1px solid var(--c-border);
}

.chat-title { margin: 0; font-size: 1.02rem; }
.chat-topic { margin: 0.15rem 0 0; font-size: 0.82rem; color: var(--c-text-muted); }
.chat-search { max-width: 15rem; padding: 0.3rem 0.5rem; font: inherit; font-size: 0.84rem;
               border: 1px solid var(--c-border); border-radius: 6px;
               background: var(--c-bg); color: var(--c-text); }

.chat-pinned {
    padding: 0.5rem 0.9rem;
    background: var(--c-bg);
    border-bottom: 1px solid var(--c-border);
    font-size: 0.82rem;
}

.chat-pinned-label { font-weight: 600; margin-right: 0.4rem; color: var(--c-text-muted); }
.chat-pinned-item { color: var(--c-text-muted); }

.chat-log { flex: 1; overflow-y: auto; padding: 0.6rem 0.9rem; }

.chat-message { padding: 0.18rem 0; }
.chat-message:hover { background: var(--c-hover); border-radius: 6px; }
.chat-message.is-pinned { border-left: 3px solid var(--c-primary); padding-left: 0.4rem; }
.chat-message.is-deleted { opacity: 0.6; }

.chat-message-row { display: flex; gap: 0.5rem; align-items: flex-start; }
.chat-message-body { min-width: 0; flex: 1; }
.chat-message-meta { display: flex; align-items: baseline; gap: 0.4rem; }
.chat-author { font-weight: 600; font-size: 0.86rem; }
.chat-when, .chat-edited { font-size: 0.72rem; color: var(--c-text-muted); }

.chat-text {
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.45;
    /* Somebody pasting a 400-character word must not widen the column. */
    overflow-wrap: anywhere;
    white-space: pre-wrap;
}

.chat-removed { font-style: italic; color: var(--c-text-muted); }

.chat-reply-to {
    display: flex;
    gap: 0.35rem;
    font-size: 0.76rem;
    color: var(--c-text-muted);
    padding-left: 2.1rem;
    margin-bottom: 0.1rem;
}

.chat-reply-body { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Only on hover, so a channel is a conversation and not a wall of buttons. */
.chat-tools { display: flex; gap: 0.15rem; opacity: 0; transition: opacity 0.1s ease; }
.chat-message:hover .chat-tools, .chat-tools:focus-within { opacity: 1; }

.chat-tool {
    border: 0;
    background: none;
    color: var(--c-text-muted);
    cursor: pointer;
    font-size: 0.85rem;
    padding: 0.1rem 0.25rem;
    border-radius: 4px;
}

.chat-tool:hover { background: var(--c-surface); color: var(--c-text); }
.chat-tool-danger:hover { color: var(--c-danger, #b4443a); }

.chat-notice { font-size: 0.8rem; color: var(--c-text-muted); text-align: center; padding: 0.4rem; margin: 0; }
.chat-typing { padding: 0 0.9rem 0.3rem; font-size: 0.76rem; color: var(--c-text-muted); min-height: 1rem; }

.chat-composer { border-top: 1px solid var(--c-border); padding: 0.6rem 0.9rem 0.7rem; }
.chat-composer-row { display: flex; gap: 0.4rem; align-items: flex-end; }

.chat-input {
    flex: 1;
    resize: none;
    font: inherit;
    font-size: 0.9rem;
    padding: 0.45rem 0.6rem;
    border: 1px solid var(--c-border);
    border-radius: 8px;
    background: var(--c-bg);
    color: var(--c-text);
    max-height: 140px;
}

.chat-emoji { border: 0; background: none; font-size: 1.1rem; cursor: pointer; padding: 0.3rem; }

.chat-replying {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.78rem;
    color: var(--c-text-muted);
    padding-bottom: 0.35rem;
}

.chat-replying-cancel { border: 0; background: none; cursor: pointer; color: inherit; font-size: 1rem; }
.chat-slow { margin: 0.35rem 0 0; font-size: 0.74rem; color: var(--c-text-muted); }
.chat-readonly { padding: 0.8rem 0.9rem; margin: 0; border-top: 1px solid var(--c-border);
                 font-size: 0.86rem; color: var(--c-text-muted); }

/*
 * 🚨 On a phone the channel list goes away rather than squashing. Two columns
 * in 375px is a list you cannot read beside a conversation you cannot read.
 */
@media (max-width: 720px) {
    .chat { grid-template-columns: minmax(0, 1fr); height: calc(100vh - 160px); }
    .chat-channels { display: none; }
}

/* ---- the chat dock ------------------------------------------------------ */

/*
 * 🚨 Left of the jump buttons, and the offset is derived rather than guessed:
 * they sit at right: 1rem and are 44px wide, so this clears them by 0.55rem.
 * A hardcoded number here drifts the moment those change size.
 */
.chat-dock {
    position: fixed;
    right: calc(1rem + 44px + 0.55rem);
    bottom: 1rem;
    z-index: 70;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.5rem;
}

.chat-dock-button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: 1px solid var(--c-border);
    background: var(--c-surface);
    color: var(--c-text-muted);
    cursor: pointer;
    box-shadow: 0 2px 10px rgb(0 0 0 / 14%);
    transition: color 0.12s ease, border-color 0.12s ease;
}

.chat-dock-button:hover { color: var(--c-primary); border-color: var(--c-primary); }
.chat-dock.is-open .chat-dock-button { color: var(--c-primary); border-color: var(--c-primary); }

.chat-dock-panel {
    width: 360px;
    max-width: calc(100vw - 2rem);
    /* Tall enough to hold a conversation, short enough to leave the page
       visible behind it — this is a dock, not a takeover. */
    height: min(30rem, calc(100vh - 8rem));
    border: 1px solid var(--c-border);
    border-radius: 12px;
    background: var(--c-surface);
    box-shadow: 0 16px 44px rgb(0 0 0 / 26%);
    overflow: hidden;
    display: flex;
}

.chat-dock-panel[hidden] { display: none; }
.chat-dock-loading { padding: 1.2rem; margin: 0; color: var(--c-text-muted); font-size: 0.86rem; }

/*
 * 🚨 The dock reuses the chat layout and drops the channel column. Two columns
 * in 360px would be a list you cannot read beside a conversation you cannot
 * read — the picker in the header does that job instead.
 */
.chat-in-dock { display: block; height: 100%; width: 100%; }
.chat-in-dock .chat-main { height: 100%; border: 0; border-radius: 0; }
.chat-in-dock .chat-log { padding: 0.5rem 0.7rem; }
.chat-in-dock .chat-composer { padding: 0.5rem 0.7rem 0.6rem; }

.chat-dock-head { align-items: center; padding: 0.5rem 0.7rem; }

.chat-dock-picker {
    font: inherit;
    font-size: 0.88rem;
    font-weight: 600;
    padding: 0.2rem 0.3rem;
    border: 1px solid transparent;
    border-radius: 6px;
    background: none;
    color: var(--c-text);
    max-width: 60%;
}

.chat-dock-picker:hover { border-color: var(--c-border); }

.chat-dock-actions { display: flex; align-items: center; gap: 0.2rem; margin-left: auto; }

.chat-dock-expand, .chat-dock-close {
    border: 0;
    background: none;
    color: var(--c-text-muted);
    cursor: pointer;
    text-decoration: none;
    font-size: 1.05rem;
    line-height: 1;
    padding: 0.2rem 0.35rem;
    border-radius: 5px;
}

.chat-dock-expand:hover, .chat-dock-close:hover { background: var(--c-hover); color: var(--c-text); }

@media (max-width: 640px) {
    /* The jump buttons shrink to 40px on a phone; this follows them. */
    .chat-dock { right: calc(0.6rem + 40px + 0.5rem); bottom: 0.6rem; }
    .chat-dock-button { width: 40px; height: 40px; }
    .chat-dock-panel { width: calc(100vw - 1.2rem); height: min(26rem, calc(100vh - 7rem)); }
}

/* ---- screenshots on a listing ------------------------------------------- */

.download-shots {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 0.6rem;
    margin-bottom: 1rem;
}

.download-shot {
    display: block;
    border: 1px solid var(--c-border);
    border-radius: 8px;
    overflow: hidden;
    background: var(--c-bg);
    line-height: 0;
}

.download-shot:hover { border-color: var(--c-primary); }

/*
 * 🚨 A fixed aspect and object-fit, so eight screenshots of eight different
 * shapes make one tidy grid rather than a staircase. The full image is one
 * click away in the lightbox.
 */
/*
 * 🚨 `height: auto` is what makes the aspect-ratio work at all.
 *
 * The template sets width= and height= on the <img> from the stored
 * dimensions, deliberately, so the page reserves the space before the image
 * arrives. But with BOTH attributes present the used height is the attribute,
 * `aspect-ratio` is ignored — it only governs when one dimension is auto — and
 * a 1200x3000 page capture rendered 308x3000 in a 180px column.
 *
 * Reported as "the image needs to be more square, it is too long" and
 * reproduced at exactly that size. The attributes still do their job: they
 * give the browser the ratio to reserve space with before the CSS loads.
 */
.download-shot img {
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 10;
    object-fit: cover;
    display: block;
}

.download-manage { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; margin: 0 0 1rem; }

/* ============================================================
   Bug tracker
   ============================================================ */

/*
 * A section label beside a rule. Used by Downloads and the tracker, and until
 * now never defined — both pages drew the rule and left the label at body
 * weight, which reads as a stray sentence rather than a heading.
 */
.cv-label {
    font-size: 0.78rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
}

.tracker-project {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    padding: 1rem 1.1rem;
    border: 1px solid var(--c-border);
    border-radius: 10px;
    background: var(--c-surface);
    color: inherit;
    text-decoration: none;
}

.tracker-project:hover { border-color: var(--c-primary); }
.tracker-project-title { font-weight: 600; color: var(--c-primary); }
.tracker-project-desc { color: var(--c-text-muted); font-size: 0.9rem; }
.tracker-project-count { margin-top: 0.4rem; font-size: 0.82rem; color: var(--c-text-muted); }

.tracker-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
    margin: 0 0 1rem;
}

.tracker-list {
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--c-border);
    border-radius: 10px;
    overflow: hidden;
    background: var(--c-surface);
}

/*
 * 🚨 Status first, then the title, then the count on the right. A tracker row
 * is scanned down the left edge — "what state is this in" is the question
 * being asked of fifty rows at once, and a badge that moves with the title
 * length cannot be scanned at all.
 */
.tracker-row {
    display: grid;
    grid-template-columns: 8.5rem minmax(0, 1fr) auto;
    grid-template-areas: "status title votes" "status meta votes";
    align-items: center;
    gap: 0.1rem 0.8rem;
    padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--c-border);
}

.tracker-row:last-child { border-bottom: 0; }
.tracker-row:hover { background: var(--c-hover); }

.tracker-row .tracker-status { grid-area: status; }
.tracker-row-title { grid-area: title; font-weight: 600; text-decoration: none; color: var(--c-text); }
.tracker-row-title:hover { color: var(--c-primary); }
.tracker-row-meta { grid-area: meta; font-size: 0.82rem; color: var(--c-text-muted); }

.tracker-row-votes {
    grid-area: votes;
    align-self: center;
    min-width: 2.4rem;
    padding: 0.25rem 0.5rem;
    border: 1px solid var(--c-border);
    border-radius: 999px;
    text-align: center;
    font-size: 0.82rem;
    font-variant-numeric: tabular-nums;
    color: var(--c-text-muted);
}

@media (max-width: 640px) {
    /* The badge goes above the title rather than shrinking the title to nothing. */
    .tracker-row {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "status votes" "title title" "meta meta";
        gap: 0.25rem 0.6rem;
    }
}

.tracker-status {
    display: inline-block;
    padding: 0.15rem 0.55rem;
    border-radius: 999px;
    font-size: 0.75rem;
    font-weight: 600;
    white-space: nowrap;
    background: var(--c-hover);
    color: var(--c-text-muted);
}

.tracker-status-open { background: var(--c-info-chip); color: var(--c-info-chip-ink); }
.tracker-status-confirmed { background: var(--c-warning-chip); color: var(--c-warning-chip-ink); }
.tracker-status-fixed { background: var(--c-success-chip); color: var(--c-success-chip-ink); }

/*
 * 🚨 Being worked on is amber like confirmed, but outlined.
 *
 * "Somebody agrees this is real" and "somebody is fixing it right now" both
 * belong to the same amber family — they are the same queue — but they have to
 * be tellable apart, or the tracker looks like nothing is moving.
 *
 * The outline is `--c-warning-edge` rather than the accent: every `-chip-ink`
 * and `-edge` variable carries a lightened dark-mode value, while the accent is
 * the same colour in both themes and loses its footing on a dark chip.
 */
.tracker-status-in-progress {
    background: var(--c-warning-chip);
    color: var(--c-warning-chip-ink);
    box-shadow: inset 0 0 0 1px var(--c-warning-edge);
}

/*
 * Won't fix, duplicate and cannot-reproduce keep the neutral chip. They are
 * closed without anything having been repaired, and dressing them in the
 * success green would say something that is not true.
 */

.tracker-report {
    border: 1px solid var(--c-border);
    border-radius: 10px;
    background: var(--c-surface);
    padding: 1.1rem 1.2rem;
    margin-bottom: 1.2rem;
}

.tracker-report-head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.6rem; margin-bottom: 0.9rem; }
.tracker-report-body { margin-bottom: 1rem; }
.tracker-report-actions { border-top: 1px solid var(--c-border); padding-top: 0.8rem; }
.tracker-report-actions .is-voted { border-color: var(--c-primary); color: var(--c-primary); }
.tracker-report-actions [data-tracker-votes] { margin-left: 0.35rem; font-variant-numeric: tabular-nums; }

.tracker-triage { margin-bottom: 1.2rem; }
.tracker-triage-form { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 0.5rem; }

.tracker-comment { display: flex; gap: 0.8rem; padding: 0.9rem 0; border-bottom: 1px solid var(--c-border); }
.tracker-comment:last-of-type { border-bottom: 0; }
.tracker-comment-main { min-width: 0; flex: 1; }
.tracker-comment-head { display: flex; gap: 0.5rem; align-items: baseline; margin-bottom: 0.3rem; }
.tracker-comment-head time { font-size: 0.8rem; color: var(--c-text-muted); }

.tracker-reply-form { margin-top: 1.2rem; }
.tracker-file-form .form-row { display: flex; gap: 1rem; flex-wrap: wrap; }
.tracker-file-form .form-row .form-group { flex: 1; min-width: 12rem; }

/* ============================================================
   Classes the templates have been setting against nothing
   ============================================================ */

/*
 * 🚨 The chip shape, which `.file-paid` never had.
 *
 * `.file-paid` set a colour and nothing else, so "Paid" beside a file name
 * rendered as an amber word rather than a badge — on the storefront, which is
 * the one page where "this one costs money" has to be unmissable.
 */
.cv-chip {
    display: inline-block;
    padding: 0.1rem 0.5rem;
    border-radius: 999px;
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1.5;
    white-space: nowrap;
    vertical-align: 0.08em;
}

/*
 * "Page 2 of 5", between the Newer and Older buttons. Four templates set this
 * and nothing styled it, so the count sat at full body weight between two
 * quiet chips and read as the loudest thing in the row.
 */
.pagination-current {
    font-size: 0.85rem;
    color: var(--c-text-muted);
    font-variant-numeric: tabular-nums;
}

/* The line under a page heading that says what you are looking at. */
.page-header-note {
    margin: 0.3rem 0 0;
    font-size: 0.88rem;
    color: var(--c-text-muted);
}

.category-description {
    margin: 0.2rem 0 0;
    font-size: 0.88rem;
    line-height: 1.5;
    color: var(--c-text-muted);
}

/* A breadcrumb chevron is punctuation, not a link. */
.crumb-sep { color: var(--c-text-muted); }

/*
 * 🚨 `min-width: 0` on the growing half of a flex row.
 *
 * A flex item will not shrink below its content by default, so a long file
 * title or a long token name pushes the button beside it off the row instead
 * of wrapping. Both of these sit in `justify-content: space-between` rows and
 * both were left to fend for themselves.
 */
.file-head-main,
.token-row-main {
    flex: 1;
    min-width: 0;
}

/* ============================================================
   Menu headings, and the More overflow menu
   ============================================================ */

/*
 * A heading is one control, not a dead label beside a chevron.
 *
 * `.menu-link-parent` on a link keeps the label clickable and puts the
 * chevron beside it, because taking a real destination away silently would
 * be worse. A heading has no destination to take away, so the label and the
 * chevron are the same button — and it has to be dressed as the link it sits
 * next to, because it is a <button> in a row of <a>s.
 */
.menu-link-heading {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    border: 0;
    background: none;
    font: inherit;
    color: rgba(255, 255, 255, 0.8);
    cursor: pointer;
}

.menu-link-heading:hover { color: #fff; background: rgba(255, 255, 255, 0.1); }

.menu-link-heading:focus-visible {
    outline: 2px solid rgba(255, 255, 255, 0.7);
    outline-offset: 1px;
}

.menu-link-heading .menu-chevron { opacity: 0.75; }

/* The overflow menu is built by script, so it starts life invisible. */
.menu-more[hidden] { display: none; }
.menu-more-panel { right: 0; left: auto; }

/*
 * 🚨 A parent that has been moved INTO the overflow panel stops being a
 * dropdown and becomes a labelled group.
 *
 * The alternative is a submenu inside a submenu, positioned absolutely
 * against a panel that is itself absolutely positioned — which is a menu
 * that opens somewhere off the side of the screen. The items are the real
 * nodes, moved rather than copied, so putting them back on resize is one
 * `insertBefore` and nothing gets out of step.
 */
.menu-more-panel .menu-parent {
    display: block;
    position: static;
}

.menu-more-panel .menu-chevron-btn,
.menu-more-panel .menu-link-heading .menu-chevron { display: none; }

.menu-more-panel .menu-link,
.menu-more-panel .menu-link-heading {
    display: block;
    width: 100%;
    padding: 0.45rem 0.6rem;
    border-radius: 6px;
    text-align: left;
    font-size: 0.88rem;
    font-weight: 500;
    color: var(--c-text);
    white-space: nowrap;
}

.menu-more-panel .menu-link:hover,
.menu-more-panel .menu-link-heading:hover {
    background: var(--c-hover);
    color: var(--c-text);
}

/* A heading inside the panel is a label for the group under it, not something
   to press — the items it holds are right there. */
.menu-more-panel .menu-link-heading {
    font-size: 0.72rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--c-text-muted);
    cursor: default;
    padding-bottom: 0.15rem;
}

.menu-more-panel .menu-link-heading:hover { background: none; color: var(--c-text-muted); }

/*
 * A heading is a label here, not a button — the script takes it out of the tab
 * order to match, and this stops the pointer offering it as something to press.
 *
 * 🚨 `.menu-link-heading`, not `.menu-link-parent`. A *link* with children that
 * ends up in this panel still goes somewhere, and killing its pointer events
 * would take that destination away for no reason other than where it is
 * standing.
 */
.menu-more-panel .menu-link-heading { pointer-events: none; }

.menu-more-panel .menu-submenu {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    min-width: 0;
    padding: 0 0 0.2rem 0.5rem;
    background: none;
    border: 0;
    box-shadow: none;
}

/*
 * 🚨 A link that says it is not underlined stays not underlined.
 *
 * `a:hover { text-decoration: underline }` is global, and most of these are
 * whole cards that happen to be one big <a>: a gallery tile, a club card, a
 * bug tracker project. On hover the underline ran through every line of text
 * inside them — the title, the description, the counts — which is what Ernest
 * saw and said should never happen.
 *
 * `text-decoration: none` on the base rule is the author saying what this link
 * looks like. There is no CSS way to say "and keep looking like that on
 * hover", so it is said here. Anything that DOES want an underline on hover
 * writes its own `:hover` — a footer link and an ordinary link in prose both
 * do, and neither is in this list.
 *
 * StylesheetContractTest fails on the next one that forgets.
 */
.attachment-file:hover,
.chat-channel:hover,
.chat-dock-close:hover,
.chat-dock-expand:hover,
.club-card-head:hover,
.event-chip:hover,
.event-month-date:hover,
.event-nav-btn:hover,
.event-view-tab:hover,
.event-week-head:hover,
.gallery-album-card:hover,
.gallery-tile:hover,
.gallery-view-author:hover,
.header-bell:hover,
.mention:hover,
.notification:hover,
.search-result-item.active:hover,
.tracker-project:hover,
.tracker-row-title:hover,
.widget-topic-list a:hover { text-decoration: none; }

/* A colour picker is a swatch, not a text field.

   `.form-input` gives it a full-width box with a tiny colour chip floating in
   it, which reads as a broken input. Sized like a control instead, and tall
   enough to be a real tap target on a phone. */
.form-input.input-colour {
    width: 4.5rem;
    min-width: 4.5rem;
    height: 2.75rem;
    padding: var(--s-1);
    cursor: pointer;
}

/* Figures that line up.

   🚨 This lived only in admin.css, and ten front-end uses of it on the
   Downloads pages were therefore styled by nothing at all — version numbers
   and download counts in a column, jittering as the digits changed, with no
   error anywhere. It is the same failure `.is-hidden` caused once already: an
   unknown class is not a mistake the browser reports.

   Kept in both files rather than moved, because the admin loads only admin.css
   and the front end loads only this one. */
.cv-tabular { font-variant-numeric: tabular-nums; text-align: right; }

/* ============================================================
   Shapes the templates were drawing with inline styles
   ============================================================

   Each of these existed as a `style="…"` attribute repeated across templates,
   which is how a design system quietly stops being one: the value is invisible
   to the audit, cannot follow a token, and the next person copies whichever
   variant they found. */

/* A control that should not run the whole width of the form. Three widths were
   in use inline — 22rem, 18rem and 9rem — and they are the same three the type
   scale already implies. */
.field-wide { max-width: 22rem; }
.field-mid { max-width: 18rem; }
.field-narrow { max-width: 9rem; }

/* A row of buttons at the end of something, pushed to the right. */
.button-row {
    display: flex;
    gap: var(--s-2);
    justify-content: flex-end;
    flex-wrap: wrap;
}

/* The same, left-aligned and with room above it. */
.button-row-start {
    display: flex;
    gap: var(--s-2);
    margin-top: var(--s-3);
    flex-wrap: wrap;
}

/* The first-run page, before anything claims the front page. It is the first
   thing anybody sees on a new install, so it follows the tokens like
   everything else — its status colours were raw hex. */
.boot-check { text-align: center; padding: var(--s-9) 0; }
.boot-title { font-size: var(--t-h1); margin-bottom: var(--s-4); }
.boot-line { color: var(--c-text-muted); font-size: var(--t-lead); }
.boot-ok { margin-top: var(--s-4); color: var(--c-success); }
.boot-warn { margin-top: var(--s-4); color: var(--c-danger); }

/* ============================================================
   Post layout — the `post_layout` theme token
   ============================================================

   🚨 `.post` is a GRID, not a flex row. There is an older flex rule for it
   near the top of this file left over from the pre-grid layout, and reading
   that one instead is how the first version of this was written: it set
   `flex-direction: column` on a grid, which is inert, and the layout did not
   move at all while every attribute and stylesheet looked correct. Computed
   styles are what found it. The handoff's warning — "when a grid item ignores
   its track, look for an old width" — is the same trap from the other side.

   So these override the TRACKS. Specificity carries them over the
   `@media (min-width: 641px)` rule further up, which is a bare `.post`.

   Below 640px every layout is already a single column, and that stays: a
   12.5rem author gutter on a 375px screen is not a choice worth offering. */

[data-post-layout="inline"] .post,
[data-post-layout="compact"] .post {
    grid-template-columns: minmax(0, 1fr);
}

/* The author column becomes a row across the top. The rule down its right
   side becomes the rule under it, or it reads as a stray vertical line. */
[data-post-layout="inline"] .post-sidebar,
[data-post-layout="compact"] .post-sidebar {
    flex-direction: row;
    align-items: center;
    gap: var(--s-3);
    text-align: left;
    padding: var(--s-3) var(--s-5);
    border-right: 0;
    border-bottom: 1px solid var(--c-border);
}

/* 🚨 `.avatar-lg`, not `.post-avatar`. The post partial renders the shared
   avatar partial, which emits `.avatar .avatar-lg` — `.post-avatar` is another
   pre-grid leftover that nothing sets any more, so the first version of this
   rule styled nothing at all and the 80px avatar stayed 80px in a row built
   for a 36px one. Two rules into this file, two dead selectors: check what the
   markup actually emits, not what the stylesheet still mentions. */
[data-post-layout="inline"] .post-sidebar .avatar-lg,
[data-post-layout="compact"] .post-sidebar .avatar-lg {
    width: 40px;
    height: 40px;
    font-size: var(--t-body);
}

/* 🚨 The name is in the header too, and stacked they read as a stutter —
   "DemoUser ADMIN" directly above "DemoUser ADMIN · 6 days ago". In the column
   layout they are in different places and it reads as a caption; in a row it
   is a repeat. The header keeps it, because it is the one with the timestamp
   and the permalink beside it. */
[data-post-layout="inline"] .post-sidebar .post-author,
[data-post-layout="compact"] .post-sidebar .post-author {
    display: none;
}

/* Inline keeps the rank and badges — the same information, turned ninety
   degrees — but the author facts are a list of pairs and do not read across a
   row, so they lie down. */
[data-post-layout="inline"] .post-author-facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s-3);
    margin: 0;
}

[data-post-layout="inline"] .post-identity,
[data-post-layout="compact"] .post-identity {
    flex-direction: row;
    align-items: center;
    gap: var(--s-2);
}

/* Compact is inline with the furniture dropped: a name, a time, and the
   words. For reading a long thread without the margin. */
[data-post-layout="compact"] .post-author-facts,
[data-post-layout="compact"] .post-badges,
[data-post-layout="compact"] .post-points,
[data-post-layout="compact"] .post-rank {
    display: none;
}

/* 🚨 The moderation checkbox is absolutely positioned at the top LEFT of the
   post, which is empty space in the column layout and directly on top of the
   avatar once the author becomes a row. Found by looking at the page, not by
   any check — nothing overflows and nothing is unclickable, the two sit on top
   of each other.

   The row is indented past it rather than the checkbox being moved, so a
   moderator finds it in the same place whichever layout they are reading in. */
[data-post-layout="inline"] .post-list.is-selectable .post-sidebar,
[data-post-layout="compact"] .post-list.is-selectable .post-sidebar {
    /* The checkbox is 1.9rem at a 0.6rem offset; --s-8 clears it. */
    padding-left: var(--s-8);
}

/* The branding previews and upload zones.

   🚨 These lived only in admin.css until the branding form moved to
   /appearance on 2026-08-10, and moving markup without its rules is precisely
   the failure that left `.cv-tabular` unstyled on the Downloads pages. The
   design audit caught it within the hour — which is the whole reason it was
   put back to a working ceiling first.

   Kept in both files: the admin loads only admin.css, the front end only this. */
.brand-preview {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    margin-bottom: var(--s-2);
    padding: var(--s-3) var(--s-4);
    border: 1px solid var(--c-border);
    border-radius: var(--radius);
    background-color: var(--c-header-bg);
}

.brand-preview img {
    /* Tall enough for a wordmark, short enough that the card does not become
       the logo. The admin's copy uses the same two numbers. */
    max-height: 3.5rem;
    max-width: 100%;
    width: auto;
    display: block;
}

/* The token is already the header colour, in both modes. */
.brand-preview.is-dark { background-color: var(--c-header-bg); }

.drop-zone-brand {
    min-height: 6.75rem;
    padding: var(--s-4);
    margin-bottom: var(--s-2);
    max-width: 32rem;
}

.drop-zone-brand .drop-face { min-height: 4.75rem; }

/* A logo is artwork with its own shape, so it is shown whole rather than
   cropped to fill — `cover` would slice the ends off a wide wordmark. */
.drop-zone-brand .drop-preview {
    object-fit: contain;
    padding: var(--s-3);
    background: var(--c-header-bg);
}

/* ---- screenshots on a file's own page ------------------------------------

   Reported by somebody trying Convoro, 2026-08-10: *"on the downloads when you
   click on it the image needs to be more square so it shows better it is to
   long."*

   The listing's 16/10 tile is right for a row of thumbnails and wrong here: a
   theme screenshot is a whole page, so it is very tall, and a wide crop of a
   tall image is a strip of its header.

   🚨 `object-position: top`, which is the half that actually answers the
   complaint. Cropping a 1200×3000 page capture to any ratio centred shows the
   middle of the page — a band of body text with no header and no context.
   Anchored to the top it shows what the thing looks like when you open it, and
   the whole image is one click away in the lightbox. */
.profile-main .download-shots {
    grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
}

.profile-main .download-shot img {
    aspect-ratio: 4 / 3;
    object-position: top;
}
