/*
 * Awajdo — application styles.
 *
 * Served from our own origin as a file, never inline. That is what lets the
 * Content-Security-Policy stay strict (no 'unsafe-inline' for styles), which
 * in turn is what makes an injected style="..." or <style> block inert.
 */

/*
 * The palette below is a FALLBACK, not the live one.
 *
 * Every colour token here is overridden per response by the <style> block
 * App\Libraries\Theme emits into <head>, which is how the admin console's
 * palette setting re-colours the site without a deploy. These values are what
 * renders if that block is ever missing — a cached page from before the
 * feature existed, a view rendered outside a request — so they are kept as a
 * complete, working theme rather than as placeholders.
 *
 * Adding a token: give it a sensible value in BOTH blocks here, and in both
 * halves of Theme::palette(). A token that only exists in one of the two is a
 * colour that vanishes in dark mode.
 */
:root {
    --bg: #f6f9f8;
    --surface: #ffffff;
    --surface-2: #f1f7f5;
    --border: #e0e9e6;
    --text: #152621;
    --muted: #5b766e;
    --brand: #16604a;
    --brand-ink: #ffffff;
    --brand-strong: #155b46;
    --brand-soft: #eef9f6;
    --brand-line: #cde7df;
    --accent: #b4531f;
    --accent-soft: #fbf1ec;
    --danger: #a32020;
    --danger-bg: #fdeceb;
    --ok: #1c6b3f;
    --ok-bg: #e8f5ec;
    --warn: #8a5a00;
    --warn-bg: #fdf3e0;
    --shadow: 0 1px 2px rgba(16, 32, 48, .06), 0 4px 12px rgba(16, 32, 48, .05);
    --shadow-lg: 0 20px 50px rgba(16, 32, 48, .13);
    --hero-from: #f1fbf8;
    --hero-to: #fdfefe;
    --band-from: #1d7c60;
    --band-to: #135340;
    --band-ink: #ffffff;

    /*
     * The brand mark's status semaphore — reported (red) -> in review (amber)
     * -> resolved (green) — mirrors what a post's own state means elsewhere in
     * the app. Not themed, on purpose: these read as a status colour code, and
     * the admin palette setting does not get a vote on what "resolved" looks
     * like. Read by the mark in partials/brand.php and by nothing else. The
     * bubble itself uses --brand, so it recolours with the rest of the site.
     */
    --mark-reported: #e8373a;
    --mark-review: #f5a623;
    --mark-resolved: #2fb35b;
    --mark-dot: #ff7a29;
    --mark-edge: rgba(16, 32, 48, .16);

    /* Not themed: geometry and rhythm are the same whatever the colour. */
    --radius: 12px;
    /*
     * No cap: the app bar, the page and the footer run the width of the
     * window, and the gutter below is what keeps them off its edge. Measure
     * is held further in instead, by the reading column (.layout--single),
     * the prose rule and the feed's own tracks, so a wide screen buys room
     * for the furniture rather than longer lines of text.
     */
    --max: none;

    /*
     * The landing page is the exception. Its bands are composed for a measure
     * (headline, sub and artwork side by side) and that composition does not
     * get better at 2560px, so `.shell` keeps a cap of its own.
     */
    --shell-max: 1280px;
    --gutter: clamp(16px, 4vw, 40px);

    /*
     * The app bar's height, as a token because three other things measure
     * themselves against it: both feed rails stick to just below it, and the
     * scrim behind an open menu starts under it. A number repeated in four
     * places is a number that goes stale in three.
     */
    --appbar-h: 56px;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --bg: #131917;
        --surface: #1c2421;
        --surface-2: #252f2c;
        --border: #33403c;
        --text: #ebf0ee;
        --muted: #a0b1ac;
        --brand: #61dbb7;
        --brand-ink: #10171f;
        --brand-strong: #93e6ce;
        --brand-soft: #1e3831;
        --brand-line: #396055;
        --accent: #e6966b;
        --accent-soft: #38271e;
        --danger: #f08a84;
        --danger-bg: #3a1f1e;
        --ok: #79d1a0;
        --ok-bg: #16301f;
        --warn: #e3b768;
        --warn-bg: #332715;
        --shadow: 0 1px 2px rgba(0, 0, 0, .45);
        --shadow-lg: 0 18px 46px rgba(0, 0, 0, .55);
        --hero-from: #172b25;
        --hero-to: #101b18;
        --band-from: #196c53;
        --band-to: #104736;
        --band-ink: #ffffff;

        /* See the light block: only the mark's edge stroke moves. */
        --mark-edge: rgba(255, 255, 255, .22);
    }
}

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

html { -webkit-text-size-adjust: 100%; }

body {
    margin: 0;
    background: var(--bg);
    color: var(--text);
    font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans Devanagari", "Noto Sans", sans-serif;
}

a { color: var(--brand); text-decoration: none; }
a:hover { text-decoration: underline; }

img, video { max-width: 100%; height: auto; display: block; }

/* ----------------------------------------------------------------- icons
 *
 * Drawn inline by partials/icon.php, which sets geometry and nothing else:
 * every icon is a 24x24 line drawing in `currentColor`, so it takes the colour
 * of the text beside it and is sized from here rather than from the markup.
 */
.icon { width: 22px; height: 22px; flex: none; }
.icon--xs { width: 14px; height: 14px; }
.icon--sm { width: 18px; height: 18px; }
.icon--xl { width: 40px; height: 40px; }

/* ---------------------------------------------------------------- layout */

.wrap {
    max-width: var(--max);
    margin: 0 auto;
    /*
     * max() rather than a plain gutter so the column clears a notch in
     * landscape on a cutout phone, where the safe-area inset is the larger of
     * the two. Everywhere else the inset is zero and the gutter wins.
     *
     * The gutter grows with the window now that nothing caps the page: 16px
     * of air on a phone is a margin, 16px on a 27" monitor is a page whose
     * furniture hangs off the edge of the glass.
     */
    padding-inline:
        max(var(--gutter), env(safe-area-inset-left))
        max(var(--gutter), env(safe-area-inset-right));
}

/*
 * The gutter for the landing page's full-bleed bands. Same contract as
 * `.wrap`, but wider and with a gutter that grows with the viewport — a band
 * whose background runs to the edge of a 27" monitor needs more margin before
 * its text starts than a 16px card does.
 */
.shell {
    width: 100%;
    max-width: var(--shell-max);
    margin-inline: auto;
    padding-inline:
        max(var(--gutter), env(safe-area-inset-left))
        max(var(--gutter), env(safe-area-inset-right));
}

.main--bleed { display: block; }
.main--bleed > .shell { padding-top: 16px; }

.site-header {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 20;
}

/*
 * The header is a three-part grid rather than a flex row, because the three
 * parts want different behaviour when space runs out: the brand never shrinks,
 * the search box absorbs whatever is left, and the navigation is sized by its
 * content. A flex row with `flex-wrap` gave the search box the whole first line
 * and pushed every link to a second one at widths where all three still fit.
 */
.site-header .wrap {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    align-items: center;
    gap: 10px 20px;
    min-height: 60px;
}

/*
 * The brand lockup — tricolour mark plus wordmark. Markup: partials/brand.php.
 *
 * The mark is a square whose ink reaches all four edges, so its box and its
 * body are the same thing and plain `center` aligns it. The older bubble mark
 * needed a downward nudge to discount the tail hanging below its body; there
 * is no tail now, and keeping the nudge would just sit the square low.
 */
.brand {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    font-weight: 700;
    font-size: 1.2rem;
    letter-spacing: -.025em;
    color: var(--text);
    white-space: nowrap;
}
.brand:hover { text-decoration: none; }

.brand__mark {
    flex: none;
    display: block;
    overflow: visible;
}

/*
 * An uploaded logo in place of the drawn mark.
 *
 * Height is pinned and width is left to follow, which is what lets one upload
 * slot accept both a square mark and a wide horizontal logo without an
 * aspect-ratio setting to go with it. The `width: auto` is not redundant: the
 * <img> carries intrinsic width/height attributes so the header reserves the
 * right box before the image loads, and without this the attribute would win
 * and squash tall logos.
 */
.brand__logo {
    flex: none;
    display: block;
    height: 34px;
    width: auto;
    max-width: 220px;
    object-fit: contain;
}

.brand__word { color: inherit; }

/*
 * "do" — the "give" half of "give voice" — carries the brand colour, so the
 * wordmark reads as one solid word with a single coloured accent rather than
 * a gradient. It moves with --brand, so it recolours with the mark and with
 * every other themed element on the page instead of needing its own tuning
 * per admin palette.
 */
.brand__word-do { color: var(--brand); }

/*
 * The mark draws itself in, a beat at a time: frame, then the pin's eyelet,
 * then the signal origin, then each arc from the inside out.
 *
 * Pointer devices drive it from hover/focus and start at the signal dot,
 * because the frame is already the thing the cursor is resting on — redrawing
 * the border under the pointer reads as a glitch rather than a flourish.
 * Touch has no hover to drive anything, so there the whole mark builds itself
 * once on load, a second per step, and then holds. Once, not on a loop: this
 * mark sits in a sticky header on every page, and a permanent animation in
 * the corner of the eye is a cost paid on every page for a flourish nobody
 * asked to watch twice.
 *
 * Every step is one reveal keyframe plus a delay, with animation-fill-mode
 * backwards. Backwards fill is what holds a step hidden through its own delay
 * without any element ever being written as hidden in a base rule — so if the
 * animation is unsupported, disabled or simply never runs, the mark is whole
 * rather than half-drawn.
 *
 * transform-box: view-box, not fill-box: the signal parts scale about the
 * origin at 18.93 45.59 — the centre of the dot — and not about each arc's
 * own bounding box, or they would pop in place and the build would read as
 * three unrelated twitches instead of one signal radiating outward. view-box
 * makes transform-origin resolve against the SVG's coordinate system, which
 * is where those numbers come from.
 */
.brand__signal-arc,
.brand__signal-dot {
    transform-box: view-box;
    transform-origin: 18.93px 45.59px;
}

.brand__mark-panel,
.brand__mark-frame,
.brand__mark-edge {
    transform-box: view-box;
    transform-origin: 32px 32px;
}

.brand__mark-eyelet {
    transform-box: view-box;
    transform-origin: 8.86px 55.46px;
}

/*
 * Two reveals. The frame is the whole silhouette, so it only settles in — a
 * pop at that size is a lurch. The small parts overshoot slightly and come
 * back, which is what makes each one land rather than merely appear.
 */
@keyframes brand-build-frame {
    from { opacity: 0; transform: scale(.94); }
    to   { opacity: 1; transform: scale(1); }
}

@keyframes brand-build-part {
    from { opacity: 0; transform: scale(.35); }
    60%  { opacity: 1; transform: scale(1.1); }
    to   { opacity: 1; transform: scale(1); }
}

/*
 * Pointer: the signal builds on hover/focus, half a second per step, so the
 * three arcs arrive in lifecycle order — reported, in review, resolved.
 */
@media (hover: hover) {
    .brand:hover .brand__signal-dot,
    .brand:focus-visible .brand__signal-dot,
    .brand:hover .brand__signal-arc,
    .brand:focus-visible .brand__signal-arc {
        animation: brand-build-part .42s cubic-bezier(.34, 1.4, .64, 1) backwards;
    }

    .brand:hover .brand__signal-arc:nth-of-type(1),
    .brand:focus-visible .brand__signal-arc:nth-of-type(1) { animation-delay: .5s; }

    .brand:hover .brand__signal-arc:nth-of-type(2),
    .brand:focus-visible .brand__signal-arc:nth-of-type(2) { animation-delay: 1s; }

    .brand:hover .brand__signal-arc:nth-of-type(3),
    .brand:focus-visible .brand__signal-arc:nth-of-type(3) { animation-delay: 1.5s; }
}

/*
 * Touch: no pointer to trigger anything, so the mark introduces itself on
 * load instead — border, eyelet, origin, then the three arcs, a second apart.
 * It runs the once and the mark stays put afterwards.
 */
@media (hover: none) {
    .brand__mark-panel,
    .brand__mark-frame,
    .brand__mark-edge {
        animation: brand-build-frame .6s ease-out backwards;
    }

    .brand__mark-eyelet,
    .brand__signal-dot,
    .brand__signal-arc {
        animation: brand-build-part .5s cubic-bezier(.34, 1.4, .64, 1) backwards;
    }

    .brand__mark-eyelet { animation-delay: 1s; }
    .brand__signal-dot { animation-delay: 2s; }
    .brand__signal-arc:nth-of-type(1) { animation-delay: 3s; }
    .brand__signal-arc:nth-of-type(2) { animation-delay: 4s; }
    .brand__signal-arc:nth-of-type(3) { animation-delay: 5s; }
}

/*
 * Last, so it outranks both blocks above on equal specificity: no build at
 * all, just the finished mark.
 */
@media (prefers-reduced-motion: reduce) {
    .brand__mark-panel,
    .brand__mark-frame,
    .brand__mark-edge,
    .brand__mark-eyelet,
    .brand__signal-arc,
    .brand__signal-dot,
    .brand:hover .brand__signal-arc,
    .brand:focus-visible .brand__signal-arc,
    .brand:hover .brand__signal-dot,
    .brand:focus-visible .brand__signal-dot { animation: none; }
}

/* The "admin" pill. Sized off the wordmark so it tracks if the brand grows. */
.brand__tag {
    margin-left: 2px;
    padding: 2px 7px;
    border: 1px solid var(--border);
    border-radius: 999px;
    font-size: .66rem;
    font-weight: 700;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--muted);
}

/* --------------------------------------------------------------- app bar */

/*
 * The signed-in site's header: brand and search, the feed tabs, the viewer's
 * own controls. `.site-header` above is the plainer bar the auth layout still
 * wears — a sign-in page has no feeds to switch between and nobody to carry
 * controls for, so it gets a brand and nothing else.
 */
.appbar {
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    z-index: 30;
}

.appbar__inner {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
    align-items: center;
    gap: 8px;
    min-height: var(--appbar-h);
}

.appbar__lead { display: flex; align-items: center; gap: 10px; min-width: 0; }
.appbar__trail { display: flex; align-items: center; justify-content: flex-end; gap: 6px; }

/*
 * The wordmark is the first thing to go when the row gets tight. The mark on
 * its own is still the brand; the tabs in the middle are what the page is for.
 */
@media (max-width: 1180px) {
    .brand--app .brand__word { display: none; }
}

/* ------------------------------------------------------------ app search */

.searchbar { position: relative; flex: 1 1 180px; min-width: 0; max-width: 260px; }

.searchbar__icon {
    position: absolute;
    inset-block: 0;
    left: 12px;
    display: flex;
    align-items: center;
    color: var(--muted);
    /* The glyph is scenery: clicking it must land in the field behind it. */
    pointer-events: none;
}
.searchbar__icon .icon { width: 18px; height: 18px; }

/*
 * Matched on the type as well as the class, and not just for tidiness: the
 * shared `input[type="search"], select, textarea { padding: 9px 11px }` rule
 * further down this file has the same specificity as a bare `.searchbar input`
 * and comes after it, so it would win and put the placeholder underneath the
 * magnifier. Naming the type here settles it at (0,2,1).
 */
.searchbar input[type="search"] {
    width: 100%;
    background: var(--surface-2);
    border-color: transparent;
    border-radius: 999px;
    padding-block: 8px;
    padding-inline: 38px 14px;
}
.searchbar input[type="search"]:focus { background: var(--surface); border-color: var(--brand-line); }

/* -------------------------------------------------------------- app tabs */

.apptabs { display: flex; align-items: stretch; gap: 2px; min-width: 0; }

/*
 * A tab is a hover pill that becomes an underline when it is the page you are
 * on — the rule sits on the bottom edge of the bar, so the strip reads as four
 * doors in one wall rather than four separate buttons.
 */
.apptab {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding-inline: 18px;
    min-height: var(--appbar-h);
    color: var(--muted);
    border-bottom: 3px solid transparent;
    white-space: nowrap;
}
.apptab:hover { color: var(--text); background: var(--surface-2); text-decoration: none; }
.apptab[aria-current="page"] { color: var(--brand); border-bottom-color: var(--brand); background: none; }
.apptab__label { font-size: .9rem; font-weight: 600; }

/* Labels go before the bar starts squeezing the search box out of shape. */
@media (max-width: 1240px) {
    .apptab { padding-inline: 14px; }
    .apptab__label { position: absolute; left: -9999px; }
}

/* ---------------------------------------------------------- app controls */

.appbar__cta {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 38px;
    padding-inline: 14px;
    border-radius: 999px;
    background: var(--brand);
    color: var(--brand-ink);
    font-size: .9rem;
    font-weight: 600;
    white-space: nowrap;
}
.appbar__cta:hover { background: var(--brand-strong); text-decoration: none; }
.appbar__cta .icon { width: 18px; height: 18px; }

/*
 * A round control with an icon in it: alerts, and the report button once the
 * screen is too narrow for its label.
 */
.iconbtn {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 38px;
    height: 38px;
    flex: none;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--text);
}
.iconbtn:hover { background: var(--brand-soft); color: var(--brand); text-decoration: none; }

.iconbtn__badge {
    position: absolute;
    top: -1px;
    right: -1px;
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 19px;
    height: 19px;
    padding-inline: 5px;
    border-radius: 999px;
    border: 2px solid var(--surface);
    background: var(--danger);
    color: #fff;
    font-size: .7rem;
    font-weight: 700;
    line-height: 1;
}

/*
 * The report button sheds its label before it sheds itself: a round "+" is
 * still legible as "add something", and the two steps buy the search box room
 * at the widths where it is being squeezed.
 */
@media (max-width: 760px) {
    .appbar__cta { width: 38px; padding-inline: 0; justify-content: center; }
    .appbar__cta-label { position: absolute; left: -9999px; }
}

@media (max-width: 560px) {
    /* The composer card at the head of the feed is the route to the same page,
       so on a 360px row this button is the one control here with a duplicate
       elsewhere on the screen — which makes it the one to drop. */
    .appbar__cta { display: none; }
}

/* ------------------------------------------------------------ menus
 *
 * Two dropdowns, one shape: the account menu in the app bar and the overflow
 * menu on a post. Both are <details>, so they open with no JavaScript; app.js
 * only adds Escape-to-close and click-away.
 */

.umenu, .pmenu { position: relative; }
.umenu form, .pmenu form { margin: 0; }

.umenu__trigger, .pmenu__trigger {
    display: flex;
    align-items: center;
    cursor: pointer;
    list-style: none;
}
.umenu__trigger::-webkit-details-marker,
.pmenu__trigger::-webkit-details-marker { display: none; }

.umenu__trigger {
    gap: 2px;
    padding: 4px 6px 4px 4px;
    border-radius: 999px;
    background: var(--surface-2);
}
.umenu__trigger:hover { background: var(--brand-soft); }
.umenu__caret { width: 16px; height: 16px; color: var(--muted); }

.pmenu__trigger {
    justify-content: center;
    width: 34px;
    height: 34px;
    border-radius: 50%;
    color: var(--muted);
}
.pmenu__trigger:hover { background: var(--surface-2); color: var(--text); }

.umenu__panel, .pmenu__panel {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    z-index: 40;
    padding: 6px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-lg);
}
.umenu__panel { width: 260px; }
.pmenu__panel { width: 220px; }

.umenu__item, .pmenu__item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 9px 8px;
    border: 0;
    border-radius: 8px;
    background: none;
    font: inherit;
    font-size: .93rem;
    font-weight: 500;
    color: var(--text);
    text-align: left;
    cursor: pointer;
}
.umenu__item:hover, .pmenu__item:hover { background: var(--surface-2); text-decoration: none; }
.umenu__item .icon, .pmenu__item .icon { width: 20px; height: 20px; color: var(--muted); }

.pmenu__item--danger, .pmenu__item--danger .icon { color: var(--danger); }

.umenu__me { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: 8px; color: var(--text); }
.umenu__me:hover { background: var(--surface-2); text-decoration: none; }
.umenu__who { display: flex; flex-direction: column; min-width: 0; }
.umenu__sep { height: 1px; background: var(--border); margin: 6px 4px; }

.umenu__count, .rail__badge {
    margin-left: auto;
    padding: 1px 7px;
    border-radius: 999px;
    background: var(--danger);
    color: #fff;
    font-size: .72rem;
    font-weight: 700;
}

/*
 * Below 900px the bar keeps its top row and drops the tabs onto a second one,
 * where they scroll sideways as a strip.
 *
 * Wrapping them instead was the obvious alternative and it is worse: on a
 * 360px screen the signed-in header wraps to three ragged rows and the page
 * content starts below the fold. A strip keeps the bar one predictable height,
 * and `scroll-snap` stops it coming to rest mid-tab.
 */
@media (max-width: 900px) {
    /*
     * The first track is `minmax(0, 1fr)`, not `auto`. An `auto` track is
     * allowed to grow to its content's max-content width, and the brand plus a
     * search field asked for more than a 360px screen has — which widened the
     * whole page, not just the bar, because a grid track is not a scroll
     * container. `minmax(0, 1fr)` lets it shrink instead, and the field inside
     * it already carries `min-width: 0` to go along with that.
     */
    .appbar__inner {
        grid-template-columns: minmax(0, 1fr) auto;
        gap: 8px;
        padding-block: 8px 0;
    }

    .appbar__lead { grid-column: 1; }
    .appbar__trail { grid-column: 2; }
    .searchbar { max-width: none; }

    .apptabs {
        grid-column: 1 / -1;
        grid-row: 2;
        overflow-x: auto;
        scrollbar-width: none;
        scroll-snap-type: x proximity;
        /* Cancel the gutter so the strip scrolls edge to edge, then restore it
           as padding — otherwise the first and last tabs are cut off. */
        margin-inline: calc(-1 * max(16px, env(safe-area-inset-left)));
        padding-inline: max(16px, env(safe-area-inset-left));
    }
    .apptabs::-webkit-scrollbar { display: none; }

    .apptab {
        flex: 1 0 auto;
        scroll-snap-align: start;
        min-height: 46px;
    }
}

.layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 24px; padding: 24px 0 64px; }
.layout--single { grid-template-columns: minmax(0, 1fr); max-width: 760px; margin-inline: auto; }

/*
 * A single-purpose form page — sign in, reset a password. Narrower than the
 * reading column: a form this short centred in a 760px well reads as an
 * unfinished page. Replaces an inline style the CSP discards.
 */
.layout--form { max-width: 520px; }

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

/* ------------------------------------------------------------ feed shell */

/*
 * The feed is three columns: navigation, the posts, and material about the
 * feed being read. The rails are fixed and the shell runs edge to edge, so
 * the window is what sets the width of the posts: the middle track takes
 * whatever the rails leave, and widening the window widens the column.
 */
.feed-shell {
    display: grid;
    grid-template-columns: 250px minmax(0, 1fr) 300px;
    gap: 24px;
    align-items: start;
    padding: 20px 0 64px;
}

.feed-col { min-width: 0; width: 100%; }

/*
 * Both rails stay put while the column scrolls. They are short and the feed is
 * not, so scrolling them away means a reader who is four screens into the feed
 * has to scroll back up to change anything.
 */
.rail, .aside {
    position: sticky;
    top: calc(var(--appbar-h) + 16px);
    max-height: calc(100vh - var(--appbar-h) - 32px);
    overflow-y: auto;
    scrollbar-width: thin;
}

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

.rail__row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 9px 10px;
    border-radius: 8px;
    color: var(--text);
    font-size: .94rem;
    font-weight: 500;
}
.rail__row:hover { background: var(--surface-2); text-decoration: none; }
.rail__row .icon { width: 22px; height: 22px; color: var(--muted); }
.rail__row[aria-current="page"] { background: var(--brand-soft); color: var(--brand); font-weight: 600; }
.rail__row[aria-current="page"] .icon { color: var(--brand); }
.rail__row--me { font-weight: 600; }

.rail__sep { height: 1px; background: var(--border); margin: 8px 10px; }

.rail__fineprint {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 10px;
    margin-top: 16px;
    padding-inline: 10px;
    color: var(--muted);
    font-size: .78rem;
}
.rail__fineprint a { color: var(--muted); }

.aside { display: flex; flex-direction: column; gap: 12px; }
.aside .card { margin-bottom: 0; }
.aside__cta { display: flex; flex-direction: column; gap: 10px; }
.aside__cta p { margin: 2px 0 0; }
.aside__place { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }

.aside__title {
    margin-bottom: 10px;
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}

.steps { display: grid; gap: 6px; margin: 0 0 10px; padding-left: 18px; color: var(--muted); font-size: .87rem; }
.steps span { color: var(--text); font-weight: 600; }

/*
 * The navigation rail goes before the supporting one does: every row in it is
 * also in the app bar or the account menu, so losing it costs nothing, while
 * the right-hand rail carries material that exists nowhere else.
 */
@media (max-width: 1080px) {
    .feed-shell { grid-template-columns: minmax(0, 1fr) 300px; }
    .rail { display: none; }
}

@media (max-width: 900px) {
    .feed-shell { grid-template-columns: minmax(0, 1fr); gap: 16px; }

    /*
     * The supporting rail follows the posts on a phone rather than leading
     * them. Above them, three cards of context would mean the first screen of
     * the feed contains no feed.
     */
    .aside { order: 1; position: static; max-height: none; overflow: visible; }
}

/* ----------------------------------------------------------------- cards */

.card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    padding: 18px;
    margin-bottom: 16px;
}

.card--tight { padding: 14px; }

h1 { font-size: 1.5rem; margin: 0 0 4px; letter-spacing: -.02em; }
h2 { font-size: 1.15rem; margin: 0 0 10px; }
h3 { font-size: 1rem; margin: 0 0 8px; }
.lede { color: var(--muted); margin: 0 0 18px; }

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

/*
 * The "start a report" card. It is a row of links dressed as a composer — see
 * partials/composer.php for why it is not a form.
 */
.composer__top { display: flex; align-items: center; gap: 10px; }

.composer__prompt {
    flex: 1;
    min-width: 0;
    padding: 11px 16px;
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--muted);
    font-size: .97rem;
}
.composer__prompt:hover { background: var(--brand-soft); color: var(--brand); text-decoration: none; }

.composer__sep { height: 1px; background: var(--border); margin: 12px 0 6px; }
.composer__actions { display: flex; gap: 4px; }

.composer__action {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 8px;
    border-radius: 8px;
    color: var(--muted);
    font-size: .88rem;
    font-weight: 600;
}
.composer__action:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.composer__action .icon { width: 20px; height: 20px; }

.composer--guest { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
.composer--guest p { margin: 2px 0 0; }

@media (max-width: 480px) {
    /* Three labelled buttons across a 360px card leaves each of them about
       eight characters. The icons carry it from here. */
    .composer__action span { position: absolute; left: -9999px; }
}

/* ---------------------------------------------------------- feed heading */

.feed-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
    flex-wrap: wrap;
    margin: 4px 2px 12px;
}
.feed-head__title { margin: 0; font-size: 1.05rem; }
.feed-head__crumbs { margin: 0; color: var(--muted); font-size: .85rem; }

/*
 * A one-line strip of what is being reported right now. Sideways is the right
 * direction here and nowhere else in the column: nothing in it needs to be
 * read in order.
 */
.tagstrip {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
    padding-bottom: 2px;
    overflow-x: auto;
    scrollbar-width: none;
    scroll-snap-type: x proximity;
}
.tagstrip::-webkit-scrollbar { display: none; }

.tagstrip__item {
    flex: none;
    scroll-snap-align: start;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 14px 6px 6px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface);
    box-shadow: var(--shadow);
    color: var(--text);
    font-size: .88rem;
    font-weight: 600;
}
.tagstrip__item:hover { border-color: var(--brand); color: var(--brand); text-decoration: none; }

.tagstrip__glyph {
    display: grid;
    place-items: center;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: var(--brand-soft);
    color: var(--brand);
}
.tagstrip__glyph .icon { width: 16px; height: 16px; }
.tagstrip__count { color: var(--muted); font-weight: 500; font-size: .8rem; }

/*
 * A switch between mutually exclusive views of the same list — the ranking
 * periods on Top. Distinct from `.tabs`, which separates pages; this one lives
 * inside a page and looks like the control it is.
 */
.segmented {
    display: inline-flex;
    gap: 2px;
    margin-bottom: 16px;
    padding: 3px;
    border-radius: 999px;
    background: var(--surface-2);
}
.segmented a { padding: 6px 14px; border-radius: 999px; color: var(--muted); font-size: .87rem; font-weight: 600; }
.segmented a:hover { color: var(--text); text-decoration: none; }
.segmented a[aria-current="page"] { background: var(--surface); color: var(--brand); box-shadow: var(--shadow); }

.feed-pager { display: flex; justify-content: center; gap: 8px; padding-top: 8px; }

/* ------------------------------------------------------------- user text */

/*
 * Anything a person typed, rendered back: a post, a comment, a bio, the claim
 * on a verification request. `overflow-wrap: anywhere` is load-bearing — an
 * unbroken 400-character "word" is a legal thing to type and must not be
 * allowed to widen the column it sits in.
 */
.usertext { margin: 0; white-space: normal; overflow-wrap: anywhere; }
.usertext a.mention { font-weight: 600; }
.usertext a.hashtag { color: var(--accent); }

/* The small muted line under a name: a time, a handle, a place. */
.metaline { color: var(--muted); font-size: .84rem; }

.avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; background: var(--border); flex: none; }
.avatar--sm { width: 28px; height: 28px; }
.avatar--lg { width: 96px; height: 96px; }

/* The tick beside a verified name. It is a colour and a shape, so it also
   carries a visually hidden label — see partials/post_card.php. */
.verified-tick { display: inline-flex; color: var(--brand); }
.verified-tick .icon { width: 16px; height: 16px; }

/* ------------------------------------------------------------------ post */

/*
 * The card sets no padding of its own: each band inside brings its own, so the
 * media can run to the card's edges the way a photo does in any feed. A 16px
 * margin around an image is a frame around a frame.
 *
 * Deliberately NOT `overflow: hidden`. The overflow menu in the header is an
 * absolutely positioned panel, and clipping it to the card would cut it off at
 * the first item.
 */
.post { padding: 0; }

.post__head { display: flex; align-items: flex-start; gap: 10px; padding: 14px 16px 10px; }
.post__avatar { flex: none; display: block; }
.post__meta { flex: 1; min-width: 0; }

.post__author { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; font-weight: 600; line-height: 1.35; }
.post__author a { color: var(--text); }
.post__author a:hover { text-decoration: underline; }

.post__sub { display: flex; align-items: center; gap: 4px; flex-wrap: wrap; color: var(--muted); font-size: .82rem; }
.post__sub a { color: var(--muted); }
.post__sub a:hover { color: var(--text); }
.post__sub .icon { color: var(--muted); }
.post__dot { color: var(--muted); }

/* Stated, not shouted. It belongs to the timestamp line because it is a fact
   about when, and it carries the exact time in its tooltip for anyone who
   needs to know whether the text changed before or after they replied. */
.post__edited { cursor: help; }

.pmenu { flex: none; margin-left: auto; }

.post__body { padding: 0 16px 12px; }

/* ----------------------------------------------------------- post media */

/*
 * The tiles are separated by the grid's own gap showing the card's border
 * colour through it, so the hairlines between photos cost no extra element.
 * How many there are picks the shape: one fills the width at its own ratio,
 * two sit side by side, three put the first across the top, four make a
 * square. Beyond four the last tile carries a "+N" — see post_card.php.
 */
.post__media {
    display: grid;
    gap: 2px;
    background: var(--border);
    border-block: 1px solid var(--border);
}

.post__media--1 { grid-template-columns: 1fr; }
.post__media--2,
.post__media--3,
.post__media--4 { grid-template-columns: 1fr 1fr; }
.post__media--3 > :first-child { grid-column: 1 / -1; }

.post__tile { position: relative; display: block; background: var(--surface-2); }
.post__tile img, .post__tile video { width: 100%; display: block; }

/* A lone photo keeps its own proportions, capped so a tall one cannot take
   over the column. Every other arrangement is a tiled grid, which only works
   if the tiles agree on a shape. */
.post__media--1 .post__tile img,
.post__media--1 .post__tile video { max-height: 640px; object-fit: contain; }

.post__media:not(.post__media--1) .post__tile { aspect-ratio: 1; }
.post__media:not(.post__media--1) .post__tile img,
.post__media:not(.post__media--1) .post__tile video { height: 100%; object-fit: cover; }
.post__media--3 > .post__tile:first-child { aspect-ratio: 16 / 9; }

.post__more {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    /* A scrim, not a themed surface: it has to darken whatever photo is under
       it, in either colour scheme. */
    background: rgba(0, 0, 0, .5);
    color: #fff;
    font-size: 1.5rem;
    font-weight: 700;
}

/* -------------------------------------------------- post counts and bar */

.post__counts {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    color: var(--muted);
    font-size: .85rem;
}
.post__counts a { color: var(--muted); }
.post__counts a:hover { color: var(--text); }
.post__counts-end { display: flex; gap: 12px; margin-left: auto; }

.post__likes { display: flex; align-items: center; gap: 6px; }

/*
 * Both of these set their own `display`, which beats the browser's [hidden]
 * rule, so they have to be told again. The card renders the counts row even
 * when every count is zero and hides it, so app.js can bring the like tally
 * back after a like without building the strip from scratch; get this wrong
 * and every untouched post carries an empty 40px strip with a lone brand-
 * coloured dot in it.
 */
.post__counts[hidden], .post__likes[hidden] { display: none; }
.post__likes-dot {
    display: grid;
    place-items: center;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--brand);
    color: var(--brand-ink);
}
.post__likes-dot .icon { width: 12px; height: 12px; }

.post__bar { display: flex; gap: 4px; padding: 4px 8px 8px; border-top: 1px solid var(--border); }
.post__bar form { flex: 1; display: flex; margin: 0; }

.post__act {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 9px 8px;
    border: 0;
    border-radius: 8px;
    background: none;
    font: inherit;
    font-size: .9rem;
    font-weight: 600;
    color: var(--muted);
    cursor: pointer;
}
.post__act:hover { background: var(--surface-2); color: var(--text); text-decoration: none; }
.post__act .icon { width: 20px; height: 20px; }
.post__act[aria-pressed="true"] { color: var(--brand); }

/*
 * Why a press did not take, written into the card by app.js. There is no page
 * reload on the fetch() path, so there is no flash message to carry it — and
 * a like that rolls itself back without a word reads as a broken button.
 */
.post__warn { margin: 0; padding: 0 16px 10px; color: var(--danger); font-size: .85rem; }

/* --------------------------------------------------------------- comment */

.comment { padding: 8px 0; }
.comment__body { padding: 10px 14px; border-radius: 14px; background: var(--surface-2); }
.comment__body .usertext { margin-top: 2px; }

/* ---------------------------------------------------------- profile head */

/*
 * Cover, then the avatar straddling its lower edge, then the name. The card
 * clips its own corners here — unlike a post card there is no menu inside it
 * to cut off, and the cover photo has to be trimmed to the card's radius.
 */
.profile-head { padding: 0 0 18px; overflow: hidden; }
.profile-head > p, .profile-head__actions { padding-inline: 20px; }

.profile-head__cover {
    width: 100%;
    height: clamp(120px, 22vw, 220px);
    object-fit: cover;
    background: var(--surface-2);
}

.profile-head__id { display: flex; align-items: flex-end; gap: 16px; padding: 16px 20px 0; }
.profile-head--covered .profile-head__id { margin-top: -36px; }

/* The ring is what lifts the avatar off the photo behind it. */
.profile-head--covered .avatar--lg { border: 4px solid var(--surface); }

.profile-head__names { min-width: 0; padding-bottom: 4px; }
.profile-head__names h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0; }
.profile-head__names .metaline { margin: 2px 0 0; }
.profile-head__names .verified-tick .icon { width: 20px; height: 20px; }

.profile-head__actions { display: flex; flex-wrap: wrap; gap: 8px; }

@media (max-width: 560px) {
    .profile-head__id { flex-direction: column; align-items: flex-start; gap: 10px; }
    .avatar--lg { width: 76px; height: 76px; }
}

/* ---------------------------------------------------------------- badges */

.badge {
    display: inline-block;
    font-size: .74rem;
    font-weight: 600;
    padding: 2px 8px;
    border-radius: 999px;
    vertical-align: middle;
}
.badge--resolved { background: var(--ok-bg); color: var(--ok); }
.badge--review { background: var(--warn-bg); color: var(--warn); }
.badge--verified { background: var(--brand); color: var(--brand-ink); }
.badge--danger { background: var(--danger-bg); color: var(--danger); }

/* ---------------------------------------------------------------- inputs */

label { display: block; font-weight: 600; font-size: .9rem; margin-bottom: 4px; }
.field { margin-bottom: 16px; }
.hint { color: var(--muted); font-size: .84rem; margin-top: 4px; }
.hint--error { color: var(--danger); font-weight: 600; }

/* intl-tel-input: make the wrapper behave like the plain input it replaces. */
.iti { width: 100%; display: block; }
.iti__search-input { width: 100%; padding: 8px 10px; }

input[type="text"], input[type="email"], input[type="password"], input[type="date"],
input[type="search"], input[type="number"], input[type="url"], input[type="tel"],
select, textarea {
    width: 100%;
    padding: 9px 11px;
    font: inherit;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
}

input:focus-visible, select:focus-visible, textarea:focus-visible, .btn:focus-visible, a:focus-visible {
    outline: 2px solid var(--brand);
    outline-offset: 2px;
}

textarea { min-height: 120px; resize: vertical; }

/* Show/hide password. The wrapper is added by app.js; without JavaScript the
   input renders exactly as it did before. */
.pw-wrap { position: relative; display: block; }
.pw-wrap input { padding-right: 44px; }

.pw-toggle {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    padding: 0;
    border: 0;
    background: none;
    color: var(--muted);
    cursor: pointer;
    border-radius: 6px;
}

.pw-toggle:hover { color: var(--text); }
.pw-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
.pw-toggle .is-hidden { display: none; }

.checkline { display: flex; gap: 8px; align-items: flex-start; font-weight: 400; }
.checkline input { margin-top: 4px; flex: none; }

/* --------------------------------------------------------------- switch
   A real checkbox, moved off-screen rather than hidden with display:none so
   it keeps its place in the tab order and still announces itself as a
   checkbox. The track and knob are drawn from the :checked state of that
   input, so the control cannot show one thing while the form holds another. */

.switch-row {
    display: flex;
    flex-wrap: wrap;
    gap: 12px 16px;
    align-items: flex-start;
    justify-content: space-between;
    margin: 16px 0 8px;
    padding: 14px 16px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg);
}

.switch { display: flex; gap: 12px; align-items: flex-start; flex: 1 1 320px; cursor: pointer; }
.switch input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.switch__text { display: flex; flex-direction: column; gap: 2px; }
.switch__text strong { font-weight: 600; }

.switch__track {
    position: relative;
    flex: none;
    width: 44px;
    height: 26px;
    margin-top: 1px;
    border-radius: 999px;
    background: #b9c0c9;
    transition: background .15s ease-in-out;
}

.switch__track::after {
    content: "";
    position: absolute;
    top: 3px;
    left: 3px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: #fff;
    box-shadow: 0 1px 2px rgba(16, 32, 48, .3);
    transition: transform .15s ease-in-out;
}

.switch input:checked + .switch__track { background: var(--brand); }
.switch input:checked + .switch__track::after { transform: translateX(18px); }
.switch input:focus-visible + .switch__track { outline: 2px solid var(--brand); outline-offset: 2px; }

/* Reduced motion: the state still changes, it just stops sliding. */
@media (prefers-reduced-motion: reduce) {
    .switch__track,
    .switch__track::after { transition: none; }
}

/* ---------------------------------------------------------------- modal
   A native <dialog>, so the browser handles the focus trap, Escape, inert
   background and the top layer — all of the parts a hand-rolled overlay gets
   subtly wrong. Only the look is ours. */

dialog.modal {
    width: min(460px, calc(100vw - 32px));
    padding: 20px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--text);
    box-shadow: 0 10px 40px rgba(16, 32, 48, .22);
}

dialog.modal::backdrop { background: rgba(16, 32, 48, .45); }
dialog.modal h3 { margin: 0 0 8px; font-size: 1.1rem; }
dialog.modal .field { margin-top: 14px; }

.modal__actions {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 18px;
}

/* --------------------------------------------------------------- buttons */

.btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 8px 14px;
    font: inherit;
    font-size: .92rem;
    font-weight: 600;
    border: 1px solid var(--border);
    border-radius: 8px;
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    text-decoration: none;
}
.btn:hover { border-color: var(--muted); text-decoration: none; }
.btn--primary { background: var(--brand); border-color: var(--brand); color: var(--brand-ink); }
.btn--danger { background: var(--danger); border-color: var(--danger); color: #fff; }
.btn--ghost { background: transparent; border-color: transparent; color: var(--muted); }
.btn--sm { padding: 5px 10px; font-size: .85rem; }
/*
 * A button whose entire label is its icon, so the padding is square and the
 * name lives in aria-label and title instead. It comes after .btn--sm on
 * purpose: the two are used together and this one owns the padding.
 *
 * The icon draws in currentColor, which means it inherits whatever the button
 * variant around it sets — including the header's lighter ghost colour and
 * its white hover — rather than needing a rule of its own per context.
 */
.btn--icon { padding: 6px; gap: 0; line-height: 0; }
.btn--icon svg { display: block; }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
.btn[aria-pressed="true"] { border-color: var(--brand); color: var(--brand); }

/* --------------------------------------------------------------- notices */

.notice { padding: 11px 14px; border-radius: 8px; margin-bottom: 16px; font-size: .93rem; border: 1px solid transparent; }
.notice--ok { background: var(--ok-bg); color: var(--ok); border-color: currentColor; }
.notice--error { background: var(--danger-bg); color: var(--danger); border-color: currentColor; }
.notice--warn { background: var(--warn-bg); color: var(--warn); border-color: currentColor; }
.notice ul { margin: 6px 0 0; padding-left: 18px; }

/* --------------------------------------------------------------- tables */

.table { width: 100%; border-collapse: collapse; font-size: .9rem; }
.table th, .table td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }
.table th { color: var(--muted); font-weight: 600; white-space: nowrap; }
.table code { font-size: .85em; overflow-wrap: anywhere; }
.table-scroll { overflow-x: auto; }

/* ---------------------------------------------------------------- misc */

.tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
.tabs a { padding: 8px 12px; color: var(--muted); border-bottom: 2px solid transparent; }
.tabs a[aria-current="page"] { color: var(--text); border-bottom-color: var(--brand); font-weight: 600; }

.chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip { padding: 4px 10px; border: 1px solid var(--border); border-radius: 999px; font-size: .85rem; color: var(--muted); }
.chip:hover { color: var(--text); text-decoration: none; }

.stat-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 20px; }
.stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 14px; }
.stat b { display: block; font-size: 1.6rem; line-height: 1.1; letter-spacing: -.02em; }
.stat span { color: var(--muted); font-size: .85rem; }

.empty { text-align: center; color: var(--muted); padding: 36px 16px; }
.empty .icon--xl { display: block; margin: 0 auto 10px; color: var(--border); }
.empty p { margin: 0 0 14px; }

.codes { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; margin: 14px 0; }
.codes code { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px 10px; text-align: center; letter-spacing: .06em; }

/*
 * QR code (two-factor enrolment).
 *
 * The symbol itself is always black on white, whatever the theme — scanners
 * expect that polarity and a fair number refuse an inverted one. The white
 * padding below is the spec's quiet zone carried into the frame, so the code
 * still reads when the surrounding card is dark.
 */
.qr-frame {
    display: inline-block;
    background: #ffffff;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 12px;
    line-height: 0;
    margin: 14px 0;
}
.qr { display: block; width: 100%; max-width: 260px; height: auto; }

.break { overflow-wrap: anywhere; }

.inline-form { display: inline; }
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.row > * { flex: 1 1 180px; }
.muted { color: var(--muted); }
.small { font-size: .85rem; }
.mt { margin-top: 16px; }

.site-footer { border-top: 1px solid var(--border); background: var(--surface); padding: 20px 0; margin-top: 40px; }
.site-footer .wrap { display: flex; gap: 16px; flex-wrap: wrap; color: var(--muted); font-size: .86rem; }

.skip-link {
    position: absolute;
    left: -9999px;
}
.skip-link:focus {
    left: 16px;
    top: 8px;
    z-index: 100;
    background: var(--surface);
    padding: 8px 12px;
    border-radius: 6px;
    border: 1px solid var(--border);
}


/* ------------------------------------------------------- admin console */

/*
 * The console is visually distinct from the public site on purpose. An
 * operator should never be a glance away from mistaking one for the other:
 * the header is dark, the brand carries an "admin" tag, and the navigation is
 * a sidebar rather than the public site's horizontal bar. That difference is
 * the cheapest safeguard against acting on the wrong surface.
 *
 * Unlike the public site the console runs close to the full width of the
 * screen. The public feed is a column of prose and wants a reading measure;
 * the console is tables of six and eight columns, and pinning those to 880px
 * inside a 1280px shell left an operator scrolling a table sideways on a
 * monitor with 700px of empty page beside it. Width here is not decoration,
 * it is how many rows of the audit log fit on the screen at once.
 *
 * Prose inside the console is capped separately (.prose and the rules below
 * it), because a paragraph of explanation 1600px wide is genuinely harder to
 * read than a narrow one.
 */

.admin-body {
    /* Slightly cooler than the public --bg so the two never look alike. */
    background: var(--admin-bg, #eef1f5);
    min-height: 100vh;
    min-height: 100dvh;
    display: flex;
    flex-direction: column;

    /*
     * One gutter for the header, the shell and the footer, so the brand, the
     * page title and the footer note all sit on the same vertical line at
     * every width. It shrinks on a phone, where the pixels are worth more to
     * a table than to a margin.
     */
    --admin-max: 1680px;
    --admin-gutter: clamp(14px, 2.2vw, 32px);
    --admin-header-h: 56px;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .admin-body { --admin-bg: #0e1217; }
}

/*
 * The shared width-and-gutter contract. The max() is what keeps the console
 * clear of a notch: in landscape on a cutout phone the safe-area inset is
 * larger than the gutter and wins, and everywhere else it is zero and the
 * gutter does.
 */
.admin-header__inner,
.admin-shell,
.admin-footer__inner {
    width: 100%;
    max-width: var(--admin-max);
    margin-inline: auto;
    padding-inline:
        max(var(--admin-gutter), env(safe-area-inset-left))
        max(var(--admin-gutter), env(safe-area-inset-right));
}

.admin-header {
    background: #16202c;
    color: #e8edf2;
    border-bottom: 3px solid var(--brand);
    position: sticky;
    top: 0;
    z-index: 30;
}

.admin-header__inner {
    min-height: var(--admin-header-h);
    display: flex;
    align-items: center;
    gap: 12px;
    /*
     * Kept from the original. The three items fit on one line down to about
     * 320px, but a long role title or a translated "Sign out" should push the
     * name onto a second line rather than off the edge of the screen.
     */
    flex-wrap: wrap;
}

/*
 * The console wears the same lockup as the public site, on a bar that is dark
 * in both themes — so --mark-edge is re-declared rather than overridden per
 * property, which keeps the mark reading from one source. The wordmark needs
 * no override: color: #fff below covers "awaj", and --brand already reads
 * against #16202c at full strength — that's the same border-bottom colour
 * three lines below in .admin-header.
 */
.admin-header .brand {
    --mark-edge: rgba(255, 255, 255, .3);
    font-size: 1.1rem;
    letter-spacing: -.02em;
    color: #fff;
    cursor: default;
}

.admin-header .brand .brand__tag {
    border-color: rgba(255, 255, 255, .28);
    color: #cfd9e3;
}

.admin-whoami {
    margin-left: auto;
    text-align: right;
    line-height: 1.25;
    min-width: 0;
}
.admin-whoami__name {
    display: block;
    font-size: .9rem;
    font-weight: 600;
    color: #fff;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.admin-whoami__role { display: block; font-size: .76rem; color: #9aa8b6; }

.admin-header .btn--ghost { color: #cfd9e3; border-color: rgba(255, 255, 255, .28); }
.admin-header .btn--ghost:hover { color: #fff; border-color: #fff; }
.admin-header .btn:focus-visible { outline-color: #fff; }

/*
 * The name is the first thing to go when space runs short: the operator knows
 * who they are, whereas the role is the part that decides what the console
 * will let them do, and it stays.
 */
@media (max-width: 420px) {
    .admin-whoami__name { display: none; }
    .admin-whoami__role { font-size: .82rem; }
}

.admin-shell {
    flex: 1;
    display: grid;
    grid-template-columns: 232px minmax(0, 1fr);
    gap: 32px;
    align-items: start;
}

/* ------------------------------------------------------ console sidebar */

.admin-nav-wrap {
    position: sticky;
    top: calc(var(--admin-header-h) + 3px);
    /* Its own scrollbar, so a menu longer than the viewport is still reachable. */
    max-height: calc(100vh - var(--admin-header-h) - 3px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 24px 0 32px;
}

/*
 * The drawer toggle is a <summary>, so opening and closing the menu on a
 * phone needs no JavaScript at all — the same rule that governs everything
 * else here. On a desktop it is hidden and the panel is forced open, which is
 * what the two override blocks further down are for.
 */
.admin-nav-toggle {
    display: none;
    align-items: center;
    gap: 8px;
    padding: 11px 14px;
    margin: 12px 0 0;
    border: 1px solid var(--border);
    border-radius: 10px;
    background: var(--surface);
    font-weight: 600;
    font-size: .95rem;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.admin-nav-toggle::-webkit-details-marker { display: none; }
.admin-nav-toggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* A chevron that turns, drawn in CSS so there is no icon file to fetch. */
.admin-nav-toggle::after {
    content: "";
    width: 8px;
    height: 8px;
    margin-left: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: rotate(45deg) translateY(-2px);
    transition: transform .15s ease;
}
.admin-nav-wrap[open] > .admin-nav-toggle::after { transform: rotate(-135deg) translateY(-2px); }
.admin-nav-wrap[open] > .admin-nav-toggle {
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
}

.admin-nav__group {
    margin: 20px 0 6px;
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--muted);
}
.admin-nav__group:first-child { margin-top: 0; }

.admin-nav__list { list-style: none; margin: 0; padding: 0; }
.admin-nav__list a {
    display: flex;
    align-items: center;
    min-height: 38px;
    padding: 7px 12px;
    border-radius: 8px;
    color: var(--text);
    font-size: .92rem;
    border-left: 3px solid transparent;
}
.admin-nav__list a:hover { background: var(--surface); text-decoration: none; }
.admin-nav__list a[aria-current="page"] {
    background: var(--surface);
    border-left-color: var(--brand);
    box-shadow: var(--shadow);
    font-weight: 600;
}

/*
 * A section with pages of its own — currently only Settings.
 *
 * The summary is styled to match the links beside it, so the sidebar reads as
 * one list whose Settings row happens to open. Its chevron is the same shape
 * as the drawer toggle's, drawn once more rather than shared, because that one
 * sits after `margin-left: auto` in a flex row and this one sits in a line of
 * text.
 */
.admin-nav__subtoggle {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 38px;
    padding: 7px 12px;
    border-radius: 8px;
    border-left: 3px solid transparent;
    color: var(--text);
    font-size: .92rem;
    cursor: pointer;
    list-style: none;
    user-select: none;
}
.admin-nav__subtoggle::-webkit-details-marker { display: none; }
.admin-nav__subtoggle:hover { background: var(--surface); }
.admin-nav__subtoggle:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.admin-nav__subtoggle::after {
    content: "";
    width: 7px;
    height: 7px;
    margin-left: auto;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    color: var(--muted);
    transform: rotate(-45deg);
    transition: transform .15s ease;
}
.admin-nav__sub[open] > .admin-nav__subtoggle::after { transform: rotate(45deg); }
.admin-nav__sub[open] > .admin-nav__subtoggle { font-weight: 600; }

/*
 * The rule down the left is what says these belong to the row above them. The
 * sub-items are smaller and quieter than a top-level link on purpose: they are
 * a second choice made after the section, not another section.
 */
.admin-nav__sublist {
    margin: 2px 0 6px;
    padding-left: 10px;
    border-left: 1px solid var(--border);
    margin-left: 14px;
}
.admin-nav__sublist a {
    min-height: 32px;
    padding: 5px 10px;
    font-size: .875rem;
    color: var(--muted);
    border-left: 2px solid transparent;
}
.admin-nav__sublist a:hover { color: var(--text); }
.admin-nav__sublist a[aria-current="page"] {
    color: var(--text);
    box-shadow: none;
    border-left-color: var(--brand);
}

/*
 * Desktop: there is no drawer. The <summary> goes away and the panel is
 * forced visible whether or not the element carries `open`.
 *
 * Two properties are needed because browsers have two implementations of a
 * closed <details>: older engines hide the children with `display: none`,
 * newer ones hide ::details-content with `content-visibility`. They are kept
 * in separate blocks so an engine that cannot parse the pseudo-element drops
 * only that rule and still applies the first.
 */
@media (min-width: 961px) {
    .admin-nav-wrap > .admin-nav-toggle { display: none; }
    .admin-nav-wrap > .admin-nav { display: block; }
}
@media (min-width: 961px) {
    .admin-nav-wrap::details-content { content-visibility: visible; block-size: auto; }
}

/* Below that, the sidebar becomes a disclosure sitting above the page. */
@media (max-width: 960px) {
    .admin-shell { grid-template-columns: minmax(0, 1fr); gap: 0; }

    .admin-nav-wrap {
        position: static;
        max-height: none;
        overflow: visible;
        padding: 0;
    }

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

    .admin-nav {
        border: 1px solid var(--border);
        border-top: 0;
        border-radius: 0 0 10px 10px;
        background: var(--surface);
        padding: 8px 10px 12px;
    }

    .admin-nav__group { margin: 14px 0 4px; }
    .admin-nav__list a,
    .admin-nav__subtoggle {
        /* 38px is a fine mouse target and a poor thumb one. */
        min-height: 44px;
        border-left: 0;
        border-radius: 8px;
    }
    .admin-nav__list a[aria-current="page"] {
        background: var(--brand);
        color: var(--brand-ink);
        box-shadow: none;
    }
}

/* ---------------------------------------------------------- console page */

.admin-main { padding: 24px 0 64px; min-width: 0; }

/*
 * No width cap: a console page fills the column it is given. The few pages
 * that genuinely want a narrow measure ask for one.
 */
.admin-page { max-width: none; }
.admin-page--narrow { max-width: 520px; }
.admin-page--mid { max-width: 620px; }

/*
 * A title and its actions on one line, stacking when there is no room for
 * both. Saves every page inventing its own header row.
 */
.admin-page__head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px 16px;
    margin-bottom: 18px;
}
.admin-page__head > :first-child { min-width: 0; }
.admin-page__head h1 { margin-bottom: 2px; }
.admin-page__head .lede { margin-bottom: 0; }

/*
 * The parent section, above the title, for a page that is one of several under
 * a single sidebar entry. Small and quiet: it is there to say where you are,
 * not to compete with the heading that says what this page is.
 */
.admin-page__kicker {
    margin: 0 0 2px;
    font-size: .7rem;
    font-weight: 700;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--muted);
}
.admin-page__actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/*
 * Explanatory copy is capped even though the page is not: a paragraph run out
 * to 1600px is a paragraph nobody finishes.
 */
.prose { max-width: 72ch; }
.admin-page .lede,
.admin-page > .notice { max-width: 96ch; }

/*
 * Panels side by side once there is room for two whole ones. auto-fit rather
 * than a fixed count, so a wide monitor gets three and a laptop two without a
 * second breakpoint to keep in step.
 */
.admin-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
    gap: 16px;
    align-items: start;
    margin-bottom: 16px;
}
.admin-grid > .card { margin-bottom: 0; }

/* Fields in columns rather than one very wide stack. */
.field-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 320px), 1fr));
    gap: 0 28px;
}

/* A stat that is a link to the queue it counts. */
.stat--link { display: block; color: inherit; }
.stat--link:hover { border-color: var(--brand); text-decoration: none; }
.stat--alert { border-color: var(--danger); }
.stat--alert b { color: var(--danger); }

/* ------------------------------------------------------- console tables */

/*
 * Tables are the console. Below 760px a horizontally scrolling table is a
 * table nobody reads: the column that matters is off-screen, and the gesture
 * that would reveal it is the one the browser uses to go back. So each row
 * becomes its own small card and every value is labelled with the column
 * heading it sat under, taken from the data-label the view supplies.
 *
 * Only tables marked .table--stack do this. A two-column summary is already
 * readable on a phone and is left as a table.
 */
@media (max-width: 760px) {
    .table--stack,
    .table--stack tbody,
    .table--stack tr,
    .table--stack td { display: block; width: 100%; }

    .table--stack thead { display: none; }

    .table--stack tr {
        border: 1px solid var(--border);
        border-radius: 10px;
        /* Not --surface: the card behind it already is, and the rows would vanish. */
        background: var(--bg);
        padding: 10px 12px;
        margin-bottom: 10px;
    }
    .table--stack tr:last-child { margin-bottom: 0; }

    .table--stack td {
        border: 0;
        padding: 4px 0;
        display: grid;
        grid-template-columns: minmax(80px, 32%) minmax(0, 1fr);
        gap: 10px;
        align-items: baseline;
    }

    /* The heading, carried down from the column this cell used to sit under. */
    .table--stack td::before {
        content: attr(data-label);
        color: var(--muted);
        font-size: .76rem;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: .04em;
    }

    /*
     * The actions column has no heading. Rather than leave an empty label
     * column beside it, it spans the card and sits at the bottom of it.
     */
    .table--stack td[data-label=""],
    .table--stack td:not([data-label]) {
        display: block;
        padding-top: 10px;
    }
    .table--stack td[data-label=""]::before,
    .table--stack td:not([data-label])::before { content: none; }

    /* The row is a card now; a scroll wrapper around it has nothing to do. */
    .table-scroll:has(> .table--stack) { overflow-x: visible; }

    /* Controls inside a stacked row get the width rather than being squeezed. */
    .table--stack select,
    .table--stack input[type="text"],
    .table--stack input[type="number"] { max-width: 100%; }
}

/*
 * A small form living inside a table cell — the role picker and its Save
 * button on the operators page. Left to itself the select claims the full
 * cell and the button drops below it; this keeps the three controls on one
 * line where there is room and wraps them tidily where there is not.
 */
.cell-form {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
}
.cell-form select { width: auto; min-width: 150px; flex: 1 1 150px; }
.cell-form .checkline { flex: 0 0 auto; align-items: center; }
.cell-form .checkline input { margin-top: 0; }

@media (max-width: 760px) {
    .cell-form select { flex-basis: 100%; }
}

/*
 * A definition list read as reference rather than prose — the role
 * descriptions. Two columns on a wide screen, one on a phone.
 */
.dl-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
    gap: 4px 28px;
    margin: 0;
}
.dl-grid > div { break-inside: avoid; }

/*
 * A key/value table — one record read top to bottom rather than a list of
 * rows. Its headings are a narrow first column, which works everywhere until
 * the value is something long and unbreakable like an email address: `.table
 * th` is nowrap, so the pair would then push the card sideways. Below 560px
 * the label moves above its value and the problem goes away.
 */
.table--kv th { width: 1%; }

@media (max-width: 560px) {
    .table--kv tr { display: block; padding: 6px 0; border-bottom: 1px solid var(--border); }
    .table--kv tr:last-child { border-bottom: 0; }
    .table--kv th,
    .table--kv td { display: block; border: 0; padding: 0; white-space: normal; }
    .table--kv th { font-size: .78rem; text-transform: uppercase; letter-spacing: .04em; }
}

/* ------------------------------------------------------------ form rows */

/*
 * A filter bar: every control grows, except the ones that say otherwise.
 * .row__fit hugs its content (a submit button), .row__narrow is for the
 * two-digit number inputs that would otherwise claim an equal third.
 *
 * These replace inline style="flex:0 0 auto" attributes, which the console's
 * Content-Security-Policy (style-src-attr 'none') discards without a word.
 */
.row__fit { flex: 0 0 auto; }
.row__narrow { flex: 0 1 160px; }

@media (max-width: 560px) {
    /* On a phone a filter bar is a stack; half-width controls help nobody. */
    .row > * { flex-basis: 100%; }
    .row__fit > .btn { width: 100%; justify-content: center; }
}

.textarea--sm { min-height: 72px; }

/*
 * A set of sibling buttons — the three decisions on a report, say. Without
 * this they are inline elements that wrap mid-row with no gap, and on a phone
 * they end up as a ragged block of touch targets too close to tap safely.
 */
.btn-row { display: flex; flex-wrap: wrap; gap: 8px; }

@media (max-width: 480px) {
    .btn-row > .btn { flex: 1 1 100%; justify-content: center; }
}

/*
 * Pagination. The two controls sit at opposite ends of the bar so the thumb
 * that wants "older" always finds it in the same place, whatever the page
 * number between them is, and whether or not "newer" exists.
 */
.pager {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 16px;
}

/*
 * Numbered jumps, when a page has them. They sit between the two end
 * controls and wrap as a block rather than breaking the bar apart, so
 * "newer" and "older" keep their ends on a narrow screen.
 */
.pager__pages {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
    margin-inline: auto;
}

.pager__pages .btn { min-width: 34px; justify-content: center; }
.pager__pages .btn--ghost { border-color: var(--border); }

/*
 * A field that another control has made irrelevant — still readable, plainly
 * not in play. Set by script; without script the field stays live and the
 * server decides, which is the same answer either way.
 */
.is-muted { opacity: .5; }

/*
 * The save bar for the long settings form. Pinned to the bottom of the
 * viewport so an operator who changed one field twelve screens up does not
 * have to scroll to the end to find out that a Save button exists.
 */
.form-actions {
    position: sticky;
    bottom: 0;
    z-index: 10;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
    margin-top: 16px;
    padding: 12px 16px;
    padding-bottom: calc(12px + env(safe-area-inset-bottom));
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius) var(--radius) 0 0;
    box-shadow: 0 -4px 14px rgba(16, 32, 48, .1);
}

/*
 * The branding form has its own submit, above the page's sticky save bar. It
 * must not also stick, or two save buttons pin themselves to the bottom of the
 * viewport and the operator cannot tell which form either belongs to.
 */
.form-actions--inline {
    position: static;
    margin-top: 4px;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
}

/* ------------------------------------------------------------- branding */

.logo-row {
    display: flex;
    flex-wrap: wrap;
    gap: 20px;
    align-items: flex-start;
    margin-top: 14px;
}

.logo-controls { flex: 1 1 300px; min-width: 0; }

.logo-preview {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.logo-preview__label {
    font-size: .72rem;
    font-weight: 700;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}

/*
 * The preview renders the real lockup partial rather than a mock-up of it, so
 * what is shown here cannot drift from what the header draws. It sits on the
 * page background rather than the card's surface for the same reason — that is
 * the colour behind the real header.
 */
.logo-preview__stage {
    display: flex;
    align-items: center;
    min-height: 62px;
    padding: 12px 16px;
    background: var(--bg);
    border: 1px dashed var(--border);
    border-radius: var(--radius);
}

/* ----------------------------------------------------------- page chrome */

.admin-footer {
    border-top: 1px solid var(--border);
    background: var(--surface);
    padding-block: 16px;
    font-size: .84rem;
    color: var(--muted);
}

.admin-footer__inner { display: flex; gap: 8px 16px; flex-wrap: wrap; }

@media (max-width: 560px) {
    .admin-body { --admin-header-h: 52px; }
    .admin-main { padding: 16px 0 48px; }
    .admin-header__inner { padding-block: 8px; gap: 10px; }
    .admin-grid { grid-template-columns: minmax(0, 1fr); }

    /* Four stats one per row wastes a phone screen; two columns fit. */
    .stat-row { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
    .stat { padding: 12px; }
    .stat b { font-size: 1.35rem; }

    .card { padding: 14px; border-radius: 12px; }
    .admin-page h1 { font-size: 1.3rem; }
}

/*
 * The console shell with no operator signed in: sign-in, the second-factor
 * challenge, a forced password change.
 *
 * None of them draws a sidebar, and a lone column left in a full-width shell
 * sits in the top-left corner of a desktop monitor with the rest of the page
 * empty — it reads as a page that failed rather than a form waiting for an
 * answer. Narrowing the shell centres the column, and the same rule gives a
 * phone a proper gutter.
 */
.admin-shell--bare {
    grid-template-columns: minmax(0, 1fr);
    max-width: 560px;
}

.admin-shell--bare .admin-main { padding: clamp(20px, 5vh, 48px) 0 48px; }

/* The shell is already the narrow one; a second cap would only re-indent. */
.admin-shell--bare .admin-page,
.admin-shell--bare .admin-page--narrow,
.admin-shell--bare .admin-page--mid { max-width: none; }

/*
 * A full-width control on a single-purpose form: on a phone the target is the
 * width of the card rather than a thumb-sized rectangle in one corner.
 */
.btn--block { width: 100%; justify-content: center; padding-block: 11px; }

/*
 * Long-form asides — why there is no emailed reset, what to do about a lost
 * device. Collapsed they stop a paragraph of prose from burying the form it
 * belongs to; opened they still say the whole thing.
 */
.note { margin-top: 4px; }

.note > summary {
    cursor: pointer;
    font-size: .88rem;
    font-weight: 600;
    color: var(--muted);
    padding: 6px 0;
}

.note > summary:hover { color: var(--text); }
.note > summary:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; border-radius: 4px; }
.note[open] > summary { color: var(--text); }
.note p { margin: 6px 0 0; }
.note code { overflow-wrap: anywhere; }

.admin-nav__list .checkline { margin: 4px 0; }
.admin-page dl dt { margin-top: 8px; }
.admin-page dl dd { margin: 0 0 4px; }

/* ==================================================================
   Landing page
   ==================================================================

   A run of full-bleed bands rather than a page inside the reading column.
   The layout gives this page `.main--bleed` (see layouts/main.php); each
   section here paints its own background edge to edge and re-applies the
   gutter with `.shell`, so the colour runs to the screen edge while the text
   still stops at a readable measure.

   Everything is sized in clamp() against the viewport rather than switched at
   breakpoints. A landing page has one job at 360px and the same job at 2560px,
   and a fixed 3rem heading is either a shout on a phone or a whisper on a
   monitor. The media queries that remain change arrangement, not scale.
   ------------------------------------------------------------------ */

.eyebrow {
    display: inline-block;
    margin: 0 0 14px;
    padding: 5px 12px;
    border-radius: 999px;
    background: var(--brand-soft);
    border: 1px solid var(--brand-line);
    color: var(--brand);
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .06em;
    text-transform: uppercase;
}

/* ------------------------------------------------------------- hero */

.hero {
    background:
        radial-gradient(1200px 600px at 12% -10%, var(--brand-soft), transparent 60%),
        linear-gradient(180deg, var(--hero-from), var(--hero-to));
    border-bottom: 1px solid var(--border);
}

.hero__inner {
    display: grid;
    grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr);
    gap: clamp(28px, 5vw, 64px);
    align-items: center;
    padding-block: clamp(40px, 7vw, 84px);
}

.hero__title {
    font-size: clamp(2rem, 1.15rem + 3.6vw, 3.6rem);
    line-height: 1.08;
    letter-spacing: -.035em;
    margin: 0 0 18px;
    text-wrap: balance;
}

.hero__lede {
    font-size: clamp(1rem, .95rem + .35vw, 1.2rem);
    color: var(--muted);
    margin: 0 0 28px;
    max-width: 60ch;
}

.hero__cta { display: flex; flex-wrap: wrap; gap: 12px; }

/*
 * A short list of claims, each with a tick drawn in CSS.
 *
 * The tick is a pseudo-element on the <li> rather than a character in the
 * markup, so a screen reader hears three list items and not "check mark,
 * free and free to read".
 */
.ticks { list-style: none; margin: 26px 0 0; padding: 0; display: grid; gap: 10px; }

.ticks li {
    position: relative;
    padding-left: 30px;
    color: var(--muted);
    font-size: .95rem;
}

.ticks li::before {
    content: "";
    position: absolute;
    left: 0;
    top: .32em;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--brand-soft);
    border: 1px solid var(--brand-line);
}

/* The check itself: two borders of a rotated box. No icon file, no glyph. */
.ticks li::after {
    content: "";
    position: absolute;
    left: 6px;
    top: .55em;
    width: 5px;
    height: 9px;
    border-right: 2px solid var(--brand);
    border-bottom: 2px solid var(--brand);
    transform: rotate(45deg);
}

/* --------------------------------------------------- hero illustration */

/*
 * A mock of the product, not a screenshot: it stays correct when the feed
 * changes, weighs nothing, and inherits the palette, so it is still on-brand
 * after an operator switches the theme. It is aria-hidden in the markup —
 * see the note in home.php for why.
 */
.hero__demo { min-width: 0; }

.mock {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 18px;
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}

.mock__bar {
    display: flex;
    gap: 6px;
    padding: 12px 16px;
    background: var(--surface-2);
    border-bottom: 1px solid var(--border);
}
.mock__dot { width: 10px; height: 10px; border-radius: 50%; background: var(--border); }

.mock__card { padding: 16px 18px; border-bottom: 1px solid var(--border); }
.mock__card:last-child { border-bottom: 0; }
.mock__card--resolved { background: var(--surface-2); }

.mock__head { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; font-size: .9rem; }
.mock__avatar { width: 34px; height: 34px; border-radius: 50%; background: var(--brand-soft); border: 1px solid var(--brand-line); flex: none; }
.mock__sub { display: block; color: var(--muted); font-size: .78rem; font-weight: 400; }
.mock__body { margin: 0 0 12px; font-size: .94rem; }
.mock__tag { color: var(--accent); font-weight: 600; }

.mock__actions { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.mock__pill {
    padding: 4px 10px;
    border-radius: 999px;
    background: var(--surface-2);
    border: 1px solid var(--border);
    color: var(--muted);
    font-size: .8rem;
    font-weight: 600;
}
.mock__card--resolved .mock__pill { background: var(--surface); }

/* ------------------------------------------------------------- band */

.band {
    background: linear-gradient(120deg, var(--band-from), var(--band-to));
    color: var(--band-ink);
}

.band__inner {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: 20px;
    padding-block: clamp(24px, 3.5vw, 40px);
    text-align: center;
}

.band__stat b {
    display: block;
    font-size: clamp(1.8rem, 1.2rem + 2vw, 2.8rem);
    line-height: 1.05;
    letter-spacing: -.03em;
}
.band__stat span { font-size: .9rem; opacity: .85; }

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

.section { padding-block: clamp(44px, 6vw, 84px); }
.section--alt { background: var(--surface-2); border-block: 1px solid var(--border); }

.section__head { max-width: 62ch; margin-bottom: clamp(24px, 3vw, 40px); }

.section__title {
    font-size: clamp(1.5rem, 1.1rem + 1.5vw, 2.1rem);
    letter-spacing: -.03em;
    margin: 0 0 8px;
    text-wrap: balance;
}

.section__lede { color: var(--muted); margin: 0; font-size: 1.02rem; }

/* ------------------------------------------------------------ steps */

/*
 * auto-fit rather than a fixed four columns: one row of four on a desktop,
 * two of two on a tablet, one column on a phone — without three breakpoints
 * that have to be kept in step with the number of steps.
 */
.steps {
    list-style: none;
    margin: 0;
    padding: 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
    gap: 16px;
}

.step {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 22px 20px;
    box-shadow: var(--shadow);
}

.step__n {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 36px;
    height: 36px;
    margin-bottom: 14px;
    border-radius: 10px;
    background: var(--brand);
    color: var(--brand-ink);
    font-weight: 700;
    font-size: 1rem;
}

.step__title { font-size: 1.05rem; margin: 0 0 6px; }
.step__text { margin: 0; color: var(--muted); font-size: .94rem; }

/* ------------------------------------------------- two-column panels */

.split {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr));
    gap: 16px;
    align-items: start;
}

.panel {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: clamp(20px, 2.5vw, 30px);
    box-shadow: var(--shadow);
}
.section--alt .panel { box-shadow: none; }

.panel__title { font-size: 1.2rem; margin: 0 0 12px; letter-spacing: -.02em; }
.panel p { margin: 0 0 12px; }
.panel p:last-child { margin-bottom: 0; }

/* ------------------------------------------------------- closing CTA */

.cta { background: linear-gradient(120deg, var(--band-from), var(--band-to)); color: var(--band-ink); }

.cta__inner {
    display: flex;
    flex-wrap: wrap;
    gap: 20px 32px;
    align-items: center;
    justify-content: space-between;
    padding-block: clamp(36px, 5vw, 64px);
}

.cta__title { font-size: clamp(1.4rem, 1.1rem + 1.2vw, 1.9rem); margin: 0 0 6px; letter-spacing: -.03em; }
.cta__text { margin: 0; opacity: .88; }
.cta__actions { display: flex; flex-wrap: wrap; gap: 12px; }

/* ------------------------------------------------- landing-only bits */

.btn--lg { padding: 12px 22px; font-size: 1rem; border-radius: 10px; }

/*
 * Buttons sitting on the coloured band. They cannot use --surface or --brand:
 * on a brand-coloured background a brand-coloured button is invisible. These
 * are drawn against --band-ink instead, which is the one colour guaranteed to
 * be readable there.
 */
.btn--on-band { background: var(--band-ink); border-color: var(--band-ink); color: var(--band-from); }
.btn--on-band:hover { border-color: var(--band-ink); opacity: .92; }
.btn--ghost-band { background: transparent; border-color: rgba(255, 255, 255, .5); color: var(--band-ink); }
.btn--ghost-band:hover { border-color: var(--band-ink); background: rgba(255, 255, 255, .12); }
.cta .btn:focus-visible { outline-color: var(--band-ink); }

.chip--lg { padding: 8px 16px; font-size: .92rem; font-weight: 600; color: var(--text); background: var(--surface-2); }
.chip--lg:hover { border-color: var(--brand); color: var(--brand); background: var(--brand-soft); }
.chip--tag { color: var(--accent); }
.chip--tag:hover { color: var(--accent); background: var(--accent-soft); border-color: var(--accent); }

/*
 * Below 880px the hero stacks. The mock goes second, not first: on a phone the
 * first screen should be the sentence that explains the site, and a decorative
 * panel above the headline pushes it off the fold.
 */
@media (max-width: 880px) {
    .hero__inner { grid-template-columns: minmax(0, 1fr); }
    .hero__demo { order: 2; max-width: 460px; }
}

@media (max-width: 560px) {
    .band__inner { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
    .band__stat span { font-size: .76rem; }
    .cta__actions > .btn { flex: 1 1 100%; justify-content: center; }
    .hero__cta > .btn { flex: 1 1 100%; justify-content: center; }
}

/*
 * The decorative panel is the first thing to go when the screen is genuinely
 * small: at 420px it is a tall illustration sitting between the call to action
 * and everything the page says after it.
 */
@media (max-width: 420px) {
    .hero__demo { display: none; }
}

/* ==================================================================
   Palette picker (admin console, Settings)
   ================================================================== */

/*
 * A fieldset that does not look like a 1996 fieldset. The border and the
 * default legend positioning are removed; what is kept is the semantics,
 * which is the only reason to use one — a group of radios needs a group
 * label or a screen reader announces ten unrelated options.
 */
.fieldset { border: 0; padding: 0; margin: 18px 0 0; min-width: 0; }
.fieldset__legend { padding: 0; font-weight: 600; font-size: .9rem; margin-bottom: 8px; }

.radio-row { display: flex; flex-wrap: wrap; gap: 8px 24px; }
.radio-row .checkline { align-items: center; margin: 0; }
.radio-row .checkline input { margin-top: 0; }

.swatch-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 210px), 1fr));
    gap: 10px;
}

/*
 * The whole card is the label, so the hit target is the card and not a 13px
 * radio button. The radio itself stays in the document (and in the tab order)
 * rather than being display:none, or it would stop being reachable by
 * keyboard and stop announcing its checked state.
 */
.swatch {
    position: relative;
    display: block;
    margin: 0;
    font-weight: 400;
    cursor: pointer;
}

.swatch > input {
    position: absolute;
    top: 12px;
    left: 12px;
    margin: 0;
}

.swatch__body {
    display: block;
    height: 100%;
    padding: 12px 14px 14px;
    border: 1px solid var(--border);
    border-radius: 12px;
    background: var(--surface);
    transition: border-color .12s ease-in-out, box-shadow .12s ease-in-out;
}

.swatch:hover .swatch__body { border-color: var(--muted); }
.swatch > input:checked + .swatch__body { border-color: var(--brand); box-shadow: 0 0 0 2px var(--brand-soft); }
.swatch > input:focus-visible + .swatch__body { outline: 2px solid var(--brand); outline-offset: 2px; }

/*
 * The preview. --sw-brand and --sw-accent are set per preset by the nonce'd
 * <style> block the settings view emits; they are NOT the live theme
 * variables, which is the point — every card must show its own colours while
 * the page around it stays in the palette currently in force.
 */
.swatch__chips { display: flex; align-items: center; gap: 6px; margin: 0 0 10px 26px; }
.swatch__chip { width: 18px; height: 18px; border-radius: 50%; flex: none; }
.swatch__chip--brand { background: var(--sw-brand, var(--brand)); }
.swatch__chip--accent { background: var(--sw-accent, var(--accent)); }
.swatch__bar { flex: 1; height: 8px; border-radius: 999px; background: var(--sw-brand, var(--brand)); opacity: .28; }

.swatch__name { display: block; font-weight: 600; font-size: .95rem; }
.swatch__blurb { display: block; color: var(--muted); font-size: .8rem; margin-top: 3px; }

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

/* ==================================================================
   The composer
   ==================================================================
   One sheet, two hosts: /post/new renders it as a page and the feed
   renders it inside a <dialog>. Everything here is written against
   .sheet rather than against either host, so the two cannot look
   different from each other.

   All of it is dressing on an ordinary multipart form. Delete the
   block and the page still posts; it just looks like 2004 again.
   ------------------------------------------------------------------ */

/*
 * The action row's colours.
 *
 * Not themed, for the same reason the brand mark's status semaphore is not:
 * these are a code — green is a picture, red is a place, blue is a tag — and
 * the palette setting does not get a vote on what "a photo" looks like. They
 * are picked to hold up against both --surface values; the two that go muddy
 * on a dark background are lifted below rather than left to fail quietly.
 */
:root {
    --ca-photo: #2e9e52;
    --ca-place: #e0503a;
    --ca-tag: #1b6fd4;
    --ca-anon: #7c5cd6;
}

@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) {
        --ca-photo: #5ccc7d;
        --ca-place: #ff8069;
        --ca-tag: #67a8ff;
        --ca-anon: #b09cf5;
    }
}

/* ---- the sheet ---- */

.sheet {
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
}

/* On its own page the sheet is the column, and the column is already capped
   by the feed's middle track. */
.feed-col--sheet { max-width: 640px; }

.sheet__head {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 14px 56px;
    border-bottom: 1px solid var(--border);
}

/* Centred on the sheet, not on what is left over beside the close button —
   a title that shifts sideways when the button appears reads as a wobble. */
.sheet__title { margin: 0; font-size: 1.18rem; font-weight: 700; letter-spacing: -.01em; }

.sheet__close {
    position: absolute;
    top: 50%;
    right: 12px;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--muted);
    cursor: pointer;
}
.sheet__close:hover { background: var(--border); color: var(--text); text-decoration: none; }
.sheet__close .icon { width: 19px; height: 19px; }

.sheet__notice { padding: 14px 16px 0; }
.sheet__notice .notice { margin-bottom: 0; }

/*
 * Head, body, foot — and the foot is never inside the part that scrolls.
 *
 * In the dialog the sheet is capped, so flex gives the body whatever is left
 * and the foot keeps its own height at the bottom of the sheet. On the page
 * nothing is capped and the three simply stack; the foot is made sticky
 * there instead (see .feed-col--sheet below) so that Post follows the reader
 * down the window. Two hosts, two scrollports, one set of boxes.
 */
.sheet__form { display: flex; flex-direction: column; min-height: 0; }
.sheet__body { flex: 1 1 auto; min-height: 0; padding: 16px; }

.sheet__foot {
    flex: none;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 12px 16px;
    background: var(--surface);
    border-top: 1px solid var(--border);
    border-radius: 0 0 var(--radius) var(--radius);
}

/* On the page there is no sheet-shaped scrollport to pin against, so the
   window does the job. */
.feed-col--sheet .sheet__foot { position: sticky; bottom: 0; z-index: 2; }

/* Full width and unmissable, the way every composer ends. The counter and
   the ring keep to the left of it. */
.sheet__post { flex: 1; justify-content: center; padding-block: 11px; font-size: .98rem; }
.sheet__count { white-space: nowrap; }

/* Only the editor has one, and it stays narrow: leaving an edit is the rarer
   intent, and the X in the header is the other way out. */
.sheet__cancel { flex: none; padding-block: 11px; }

/* ---- who this is going out as ---- */

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

/* The mask sits on top of the avatar and is revealed by .is-anon, rather than
   the two swapping places, so the row cannot change height when it flips. */
.compose__face { position: relative; flex: none; display: block; width: 44px; height: 44px; }
.compose__face .avatar { width: 44px; height: 44px; }

.compose__mask {
    position: absolute;
    inset: 0;
    display: none;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--muted);
    box-shadow: inset 0 0 0 1px var(--border);
}
.compose__mask .icon { width: 22px; height: 22px; }

.compose__whotext { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.compose__name { font-weight: 700; font-size: .98rem; }
.compose__anonname { display: none; }

.compose__who.is-anon .compose__mask { display: flex; }
.compose__who.is-anon [data-compose-realname] { display: none; }
.compose__who.is-anon .compose__anonname { display: inline; }

.compose__pills { display: flex; flex-wrap: wrap; gap: 6px; }

.cpill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    max-width: 100%;
    padding: 3px 9px;
    border-radius: 6px;
    background: var(--surface-2);
    color: var(--muted);
    font-size: .78rem;
    font-weight: 600;
}
.cpill .icon { width: 14px; height: 14px; flex: none; }
.cpill__caret { width: 13px !important; height: 13px !important; }
.cpill > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

a.cpill:hover { background: var(--border); color: var(--text); text-decoration: none; }
.cpill--place:not(.is-empty) { background: var(--brand-soft); color: var(--brand); }
a.cpill--place:not(.is-empty):hover { background: var(--brand-line); color: var(--brand); }

/* ---- the box you type into ----
   Borderless and oversized: on a composer the text is the page, and a 1px
   rectangle around it turns it back into field number one of four. The focus
   ring stays, though — it is the only thing telling a keyboard user where
   they are, and "it looks tidier without one" is not a reason to take that
   away from them. */

.compose__grow { display: grid; }

.compose__text,
.compose__grow::after {
    grid-area: 1 / 1;
    padding: 2px 0 8px;
    font: inherit;
    font-size: 1.12rem;
    line-height: 1.5;
    border: 0;
    border-radius: 0;
    background: none;
    overflow-wrap: anywhere;
}

.compose__text { min-height: 78px; color: var(--text); }
.compose__text::placeholder { color: var(--muted); }
.compose__text:focus-visible { outline-offset: 4px; }
.compose__text:disabled { background: none; opacity: .6; }

/*
 * Auto-grow without measuring anything.
 *
 * app.js copies the value into data-replicated-value on the wrapper; the
 * pseudo-element renders the same text invisibly in the same grid cell, and
 * the taller of the two sets the row height. No scrollHeight reads and no
 * element.style writes — which matters here, because style-src-attr is none
 * and this file is the only place allowed to set a height.
 *
 * Both halves are gated behind .is-autogrow, which app.js adds. Without it
 * the textarea keeps its own scrollbar and resize handle, so a long report is
 * still readable with JavaScript off.
 */
.compose__grow.is-autogrow::after {
    content: attr(data-replicated-value) " ";
    white-space: pre-wrap;
    visibility: hidden;
}
.compose__grow.is-autogrow > .compose__text { resize: none; overflow: hidden; }

/* #tags and @names picked out of what is being typed. */
.compose__chips { display: flex; flex-wrap: wrap; gap: 6px; margin: 6px 0 0; }

.ctag {
    padding: 2px 9px;
    border-radius: 999px;
    font-size: .8rem;
    font-weight: 600;
    background: var(--accent-soft);
    color: var(--accent);
}
.ctag--at { background: var(--brand-soft); color: var(--brand); }

/* ---- attachments ---- */

/* A two-row band, not a four-row one: the title and the file control share a
   line while there is room for them, and everything that follows — the
   thumbnails, "Add more", the metadata note — claims a full row of its own.
   An empty composer should not spend a third of its height on an empty
   dropzone. */
.dropzone {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px 16px;
    margin-top: 12px;
    padding: 12px 14px;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    background: var(--bg);
}
.dropzone > .hint, .dropzone > .thumbs { flex-basis: 100%; }
.dropzone .hint { margin-top: 0; }
.dropzone.is-locked { opacity: .65; }

/*
 * A `display` of our own beats the browser's [hidden] rule, so anything here
 * that app.js hides by setting .hidden has to be told again. Getting this
 * wrong is not subtle — an empty grid keeps its gaps and an empty badge is a
 * little coloured blob sitting on the page with nothing in it.
 */
.thumbs[hidden], .compose__chips[hidden], .placenote .badge[hidden] { display: none; }

/* Drag feedback. Added on dragenter and taken off again on dragleave and on
   drop, so it can never be left showing. */
.dropzone.is-dragging { border-color: var(--brand); border-style: solid; background: var(--brand-soft); }

.dropzone__head { display: flex; align-items: flex-start; gap: 10px; flex: 1 1 240px; }
.dropzone__icon { width: 22px; height: 22px; color: var(--muted); flex: none; margin-top: 1px; }
.dropzone__text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.dropzone__text strong { font-size: .92rem; }

.dropzone__input { flex: 0 1 auto; width: auto; max-width: 100%; font-size: .88rem; color: var(--muted); }
.dropzone__input::file-selector-button {
    margin-right: 10px;
    padding: 7px 13px;
    font: inherit;
    font-weight: 600;
    color: var(--text);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 8px;
    cursor: pointer;
}
.dropzone__input::file-selector-button:hover { border-color: var(--muted); }

/* Once app.js is drawing thumbnails the bare control has nothing left to say
   — "3 files selected" under three visible files — so it moves off-screen,
   still focusable and still in the tab order. "Add more" takes its place and
   is a <label> pointing at the very same input. */
.dropzone.has-thumbs .dropzone__input { position: absolute; left: -9999px; }
.dropzone__more { display: none; align-self: flex-start; }
.dropzone.has-thumbs .dropzone__more { display: inline-flex; }

/* auto-fit, not auto-fill: with one attachment the empty tracks collapse and
   the single photo takes the width of the sheet, which is the whole point of
   the :only-child rule below. auto-fill would leave it sitting in a 104px
   box with four empty columns beside it. */
.thumbs {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
    gap: 8px;
    margin: 0;
    padding: 0;
    list-style: none;
}

.thumb {
    position: relative;
    aspect-ratio: 1;
    border-radius: 10px;
    overflow: hidden;
    background: var(--surface-2);
    border: 1px solid var(--border);
}
.thumb img, .thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* One attachment is the picture, not a thumbnail of it: it gets the width of
   the sheet and is fitted rather than cropped, because a single photo of a
   broken thing is evidence and cropping evidence to a square is how the
   broken thing ends up outside the frame. */
.thumb:only-child { aspect-ratio: 16 / 10; max-height: 240px; }
.thumb:only-child img, .thumb:only-child video { object-fit: contain; }

/* A file the browser will not preview — a video in a codec it cannot decode,
   or anything else that failed to load. The name is all there is to show. */
.thumb__name {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    padding: 8px;
    font-size: .74rem;
    color: var(--muted);
    text-align: center;
    overflow-wrap: anywhere;
}

.thumb__drop {
    position: absolute;
    top: 6px;
    right: 6px;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: rgba(12, 20, 18, .62);
    color: #fff;
    cursor: pointer;
}
.thumb__drop:hover { background: rgba(12, 20, 18, .82); }
.thumb__drop:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ---- attachments a post already has, while it is being edited ---- */

/*
 * A real checkbox sitting on the tile, not an X button.
 *
 * The X on a freshly picked file above removes it there and then, and it can
 * be a button because nothing has happened yet. This one is a promise about
 * what saving will do — the file is on the post until the form is submitted —
 * and a control that means "this will be deleted when you press Save" has to
 * be something the reader can see the state of, and can change their mind
 * about, without JavaScript. That is a checkbox.
 */
.thumbs--kept .thumb__keep {
    position: absolute;
    left: 6px;
    right: 6px;
    bottom: 6px;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 4px 8px;
    border-radius: 8px;
    background: rgba(12, 20, 18, .68);
    color: #fff;
    font-size: .72rem;
    font-weight: 600;
    cursor: pointer;
}
.thumbs--kept .thumb__keep input { accent-color: var(--danger, #d64545); margin: 0; }
.thumbs--kept .thumb__keep:hover { background: rgba(12, 20, 18, .84); }
.thumbs--kept .thumb:has(.thumb__keep input:checked) { border-color: var(--danger, #d64545); }

/* Struck through rather than hidden: it is still on the post until they save,
   and a tile that vanished on tick would be indistinguishable from one that
   had already gone. Progressive — a browser without :has() simply shows the
   ticked box, which says the same thing in words. */
.thumbs--kept .thumb:has(.thumb__keep input:checked) img,
.thumbs--kept .thumb:has(.thumb__keep input:checked) video { opacity: .35; }

/* The "Video" badge lives bottom-left, which is where the checkbox now is. */
.thumbs--kept .thumb__tag { bottom: auto; top: 6px; }

.thumb__tag {
    position: absolute;
    left: 6px;
    bottom: 6px;
    padding: 1px 7px;
    border-radius: 999px;
    background: rgba(12, 20, 18, .62);
    color: #fff;
    font-size: .68rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
}

/* ---- "add to your post" ---- */

/*
 * The bordered row of coloured actions, the shape every social composer has
 * settled on. It is a row of links and one <label>, not buttons that do
 * things: each one takes you to a control that is already on the form below.
 * That is what lets it work with JavaScript off, and it is why nothing in
 * here needs a disabled state except the photo action, which is genuinely
 * unavailable to accounts that have not unlocked it yet.
 */
.cadd {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 14px;
    padding: 8px 8px 8px 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
}
.cadd__label { font-size: .92rem; font-weight: 600; }
.cadd__row { display: flex; gap: 2px; flex: 1; justify-content: flex-end; }

.cadd__item {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    color: var(--muted);
    cursor: pointer;
}
.cadd__item:hover { background: var(--surface-2); text-decoration: none; }
.cadd__item:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }
.cadd__item .icon { width: 23px; height: 23px; flex: none; }

/* The label is the control's accessible name, so it is hidden rather than
   dropped — off-screen, not display:none, which would take it out of the
   accessibility tree along with the pixels. */
.cadd__text { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

.cadd__item--photo .icon { color: var(--ca-photo); }
.cadd__item--place .icon { color: var(--ca-place); }
.cadd__item--tag .icon { color: var(--ca-tag); }
.cadd__item--anon .icon { color: var(--ca-anon); }

.cadd__item[aria-disabled="true"] { opacity: .45; cursor: not-allowed; }
.cadd__item[aria-disabled="true"]:hover { background: none; color: var(--muted); }

/* ---- the sections the row points at ---- */

.cblock { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--border); }
.cblock__title { display: flex; align-items: center; gap: 8px; margin: 0 0 10px; font-size: .98rem; }
.cblock__title .icon { width: 18px; height: 18px; color: var(--muted); }

/* Inside the sheet the switches sit closer together than they do on a
   settings page: there are two of them and they are not the subject. */
.switch-row--tight { margin: 0 0 8px; padding: 12px 14px; }
.switch-row--tight:last-of-type { margin-bottom: 0; }

/* The search box is created by app.js — without it the select is an ordinary
   dropdown and nothing is missing. */
.placepick__search { margin-bottom: 8px; }

/* While a search is running the select is given a size, which turns it into a
   listbox: the matches are a list you scan, not a menu you reopen. */
.placepick__select[size] { padding: 4px; }
.placepick__select[size] option { padding: 6px 8px; border-radius: 6px; }

.placenote {
    display: flex;
    align-items: flex-start;
    gap: 7px;
    margin: 10px 0 0;
    color: var(--muted);
    font-size: .84rem;
}
.placenote .icon { width: 16px; height: 16px; flex: none; }
.placenote__icon { flex: none; line-height: 0; margin-top: 2px; }
.placenote__icon--set, .placenote__set { display: none; }
.placenote__set { align-items: center; gap: 8px; flex-wrap: wrap; }

.placenote.is-set { color: var(--text); font-weight: 600; }
.placenote.is-set .placenote__icon--set { display: block; color: var(--ok); }
.placenote.is-set .placenote__icon--idle,
.placenote.is-set .placenote__idle { display: none; }
.placenote.is-set .placenote__set { display: flex; }
.placenote .badge { flex: none; background: var(--brand-soft); color: var(--brand); }

/* ---- choosing a place, as a panel over the sheet ---- */

/*
 * The panel covers the whole sheet, header and footer included.
 *
 * Picking a place is a search, and a search wants the surface to itself: a
 * field at the top, one row per place under it, and nothing else competing.
 * Covering the footer is deliberate — Post is not an answer to "which place",
 * and a button that submits the form sitting under a list of places is a
 * misfire waiting to happen.
 *
 * `inset: 0` against .sheet, which is why that is positioned. In the dialog
 * the sheet is capped and the panel inherits the cap; on /post/new the sheet
 * is only as tall as its folded contents, which is short.
 */
.sheet { position: relative; }

.placepanel {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: flex;
    flex-direction: column;
    min-height: 0;
    background: var(--surface);
    border-radius: var(--radius);
}

.placepanel__head {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    padding: 14px 56px;
    border-bottom: 1px solid var(--border);
}

.placepanel__title { margin: 0; font-size: 1.1rem; font-weight: 700; letter-spacing: -.01em; }

/* Left, where every "back" in every app has always been — and the same
   circle as the × it replaces, so the header does not change shape. */
.placepanel__back {
    position: absolute;
    top: 50%;
    left: 12px;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    justify-content: center;
    width: 34px;
    height: 34px;
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--muted);
    cursor: pointer;
}
.placepanel__back:hover { background: var(--border); color: var(--text); }
.placepanel__back .icon { width: 19px; height: 19px; }

.placepanel__field { position: relative; flex: none; padding: 12px 16px; }

.placepanel__fieldicon {
    position: absolute;
    top: 50%;
    left: 28px;
    transform: translateY(-50%);
    width: 17px;
    height: 17px;
    color: var(--muted);
    pointer-events: none;
}

.placepanel__input {
    width: 100%;
    padding: 10px 12px 10px 38px;
    border: 1px solid var(--border);
    border-radius: 999px;
    background: var(--surface-2);
    color: var(--text);
    font: inherit;
    font-size: .95rem;
}
.placepanel__input::placeholder { color: var(--muted); }

/* Said in the panel's own voice rather than in red: nothing has gone wrong,
   there is simply one more thing to answer. */
.placepanel__need {
    flex: none;
    margin: 0 16px 8px;
    padding: 9px 12px;
    border: 1px solid var(--warn);
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--text);
    font-size: .88rem;
}

.placepanel__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 0 8px 12px; }

.placepanel__group + .placepanel__group { margin-top: 6px; }

.placepanel__label {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 10px 8px 4px;
    font-size: .78rem;
    font-weight: 700;
    letter-spacing: .04em;
    text-transform: uppercase;
    color: var(--muted);
}
.placepanel__label .icon { width: 14px; height: 14px; }

.placepanel__empty {
    display: flex;
    gap: 10px;
    margin: 14px 8px;
    padding: 12px 14px;
    border: 1px dashed var(--border);
    border-radius: var(--radius);
    color: var(--muted);
    font-size: .88rem;
}
.placepanel__empty .icon { width: 18px; height: 18px; flex: none; margin-top: 1px; }

.placepanel__note { margin: 6px 10px 0; color: var(--muted); font-size: .84rem; }

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

/*
 * One row per place: the name, the trail above it, and how wide it is.
 *
 * Two lines rather than one because "Sasaram" on its own is not an answer —
 * the same name sits under two districts in most states, and the row under it
 * is the only thing that tells a reader which one they are about to pick.
 */
.placerow {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 9px 10px;
    border: 0;
    border-radius: var(--radius);
    background: none;
    color: var(--text);
    font: inherit;
    text-align: left;
    cursor: pointer;
}
.placerow:hover { background: var(--surface-2); }
.placerow:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }

.placerow__icon {
    display: flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: 36px;
    height: 36px;
    border-radius: 50%;
    background: var(--surface-2);
    color: var(--muted);
}
.placerow__icon .icon { width: 19px; height: 19px; }

.placerow__body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1; }
.placerow__name { font-weight: 600; font-size: .96rem; }

/* One line, ellipsised. A path four levels deep wrapping onto a second line
   turns a scannable list into a paragraph. */
.placerow__path {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--muted);
    font-size: .82rem;
}

.placerow .badge { flex: none; background: var(--surface-2); color: var(--muted); }

.placerow.is-chosen { background: var(--brand-soft); }
/* --brand-ink, not white: in the dark theme --brand is a light mint and white
   on it is unreadable. It is the token that knows what reads on the brand. */
.placerow.is-chosen .placerow__icon,
.placerow.is-chosen .badge { background: var(--brand); color: var(--brand-ink); }
.placerow.is-chosen .placerow__name { color: var(--brand); }

/*
 * Everything app.js folds away with `hidden`, told again.
 *
 * A `display` of our own beats the browser's [hidden] rule — the same trap as
 * the thumbnails above. Getting it wrong here is not subtle: the panel would
 * sit over the composer permanently, which is a form nobody can use.
 */
.placepanel[hidden],
.placepanel__group[hidden],
.placepanel__empty[hidden],
.placepanel__need[hidden],
.placepanel__note[hidden],
.placepanel__proto[hidden],
.dropzone[hidden] { display: none; }

/* ---- the character ring ---- */

/*
 * Drawn the way every composer with a limit draws one.
 *
 * app.js sets stroke-dasharray, which is a presentation attribute rather than
 * a style attribute — the one kind of per-element geometry this CSP lets a
 * script write.
 */
.cring { flex: none; line-height: 0; }
.cring svg { display: block; transform: rotate(-90deg); }
.cring__track { stroke: var(--border); }
.cring__value { stroke: var(--brand); transition: stroke-dasharray .1s linear; }
.cring.is-near .cring__value { stroke: var(--warn); }
.cring.is-full .cring__value { stroke: var(--danger); }
.cring.is-full + .sheet__count { color: var(--danger); font-weight: 600; }

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

/* ---- the dialog the feed opens ---- */

/*
 * Taller than the modal used for a confirmation, and capped so the body
 * scrolls rather than the page behind it. `dialog.modal` already supplies the
 * surface, the border and the backdrop; this only changes the geometry, and
 * drops the padding because the sheet inside brings its own.
 */
dialog.modal--sheet {
    width: min(560px, calc(100vw - 24px));
    max-height: min(92vh, 860px);
    padding: 0;
    border: 0;
    overflow: visible;
    background: none;
    box-shadow: none;
}

dialog.modal--sheet .sheet { max-height: min(92vh, 860px); box-shadow: var(--shadow-lg); }
dialog.modal--sheet .sheet__body { overflow-y: auto; overscroll-behavior: contain; }

@media (max-width: 600px) {
    /* Edge to edge and anchored to the bottom, the way a sheet behaves on a
       phone: the thumb is at the bottom of the screen and so is Post. */
    dialog.modal--sheet {
        width: 100vw;
        max-width: none;
        max-height: 94vh;
        margin: 0 0 auto;
        top: auto;
        bottom: 0;
    }

    dialog.modal--sheet .sheet {
        max-height: 94vh;
        border-width: 1px 0 0;
        border-radius: var(--radius) var(--radius) 0 0;
    }

    .sheet__body { padding-bottom: calc(16px + env(safe-area-inset-bottom)); }
    .sheet__foot { padding-bottom: calc(12px + env(safe-area-inset-bottom)); }

    .compose__text, .compose__grow::after { font-size: 1.04rem; }

    .cadd { padding: 6px 6px 6px 12px; }
    .cadd__label { font-size: .86rem; }
}
