/*
 * TechCraft CZ/SK - main stylesheet
 * The palette is inherited from the original portal: burnt orange accent on
 * a very dark, warm grey background.
 *
 * Organisation
 *   1. Design tokens          9. Forms
 *   2. Reset and base        10. Notices and flash messages
 *   3. Utilities             11. Badges
 *   4. Header and navigation 12. Tables
 *   5. Hero                  13. Content components
 *   6. Layout                14. Administration
 *   7. Posts                 15. Footer and error pages
 *   8. Buttons               16. Responsive and accessibility
 *
 * The discussion forum (sections, threads, posts, reactions, the editor and
 * the BBCode output) lives in the imported forum.css, and the chat - the
 * bubble in the header, the rooms docked in the corner and the section of
 * the administration which reads them - in chat.css.
 */

/* --------------------------------------------------------- Design tokens */

:root {
    --accent: #e34f0e;
    --brand-yellow: #f2c400;
    --brand-grey: #b9b8b8;
    --accent-dark: #ce480e;
    --accent-soft: rgba(227, 79, 14, 0.14);
    --danger: #e2212f;
    --danger-dark: #b71824;
    --ok: #3ba55d;
    --warn: #d99e2b;

    --bg: #131211;
    --bg-alt: #111111;
    --panel: #1a1917;
    --panel-alt: #201f1c;
    --panel-hover: #262421;

    --text: #b9b8b8;
    --text-strong: #e6e5e3;
    /* Raised from #86847f so that muted text keeps 4.5:1 on every panel. */
    --text-dim: #9a9791;

    --border: rgba(255, 255, 255, 0.11);
    --border-strong: rgba(255, 255, 255, 0.2);

    /* One radius for the whole portal, panels and pills included. */
    --radius: 6px;
    --radius-lg: 6px;
    --shadow: 0 6px 22px rgba(0, 0, 0, 0.45);
    --shadow-soft: 0 2px 10px rgba(0, 0, 0, 0.3);

    --header-height: 62px;
    --content-width: 1560px;
    --measure: 86ch;

    --font: "Raleway", "Segoe UI", system-ui, -apple-system, Arial, sans-serif;
    --mono: "JetBrains Mono", "Cascadia Mono", "Courier New", monospace;

    /*
     * Everything the browser draws on its own - the list of a select, the
     * scrollbars, the date pickers - follows the dark palette instead of
     * showing a white system widget in the middle of the page.
     */
    color-scheme: dark;
    accent-color: var(--accent);
}

/* ---------------------------------------------------------- Reset and base */

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

html {
    scroll-behavior: smooth;
    -webkit-text-size-adjust: 100%;
    /* `clip` never creates a scroll container, so sticky elements keep working. */
    overflow-x: clip;
}

body {
    margin: 0;
    min-height: 100vh;
    max-width: 100%;
    display: flex;
    flex-direction: column;
    background: var(--bg);
    color: var(--text);
    font-family: var(--font);
    font-size: 16px;
    font-weight: 400;
    line-height: 1.65;
    letter-spacing: 0.01em;
    overflow-x: clip;
}

/* Scroll lock while the mobile navigation drawer is open. */
body.has-open-nav {
    overflow: hidden;
    touch-action: none;
}

img {
    max-width: 100%;
    height: auto;
}

/*
 * The scripts toggle the `hidden` property on filtered rows, on reaction
 * buttons and on the editor panes - component classes must never win over it.
 */
[hidden] {
    display: none !important;
}

a {
    color: var(--accent);
    text-decoration: none;
    transition: color 0.2s ease-in-out;
}

a:hover {
    color: var(--accent-dark);
}

h1,
h2,
h3,
h4,
h5 {
    margin: 0 0 0.6rem 0;
    color: var(--text-strong);
    font-weight: 600;
    line-height: 1.25;
    letter-spacing: 0.02em;
    overflow-wrap: break-word;
}

h1 {
    font-size: 2rem;
}

h2 {
    font-size: 1.6rem;
}

h3 {
    font-size: 1.25rem;
}

h4 {
    font-size: 1.05rem;
}

p {
    margin: 0 0 1rem 0;
}

hr {
    border: 0;
    border-top: 1px solid var(--border);
    margin: 2rem 0;
}

code,
pre {
    font-family: var(--mono);
    font-size: 0.9em;
}

pre {
    max-width: 100%;
    overflow-x: auto;
}

/* ------------------------------------------------------------- Utilities */

.icon {
    width: 1em;
    height: 1em;
    vertical-align: -0.125em;
    flex: none;
}

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

.inline-form {
    display: inline-flex;
    margin: 0;
}

.avatar {
    border-radius: var(--radius);
    background: var(--panel-alt);
    image-rendering: pixelated;
}

/* Jumps straight to the main landmark, only visible while focused. */
.skip-link {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 300;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    background: var(--accent);
    color: #fff;
    font-size: 0.82rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.skip-link:focus,
.skip-link:focus-visible {
    width: auto;
    height: auto;
    margin: 0.5rem;
    padding: 0.7rem 1.1rem;
    overflow: visible;
    clip: auto;
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    color: #fff;
}

/*
 * `--chat-lift` is how tall the open chat is, written by the script which
 * draws it. Both of the things which live in this corner read it and sit
 * above whatever it says, so a toast and a room are never in the same place.
 */
.back-to-top {
    position: fixed;
    right: 1rem;
    bottom: calc(1rem + var(--chat-lift, 0px));
    z-index: 180;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    background: var(--panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transform: translateY(10px);
    box-shadow: var(--shadow);
    transition: bottom 0.2s ease, opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease, color 0.2s ease,
        border-color 0.2s ease;
}

.back-to-top svg {
    width: 18px;
    height: 18px;
}

.back-to-top:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.back-to-top.is-visible {
    opacity: 1;
    visibility: visible;
    transform: none;
}

/* Keep the toast stack clear of the button once it appears - and of the
   chat underneath both of them, which is what `--chat-lift` measures. */
body:has(.back-to-top.is-visible) .flash-stack {
    bottom: calc(4.6rem + var(--chat-lift, 0px));
}

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

/*
 * One flex row: the brand, the navigation right next to it and the account
 * menu pushed to the far end. `backdrop-filter` is deliberately not used
 * here - it would turn the header into the containing block of the fixed
 * mobile drawer, which would then collapse into an invisible strip.
 */
.site-header {
    position: sticky;
    top: 0;
    z-index: 60;
    display: flex;
    align-items: center;
    gap: 1.1rem;
    height: var(--header-height);
    padding: 0 1.25rem;
    background: rgba(17, 17, 17, 0.97);
    border-bottom: 1px solid var(--border);
}

/* The wordmark repeats the two colours of the icon next to it. */
.site-header__brand {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex: none;
    color: var(--text-strong);
    font-size: 1.15rem;
    font-weight: 700;
    letter-spacing: 0.01em;
    white-space: nowrap;
}

.site-header__brand-tech {
    color: var(--brand-grey);
}

.site-header__brand-craft {
    color: var(--brand-yellow);
}

.site-header__brand img {
    width: 34px;
    height: 34px;
    flex: none;
}

/* Main navigation, left aligned right behind the brand. */

.site-nav {
    display: flex;
    align-items: center;
    min-width: 0;
    margin-right: auto;
    /* An administrator may add entries, they must never push the chip out. */
    overflow: hidden;
}

.site-nav__list {
    display: flex;
    align-items: center;
    gap: 0.1rem;
    min-width: 0;
    margin: 0;
    padding: 0;
    list-style: none;
}

.site-nav__link {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 38px;
    padding: 0.4rem 0.62rem;
    border-radius: var(--radius);
    color: var(--text);
    font-size: 0.8rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    white-space: nowrap;
    transition: background-color 0.18s ease, color 0.18s ease;
}

.site-nav__link .icon {
    flex: none;
    color: var(--text-dim);
    transition: color 0.18s ease;
}

.site-nav__link:hover {
    background: rgba(255, 255, 255, 0.05);
    color: var(--text-strong);
}

.site-nav__link:hover .icon,
.site-nav__link.is-active .icon {
    color: var(--accent);
}

.site-nav__link.is-active {
    background: var(--accent-soft);
    color: var(--accent);
}

/* Only ever shown inside the drawer, the bar has its own actions. */
.site-nav__mobile-actions {
    display: none;
}

/* Account area. */

.site-header__actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex: none;
}

.site-header__actions form {
    margin: 0;
}

.user-menu {
    position: relative;
}

.user-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    max-width: 14rem;
    min-height: 38px;
    padding: 0.25rem 0.7rem 0.25rem 0.3rem;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-family: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    cursor: pointer;
    transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}

.user-chip img {
    width: 26px;
    height: 26px;
    flex: none;
    border-radius: var(--radius);
    image-rendering: pixelated;
}

.user-chip__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.user-chip__caret {
    flex: none;
    color: var(--text-dim);
    transition: transform 0.18s ease;
}

.user-chip:hover {
    background: var(--panel-hover);
    border-color: var(--accent);
    color: var(--text-strong);
}

.user-chip[aria-expanded="true"] {
    border-color: var(--accent);
    color: var(--accent);
}

.user-chip[aria-expanded="true"] .user-chip__caret {
    color: var(--accent);
    transform: rotate(180deg);
}

.user-menu__panel {
    position: absolute;
    top: calc(100% + 0.55rem);
    right: 0;
    z-index: 70;
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 16.5rem;
    padding: 0.45rem;
    background: var(--panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    animation: menu-drop 0.14s ease;
}

.user-menu__panel[hidden] {
    display: none;
}

@keyframes menu-drop {
    from {
        opacity: 0;
        transform: translateY(-0.35rem);
    }
}

/* Shared by the language panel, which puts a heading where the account menu
   puts a card. */

.user-menu__header {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 0;
    margin-bottom: 0.35rem;
    padding: 0.5rem 0.6rem 0.75rem 0.6rem;
    border-bottom: 1px solid var(--border);
}

.user-menu__header strong {
    display: block;
    overflow: hidden;
    color: var(--text-strong);
    font-size: 0.88rem;
    text-overflow: ellipsis;
}

/* Who you are, at the top of your own menu: a card rather than a line with a
   rule under it, so the panel reads as "this is you" and then as the three
   things you can do about it. */

.user-menu__card {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    min-width: 0;
    margin-bottom: 0.35rem;
    padding: 0.7rem 0.75rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.user-menu__card img {
    width: 44px;
    height: 44px;
    flex: none;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    image-rendering: pixelated;
}

.user-menu__identity {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
    min-width: 0;
}

.user-menu__card strong {
    display: block;
    max-width: 100%;
    overflow: hidden;
    color: var(--text-strong);
    font-size: 0.95rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.user-menu__card .role-badge {
    padding: 0.1rem 0.5rem;
    font-size: 0.63rem;
}

.user-menu form {
    margin: 0;
}

.user-menu__item {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    width: 100%;
    padding: 0.6rem;
    background: transparent;
    border: 0;
    border-radius: var(--radius);
    color: var(--text);
    font-family: inherit;
    font-size: 0.84rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
}

.user-menu__item .icon {
    flex: none;
    width: 1.1rem;
    color: var(--text-dim);
    transition: color 0.15s ease;
}

.user-menu__item:hover {
    background: var(--panel-hover);
    color: var(--text-strong);
}

.user-menu__item:hover .icon {
    color: var(--accent);
}

/* The way out is the last thing on the list and the one press which cannot be
   taken back by pressing it again, so a rule stands in front of it. */

.user-menu__item--danger {
    margin-top: 0.35rem;
    padding-top: 0.7rem;
    border-top: 1px solid var(--border);
    border-radius: 0 0 var(--radius) var(--radius);
    color: #f0736a;
}

.user-menu__item--danger .icon {
    color: inherit;
}

.user-menu__item--danger:hover {
    background: rgba(226, 33, 47, 0.14);
    color: #ff8a80;
}

/* Drawer toggle. */

.site-header__toggle {
    display: none;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex: none;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
}

.site-header__toggle:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.site-header__toggle-close {
    display: none;
}

.site-header__toggle[aria-expanded="true"] {
    border-color: var(--accent);
    color: var(--accent);
}

.site-header__toggle[aria-expanded="true"] .site-header__toggle-open {
    display: none;
}

.site-header__toggle[aria-expanded="true"] .site-header__toggle-close {
    display: inline-flex;
}

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

.hero {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 21rem;
    /* Bottom heavier than top on purpose: the block is centred in the banner,
       so the difference is what lifts the logo off the middle of it. */
    padding: 2.25rem 1.5rem 4rem;
    text-align: center;
    background-image: linear-gradient(rgba(19, 18, 17, 0.72), rgba(19, 18, 17, 0.94)),
        url("/static/assets/img/banner.jpg");
    background-position: center;
    background-size: cover;
    box-shadow: inset 0 -40px 60px -40px rgba(0, 0, 0, 0.9);
}

.hero__inner {
    max-width: 900px;
}

.hero__logo {
    max-width: 420px;
    margin: 0 auto -3rem auto;
    filter: drop-shadow(0 6px 18px rgba(0, 0, 0, 0.6));
}

.hero p {
    margin: 0 auto;
    max-width: 620px;
    color: var(--text);
    font-size: 1.05rem;
}

.hero__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem;
    justify-content: center;
    margin-top: 1.5rem;
}

.hero--compact {
    min-height: 12rem;
    padding: 2.5rem 1.5rem;
}

.hero--compact h1 {
    margin-bottom: 0.35rem;
}

.hero__eyebrow {
    display: inline-block;
    margin-bottom: 0.5rem;
    padding: 0.2rem 0.7rem;
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    color: var(--accent);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

/* --------------------------------------------------------------- Layout */

.page {
    flex: 1 0 auto;
    width: 100%;
    min-width: 0;
}

.wrapper {
    width: 100%;
    max-width: var(--content-width);
    margin: 0 auto;
    padding: 2.5rem 1.25rem;
}

/* Reading width for articles, still far wider than the old 820px column. */
.wrapper--narrow {
    max-width: 1080px;
}

/* A form of two fields does not need a page of width to be filled in. */
.wrapper--form {
    max-width: 520px;
}

.wrapper--wide {
    max-width: 1720px;
}

.section-heading {
    margin-bottom: 1.75rem;
    text-align: center;
}

.section-heading p {
    max-width: 720px;
    margin: 0 auto;
    color: var(--text-dim);
}

.section-heading--left {
    text-align: left;
}

.section-heading--left p {
    margin: 0;
}

.band {
    background-image: linear-gradient(rgba(19, 18, 17, 0.92), rgba(19, 18, 17, 0.97)),
        url("/static/assets/img/bg-main.jpg");
    background-attachment: fixed;
    background-position: bottom;
    background-size: cover;
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
    text-align: center;
}

.grid {
    display: grid;
    gap: 1.25rem;
}

.grid > * {
    min-width: 0;
}

.grid--2 {
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
}

.grid--3 {
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}

.grid--4 {
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}

/* ---------------------------------------------------------------- Panels */

.panel {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 1.5rem;
    box-shadow: var(--shadow-soft);
}

.panel--flush {
    padding: 0;
    overflow: hidden;
}

.panel__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding: 1rem 1.25rem;
    border-bottom: 1px solid var(--border);
    background: var(--panel-alt);
}

.panel__header h2,
.panel__header h3 {
    margin: 0;
    font-size: 1.05rem;
}

.panel__body {
    padding: 1.25rem;
}

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

.post-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: 1.25rem;
}

.post-grid > * {
    min-width: 0;
}

/* Featured post - media on the left, body on the right. */

.post-hero {
    display: grid;
    grid-template-columns: 1.15fr 1fr;
    margin-bottom: 1.5rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    box-shadow: var(--shadow-soft);
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.post-hero:hover {
    border-color: var(--accent);
    box-shadow: var(--shadow);
}

.post-hero__media {
    position: relative;
    display: block;
    min-width: 0;
    min-height: 220px;
    background: var(--panel-alt);
    overflow: hidden;
}

.post-hero__media img {
    display: block;
    width: 100%;
    height: 100%;
    min-height: 220px;
    object-fit: cover;
    transition: transform 0.35s ease;
}

/* The hero reuses the card media box, which then has to fill the column. */
.post-hero__media > .post-card__media,
.post-card__media--hero {
    height: 100%;
    border-bottom: 0;
    border-radius: 0;
}

.post-hero__media > .post-card__media img {
    min-height: 0;
}

.post-hero:hover .post-hero__media img {
    transform: scale(1.04);
}

.post-hero__body {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    min-width: 0;
    padding: 1.6rem 1.6rem 1.4rem 1.6rem;
}

.post-hero__title {
    margin: 0;
    font-size: 1.55rem;
    line-height: 1.25;
}

.post-hero__title a {
    color: var(--text-strong);
}

.post-hero__title a:hover {
    color: var(--accent);
}

.post-hero__summary {
    flex: 1;
    margin: 0;
    color: var(--text-dim);
    font-size: 0.96rem;
}

.post-hero__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-top: 0.5rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--border);
}

/* Post cards. */

.post-card {
    position: relative;
    display: flex;
    flex-direction: column;
    min-width: 0;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}

.post-card:hover {
    transform: translateY(-3px);
    border-color: var(--accent);
    box-shadow: var(--shadow);
}

.post-card__media {
    position: relative;
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    background: var(--panel-alt);
    border-bottom: 1px solid var(--border);
    overflow: hidden;
}

/* Empty overlay anchor turning the whole media box into one big target. */
.post-card__media-link {
    position: absolute;
    inset: 0;
    z-index: 2;
    display: block;
}

.post-card__image {
    display: block;
    width: 100%;
    height: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    transition: transform 0.35s ease;
}

.post-card:hover .post-card__image {
    transform: scale(1.04);
}

/*
 * Shown instead of the image when an entry carries no illustration: one of
 * the generated voxel scenes over the same warm glow the feed cards use.
 */
.post-card__fallback {
    position: absolute;
    inset: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.6rem;
    background:
        radial-gradient(circle at 50% 42%, rgba(227, 79, 14, 0.16), transparent 62%),
        linear-gradient(160deg, var(--panel-alt), var(--panel));
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    user-select: none;
}

.post-card__fallback img {
    /*
     * The scenes do not share one shape, so the drawing is given a box of a
     * fixed size and fitted into it - otherwise a tall one would stretch its
     * whole card and the row would stop lining up.
     */
    width: min(52%, 140px);
    height: 6rem;
    object-fit: contain;
}

/* Not inside a media box - keep it a self contained 16/9 block. */
.post-card > .post-card__fallback {
    position: relative;
    inset: auto;
    aspect-ratio: 16 / 9;
    border-bottom: 1px solid var(--border);
}

/*
 * A flag sitting on the image instead of the diagonal ribbon the card used
 * to wear, which fought with both the rounded corner and the headline.
 */
.post-card__ribbon {
    position: absolute;
    top: 0.7rem;
    left: 0.7rem;
    z-index: 3;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.6rem;
    background: rgba(19, 18, 17, 0.82);
    border: 1px solid rgba(227, 79, 14, 0.55);
    border-radius: var(--radius);
    color: var(--accent);
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    box-shadow: var(--shadow-soft);
    pointer-events: none;
}

.post-card__ribbon .icon {
    color: inherit;
}

.post-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    flex: 1;
    min-width: 0;
    padding: 1.25rem;
}

.post-card__title {
    margin: 0;
    font-size: 1.15rem;
    line-height: 1.3;
}

.post-card__title a {
    color: var(--text-strong);
}

.post-card__title a:hover {
    color: var(--accent);
}

.post-card__summary {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    -webkit-box-orient: vertical;
    flex: 1;
    margin: 0;
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.94rem;
}

.post-card__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.4rem 0.75rem;
    flex-wrap: wrap;
    margin-top: 0.5rem;
    padding-top: 0.85rem;
    border-top: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.78rem;
}

.post-card__author {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-width: 0;
    color: var(--accent);
    font-weight: 700;
}

.post-card__author .avatar {
    width: 18px;
    height: 18px;
    border-radius: var(--radius);
    image-rendering: pixelated;
    flex: none;
}

.post-card__date,
.post-card__reading {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--text-dim);
    white-space: nowrap;
}

.post-card__reading::before {
    content: "\2022";
    color: var(--border-strong);
}

/* Subtle busy state while the live search is fetching. */
.post-grid.is-loading,
.grid.is-loading,
[aria-busy="true"].post-grid,
[aria-busy="true"].grid {
    opacity: 0.55;
    transition: opacity 0.2s ease;
    pointer-events: none;
}

/* Category chip and meta line. */

.post-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.55rem;
    background: var(--accent-soft);
    border: 1px solid rgba(227, 79, 14, 0.35);
    border-radius: var(--radius);
    color: var(--accent);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    white-space: nowrap;
}

a.post-chip:hover {
    background: rgba(227, 79, 14, 0.22);
    color: var(--accent);
}

.post-meta {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    color: var(--text-dim);
    font-size: 0.8rem;
}

.post-meta__item {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    white-space: nowrap;
}

.post-meta strong {
    color: var(--accent);
    font-weight: 600;
}

.post-meta .avatar,
.post-meta img.avatar {
    width: 20px;
    height: 20px;
    border-radius: var(--radius);
    image-rendering: pixelated;
    flex: none;
}

/* Legacy profile cards render a large avatar inside a meta row. */
.post-meta img.avatar[width="48"] {
    width: 48px;
    height: 48px;
    border-radius: var(--radius);
}

.post-meta img.avatar[width="64"] {
    width: 64px;
    height: 64px;
    border-radius: var(--radius);
}

/* Post detail. */

.post-article {
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 2rem;
}

.post-article__header {
    margin-bottom: 1.5rem;
    padding-bottom: 1.25rem;
    border-bottom: 1px solid var(--border);
}

.post-article__header h1 {
    margin-bottom: 0.6rem;
}

.post-article__header .post-chip {
    margin-bottom: 0.75rem;
}

.post-article__lead {
    max-width: var(--measure);
    margin: 0 0 0.9rem 0;
    color: var(--text-dim);
    font-size: 1.05rem;
    line-height: 1.6;
}

.post-article__header .post-meta {
    margin-top: 0.6rem;
}

.post-article__figure {
    margin: 0 0 1.75rem 0;
}

.post-article__figure img,
.post-article__image {
    display: block;
    width: 100%;
    max-height: 420px;
    object-fit: cover;
    border-radius: var(--radius);
    margin-bottom: 1.5rem;
}

.post-article__figure figcaption {
    margin-top: -1rem;
    color: var(--text-dim);
    font-size: 0.8rem;
    text-align: center;
}

.post-article__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    flex-wrap: wrap;
    margin-top: 2rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
}

/* A footer holding one button rather than two groups: it belongs at the end
   of the line, not stranded on the left of an empty row. */

.post-article__footer--end {
    justify-content: flex-end;
}

/* Previous / next navigation below an article. */

.post-nav {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1rem;
    margin-top: 1.5rem;
}

.post-nav__item {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    min-width: 0;
    padding: 1rem 1.15rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-strong);
    font-weight: 600;
    transition: transform 0.2s ease, border-color 0.2s ease, background-color 0.2s ease;
}

.post-nav__item:hover {
    transform: translateY(-2px);
    border-color: var(--accent);
    background: var(--panel-hover);
    color: var(--accent);
}

.post-nav__item--next {
    text-align: right;
    align-items: flex-end;
}

.post-nav__direction {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.11em;
    text-transform: uppercase;
}

/* Long form typography of a rendered post body. */

.post-body,
.post-article__body,
.post-article__content {
    max-width: var(--measure);
    color: var(--text);
    font-size: 1rem;
    line-height: 1.75;
    overflow-wrap: break-word;
}

.post-body > :first-child,
.post-article__body > :first-child {
    margin-top: 0;
}

.post-body > :last-child,
.post-article__body > :last-child {
    margin-bottom: 0;
}

.post-body h2,
.post-article__body h2 {
    margin: 2rem 0 0.75rem 0;
    font-size: 1.45rem;
}

.post-body h3,
.post-article__body h3 {
    margin: 1.75rem 0 0.6rem 0;
    font-size: 1.2rem;
}

.post-body h4,
.post-article__body h4 {
    margin: 1.5rem 0 0.5rem 0;
    font-size: 1.02rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-dim);
}

.post-body p,
.post-article__body p,
.post-article__content p {
    margin: 0 0 1.15rem 0;
    color: var(--text);
}

.post-body ul,
.post-body ol,
.post-article__body ul,
.post-article__body ol {
    margin: 0 0 1.15rem 0;
    padding-left: 1.4rem;
}

.post-body li,
.post-article__body li {
    margin-bottom: 0.4rem;
}

.post-body li::marker,
.post-article__body li::marker {
    color: var(--accent);
}

.post-body blockquote,
.post-article__body blockquote {
    margin: 0 0 1.15rem 0;
    padding: 0.8rem 1.1rem;
    background: var(--panel-alt);
    border-left: 3px solid var(--accent);
    border-radius: 0 var(--radius) var(--radius) 0;
    color: var(--text);
    font-style: italic;
}

.post-body blockquote > :last-child,
.post-article__body blockquote > :last-child {
    margin-bottom: 0;
}

.post-body code,
.post-article__body code {
    padding: 0.1rem 0.35rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-strong);
    font-size: 0.86em;
}

.post-body pre,
.post-article__body pre {
    margin: 0 0 1.15rem 0;
    padding: 0.9rem 1rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-strong);
    line-height: 1.6;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    tab-size: 4;
}

.post-body pre code,
.post-article__body pre code {
    padding: 0;
    background: transparent;
    border: 0;
}

.post-body a,
.post-article__body a {
    color: var(--accent);
    text-decoration: underline;
    text-underline-offset: 2px;
    text-decoration-thickness: 1px;
}

.post-body a:hover,
.post-article__body a:hover {
    color: var(--accent-dark);
}

.post-body img,
.post-article__body img {
    display: block;
    max-width: 100%;
    height: auto;
    margin: 1.25rem 0;
    border-radius: var(--radius);
}

.post-body hr,
.post-article__body hr {
    margin: 2rem 0;
    border-top: 1px solid var(--border);
}

.post-body table,
.post-article__body table {
    width: 100%;
    margin: 0 0 1.25rem 0;
    border-collapse: collapse;
    font-size: 0.92rem;
}

.post-body th,
.post-article__body th {
    padding: 0.55rem 0.75rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.74rem;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
    text-align: left;
}

.post-body td,
.post-article__body td {
    padding: 0.55rem 0.75rem;
    border: 1px solid var(--border);
    vertical-align: top;
}

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

/*
 * `button.button` used to be part of this list, which made the base rule win
 * over every modifier on a real button element - a submit button lost both
 * its colour and its size.
 */
.button,
input[type="submit"],
input[type="button"] {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.45rem;
    height: 2.7rem;
    padding: 0 1.2rem;
    border: 0;
    border-radius: var(--radius);
    background: transparent;
    box-shadow: inset 0 0 0 1px var(--text);
    color: var(--text);
    font-family: inherit;
    font-size: 0.83rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    white-space: nowrap;
    cursor: pointer;
    transition: background-color 0.2s ease, box-shadow 0.2s ease, color 0.2s ease;
}

.button:hover,
input[type="submit"]:hover,
input[type="button"]:hover {
    box-shadow: inset 0 0 0 1px var(--accent);
    color: var(--accent);
}

.button:active {
    background: var(--accent-soft);
}

.button--primary,
input[type="submit"].button--primary {
    background: var(--accent);
    box-shadow: none;
    color: #fff;
}

.button--primary:hover,
input[type="submit"].button--primary:hover {
    background: var(--accent-dark);
    box-shadow: none;
    color: #fff;
}

.button--danger {
    box-shadow: inset 0 0 0 1px var(--danger);
    color: var(--danger);
}

.button--danger:hover {
    background: var(--danger);
    box-shadow: none;
    color: #fff;
}

.button--ghost {
    box-shadow: inset 0 0 0 1px var(--border-strong);
    color: var(--text-dim);
}

.button--ghost:hover {
    box-shadow: inset 0 0 0 1px var(--accent);
    color: var(--accent);
}

.button--small {
    height: 2.1rem;
    padding: 0 0.8rem;
    font-size: 0.72rem;
}

.button--block {
    width: 100%;
}

/*
 * A button which is only its symbol. For a card too narrow to spell out what
 * pressing it does, where the word would be the widest thing on it - the name
 * it is not showing lives in `title` and `aria-label` instead.
 */
.button--icon {
    width: auto;
    min-width: 2.1rem;
    padding: 0 0.55rem;
    flex: none;
}

.button[disabled],
.button.is-disabled {
    opacity: 0.5;
    pointer-events: none;
}

.button-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    min-width: 0;
}

.button-row > form {
    margin: 0;
}

/* ----------------------------------------------------------------- Forms */

form {
    min-width: 0;
}

.form-grid {
    display: grid;
    gap: 1.1rem;
}

.form-grid > * {
    min-width: 0;
}

.form-grid--2 {
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}

.field {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    min-width: 0;
}

.field > label,
.label {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    /* A label carrying a badge is taller than one which does not, and two
       fields side by side then start at different heights. The floor is the
       height of the badge, so every label of a row is one line tall. */
    min-height: 1.45rem;
    color: var(--text-strong);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.field__hint {
    color: var(--text-dim);
    font-size: 0.78rem;
}

input[type="text"],
input[type="password"],
input[type="email"],
input[type="url"],
input[type="tel"],
input[type="search"],
input[type="number"],
input[type="date"],
input[type="datetime-local"],
select,
textarea {
    width: 100%;
    max-width: 100%;
    padding: 0.65rem 0.8rem;
    background: rgba(255, 255, 255, 0.045);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-strong);
    font-family: inherit;
    font-size: 0.92rem;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

textarea {
    min-height: 8rem;
    resize: vertical;
    line-height: 1.6;
}

/*
 * A control with a box of its own shows focus by colouring that box. A
 * checkbox, a radio and a file input have no such box - the shadow drew a
 * tight ring around the 18 pixel square on every click - so they are left out
 * and get the ring below instead.
 */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus,
select:focus,
textarea:focus {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}

input::placeholder,
textarea::placeholder {
    color: var(--text-dim);
}

select {
    appearance: none;
    background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='none' stroke='%23b9b8b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 0.6rem center;
    background-size: 16px;
    padding-right: 2.2rem;
}

select option,
select optgroup {
    background-color: var(--panel-alt);
    color: var(--text-strong);
    font-family: inherit;
    font-size: 0.92rem;
    padding: 0.4rem 0.6rem;
}

select option:disabled {
    color: var(--text-dim);
}

/* Chrome paints the highlighted row with this pair. */
select option:checked,
select option:hover {
    background-color: var(--accent);
    color: #ffffff;
}

/*
 * The list a select opens.
 *
 * The closed control was always ours - a chevron of our own on a box of our
 * own - but the list it opened belonged to the system, and a grey Windows
 * menu in the middle of a dark page is the one part of a form nobody can
 * style. So the native list is never opened: the real <select> stays in the
 * page, invisible but focused and validated, and what is drawn over it is a
 * button and a panel of our own. The value, the tab order, the required mark
 * and everything a screen reader is told still come from the <select>.
 *
 * The panel is exactly as wide as the control it belongs to. A menu wider
 * than its own field reads as something that fell out of another page.
 */

.select {
    position: relative;
    display: block;
    min-width: 0;
}

/*
 * Kept in the layout rather than hidden: a control which is `display: none`
 * cannot be focused, and the browser refuses to submit a form whose invalid
 * field it cannot show. It sits exactly under the button, so the message the
 * browser pops up points at the right box.
 *
 * The disabled state is spelled out because the rule which dims every
 * disabled control is written further down the sheet and is exactly as
 * specific as this one - so it won, and a switched-off control showed its
 * own text at half opacity through the button drawn over it.
 */
.select > select,
.select > select:disabled {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    opacity: 0;
    pointer-events: none;
    appearance: none;
}

.select__button {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    width: 100%;
    padding: 0.65rem 0.8rem;
    background: rgba(255, 255, 255, 0.045);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-strong);
    font-family: inherit;
    font-size: 0.92rem;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

.select__button:hover {
    border-color: var(--border-strong);
}

/* The ring belongs to the select, which is the thing actually focused. */
.select > select:focus + .select__button,
.select.is-open .select__button {
    outline: none;
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}

.select__value {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The option which is not an answer yet reads like a placeholder. */
.select__value.is-placeholder {
    color: var(--text-dim);
}

.select__chevron {
    flex: none;
    color: var(--text-dim);
    transition: transform 0.18s ease;
}

.select.is-open .select__chevron {
    transform: rotate(180deg);
}

.select.is-disabled .select__button {
    opacity: 0.55;
    cursor: not-allowed;
}

/*
 * The list a control drops open.
 *
 * Fixed rather than absolute, and placed by the script against the window.
 * Drawn inside the control it was clipped by anything above it which scrolls
 * - a dialog, a panel with a ceiling of its own - and came out with a slice
 * missing off the bottom. Its position, width and ceiling are written by the
 * script, which is the only thing which knows where the control is.
 */
.select__menu {
    position: fixed;
    z-index: 80;
    display: flex;
    flex-direction: column;
    max-height: 17rem;
    padding: 0.3rem;
    background: var(--panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    animation: menu-drop 0.14s ease;
}

/*
 * Opened upwards where the room below has run out. Which way that is comes
 * from the script; this only says the panel should rise into place rather
 * than drop into it.
 */
.select__menu--above {
    animation-name: menu-rise;
}

@keyframes menu-rise {
    from {
        opacity: 0;
        transform: translateY(0.35rem);
    }
}

.select__menu[hidden] {
    display: none;
}

.select__search {
    flex: none;
    margin-bottom: 0.3rem;
}

.select__search input {
    padding: 0.45rem 0.6rem;
    font-size: 0.86rem;
}

.select__list {
    overflow-y: auto;
    overscroll-behavior: contain;
}

.select__option {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.6rem;
    border-radius: calc(var(--radius) - 2px);
    color: var(--text);
    font-size: 0.9rem;
    line-height: 1.35;
    cursor: pointer;
}

/*
 * The face of whoever a row stands for. Square, pixelated and of a fixed
 * size, so a list of a hundred nicknames is a hundred rows of one height
 * whatever the pictures turn out to be.
 */
.select__face {
    width: 20px;
    height: 20px;
    flex: none;
    border-radius: var(--radius);
    background: var(--bg);
    image-rendering: pixelated;
}

.select__option .icon {
    flex: none;
    opacity: 0;
    color: var(--accent);
}

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

/* The row the keyboard is on, which the mouse also paints as it passes. */
.select__option.is-active {
    background: var(--panel-alt);
    color: var(--text-strong);
}

.select__option.is-chosen {
    color: var(--text-strong);
    font-weight: 600;
}

.select__option.is-chosen .icon {
    opacity: 1;
}

.select__option.is-locked {
    color: var(--text-dim);
    cursor: not-allowed;
}

.select__option[hidden] {
    display: none;
}

/* What the search says when it has narrowed the list down to nothing. */
.select__nothing {
    padding: 0.75rem 0.6rem;
    color: var(--text-dim);
    font-size: 0.85rem;
    text-align: center;
}

.checkbox {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    min-height: 40px;
    padding: 0.35rem 0;
    color: var(--text);
    font-size: 0.9rem;
    cursor: pointer;
}

.checkbox input[type="checkbox"] {
    width: 18px;
    height: 18px;
    margin: 0.2rem 0 0 0;
    accent-color: var(--accent);
    cursor: pointer;
    flex: none;
}

.checkbox span small {
    display: block;
    color: var(--text-dim);
    font-size: 0.78rem;
}

.form-rule {
    margin: 1.5rem 0;
    border: 0;
    border-top: 1px solid var(--border);
}

.form-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.7rem;
    align-items: center;
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
}

/*
 * Saving is the last thing done on a screen, so it sits where a reader ends,
 * and an editing panel needs no rule cutting it in two on the way there.
 */
.form-actions--end {
    justify-content: flex-end;
    padding-top: 0;
    border-top: 0;
}

.char-counter {
    display: block;
    margin-top: 0.35rem;
    color: var(--text-dim);
    font-size: 0.76rem;
    font-variant-numeric: tabular-nums;
}

.char-counter.is-invalid {
    color: var(--danger);
}

/*
 * How far a password is from the four rules the hint lists, counted rather
 * than named. Nothing is drawn until something has been typed - a password
 * nobody has started is not a weak one.
 */
/*
 * How strong the password being typed is. The bar runs the whole width of
 * the field it belongs to and the word for it stands underneath, rather
 * than beside it: standing beside it, the word held eight rem of the row
 * whether it had anything to say or not, so the bar stopped well short of
 * the edge of the box above it and the two never lined up.
 *
 * While there is no password there is nothing to say about one, so the word
 * is taken out of the flow rather than left standing empty - an empty line
 * plus the gap above it is a finger of blank space under the bar, on a form
 * where every other field sits directly under its own explanation.
 */
.password-meter {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin-top: 0.45rem;
}

.password-meter__track {
    width: 100%;
    height: 0.35rem;
    background: var(--border);
    border-radius: 999px;
    overflow: hidden;
}

.password-meter__bar {
    display: block;
    width: 0;
    height: 100%;
    background: var(--danger);
    border-radius: 999px;
    transition: width 0.2s ease, background-color 0.2s ease;
}

.password-meter__level {
    color: var(--text-dim);
    font-size: 0.75rem;
    text-align: right;
}

/*
 * Gone entirely rather than merely blank: a flex gap is only drawn between
 * things which are there, so removing the word removes the space above it
 * as well.
 */
.password-meter__level:empty {
    display: none;
}

.password-meter[data-level="fair"] .password-meter__bar {
    background: var(--danger);
}

.password-meter[data-level="good"] .password-meter__bar {
    background: var(--warn);
}

.password-meter[data-level="strong"] .password-meter__bar {
    background: var(--ok);
}

.password-meter[data-level="strong"] .password-meter__level {
    color: var(--ok);
}

[data-password-hint].is-invalid,
[data-password-match].is-invalid {
    color: var(--danger);
}

/* --------------------------------------------------------------- Notices */

.notice {
    display: flex;
    align-items: center;
    gap: 0.7rem;
    padding: 0.85rem 1rem;
    margin-bottom: 1.25rem;
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    background: var(--panel-alt);
    font-size: 0.9rem;
}

.notice svg {
    flex: none;
}

/* The whole frame carries the state, rather than a bar down one edge. */
.notice--error {
    border-color: var(--danger);
    color: #f0c5c8;
}

/* Something the reader has to know about their own account, not an error. */
.notice--warn {
    border-color: var(--warn);
}

.notice--warn svg {
    color: var(--warn);
}

.notice--success {
    border-color: var(--ok);
}

.notice--info {
    border-color: var(--warn);
}

/*
 * The people an announcement is addressed to, gathered under the picker.
 *
 * Each one is a chip which can be pressed off again, because a list built by
 * picking is only usable if it can also be unpicked - and the whole point of
 * moving off a single text field was to stop retyping the other four names.
 */
.recipient-list {
    display: flex;
    flex-wrap: wrap;
    gap: 0.45rem;
    margin-top: 0.9rem;
}

.recipient-list:empty {
    display: none;
}

/* A picked player: their face, their nickname and the button which takes them
   off the list, in a box drawn with the corners of every other box of the
   portal rather than as a pill nothing else is shaped like. */
.recipient {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.25rem 0.3rem 0.25rem 0.35rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-strong);
    font-size: 0.84rem;
}

.recipient .avatar {
    width: 20px;
    height: 20px;
    flex: none;
    border-radius: 3px;
    image-rendering: pixelated;
}

.recipient__drop {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.3rem;
    height: 1.3rem;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: 999px;
    color: var(--text-dim);
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.recipient__drop:hover {
    background: var(--danger);
    color: #ffffff;
}

.recipient__drop:disabled {
    cursor: not-allowed;
    opacity: 0.5;
}

/*
 * A statement rather than a state: there is nothing here, and nothing has
 * gone wrong either. Used where a panel would otherwise hold a bare grey
 * sentence with no frame around it.
 */
.notice--muted {
    margin-bottom: 0;
    border-color: var(--border);
    color: var(--text-dim);
}

.notice--muted svg {
    color: var(--text-dim);
    opacity: 0.7;
}

/* ------------------------------------------------------------------ Modal */

/*
 * The question asked before something irreversible, in place of the box the
 * browser draws. A native <dialog>, so the focus trap, the Escape key and the
 * inert page behind it are the browser's own doing rather than ours.
 */
.modal {
    width: min(30rem, calc(100vw - 2rem));
    /*
     * Never taller than the window it is drawn in. A dialog is centred by the
     * browser, so one taller than the window loses the top of itself off the
     * edge - the heading first, then whatever the window is asking - and
     * there is no way to scroll up to it. It stops at the height of the
     * window instead and what does not fit is scrolled inside it.
     *
     * The small viewport unit is what a phone measures with the address bar
     * showing; the plain one is the fallback where it is not understood.
     */
    max-height: calc(100vh - 2rem);
    max-height: calc(100svh - 2rem);
    margin: auto;
    padding: 0;
    background: var(--panel);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    color: var(--text);
    overflow: hidden;
}

.modal::backdrop {
    background: rgba(0, 0, 0, 0.6);
}

/* What does not fit the window scrolls, and the frame of the dialog stays. */
.modal__inner {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    max-height: inherit;
    padding: 1.25rem;
    overflow-y: auto;
    overscroll-behavior: contain;
}

/*
 * The challenge in front of the public forms.
 *
 * The picture is the loudest thing in the dialog on purpose - it is the one
 * part a visitor has to look at - and the button which draws a new one sits
 * on it rather than beside the field, where it would be read as a second way
 * of sending the form.
 */

/* The dialog names itself louder than a confirmation does: it interrupts
   somebody in the middle of sending a form, so it says why straight away. */

.captcha__title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    font-size: 1.35rem;
}

.captcha__title .icon {
    color: var(--accent);
}

.captcha__frame {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 0.6rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.captcha__picture {
    width: 100%;
    max-width: 280px;
    height: auto;
    border-radius: calc(var(--radius) - 2px);
}

.captcha__again {
    position: absolute;
    right: 0.85rem;
    bottom: 0.85rem;
    /* A square of the height every small button has: with the padding of one
       carrying words it came out taller than it was wide. */
    width: 2.1rem;
    height: 2.1rem;
    padding: 0;
    line-height: 0;
    background: rgba(19, 18, 17, 0.85);
}

.captcha__again .icon {
    margin: 0;
}

.captcha__field {
    margin-top: 0.2rem;
}

.captcha__field input {
    font-family: var(--mono);
    font-size: 1.35rem;
    letter-spacing: 0.5rem;
    text-align: center;
}

.captcha__field .field__hint {
    min-height: 1.1rem;
}

.captcha__field .field__hint.is-wrong {
    color: #f0736a;
}

/* A line of the log: the symbol keeps the two lines of text beside it. */

.log-event {
    display: inline-flex;
    align-items: flex-start;
    gap: 0.5rem;
}

.log-event .icon {
    margin-top: 0.15rem;
    flex: none;
    color: var(--text-dim);
}

.log-event--refused .icon {
    color: #f0736a;
}

.log-address {
    display: inline-block;
    padding: 0.1rem 0.4rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 0.78rem;
}

.modal__title {
    margin: 0;
    color: var(--text-strong);
    font-size: 1.05rem;
}

.modal__text {
    margin: 0;
    font-size: 0.9rem;
    line-height: 1.55;
}

.modal__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 0.6rem;
    margin-top: 0.5rem;
}

/* A dialog carrying a form needs the room a question does not. */
.modal:has(.field) {
    width: min(34rem, calc(100vw - 2rem));
}

/*
 * One which asked to be wide by name stays wide. The rule above is a class
 * heavier than `.modal--wide`, so a wide dialog which also holds fields was
 * quietly given the width of an ordinary form and its two column rows came
 * out one on top of the other.
 */
.modal--wide:has(.field) {
    width: min(46rem, calc(100vw - 2rem));
}

/*
 * Wider still, for a window which holds the box the forum is written in. A
 * toolbar of a dozen symbols and a preview beside it do not fit a column of
 * forty characters, and neither do three lists side by side.
 */
.modal--huge,
.modal--huge:has(.field) {
    width: min(66rem, calc(100vw - 2rem));
}

.modal .form-grid {
    margin-top: 0.35rem;
}

.flash-stack {
    position: fixed;
    right: 1rem;
    bottom: calc(1rem + var(--chat-lift, 0px));
    z-index: 200;
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    max-width: 380px;
    transition: bottom 0.2s ease;
}

.flash {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.8rem 1rem;
    background: var(--panel-alt);
    border: 1px solid var(--ok);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    color: var(--text-strong);
    font-size: 0.88rem;
    animation: flash-in 0.25s ease-out;
}

/* The whole frame carries the state, the way a notice does. */
.flash--error {
    border-color: var(--danger);
}

.flash--info {
    border-color: var(--accent);
}

.flash button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    margin-left: auto;
    background: none;
    border: 0;
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
    font-size: 1rem;
    line-height: 1;
}

.flash button:hover {
    color: var(--accent);
}

@keyframes flash-in {
    from {
        opacity: 0;
        transform: translateY(8px);
    }

    to {
        opacity: 1;
        transform: none;
    }
}

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

.badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.55rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-dim);
    white-space: nowrap;
}

.badge--accent {
    border-color: var(--accent);
    color: var(--accent);
}

.badge--ok {
    border-color: var(--ok);
    color: var(--ok);
}

.badge--danger {
    border-color: var(--danger);
    color: var(--danger);
}

.badge--warn {
    border-color: var(--warn);
    color: var(--warn);
}

.badge--muted {
    border-color: var(--border);
    color: var(--text-dim);
}

/*
 * Whether somebody is about.
 *
 * The same shape as the role badge standing next to it, with a dot in front
 * rather than a different outline: the three states differ only in colour,
 * and giving them different shapes as well would have somebody reading the
 * shape instead. The wording carries the answer too, so the colour is never
 * the only thing saying it.
 */
.presence {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
}

.presence__dot {
    flex: none;
    width: 7px;
    height: 7px;
    background: currentcolor;
    border-radius: 50%;
}

.presence--online {
    border-color: var(--ok);
    color: var(--ok);
}

/* A breath rather than a blink: the point is "still here", not "look here". */
.presence--online .presence__dot {
    animation: presence-breathes 2.4s ease-in-out infinite;
}

.presence--offline {
    border-color: var(--border);
    color: var(--text-dim);
}

/*
 * Nothing known is drawn fainter than offline rather than in a colour of its
 * own. It is the absence of an answer, and an absence which stands out is an
 * absence pretending to be one.
 */
.presence--unknown {
    border-color: var(--border);
    color: var(--text-dim);
    opacity: 0.7;
}

@keyframes presence-breathes {
    50% {
        opacity: 0.35;
    }
}

/*
 * A face with a dot on the corner of it saying whether they are about.
 *
 * On the picture rather than beside it: a line of a conversation already
 * holds a face, a name and a time, and a fourth thing along it would push the
 * words further from the person who said them. The dot carries a ring in the
 * colour behind it so it reads as sitting on top of the face rather than as
 * part of the picture.
 */
.face-presence {
    position: relative;
    display: inline-flex;
    flex: none;
    line-height: 0;
}

.face-presence__dot {
    position: absolute;
    right: -2px;
    bottom: -2px;
    width: 8px;
    height: 8px;
    background: var(--brand-grey);
    border: 2px solid var(--panel);
    border-radius: 50%;
}

.face-presence__dot--online {
    background: var(--ok);
}

/* Inside a bubble the face stands on the panel behind the line, not on it. */
.chat-line .face-presence__dot {
    border-color: var(--bg);
}

/* The same answer as a dot alone, where a row has no room to say it twice. */
.presence-dot {
    display: inline-block;
    flex: none;
    width: 8px;
    height: 8px;
    background: var(--text-dim);
    border-radius: 50%;
}

.presence-dot--online {
    background: var(--ok);
}

.presence-dot--unknown {
    background: var(--border-strong);
}

@media (prefers-reduced-motion: reduce) {
    .presence--online .presence__dot {
        animation: none;
    }
}

.role-badge[data-role="ADMIN"] {
    border-color: #5555ff;
    color: #8f8fff;
}

.role-badge[data-role="MODERATOR"] {
    border-color: #ff5555;
    color: #ff8a8a;
}

.role-badge[data-role="HELPER"] {
    border-color: #00aa00;
    color: #56c356;
}

.role-badge[data-role="PLAYER"] {
    border-color: #55ffff;
    color: #8fdede;
}

/* Ban list. */

.ban-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.15rem 0.6rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    background: rgba(255, 255, 255, 0.03);
    color: var(--text-dim);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
}

.ban-badge--permanent {
    border-color: var(--danger);
    background: rgba(226, 33, 47, 0.12);
    color: #ff8f97;
}

.ban-badge--temporary {
    border-color: var(--warn);
    background: rgba(217, 158, 43, 0.12);
    color: #edc06a;
}

.ban-badge--mute {
    border-color: var(--border-strong);
    background: rgba(255, 255, 255, 0.04);
    color: var(--text-dim);
}

/* Audience of a navigation entry in the administration. */

.audience-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.1rem 0.5rem;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.66rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
    white-space: nowrap;
}

.audience-badge--public {
    border-color: var(--ok);
    color: #6fcb8b;
}

.audience-badge--authenticated {
    border-color: var(--accent);
    color: var(--accent);
}

.audience-badge--staff {
    border-color: var(--danger);
    color: #ff8f97;
}

.audience-badge--anonymous {
    border-color: var(--border-strong);
    color: var(--text-dim);
}

/* ---------------------------------------------------------------- Tables */

.table-wrap {
    width: 100%;
    max-width: 100%;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    -webkit-overflow-scrolling: touch;
}

table.data {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9rem;
}

table.data th {
    padding: 0.7rem 0.9rem;
    background: var(--panel-alt);
    border-bottom: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.74rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    text-align: left;
    white-space: nowrap;
}

table.data td {
    padding: 0.75rem 0.9rem;
    border-bottom: 1px solid var(--border);
    vertical-align: middle;
}

table.data tbody tr:hover {
    background: rgba(255, 255, 255, 0.025);
}

table.data tbody tr:last-child td {
    border-bottom: 0;
}

table.data td.actions {
    text-align: right;
    white-space: nowrap;
}

table.data td.actions .button-row {
    justify-content: flex-end;
    /* The column is narrow, the buttons still belong on a single line. */
    flex-wrap: nowrap;
}

.cell-title {
    color: var(--text-strong);
    font-weight: 600;
}

/*
 * A picture with two lines of text beside it. Without a box of its own the
 * image sits on the baseline of the first line, which leaves it hanging above
 * the second one.
 */
.user-cell {
    display: inline-flex;
    align-items: center;
    gap: 0.65rem;
    min-width: 0;
}

.user-cell .avatar {
    flex: none;
}

.user-cell__text {
    display: flex;
    flex-direction: column;
    align-items: start;
    gap: 0.2rem;
    min-width: 0;
}

.cell-sub {
    display: block;
    color: var(--text-dim);
    font-size: 0.78rem;
}

.empty-state {
    padding: 3rem 1.5rem;
    text-align: center;
    color: var(--text-dim);
}

.empty-state svg {
    width: 42px;
    height: 42px;
    margin-bottom: 0.75rem;
    opacity: 0.5;
}

/* ----------------------------------------------------- Content components */

.breadcrumb {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin-bottom: 1.25rem;
    color: var(--text-dim);
    font-size: 0.82rem;
}

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

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

.accordion {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
    overflow: hidden;
}

.accordion + .accordion {
    margin-top: 0.75rem;
}

.accordion__trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    width: 100%;
    min-height: 48px;
    padding: 1rem 1.25rem;
    background: transparent;
    border: 0;
    color: var(--text-strong);
    font-family: inherit;
    font-size: 0.98rem;
    font-weight: 600;
    text-align: left;
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease;
}

.accordion__trigger:hover {
    background: var(--panel-hover);
    color: var(--accent);
}

.accordion__trigger svg {
    width: 16px;
    height: 16px;
    flex: none;
    transition: transform 0.2s ease;
}

.accordion.is-open .accordion__trigger {
    color: var(--accent);
}

.accordion.is-open .accordion__trigger svg {
    transform: rotate(180deg);
}

/*
 * The answer starts where the question does, keeps a readable line length
 * and never inherits the centring of the section it happens to stand in.
 */
.accordion__panel {
    display: none;
    padding: 1.1rem 1.25rem 1.25rem 1.25rem;
    border-top: 1px solid var(--border);
    color: var(--text);
    font-size: 0.94rem;
    line-height: 1.7;
    text-align: left;
}

.accordion__panel > * {
    max-width: var(--measure);
}

.accordion__panel > :last-child {
    margin-bottom: 0;
}

.accordion.is-open .accordion__panel {
    display: block;
}

/* Team cards. */

/*
 * The team, however many of it there is.
 *
 * A card of a member is a portrait and a few lines under it, and a portrait
 * stretched across a whole screen because the team has one member that day
 * is not a portrait. The columns therefore have a width of their own and the
 * row is centred, so one card looks like one card and eight look like eight.
 */
.team-grid {
    display: grid;
    gap: 1.25rem;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 20rem));
    justify-content: center;
}

.team-grid--wide {
    grid-template-columns: repeat(auto-fit, minmax(18rem, 26rem));
}

.team-card {
    padding: 1.5rem;
    text-align: center;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.2s ease, transform 0.2s ease;
}

.team-card:hover {
    border-color: var(--accent);
    transform: translateY(-2px);
}

.team-card img {
    image-rendering: pixelated;
}

.team-card__avatar-link {
    display: inline-block;
    line-height: 0;
    border-radius: var(--radius);
}

/* The nickname is a link, and a link is orange everywhere else; here it stays
   the heading it looks like until the pointer is on it. */

.team-card__name,
a.team-card__name {
    display: block;
    margin-top: 0.6rem;
    color: var(--text-strong);
    font-size: 1.05rem;
    font-weight: 700;
}

a.team-card__name:hover {
    color: var(--accent);
}

.team-card__position {
    color: var(--accent);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.team-card__description {
    margin-top: 0.75rem;
    color: var(--text-dim);
    font-size: 0.9rem;
}

/* Statistics. */

.stat-card {
    padding: 1.25rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.stat-card__label {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    color: var(--text-dim);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.09em;
    text-transform: uppercase;
}

.stat-card__value {
    margin-top: 0.4rem;
    color: var(--text-strong);
    font-size: 1.9rem;
    font-weight: 700;
    line-height: 1.1;
}

.stat-card--accent .stat-card__value {
    color: var(--accent);
}

/*
 * The overview of the administration.
 *
 * Six colours, named rather than numbered, so a line of a chart and a part of
 * a ring drawn in the same one say so in the markup as well as on the screen.
 * They are picked to stay apart from each other rather than to sit on a
 * gradient: the first two are the colours of the portal, and the rest are as
 * far from those and from one another as a set of six gets.
 */

:root {
    --chart-one: var(--accent);
    --chart-two: var(--brand-yellow);
    --chart-three: #3f9ad6;
    --chart-four: #3ba55d;
    --chart-five: #a479e0;
    --chart-six: #8d8b87;
}

.stat-card__hint {
    margin-top: 0.45rem;
    color: var(--text-dim);
    font-size: 0.78rem;
    line-height: 1.45;
}

/* Drawn only where the number is a part of something. */
.stat-card__bar {
    height: 3px;
    margin-top: 0.6rem;
    overflow: hidden;
    background: var(--border);
    border-radius: 999px;
}

.stat-card__bar span {
    display: block;
    width: var(--share, 0%);
    height: 100%;
    background: var(--accent);
    border-radius: inherit;
}

/*
 * A tone tints the bar under the number and nothing else. It used to draw a
 * line down the edge of the card as well, which made some cards on a row
 * look like a different kind of thing from the ones beside them - and the
 * number is what is being read either way.
 */
.stat-card--ok .stat-card__bar span {
    background: var(--ok);
}

.stat-card--warn .stat-card__bar span {
    background: var(--warn);
}

.stat-card--bad .stat-card__bar span {
    background: var(--danger);
}

.overview-panel {
    margin-top: 2.25rem;
}

.overview-cards {
    margin-top: 1rem;
}

/*
 * A clear line between the numbers and the picture under them, and between
 * one picture and the next. They are separate answers to separate questions
 * and reading them as one block is how a row of cards ends up looking like
 * the legend of the chart below it.
 */
.overview-figure {
    margin-top: 1.75rem;
    padding: 1.25rem;
}

.overview-figures {
    margin-top: 1.75rem;
}

.overview-figures .overview-figure {
    margin-top: 0;
}

.chart__head h3 {
    margin: 0;
    color: var(--text-strong);
    font-size: 0.95rem;
}

.chart__head .field__hint {
    margin-top: 0.25rem;
}

.chart {
    margin: 0;
}

.chart__quiet {
    margin: 1.5rem 0;
    color: var(--text-dim);
    font-size: 0.85rem;
    text-align: center;
}

.chart__legend {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1.1rem;
    margin: 0.9rem 0 0;
    padding: 0;
    list-style: none;
}

.chart__legend--stacked {
    flex-direction: column;
    gap: 0.4rem;
}

.chart__key {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    color: var(--text-dim);
    font-size: 0.8rem;
}

.chart__key strong {
    color: var(--text-strong);
}

/* The name gives up its width first, so the number never wraps under it. */
.chart__key-name {
    flex: 1 1 auto;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.chart__swatch {
    flex: none;
    width: 9px;
    height: 9px;
    background: var(--chart-six);
    border-radius: 2px;
}

.chart__key--one .chart__swatch {
    background: var(--chart-one);
}

.chart__key--two .chart__swatch {
    background: var(--chart-two);
}

.chart__key--three .chart__swatch {
    background: var(--chart-three);
}

.chart__key--four .chart__swatch {
    background: var(--chart-four);
}

.chart__key--five .chart__swatch {
    background: var(--chart-five);
}

.chart__key--six .chart__swatch {
    background: var(--chart-six);
}

/*
 * The box is stretched to the width it is given and the strokes are told not
 * to stretch with it, so one chart is the width of the page and another is a
 * third of it without the lines of the second being three times thinner.
 */
.chart__plot {
    display: block;
    width: 100%;
    height: 11rem;
    /*
     * A chart may never draw outside its own box. Nothing should reach past
     * it, but a single bad coordinate in a run of thirty is the difference
     * between one wrong point and a shape thrown across the panels below,
     * and the second of those is not worth risking to save a clipped pixel.
     */
    overflow: hidden;
}

.chart__rule,
.chart__base {
    stroke: var(--border);
    stroke-width: 1;
    vector-effect: non-scaling-stroke;
}

.chart__rule {
    stroke-dasharray: 3 5;
}

.chart__base {
    stroke: var(--border-strong);
}

.chart__line {
    fill: none;
    stroke: var(--chart-six);
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.chart__area {
    fill: var(--chart-six);
    fill-opacity: 0.12;
    stroke: none;
}

.chart__line--one {
    stroke: var(--chart-one);
}

.chart__area--one {
    fill: var(--chart-one);
}

.chart__line--two {
    stroke: var(--chart-two);
}

.chart__area--two {
    fill: var(--chart-two);
}

.chart__line--three {
    stroke: var(--chart-three);
}

.chart__area--three {
    fill: var(--chart-three);
}

.chart__line--four {
    stroke: var(--chart-four);
}

.chart__area--four {
    fill: var(--chart-four);
}

/*
 * The axis is written at the size it is meant to be read at rather than at
 * the size the stretched box would make it, which is what the vector effect
 * cannot do for text.
 */
.chart__tick {
    fill: var(--text-dim);
    font-size: 11px;
    text-anchor: middle;
}

/* The picture and everything drawn over it. */
.chart__frame {
    position: relative;
    margin-top: 0.75rem;
}

.chart__column {
    fill: var(--chart-one);
    opacity: 0.85;
}

/*
 * The bands a pointer is caught by. Invisible, but they must still answer to
 * one - a shape painted with nothing at all is a shape no pointer can reach.
 */
.chart__hit {
    fill: transparent;
    cursor: crosshair;
}

.chart__hit:hover,
.chart__hit:focus-visible {
    fill: rgb(255 255 255 / 4%);
    outline: none;
}

.chart__guide {
    stroke: var(--border-strong);
    stroke-width: 1;
    stroke-dasharray: 2 3;
    vector-effect: non-scaling-stroke;
    pointer-events: none;
}

/*
 * What the day being pointed at was worth. Placed by the script against the
 * band rather than against the pointer, so it holds still while the pointer
 * moves about inside one day.
 */
.chart__readout {
    position: absolute;
    bottom: calc(100% - 0.5rem);
    z-index: 5;
    padding: 0.3rem 0.55rem;
    color: var(--text-strong);
    font-size: 0.75rem;
    white-space: nowrap;
    background: var(--panel-alt);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow-soft);
    pointer-events: none;
}

/*
 * The dates under a chart, each one standing where its own point does.
 *
 * Markup rather than writing inside the picture: the picture is stretched to
 * whatever width it is given, and a letter stretched with it comes out
 * smeared sideways - which is exactly what the dates used to look like.
 */
.chart__axis {
    position: relative;
    height: 1.1rem;
    margin-top: 0.4rem;
}

.chart__axis span {
    position: absolute;
    left: var(--at, 0%);
    color: var(--text-dim);
    font-size: 0.7rem;
    white-space: nowrap;
    transform: translateX(-50%);
}

/* A whole divided into parts, as one column each. */
.column-chart {
    display: flex;
    align-items: flex-end;
    gap: 0.75rem;
    justify-content: space-around;
    height: 11rem;
    margin-top: 1rem;
}

.column-chart__item {
    display: flex;
    flex: 1 1 0;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    max-width: 5rem;
    height: 100%;
}

.column-chart__value {
    margin-bottom: 0.35rem;
    color: var(--text-strong);
    font-size: 0.85rem;
    font-weight: 700;
}

.column-chart__bar {
    /* The tallest part fills the chart, so the shortest is still visible. */
    min-height: 2px;
    height: var(--tall, 0%);
    width: 100%;
    background: var(--chart-six);
    border-radius: 3px 3px 0 0;
}

.column-chart__bar--one {
    background: var(--chart-one);
}

.column-chart__bar--two {
    background: var(--chart-two);
}

.column-chart__bar--three {
    background: var(--chart-three);
}

.column-chart__bar--four {
    background: var(--chart-four);
}

.column-chart__bar--five {
    background: var(--chart-five);
}

.column-chart__bar--six {
    background: var(--chart-six);
}

.column-chart__name {
    margin-top: 0.45rem;
    overflow: hidden;
    max-width: 100%;
    color: var(--text-dim);
    font-size: 0.72rem;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* A whole divided into parts, as one line. */
.split-bar {
    display: flex;
    height: 10px;
    margin-top: 1rem;
    overflow: hidden;
    background: var(--border);
    border-radius: 999px;
}

.split-bar__part {
    width: var(--share, 0%);
    background: var(--chart-six);
}

.split-bar__part--one {
    background: var(--chart-one);
}

.split-bar__part--two {
    background: var(--chart-two);
}

.split-bar__part--three {
    background: var(--chart-three);
}

.split-bar__part--four {
    background: var(--chart-four);
}

.split-bar__part--five {
    background: var(--chart-five);
}

.split-bar__part--six {
    background: var(--chart-six);
}

/*
 * Four rows tall whatever it holds, and scrolled past that. A panel which
 * grows with its own contents pushes whatever stands beside it out of line,
 * and the rows past the fourth are the tail of a list ordered largest first.
 *
 * The row is given a height rather than left to its contents, so that "four
 * rows" is something this can be worked out from instead of a guess which
 * cuts the fourth one in half the moment a label wraps.
 */
.rank-table {
    --rank-row: 2.75rem;

    max-height: calc(4 * var(--rank-row));
    margin-top: 1rem;
    overflow-y: auto;
}

.rank-table tbody tr {
    height: var(--rank-row);
}

.rank-table td {
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
}

/*
 * The number and what it counts on one line. Stacked they made the row half
 * again as tall, which is what left the fourth of four rows cut through the
 * middle - a height on a row is a floor rather than a ceiling, so the way to
 * make four rows fit is to make a row shorter rather than to insist.
 */
.rank-table .cell-sub {
    display: inline;
    margin-left: 0.3rem;
}

.rank-table__number {
    text-align: right;
    white-space: nowrap;
}

.rank-table__number strong {
    color: var(--text-strong);
}

.chart--rows tr.is-bad .cell-title {
    color: var(--danger);
}

/* Profiles. */

.profile-header {
    display: flex;
    align-items: center;
    gap: 1.25rem;
    flex-wrap: wrap;
    padding: 1.35rem 1.5rem;
    background: linear-gradient(180deg, var(--panel-alt), var(--panel));
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-soft);
}

.profile-header__avatar {
    width: 96px;
    height: 96px;
    border-radius: var(--radius);
    background: var(--bg-alt);
    border: 1px solid var(--border);
    image-rendering: pixelated;
    flex: none;
}

.profile-identity {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    min-width: 0;
    flex: 1 1 14rem;
}

/* Up in the corner, out of the column of the nickname - inside it a button
   stretches to the width of the card and reads as the loudest thing on it. */

.profile-header__actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    align-self: flex-start;
    margin-left: auto;
}

.profile-identity h1,
.profile-identity h2 {
    margin: 0;
    font-size: 1.5rem;
    word-break: break-word;
}

.profile-identity__badges {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
}

.profile-identity__meta {
    color: var(--text-dim);
    font-size: 0.82rem;
}

.profile-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
    gap: 0.75rem;
    margin-top: 1.25rem;
}

.profile-stat {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    padding: 0.85rem 1rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.profile-stat__value {
    color: var(--text-strong);
    font-size: 1.5rem;
    font-weight: 700;
    line-height: 1.15;
}

.profile-stat__label {
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

/* ------------------------------------------------------------ Pagination */

.pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    flex-wrap: wrap;
    margin-top: 2rem;
}

.pagination a,
.pagination span {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    min-width: 2.5rem;
    height: 2.5rem;
    padding: 0 0.7rem;
    justify-content: center;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 0.85rem;
    font-weight: 600;
}

.pagination a:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.pagination .is-current {
    background: var(--accent);
    border-color: var(--accent);
    color: #fff;
}

.pagination .is-disabled {
    opacity: 0.4;
}

/* ---------------------------------------------------------------- Toolbar */

.toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.5rem;
}

.toolbar > * {
    min-width: 0;
}

.search-box {
    position: relative;
    flex: 1 1 260px;
    max-width: 420px;
    margin: 0;
}

.search-box svg {
    position: absolute;
    left: 0.7rem;
    top: 50%;
    transform: translateY(-50%);
    width: 16px;
    height: 16px;
    color: var(--text-dim);
    pointer-events: none;
}

.search-box input {
    padding-left: 2.2rem;
}

.filter-chips,
/* A column named with the symbol of what it holds. */
.column-head {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

.column-head svg {
    flex: none;
    color: var(--text-dim);
}

.chip-row {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    min-width: 0;
}

.chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-height: 40px;
    padding: 0.3rem 0.85rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.78rem;
    font-weight: 600;
    white-space: nowrap;
    cursor: pointer;
}

.chip:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.chip.is-active {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent);
}

/* ------------------------------------------------------------------ Admin */

.admin-layout {
    display: grid;
    grid-template-columns: 250px minmax(0, 1fr);
    gap: 1.75rem;
    width: 100%;
    max-width: 1720px;
    margin: 0 auto;
    padding: 2rem 1.25rem;
}

.admin-sidebar {
    align-self: start;
    position: sticky;
    top: calc(var(--header-height) + 1rem);
    min-width: 0;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.admin-sidebar__title {
    padding: 0.9rem 1.1rem;
    background: var(--panel-alt);
    border-bottom: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.admin-sidebar nav {
    display: flex;
    flex-direction: column;
    padding: 0.5rem;
}

.admin-sidebar nav a {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    min-height: 40px;
    padding: 0.55rem 0.7rem;
    border-radius: var(--radius);
    color: var(--text);
    font-size: 0.87rem;
    font-weight: 600;
}

.admin-sidebar nav a svg {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--text-dim);
}

.admin-sidebar nav a:hover {
    background: var(--panel-hover);
    color: var(--text-strong);
}

.admin-sidebar nav a:hover svg {
    color: var(--text-strong);
}

/* The open section is told apart by its colour, not by a bar on the edge. */
.admin-sidebar nav a.is-active {
    background: var(--accent-soft);
    color: var(--accent);
}

.admin-sidebar nav a.is-active svg {
    color: var(--accent);
}

.admin-content {
    min-width: 0;
}

.admin-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    margin-bottom: 1.5rem;
}

.admin-header > div {
    min-width: 0;
}

.admin-header h1 {
    margin: 0;
    font-size: 1.55rem;
}

.admin-header p {
    margin: 0.2rem 0 0 0;
    color: var(--text-dim);
    font-size: 0.88rem;
}

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

.permission-item {
    display: flex;
    align-items: flex-start;
    gap: 0.6rem;
    padding: 0.7rem 0.85rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 0.87rem;
}

.permission-item.is-inherited {
    border-style: dashed;
    color: var(--text-dim);
}

.permission-item code {
    display: block;
    color: var(--text-dim);
    font-size: 0.72rem;
    word-break: break-all;
}

/* Anything which only applies while a checkbox above it is ticked. */

.reveal-block {
    transition: border-color 0.2s ease, opacity 0.2s ease;
}

.reveal-block.is-inactive {
    opacity: 0.6;
}

/*
 * A single field out of play, which is the state a punishment with no end
 * leaves its validity in. The reveal blocks grey a whole panel; this greys
 * one field, label and hint included, so that a disabled box is visibly
 * disabled rather than merely unresponsive.
 */
.field.is-inactive > label,
.field.is-inactive > .field__hint {
    opacity: 0.5;
}

.field.is-inactive > input,
.field.is-inactive > select,
.field.is-inactive > textarea,
.field.is-inactive > .select .select__button {
    opacity: 0.5;
    cursor: not-allowed;
}

/*
 * Every disabled control, wherever it is. The browser dims the text of one
 * and nothing else, which on a dark page is not a difference anybody sees.
 */
input:disabled,
select:disabled,
textarea:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* A block big enough to be drawn as a panel of its own. */
.reveal-panel {
    margin-top: 1rem;
    padding: 1rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.reveal-panel.is-inactive {
    border-style: dashed;
}

/*
 * A block dimmed because it is not the one in play, rather than because it is
 * out of bounds. The notice of a lockdown mode which is not switched on is
 * one: it has to stay writable while the other mode is live, or it could only
 * ever be written with the portal already closed behind it.
 *
 * It stays exactly as dim while it is being written in. Brightening under the
 * cursor made the panel flicker between two states on the way past it, and
 * what the dimming says - this is not the notice visitors are getting - does
 * not stop being true because somebody is typing in it. Picking its mode in
 * the switch above is the one thing that brings it back.
 */
.reveal-block--standby.is-inactive {
    opacity: 0.55;
}

/*
 * The state is painted twice on purpose. The class comes from the server and
 * is then rewritten by the script, which is what makes the block right on the
 * very first frame and really enforced afterwards. The rules below paint the
 * same thing straight from the checkbox, so ticking the box still un-greys
 * the block on a page whose script never runs at all.
 *
 * They are kept in a rule of their own because a selector list holding an
 * unsupported :has() is dropped whole, which would take the class rules with
 * it. Each screen names its own boxes: there is no arrangement of the markup
 * that reaches every one of them, and a wrong guess greys the wrong thing.
 */
:root:has(#show_on_team:not(:checked)) #team-profile,
:root:has(#is_published:checked) #post-schedule,
:root:has(#is_published:checked) #post-schedule-time,
:root:has(#is_scheduled:not(:checked)) #post-schedule-time {
    opacity: 0.6;
}

/*
 * The addresses of a punishment are taken away rather than greyed. They are a
 * list to pick from: one nobody is picking from is only in the way of the
 * reason above it, and the fields are disabled with it so that an untick
 * really does let every address go.
 */
#ban-addresses.is-inactive {
    display: none;
}

:root:has(#ban_addresses:not(:checked)) #ban-addresses {
    display: none;
}

/* The same again for the two lockdown notices. */
:root:has(#mode-MAINTENANCE:not(:checked)) #notice-MAINTENANCE,
:root:has(#mode-BETA:not(:checked)) #notice-BETA {
    opacity: 0.55;
}

:root:has(#show_on_team:not(:checked)) #team-profile {
    border-style: dashed;
}

.reveal-block input:read-only,
.reveal-block textarea:read-only,
.reveal-block input:disabled,
.reveal-block select:disabled,
.reveal-block textarea:disabled {
    cursor: not-allowed;
}

/* Ordering row of the navigation administration. */

.nav-row {
    display: flex;
    align-items: center;
    gap: 0.6rem 0.9rem;
    flex-wrap: wrap;
    padding: 0.75rem 0.9rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

.nav-row + .nav-row {
    margin-top: 0.6rem;
}

.nav-row:hover {
    border-color: var(--border-strong);
    background: var(--panel-hover);
}

.nav-row.is-hidden {
    border-style: dashed;
    opacity: 0.6;
}

.nav-row__order {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.2rem;
    height: 2.2rem;
    flex: none;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-family: var(--mono);
    font-size: 0.8rem;
}

.nav-row__main {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    flex: 1 1 12rem;
    min-width: 0;
}

.nav-row__label {
    color: var(--text-strong);
    font-weight: 600;
    word-break: break-word;
}

.nav-row__url {
    color: var(--text-dim);
    font-family: var(--mono);
    font-size: 0.76rem;
    word-break: break-all;
}

.nav-row__badges {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    flex-wrap: wrap;
}

.nav-row .button-row,
.nav-row__actions {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin-left: auto;
}

.nav-row input[type="number"] {
    width: 5rem;
}

/* ---------------------------------------------------------------- Footer */

.site-footer {
    flex: none;
    margin-top: auto;
    padding: 2.5rem 1.25rem 1.5rem 1.25rem;
    background: var(--bg-alt);
    border-top: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.88rem;
}

.site-footer__inner {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: 2rem;
    max-width: var(--content-width);
    margin: 0 auto;
}

.site-footer h4 {
    margin-bottom: 0.6rem;
    color: var(--text-strong);
    font-size: 0.82rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.site-footer ul {
    margin: 0;
    padding: 0;
    list-style: none;
}

.site-footer li + li {
    margin-top: 0.35rem;
}

.site-footer li a {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--text-dim);
}

.site-footer li a:hover {
    color: var(--accent);
}

.site-footer__bottom {
    max-width: var(--content-width);
    margin: 2rem auto 0 auto;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
    text-align: center;
    font-size: 0.82rem;
}

/*
 * The footer of a closed portal, which carries the bottom line and nothing
 * above it. Both hairlines go: the one under the columns has no columns left
 * to separate it from, and the one under the page would draw a second rule a
 * few pixels away from it. It stands on the banner instead.
 */
.site-footer--bare {
    padding: 0.75rem 1.25rem 1.75rem;
    background: transparent;
    border-top: 0;
}

.site-footer--bare .site-footer__bottom {
    margin-top: 0;
    padding-top: 0;
    border-top: 0;
}

.copy-ip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.25rem 0.6rem;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
    background: transparent;
    color: var(--accent);
    font-family: var(--mono);
    font-size: 0.85rem;
    cursor: pointer;
}

.copy-ip:hover {
    border-color: var(--accent);
    background: var(--accent-soft);
}

/* ------------------------------------------------------------ Error pages */

/*
 * The page every failure is drawn on. It wears the whole portal - the menu,
 * the footer with the operator who really runs the site, the account in the
 * corner - and the middle of it reads from the top down: what kind of
 * failure, its number, its name, what it means, and then what can be done
 * about it. A dead end is the one thing it must not be.
 */
.error-page {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 4.5rem 1.5rem 5rem;
    text-align: center;
}

.error-page__glyph {
    display: grid;
    place-items: center;
    width: 66px;
    height: 66px;
    margin-bottom: 0.35rem;
    color: var(--accent);
    background: var(--accent-soft);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.error-page__code {
    font-size: 5rem;
    font-weight: 700;
    line-height: 1;
    color: var(--accent);
    text-shadow: 0 6px 24px rgba(227, 79, 14, 0.35);
}

.error-page h1 {
    margin: 0;
}

.error-page p {
    max-width: 520px;
    color: var(--text-dim);
}

/*
 * The address which went nowhere. It is quoted rather than retold, because
 * a mistyped character is the whole answer and only shows if the text is
 * left exactly as it was asked for - however long, and wrapping instead of
 * pushing the page sideways.
 */
.error-page__address {
    max-width: 100%;
    margin-top: 0.35rem;
    font-size: 0.85rem;
}

.error-page__address code {
    padding: 0.15rem 0.4rem;
    font-family: var(--mono);
    color: var(--text-strong);
    word-break: break-all;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* The name of one single failure, sitting on the clipboard in one click. */
.error-page__reference {
    display: flex;
    flex-direction: column;
    gap: 0.4rem;
    align-items: center;
    margin-top: 0.9rem;
}

.error-page__ticket {
    display: inline-flex;
    gap: 0.5rem;
    align-items: center;
    padding: 0.45rem 0.75rem;
    font: inherit;
    color: var(--text-dim);
    cursor: pointer;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, color 0.15s ease;
}

.error-page__ticket:hover,
.error-page__ticket:focus-visible {
    color: var(--text-strong);
    border-color: var(--border-strong);
}

.error-page__ticket-label {
    font-size: 0.78rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
}

.error-page__ticket-code {
    font-family: var(--mono);
    font-weight: 700;
    color: var(--accent);
    letter-spacing: 0.08em;
}

.error-page__hint {
    font-size: 0.82rem;
}

.error-page__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    justify-content: center;
    margin-top: 1.4rem;
}

/*
 * The way back in. The very menu of the header, so a reader is offered the
 * sections they may actually open - and a visitor is never sent to a page
 * which would only refuse them a second time.
 */
.error-page__elsewhere {
    width: 100%;
    max-width: 620px;
    margin-top: 2.75rem;
    padding-top: 1.75rem;
    border-top: 1px solid var(--border);
}

.error-page__elsewhere h2 {
    margin: 0 0 0.9rem;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

.error-page__elsewhere ul {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: center;
    padding: 0;
    margin: 0;
    list-style: none;
}

.error-page__elsewhere a {
    display: inline-flex;
    gap: 0.4rem;
    align-items: center;
    padding: 0.45rem 0.8rem;
    font-size: 0.88rem;
    color: var(--text);
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.error-page__elsewhere a:hover,
.error-page__elsewhere a:focus-visible {
    color: var(--text-strong);
    background: var(--panel-hover);
    border-color: var(--border-strong);
}

/*
 * Every block of the protection screen stands apart from the next. They used
 * to be flush against one another, which turned four separate answers - what
 * was stopped, where from, what the guard is told to do, who it has been told
 * about - into one continuous wall nobody could find anything in.
 */
.protection-orders,
.protection-blocks,
.protection-rules,
.protection-try-form,
.protection-feeds,
.protection-geo,
.protection-events {
    margin-top: 2.25rem;
}

/* The first block of the screen sits under the heading of the page, which
   already keeps its own distance; the gap is for between two blocks. */
.admin-header + .protection-orders,
.admin-header + .notice + .protection-orders {
    margin-top: 0;
}

/* ---------------------------------------------------------- The guard */

/*
 * The section which says who is being kept out of the portal and why. It
 * borrows every figure it draws from the overview - the same cards, charts
 * and short tables - and adds only what is its own: the switches and numbers
 * the guard goes by, and the form which writes a rule.
 */
.protection-orders h2,
.protection-rules h2 {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin: 0 0 0.35rem;
    font-size: 1.05rem;
}

.protection-orders > .field__hint,
.protection-rules > .field__hint {
    display: block;
    max-width: var(--measure);
    margin-bottom: 1.25rem;
}

/*
 * The switches read as a list of sentences rather than as a row of boxes:
 * each one is a thing the guard either does or does not do, and they are
 * read down the page the way the guard works through them.
 */
/* Each switch carries a line saying what it does, so the rows are two lines
   tall and need more air between them than a column of bare labels would. */
.protection-switches {
    display: grid;
    gap: 0.9rem;
}

@media (min-width: 720px) {
    .protection-switches {
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: 1.75rem;
    }
}

/* The pattern of a rule is quoted exactly, because a stray character in it
   is the difference between a rule which works and one which matches
   nothing. */
.protection-rules code,
.protection-blocks code,
.protection-events code {
    font-family: var(--mono);
    font-size: 0.85rem;
    color: var(--text-strong);
    overflow-wrap: anywhere;
}

/* The form which writes a rule is part of the panel above the list rather
   than a screen of its own: a rule is four short fields, and a page turn to
   type four fields is a page turn nobody wanted. */
.protection-rule-form {
    padding-bottom: 1.5rem;
    margin-bottom: 1.5rem;
    border-bottom: 1px solid var(--border);
}

.protection-events h2,
.protection-blocks h2 {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin: 0 0 0.35rem;
    font-size: 1.05rem;
}

/* How many there are in all, beside the heading rather than under it, so the
   listing says how much of itself is on the page being read. */
.protection-events .count,
.protection-blocks .count,
.protection-rules .count {
    padding: 0.1rem 0.5rem;
    font-family: var(--mono);
    font-size: 0.8rem;
    color: var(--text-dim);
    background: var(--panel-alt);
    border-radius: var(--radius);
}

.protection-events .toolbar {
    padding: 1.25rem 0 0;
}

/* The form says which rule it is changing, rather than looking like the one
   which writes a new one. */
.protection-rule-form__editing {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin: 0 0 1rem;
    font-size: 0.85rem;
    color: var(--accent);
}

/*
 * The heading of a listing and the one button which acts on all of it, side
 * by side while there is room and one under the other when there is not.
 */
.protection-section__head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.5rem;
    align-items: flex-start;
    justify-content: space-between;
}

.protection-section__head > div {
    min-width: 0;
    flex: 1 1 22rem;
}

.protection-section__head .field__hint {
    display: block;
    max-width: var(--measure);
    margin: 0;
}

.protection-blocks .toolbar,
.protection-events .toolbar {
    padding: 1.25rem 0 0;
}

/* How many of each the filter would show, inside the chip rather than after
   it, so a reason nobody was stopped for is plainly nought. */
.chip__count {
    padding: 0 0.4rem;
    font-family: var(--mono);
    font-size: 0.72rem;
    color: var(--text-dim);
    background: var(--panel-alt);
    border-radius: var(--radius);
}

.chip.is-active .chip__count {
    color: inherit;
}

/* An address is a way into everything else it did: pointing it at the list of
   refusals filtered to it. Drawn as the address rather than as a link. */
.protection-address {
    text-decoration: none;
}

.protection-address:hover code,
.protection-address:focus-visible code {
    color: var(--accent);
    text-decoration: underline;
    text-underline-offset: 0.2em;
}

.protection-events__reason,
.protection-events__who {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    align-items: center;
}

.protection-events__reason .chip {
    min-height: 0;
    padding: 0.2rem 0.55rem;
    cursor: default;
}

/*
 * What was asked for. The method is a small tag in front of the path, and
 * the path is cut at the edge of its column rather than wrapping a long probe
 * over five lines. The browser the request named itself as is what it says
 * when somebody points at it, which the dotted edge promises.
 */
.protection-events__request {
    display: inline-flex;
    gap: 0.45rem;
    align-items: baseline;
    max-width: 28rem;
    min-width: 0;
    cursor: help;
    border-bottom: 1px dotted var(--border-strong);
}

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

.protection-events__method {
    flex: none;
    padding: 0.05rem 0.4rem;
    font-family: var(--mono);
    font-size: 0.68rem;
    font-weight: 700;
    color: var(--text-dim);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.protection-events code.protection-events__path {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    overflow-wrap: normal;
}

/* A number rather than a sentence, so it lines up down the column. */
.protection-blocks__stopped {
    font-family: var(--mono);
    font-size: 0.9rem;
    color: var(--text-strong);
}

.protection-try__asked,
.protection-try__answer {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.4rem 1rem;
    margin: 0 0 1.25rem;
    font-size: 0.9rem;
}

.protection-try__asked dt,
.protection-try__answer dt {
    color: var(--text-dim);
}

.protection-try__asked dd,
.protection-try__answer dd {
    margin: 0;
    overflow-wrap: anywhere;
}

.protection-try-form h2,
.protection-feeds h2 {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin: 0 0 0.35rem;
    font-size: 1.05rem;
}

.protection-feeds__suggested {
    margin: 2rem 0 0.35rem;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* The offered lists fill the form in rather than being added by pressing
   them, so they are buttons which look like the chips they sit among. */
.protection-feeds__offers {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem;
    padding: 0;
    margin: 0 0 1.5rem;
    list-style: none;
}

/* Offered as buttons, because pressing one fills the form in rather than
   going anywhere - so they are told to stop looking like the grey boxes a
   browser draws a button as, and to look like the chips they sit among. */
.protection-feeds__offers .chip {
    font: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    background: var(--panel);
    cursor: pointer;
}

.protection-feeds__offers .chip:hover {
    background: var(--panel-hover);
}

.protection-feed-form {
    padding-top: 1.5rem;
    border-top: 1px solid var(--border);
}

.protection-geo h2 {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin: 0 0 0.35rem;
    font-size: 1.05rem;
}

.protection-geo__sources {
    display: grid;
    gap: 0.8rem;
    margin: 1.25rem 0;
}

@media (min-width: 720px) {
    .protection-geo__sources {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

.protection-geo__source {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 0.5rem 1rem;
    align-items: center;
    padding: 0.85rem 1rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.protection-geo__what {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    align-items: flex-start;
    min-width: 0;
}

.protection-geo__state {
    min-height: 0;
    padding: 0.15rem 0.5rem;
    font-size: 0.72rem;
    cursor: default;
}

/*
 * How far a reading has got. Drawn only while one is going on, and driven
 * from what the portal wrote down rather than from a guess about how long
 * the work takes - a bar which is really a timer lies the first time the
 * network is slow.
 */
.protection-geo__progress {
    grid-column: 1 / -1;
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
}

.protection-geo__track {
    height: 0.35rem;
    overflow: hidden;
    background: var(--border);
    border-radius: 999px;
}

.protection-geo__bar-inner {
    display: block;
    width: 0;
    height: 100%;
    background: var(--accent);
    border-radius: 999px;
    transition: width 0.4s ease;
}

/*
 * The world. One path per country, drawn once into a template rather than
 * assembled in the browser: the alternative is a mapping library and a
 * quarter of a megabyte of geometry sent to everybody who opens the screen,
 * for a picture which never changes.
 *
 * A country carries how busy it has been as a number between nought and one,
 * and the colour is mixed from it. A country nobody has been stopped from
 * keeps the colour of the land.
 */
.protection-geo__map {
    padding: 1rem;
    margin-top: 1.5rem;
    overflow: hidden;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* Narrower than the panel: the map says where at a glance, and drawn the
   full width of the screen it pushed the countries under it out of sight. */
.world {
    display: block;
    width: 100%;
    max-width: 40rem;
    height: auto;
    margin-inline: auto;
}

.world__country {
    fill: var(--panel-hover);
    stroke: var(--bg);
    stroke-width: 0.7;
    stroke-linejoin: round;
    transition: fill 0.2s ease;
}

/*
 * Mixed rather than switched: a country which was stopped once is barely
 * warmer than the land around it, and one which was stopped all day is the
 * accent. Between them the colour says how much without a legend.
 */
.world__country[style*="--share"] {
    fill: color-mix(in srgb, var(--accent) calc(var(--share, 0) * 100%), var(--panel-hover));
}

/* Pointing at a country names it - the shape carries a title the browser
   shows on its own, which needs no script and works for a reader who is
   tabbing through rather than pointing. */
.world__country:hover,
.world__country:focus {
    fill: var(--brand-yellow);
    outline: none;
}

/*
 * Four rows, and the rest scrolled to. Every row is given the same height -
 * the button which shuts a country out is there on most of them and missing
 * on the one for addresses nobody could place - so four rows are exactly
 * four rows tall rather than four and a bit.
 */
.protection-geo__bars {
    --geo-row: 3.1rem;
    --geo-gap: 0.35rem;

    display: flex;
    flex-direction: column;
    gap: var(--geo-gap);
    max-height: calc(4 * var(--geo-row) + 3 * var(--geo-gap));
    padding: 0 0.35rem 0 0;
    margin: 1.5rem 0 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    list-style: none;
}

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

.protection-geo__bar {
    position: relative;
    display: grid;
    flex: none;
    grid-template-columns: 1fr auto auto;
    gap: 0.75rem;
    align-items: center;
    height: var(--geo-row);
    padding: 0 0.75rem;
    overflow: hidden;
    background: var(--panel-alt);
    border-radius: var(--radius);
}

/* The bar itself is drawn behind the words rather than beside them, so a
   long country name never squeezes the reading out. */
.protection-geo__bar::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    z-index: 0;
    width: var(--share, 0%);
    background: var(--accent-soft);
}

.protection-geo__place,
.protection-geo__count,
.protection-geo__bar form {
    position: relative;
    z-index: 1;
}

.protection-geo__place {
    font-size: 0.9rem;
    color: var(--text-strong);
}

.protection-geo__count {
    font-family: var(--mono);
    font-size: 0.85rem;
    color: var(--text-dim);
}

/* ------------------------------------------------------ The mail of the team */

/*
 * The form which writes a letter to many people, read from the top down in
 * the order it is written: who it goes to, what it says, the drafts it can be
 * started from. Each part is headed the same way and kept a line apart.
 */
.mail-compose__section + .mail-compose__section {
    margin-top: 1.75rem;
}

.mail-compose__heading {
    display: flex;
    gap: 0.5rem;
    align-items: center;
    margin: 0 0 0.9rem;
    font-size: 1.05rem;
}

/* The heading of the letter and how much of it is translated, on one line. */
.mail-compose__head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 1rem;
    align-items: center;
    justify-content: space-between;
    margin: 0 0 0.9rem;
}

.mail-compose__head .mail-compose__heading {
    margin: 0;
}

.mail-compose__subheading {
    margin: 0 0 0.25rem;
    font-size: 0.95rem;
    color: var(--text-strong);
}

/* The two ways of reaching people stand side by side while there is room. */
@media (min-width: 720px) {
    .mail-delivery {
        grid-template-columns: repeat(2, minmax(0, 1fr));
    }
}

/* The chosen way is marked by the choice itself, whether or not a script ran. */
.mail-delivery .mode-choice__item:has(input:checked) {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

.mail-compose__block {
    margin-top: 1rem;
}

/* How many people the letter will reach, said under the choice which decides it. */
.mail-compose__reach {
    display: flex;
    gap: 0.45rem;
    align-items: baseline;
    margin: 0.9rem 0 0;
    font-size: 0.85rem;
    color: var(--text-dim);
}

.mail-compose__reach svg {
    flex: none;
    position: relative;
    top: 0.1rem;
}

.mail-compose__reach .is-invalid {
    color: var(--danger);
}

/* Which languages a letter was written in, as the codes the blocks carry. */
.mail-languages {
    display: flex;
    flex-wrap: wrap;
    gap: 0.3rem;
    margin-top: 0.35rem;
}

.mail-compose__languages {
    display: flex;
    gap: 0.4rem;
    align-items: baseline;
    margin: 0 0 0.9rem;
}

.mail-campaign__language + .mail-campaign__language {
    margin-top: 0.6rem;
}

.mail-campaign__language .mail-frame {
    border-radius: var(--radius);
}

.mail-compose__placeholders {
    display: block;
    margin-top: 0.6rem;
}

.mail-compose__placeholders code {
    padding: 0.05rem 0.35rem;
    font-family: var(--mono);
    font-size: 0.8rem;
    color: var(--text-strong);
    background: var(--panel-alt);
    border-radius: var(--radius);
}

/*
 * The drafts under the editor. A button per draft rather than a select: the
 * name alone rarely says which one is wanted, and the line under it does.
 */
.mail-templates__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 0.7rem;
    margin-top: 0.9rem;
}

.mail-template {
    display: flex;
    gap: 0.75rem;
    align-items: flex-start;
    width: 100%;
    padding: 0.8rem 0.9rem;
    font: inherit;
    color: var(--text);
    text-align: left;
    cursor: pointer;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, background 0.15s ease;
}

.mail-template:hover,
.mail-template:focus-visible {
    border-color: var(--accent);
    outline: none;
}

.mail-template.is-active {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

.mail-template__icon {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 2rem;
    height: 2rem;
    color: var(--accent);
    background: var(--accent-soft);
    border-radius: var(--radius);
}

.mail-template__body {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

.mail-template__name {
    font-weight: 600;
    color: var(--text-strong);
}

.mail-template__body small {
    font-size: 0.78rem;
    line-height: 1.45;
    color: var(--text-dim);
}

.mail-compose__actions {
    margin-top: 1.75rem;
}

/*
 * The letter itself, in a frame of its own: the portal's stylesheet stays
 * out of it, so the preview is the letter somebody receives.
 */
.mail-frame {
    display: block;
    width: 100%;
    min-height: 12rem;
    background: #efede9;
    border: 0;
}

/*
 * The letter a setting sends, named under its switches with the button which
 * shows it - one row, the words on the left and the button at the edge.
 */
.setting-letter {
    display: flex;
    flex-wrap: wrap;
    gap: 0.75rem 1.25rem;
    align-items: center;
    justify-content: space-between;
    margin-top: 1.25rem;
    padding: 0.85rem 1rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.setting-letter__text {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

.setting-letter__text strong {
    color: var(--text-strong);
}

.letter-dialog__head {
    display: flex;
    gap: 1rem;
    align-items: center;
    justify-content: space-between;
}

/* The letter sits on the grey of an inbox, framed like one, whatever the tab. */
.letter-dialog__frame {
    margin-top: 0.9rem;
    overflow: hidden;
    background: #efede9;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* Inside tabs the gap of the tabs already spaces it. */
.tabs .letter-dialog__frame {
    margin-top: 0;
}

.letter-dialog__waiting {
    margin: 0;
    padding: 1.25rem;
    color: #55524e;
}

.mail-compose .editor__preview {
    padding: 0;
}

.mail-campaign__letter .mail-frame {
    margin-top: 0.9rem;
    border-radius: var(--radius);
}

/* How far a campaign has got: its state and count over a thin bar. */
.mail-progress {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    min-width: 9rem;
}

.mail-progress__head {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    align-items: center;
}

.mail-progress__count {
    font-family: var(--mono);
    font-size: 0.82rem;
    color: var(--text-strong);
}

.mail-progress__track {
    display: block;
    height: 0.35rem;
    overflow: hidden;
    background: var(--border);
    border-radius: 999px;
}

.mail-progress__bar {
    display: block;
    height: 100%;
    background: var(--accent);
    border-radius: 999px;
    transition: width 0.4s ease;
}

.mail-campaign__state {
    display: flex;
    flex-wrap: wrap;
    gap: 1rem 1.5rem;
    align-items: center;
    justify-content: space-between;
    margin-top: 1.5rem;
}

.mail-progress--wide {
    flex: 1 1 20rem;
}

.mail-campaign__grid {
    margin-top: 1.5rem;
    align-items: start;
}

.mail-campaign__facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.5rem 1rem;
    margin: 0 0 1rem;
    font-size: 0.9rem;
}

.mail-campaign__facts dt {
    color: var(--text-dim);
}

.mail-campaign__facts dd {
    margin: 0;
    color: var(--text-strong);
    overflow-wrap: anywhere;
}

.mail-campaign__letters {
    margin-top: 2.25rem;
}

.mail-campaign__letters .toolbar {
    padding: 1rem 0 0;
}

.mail-campaign__letters .count {
    padding: 0.1rem 0.5rem;
    font-family: var(--mono);
    font-size: 0.8rem;
    color: var(--text-dim);
    background: var(--panel-alt);
    border-radius: var(--radius);
}

/* ------------------------------------------------------------ Signing up */

/*
 * Opening an account, in the same column the sign in page stands in. Four
 * values one under the other rather than two abreast: the fields carry a
 * line of explanation each, which made every row of a grid a different
 * height, and the fourth one dropped to a row of its own the moment the
 * screen was wide enough for three.
 *
 * The rhythm is looser than a form of bare inputs needs, because each field
 * here is a small block - a label, a box, a sentence under it, and beneath
 * the password a meter which appears as somebody types.
 */
.signup__fields {
    display: flex;
    flex-direction: column;
    gap: 1.4rem;
}

/* The icon belongs to the label and carries none of its weight. */
.signup__fields .field > label svg {
    color: var(--text-dim);
}

/* Stacked rather than in a row, so the two read the same in every language. */
.signup__actions {
    flex-direction: column;
    align-items: stretch;
    gap: 0.6rem;
}

/*
 * What somebody is agreeing to by filling the form in. Below the panel
 * rather than inside it, where the sign in page keeps the same kind of
 * aside, so the two screens end the same way.
 */
.signup__terms {
    margin-top: 1.5rem;
    margin-bottom: 0;
}

/* ------------------------------------------------------ Confirming an address */

/*
 * What the link of the letter did, said once and in the middle of the column:
 * a symbol in the colour of the outcome, a heading, a sentence and the one
 * button which leads on. The colour stays with the symbol; the panel itself
 * is framed like every other one. The form asking for another letter stands under it
 * as a panel of its own, so it reads as the way out rather than as the page.
 */
.verify__card {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    align-items: center;
    padding: 2.2rem 1.75rem 2rem;
    text-align: center;
}

.verify__card--ok {
    --verify-tone: var(--ok);
}

.verify__card--warn {
    --verify-tone: var(--warn);
}

.verify__card--bad {
    --verify-tone: var(--danger);
}

.verify__glyph {
    display: grid;
    place-items: center;
    width: 66px;
    height: 66px;
    margin-bottom: 0.35rem;
    color: var(--verify-tone, var(--accent));
    background: color-mix(in srgb, var(--verify-tone, var(--accent)) 14%, transparent);
    border: 1px solid var(--border);
    border-radius: 50%;
}

.verify__title {
    margin: 0;
    font-size: 1.6rem;
    color: var(--text-strong);
}

.verify__card > p {
    max-width: 42ch;
    margin: 0;
    color: var(--text-dim);
}

/* The three things left to do, in the order they are done. */
.verify__steps {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    width: 100%;
    margin: 0.9rem 0 0;
    padding: 0;
    text-align: left;
    list-style: none;
}

.verify__steps li {
    display: flex;
    gap: 0.65rem;
    align-items: center;
    padding: 0.6rem 0.8rem;
    color: var(--text);
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.verify__steps svg {
    flex: none;
    color: var(--accent);
}

.verify__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    justify-content: center;
    margin-top: 0.9rem;
}

.verify__resend {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    margin-top: 1.25rem;
}

.verify__resend-title {
    margin: 0;
    font-size: 1.05rem;
}

.verify__resend .field__hint {
    margin: -0.6rem 0 0;
}

/* The address of an account with whether it was ever confirmed beside it. */
.user-card__email {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.6rem;
    align-items: center;
    margin-top: 0.55rem;
}

/* An address on the profile which is still waiting for its letter to be followed. */
.profile-verify {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem 0.8rem;
    align-items: center;
    justify-content: space-between;
    margin-top: 0.5rem;
    padding: 0.55rem 0.75rem;
    background: color-mix(in srgb, var(--warn) 10%, transparent);
    border: 1px solid color-mix(in srgb, var(--warn) 35%, transparent);
    border-radius: var(--radius);
}

.profile-verify .field__hint {
    margin: 0;
}

/* A sign in refused for want of a confirmed address, with the way out beside the reason. */
.login-unverified {
    display: flex;
    flex-wrap: wrap;
    gap: 0.6rem;
    align-items: center;
    justify-content: space-between;
    margin: -0.4rem 0 1.25rem;
}

.login-unverified .field__hint {
    margin: 0;
}

/* ------------------------------------------------------------- Lockdown */

/*
 * A portal which is closed is still the portal, so the page in front of it
 * wears the front door of the site: the banner of the home page.
 *
 * It covers the top of the screen and nothing more. The last stop of the
 * gradient is the background of the page itself, so the picture does not end
 * at an edge - it simply becomes the page, and the notice reads on plain dark
 * from there down. Drawn behind the content and fixed, so scrolling a long
 * notice does not drag the sky with it.
 */
body.is-locked::before {
    content: "";
    position: fixed;
    top: 0;
    right: 0;
    left: 0;
    z-index: -1;
    height: 58vh;
    background-image: linear-gradient(to bottom, rgba(19, 18, 17, 0.32) 0%, rgba(19, 18, 17, 0.72) 52%, #131211 100%),
        url("/static/assets/img/banner.jpg");
    background-position: center top;
    background-size: cover;
    background-repeat: no-repeat;
    pointer-events: none;
}

/*
 * The bar of a closed portal: who we are, and the language the page is read
 * in. No menu - it would list the sections which were just closed - and no
 * background of its own, so it sits on the banner rather than on a strip.
 */
.lockdown-bar {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    width: 100%;
    max-width: var(--content-width);
    margin: 0 auto;
    padding: 1rem 1.25rem 0;
}

/*
 * A short notice belongs in the middle of a tall screen, not pinned to the
 * top with the rest of it left empty. `safe` is what stops a notice taller
 * than the screen from being centred into its own scrolled-off top edge; a
 * browser which does not know the keyword drops the line and leaves the
 * notice at the top, which is the harmless half of the choice.
 */
body.is-locked .page {
    display: flex;
    flex-direction: column;
    justify-content: center;
    justify-content: safe center;
    /* The bar above is a single chip; the notice starts under it, not under
       the space a header would have taken. */
    margin-top: -2.5rem;
}

.lockdown {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.7rem;
    width: min(100%, 600px);
    margin: 0 auto;
    padding: 0 1.25rem;
}

/*
 * On a screen with room to spare the column takes all of it and hands the
 * spare height to the three automatic margins below - above the logo, under
 * it, and above the picture. The logo therefore floats in the middle of the
 * empty band at the top whether the notice below it is a tall beta card or a
 * two line maintenance one, and the picture stays down on the footer instead
 * of drifting up with the rest. On a screen with nothing to spare every one
 * of those margins collapses to nothing and the column reads as it always
 * did, top to bottom.
 */
body.is-locked .lockdown {
    flex: 1 1 auto;
}

/*
 * The few pages which stay readable while the portal is closed - the terms,
 * and whatever else the lock lets through - carry a hero of their own, with
 * the banner already in it. For those the backdrop above would be the second
 * picture of the same thing, with the text of the page floating on it, so it
 * steps aside and the page flows from the top the way it always does.
 */

body.is-locked:has(.hero)::before {
    display: none;
}

body.is-locked:has(.hero) .page {
    display: block;
    margin-top: 0;
}

/*
 * The notice itself. Translucent rather than solid, so the banner stays
 * readable behind it and the card looks like it belongs to the picture
 * instead of being pasted on top of it.
 */
.lockdown__card {
    width: 100%;
    padding: 2rem 2rem 1.75rem;
    background: rgba(24, 22, 21, 0.82);
    border: 1px solid var(--border);
    border-radius: calc(var(--radius) * 2);
    /* The drop below it, and the one line of light along the top edge which
       keeps the card from reading as a hole cut in the banner. */
    box-shadow:
        0 24px 60px -30px rgba(0, 0, 0, 0.95),
        inset 0 1px 0 rgba(255, 255, 255, 0.06);
    backdrop-filter: blur(6px);
    text-align: center;
}

/*
 * Above the card, not inside it: the card is the message.
 *
 * A third of the logo file is empty sky under the drawing, which on a page
 * this short reads as a hole between the logo and the notice. The box is
 * given the aspect the drawing itself has and the picture is anchored to the
 * top of it, so the empty strip is cropped away rather than paid for in
 * whitespace - and the logo can be drawn larger for the same gap.
 */
.lockdown__logo {
    display: block;
    width: min(424px, 92%);
    height: auto;
    aspect-ratio: 2000 / 1090;
    /* At the top of the column, with the notice right under it: the spare
       height of the screen belongs to the picture at the bottom, so a short
       maintenance notice sits where a tall beta one does instead of drifting
       into the middle of the page. The notice keeps its distance below: the
       logo is the name over the door, not part of what is written on it. */
    margin: 0 auto 1.4rem;
    object-fit: cover;
    object-position: top center;
    filter: drop-shadow(0 8px 26px rgba(0, 0, 0, 0.75));
}

/*
 * Somebody at work at the bottom of the page, walking a cable over to the
 * reactor. It stands on the line where the notice ends and the footer
 * begins, off to one side, so it reads as a detail of the scene rather than
 * as an illustration somebody centred.
 */
.lockdown__crew {
    display: block;
    /* Sized off the width of the screen rather than of the notice: the notice
       stops growing at 600px and the picture would then sit next to a phone
       sized column at full size. It never grows past what it was drawn for,
       and never shrinks below the point where the man stops being a man. */
    width: clamp(155px, 30vw, 272px);
    height: auto;
    /* The spare height of the screen goes above it, so it settles down at
       the bottom of the page rather than hanging under the notice. */
    margin: auto 0 0 auto;
    /* Out past the notice and towards the edge of the screen - but only as
       far as the room beside the notice allows, or a narrow window would
       scroll sideways to show empty space. */
    margin-right: clamp(-16rem, calc((600px - 100vw) / 2 + 0.75rem), 0rem);
}

/*
 * On a screen with height to spare the picture is lifted a step off the
 * footer, and given back the height it leaves behind so the page does not
 * grow a scrollbar for it. The lift is asked for by height rather than by
 * width on purpose: in a window too short for the notice there is no spare
 * room to lift into, and the picture would be pulled up into the form.
 */
@media screen and (min-width: 561px) and (min-height: 780px) {
    .lockdown__crew {
        position: relative;
        bottom: 2.5rem;
        margin-bottom: -1.25rem;
    }
}

/* Which of the two notices this is, read before the heading is. */
.lockdown__badge {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin-bottom: 1rem;
    padding: 0.25rem 0.75rem;
    background: rgba(227, 79, 14, 0.1);
    border: 1px solid var(--accent);
    border-radius: 999px;
    color: var(--accent);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.lockdown__title {
    margin-bottom: 0.75rem;
    font-size: clamp(1.45rem, 1.1rem + 1.5vw, 1.95rem);
    line-height: 1.2;
}

.lockdown__message {
    max-width: 46ch;
    margin: 0 auto;
    color: var(--text-dim);
    line-height: 1.7;
}

.lockdown__message p {
    margin: 0 0 0.8rem 0;
}

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

/* The only field on the page: an address, and the button which leaves it. */
.lockdown__form {
    margin-top: 1.5rem;
    padding: 1.25rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    text-align: left;
}

.lockdown__label {
    display: block;
    margin-bottom: 0.45rem;
    color: var(--text-strong);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.lockdown__row {
    display: flex;
    gap: 0.6rem;
}

.lockdown__row input {
    flex: 1 1 auto;
    min-width: 0;
}

.lockdown__row .button {
    flex: none;
}

.lockdown__hint {
    display: block;
    margin-top: 0.5rem;
    color: var(--text-dim);
    font-size: 0.8rem;
    line-height: 1.5;
}

.lockdown__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    margin-top: 1.6rem;
}

/*
 * Discreet on purpose. The way in belongs to the few people who have one, and
 * a loud button would invite everybody else to keep trying.
 */
.lockdown__signin {
    color: var(--text-dim);
    background: transparent;
    border-color: transparent;
}

.lockdown__signin:hover,
.lockdown__signin:focus-visible {
    color: var(--text-strong);
    border-color: var(--border);
}

/* Said to the administrator looking at a notice, never to a visitor. */
.lockdown__preview {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.55rem;
    width: 100%;
    padding: 0.6rem 1rem;
    background: rgba(24, 22, 21, 0.82);
    border: 1px dashed var(--accent);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.82rem;
    text-align: center;
}

.lockdown__preview .icon {
    flex: none;
    color: var(--accent);
}

@media screen and (max-width: 560px) {
    .lockdown__card {
        padding: 2rem 1.25rem 1.75rem;
    }

    /*
     * There is no room beside the notice left to lean into, only the padding
     * of the column itself. The page scrolls on a phone anyway, so lifting
     * the picture off the footer buys nothing and would only push it into the
     * form above it - it stays in the flow, and smaller, a detail at the foot
     * of the notice rather than a second illustration of it.
     */
    .lockdown__crew {
        position: static;
        width: clamp(120px, 34vw, 190px);
        margin: auto -1rem 0 auto;
    }

    .lockdown__row {
        flex-wrap: wrap;
    }

    .lockdown__row .button {
        width: 100%;
        justify-content: center;
    }
}

/*
 * Which notice stands in front of the portal, as three cards rather than a
 * dropdown: the choice is worth reading before it is made, and each one says
 * what it does underneath its name.
 */
.mode-choice {
    display: grid;
    gap: 0.75rem;
}

.mode-choice__item {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.9rem 1rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    cursor: pointer;
}

.mode-choice__item:hover {
    border-color: var(--accent);
}

.mode-choice__item.is-active {
    border-color: var(--accent);
    box-shadow: inset 0 0 0 1px var(--accent);
}

.mode-choice__item input {
    flex: none;
    margin-top: 0.2rem;
}

.mode-choice__body {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    min-width: 0;
}

.mode-choice__name {
    display: inline-flex;
    align-items: center;
    gap: 0.45rem;
    color: var(--text-strong);
    font-weight: 600;
}

.mode-choice__body small {
    color: var(--text-dim);
    font-size: 0.8rem;
    line-height: 1.5;
}

@media screen and (max-width: 560px) {
    .lockdown__row {
        flex-wrap: wrap;
    }

    .lockdown__row .button {
        width: 100%;
        justify-content: center;
    }
}

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

@media screen and (max-width: 1024px) {
    .admin-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    .admin-sidebar {
        position: static;
    }

    .admin-sidebar nav {
        flex-direction: row;
        flex-wrap: wrap;
    }

    .post-hero {
        grid-template-columns: 1.05fr 1fr;
    }
}

/* One step of compression before the bar gives up and becomes a drawer. */
@media screen and (max-width: 1360px) {
    .site-header {
        gap: 0.85rem;
    }

    .site-nav__link {
        gap: 0.32rem;
        padding: 0.4rem 0.5rem;
        font-size: 0.76rem;
    }
}

/*
 * The bar collapses well before the entries would start wrapping, the exact
 * width comes from the widest possible menu plus the account chip.
 */
@media screen and (max-width: 1180px) {
    .site-header {
        gap: 0.75rem;
    }

    .site-header__toggle {
        display: inline-flex;
    }

    /*
     * The navigation leaves the flow, so the account area takes the slack and
     * pushes both itself and the burger to the right edge of the bar.
     */
    .site-header__actions {
        margin-left: auto;
    }

    /* Sits under the header and covers the rest of the viewport. */
    .site-nav {
        position: fixed;
        top: var(--header-height);
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 55;
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 0;
        margin: 0;
        padding: 1rem 1.1rem 2rem 1.1rem;
        background: var(--bg-alt);
        border-top: 1px solid var(--border);
        overflow-x: hidden;
        overflow-y: auto;
        overscroll-behavior: contain;
        transform: translateX(100%);
        visibility: hidden;
        transition: transform 0.25s ease, visibility 0.25s ease;
    }

    .site-nav.is-open {
        transform: none;
        visibility: visible;
    }

    .site-nav__list {
        display: flex;
        flex-direction: column;
        align-items: stretch;
        gap: 0.2rem;
        width: 100%;
    }

    .site-nav__link {
        min-height: 48px;
        padding: 0.8rem 0.85rem;
        border: 1px solid transparent;
        font-size: 0.95rem;
    }

    .site-nav__link .icon:first-child {
        width: 18px;
        height: 18px;
        margin-right: 0.2rem;
    }

    .site-nav__link.is-active {
        border-color: rgba(227, 79, 14, 0.35);
    }

    .site-nav__mobile-actions {
        display: flex;
        flex-direction: column;
        gap: 0.5rem;
        margin-top: 1.1rem;
        padding-top: 1.1rem;
        border-top: 1px solid var(--border);
    }

    .site-nav__mobile-actions .button,
    .site-nav__mobile-actions form,
    .site-nav__mobile-actions form .button {
        width: 100%;
        min-height: 44px;
        justify-content: center;
    }

    /*
     * The login buttons move into the drawer, the chip and the bell stay in
     * the bar. Only the direct children are hidden, the buttons inside the
     * notification panel belong to the panel.
     */
    .site-header__actions > .button {
        display: none;
    }

    .user-chip__name {
        max-width: 8rem;
    }
}

@media screen and (max-width: 560px) {
    .site-header__brand span {
        display: none;
    }

    .user-chip {
        padding: 0.25rem;
        border-color: transparent;
    }

    .user-chip__name,
    .user-chip__caret {
        display: none;
    }

    .user-menu__panel {
        position: fixed;
        top: calc(var(--header-height) + 0.4rem);
        right: 0.75rem;
        left: 0.75rem;
        min-width: 0;
    }
}


@media screen and (max-width: 860px) {
    .post-hero {
        grid-template-columns: minmax(0, 1fr);
    }

    .post-hero__media {
        min-height: 0;
    }

    .post-hero__media img {
        aspect-ratio: 16 / 9;
        min-height: 0;
    }

    .post-hero__body {
        padding: 1.25rem;
    }

    .post-hero__title {
        font-size: 1.35rem;
    }

    .post-nav {
        grid-template-columns: minmax(0, 1fr);
    }

    .post-nav__item--next {
        text-align: left;
        align-items: flex-start;
    }
}

@media screen and (max-width: 640px) {
    body {
        font-size: 15px;
    }

    .hero {
        min-height: 17rem;
        padding: 1.75rem 1rem 3rem;
    }

    .wrapper {
        padding: 1.75rem 1rem;
    }

    .panel {
        padding: 1.1rem;
    }

    .panel__body {
        padding: 1rem;
    }

    h1 {
        font-size: 1.6rem;
    }

    h2 {
        font-size: 1.35rem;
    }

    .post-grid {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Chips wrap into a compact block, a sideways scroller reads as a bug. */
    .chip-row,
    .filter-chips {
        width: 100%;
        gap: 0.35rem;
    }

    .chip-row .chip,
    .filter-chips .chip {
        min-height: 34px;
        padding: 0.25rem 0.7rem;
        font-size: 0.74rem;
    }

    .post-article {
        padding: 1.15rem;
    }

    .post-article__figure img,
    .post-article__image {
        max-height: 240px;
    }

    .flash-stack {
        left: 1rem;
        right: 1rem;
        max-width: none;
    }

    .admin-layout {
        padding: 1.25rem 1rem;
    }

    /* The sidebar navigation becomes a scrollable chip row. */
    .admin-sidebar {
        border-radius: var(--radius);
    }

    .admin-sidebar__title {
        padding: 0.7rem 1rem;
    }

    .admin-sidebar nav {
        flex-direction: row;
        flex-wrap: nowrap;
        gap: 0.4rem;
        padding: 0.6rem;
        overflow-x: auto;
        overscroll-behavior-x: contain;
        -webkit-overflow-scrolling: touch;
        scroll-snap-type: x proximity;
    }

    .admin-sidebar nav a {
        flex: none;
        min-height: 40px;
        padding: 0.5rem 0.85rem;
        border: 1px solid var(--border);
        border-radius: var(--radius);
        white-space: nowrap;
        scroll-snap-align: start;
    }

    .admin-sidebar nav a.is-active {
        border-color: var(--accent);
    }

    /* Stacked table cards, the labels come from `data-label`. */
    .data.data--stack,
    .data.data--stack tbody,
    .data.data--stack tr,
    .data.data--stack td {
        display: block;
        width: 100%;
    }

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

    .data.data--stack tbody tr {
        margin-bottom: 0.75rem;
        padding: 0.35rem 0.85rem;
        background: var(--panel-alt);
        border: 1px solid var(--border);
        border-radius: var(--radius);
    }

    .data.data--stack tbody tr:last-child {
        margin-bottom: 0;
    }

    .data.data--stack tbody tr:hover {
        background: var(--panel-hover);
    }

    .data.data--stack td {
        display: flex;
        align-items: center;
        justify-content: flex-start;
        gap: 0.4rem 0.5rem;
        flex-wrap: wrap;
        padding: 0.5rem 0;
        border-bottom: 1px dashed var(--border);
        text-align: right;
    }

    .data.data--stack td:last-child,
    .data.data--stack tbody tr:last-child td:last-child {
        border-bottom: 0;
    }

    /* The label stays on the left, the cell content groups on the right. */
    .data.data--stack td[data-label]::before {
        content: attr(data-label);
        flex: 0 0 auto;
        margin-right: auto;
        padding-right: 0.75rem;
        color: var(--text-dim);
        font-size: 0.68rem;
        font-weight: 700;
        letter-spacing: 0.09em;
        text-transform: uppercase;
        text-align: left;
    }

    .data.data--stack td.actions {
        justify-content: flex-end;
        white-space: normal;
    }

    .data.data--stack td.actions .button-row {
        justify-content: flex-end;
    }

    .table-wrap:has(.data--stack) {
        overflow-x: visible;
    }
}

@media screen and (max-width: 430px) {
    .wrapper {
        padding: 1.5rem 0.85rem;
    }

    .site-header {
        padding: 0 0.85rem;
        gap: 0.6rem;
    }

    .post-card__body {
        padding: 1rem;
    }

    .button {
        padding: 0 0.9rem;
    }

    .hero__actions .button,
    .hero__actions .copy-ip {
        width: 100%;
        justify-content: center;
    }
}

/* --------------------------------------------------------- Accessibility */

/*
 * One indicator per control, never two.
 *
 * The ring is drawn only for `:focus-visible`, so it answers a keyboard and
 * not a mouse click - which is what stopped every tick of a checkbox from
 * framing itself.
 */
:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/*
 * A text field, a select and a textarea already say they are focused by
 * colouring their own border, for the mouse and the keyboard alike. The ring
 * on top of that drew a second frame standing two pixels off the first, which
 * is what made a focused select look wrong.
 */
input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus-visible,
select:focus-visible,
textarea:focus-visible {
    outline: none;
}

/*
 * Where the control itself is off the screen, the ring goes on the thing
 * standing in for it: the upload box owns a clipped file input, and a tile of
 * the icon grid owns a hidden radio.
 */
label.upload:has(:focus-visible),
label.icon-choice:has(:focus-visible) {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }

    .post-card:hover,
    .post-nav__item:hover,
    .team-card:hover,
    .release-card:hover,
    .release-card:hover .release-card__image,
    .back-to-top {
        transform: none;
    }

    .post-card:hover .post-card__image,
    .post-hero:hover .post-hero__media img {
        transform: none;
    }
}

@media print {
    .site-header,
    .site-footer,
    .flash-stack,
    .back-to-top,
    .pagination,
    .button-row {
        display: none !important;
    }

    body {
        background: #fff;
        color: #000;
    }
}


/* The featured post keeps one media box, never a box inside a box. */
.post-hero__media {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border-radius: var(--radius);
    background: var(--panel-alt);
}


/* --------------------------------------------------- Hero shrink guard */

/*
 * The logo is a replaced element, so its max-width also becomes the minimum
 * content width of the flex item holding it. Without these three rules the
 * hero refuses to shrink below 420px and the centred text is clipped on a
 * phone.
 */
.hero__inner {
    width: 100%;
    min-width: 0;
}

.hero__logo {
    width: 100%;
    height: auto;
}

.hero p,
.hero__actions {
    min-width: 0;
}

/* ----------------------------------------------------------------- Tasks */

/*
 * The board of tasks, on both sides of the portal.
 *
 * The sections take a fifth of the width and the board the rest. Everything
 * on the board is written in board units multiplied by `--zoom`, so the one
 * variable moves the squares, resizes them and grows the scrollable area
 * together. A `transform` would have scaled the picture and left the
 * scrollbars measuring the board it used to be.
 */

.quest-layout {
    display: grid;
    grid-template-columns: minmax(13rem, 1fr) 4fr;
    gap: 1.25rem;
    align-items: start;
}

.quest-sections {
    position: sticky;
    top: calc(var(--header-height) + 1rem);
    padding: 0.85rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.quest-sections__head {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin-bottom: 0.7rem;
    padding-bottom: 0.6rem;
    border-bottom: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.quest-sections__nav {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
}

.quest-section {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.55rem 0.6rem;
    border-radius: var(--radius);
    color: var(--text);
}

.quest-section:hover {
    background: var(--panel-hover);
    color: var(--text-strong);
}

/* Marked the way the rest of the portal marks the one being read. */
.quest-section.is-active {
    background: var(--accent-soft);
    box-shadow: inset 0 0 0 1px var(--accent);
    color: var(--accent);
}

.quest-section.is-active .quest-section__icon .icon {
    color: var(--accent);
}

/*
 * A shut section is dimmed rather than hidden or disabled. Somebody has to
 * be able to press it to read what would open it.
 */
.quest-section.is-shut .quest-section__name {
    color: var(--text-dim);
}

.quest-section__icon {
    display: flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 1.75rem;
    height: 1.75rem;
    background: var(--panel-alt);
    border-radius: var(--radius);
}

.quest-section__icon .icon {
    color: var(--text-dim);
}

.quest-section__body {
    display: flex;
    flex: 1;
    min-width: 0;
    flex-direction: column;
    gap: 0.1rem;
}

.quest-section__name {
    overflow: hidden;
    font-size: 0.85rem;
    font-weight: 600;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.quest-section__meta {
    display: flex;
    align-items: center;
    gap: 0.25rem;
    color: var(--text-dim);
    font-size: 0.7rem;
}

.quest-section__done {
    display: flex;
    flex: none;
    align-items: center;
    color: var(--ok);
}

.quest-sections__add {
    display: flex;
    /*
     * Middle to middle. A field is taller than a small button, so left to
     * find their own the two sat on the same top edge and the button read as
     * something which had slipped.
     */
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.7rem;
    padding-top: 0.7rem;
    border-top: 1px solid var(--border);
}

.quest-sections__add input {
    min-width: 0;
    flex: 1;
}

.quest-stage {
    min-width: 0;
}

.quest-board {
    --zoom: 1;

    position: relative;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /*
     * Nothing on the board is text to be selected. Without this, dragging the
     * view paints every name it passes blue, and dragging a square selects
     * the one under it instead of moving it.
     */
    user-select: none;
    -webkit-user-select: none;
}

/*
 * The buttons sit on the board rather than above it. A board which fills the
 * screen would otherwise have the thing which shrinks it somewhere off the
 * top of the page.
 */
.quest-board__tools {
    position: absolute;
    z-index: 2;
    top: 0.6rem;
    right: 0.6rem;
    display: flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.3rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.quest-board__zoom {
    min-width: 3.4rem;
    color: var(--text-dim);
    font-size: 0.72rem;
    font-weight: 700;
    text-align: center;
}

.quest-board__viewport {
    overflow: auto;
    height: min(68vh, 44rem);
    border-radius: var(--radius);
    /*
     * The dots are the board itself rather than a picture on it, so they
     * stay put while it is dragged and say how far it has moved.
     */
    background-image: radial-gradient(circle, rgba(255, 255, 255, 0.07) 1px, transparent 1px);
    background-size: calc(40px * var(--zoom)) calc(40px * var(--zoom));
    cursor: grab;
    /*
     * A finger scrolls the board the way it scrolls anything else, and
     * carries on to the page once the board runs out. Taking the gesture over
     * would have left a phone with a full width map it cannot get past.
     */
    touch-action: pan-x pan-y;
    /*
     * The board scrolls and says nothing about it. A map is moved by taking
     * hold of it rather than by aiming at a slider down its edge, and the
     * portal draws a bar on every scrolling panel it has - two of them across
     * the corner of the board, moving whenever the hand does. The scrolling
     * itself is untouched: the wheel, the drag, the edge creep and the jump
     * to a corner all still work, and the dots are what say how far it has
     * moved.
     */
    scrollbar-width: none;
}

.quest-board__viewport.is-panning {
    cursor: grabbing;
}

/* Said twice, because the two engines listen to different things. */
.quest-board__viewport::-webkit-scrollbar {
    display: none;
}

/*
 * A square is a link, and a browser drags a link by itself. Told not to here
 * as well as in the markup, because the markup only reaches the squares which
 * were on the page when the script ran.
 */
.quest-node {
    -webkit-user-drag: none;
}

.quest-board__space {
    position: relative;
    width: calc(var(--board-width) * var(--zoom) * 1px);
    height: calc(var(--board-height) * var(--zoom) * 1px);
}

.quest-board__lines {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.quest-line {
    stroke: var(--border-strong);
    stroke-width: 2;
    stroke-linecap: round;
    /* Kept one pixel wide at every zoom, like a rule rather than a drawing. */
    vector-effect: non-scaling-stroke;
}

.quest-line.is-walked {
    stroke: var(--accent);
}

.quest-board__hint {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin: 0;
    padding: 0.55rem 0.75rem;
    border-top: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.72rem;
}

/*
 * One square.
 *
 * A hundred board units across, which is what the server measures a line to
 * the middle of. The title sits under it rather than inside it: a name long
 * enough to be useful is longer than a square is wide.
 */
/*
 * The slot is where a square stands; the square fills it. What can be done to
 * a square hangs off the side of the slot, so it travels with it as it is
 * dragged - and a form, which is what throwing a task away is, cannot live
 * inside the link the square is.
 */
.quest-slot {
    position: absolute;
    left: calc(var(--quest-x) * var(--zoom) * 1px);
    top: calc(var(--quest-y) * var(--zoom) * 1px);
    width: calc(100px * var(--zoom));
    height: calc(100px * var(--zoom));
}

.quest-node {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--panel-alt);
    border: 2px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    transition: border-color 0.15s ease, background-color 0.15s ease, transform 0.1s ease;
}

.quest-node:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.quest-node__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: calc(34px * var(--zoom));
    font-weight: 800;
    line-height: 1;
}

.quest-node__mark .icon {
    width: calc(30px * var(--zoom));
    height: calc(30px * var(--zoom));
}

/*
 * The name under the square, at a size which stops shrinking before it stops
 * being readable. A board zoomed right out is a map of shapes, and a map
 * whose labels are two pixels tall is a map with none.
 */
.quest-node__title {
    position: absolute;
    top: calc(100% + 0.3rem);
    left: 50%;
    width: calc(150px * var(--zoom));
    color: var(--text-dim);
    font-size: max(0.62rem, calc(12px * var(--zoom)));
    font-weight: 600;
    line-height: 1.2;
    text-align: center;
    transform: translateX(-50%);
    pointer-events: none;
}

.quest-node:hover .quest-node__title {
    color: var(--text-strong);
}

/*
 * A finished square is told apart by its edge rather than by its ground. The
 * board is read as one surface with things standing on it, and a square
 * filled in a different colour reads as a hole in that surface.
 */
.quest-node--done {
    border-color: var(--ok);
    color: var(--ok);
}

.quest-node--open {
    border-color: var(--accent);
    color: var(--accent);
}

/*
 * A square which may be started breathes.
 *
 * The board answers "what can I do next", and on a board of thirty squares
 * that answer is two or three of them - which a still colour states and a
 * moving one points at. Only where the board is being read: in the
 * administration every square is open, and thirty of them pulsing at once is
 * a screen nobody can work on.
 */
@keyframes quest-node-breathes {
    0%,
    100% {
        box-shadow: 0 0 0 0 rgba(227, 79, 14, 0);
    }

    50% {
        box-shadow: 0 0 0 5px rgba(227, 79, 14, 0.16);
    }
}

.quest-board:not([data-quest-editable]) .quest-node--open {
    animation: quest-node-breathes 2.4s ease-in-out infinite;
}

/* Somebody who asked for less movement is given none. */
@media (prefers-reduced-motion: reduce) {
    .quest-board .quest-node--open {
        animation: none;
    }
}

.quest-node--shut {
    background: var(--panel);
    border-color: var(--border);
    border-style: dashed;
    color: var(--text-dim);
}

.quest-node__flag {
    position: absolute;
    top: calc(4px * var(--zoom));
    right: calc(4px * var(--zoom));
    display: flex;
    color: var(--warn);
}

.quest-slot[data-quest-draggable] {
    cursor: grab;
    /* A finger on a square moves the square rather than the board under it. */
    touch-action: none;
}

.quest-slot.is-held {
    z-index: 4;
    cursor: grabbing;
}

.quest-slot.is-held .quest-node {
    border-color: var(--accent);
    transform: scale(1.04);
}

/*
 * What can be done to a square, shown by pointing at it.
 *
 * Beside it rather than on it: a hundred pixel square with three buttons
 * drawn inside is a square with no room left for what it says. Hidden with
 * `visibility` rather than `display` so that the strip keeps its size and
 * nothing on the board moves as it appears.
 */
.quest-node__tools {
    position: absolute;
    z-index: 5;
    top: 0;
    left: 100%;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    margin-left: 0.4rem;
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.12s ease, visibility 0.12s ease;
}

.quest-slot:hover .quest-node__tools,
.quest-slot:focus-within .quest-node__tools {
    opacity: 1;
    visibility: visible;
}

/*
 * Every symbol the same width. One of them is a button inside a form, which
 * takes the width of its own contents rather than of the strip, so the column
 * came out ragged.
 */
.quest-node__tools > *,
.quest-node__tools .button {
    width: 100%;
}

/* Out of the way while the square is in the air. */
.quest-slot.is-held .quest-node__tools {
    opacity: 0;
    visibility: hidden;
}

/* A square being pointed at stands above the ones beside it. */
.quest-slot:hover,
.quest-slot:focus-within {
    z-index: 3;
}

/*
 * One row of the list of sections: the section itself, and the handle it is
 * carried by. The handle is on the right because the left is where the name
 * starts, and a column of names which start one indent in is a column which
 * reads as something else.
 */
.quest-section-row {
    display: flex;
    align-items: center;
    gap: 0.35rem;
}

.quest-section-row .quest-section {
    flex: 1;
    min-width: 0;
}

.quest-section-row__handle {
    flex: none;
}

/* The questions under the board, which are not part of it. */
.quest-faq {
    margin-top: 2.5rem;
}

/* ------------------------------------------------------ A shut section */

.quest-shut {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.6rem;
    padding: 3rem 1.5rem;
    text-align: center;
}

.quest-shut__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 4.5rem;
    height: 4.5rem;
    background: var(--panel-alt);
    border-radius: var(--radius);
    color: var(--text-dim);
}

.quest-shut h2 {
    margin: 0;
    font-size: 1.15rem;
}

.quest-shut p {
    max-width: 42ch;
    margin: 0;
    color: var(--text-dim);
    font-size: 0.85rem;
}

/*
 * What is left to do, as buttons which go and do it. They wrap rather than
 * stack, because a section is usually held shut by one or two tasks and a
 * column of one reads as a list which lost the rest of itself.
 */
.quest-shut__list {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.4rem;
    flex-wrap: wrap;
    margin: 0.5rem 0 0;
    padding: 0;
    list-style: none;
}

.quest-shut__list li {
    display: flex;
}

/* A task with nowhere to send them is said, not offered. */
.quest-shut__unreachable {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.4rem 0.75rem;
    background: var(--panel-alt);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.85rem;
    font-weight: 600;
}

/* ------------------------------------------------- The window of a task */

.quest-window__inner {
    max-width: 34rem;
}

.quest-window__head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}

.quest-window__head .modal__title {
    flex: 1;
    min-width: 8rem;
    margin: 0;
}

.quest-window__type {
    display: inline-block;
    margin: 0 0 0.75rem;
    padding: 0.15rem 0.5rem;
    background: var(--panel-alt);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* The name and how it stands share a line; the kind takes the next one. */
.quest-window__head {
    margin-bottom: 0;
}

.quest-window__head .modal__title {
    display: flex;
    align-items: center;
    gap: 0.4rem;
}

/*
 * The one thing a player may do to a task from here, on a task which asks
 * for nothing in particular.
 */
.quest-window__finish {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
}

.quest-window__finish .field__hint {
    flex: 1;
    min-width: 12rem;
}

.quest-window__state {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    padding: 0.2rem 0.55rem;
    border-radius: var(--radius);
    font-size: 0.7rem;
    font-weight: 700;
}

.quest-window__state--done {
    background: rgba(59, 165, 93, 0.14);
    color: var(--ok);
}

.quest-window__state--open {
    background: var(--accent-soft);
    color: var(--accent);
}

.quest-window__state--shut {
    background: var(--panel-alt);
    color: var(--text-dim);
}

.quest-window__body {
    color: var(--text);
    font-size: 0.88rem;
    line-height: 1.6;
}

.quest-window__body p:last-child {
    margin-bottom: 0;
}

.quest-window__block {
    margin-top: 1.1rem;
    padding-top: 0.9rem;
    border-top: 1px solid var(--border);
}

.quest-window__block h3 {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin: 0 0 0.5rem;
    color: var(--text-dim);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.1em;
    text-transform: uppercase;
}

.quest-window__objectives,
.quest-window__requires {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.quest-window__objectives li,
.quest-window__requires li {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.35rem 0.55rem;
    background: var(--panel-alt);
    border-radius: var(--radius);
    font-size: 0.83rem;
}

.quest-window__objectives .icon {
    flex: none;
    color: var(--accent);
}

.quest-window__requires .is-met {
    color: var(--ok);
}

.quest-window__requires .is-missing {
    color: var(--text-dim);
}

.quest-window__actions {
    align-items: center;
    flex-wrap: wrap;
}

.quest-window__actions .field__hint {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    margin-right: auto;
}

/* --------------------------------------- The board of the administration */

.quest-layout--admin .quest-board__viewport {
    height: min(60vh, 40rem);
}

.quest-objectives {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
    gap: 0.4rem;
    margin-top: 0.4rem;
}

/*
 * The tasks which may come first, in a box which scrolls. A portal which has
 * been running a while has hundreds, and a window taller than the screen is
 * a window whose save button cannot be reached.
 */
.quest-requires {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
    gap: 0.2rem 0.75rem;
    overflow-y: auto;
    max-height: 11rem;
    margin-top: 0.4rem;
    padding: 0.5rem;
    background: var(--panel-alt);
    border-radius: var(--radius);
}

/*
 * A list built one item at a time: what is already in it, and the box which
 * puts the next thing there. The items wrap, because most of these lists are
 * two or three short lines and a column of them is mostly empty panel.
 */
.chip-list {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    flex-wrap: wrap;
    margin-top: 0.4rem;
}

.chip-list:empty {
    display: none;
}

.chip-list__item {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.3rem 0.25rem 0.6rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    font-size: 0.8rem;
    font-weight: 600;
}

.chip-list__drop {
    display: inline-flex;
    align-items: center;
    padding: 0.15rem;
    background: transparent;
    border: 0;
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
}

.chip-list__drop:hover {
    background: var(--danger);
    color: #fff;
}

.chip-list__add {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin-top: 0.4rem;
}

.chip-list__add input {
    min-width: 0;
    flex: 1;
}

/* A list which has taken everything it may says so by going quiet. */
.chip-list__add input:disabled,
.chip-list__add button:disabled {
    opacity: 0.45;
}

.quest-completions {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin: 0.4rem 0 0;
    padding: 0;
    list-style: none;
}

/*
 * Four rows and then it scrolls. A task everybody on the server has done is a
 * list of hundreds, and a window which grows with it is one whose buttons end
 * up below the bottom of the screen.
 */
.quest-completions--paged {
    overflow-y: auto;
    /*
     * Four rows and the three gaps between them, which measures two hundred
     * pixels: a row is a face, a nickname and a button. Measured rather than
     * guessed, because a cap a few pixels short shows three and a half of
     * them and reads as a list which was cut off.
     */
    max-height: 12.5rem;
    padding-right: 0.2rem;
}

/*
 * A search which is one line of a window rather than the head of a page.
 *
 * The one across a page is `flex: 1 1 260px` so that it shares a row with the
 * filters beside it. Dropped into a field, which stacks its parts, that basis
 * becomes a height - and the box came out a quarter of the window tall with
 * its symbol stranded underneath.
 */
.search-box--tight {
    flex: none;
    max-width: none;
    margin: 0.4rem 0 0;
}

.quest-completions li {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.35rem 0.55rem;
    background: var(--panel-alt);
    border-radius: var(--radius);
    font-size: 0.83rem;
}

.quest-completions .field__hint {
    margin-left: auto;
}

.quest-completions .inline-form {
    display: flex;
}

/*
 * The lists a task pays and asks for, read in its window.
 */
.quest-window__rewards {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.quest-window__rewards li {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    padding: 0.35rem 0.55rem;
    background: var(--accent-soft);
    border-radius: var(--radius);
    color: var(--accent);
    font-size: 0.85rem;
    font-weight: 700;
}

.quest-grant__row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.35rem;
}

.quest-grant__row .select,
.quest-grant__row select {
    flex: 1;
    min-width: 0;
}

/*
 * The two things which are not part of writing a task, in a strip of their
 * own under the form. A form inside a form is not something a browser will
 * parse, so they cannot be anywhere else.
 */
.modal__inner--footer {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
    padding-top: 0;
}

.modal__inner--footer .quest-grant {
    flex: 1;
    min-width: 15rem;
}

.icon-grid--compact {
    max-height: 8rem;
    overflow-y: auto;
}

@media (max-width: 900px) {
    .quest-layout {
        grid-template-columns: minmax(0, 1fr);
    }

    .quest-sections {
        position: static;
    }
}

/* -------------------------------------------------------- Call to actions */

.team-cta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1.25rem;
    flex-wrap: wrap;
    margin-top: 2.5rem;
}

.team-cta h3 {
    margin: 0 0 0.25rem 0;
    font-size: 1.15rem;
}

.team-cta .field__hint {
    margin: 0;
}

.team-cta .button {
    flex: none;
}

@media screen and (max-width: 640px) {
    /* The read button gets a row of its own instead of squeezing the meta. */
    .post-hero__footer {
        align-items: stretch;
        flex-direction: column;
        gap: 0.6rem;
    }

    .post-hero__footer .button {
        width: 100%;
        justify-content: center;
    }

    .post-hero__footer .post-meta__item {
        font-size: 0.78rem;
    }

}

/* ---------------------------------------------------------- Notifications */

.notification-menu {
    position: relative;
}

.notification-bell {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex: none;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
    transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}

.notification-bell:hover {
    background: var(--panel-hover);
    border-color: var(--accent);
    color: var(--accent);
}

.notification-bell[aria-expanded="true"] {
    border-color: var(--accent);
    color: var(--accent);
}

/* The counter of the unread notifications, hidden while there are none. */
.notification-bell__badge {
    position: absolute;
    top: -4px;
    right: -4px;
    min-width: 18px;
    height: 18px;
    padding: 0 0.25rem;
    background: var(--accent);
    border: 2px solid var(--bg-alt);
    border-radius: var(--radius);
    color: #fff;
    font-size: 0.62rem;
    font-weight: 700;
    line-height: 14px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

.notification-bell__badge.is-empty {
    display: none;
}

.notification-panel {
    width: min(23rem, calc(100vw - 1.5rem));
    padding: 0;
    overflow: hidden;
}

.notification-panel__head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 0.75rem;
    padding: 0.85rem 0.9rem;
    border-bottom: 1px solid var(--border);
}

.notification-panel__head strong {
    color: var(--text-strong);
    font-size: 0.9rem;
}

.notification-panel__count {
    color: var(--text-dim);
    font-size: 0.74rem;
}

.notification-panel__list {
    max-height: min(24rem, 60vh);
    overflow-y: auto;
    overscroll-behavior: contain;
}

.notification-panel__empty {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    margin: 0;
    padding: 1.5rem 1rem;
    color: var(--text-dim);
    font-size: 0.85rem;
}

.notification-panel__empty .icon {
    color: var(--border-strong);
}

.notification-panel__foot {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    padding: 0.6rem;
    background: var(--panel-alt);
    border-top: 1px solid var(--border);
}

.notification-panel__foot form {
    margin: 0;
    flex: 1;
}

.notification-panel__foot .button {
    width: 100%;
    justify-content: center;
}

.notification-panel__foot > .button {
    flex: 1;
}

.notification-item {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 0.7rem;
    padding: 0.75rem 0.9rem;
    border-bottom: 1px solid var(--border);
    color: var(--text);
    transition: background-color 0.15s ease;
}

.notification-item:last-child {
    border-bottom: 0;
}

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

.notification-item__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    flex: none;
    margin-top: 0.1rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
}

.notification-item__main {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
    min-width: 0;
}

.notification-item__title {
    color: var(--text-strong);
    font-size: 0.84rem;
    font-weight: 600;
    line-height: 1.35;
}

.notification-item__body {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.78rem;
    line-height: 1.45;
}

.notification-item__meta {
    color: var(--text-dim);
    font-size: 0.7rem;
    letter-spacing: 0.02em;
}

/* An unread entry is marked by the accent bar on its left edge. */
.notification-item.is-unread {
    background: rgba(227, 79, 14, 0.06);
}

.notification-item.is-unread::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 3px;
    background: var(--accent);
}

.notification-item.is-unread .notification-item__icon {
    border-color: rgba(227, 79, 14, 0.45);
    color: var(--accent);
}

/* The full history on its own page. */

.notification-feed {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
}

.notification-row {
    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 0.9rem;
    padding: 1.1rem 1.25rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.notification-row.is-unread {
    border-color: rgba(227, 79, 14, 0.35);
}

.notification-row.is-unread::before {
    content: "";
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    width: 3px;
    background: var(--accent);
}

.notification-row__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    flex: none;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
}

.notification-row.is-unread .notification-row__icon {
    border-color: rgba(227, 79, 14, 0.45);
    color: var(--accent);
}

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

.notification-row__head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.4rem;
    color: var(--text-dim);
    font-size: 0.75rem;
}

.notification-row__title {
    margin: 0;
    color: var(--text-strong);
    font-size: 1rem;
    line-height: 1.35;
}

.notification-row__body {
    margin: 0.3rem 0 0 0;
    color: var(--text-dim);
    font-size: 0.88rem;
}

.notification-row__actions {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    flex: none;
    flex-wrap: wrap;
    justify-content: flex-end;
}

.notification-row__actions form {
    margin: 0;
}

/* -------------------------------------------------------------- Releases */

.release-list {
    display: flex;
    flex-direction: column;
    gap: 1.1rem;
}

.release-card {
    display: grid;
    grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
    min-width: 0;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}

.release-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    min-width: 0;
    padding: 1.4rem;
}

.release-card:hover {
    transform: translateY(-2px);
    border-color: var(--accent);
    box-shadow: var(--shadow-soft);
}

.release-card--feature {
    background: linear-gradient(180deg, rgba(227, 79, 14, 0.08), transparent 60%), var(--panel);
    border-color: rgba(227, 79, 14, 0.35);
}

.release-card__head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.release-card__version {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.2rem 0.6rem;
    background: var(--accent-soft);
    border: 1px solid rgba(227, 79, 14, 0.35);
    border-radius: var(--radius);
    color: var(--accent);
    font-family: var(--mono);
    font-size: 0.78rem;
    font-weight: 700;
}

.release-card__channel {
    padding: 0.2rem 0.55rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.release-card--beta .release-card__channel {
    border-color: rgba(217, 158, 43, 0.45);
    color: var(--warn);
}

.release-card--hotfix .release-card__channel {
    border-color: rgba(226, 33, 47, 0.45);
    color: var(--danger);
}

/*
 * Who wrote it and when, kept together at the end of the line the way the
 * news stream keeps them together under a card.
 */
.release-card__author {
    margin-left: auto;
}

.release-card__date {
    color: var(--text-dim);
    font-size: 0.76rem;
}

.release-card__title {
    margin: 0;
    font-size: 1.2rem;
    line-height: 1.3;
}

.release-card__title a {
    color: var(--text-strong);
}

.release-card__title a:hover {
    color: var(--accent);
}

.release-card__summary {
    margin: 0;
    color: var(--text-dim);
    font-size: 0.92rem;
}

.release-card__footer {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.75rem;
    flex-wrap: wrap;
    padding-top: 0.9rem;
    border-top: 1px solid var(--border);
}

.release-card__author {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    color: var(--accent);
    font-size: 0.8rem;
    font-weight: 700;
}

.release-card__author .avatar {
    width: 18px;
    height: 18px;
    border-radius: var(--radius);
    image-rendering: pixelated;
}

.release-detail__tags {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-bottom: 0.75rem;
}

/* The dashboard block introducing the newest version. */
.wrapper--tight {
    padding-top: 0;
}

@media screen and (max-width: 640px) {
    .notification-row {
        flex-wrap: wrap;
        padding: 1rem;
    }

    .notification-row__actions {
        width: 100%;
        justify-content: flex-start;
    }

    .release-card__date {
        margin-left: 0;
        width: 100%;
    }

    .release-card__footer .button-row {
        width: 100%;
    }

    .release-card__footer .button {
        flex: 1;
        justify-content: center;
    }
}

@media screen and (max-width: 560px) {
    /* The bell stays in the bar, its panel becomes a sheet under the header. */
    .notification-panel {
        position: fixed;
        top: calc(var(--header-height) + 0.4rem);
        right: 0.75rem;
        left: 0.75rem;
        width: auto;
    }
}

/* ------------------------------------------------------------------- Feed */

/*
 * The dashboard stream. News posts and released versions share the card, a
 * modifier only swaps the accent so that the two are told apart at a glance.
 */
.feed {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.feed-card {
    display: grid;
    grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);
    min-width: 0;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
}

.feed-card:hover {
    transform: translateY(-2px);
    border-color: var(--accent);
    box-shadow: var(--shadow);
}

.feed-card--release {
    border-color: rgba(227, 79, 14, 0.28);
    background: linear-gradient(120deg, rgba(227, 79, 14, 0.07), transparent 45%), var(--panel);
}

.feed-card__media {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 13rem;
    background: var(--panel-alt);
    overflow: hidden;
}

.feed-card__image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.35s ease;
}

.feed-card:hover .feed-card__image {
    transform: scale(1.04);
}

/* Shown instead of a picture: the mark of the server and what the entry is. */
.feed-card__fallback {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    width: 100%;
    height: 100%;
    background:
        radial-gradient(circle at 50% 40%, rgba(227, 79, 14, 0.16), transparent 60%),
        linear-gradient(160deg, var(--panel-alt), var(--panel));
    color: var(--text-dim);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.feed-card__fallback img {
    /* A fixed box, for the same reason as `.post-card__fallback img`. */
    width: min(52%, 190px);
    height: 8.5rem;
    object-fit: contain;
}

/* Tells a version apart from a news post without reading a single word. */
.feed-card__kind {
    position: absolute;
    top: 0.7rem;
    left: 0.7rem;
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0.25rem 0.6rem;
    background: rgba(19, 18, 17, 0.85);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text-strong);
    font-family: var(--mono);
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.06em;
}

.feed-card--release .feed-card__kind {
    border-color: rgba(227, 79, 14, 0.55);
    color: var(--accent);
}

.feed-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.55rem;
    min-width: 0;
    padding: 1.5rem 1.6rem;
}

.feed-card__meta {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    color: var(--text-dim);
    font-size: 0.78rem;
}

/* The chip stays on the left, the byline and the counters go to the right. */
.feed-card__byline {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-left: auto;
}

.feed-card__author {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--accent);
    font-weight: 700;
}

.feed-card__author .avatar {
    width: 18px;
    height: 18px;
    border-radius: var(--radius);
    image-rendering: pixelated;
}

.feed-card__title {
    margin: 0;
    font-size: 1.35rem;
    line-height: 1.28;
}

.feed-card__title a {
    color: var(--text-strong);
}

.feed-card__title a:hover {
    color: var(--accent);
}

.feed-card__summary {
    margin: 0;
    color: var(--text);
    font-size: 0.98rem;
    line-height: 1.55;
}

.feed-card__footer {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    flex-wrap: wrap;
    margin-top: auto;
    padding-top: 1rem;
    border-top: 1px solid var(--border);
}

.feed-card__stats {
    display: inline-flex;
    align-items: center;
    gap: 0.9rem;
    flex-wrap: wrap;
    margin-left: auto;
}

.feed-card__stat {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    color: var(--text-dim);
    font-size: 0.78rem;
    white-space: nowrap;
}

.feed.is-loading {
    opacity: 0.55;
    pointer-events: none;
    transition: opacity 0.2s ease;
}

.feed-more {
    display: flex;
    align-items: center;
    justify-content: center;
    margin-top: 1.75rem;
}

.feed-more .button {
    min-width: 15rem;
    justify-content: center;
}

.feed-more__done {
    margin: 0;
    color: var(--text-dim);
    font-size: 0.85rem;
}

/* -------------------------------------------------------- Language switch */

.language-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    height: 38px;
    padding: 0 0.6rem;
    flex: none;
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-family: inherit;
    font-size: 0.76rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    cursor: pointer;
    transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}

.language-chip:hover {
    background: var(--panel-hover);
    border-color: var(--accent);
    color: var(--accent);
}

.language-chip[aria-expanded="true"] {
    border-color: var(--accent);
    color: var(--accent);
}

.language-panel {
    min-width: 12.5rem;
}

.language-panel__head {
    padding-bottom: 0.6rem;
}

.language-panel__item {
    justify-content: flex-start;
}

.language-panel__code {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.1rem;
    padding: 0.1rem 0.35rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-family: var(--mono);
    font-size: 0.68rem;
}

.language-panel__item.is-active {
    color: var(--accent);
}

.language-panel__item.is-active .language-panel__code {
    border-color: rgba(227, 79, 14, 0.45);
    color: var(--accent);
}

.language-panel__item .icon:last-child {
    margin-left: auto;
    color: var(--accent);
}

/* The release listing borrows the media box of the feed: a column of its own
   beside the text rather than a strip pasted across the top of the card. */

.release-card__media {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 13rem;
    background: var(--panel-alt);
    overflow: hidden;
}

.release-card__image {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.35s ease;
}

.release-card:hover .release-card__image {
    transform: scale(1.04);
}

/* A version without a picture gets the generated art of the stream, so that
   the rows keep one shape instead of alternating between one and two columns. */

.release-card__fallback {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 0.75rem;
    width: 100%;
    height: 100%;
    background:
        radial-gradient(circle at 50% 40%, rgba(227, 79, 14, 0.16), transparent 60%),
        linear-gradient(160deg, var(--panel-alt), var(--panel));
    color: var(--text-dim);
    font-family: var(--mono);
    font-size: 0.72rem;
    font-weight: 700;
    letter-spacing: 0.14em;
    text-transform: uppercase;
}

.release-card__fallback img {
    width: min(52%, 190px);
    height: 8.5rem;
    object-fit: contain;
    image-rendering: pixelated;
}

@media screen and (max-width: 860px) {
    .release-card {
        grid-template-columns: minmax(0, 1fr);
    }

    .release-card__media {
        min-height: 0;
        aspect-ratio: 16 / 9;
    }
}

@media screen and (max-width: 860px) {
    .feed-card {
        grid-template-columns: minmax(0, 1fr);
    }

    .feed-card__media {
        min-height: 0;
        aspect-ratio: 16 / 9;
    }

    .feed-card__body {
        padding: 1.15rem;
    }

    .feed-card__title {
        font-size: 1.15rem;
    }
}

@media screen and (max-width: 640px) {
    .feed-card__footer .button {
        width: 100%;
        justify-content: center;
    }

    .feed-card__stats,
    .feed-card__byline {
        margin-left: 0;
    }

    .feed-more .button {
        width: 100%;
    }
}

@media screen and (max-width: 560px) {
    .language-chip__code {
        display: none;
    }

    .language-panel {
        position: fixed;
        top: calc(var(--header-height) + 0.4rem);
        right: 0.75rem;
        left: 0.75rem;
        min-width: 0;
    }
}

/* ------------------------------------------------------------------ Rules */

.rule-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 28rem), 1fr));
    gap: 1rem;
    /* Every card of a row is as tall as the tallest one, so the grid reads as
       a grid instead of a ragged pile. */
    align-items: stretch;
}

.rule-card {
    display: flex;
    align-items: flex-start;
    gap: 1.1rem;
    padding: 1.35rem 1.5rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.2s ease;
}

.rule-card:hover {
    border-color: var(--accent);
}

.rule-card__number {
    flex: none;
    color: var(--accent);
    font-family: var(--mono);
    font-size: 1.35rem;
    font-weight: 700;
    line-height: 1.2;
    opacity: 0.7;
}

.rule-card__body {
    min-width: 0;
}

.rule-card__body h3 {
    margin: 0 0 0.45rem 0;
    font-size: 1.05rem;
}

.rule-card__body p {
    margin: 0 0 0.6rem 0;
    color: var(--text-dim);
    font-size: 0.94rem;
}

.rule-card__body p:last-child {
    margin-bottom: 0;
}

/* When the rule last moved: a footnote of the card rather than a sentence of
   it, so it is read only by somebody looking for it. */

.rule-card__stamp {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    margin: 0.9rem 0 0 0;
    color: var(--text-dim);
    font-size: 0.78rem;
    opacity: 0.75;
}

.rule-card__body p.rule-card__stamp {
    margin-bottom: 0;
}

/* ----------------------------------------------------------- Translations */

.translation-block {
    margin-top: 1.75rem;
    padding-top: 1.5rem;
    border-top: 1px solid var(--border);
}

.translation-block__head h3 {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0 0 0.35rem 0;
    font-size: 1rem;
}

.translation-block__head p {
    margin: 0 0 1rem 0;
}

.translation {
    margin-bottom: 0.6rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.translation > summary {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.75rem 0.9rem;
    color: var(--text-strong);
    font-size: 0.86rem;
    font-weight: 600;
    cursor: pointer;
    list-style: none;
}

.translation > summary::-webkit-details-marker {
    display: none;
}

.translation > summary:hover {
    background: var(--panel-hover);
}

.translation[open] > summary {
    border-bottom: 1px solid var(--border);
}

.translation__code {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 2.2rem;
    padding: 0.15rem 0.4rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--accent);
    font-family: var(--mono);
    font-size: 0.7rem;
}

.translation__body {
    padding: 1.1rem 0.9rem;
}

/* ------------------------------------------------------------------- Wiki */

/*
 * Three columns on a wide screen: the categories, the content itself and the
 * list of the freshly changed articles. The two side columns fold away on a
 * narrow one, the content never does.
 */
.wiki-layout {
    display: grid;
    grid-template-columns: 16rem minmax(0, 1fr) 18rem;
    gap: 1.5rem;
    align-items: start;
}

.wiki-layout:not(:has(.wiki-aside)) {
    grid-template-columns: 16rem minmax(0, 1fr);
}

.wiki-sidebar,
.wiki-aside {
    position: sticky;
    top: calc(var(--header-height) + 1rem);
    padding: 1rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.wiki-sidebar__head {
    display: flex;
    align-items: center;
    gap: 0.45rem;
    margin-bottom: 0.75rem;
    padding-bottom: 0.6rem;
    border-bottom: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
}

.wiki-sidebar__nav {
    display: flex;
    flex-direction: column;
    gap: 0.15rem;
}

.wiki-sidebar__link {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    padding: 0.5rem 0.6rem;
    border-radius: var(--radius);
    color: var(--text);
    font-size: 0.85rem;
    font-weight: 600;
}

.wiki-sidebar__link .icon {
    flex: none;
    color: var(--text-dim);
}

.wiki-sidebar__link:hover {
    background: var(--panel-hover);
    color: var(--text-strong);
}

.wiki-sidebar__link:hover .icon {
    color: var(--text-strong);
}

.wiki-sidebar__link.is-active {
    background: var(--accent-soft);
    color: var(--accent);
}

.wiki-sidebar__link.is-active .icon {
    color: var(--accent);
}

.wiki-content {
    min-width: 0;
}

.wiki-roots {
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
}

.wiki-root {
    display: flex;
    align-items: flex-start;
    gap: 0.85rem;
    padding: 0.95rem 1.1rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
}

.wiki-root:hover {
    border-color: var(--accent);
    box-shadow: var(--shadow-soft);
}

.wiki-root__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 2.35rem;
    height: 2.35rem;
    flex: none;
    border-radius: var(--radius);
    background: var(--panel-alt);
    color: var(--accent);
}

.wiki-root__body {
    display: flex;
    flex-direction: column;
    gap: 0.3rem;
    flex: 1;
    min-width: 0;
}

.wiki-root__title {
    font-size: 1.02rem;
    font-weight: 700;
}

.wiki-root:hover .wiki-root__title {
    color: var(--accent);
}

.wiki-root__desc {
    color: var(--text-dim);
    font-size: 0.9rem;
}

.wiki-root__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6rem;
    margin-top: 0.15rem;
    color: var(--text-dim);
    font-size: 0.8rem;
}

.wiki-root__meta span {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}

.wiki-root__arrow {
    display: inline-flex;
    align-self: center;
    flex: none;
    color: var(--text-dim);
}

.wiki-root:hover .wiki-root__arrow {
    color: var(--accent);
}

.wiki-article-list {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    margin-top: 0.9rem;
}

.wiki-article-row {
    padding: 1rem 1.15rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

.wiki-article-row:hover {
    border-color: var(--accent);
    background: var(--panel-hover);
}

.wiki-article-row__title {
    display: block;
    color: var(--text-strong);
    font-size: 1rem;
    font-weight: 700;
}

.wiki-article-row__title:hover {
    color: var(--accent);
}

.wiki-article-row__summary {
    margin: 0.3rem 0 0 0;
    color: var(--text-dim);
    font-size: 0.88rem;
}

.wiki-article-row__meta {
    display: flex;
    align-items: center;
    gap: 0.9rem;
    flex-wrap: wrap;
    margin-top: 0.6rem;
    color: var(--text-dim);
    font-size: 0.74rem;
}

.wiki-article-row__meta span {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}

.wiki-recent {
    display: flex;
    flex-direction: column;
    gap: 0.65rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.wiki-recent li {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
}

.wiki-recent a {
    color: var(--text);
    font-size: 0.85rem;
    font-weight: 600;
}

.wiki-recent a:hover {
    color: var(--accent);
}

.wiki-recent span {
    color: var(--text-dim);
    font-size: 0.72rem;
}

.wiki-related {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr));
    gap: 0.6rem;
}

.wiki-related__item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.7rem 0.9rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    font-size: 0.86rem;
    font-weight: 600;
}

.wiki-related__item:hover {
    border-color: var(--accent);
    color: var(--accent);
}

@media screen and (max-width: 1280px) {
    .wiki-layout,
    .wiki-layout:not(:has(.wiki-aside)) {
        grid-template-columns: 15rem minmax(0, 1fr);
    }

    .wiki-aside {
        grid-column: 1 / -1;
        position: static;
    }
}

@media screen and (max-width: 860px) {
    .wiki-layout,
    .wiki-layout:not(:has(.wiki-aside)) {
        grid-template-columns: minmax(0, 1fr);
    }

    .wiki-sidebar {
        position: static;
        max-height: none;
    }

    /* Only the top level wraps into chips on a phone; letting the whole tree
       do it would flatten the nesting into an unreadable pile. */
    .wiki-sidebar__nav > .wiki-sidebar__link {
        display: inline-flex;
    }
}

/* ---------------------------------------------------- Footer refinements */

/*
 * The independence notice sits above the copyright, in the same strip. Both
 * lines are written as paragraphs, so the compound selector keeps the notice
 * centred - the plain class alone would lose to the rule below it.
 */
.site-footer__bottom p {
    margin: 0 auto;
    text-align: center;
    /* Once it has to break, both lines end up of a similar length. */
    text-wrap: balance;
}

.site-footer__bottom .site-footer__disclaimer {
    /* One line as long as it fits, two only on a narrow screen. The two of
       them are one block of small print and read as one: barely a line
       between them, not a paragraph break. */
    margin-bottom: 0.2rem;
    color: var(--text-dim);
    font-size: 0.79rem;
    line-height: 1.5;
}

/* The column of the useful links carries no heading, only the links. */
.site-footer__links {
    display: flex;
    flex-direction: column;
    gap: 0.9rem;
}

/* The way into the administration: a wrench, framed like the bell beside it. */
.site-header__admin {
    display: inline-flex;
    flex: none;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    color: var(--text);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease;
}

.site-header__admin:hover,
.site-header__admin:focus-visible {
    color: var(--accent);
    background: var(--panel-hover);
    border-color: var(--accent);
}

@media screen and (max-width: 1180px) {
    .site-header__admin {
        display: none;
    }
}

/* --------------------------------------------------------------- Contact */

/*
 * The whole section is one column: the two forms share a tab strip, the own
 * reports and the questions follow underneath instead of standing aside.
 */
.contact-layout {
    display: flex;
    flex-direction: column;
    gap: 1.5rem;
    min-width: 0;
}

.contact-layout h2,
.contact-layout h3 {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
}

/* The heading of a panel is dropped once the tabs work, so a signed in
   visitor never sees it. One who is not signed in has no tabs to switch
   between, and their heading needs the room the tab strip would have taken. */

.contact-layout .tabs__title {
    margin-bottom: 1.75rem;
}

/* The contact details of a member come from the account, not from a field. */
.contact-identity {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-top: 1rem;
    padding: 0.75rem 0.9rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.contact-identity img {
    width: 40px;
    height: 40px;
    border-radius: var(--radius);
    image-rendering: pixelated;
}

.contact-identity strong {
    display: block;
    color: var(--text-strong);
    font-size: 0.9rem;
}

.contact-identity span {
    color: var(--text-dim);
    font-size: 0.8rem;
}

.contact-identity .badge {
    margin-left: auto;
}

.bug-form {
    border-color: rgba(227, 79, 14, 0.35);
}

.report-list {
    display: flex;
    flex-direction: column;
    gap: 0.7rem;
    margin: 1rem 0 0 0;
    padding: 0;
    list-style: none;
}

/* Every report opens, so the whole row behaves like a link. */
.report-list__item {
    display: flex;
    align-items: center;
    gap: 1rem;
    padding: 0.8rem 0.95rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: inherit;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

.report-list__item:hover {
    background: var(--panel-hover);
    border-color: var(--accent);
}

.report-list__item .icon {
    flex: none;
    margin-left: auto;
    color: var(--text-dim);
}

.report-list__title {
    display: block;
    color: var(--text-strong);
    font-size: 0.86rem;
    font-weight: 600;
}

.report-list__meta {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
    margin-top: 0.35rem;
    color: var(--text-dim);
    font-size: 0.74rem;
}

/* ------------------------------------------------------------- Uploads */

.upload {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.9rem 1rem;
    background: var(--panel-alt);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
    cursor: pointer;
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

.upload:hover {
    border-color: var(--accent);
    background: var(--panel-hover);
}

.upload input[type="file"] {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}

.upload__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    flex: none;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--accent);
}

.upload__text strong {
    display: block;
    color: var(--text-strong);
    font-size: 0.86rem;
}

.upload__text small {
    color: var(--text-dim);
    font-size: 0.76rem;
}

/*
 * A picture which is already stored, and the button dropping it.
 *
 * Every one of them is drawn at the same size whatever it really measures,
 * so a form does not change shape with the picture somebody happened to
 * upload.
 */
.image-field__current {
    position: relative;
    /*
     * `.field` is a column flexbox, which blockifies its children and
     * stretches them across the panel. Without this the box is the full width
     * of the form and the button in its corner lands nowhere near the
     * picture.
     */
    align-self: flex-start;
    margin: 0.6rem 0;
    line-height: 0;
}

.image-field__current img {
    display: block;
    width: 140px;
    height: 92px;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    /* Contained rather than cropped: the same box holds a wide illustration
     * and a square section icon, and neither may lose a corner to it. */
    object-fit: contain;
}

.image-field__drop {
    position: absolute;
    top: 0.35rem;
    right: 0.35rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    background: var(--bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    color: var(--text);
    cursor: pointer;
    transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.image-field__drop:hover {
    background: var(--danger);
    border-color: var(--danger);
    color: #ffffff;
}

.image-field--filled:has([data-image-remove]:focus-visible) .image-field__drop {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent);
}

/* The line saying the picture is on its way out, and the way back. */
.image-field__dropped {
    display: none;
    align-items: center;
    gap: 0.4rem;
    margin: 0.6rem 0;
    color: var(--text-dim);
    font-size: 0.8rem;
}

.image-field__undo {
    color: var(--accent);
    cursor: pointer;
    text-decoration: underline;
}

/*
 * There is nothing to pick while a picture is already there, and nothing to
 * show once it has been dropped. One checkbox is the whole state.
 *
 * Every rule is guarded by :has() on purpose, the hiding one included: a
 * browser which does not support it drops all three and shows both halves at
 * once, which is the shape this field had before and still works. Hiding the
 * picker with a plain selector would have left such a browser with no way to
 * upload anything at all.
 */
.image-field--filled:not(:has([data-image-remove]:checked)) .upload {
    display: none;
}

.image-field--filled:has([data-image-remove]:checked) .image-field__current {
    display: none;
}

.image-field--filled:has([data-image-remove]:checked) .image-field__dropped {
    display: flex;
}

/* The picture which was just picked, before anything is uploaded. */
.upload__preview {
    width: 64px;
    height: 64px;
    margin-left: auto;
    flex: none;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    object-fit: cover;
}

.upload.is-filled {
    border-style: solid;
    border-color: var(--ok);
}

.upload.is-filled .upload__icon {
    border-color: var(--ok);
    color: var(--ok);
}

.upload.is-invalid {
    border-color: var(--danger);
}

.upload.is-invalid .upload__text small {
    color: var(--danger);
}

/* The screenshots as the admin team sees them. */
.attachment-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
    gap: 0.75rem;
    margin-top: 1.25rem;
}

.attachment {
    display: block;
    padding: 0.5rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.74rem;
}

.attachment:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.attachment img {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 10;
    margin-bottom: 0.4rem;
    object-fit: cover;
    border-radius: var(--radius);
}

.attachment span {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.report-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
    flex-wrap: wrap;
}

.report-head h2 {
    margin: 0 0 0.35rem 0;
}



/* -------------------------------------------------------------- Tabs */

/*
 * Without JavaScript every panel is simply rendered one under another, the
 * strip above them only becomes a real switch once `initTabs` marks the
 * group as ready.
 */
.tabs {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    min-width: 0;
}

.tabs__list {
    display: flex;
    gap: 0.4rem;
    flex-wrap: wrap;
    padding: 0.35rem;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.tabs__tab {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    flex: 1 1 12rem;
    justify-content: center;
    min-height: 42px;
    padding: 0.5rem 1rem;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius);
    color: var(--text-dim);
    font-family: inherit;
    font-size: 0.84rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease;
}

.tabs__tab:hover {
    background: var(--panel-alt);
    color: var(--text-strong);
}

.tabs__tab .icon {
    color: inherit;
}

.tabs__tab.is-active {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent);
}

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

.tabs__panel {
    min-width: 0;
}

/* The heading of a panel repeats its tab, so it is dropped once they work. */
.tabs.is-ready .tabs__title {
    display: none;
}

@media screen and (max-width: 560px) {
    .tabs__tab {
        flex: 1 1 100%;
    }
}

/* ------------------------------------------------ Translation progress */

/*
 * The strip above an editing form: what the screen holds on the left, and
 * what removes it on the right.
 *
 * It sits inside the panel but outside the <form>, and it has to stay there.
 * A <form> may not hold another one - the parser drops the inner tag and its
 * button then submits the outer form instead, which is how a delete button
 * quietly became a save button.
 */
.panel-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    margin-bottom: 1.25rem;
}

/*
 * A heading which carries its own symbol. The symbol is the colour of the
 * words beside it, the way every other symbol on the portal is: an orange
 * mark in front of a heading reads as something to press.
 */
.panel-title {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    margin: 0;
}

.panel-title > svg {
    flex: none;
}

/* What the screen holds, on the left; what removes it stays on the right. */
.panel-head__facts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/*
 * Collapsed when the strip holds nothing. Not `:empty`, which counts the
 * newline Jinja leaves behind between the tags and therefore never matches.
 */
.panel-head:not(:has(*)) {
    display: none;
}

/*
 * One fact about the screen, shown beside the others.
 *
 * They stand next to each other, so they are given a height rather than left
 * to take one from what is inside them - a bar four pixels tall and a line of
 * text do not measure the same.
 */
.info-chip,
.translation-status {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 32px;
    padding: 0.25rem 0.6rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.78rem;
    line-height: 1;
}

.info-chip svg,
.translation-status svg {
    flex: none;
}

/*
 * Who an account is, with the one button which removes it. The button keeps
 * the right so it never sits among the things which merely describe the
 * account.
 */
.user-card {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1rem;
}

/*
 * Reading the account, punishing it and removing it, in that order: the
 * harmless button first, the irreversible one last, so a hurried press lands
 * on the one which asks a question rather than the one which does not. They
 * keep to the right edge of the card even when the row has to wrap.
 */
.user-card__actions {
    flex: none;
    justify-content: flex-end;
}

/*
 * A screen built from several panels, each its own form with its own button.
 * The gap belongs to the stack rather than to the panels: a panel which is
 * not rendered - the password of one's own account - then leaves no hole
 * where it would have been.
 */
.panel-stack {
    display: grid;
    gap: 1.5rem;
}

.panel-stack > form {
    min-width: 0;
}

/*
 * The role and the switches beside it: one answer to one question. The
 * switches take the width the role leaves rather than exactly half of it, and
 * both drop onto lines of their own where there is no room.
 */
.state-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 1.25rem;
}

.state-row__role {
    flex: 0 1 18rem;
    min-width: 12rem;
}

.state-row__switches {
    flex: 1 1 20rem;
    min-width: 0;
}

.translation-progress {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    flex: none;
}

.translation-progress__track {
    display: block;
    width: 6rem;
    height: 0.4rem;
    background: var(--border);
    border-radius: 999px;
    overflow: hidden;
}

.translation-progress--compact .translation-progress__track {
    width: 3.5rem;
}

.translation-progress__fill {
    display: block;
    height: 100%;
    background: var(--warn);
    border-radius: 999px;
    transition: width 0.2s ease;
}

.translation-progress__fill.is-complete {
    background: var(--ok);
}

.translation-progress__label {
    font-family: var(--mono);
    font-size: 0.75rem;
    color: var(--text-dim);
}

/* ------------------------------------------------------------------- FAQ */

/*
 * A hint standing at the far end of a toolbar, opposite the search.
 *
 * It lines up with the bottom of the search box rather than with its middle,
 * so it reads as a note under the row instead of floating in it. The row
 * keeps the spacing every other listing has.
 */
.toolbar__hint {
    display: inline-flex;
    align-items: center;
    align-self: flex-end;
    gap: 0.35rem;
    margin: 0;
    padding-bottom: 0.55rem;
}

.toolbar__hint svg {
    flex: none;
}

/*
 * The heading, what it says about itself and the badges under it all stand in
 * one column on the left, and the buttons keep the right.
 */
.faq-section__head {
    align-items: flex-start;
    gap: 1rem;
}

.faq-section__title {
    display: flex;
    flex-direction: column;
    gap: 0.45rem;
    min-width: 0;
    /* Half the panel and no more: a long heading otherwise runs under the
     * buttons and pushes them onto a line of their own. */
    max-width: 50%;
}

.faq-section__title h2 {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    min-width: 0;
    margin: 0;
    font-size: 1.1rem;
}

.faq-section__name,
.faq-section__description {
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
}

.faq-section__description {
    margin: 0;
}

.faq-section__facts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/* A section nobody can read is drawn as one. */
.faq-section.is-hidden {
    border-style: dashed;
}

.faq-section.is-hidden .faq-section__title h2 {
    color: var(--text-dim);
}

.faq-section__body {
    margin-top: 1.5rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.faq-section__body .data {
    margin: 0;
}

/*
 * An empty section says so from inside its own list, which is also what gives
 * that list a height for a question to be dropped onto.
 */
.faq-section__empty-row td {
    padding: 0;
    border: 0;
}

.faq-section__empty {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    margin: 0;
    padding: 1.35rem 1rem;
    color: var(--text-dim);
    font-size: 0.84rem;
}

.faq-section__empty svg {
    flex: none;
    color: var(--text-dim);
}

/*
 * Hidden the moment the section holds something. Written with the table in
 * the selector so it outranks the rule turning every row into a block on a
 * narrow screen.
 */
.data tr.faq-section__empty-row[hidden] {
    display: none;
}

/* A list holding no question shows no headings above the nothing. */
.faq-section__body .data:not(:has(tbody [data-sortable-id])) thead {
    display: none;
}

@media (max-width: 900px) {
    /* Nothing is beside anything any more, so the cap comes off. */
    .faq-section__title {
        max-width: 100%;
    }
}

/*
 * A switch pressed rather than ticked: it acts on the spot, after the portal
 * has asked, so there is nothing left to save.
 */
.toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 32px;
    height: 32px;
    padding: 0;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.toggle:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.toggle.is-on {
    background: var(--accent-soft);
    border-color: var(--accent);
    color: var(--accent);
}

.toggle-row {
    gap: 0.4rem;
}

/* --------------------------------------------------- Drag and drop lists */

.sortable__row {
    cursor: grab;
    transition: background-color 0.15s ease, opacity 0.15s ease;
}

/* The row being carried, drawn as the hole it left behind. */
.sortable__row.is-dragging {
    opacity: 0.35;
    outline: 1px dashed var(--border-strong);
    outline-offset: -1px;
}

/*
 * Where it would land, said by lighting the row up rather than by a line
 * drawn along one of its edges - which edge that was depended on the
 * direction of travel and read as a rendering fault.
 */
.sortable__row.is-over {
    background: var(--accent-soft);
}

/* The list a row is being carried into, when it came from another one. */
[data-sortable].is-target {
    background: var(--accent-soft);
}

/*
 * The handle is a chip of its own rather than plain text: it sits inside a
 * heading on the FAQ, and without a size of its own it took the size of the
 * heading with it.
 */
.sortable__handle {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    min-height: 24px;
    padding: 0.1rem 0.4rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-size: 0.72rem;
    font-weight: 600;
    line-height: 1;
    vertical-align: middle;
    cursor: grab;
    user-select: none;
    transition: border-color 0.15s ease, color 0.15s ease;
}

.sortable__handle svg {
    flex: none;
}

.sortable__handle:hover {
    border-color: var(--accent);
    color: var(--accent);
}

.sortable__handle:active {
    cursor: grabbing;
}

.sortable__position {
    font-family: var(--mono);
}

.sortable[aria-busy="true"] {
    opacity: 0.6;
    pointer-events: none;
}

/* --------------------------------------------------------- Wiki section tree */

.wiki-sidebar {
    /* Sticky with no bound scrolled a five level tree off the viewport. */
    max-height: calc(100vh - var(--header-height) - 2rem);
    overflow-y: auto;
}

.wiki-tree {
    display: flex;
    flex-direction: column;
    gap: 0.1rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.wiki-tree__row {
    display: flex;
    align-items: center;
    gap: 0.15rem;
}

.wiki-tree__link {
    flex: 1;
    min-width: 0;
}

.wiki-tree__label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.wiki-tree__children {
    margin-left: 0.55rem;
    padding-left: 0.55rem;
    border-left: 1px solid var(--border);
}

/* The chevron is its own disclosure, so the link beside it always navigates. */
.wiki-tree__group {
    flex: none;
}

.wiki-tree__chevron-only {
    display: flex;
    align-items: center;
    padding: 0.2rem;
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
    list-style: none;
}

.wiki-tree__chevron-only::-webkit-details-marker {
    display: none;
}

.wiki-tree__chevron-only .icon {
    transition: transform 0.2s ease;
}

.wiki-tree__group[open] > .wiki-tree__chevron-only .icon {
    transform: rotate(90deg);
}

.wiki-tree__group:not([open]) ~ .wiki-tree__children,
.wiki-tree__node > .wiki-tree__children:not(.is-open) {
    display: none;
}

.wiki-tree__node > .wiki-tree__row:has(.wiki-tree__group[open]) ~ .wiki-tree__children {
    display: block;
}

.wiki-tree__spacer {
    display: inline-block;
    width: 1.15rem;
    flex: none;
}

.wiki-icon {
    flex: none;
    border-radius: 3px;
    object-fit: cover;
}

/* ------------------------------------------------------- Admin section tree */

.admin-tree {
    display: flex;
    flex-direction: column;
    gap: 0.2rem;
    margin: 0;
    padding: 0;
    list-style: none;
}

.admin-tree .admin-tree {
    margin-top: 0.2rem;
    margin-left: 0.75rem;
    padding-left: 0.75rem;
    border-left: 1px solid var(--border);
}

.admin-tree__row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 0.6rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
}

.admin-tree__title {
    flex: 1;
    min-width: 8rem;
    color: var(--text-strong);
    font-weight: 600;
}

.admin-tree__actions {
    margin-left: auto;
}

/*
 * An empty branch is nothing to aim at until something is being carried, at
 * which point it becomes the way to put a page one level deeper.
 */
.admin-tree .admin-tree:empty {
    min-height: 0.4rem;
    border-left-style: dashed;
}

body.is-sorting .admin-tree .admin-tree:empty {
    min-height: 2rem;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
}

body.is-sorting .admin-tree.is-target {
    border-color: var(--accent);
    background: var(--accent-soft);
}

/*
 * A group of fields is a field.
 *
 * A `<fieldset>` is what says that several controls answer one question, and
 * it is what a screen reader announces the legend of. Left alone the browser
 * draws its own frame around it - a two pixel groove in a colour from nothing
 * on the page - so every one of them is reset to look like the plain block
 * beside it. The icon picker asked for this by name before there were three
 * more of them in the window a task is written in.
 */
fieldset.field,
.icon-source {
    /*
     * The inline margin only. A browser gives a fieldset two pixels either
     * side and nothing above or below, so this is the whole of what has to
     * go - and taking the block margin as well would have been a rule one
     * class heavier than every "leave a line above this" beside it.
     */
    margin-inline: 0;
    padding: 0;
    border: 0;
}

/* A legend is not laid out like anything else, so it is told to be. */
fieldset.field > legend {
    display: block;
    float: none;
    width: 100%;
    padding: 0;
}

/*
 * A block which begins a new subject rather than continuing the one above.
 * Half of what a window is made of is fields which follow each other, and the
 * three or four which start something need a line of air to be read as such.
 */
.field--parted {
    margin-top: 1.5rem;
}

/* ------------------------------------------------------------- Icon picker */

.icon-source__half {
    margin-top: 0.6rem;
}

.icon-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(2.9rem, 1fr));
    gap: 0.4rem;
    max-height: 13rem;
    overflow-y: auto;
    padding: 0.6rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-alt);
}

.icon-choice {
    display: flex;
    align-items: center;
    justify-content: center;
    aspect-ratio: 1;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--text-dim);
    cursor: pointer;
    transition:
        background 0.12s ease,
        border-color 0.12s ease,
        color 0.12s ease;
}

.icon-choice:hover {
    border-color: var(--border-strong);
    background: var(--panel-hover);
    color: var(--text-strong);
}

/* The one in use is filled rather than outlined, so it reads at a glance. */
.icon-choice:has(:checked) {
    border-color: var(--accent);
    background: var(--accent);
    color: #fff;
    box-shadow: var(--shadow-soft);
}

.editor__uploading {
    margin: 0;
    padding: 0.4rem 0.75rem;
    border-top: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.8rem;
}

/* ------------------------------------------------------------- Ticketing */

/*
 * The two badges which carry a ticket's whole state at a glance. Both are
 * driven by the raw value in a data attribute rather than by a class per
 * state, so a template writes the enum it already has and the stylesheet
 * decides what it looks like.
 */
.status-badge[data-status="NEW"] {
    border-color: var(--accent);
    color: var(--accent);
}

.status-badge[data-status="OPEN"],
.status-badge[data-status="WAITING"] {
    border-color: var(--warn);
    color: #e7bd63;
}

.status-badge[data-status="RESOLVED"] {
    border-color: var(--ok);
    color: #6fcb8b;
}

.status-badge[data-status="REJECTED"],
.status-badge[data-status="SPAM"] {
    border-color: var(--border-strong);
    color: var(--text-dim);
}

.priority-badge[data-priority="LOW"] {
    border-color: var(--border-strong);
    color: var(--text-dim);
}

.priority-badge[data-priority="HIGH"] {
    border-color: var(--warn);
    color: #e7bd63;
}

.priority-badge[data-priority="URGENT"] {
    border-color: var(--danger);
    color: #ff8f97;
}

/* The second row of filters sits under the first without its own margin. */
.toolbar--secondary {
    margin-top: -0.35rem;
}

.ticket-body {
    margin-top: 0.75rem;
    color: var(--text);
    line-height: 1.7;
}

.ticket-subheading {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 1.25rem 0 0.6rem;
    color: var(--text-dim);
    font-size: 0.78rem;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/*
 * The history of a ticket: one line down the left, one mark per entry. It is
 * an ordered list because that is what it is - the marks are drawn on the
 * list items rather than as bullets so the line can pass behind them.
 */
.timeline {
    position: relative;
    margin: 0.75rem 0 0;
    padding: 0 0 0 1.6rem;
    list-style: none;
}

.timeline::before {
    content: "";
    position: absolute;
    top: 0.35rem;
    bottom: 0.35rem;
    left: 0.44rem;
    width: 1px;
    background: var(--border);
}

.timeline__item {
    position: relative;
    padding-bottom: 1.1rem;
}

.timeline__item:last-child {
    padding-bottom: 0;
}

.timeline__mark {
    position: absolute;
    top: 0.05rem;
    left: -1.6rem;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 1.15rem;
    height: 1.15rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
}

.timeline__item[data-event="REPLY"] .timeline__mark,
.timeline__item[data-event="MESSAGE"] .timeline__mark {
    border-color: rgba(227, 79, 14, 0.45);
    color: var(--accent);
}

.timeline__item[data-event="NOTE"] .timeline__mark {
    border-color: rgba(217, 158, 43, 0.45);
    color: #e7bd63;
}

.timeline__item[data-event="STATUS"] .timeline__mark {
    border-color: rgba(59, 165, 93, 0.5);
    color: #6fcb8b;
}

.timeline__head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.5rem;
}

.timeline__meta,
.timeline__tag {
    color: var(--text-dim);
    font-size: 0.76rem;
}

.timeline__text {
    margin: 0.35rem 0 0;
    color: var(--text);
    line-height: 1.6;
}

/* A note reads as an aside rather than as something somebody was told. */
.timeline__text.is-internal {
    padding: 0.55rem 0.75rem;
    background: var(--panel-alt);
    border-left: 2px solid var(--warn);
    border-radius: 0 var(--radius) var(--radius) 0;
}

.timeline__delivery {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-top: 0.45rem;
}

.timeline__tag {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    margin-top: 0.4rem;
}

/*
 * The letter, as the server composed it. A wider dialog than the rest,
 * because an e-mail read in a column of thirty characters is not read.
 */
.modal--wide {
    width: min(46rem, calc(100vw - 2rem));
}

.letter {
    margin: 0.85rem 0;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.letter__head {
    display: grid;
    gap: 0.3rem;
    margin: 0;
    padding: 0.75rem 0.9rem;
    border-bottom: 1px solid var(--border);
}

.letter__head > div {
    display: grid;
    grid-template-columns: 6.5rem minmax(0, 1fr);
    gap: 0.5rem;
}

.letter__head dt {
    color: var(--text-dim);
    font-size: 0.76rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.letter__head dd {
    margin: 0;
    color: var(--text-strong);
    font-size: 0.86rem;
    overflow-wrap: anywhere;
}

.letter__body {
    max-height: 22rem;
    margin: 0;
    padding: 0.9rem;
    overflow: auto;
    color: var(--text);
    font-family: var(--font);
    font-size: 0.86rem;
    line-height: 1.65;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}

@media screen and (max-width: 720px) {
    .letter__head > div {
        grid-template-columns: minmax(0, 1fr);
        gap: 0.1rem;
    }
}

/*
 * The rows of a screen made of panels. The gap is the same one the grids
 * inside those rows use, so a row of two panels and a row of one are spaced
 * alike however the screen is cut.
 */
.panel-rows {
    display: grid;
    gap: 1.25rem;
}

/*
 * A ticket read as what it is: a conversation, stacked one bubble under the
 * next in the order it was said. Who said it is the tag beside the name and
 * the tint of the bubble rather than which edge it is glued to - a thread
 * read top to bottom is read once.
 *
 * The three lists of the screen - this one, the notes and the history - are
 * given a height and scroll inside it. A ticket which has been going on for a
 * month must not push the box somebody is typing in off the screen.
 */
.chat,
.note-list,
.panel > .timeline {
    max-height: 26rem;
    overflow-y: auto;
    overscroll-behavior-y: contain;
}

.chat {
    display: grid;
    gap: 0.9rem;
    margin: 1rem 0 0;
    padding: 0 0.35rem 0 0;
    list-style: none;
}

.chat__item {
    display: grid;
    gap: 0.35rem;
    justify-items: start;
}

.chat__who {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    color: var(--text-dim);
    font-size: 0.78rem;
}

.chat__who .avatar {
    border-radius: var(--radius);
}

.chat__who strong {
    color: var(--text-strong);
}

.chat__role {
    padding: 0.05rem 0.4rem;
    border: 1px solid var(--border-strong);
    border-radius: 999px;
    font-size: 0.68rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
}

.chat__bubble {
    width: 100%;
    padding: 0.75rem 0.95rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text);
    line-height: 1.65;
    overflow-wrap: anywhere;
}

.chat__bubble > p {
    margin: 0 0 0.6rem;
}

.chat__bubble > p:last-child {
    margin-bottom: 0;
}

/* The two voices: the one which wrote in, and ours answering it. */
.chat__item--team .chat__bubble {
    background: rgba(227, 79, 14, 0.07);
    border-color: rgba(227, 79, 14, 0.3);
}

.chat__item--team .chat__role {
    border-color: var(--accent);
    color: var(--accent);
}

.chat__delivery {
    display: inline-flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/*
 * The notes the team keeps, beside the history rather than inside the thread:
 * they are about the ticket, not part of the conversation, and every one of
 * them carries the button which takes it back.
 */
.note-list {
    display: grid;
    gap: 0.8rem;
    margin: 0.85rem 0 0;
    padding: 0 0.35rem 0 0;
    list-style: none;
}

.note-list__item {
    padding: 0.7rem 0.85rem;
    background: rgba(217, 158, 43, 0.08);
    border: 1px solid rgba(217, 158, 43, 0.32);
    border-radius: var(--radius);
}

.note-list__head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
    margin-bottom: 0.35rem;
    color: var(--text-dim);
    font-size: 0.78rem;
}

.note-list__head strong {
    color: var(--text-strong);
}

.note-list__head .inline-form {
    margin-left: auto;
}

.note-list__when {
    opacity: 0.75;
}

.note-list__text {
    color: var(--text);
    line-height: 1.6;
}

.note-list__text > p {
    margin: 0 0 0.5rem;
}

.note-list__text > p:last-child {
    margin-bottom: 0;
}

/*
 * The key and value table of a panel, framed the way every other box of the
 * administration is: a hairline around it and the same corner radius, so it
 * reads as part of the panel rather than as a spreadsheet dropped into one.
 */
.table-wrap--framed {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.table-wrap--framed table.data tr:last-child th,
.table-wrap--framed table.data tr:last-child td {
    border-bottom: 0;
}

/* The three fields of the state form are three lines, not two and a half. */
.ticket-state .form-grid + .field {
    margin-top: 1.1rem;
}

/* The box the sender writes their answer in, under the conversation. */
.ticket-reply {
    margin-top: 1.5rem;
    padding-top: 1.25rem;
    border-top: 1px solid var(--border);
}

@media screen and (max-width: 620px) {
    .chat__item,
    .chat__item--internal {
        max-width: 100%;
    }
}

/* The name of a sender and what kind of sender they are, on one line. */
.sender-line {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.5rem;
}

/*
 * The scrollbars of the portal.
 *
 * Thin, and in the colours of the page rather than in the ones the operating
 * system happens to use: a pale slab down the side of a dark panel is the
 * loudest thing on the screen, and it is the one thing on it nobody wanted to
 * look at. Firefox is told in two lines, everything else through the WebKit
 * pseudo elements, and both say the same thing.
 */
* {
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
}

::-webkit-scrollbar {
    width: 9px;
    height: 9px;
}

::-webkit-scrollbar-track {
    background: transparent;
}

::-webkit-scrollbar-thumb {
    background: var(--border-strong);
    border: 2px solid transparent;
    border-radius: 999px;
    background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
    background: var(--accent);
    background-clip: padding-box;
}

::-webkit-scrollbar-corner {
    background: transparent;
}

/* A row which does not move says so where its handle would have been. */
.sortable__handle.is-fixed {
    cursor: default;
    opacity: 0.55;
}

/* A ticket which takes no more messages says so where the box used to be. */
.ticket-closed {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    margin: 1.25rem 0 0;
}

/*
 * A list of what came before, beside the decision being taken now: three of
 * them are as much as a panel can carry without becoming the panel, and the
 * rest scroll where they are.
 */
.report-list--short {
    max-height: 20rem;
    overflow-y: auto;
    padding-right: 0.35rem;
}

/* ------------------------------------------------------- History of a page */

/*
 * One entry per save: who, when, what settings moved, and what the body
 * became. The difference is drawn the way every code review draws one - the
 * lines which went above the lines which came - because that is the shape
 * everybody already knows how to read.
 */
.form-heading {
    margin: 1.75rem 0 0.75rem 0;
    color: var(--text-strong);
    font-size: 0.95rem;
    font-weight: 700;
}

.form-heading:first-of-type {
    margin-top: 1.25rem;
}

.history {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    margin: 1rem 0 0 0;
    padding: 0;
    list-style: none;
}

.history__entry {
    padding: 0.85rem 1rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel-alt);
}

.history__head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6rem;
    color: var(--text-dim);
    font-size: 0.85rem;
}

.history__who,
.history__when {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
}

.history__who strong {
    color: var(--text-strong);
}

.history__restore {
    margin-left: auto;
}

.history__changes {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.4rem;
    margin-top: 0.6rem;
}

.change-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    max-width: 100%;
    padding: 0.2rem 0.5rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
    color: var(--text-dim);
    font-size: 0.78rem;
}

.change-chip__field {
    color: var(--text-strong);
    font-weight: 600;
}

.change-chip__before {
    text-decoration: line-through;
}

.change-chip__after {
    color: var(--ok);
}

.diff {
    margin-top: 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-alt);
    overflow: hidden;
}

.diff__counts {
    display: flex;
    align-items: baseline;
    gap: 0.6rem;
    padding: 0.35rem 0.7rem;
    border-bottom: 1px solid var(--border);
    font-family: var(--mono);
    font-size: 0.78rem;
}

/* Which of the two texts of a page this difference is about. The counts are
   pushed to the far end, where they read as the size of the change. */
.diff__what {
    margin-right: auto;
    color: var(--text-strong);
    font-family: var(--font);
    font-weight: 600;
}

.diff__added {
    color: var(--ok);
}

.diff__removed {
    color: var(--danger);
}

.diff__body {
    display: flex;
    flex-direction: column;
    max-height: 26rem;
    overflow: auto;
    font-family: var(--mono);
    font-size: 0.78rem;
    line-height: 1.55;
}

/*
 * One line, the way git draws it: its number in the text before and in the
 * text after, its sign, and the line itself. The numbers and the sign sit in
 * a gutter of their own so the text of every line starts in one column,
 * however long the page is.
 */
.diff__line {
    display: grid;
    grid-template-columns: 2.6rem 2.6rem 1.3rem minmax(0, 1fr);
    color: var(--text);
}

.diff--set .diff__line {
    grid-template-columns: 1.6rem minmax(0, 1fr);
}

.diff__number {
    padding: 0 0.45rem;
    border-right: 1px solid var(--border);
    color: var(--text-dim);
    opacity: 0.7;
    text-align: right;
    user-select: none;
}

.diff__sign {
    color: var(--text-dim);
    text-align: center;
    user-select: none;
}

.diff__text {
    padding-right: 0.7rem;
    white-space: pre-wrap;
    word-break: break-word;
}

.diff__line--added {
    background: rgba(59, 165, 93, 0.12);
    color: var(--text-strong);
}

.diff__line--added .diff__sign {
    color: var(--ok);
}

.diff__line--removed {
    background: rgba(226, 33, 47, 0.1);
}

.diff__line--removed .diff__sign {
    color: var(--danger);
}

.diff__line--context {
    color: var(--text-dim);
}

/* The words a rewritten line changed, marked inside it the way a review does. */
.diff__mark {
    padding: 0 0.1rem;
    border-radius: 2px;
    color: inherit;
}

.diff__line--added .diff__mark {
    background: rgba(59, 165, 93, 0.38);
}

.diff__line--removed .diff__mark {
    background: rgba(226, 33, 47, 0.36);
    text-decoration: line-through;
    text-decoration-color: rgba(255, 255, 255, 0.35);
}

/* Where a stretch of change sits, as git heads it. */
.diff__hunk {
    padding: 0.15rem 0.7rem;
    border-block: 1px solid var(--border);
    background: rgba(63, 154, 214, 0.1);
    color: #7fb8e6;
    user-select: none;
}

.diff__hunk:first-child {
    border-top: 0;
}

.diff__note {
    color: var(--text-dim);
    font-family: var(--font);
}

.diff__line--more {
    color: var(--text-dim);
    font-style: italic;
}

/*
 * Golds
 *
 * The currency reads as one object rather than as a number beside a word: a
 * coin, the figure and the unit inside one badge. It wears the corner radius
 * of the portal like everything else - a pill in a page built out of square
 * panels is the one thing on it which came from somewhere else.
 */
/*
 * Given the same height as the chips of facts the administration puts in the
 * head of a panel, so that a row holding one does not look squeezed.
 */
.gold-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    min-height: 32px;
    padding: 0.3rem 0.65rem;
    background: rgba(242, 196, 0, 0.09);
    border: 1px solid rgb(242, 198, 0);
    border-radius: var(--radius);
    color: var(--brand-yellow);
    font-size: 0.95rem;
    font-weight: 700;
    line-height: 1.2;
    white-space: nowrap;
    vertical-align: middle;
}

/* In the corner of the account menu, where the room is a menu wide. */
.gold-chip--small {
    min-height: 24px;
    padding: 0.15rem 0.45rem;
    gap: 0.3rem;
    font-size: 0.8rem;
}

/* Standing in a row of badges, beside the role of the account. */
.gold-chip--badge {
    min-height: 25px;
    padding: 0.25rem 0.6rem;
    font-size: 0.85rem;
}

.gold-chip__coin {
    display: inline-flex;
    align-items: center;
    color: var(--brand-yellow);
}

.gold-chip__value {
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.01em;
}

/* The name of the currency is part of it, so it wears its colour too. */
.gold-chip__unit {
    color: var(--brand-yellow);
    font-size: 0.82em;
    font-weight: 600;
    opacity: 0.85;
}

/* The pencil is part of the badge and sits against its right edge. */
.gold-chip__edit {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.35rem;
    height: 1.35rem;
    margin: -0.2rem -0.35rem -0.2rem 0.1rem;
    padding: 0;
    background: transparent;
    border: 0;
    border-radius: var(--radius);
    color: var(--text-dim);
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.gold-chip__edit:hover {
    background: rgba(242, 196, 0, 0.18);
    color: var(--brand-yellow);
}

/* The balance sits with the name and the badge rather than under them. */
.user-card .post-meta {
    display: flex;
    align-items: center;
    gap: 1rem;
    flex-wrap: wrap;
}

.user-card .post-meta > div:first-of-type {
    margin-right: auto;
}

.gold-dialog__title {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.gold-dialog__title .icon {
    color: var(--brand-yellow);
}

/*
 * What one row of the history says. The figure which moved is the thing
 * somebody is scanning for, so it carries the colour and the weight; the
 * balance beside it is the same number in the ordinary voice of a table.
 */
.gold-move {
    font-size: 0.95rem;
    font-variant-numeric: tabular-nums;
}

.gold-move.is-gain {
    color: var(--ok);
}

.gold-move.is-loss {
    color: var(--danger);
}

.gold-balance {
    color: var(--text-strong);
    font-variant-numeric: tabular-nums;
}

/*
 * Skins
 *
 * A skin is sixty-four pixels across and is meant to look it. Every picture
 * drawn from one is rendered with its own pixels rather than smoothed, which
 * is the difference between a character and a smudge - the browser is told so
 * here, once, for the faces, the sheets and the whole bodies alike.
 */
.skin-face,
.skin-sheet,
.skin-body,
.avatar {
    image-rendering: pixelated;
}

/*
 * A gallery of characters rather than of faces, so the cards are the shape a
 * person is: narrow and upright, and packed tightly enough that a screenful
 * of them can be compared by looking rather than by scrolling.
 */
.skin-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
    gap: 1rem;
    margin-top: 1rem;
}

.skin-card {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding: 0.75rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: border-color 0.15s ease, background 0.15s ease;
}

.skin-card:hover {
    background: var(--panel-hover);
    border-color: var(--border-strong);
}

/*
 * Where the character stands. Every stage is the same height whatever is on
 * it, so a row of cards lines up along the floor instead of along the top of
 * whichever head happens to be tallest.
 */
.skin-card__stage {
    position: relative;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    min-height: 14.5rem;
    padding: 0.75rem 0.5rem 0.6rem;
    background:
        radial-gradient(120% 70% at 50% 100%, rgba(255, 255, 255, 0.05), transparent 70%),
        var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}

.skin-card__stage .skin-body {
    max-width: 100%;
    height: auto;
}

/*
 * What is done to a skin, in the corner of the picture it is done to.
 *
 * Off the card's width and onto the picture, which has room to spare beside
 * a character: a row of symbols underneath was a line the card had to be
 * tall enough for, and every one of them was further from the thing it acts
 * on than the picture itself.
 */
.skin-card__tools {
    position: absolute;
    right: 0.4rem;
    bottom: 0.4rem;
    display: flex;
    align-items: center;
    gap: 0.3rem;
}

.skin-card__tools .inline-form,
.skin-card__tools form {
    margin: 0;
}

/*
 * Dark enough to read against whatever the character happens to be wearing,
 * which is a picture rather than a background this can be sure of.
 */
.skin-card__tools .button {
    width: auto;
    min-width: 1.9rem;
    height: 1.9rem;
    padding: 0 0.45rem;
    background: rgba(19, 18, 17, 0.82);
    backdrop-filter: blur(2px);
}

.skin-card__tools .button:hover {
    background: var(--panel-hover);
}

/*
 * Except the one which is not merely done to the skin but decides something
 * about it - offering a player's to everybody. It keeps the colour which
 * says so, or it is three identical symbols and one of them is not.
 */
.skin-card__tools .button--primary {
    background: var(--accent);
}

.skin-card__tools .button--primary:hover {
    background: var(--accent-dark);
}

/* The one in play says so on the picture, where the eye already is. */
.skin-card__flag {
    position: absolute;
    top: 0.4rem;
    right: 0.4rem;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.15rem 0.45rem;
    background: var(--accent);
    border-radius: var(--radius);
    color: #fff;
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.02em;
}

.skin-card__flag--default {
    background: var(--panel-hover);
    border: 1px solid var(--border-strong);
    color: var(--text-strong);
}

/*
 * A fact about the card rather than a label on it - when a skin arrived, say.
 * Quiet on purpose: it is read once, by somebody already looking at the
 * picture it sits on.
 */
.skin-card__flag--quiet {
    background: rgba(19, 18, 17, 0.72);
    border: 1px solid var(--border);
    color: var(--text-dim);
    font-weight: 500;
    text-transform: none;
    letter-spacing: 0;
}

/* One's own, which is the one thing about a card nobody else's card says. */
.skin-card__flag--mine {
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    color: var(--text-strong);
}

.skin-card.is-worn {
    border-color: var(--accent);
}

.skin-card.is-worn .skin-card__stage {
    border-color: rgba(227, 79, 14, 0.45);
    background:
        radial-gradient(120% 70% at 50% 100%, rgba(227, 79, 14, 0.16), transparent 72%),
        var(--bg);
}

.skin-card__info {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    min-width: 0;
}

.skin-card__name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--text-strong);
    font-size: 0.9rem;
    font-weight: 600;
}

.skin-card__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.35rem;
}

.skin-card__when {
    color: var(--text-dim);
    font-size: 0.75rem;
}

.skin-card__owner {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    min-width: 0;
    font-size: 0.8rem;
}

.skin-card__owner a {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.skin-card__actions {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 0.4rem;
    margin-top: auto;
}

.skin-card__actions .inline-form,
.skin-card__actions form {
    margin: 0;
}

/*
 * Two rows of skins and then it scrolls.
 *
 * A team which has been running a while has hundreds of them, and a gallery
 * of hundreds is a page whose every other panel sits somewhere below the
 * bottom of the screen. The height is two cards and the gap between them, so
 * the third row is half in view - which is what says there is more.
 */
.skin-grid--paged {
    /* Two cards of 385 pixels and the gap between them, rounded up a hair. */
    max-height: 50rem;
    overflow-y: auto;
    overscroll-behavior-y: contain;
    padding-right: 0.4rem;
}

/*
 * A button inside a skin card wraps rather than running out of it. The cards
 * are as narrow as a character and some of the things which can be done to
 * one are said in three words.
 */
.skin-card__actions .button {
    width: 100%;
    justify-content: center;
    white-space: normal;
    line-height: 1.3;
    text-align: center;
}

/* The quiet pair - fetch the file, throw the skin away - share a line. */
.skin-card__row {
    display: flex;
    gap: 0.4rem;
}

.skin-card__row > * {
    flex: 1;
    min-width: 0;
}

/*
 * The bottom of a card: what is done to the skin on the left, taking the
 * room, and the two things only one's own skin offers in the corner beside
 * it. Everything on the line is the same height, so a row of cards ends on
 * one line whichever of them happen to be one's own.
 */
.skin-card__bar {
    display: flex;
    align-items: stretch;
    gap: 0.4rem;
}

.skin-card__bar > :first-child {
    flex: 1;
    min-width: 0;
}

.skin-card__bar > :not(:first-child) {
    flex: none;
}

/*
 * On this line the words keep to one line. Cards elsewhere let a button wrap
 * because the whole width is theirs; here two symbols share it, and a label
 * folding in half makes one card taller than the four beside it.
 */
.skin-card__bar .button {
    white-space: nowrap;
    padding: 0 0.6rem;
}

/* An icon keeps its own width where a sentence would take the whole line. */
.skin-card__actions .button--icon {
    width: auto;
}

/*
 * A line of nothing but symbols. None of them takes the room, so they gather
 * at the end of it the way the two quiet ones do on every other card.
 */
.skin-card__bar--icons {
    justify-content: flex-end;
}

.skin-card__bar--icons > :first-child {
    flex: none;
}

/*
 * What stands where the button would be once the skin is already on. It is
 * not a button: there is nothing left to press, and a greyed one would say
 * that there is and that it is out of reach.
 */
.skin-card__worn {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    height: 2.1rem;
    padding: 0 0.8rem;
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: var(--radius);
    color: var(--text-strong);
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.skin-card__worn > svg {
    flex: none;
    color: var(--accent);
}

/* Renaming happens where the name is, rather than on a screen of its own. */
.skin-card__rename {
    display: flex;
    gap: 0.3rem;
}

.skin-card__rename input {
    min-width: 0;
    flex: 1;
    padding: 0.3rem 0.5rem;
    font-size: 0.8rem;
}

.skin-card__rename .button {
    width: auto;
    flex: none;
}

/*
 * The box a skin is uploaded into. Dashed, because it is the one part of
 * these screens which is waiting for something rather than showing it.
 */
.skin-upload {
    margin-top: 1.25rem;
    padding: 1.1rem;
    background: var(--panel-alt);
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius);
}

/* Each gallery is a panel of its own, spaced like every other panel. */
.skin-section {
    margin-top: 1.5rem;
}

/*
 * The two things somebody arrives at this screen to do, side by side: look at
 * what they are wearing, and bring something new. The left one is wider - it
 * holds a character standing up, and the right one holds two fields.
 */
.skin-top {
    display: grid;
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    align-items: start;
    gap: 1.5rem;
}

@media (max-width: 900px) {
    .skin-top {
        grid-template-columns: minmax(0, 1fr);
    }
}

.skin-current__body {
    display: flex;
    align-items: stretch;
    flex-wrap: wrap;
    gap: 1.25rem;
    margin-top: 0.35rem;
}

.skin-current__side {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.3rem;
    flex: 1;
    min-width: 11rem;
}

.skin-current__name {
    color: var(--text-strong);
    font-size: 1.3rem;
    line-height: 1.2;
}

.skin-current__sheet {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
    margin-top: 0.9rem;
}

.skin-current__sheet .skin-sheet {
    max-width: 100%;
    height: auto;
    padding: 0.4rem;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.skin-current__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 0.4rem;
    margin-top: auto;
    padding-top: 0.85rem;
}

.skin-current__actions .inline-form {
    margin: 0;
}

.skin-current__hint {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    margin: 0.85rem 0 0;
}

/*
 * The space a character stands and turns in.
 *
 * It arrives holding the picture the server drew; when the script can build
 * the model that picture is of, the model is put in the same place and the
 * picture is put away. `touch-action` is what stops a finger dragging the
 * page underneath instead of turning the character.
 */
.skin-stage {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 13.5rem;
    min-height: 20rem;
    padding: 1rem;
    background:
        radial-gradient(120% 65% at 50% 100%, rgba(227, 79, 14, 0.14), transparent 70%),
        var(--bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    overflow: hidden;
    perspective: 900px;
}

.skin-stage[data-skin-viewer] {
    cursor: grab;
    touch-action: none;
}

.skin-stage.is-turning {
    cursor: grabbing;
}

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

/*
 * Six boxes of six faces, each face one rectangle of the same sheet. The
 * model itself has no size: it is a point in the middle of the stage which
 * everything else is hung around, which is also what it turns about.
 */
.skin-model {
    position: relative;
    width: 0;
    height: 0;
    transform-style: preserve-3d;
    transition: transform 0.25s ease-out;
}

/* While a finger is on it the model follows the finger rather than trailing it. */
.skin-stage.is-turning .skin-model {
    transition: none;
}

.skin-model__box {
    position: absolute;
    left: 0;
    top: 0;
    transform-style: preserve-3d;
}

.skin-model__face {
    position: absolute;
    left: 0;
    top: 0;
    background-repeat: no-repeat;
    image-rendering: pixelated;
    backface-visibility: hidden;
}

/*
 * Bringing a skin in. The dashed box belongs to the picker inside the form
 * rather than to the form, which is a panel like any other.
 */
.skin-upload {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    margin-top: 0.85rem;
}

.skin-upload .form-actions {
    margin-top: 0.15rem;
}

/* A sheet is unreadable small, so the picked one is shown as large as it fits. */
.skin-upload__preview {
    width: 64px;
    height: auto;
    flex: none;
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

/* Inside a panel which already names itself, an empty gallery is one line. */
.skin-section .empty-state {
    padding: 1.75rem 1rem;
}

/*
 * Who somebody is right now, said once and at the top of the screen. It is
 * the answer every gallery under it is a way of changing, so the character is
 * drawn larger here than in the galleries it is being chosen from.
 */
.skin-worn {
    display: flex;
    align-items: stretch;
    gap: 1.5rem;
    flex-wrap: wrap;
}

.skin-worn__stage {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    padding: 1rem 1.25rem 0.75rem;
    background:
        radial-gradient(120% 70% at 50% 100%, rgba(227, 79, 14, 0.12), transparent 70%),
        var(--bg);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
}

.skin-worn__text {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 0.35rem;
    flex: 1;
    min-width: 14rem;
}

.skin-worn__text strong {
    color: var(--text-strong);
    font-size: 1.35rem;
    line-height: 1.2;
}

.skin-worn__badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
}

.skin-worn__note {
    max-width: 42ch;
    margin: 0.25rem 0 0;
}

.skin-worn__actions {
    margin-top: auto;
    padding-top: 0.75rem;
}

/*
 * The file the character was folded out of, standing beside it. A sheet is
 * unreadable as a picture and that is the point: it is what leaves with you
 * when you press the button under it.
 */
.skin-worn__file {
    display: flex;
    flex-direction: column;
    align-items: center;
    align-self: center;
    gap: 0.5rem;
    padding: 0.85rem;
    background: var(--panel-alt);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.skin-worn__file .skin-sheet {
    background: var(--bg);
    border: 1px solid var(--border);
    border-radius: var(--radius);
}

.skin-worn__file .button {
    width: 100%;
    justify-content: center;
}

/*
 * On a telephone the character is the thing worth keeping, so it stays whole
 * and the rest of the card simply stacks underneath it.
 */
@media (max-width: 640px) {
    .skin-worn {
        gap: 1rem;
    }

    .skin-worn__stage,
    .skin-worn__file {
        width: 100%;
    }

    .skin-card__stage {
        min-height: 12rem;
    }
}

/*
 * The history of the administration
 *
 * One save is one card, the way one commit is one commit: who made it and
 * when across the top, every thing it touched underneath, and the address and
 * the browser it came from along the bottom where they are found when they
 * are wanted and ignored when they are not. What was done to each thing is
 * said by a colour down its left edge as well as by a word, so a page of saves
 * reads at a glance as mostly changes with the odd creation and deletion.
 */
.audit {
    display: flex;
    flex-direction: column;
    gap: 0.85rem;
    margin: 0.75rem 0 0 0;
    padding: 0;
    list-style: none;
}

.audit__entry {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel);
    overflow: hidden;
}

.audit__head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 0.6rem 1rem;
    padding: 0.7rem 1rem;
    border-bottom: 1px solid var(--border);
    background: var(--panel-alt);
}

.audit__who {
    display: inline-flex;
    align-items: center;
    gap: 0.6rem;
    min-width: 0;
}

.audit__who .avatar {
    flex: none;
    border-radius: var(--radius);
}

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

.audit__name {
    color: var(--text-strong);
    font-weight: 700;
    text-decoration: none;
}

a.audit__name:hover {
    color: var(--accent);
}

.audit__meta {
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6rem;
    color: var(--text-dim);
    font-size: 0.82rem;
}

.audit__area {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    text-decoration: none;
}

a.audit__area:hover {
    border-color: var(--accent);
    color: var(--text-strong);
}

.audit__when {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    white-space: nowrap;
}

.audit__changes {
    display: flex;
    flex-direction: column;
    gap: 0.6rem;
    padding: 0.8rem 1rem;
}

.audit-change {
    padding: 0.6rem 0.85rem 0.7rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--panel-alt);
}

.audit-change__head {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.45rem;
    min-width: 0;
    font-size: 0.86rem;
}

.audit-action {
    padding: 0.08rem 0.45rem;
    border-radius: var(--radius);
    background: rgba(217, 158, 43, 0.14);
    color: var(--warn);
    font-size: 0.7rem;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

.audit-action--created {
    background: rgba(59, 165, 93, 0.14);
    color: var(--ok);
}

.audit-action--deleted {
    background: rgba(226, 33, 47, 0.14);
    color: var(--danger);
}

.audit-change__noun {
    color: var(--text-dim);
}

.audit-change__title {
    min-width: 0;
    color: var(--text-strong);
    font-weight: 700;
    overflow-wrap: anywhere;
}

a.audit-change__title {
    text-decoration: none;
}

a.audit-change__title:hover {
    color: var(--accent);
    text-decoration: underline;
}

.audit-change__id {
    color: var(--text-dim);
    font-family: var(--mono);
    font-size: 0.75rem;
}

/* The way into the history of this one thing, kept out of the way of its name. */
.audit-change__follow {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    margin-left: auto;
    color: var(--text-dim);
    font-size: 0.75rem;
    text-decoration: none;
}

.audit-change__follow:hover {
    color: var(--accent);
}

/*
 * The fields of a thing which are values rather than texts, as a table: the
 * name down the left, what it was and what it became on the right. One row
 * each, so a task created with seven fields reads as seven lines rather than
 * as a heap of labels wrapping into each other.
 */
.audit-fields {
    display: flex;
    flex-direction: column;
    margin-top: 0.55rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-alt);
    overflow: hidden;
}

.audit-field {
    display: grid;
    grid-template-columns: minmax(8rem, 13rem) minmax(0, 1fr);
    align-items: baseline;
    gap: 0.75rem;
    padding: 0.32rem 0.7rem;
    font-size: 0.82rem;
}

.audit-field + .audit-field {
    border-top: 1px solid var(--border);
}

.audit-field__name {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--text-dim);
    font-weight: 600;
}

.audit-field__values {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.3rem 0.5rem;
    min-width: 0;
    overflow-wrap: anywhere;
}

.audit-field__before {
    padding: 0 0.3rem;
    border-radius: 3px;
    background: rgba(226, 33, 47, 0.1);
    color: var(--text);
    text-decoration: line-through;
    text-decoration-color: rgba(226, 33, 47, 0.6);
}

.audit-change--deleted .audit-field__before {
    text-decoration: none;
}

.audit-field__after {
    padding: 0 0.3rem;
    border-radius: 3px;
    background: rgba(59, 165, 93, 0.12);
    color: var(--text-strong);
}

.audit-field__arrow {
    color: var(--text-dim);
}

.audit-lang {
    padding: 0 0.3rem;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    color: var(--text-dim);
    font-family: var(--mono);
    font-size: 0.66rem;
    font-weight: 600;
}

.audit-none {
    color: var(--text-dim);
    font-style: italic;
}

.audit-secret {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    color: var(--text-strong);
}

.audit-image {
    width: 44px;
    height: 30px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    object-fit: cover;
    vertical-align: middle;
}

.audit-change .diff {
    margin-top: 0.55rem;
}

.audit__more > summary {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
    color: var(--text-dim);
    font-size: 0.82rem;
    cursor: pointer;
    list-style: none;
}

.audit__more > summary::-webkit-details-marker {
    display: none;
}

.audit__more > summary:hover {
    color: var(--accent);
}

.audit__more[open] > summary {
    margin-bottom: 0.6rem;
}

.audit__more[open] > summary .icon {
    transform: rotate(180deg);
}

.audit__more > .audit-change + .audit-change {
    margin-top: 0.6rem;
}

.audit__foot {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem 1.1rem;
    padding: 0.5rem 1rem;
    border-top: 1px solid var(--border);
    color: var(--text-dim);
    font-size: 0.75rem;
}

.audit__foot > span {
    display: inline-flex;
    align-items: center;
    gap: 0.3rem;
    min-width: 0;
}

.audit__foot code {
    color: var(--text);
    font-family: var(--mono);
    font-size: 0.74rem;
    overflow-wrap: anywhere;
}

.audit__agent {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* The rest of the question, under the chips: who, what, from and until when. */
.audit-filters {
    display: flex;
    align-items: flex-end;
    flex-wrap: wrap;
    gap: 0.75rem 1rem;
    margin-top: 0.75rem;
    padding: 0.85rem 1rem;
}

.audit-filters .field {
    flex: 1 1 11rem;
    min-width: 10rem;
    margin: 0;
}

.audit-filters__actions {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    margin-left: auto;
}

.audit-following {
    align-items: center;
    margin-top: 0.75rem;
}

.audit-following .button {
    margin-left: auto;
}

.audit-total {
    margin: 1rem 0 0 0;
    color: var(--text-dim);
    font-size: 0.82rem;
}

/* The history a section keeps on its own screen, under the thing it is the history of. */
.own-history {
    margin-top: 1.25rem;
}

.own-history__head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 0.4rem 1rem;
}

.own-history__head h3 {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    margin: 0;
}

@media (max-width: 640px) {
    .audit__head,
    .audit__changes,
    .audit__foot {
        padding-left: 0.75rem;
        padding-right: 0.75rem;
    }

    .audit-change__follow {
        margin-left: 0;
    }

    .audit-filters__actions {
        width: 100%;
        margin-left: 0;
    }
}
