/*
 * GSGO — classic 2005–2010 portal look
 * Fixed navbar + fixed sidebar + scrolling main
 * Simple fonts, sharp corners, no heroes / no soft UI bling
 */

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

html,
body {
    margin: 0;
    padding: 0;
    height: 100%;
}

html {
    /* Always show the scrollbar track. The navbar is fixed and right-anchored,
       so on browsers with classic (space-taking) scrollbars the whole right
       cluster — bell included — stepped ~12px sideways the moment a loading
       page grew tall enough to scroll. A permanent scrollbar keeps the
       viewport width constant from the first paint. Headless/overlay-scrollbar
       browsers never showed this, which is why no probe could see it. */
    overflow-y: scroll;

    /* Root size — everything inherits from here (was 13 → 16, eased to 14,
       now 15).

       This is the site's zoom knob, and it only works as one because the
       layout tokens below are expressed in rem as well: change this number
       and the navbar, the sidebar, the content column and the type all move
       together. Bumped for 1440p, where 14 read a shade small; 1080p (the
       design target, D20) still fits the shell with room over. */
    font-size: 15px;
}

/* Bigger screens get a bigger shell, not just wider margins.

   D20 caps the shell so it does not sprawl, and that is right — but on a
   1440p monitor the cap left the site sitting small in the middle of a lot of
   grey. Because every layout token is rem, stepping the root steps the whole
   thing at once: type, navbar, sidebar and the content column together, in
   the proportions the design was drawn in.

   The first step starts at 2000px, deliberately above 1920: 1080p is the
   design target (D20) and keeps the base size, so the layout everything was
   tuned against is the one it still gets. */
@media (min-width: 2000px) {
    html { font-size: 16px; }
}

@media (min-width: 2400px) {
    html { font-size: 17px; }
}

@media (min-width: 3200px) {
    html { font-size: 18px; }
}

body {
    /* The site font, declared once. Nunito is self-hosted with a 200–1000
       variable axis (@font-face near the SGO block) — weight is a design
       lever here, not two fixed faces.

       Everything below inherits it. Form controls do NOT inherit font by
       default, which is why input/button/select/textarea are named again in
       the SGO block; any rule that sets its own family is a bug, and six of
       them were quietly keeping Arial alive on buttons, inputs and the two
       navbar buttons — beaten into place by specificity (`.btn` outranks
       `button`, `#navbar .nav-user-toggle` outranks `.nav-user-name`), so
       the page mixed two typefaces and looked merely "off". */
    font-family: 'Nunito', 'Nunito-fallback', 'Segoe UI', system-ui, sans-serif;
    font-size: 1rem;
    line-height: 1.45;
    color: var(--text);
    background: var(--page-bg);
}

/* NO LINK UNDERLINES ANYWHERE, IN ANY STATE — Sebastian's rule.
 *
 * It started as "no underline on hover", because an underline that appears on
 * hover is a 1990s tell: it says "this is a link" to someone who has already
 * found it, and it shouts while reflowing nothing. It ends here, at rest as
 * well: a forum index is mostly links — every title, every handle, every
 * timestamp — and underlining all of them turns a page of content into a page
 * of ruled lines.
 *
 * Colour is the whole affordance: --link, and the --link-hover shift on
 * hover. Focus keeps its ring, which is an outline, not an underline, and is
 * how the keyboard finds its way.
 *
 * If you are writing a rule and reaching for `text-decoration: underline`,
 * reach for a colour token instead. This applies to the elements that only
 * look like links too (.linkish, .viewas-exit) — they are buttons wearing a
 * link's clothes, and the clothes are the colour.
 */
a,
a:link,
a:visited,
a:hover,
a:focus,
a:active {
    color: var(--link);
    text-decoration: none;
}

a:hover {
    color: var(--link-hover);
}

/* Scrollbars: no track background, ever. The thumb is invisible at rest, fades
   in when the pointer is in the scroll area, and WIDENS + darkens when you go to
   grab it — an easy, forgiving target (the click zone is the full column even
   while the visible band is slim). Firefox via scrollbar-color, WebKit/Blink via
   the pseudo-elements. */
* {
    scrollbar-width: thin;
    scrollbar-color: transparent transparent;   /* invisible at rest */
}
*:hover {
    scrollbar-color: var(--scrollbar-thumb) transparent;   /* Firefox: show in-area */
}
::-webkit-scrollbar { width: 15px; height: 15px; background: transparent; }
::-webkit-scrollbar-track,
::-webkit-scrollbar-track:hover,
::-webkit-scrollbar-corner { background: transparent; }
::-webkit-scrollbar-thumb {
    background-color: transparent;               /* invisible at rest */
    border: 5px solid transparent;               /* slim visible band, full hit area */
    background-clip: padding-box;
    border-radius: 8px;
}
:hover::-webkit-scrollbar-thumb {                /* pointer in the scroll area */
    background-color: var(--scrollbar-thumb);
}
::-webkit-scrollbar-thumb:hover {                /* going for the grab → fat + darker */
    background-color: var(--scrollbar-thumb-hover);
    border-width: 2px;
}

h1,
h2,
h3 {
    font-weight: bold;
    margin: 0 0 10px 0;
    color: var(--heading);
}

h1 {
    font-size: 1.25rem;
    padding-bottom: 6px;
    margin-bottom: 12px;
}

h2 {
    font-size: 1.1rem;
}

p {
    margin: 0 0 10px 0;
}

code {
    font-family: "Courier New", Courier, monospace;
    font-size: 13px;
    background: var(--code-bg);
    border: 1px solid var(--code-border);
    color: var(--code-fg);
    padding: 0 3px;
}

/* ----- layout tokens -----
   Colors live in css/themes/{default,dark}.css — no color tokens here.

   Sizes are rem, written as `calc(<n>rem / 14)` so each one still says the
   pixel value it was designed at (n px at the old 14px root) while scaling
   with the root font-size. That is what makes `html { font-size }` a zoom for
   the whole shell rather than a way to grow the text inside boxes that stay
   put. */
:root {
    /* 38 → 46: the bar grew a step so the bell and the handle can breathe.
       Everything below the bar (sidebar, #main-wrap, dropdown anchors, the
       view-as chip, watermarks) reads this token, so the extra height never
       collides with anyone's top margin. */
    --nav-h: calc(46rem / 14);
    /* Gap between the bottom of the fixed navbar and the first thing in the
       content column. It is a gap ON TOP of --nav-h, never a replacement for
       it: the bar is position:fixed, so #main-wrap must still reserve the bar's
       own height or every page's first element renders underneath it.
       The value is the sidebar's own head start — .sidebar-section's 8px top
       padding plus .sidebar-heading's 2px — so the content column's first
       element sits on the same line as the first sidebar heading. Retune the
       sidebar's padding and this needs to follow. */
    --content-gap: calc(10rem / 14);
    --sidebar-w: calc(194rem / 14);

    /* The app is a centred shell, not a full-bleed page. 1080p is the design
       target and sees no gutter at all; wider screens get margins rather than
       more sprawl. Below the cap --shell-gutter computes to 0px, so narrow
       screens are untouched by construction — no media query, no second path. */
    /* The shell ends where the content ends. It used to be a round 1600px, which
       left a ~300px band to the right of the last panel that belonged to nothing:
       #main-wrap is the scroll container, so its scrollbar sat out at the far edge
       of that band, detached from the column it scrolls. Derived rather than
       written out, so the shell cannot drift away from the column again — widen
       --content-w and everything follows. Below the cap the gutter is 0, so
       smaller screens are untouched by construction, no media query. */
    --shell-max: calc(var(--content-right) + var(--main-pad));
    --shell-gutter: max(0px, (100vw - var(--shell-max)) / 2);

    /* The content column, and the padding #main-wrap puts around it. Tokens
       rather than three separate literals because the navbar now has to end
       exactly where the content ends: with the numbers written out in each
       place, tuning the column would silently unalign the bar above it. */
    --content-w: calc(1080rem / 14);
    --main-pad: calc(12rem / 14);
    /* Where the content's right edge falls, measured from the shell's left. */
    --content-right: calc(var(--sidebar-w) + var(--main-pad) + var(--content-w));
}

/* ========== GUEST LAYOUT ========== */
.layout-guest {
    min-height: 100%;
}

.guest-top {
    background: var(--header-bg);
    color: var(--header-fg);
    height: var(--nav-h);
    line-height: var(--nav-h);
    padding: 0 12px;
    border-bottom: 2px solid var(--header-border);
}

.guest-top .site-logo {
    color: var(--header-fg);
    font-weight: bold;
    text-decoration: none;
    font-size: 1.05rem;
    margin-right: 16px;
}

.guest-top-links a {
    color: var(--header-link);
    margin-left: 12px;
    text-decoration: none;
    font-size: 1rem;
}

.guest-top-links a:hover {
    color: var(--header-link-hover);
}

.guest-wrap {
    max-width: 640px;
    margin: 24px auto;
    padding: 0 12px;
}

/* ----- landing (1.3 — the one public page) ----- */
/* Dense ruled sections, no hero, no web-3.0 flourishes (principle 1). */
.landing h2:not(:first-of-type) {
    border-top: 1px solid var(--border);
    padding-top: 12px;
    margin-top: 18px;
}

/* The closed version: a name, a sentence, two doors. With the explanatory
   sections gone the panel was a wide box holding three short lines, which
   reads as a page that failed to load rather than one that is deliberately
   brief. Narrow enough to look finished. */
.landing--closed {
    max-width: 32rem;
    margin: 0 auto;
}
.landing--closed .lead {
    margin-bottom: 20px;
}

.inside-list {
    margin: 12px 0;
    padding-left: 22px;
    list-style: square;
}

.inside-list li {
    margin-bottom: 4px;
}

/* Themed production error page. */
.error-page {
    text-align: center;
    padding: 32px 20px 40px;
}
.error-page .error-code {
    font-size: 5rem;
    font-weight: 800;
    line-height: 1;
    color: var(--muted);
    margin: 0 0 6px;
}
.error-page h1 {
    border: 0;
    margin: 0 0 6px;
}

.guest-foot {
    text-align: center;
    color: var(--muted);
    font-size: 0.9rem;
    padding: 16px;
    border-top: 1px solid var(--border);
    margin-top: 32px;
}

/* ========== LEGAL PAGES (Impressum, Datenschutz — 0.C.8) ========== */
/* Long-form documents, so the measure is narrower than the shell and the
   leading looser: these are the only pages here anyone reads end to end. */
.legal {
    max-width: 46rem;
    margin: 0 auto;
    line-height: 1.6;
}

.legal h2 {
    margin-top: 1.8em;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--border-soft);
}

.legal h3 {
    margin-top: 1.4em;
    font-size: 1rem;
}

.legal ul {
    padding-left: 1.4em;
}

.legal li {
    margin-bottom: 0.5em;
}

.legal code {
    font-size: 0.9em;
}

.legal-updated,
.legal-foot {
    color: var(--muted);
    font-size: 0.9rem;
}

.legal-foot {
    margin-top: 2.5em;
    padding-top: 12px;
    border-top: 1px solid var(--border);
}

/* An Impressum rendered from empty values looks compliant and is not, so the
   unconfigured state is loud rather than blank. */
.legal-missing {
    border-left: 3px solid var(--danger);
    padding: 8px 12px;
    color: var(--danger-strong);
}

/* Member shell: the same two links, quietly, under the page content. */
.app-foot {
    margin-top: 32px;
    padding-top: 12px;
    border-top: 1px solid var(--border-soft);
    color: var(--muted);
    font-size: 0.85rem;
}

/* ========== APP SHELL (fixed nav + sidebar) ========== */
.layout-app {
    overflow: hidden;
}

#navbar {
    position: fixed;
    top: 0;
    left: var(--shell-gutter);
    right: var(--shell-gutter);
    height: var(--nav-h);
    background: var(--header-bg);
    color: var(--header-fg);
    border-bottom: 2px solid var(--navbar-border);
    z-index: 100;
    display: flex;
    align-items: center;
    padding: 0 10px;
    font-size: 1rem;
}

#navbar .site-logo {
    color: var(--header-fg);
    font-weight: bold;
    text-decoration: none;
    font-size: 1.2rem;
    margin-right: 18px;
    white-space: nowrap;
}

#navbar .nav-user {
    margin-left: auto;
    white-space: nowrap;
    /* One rhythm for the right side: language switch, bell, handle — evenly
       spaced instead of each element bringing its own ad-hoc margin. */
    display: flex;
    align-items: center;
    gap: 12px;
}

#navbar .nav-user-name {
    color: var(--header-link-dim);
}

#navbar .nav-user a {
    color: var(--header-link-soft);
    text-decoration: none;
}

#navbar .nav-user a:hover {
    color: var(--header-link-hover);
}

#navbar .nav-logout-btn {
    font-size: 1rem;
    color: var(--header-link-soft);
    background: transparent;
    border: none;
    padding: 0;
    margin: 0;
    cursor: pointer;
    text-decoration: none;
}

#navbar .nav-logout-btn:hover {
    color: var(--header-link-hover);
}

/* --- topnav user dropdown (handle menu) --- */
.nav-user-menu {
    display: inline-block;
}

#navbar .nav-user-toggle {
    font-size: 1.1rem;
    background: transparent;
    border: none;
    cursor: pointer;
    padding: 4px 8px;
    margin: 0;
    color: var(--header-link-dim);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

#navbar .nav-user-toggle:hover {
    color: var(--header-link-hover);
}

/* While the menu is open, keep the hover state on the handle. */
#navbar .nav-user-toggle[aria-expanded="true"] {
    color: var(--header-link-hover);
}

/* The navbar bell (partials/notification_bell.twig). It shares the user
   dropdown's skeleton; everything here is only what differs — the badge chip
   pinned to the button corner, and a panel that scrolls instead of growing. */
.nav-bell-toggle {
    position: relative;
    /* No right padding: the flex gap alone separates bell and handle, the
       same 20px the language switch gets on the other side. */
    padding: 4px 0 4px 8px;
}
.nav-bell-svg {
    width: 22px;
    height: 22px;
    display: block;
    fill: currentColor;
    /* Flex centers boxes, not baselines: dropped 2px so the bell's foot sits
       on the neighbours' text baseline instead of hovering above it. */
    transform: translateY(2px);
}
.nav-bell-badge .badge {
    position: absolute;
    top: -3px;
    right: -2px;
}
.nav-bell-dropdown {
    min-width: 320px;
    max-width: 380px;
    max-height: min(420px, 70vh);
    /* The panel is a column with one scrolling part. The footer used to be
       `position: sticky` INSIDE the scroller, which leaves it riding above
       content that is still in the same scroll box — a subpixel of the next
       thumb shone through beneath it. Scrolling the list instead of the panel
       means there is no "beneath": the footer is simply not in the scroll
       area. */
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* The panel hangs inside #navbar .nav-user, which is nowrap so the bar's
       own items never fold. Sentences are not bar items: inherited, that rule
       ran every notification onto one endless line, and since overflow-y
       computes overflow-x to auto, the dropdown grew a horizontal scrollbar.
       Wrapping is restored here, and hidden is the belt — a dropdown you have
       to scroll sideways to read is not a dropdown. */
    white-space: normal;
    overflow-x: hidden;
}
/* A handle or album title with no spaces must break rather than push. */
.nav-bell-dropdown .notification-text {
    overflow-wrap: anywhere;
}
.nav-bell-dropdown .notification-list {
    margin: 0;
}
.nav-bell-dropdown .notification {
    padding: 6px 10px;
    font-size: .9rem;
}
.nav-bell-empty {
    margin: 0;
    padding: 10px 12px;
}
.nav-bell-all .nav-menu-label {
    margin-left: 0;
}
/* The rows are the only thing that scrolls. */
.nav-bell-list {
    overflow-y: auto;
    min-height: 0; /* a flex item refuses to shrink below content without it */
}
/* The way to the full page must not scroll away under eight rows — it sits
   outside the scroller, so it cannot be scrolled past or shone through. */
.nav-bell-all {
    flex: 0 0 auto;
    background: var(--panel-bg);
    border-top: 1px solid var(--border);
}

.nav-caret {
    font-size: 0.8rem;
    color: var(--header-link-soft);
}

/* Clings to the underside of the navbar, its right edge flush with the
   navbar's right corner; anchored to #navbar itself (position: fixed is the
   nearest positioned ancestor once .nav-user-menu is static). */
.nav-user-dropdown {
    position: absolute;
    /* -2px: covers the navbar's 2px bottom border so no line sits between
       the bar and the open menu. */
    top: calc(var(--nav-h) - 2px);
    /* 1px nudge right: the flush right:0 read as slightly inside the corner. */
    right: -1px;
    /* Wide enough that hovering cannot resize the menu.
     *
     * Rows bold their label on hover (font-weight 600, below), and the menu is
     * sized by its widest label — so once a label became the widest thing in
     * here, hovering it grew the whole box and everything twitched sideways.
     * "Als Mitglied ansehen" is that label: measured at 182px resting and
     * 184px bold, against the old 160px floor. English tops out at 160px and
     * never moved, which is why this only ever showed in German.
     *
     * 200px clears the bold worst case with room to spare. A translation
     * longer than that brings the twitch back — if that happens, this number
     * is the thing to raise (or drop the weight bump, which is the only other
     * way the width can depend on what the pointer is doing). */
    min-width: 200px;
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-top: none;
    /* The top strip is plain panel bg — it melts into the bar — and the
       2px padding keeps the logout row's hover highlight below the seam. */
    padding-top: 2px;
    border-radius: 0 0 8px 8px;
    z-index: 110;
}

.nav-logout-form {
    margin: 0;
}

/* --- dropdown rows: icon at the left, label pushed right (2005-style,
   e.g. "⌁  Abmelden"). Shared by the account link and the logout form;
   the profile row is the stack variant below. --- */
.nav-user-dropdown .nav-menu-row {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 8px 12px;
    color: var(--header-link-soft);
    text-decoration: none;
}

.nav-user-dropdown .nav-menu-row:hover {
    background: var(--dropdown-hover-bg);
}

/* Hover: colour plus a notch of weight (600 — Nunito's variable axis makes
   "slight" cheap). Normal rows hover as one block; the stacked profile
   row's two links hover individually — only its background follows the
   whole row.
 *
 * The one-button form rows (log out, view as member) are included here via
 * their button. They used to get the weight from a rule of their own and the
 * colour from `#navbar .nav-logout-btn:hover`, which only fires over the
 * button itself — so crossing the row's padding bolded the label without
 * colouring it, and the menu had two different hover effects depending on
 * which row the pointer was over. The button fills its row, so treating it as
 * the row's target is what the other rows already do.
 *
 * Weight is set on the button rather than the label because it inherits, and
 * because the icon should thicken with the word rather than stay behind. */
.nav-user-dropdown a.nav-menu-row:hover,
#navbar .nav-user-dropdown .nav-menu-row:hover .nav-logout-btn,
.nav-user-dropdown .nav-menu-main:hover,
.nav-user-dropdown .nav-menu-sub:hover {
    color: var(--header-link-hover);
    font-weight: 600;
    text-decoration: none;
}

.nav-user-dropdown .nav-menu-label,
.nav-user-dropdown .nav-logout-label {
    margin-left: auto;
}

.nav-user-dropdown .icon {
    font-size: 1.1em;
}

/* Stacked profile row: the main link is the row (profile view), the
   sub-line underneath is the edit form. One hoverable block, two targets.
   The icon is a direct child of the row, so the flex centering puts it at
   the middle height of the whole stack. */
.nav-user-dropdown .nav-menu-row--stack {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
}

.nav-user-dropdown .nav-menu-stack {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    margin-left: auto;
}

.nav-user-dropdown .nav-menu-main {
    color: var(--header-link-soft);
    text-decoration: none;
    line-height: 1.35;
}

.nav-user-dropdown .nav-menu-sub {
    font-size: 0.85rem;
    color: var(--header-link-dim);
    text-decoration: none;
    line-height: 1.35;
}

/* The generic navbar rules underline links/buttons on hover
   (#navbar .nav-user a:hover, #navbar .nav-logout-btn:hover) — menu rows
   never underline. */
#navbar .nav-user-dropdown a:hover,
#navbar .nav-user-dropdown .nav-logout-btn:hover {
    text-decoration: none;
}

#navbar .nav-user-dropdown .nav-logout-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-family: inherit;
    font-size: 1rem;
    color: inherit;
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 0;
}

/* The bottom row's hover fill must respect the menu's rounded corner:
   7px = the menu's 8px minus its 1px border, so the highlight stays flush
   with the visible curve. Targets the last row whatever it is. */
.nav-user-dropdown > :last-child.nav-menu-row {
    border-radius: 0 0 7px 7px;
}

.lang-switch {
    /* Spacing comes from .nav-user's flex gap; a margin here would double it. */
    font-size: 0.95rem;
    white-space: nowrap;
}

.lang-switch a {
    color: var(--header-link);
    text-decoration: none;
}

.layout-app .lang-switch a {
    color: var(--header-link);
}

.lang-switch a:hover {
    color: var(--header-link-hover);
}

.lang-switch strong {
    color: var(--header-strong);
    font-weight: bold;
}

.layout-guest .lang-switch {
    margin-left: 14px;
}

.layout-guest .lang-switch a {
    color: var(--header-link);
}

.layout-guest .lang-switch strong {
    color: var(--header-strong);
}

/* ========== ADMIN SHELL ==========
   Admin pages carry `layout-app` too, so they inherit the whole community dark
   theme. The only difference is a red-tinted top navbar — enough to always know
   you are in the admin area, without a second full theme to maintain. The
   red-navbar rules live at the very end of the file so they win over the dark
   theme's navbar. The staff banner (.admin-banner) is styled there too. */

/*
 * Count badge — classic forum/mod panel tally (phpBB-ish),
 * not a modern “notification pill”.
 */
.badge.badge-empty { display: none; }
.badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.25em;
    height: 1.25em;
    padding: 0 4px;
    margin: 0;
    font-size: 10px;
    font-weight: bold;
    line-height: 1;
    letter-spacing: 0;
    color: var(--badge-fg);
    background: var(--badge-bg);
    border: 1px solid var(--badge-border);
    border-top-color: var(--badge-border-top);
    border-left-color: var(--badge-border-left);
    box-shadow: inset 0 -1px 0 var(--badge-shadow);
    vertical-align: middle;
    flex-shrink: 0;
}

/* Sidebar: row layout, badge on the right */
.sidebar-nav a.nav-alert,
.sidebar-nav a#admin-entry-side,
.sidebar-nav a#admin-entry-approvals {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
}

.sidebar-nav a.nav-alert {
    font-weight: bold;
    background: var(--nav-alert-bg);
    border-left-color: var(--nav-alert-border);
    color: var(--nav-alert-fg);
}

.sidebar-nav a.nav-alert:hover {
    background: var(--nav-alert-hover-bg);
}

.sidebar-nav a .badge {
    float: none;
    margin: 0;
}

body.layout-admin .badge {
    color: var(--admin-badge-fg);
    background: var(--admin-badge-bg);
    border: 1px solid var(--admin-badge-border);
    border-top-color: var(--admin-badge-border-top);
    border-left-color: var(--admin-badge-border-top);
    box-shadow: inset 0 -1px 0 var(--admin-badge-shadow);
}

body.layout-admin .sidebar-nav a.nav-alert {
    background: var(--admin-nav-alert-bg);
    border-left-color: var(--admin-nav-alert-border);
}

body.layout-admin .sidebar-nav a.nav-alert:hover {
    background: var(--admin-nav-alert-hover-bg);
}

.admin-attention-poller {
    position: absolute;
    width: 0;
    height: 0;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
}

/* Alpine cloak */
[x-cloak] {
    display: none !important;
}

/* ----- Appeal queue: Web 2.0 hover card ----- */
.appeals-queue .appeal-preview-cell {
    position: relative;
    max-width: 280px;
    vertical-align: middle;
}

.appeals-queue .appeal-snip {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    cursor: default;
    border-bottom: 1px dotted var(--appeal-snip-border);
}

.appeal-hovercard {
    /* position/size set via Alpine (fixed + viewport clamp) */
    position: fixed;
    z-index: 1000;
    padding: 10px 12px;
    background: var(--hovercard-bg);
    border: 2px solid var(--hovercard-border);
    box-shadow: 3px 3px 0 var(--hovercard-shadow);
    text-align: left;
    white-space: normal;
    color: var(--text);
    font-weight: normal;
    box-sizing: border-box;
}

.appeal-hovercard-head {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 4px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--hovercard-head-border);
}

.appeal-hovercard-meta {
    font-size: 0.9rem;
    margin-bottom: 8px;
}

.appeal-hovercard-section {
    margin-bottom: 8px;
}

.appeal-hovercard-label {
    font-size: 0.8rem;
    font-weight: bold;
    text-transform: uppercase;
    color: var(--hovercard-label);
    margin-bottom: 2px;
}

.appeal-hovercard-body {
    white-space: pre-wrap;
    max-height: var(--appeal-body-max, 140px);
    overflow-y: auto;
    padding: 6px 8px;
    background: var(--hovercard-body-bg);
    border: 1px solid var(--hovercard-body-border);
    font-size: 0.95rem;
    line-height: 1.35;
}

.appeal-hovercard-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin: 10px 0 8px 0;
    padding-top: 8px;
    border-top: 1px solid var(--hovercard-head-border);
}

.appeal-hovercard-reply {
    margin-top: 4px;
}

.appeal-hovercard-reply textarea {
    display: block;
    width: 100%;
    max-width: 100%;
    margin: 4px 0 6px 0;
    font-size: 0.95rem;
}

.btn-sm {
    font-size: 11px !important;
    padding: 3px 8px !important;
}

.btn-warn {
    background: var(--btn-warn-bg) !important;
    border-color: var(--btn-warn-border) !important;
}

/* --btn-danger-bg is #400 — near-black maroon — and this rule set no colour,
   so the label stayed --btn-fg (#23232a) and every destructive button on the
   site was dark grey text on a dark red ground. Affects the account deletion
   button and both admin block buttons, not only the forum. */
.btn-danger {
    background: var(--btn-danger-bg) !important;
    border-color: var(--btn-danger-border) !important;
    color: var(--on-danger) !important;
}

.btn-ghost {
    background: var(--btn-ghost-bg) !important;
    border-color: var(--btn-ghost-border) !important;
    color: var(--btn-ghost-fg) !important;
}

.btn-ghost:hover {
    background: var(--btn-ghost-hover-bg) !important;
    color: var(--btn-ghost-hover-fg) !important;
}

.appeal-quick-col {
    white-space: nowrap;
}

.appeal-quick-col .btn-sm {
    margin-right: 2px;
}



/* Gallery */
.album-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
    gap: 12px;
    margin-top: 12px;
}

.album-card {
    display: block;
    border: 1px solid var(--border);
    background: var(--album-card-bg);
    text-decoration: none;
    color: inherit;
}

.album-card:hover {
    border-color: var(--accent);
    text-decoration: none;
}

/* Cards stay a uniform height — they are navigation, not the photograph — so
   the cover still crops. But a 120px landscape strip took the boots off the
   bottom of every standing shot, which is the part worth seeing; 200px leaves
   far more of a portrait intact. */
.album-cover {
    display: block;
    width: 100%;
    height: 200px;
    object-fit: cover;
    background: var(--image-bg);
}

/* A photo that failed to load, marked by partials/_media_fallback.twig, which
   also swaps its src for the placeholder. No background-image here on purpose:
   the element now *is* the boot, and painting a second one behind it would
   show two boots at two sizes. `contain` because the box it inherits — an
   album cover, a grid tile, a forum embed — is whatever shape the real photo
   had, and `cover` would crop the toe off. */
.img-missing {
    background-color: var(--empty-bg);
    object-fit: contain;
    padding: 8%;
    min-width: 48px;
    min-height: 48px;
}
/* Percentage padding resolves against the CONTAINER, not the image — inside a
   wide row it inflates a fixed 44px thumb into a ~165px slab (F92). Fixed-size
   thumbs get fixed padding so the placeholder stays thumb-sized. */
.notification-thumb.img-missing,
.notification-avatar.img-missing,
.avatar-sm.img-missing,
.avatar.img-missing {
    padding: 6px;
}

/* An <a> whose only content was a photo that will not load: the href is gone
   (see partials/_media_fallback.twig), so it must stop advertising itself as
   somewhere to go. */
.img-missing-link {
    cursor: default;
}

/* A missing embed inside a post needs a box of its own.
 *
 * Clearing `src` also clears the image's intrinsic size, and a forum embed is
 * styled with maxima (max-width/max-height above) rather than a size — so it
 * collapsed to the 48px floor and read as a stray icon rather than as a photo
 * that is not there. Album covers and avatars are unaffected because they
 * carry explicit dimensions already; this is scoped to the one context that
 * sizes itself from the image. */
.forum-post-body .img-missing {
    width: 180px;
    height: 135px;
}

/* The one line under an embedded photo that would not load. Its own block so
   the sentence sits under the picture rather than beside it, and quiet: it is
   an explanation, not an error. */
.img-missing-note {
    display: block;
    margin: 2px 0 6px;
    font-size: 0.85rem;
    color: var(--muted);
}

/* ----- locked: it exists, and friendship is the key -----------------------
   A friends-only album or photo is shown rather than hidden (private is what
   hides). The padlock boot is a second placeholder, distinct from the
   question-mark one on purpose: "there is nothing here" and "there is
   something here you may not open" are different sentences, and the member
   can do something about only one of them. */
/* Both placeholder covers sit on --album-empty-bg, which was retuned for
   exactly this: at its old #c8c8b8 the #c8c8c8 boot vanished into its own
   background and only the darker mark on it showed. */
.album-cover-locked,
.img-locked {
    display: block;
    background-color: var(--album-empty-bg);
    background-image: url('../images/boot-locked.svg');
    background-repeat: no-repeat;
    background-position: center;
    background-size: auto 62%;
}

/* Matches .album-cover exactly, so a locked card is the same size as the
   others and the grid does not go ragged. */
.album-cover-locked {
    width: 100%;
    height: 200px;
}

/* The card is not a link, so it must not behave like one. */
.album-card--locked {
    cursor: default;
}

/* The one thing the meta line above does not already say. "Nur Freunde" is
   the state; this is what to do about it — so it is muted rather than amber:
   a locked album is normal, not a warning. */
.album-card--locked .locked-note {
    display: block;
    margin-top: 2px;
    font-size: 0.85rem;
    color: var(--muted);
}

/* Fills the tile the real photo would have filled, so a locked photo leaves
   the grid the shape it would have been. */


.photo-tile--locked {
    cursor: default;
}

/* Visible only to screen readers — the padlock is the sighted cue. */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

/* An album with no cover yet. Same boot, deliberately NOT `contain` here:
   the card is a wide landscape tile, and a boot stretched to its full height
   reads as a picture of a boot rather than as the absence of one. Sized to a
   little over half the tile it stays a mark, which is what it is. */
.album-cover-empty {
    height: 200px;
    background-color: var(--album-empty-bg);
    background-image: url('../images/boot-placeholder.svg');
    background-repeat: no-repeat;
    background-position: center;
    background-size: auto 55%;
}

.album-card-meta {
    padding: 6px 8px;
    font-size: 0.9rem;
}

.album-card-meta strong {
    display: block;
}

/* The workbench card holds controls, so it is a <div> rather than one link:
   the cover block is the link (same cover/meta classes as the readable card),
   and the action row below it is the work. The grid is a touch wider so the
   two buttons fit without crowding the title. */
.workbench-grid {
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

.workbench-card {
    display: flex;
    flex-direction: column;
}

.workbench-card .workbench-cover {
    display: block;
    color: inherit;
    text-decoration: none;
}

.workbench-card .workbench-cover:hover strong {
    color: var(--accent);
}

.workbench-actions {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: auto;
    padding: 6px 8px;
    border-top: 1px solid var(--border);
}

/* Photos are the product, so thumbnails get real size. 120px postage stamps
   made sense when the chrome was the point; here the picture is.

   Masonry, because the archive is 39% portrait and a fixed 4:3 box gave those
   shots barely half their tile — the rest dark bar. On a site whose principle
   is cap the chrome, never the photo, that was the chrome eating two-fifths of
   the library at exactly the moment somebody is deciding what to open.

   Rows are a fine unit and each tile spans as many as it needs; the span is
   measured in partials/_masonry.twig. CSS `columns` would have been simpler and
   is wrong here — it flows top-to-bottom per column, which would scramble the
   order members set by dragging. `grid-template-rows: masonry` is Firefox-only
   behind a flag. */
.photo-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    grid-auto-rows: 4px;
    column-gap: 10px;
    /* Matches the column gap. The span maths in _masonry.twig folds this in,
       so a tile still lands on the row grid; with 0 here the tiles came out
       flush vertically and 10px apart horizontally, which read as a mistake. */
    row-gap: 10px;
    align-items: start;
    margin-top: 12px;
}

/* Fallback before the script has measured: roughly a 4:3 tile, so an album
   renders sanely even with JS off rather than collapsing into one row.
   The lightbox also lives inside these grids but is position:fixed, so it is
   out of flow and never a grid item. */
.photo-grid > .photo-tile,
.photo-grid > .media-card {
    grid-row-end: span var(--span, 52);
}

.photo-thumb {
    display: block;
    border: 1px solid var(--border);
    overflow: hidden;
    background: var(--thumb-bg);
}

/* Declared after .photo-thumb on purpose: that rule paints with the
   `background` SHORTHAND, which resets background-image, so a locked thumb
   listed earlier lost its padlock and rendered as a dark empty box.
 *
 * The height is the other half. It normally comes from `aspect-ratio` on the
 * <img>, and a locked tile has none — so it takes the ratio itself. `--ratio`
 * is already set inline on the tile from the photo's own dimensions, so the
 * padlock occupies exactly the space the photo would have and the grid looks
 * the same shape to everyone. */
.photo-thumb--locked {
    width: 100%;
    aspect-ratio: var(--ratio, 4 / 3);
    background-color: var(--empty-bg);
    background-image: url('../images/boot-locked.svg');
    background-repeat: no-repeat;
    background-position: center;
    background-size: auto 62%;
}

/* The photo's own ratio, from width/height on the media row — so the grid is
   correct before a single image has loaded and nothing reflows as 48 of them
   arrive. */
.photo-thumb img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: var(--ratio, 4 / 3);
    object-fit: contain;
}

/* ----- photo tile + inline photo management ----- */
.photo-tile {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    background: var(--bg-alt);
    padding: 4px;
}

.photo-tile .photo-thumb {
    border: 0;
}

.photo-manage {
    display: flex;
    flex-direction: column;
    gap: 4px;
    padding: 4px 2px 2px 2px;
    font-size: 0.85rem;
}

.photo-manage form {
    display: flex;
    gap: 4px;
    margin: 0;
}

.photo-manage select,
.photo-manage input[type="text"] {
    flex: 1;
    min-width: 0;
    padding: 2px;
    font-size: 0.85rem;
}

.status-pending {
    color: var(--warn);
}

.status-rejected {
    color: var(--danger);
}

.btn.is-active {
    background: var(--accent);
    border-color: var(--accent-edge);
}

/* The album head's manage toggle: accent-outlined at rest, filled while the
   tooling is open. More present than the ghost it replaced, quieter than the
   solid green that marks the one action a page exists for (.btn-ok). */
.btn.btn-accent {
    background: transparent;
    border-color: var(--accent);
    color: var(--accent);
    font-weight: 600;
    /* A tad larger than .btn-sm without going near the 1rem base — and
       scoped to this button (the only .btn-accent on the site), so the
       site-wide .btn-sm stays exactly where it is. */
    font-size: 12px !important;
    padding: 5px 10px !important;
}

.btn.btn-accent:hover {
    background: var(--accent);
    border-color: var(--accent-edge);
    color: var(--on-accent);
}

.btn.btn-accent.is-active {
    background: var(--accent);
    border-color: var(--accent-edge);
    color: var(--on-accent);
}

.album-manage {
    margin-top: 20px;
}

.photo-status {
    margin: 0;
    padding: 2px 4px 0 4px;
    font-size: 0.85rem;
}

.album-manage-field {
    margin-bottom: 10px;
}

.album-manage-field input[type="file"],
.album-manage-field .muted {
    margin-top: 5px;
}

.album-manage-danger {
    margin-top: 15px;
    padding-top: 15px;
    border-top: 1px solid var(--border);
}

/* "You are here" — set by partials/nav_active.twig, not by the server. */
.sidebar-nav a.is-active {
    background: var(--accent);
    color: var(--on-accent);
    font-weight: bold;
    text-decoration: none;
}

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

/* ----- admin field builder: form left, live preview right ----- */
.field-builder {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 18px;
    align-items: start;
}

.field-builder-form input[type="text"],
.field-builder-form input[type="number"],
.field-builder-form select,
.field-builder-form textarea {
    width: 100%;
    max-width: 420px;
}

.field-builder .checkbox-row {
    margin: 6px 0;
}

.field-builder .checkbox-row label {
    display: inline;
    font-weight: normal;
}

.field-preview {
    position: sticky;
    top: 12px;
    border: 1px solid var(--border);
    background: var(--bg-alt);
}

.field-preview-head {
    padding: 6px 10px;
    font-weight: bold;
    font-size: 0.85rem;
    text-transform: uppercase;
    color: var(--on-accent);
    background: var(--accent);
}

.field-preview-pane {
    padding: 10px 12px;
    border-bottom: 1px solid var(--border);
    background: var(--preview-pane-bg);
}

.field-preview-pane:last-child {
    border-bottom: 0;
}

.field-preview-caption {
    font-size: 0.8rem;
    color: var(--muted);
    margin-bottom: 8px;
}

.field-preview input,
.field-preview select,
.field-preview textarea {
    width: 100%;
    max-width: 260px;
}

.preview-checks {
    display: flex;
    flex-wrap: wrap;
    gap: 4px 14px;
}

.preview-check {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-weight: normal;
}

.preview-warn {
    color: var(--warn);
}

/* Stack once the two columns stop being readable side by side. */
@media (max-width: 1100px) {
    .field-builder {
        grid-template-columns: minmax(0, 1fr);
    }
    .field-preview {
        position: static;
    }
}

/* ----- dashboard media cards ----- */
.dash-section {
    margin-top: 10px;
}

/* "Since your last visit" — a compact digest of unread notifications, so the
   dashboard opens with what happened to you, not just a wall of pictures. */
.dash-activity {
    margin-top: 12px;
    border: 1px solid var(--border);
    background: var(--panel-inset-bg);
    padding: 10px 14px;
}

.dash-activity h2 {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    border: 0;
    margin: 0 0 6px;
    padding: 0;
}

.activity-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 4px 22px;
}

.activity-list a {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
    text-decoration: none;
    color: var(--text);
}

.activity-list a:hover .activity-text {
    color: var(--link);
}

.activity-count {
    font-weight: 700;
    color: var(--accent);
}

/* New members: avatar + handle chips linking to each profile. */
.dash-members {
    display: flex;
    flex-wrap: wrap;
    gap: 10px 18px;
    margin-top: 12px;
}

.dash-member {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    text-decoration: none;
    color: var(--text);
}

.dash-member:hover .dash-member-name {
    color: var(--card-heading-hover);
}

.photo-grid--wide {
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
}

.media-card {
    display: flex;
    flex-direction: column;
    border: 1px solid var(--border);
    background: var(--card-bg);
}

.media-card-image {
    display: block;
    background: var(--image-bg);
}

/* Same reasoning as the album grid: a 140px landscape crop took the boots off
   the bottom of every standing shot, which is the part worth seeing. */
.media-card-image img {
    display: block;
    width: 100%;
    height: auto;
    aspect-ratio: var(--ratio, 4 / 3);
    object-fit: contain;
}

/* Approve / reject, side by side under a queued photo. The forms are separate
   POSTs rather than one form with two buttons, so neither can be submitted by
   pressing Enter in the other's context. */
.media-card-actions {
    display: flex;
    justify-content: center;
    gap: 6px;
    margin-top: 6px;
}

.media-card-actions form {
    margin: 0;
}

.media-card-meta {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 6px;
    font-size: 0.9rem;
}

.media-card-meta a {
    text-decoration: none;
}

.media-card-meta .muted {
    font-size: 0.85rem;
}

/* ----- prose measure -----
   Text needs a readable line length; photo grids and tables want the whole
   width. Two rules rather than one cap on #main. */
.prose {
    max-width: 78ch;
}

/* ----- thematic reaction (mucksplat) ----- */
.splat-form {
    margin: 0;
}

.splat-btn {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    width: 100%;
    padding: 3px 6px;
    font-family: inherit;
    font-size: 0.85rem;
    line-height: 1;
    color: var(--muted);
    background: var(--splat-bg);
    border: 1px solid var(--border);
    cursor: pointer;
}

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

.splat-btn--on {
    color: var(--on-accent);
    background: var(--accent);
    border-color: var(--accent-edge);
}

.splat-btn--on:hover {
    color: var(--on-accent);
    background: var(--accent-edge);
}

.splat-icon {
    display: inline-flex;
    flex: 0 0 auto;
}

/* An inlined icon has no intrinsic size — em units so it tracks the button's
   text rather than a number that drifts from it. */
.splat-icon svg {
    width: 1.15em;
    height: 1.15em;
    display: block;
}

.splat-count {
    margin-left: auto;
    font-weight: bold;
}

.photo-viewcount {
    display: block;
    font-size: 0.8rem;
    text-align: right;
    margin-top: 2px;
    padding: 0 2px;
}

/* ----- notification inbox ----- */
.badge-alert {
    background: var(--danger);
    color: var(--on-danger);
}

.notification-list {
    list-style: none;
    margin: 12px 0 0 0;
    padding: 0;
    border: 1px solid var(--border);
    background: var(--list-bg);
}

.notification {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    border-bottom: 1px solid var(--border-soft);
}

.notification:last-child {
    border-bottom: 0;
}

.notification--unread {
    background: var(--unread-bg);
    border-left: 3px solid var(--accent);
}

.notification-avatar {
    display: block;
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    object-fit: cover;
    border: 1px solid var(--border);
}

.notification-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.notification-time {
    font-size: 0.8rem;
    color: var(--muted);
}

/* ----- member cards (connections) ----- */
.member-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
    gap: 12px;
    margin-top: 12px;
}

.member-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    border: 1px solid var(--border);
    background: var(--tile-bg);
    padding: 8px;
}

.member-card-avatar {
    display: block;
    width: 96px;
    height: 96px;
    object-fit: cover;
    border: 1px solid var(--border);
}

.member-card-meta {
    flex: 1;
    padding: 6px 0;
    font-size: 0.9rem;
}

.member-card-actions {
    display: flex;
    gap: 4px;
}

.member-card-actions form {
    margin: 0;
}

.panel-attention {
    border-color: var(--accent);
    background: var(--panel-attention-bg);
    margin-bottom: 20px;
}

.panel-attention h2 {
    margin-top: 0;
    color: var(--accent);
}

/* ----- sidebar people lists (suggested / recent viewers) ----- */
.sidebar-people {
    padding: 6px 10px 2px 10px;
}

.sidebar-person-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    margin-bottom: 8px;
}

.sidebar-person-row form {
    margin: 0;
}

.sidebar-person {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    margin-bottom: 8px;
    text-decoration: none;
    color: inherit;
}

.sidebar-person-row .sidebar-person {
    flex: 1;
    margin-bottom: 0;
}

.sidebar-person:hover {
    text-decoration: none;
    color: var(--link);
}

.sidebar-person-avatar {
    display: block;
    width: 32px;
    height: 32px;
    flex-shrink: 0;
    object-fit: cover;
    border: 1px solid var(--sidebar-border);
}

.sidebar-person-text {
    display: flex;
    flex-direction: column;
    line-height: 1.2;
    min-width: 0;
}

.sidebar-person-name {
    font-weight: bold;
    font-size: 0.9rem;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}


/* ----- confirmation code entry ----- */
.code-input {
    font-family: "DejaVu Sans Mono", Consolas, monospace;
    font-size: 1.4rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    padding: 6px 8px;
    width: 100%;
    max-width: 12ch;
}

.field-help {
    margin-top: 4px;
    font-size: 0.85rem;
}

/* ----- dev mail preview ----- */
.mail-preview-row {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
    gap: 12px;
    margin-bottom: 22px;
}

.mail-preview {
    border: 1px solid var(--border);
    background: var(--dev-panel-bg);
}

.mail-preview-head {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 6px 8px;
    background: var(--bg-alt);
    border-bottom: 1px solid var(--border);
}

.mail-preview-locale {
    font-size: 0.75rem;
    font-weight: bold;
    text-transform: uppercase;
    color: var(--on-accent);
    background: var(--accent);
    padding: 1px 5px;
}

.mail-preview-body {
    margin: 0;
    padding: 10px 12px;
    font-family: "DejaVu Sans Mono", Consolas, monospace;
    font-size: 0.85rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
}

.mail-preview-foot {
    padding: 4px 8px;
    font-size: 0.75rem;
    border-top: 1px solid var(--border-soft);
}

.mail-spool {
    border: 1px solid var(--border);
    background: var(--dev-panel-bg);
    margin-bottom: 6px;
}

.mail-spool summary {
    padding: 6px 8px;
    cursor: pointer;
    font-family: "DejaVu Sans Mono", Consolas, monospace;
    font-size: 0.8rem;
}

/* Visually hidden but available to screen readers */
.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.lightbox {
    position: fixed;
    inset: 0;
    z-index: 2000;
    background: var(--lightbox-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    gap: 8px;
}

.lightbox-figure {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    margin: 0;
    min-width: 0;
    max-height: 100%;
}

/* The stage is what scrolls when magnified; the image grows past it.
   Zoomed it takes fixed viewport dimensions rather than max-* caps — the figure
   is a column flex container with align-items:center, which shrinks children to
   content width, so a cap alone would leave the stage at the fitted image size
   and the photo would grow inside a small window instead of the window becoming
   a viewport onto a large photo. */
.lightbox-stage {
    display: flex;
    align-items: center;
    justify-content: center;
    min-width: 0;
    overflow: hidden;
}

.lightbox-stage.is-zoomed {
    overflow: auto;
    width: 95vw;
    height: 86vh;
    align-self: stretch;
    cursor: grab;
    background: var(--lightbox-stage-bg);
    border: 1px solid var(--lightbox-stage-border);
    /* Scrollbars are chrome; panning is the interface. */
    scrollbar-width: thin;
}

.lightbox-stage.is-zoomed:active {
    cursor: grabbing;
}

/* Stop a magnified image being centred-and-clipped: it must start top-left so
   the whole of it is reachable by scrolling. */
.lightbox-stage.is-zoomed > .lightbox-img {
    margin: auto;
    flex: 0 0 auto;
}

.lightbox-caption {
    display: flex;
    align-items: baseline;
    gap: 10px;
    color: var(--lightbox-caption);
    font-size: 0.9rem;
}

.lightbox-counter {
    font-family: "DejaVu Sans Mono", Consolas, monospace;
    color: var(--lightbox-counter);
}

.lightbox-owner {
    color: var(--lightbox-owner);
}

/* Big hit areas: this is a lean-back, one-hand-on-the-arrow-keys interface. */
.lightbox-nav {
    flex: 0 0 auto;
    width: 52px;
    height: 96px;
    font-size: 2.4rem;
    line-height: 1;
    color: var(--lightbox-nav-fg);
    background: var(--lightbox-nav-bg);
    border: 1px solid var(--lightbox-nav-border);
    cursor: pointer;
}

.lightbox-nav:hover {
    background: var(--lightbox-nav-hover-bg);
    color: var(--lightbox-nav-hover-fg);
}

/* Cap the chrome, never the photo. The lightbox deliberately ignores
   --shell-max and uses the whole screen: full-size viewing on a real monitor is
   the point of the site, and it is what the lossless EXIF handling is for.
   width:auto keeps a small photo at its natural size rather than upscaling it
   into blur. */
.lightbox-img {
    /* Sized against what is actually free, not against a round fraction of
       the viewport: the overlay's own 16px padding, the caption row under the
       photo and the gap above it are the only things that must fit beside it.
       95vw/92vh left a band of dead space at every window size for no reason
       anybody could point at. */
    max-width: calc(100vw - 32px);
    max-height: calc(100vh - 72px);
    width: auto;
    height: auto;
    border: 2px solid var(--lightbox-img-border);
    background: var(--lightbox-stage-bg);
    cursor: zoom-in;
}

/* Magnified. The caps come off so the image can overflow the stage and be
   panned — this is where the close-up detail actually lives. */
.lightbox-img.is-zoomed {
    max-width: none;
    max-height: none;
    height: auto;
    border-width: 0;
    cursor: zoom-in;
    /* Show the actual pixels rather than smoothing them away at high zoom. */
    image-rendering: -webkit-optimize-contrast;
}

.lightbox-hint {
    color: var(--lightbox-hint);
    font-size: 0.85rem;
}

.lightbox-zoom {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.lightbox-zoom button {
    width: 22px;
    height: 22px;
    font-size: 1rem;
    line-height: 1;
    color: var(--lightbox-nav-fg);
    background: var(--lightbox-btn-bg);
    border: 1px solid var(--lightbox-btn-border);
    cursor: pointer;
}

.lightbox-zoom button:hover {
    background: var(--lightbox-btn-hover-bg);
    color: var(--lightbox-btn-hover-fg);
}

.lightbox-zoom-level {
    min-width: 4ch;
    text-align: center;
    font-family: "DejaVu Sans Mono", Consolas, monospace;
    color: var(--lightbox-level);
}

.lightbox-close {
    position: fixed;
    top: 16px;
    right: 16px;
}

/* Profile */
.profile-header {
    display: flex;
    gap: 14px;
    align-items: flex-start;
}

/* Where you are inside a member's pages — a band between the identity header
   and the content, which is what tells you an album is a place *within*
   somebody's profile rather than a page somewhere else. The band is made of
   space, not of rules: it used to be fenced by two hairlines that were only
   ever invisible because they matched the ground, so the first theme change
   turned them into lines nobody asked for. */
.crumbs {
    margin: 10px 0 14px;
    padding: 8px 14px;
    font-size: 0.9em;
    /* Crumb links are green outright, not link-grey-turning-green: the strip
       is a trail of where-to-go, so the affordance is the colour, and the
       hover step darkens it instead of shifting the hue (the window theme's
       default pair, which the admin identity swap leaves untouched). */

    /* The strip wears the same surface as the head bars under it — a tonal
       step off the page (--forum-head-bg) behind a hairline — so the trail
       reads as a band of its own instead of text floating on the ground. */
    background: var(--forum-head-bg);
    border: 1px solid var(--border);
}

.crumbs a {
    color: var(--link);
}

.crumbs a:hover {
    color: var(--link-hover);
}

.crumbs-sep {
    margin: 0 6px;
    color: var(--muted);
}

.crumbs-current {
    color: var(--link);
}

.avatar-sm,
.avatar-lg {
    display: inline-block;
    object-fit: cover;
    background: var(--image-bg);
    border-radius: 0;
}

.avatar-sm {
    width: 36px;
    height: 36px;
    vertical-align: middle;
}

.avatar-lg {
    width: 96px;
    height: 96px;
    flex-shrink: 0;
}

/* The house placeholder: a boot with a question mark, traced from the
   artwork into ~1.7 KB of SVG so it is crisp at 32px in the sidebar and at
   128px on a profile header, from one file.

   The SVG has no background of its own — the shape is drawn on transparency
   and the surrounding --empty-bg shows through, which is what lets one asset
   sit on the avatar tint and on the album tint without a seam.

   `contain` rather than `cover`: an avatar slot is square and so is the
   viewBox, but a cover would crop the toe off the moment either stops being
   square. */
.avatar-placeholder {
    background-color: var(--empty-bg);
    background-image: url('../images/boot-placeholder.svg');
    background-repeat: no-repeat;
    background-position: center;
    background-size: contain;
}

.field-block {
    margin-bottom: 12px;
}


/* Also matches an <a class="linkish">: the forum's post actions are a mixed
   row of links (edit) and one-button forms (remove), and they have to be one
   another's twins or the row reads as two different kinds of control. */
.linkish,
button.linkish {
    font-family: inherit;
    font-size: inherit;
    color: var(--link);
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    text-decoration: none;
}

.linkish.danger,
button.linkish.danger {
    color: var(--btn-warn-bg);
}

/* A form is a block, so a row of one-button forms stacked vertically — the
   class was on three templates already and had never had a rule, which is why
   "lock" and "remove" sat on separate lines pretending to be unrelated. */
.inline-form {
    display: inline-block;
    margin-right: 10px;
}


#sidebar {
    position: fixed;
    top: var(--nav-h);
    left: var(--shell-gutter);
    bottom: 0;
    width: var(--sidebar-w);
    background: var(--sidebar-bg);
    overflow-y: auto;
    z-index: 90;
    font-size: 1rem;
}

.sidebar-section {
    padding: 8px 0;
}

/* Elements Alpine will take over (x-show) are hidden from the very first
   paint so collapsed sections never flash open before Alpine initializes.
   Alpine removes x-cloak when it binds the element. */
[x-cloak] {
    display: none !important;
}

/* Headings are signposts, not entries. They used to be set at nearly the
   weight of the links they label — caps, bold, only a shade quieter — and
   four of them stacked read as a table of contents rather than a nav. Smaller
   and dimmer, with a hairline above each section after the first doing the
   separating, they sit behind the links instead of competing with them. */
.sidebar-heading {
    font-weight: bold;
    font-size: 0.72rem;
    text-transform: uppercase;
    color: var(--sidebar-heading);
    opacity: 0.75;
    padding: 2px 10px 5px 10px;
    letter-spacing: 0.08em;
}

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

/* Headings are toggle buttons (collapsible sections). Resets the UA button
   chrome so they read exactly like the old div headings; the chevron hints
   at the collapse affordance without shouting (D1: aged look, modern
   ergonomics underneath). */
button.sidebar-heading {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    width: 100%;
    margin: 0;
    border: 0;
    background: none;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
}

button.sidebar-heading:hover {
    color: var(--sidebar-link);
}

.collapse-chevron {
    display: inline-block;
    font-size: 0.7em;
    line-height: 1;
    transition: transform 0.15s ease;
}

/* The chevron follows the button's aria-expanded state, which the server
   seeds and Alpine keeps updated — so the rotation is correct from the
   first paint and never flickers at Alpine init. */
button.sidebar-heading[aria-expanded="false"] .collapse-chevron {
    transform: rotate(-90deg);
}

/* Badge + chevron sit together at the heading's right edge, badge left of
   the chevron (the button is flex space-between, so the text stays left).
   The badge reuses the shell's .badge tally look; badge-empty (JS) hides
   it until a collapsed section actually has something to hoist. */
.section-heading-end {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.section-heading-badge {
    margin-right: 0;
}

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

/* Row = icon cell, label, then whatever tally the item carries pushed to the
   right by the label's flex-grow (space-between cannot do that once there are
   three children). */
.sidebar-nav a {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 10px 5px 11px;
    color: var(--sidebar-link);
    text-decoration: none;
    border-left: 3px solid transparent;
}

.sidebar-nav .nav-label {
    flex: 1 1 auto;
    min-width: 0;
}

/* Bands inside one section (the ADMIN list: queue · people · site · ops).
   Unlabelled on purpose — eleven rows need grouping, but four more headings
   would put back exactly the furniture the sidebar just lost. Inset from the
   left so it reads as a rule under the labels, not a border on the section. */
.sidebar-nav .nav-divider {
    height: 0;
    margin: 5px 10px 5px 24px;
    border-top: 1px solid var(--border-soft);
}

/* Fixed cell so labels line up whether or not a module named a glyph, and
   dimmer than the label: the icon is there to be recognised at a glance, not
   read. The active row brings its icon up with it. */
.nav-icon {
    flex: 0 0 auto;
    width: 1.15em;
    font-size: 1.05em;
    text-align: center;
    opacity: 0.65;
}

.sidebar-nav a:hover .nav-icon,
.sidebar-nav a.active .nav-icon,
.sidebar-nav a.is-active .nav-icon {
    opacity: 1;
}

.sidebar-nav a:hover {
    background: var(--sidebar-hover-bg);
    text-decoration: none;
}

.sidebar-nav a.active {
    background: var(--sidebar-active-bg);
    color: var(--sidebar-active-fg);
    border-left-color: var(--accent);
    font-weight: bold;
}

#main-wrap {
    margin-left: calc(var(--shell-gutter) + var(--sidebar-w));
    margin-right: var(--shell-gutter);
    margin-top: var(--nav-h);
    height: calc(100vh - var(--nav-h));
    overflow-y: auto;
    background: var(--main-wrap-bg);
    padding: var(--main-pad);
    /* The top gap is padding inside the scroll container, not margin: as margin
       it would sit above the scrollport and the first card would still start
       flush against the bar once the page scrolled. */
    padding-top: var(--content-gap);
    /* Fixed low-opacity farm boot watermark behind main content */
    position: relative;
}

#main-wrap::before {
    content: "";
    position: fixed;
    /* Nora Anton-style boot watermark: bottom-right of content area */
    top: auto;
    left: auto;
    right: 16px;
    bottom: 12px;
    width: min(58vmin, 480px);
    height: min(78vmin, 640px);
    background: url("../images/boot-bg.png") right bottom / contain no-repeat;
    opacity: 0.34;
    pointer-events: none;
    z-index: 0;
}

/* Every page's first element starts on the --content-gap line, whatever it is
   (a heading, a .dash-section, the profile header, the admin banner). Without
   this each page's own top margin stacked on top of the gap, so every page
   started at a different height and none of them lined up with the sidebar.
   Both levels: the admin banner is a child of the wrap, everything else of
   #main. */
#main-wrap > *:first-child,
#main > *:first-child {
    margin-top: 0;
}

/* Deliberately no z-index here. `position: relative` alone is enough to lift
   content above the watermark (::before is the first child, so DOM order wins),
   and adding a z-index would create a stacking context that traps the
   full-screen lightbox underneath the navbar and sidebar. */
#main-wrap > * {
    position: relative;
}

#main {
    /* Deliberately see-through: the boot watermark is pinned to the viewport
       while this panel scrolls, so content drifts over a fixed background and
       the boot shows through. Keep it light enough that body text stays
       comfortable over the pale parts of the image. */
    background: var(--main-bg);
    padding: 12px 14px;
    /* Fill the viewport rather than stopping at the content: a short page should
       not leave a sea of grey under a floating white box. */
    min-height: calc(100vh - var(--nav-h) - var(--content-gap) - var(--main-pad) - 2px);
}

/* ----- panels / tables / forms ----- */
.panel {
    background: var(--panel-bg);
    border: 1px solid var(--border);
    padding: 14px 16px;
}

.panel-inset {
    background: var(--panel-inset-bg);
    border: 1px solid var(--panel-inset-border);
    padding: 10px 12px;
    margin: 10px 0;
}

.lead {
    font-size: 1.05rem;
    margin-bottom: 14px;
}

.muted {
    color: var(--muted);
    font-size: 0.95rem;
}

.info-table {
    width: 100%;
    border-collapse: collapse;
    margin: 12px 0;
    font-size: 1rem;
}

.info-table th,
.info-table td {
    padding: 5px 8px;
    text-align: left;
    vertical-align: top;
}

.info-table th {
    width: 120px;
    background: var(--table-head-bg);
    color: var(--text);
    font-weight: bold;
}

.info-table tbody tr:nth-child(even) td,
.info-table > tr:nth-child(even) td {
    background: var(--stripe-bg);
}

.actions {
    margin: 16px 0 10px 0;
}

.btn,
button.btn,
input[type="submit"].btn {
    display: inline-block;
    font-size: 1rem;
    color: var(--btn-fg);
    background: var(--btn-bg);
    border: 1px solid var(--btn-border);
    padding: 5px 12px;
    text-decoration: none;
    cursor: pointer;
    border-radius: 0;
}

.btn:hover {
    background: var(--btn-hover-bg);
    color: var(--btn-hover-fg);
    text-decoration: none;
}

/* ----- "you are browsing as a member" ------------------------------------
   A chip inside the fixed navbar (see partials/_view_as_bar.twig for why it
   is not a band under it). Warm rather than red: this is a state the operator
   asked for, not a fault — the red identity is already spoken for by the
   admin area, and reusing it here would make an ordinary preview look like an
   alarm. It sits in the middle of the bar, hard to miss and impossible to
   confuse with the account menu it sits beside. */
.viewas-chip {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-left: 16px;
    padding: 0 10px;
    height: calc(var(--nav-h) - 12px);
    line-height: 1;
    background: var(--panel-attention-bg);
    border: 1px solid var(--warn);
    color: var(--warn);
    font-size: 0.8rem;
    white-space: nowrap;
}

.viewas-label {
    font-weight: bold;
}

/* Not a .btn: a full button inside a 38px bar makes the bar look broken. It
   is underlined at rest precisely because it is the way out and should read
   as the one thing here you can press. */
.viewas-exit {
    font: inherit;
    color: var(--warn);
    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    text-decoration: none;
}

.viewas-exit:hover {
    color: var(--danger);
}

/* The low-emphasis button. The class was on nine templates with no rule
   behind it, so every "secondary" button — the forum's thread controls, the
   cancel links, the report submit, the two admin report actions — was
   rendering as a full-weight .btn, which is why a row of them had no
   hierarchy in it. Transparent ground, softer text; it takes the ordinary
   button surface on hover, so it still announces itself as pressable.

   No !important, unlike .btn-warn and .btn-sm above: those sit *before* the
   .btn block and have to shout over it. This one sits after and simply wins. */
.btn-quiet {
    background: transparent;
    border-color: var(--border);
    color: var(--muted);
}

.btn-quiet:hover {
    background: var(--btn-bg);
    border-color: var(--btn-border);
    color: var(--btn-hover-fg);
}

/* The destructive one in a row of quiet ones. A solid --btn-danger block
   beside three ghost buttons shouts, and the row stops reading as one set of
   controls; the red belongs on the word, not behind it. It still fills in on
   hover, which is the point at which the warning should get louder. */
.btn-quiet.btn-quiet--danger {
    color: var(--danger);
}

.btn-quiet.btn-quiet--danger:hover {
    background: var(--danger);
    border-color: var(--danger);
    color: var(--on-danger);
}

/* An icon inside a button: the label's own whitespace is the gap, so this
   only has to fix the baseline. */
.btn .icon {
    vertical-align: -0.08em;
    opacity: 0.85;
}

/* The foreground belongs here, with the background it has to sit on.
   Without it these kept .btn's green text on a dark green fill — 1.08:1,
   which is not low contrast, it is no contrast. It looked fine only because
   `body.layout-app` sets white further down, and the guest shell is
   layout-guest: the fix existed and never reached the one page a stranger
   can see, where this is the primary call to action (F101).

   Doubled class on purpose. These buttons are anchors, and the global
   `a:link` rule is (0,1,1) because a pseudo-class scores as a class — so a
   bare `.btn-ok` at (0,1,0) loses to it and the white is silently dropped.
   `.btn.btn-ok` is (0,2,0) and wins, which is also why the layout-app rule
   appeared to work: it was specific enough by accident. Every use in the
   templates already pairs the two classes. */
.btn.btn-accept,
.btn.btn-ok {
    background: var(--ok);
    border-color: var(--btn-ok-border);
    color: #ffffff;
}

/* forms (for later auth pages) */
label {
    display: block;
    font-weight: bold;
    margin: 8px 0 3px 0;
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
textarea,
select {
    font-size: 1rem;
    border: 1px solid var(--input-border);
    padding: 4px 6px;
    width: 100%;
    max-width: 360px;
    background: var(--input-bg);
    color: var(--input-fg);
    border-radius: 0;
}

/* flash */
/* ----- toasts (top-right, stackable, auto-dismiss) -----
   Modern behaviour, 2005 surface: sharp corners, a solid dark panel, one
   colour-coded left edge by kind. Fixed to the viewport so they float over
   content; the stack grows downward, newest on top. */
/* ----- top progress bar ----- */
/* A thin line across the very top, for work the member is waiting on: a form
   that sends mail (~2s of relay conversation) or an HTMX swap that replaces
   #main. partials/_progress_bar.twig decides WHEN — the rule is narrow on
   purpose, because this shell polls every 15 and 30 seconds and a bar that
   twitches at rest is one nobody reads.

   z-index above .toast-stack (3000), the previous maximum: this is the one
   element that must never be hidden by whatever else is on screen. It occupies
   the 3px nothing else uses, so it covers nothing in return.

   Width and opacity are driven from JS; everything else lives here. Motion is
   transition-only — there are no @keyframes anywhere in this file and this is
   not the place to introduce the first. */
.top-progress {
    position: fixed;
    top: 0;
    left: 0;
    width: 0;
    height: 3px;
    background: var(--progress);
    opacity: 0;
    z-index: 3100;
    pointer-events: none;
}

.toast-stack {
    position: fixed;
    top: calc(var(--nav-h) + 10px);
    right: 14px;
    z-index: 3000;
    display: flex;
    flex-direction: column;
    gap: 8px;
    width: min(340px, calc(100vw - 28px));
    pointer-events: none;
}

.toast {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 12px;
    background: var(--toast-bg);
    color: var(--toast-fg);
    border: 1px solid var(--border);
    border-left: 4px solid var(--muted);
    box-shadow: 0 3px 10px var(--toast-shadow);
    pointer-events: auto;
    /* Enter from the right; is-in/is-out toggle the animation. */
    transform: translateX(120%);
    opacity: 0;
    transition: transform 0.22s ease, opacity 0.22s ease;
}

.toast.is-in {
    transform: none;
    opacity: 1;
}

.toast.is-out {
    transform: translateX(120%);
    opacity: 0;
}

.toast-success { border-left-color: var(--toast-ok); }
.toast-error { border-left-color: var(--danger-strong); }
.toast-info { border-left-color: var(--info); }

.toast-msg {
    flex: 1;
    line-height: 1.35;
    font-size: 0.95rem;
}

.toast-x {
    flex-shrink: 0;
    background: none;
    border: none;
    color: var(--muted);
    font-size: 18px;
    line-height: 1;
    padding: 0 2px;
    cursor: pointer;
}

.toast-x:hover { color: var(--text); }

/* Inline form-validation errors (login failed, bad field, …). These stay in
   place on the form — a toast is wrong for "your password was wrong". Same dark
   surface + colour-coded edge as a toast, just not floating. */
.flash {
    border: 1px solid var(--border);
    border-left: 4px solid var(--muted);
    background: var(--toast-bg);
    color: var(--toast-fg);
    padding: 8px 10px;
    margin-bottom: 10px;
}
.flash-error { border-left-color: var(--danger-strong); }

/* ----- handle rename marker + banner -----
   The tag is the quiet marker on the new handle (glyph + hover tooltip,
   "formerly: X"); the banner is the loud notice on profile/gallery pages
   while the redirect window is open. Both are theme-aware via tokens. */
.handle-tag {
    color: var(--accent);
    font-size: 1.3em;
    vertical-align: super;
    cursor: help;
    font-variant-emoji: text;
}
.handle-banner {
    border: 1px solid var(--border);
    border-left: 4px solid var(--info);
    background: var(--toast-bg);
    color: var(--toast-fg);
    padding: 8px 10px;
    margin-bottom: 10px;
}

/* htmx indicator (subtle) */
.htmx-request .btn {
    opacity: 0.7;
}

/* ----- profile field groups ----- */
/* Profile: a photo-first two-column body. The member's albums are the wide
   right column (their work is who they are); the info fields sit in a compact
   left rail instead of full-bleed tables floating in a widescreen void. Below
   860px the two columns stack so it survives a small screen. */
.profile-avatar-link {
    display: inline-block;
    cursor: pointer;
}

.profile-header .avatar-lg,
.profile-header .avatar-placeholder {
    width: 128px;
    height: 128px;
}

.profile-ident {
    min-width: 0;
}

.profile-loc {
    margin: 2px 0 10px;
}

.profile-bio {
    margin-top: 12px;
}

.profile-body {
    display: grid;
    grid-template-columns: minmax(240px, 300px) 1fr;
    gap: 24px;
    margin-top: 18px;
    align-items: start;
}

.profile-aside .profile-group:first-child {
    margin-top: 0;
}

.profile-gallery h2 {
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    padding-bottom: 4px;
    margin: 0;
}

.profile-gallery-all {
    margin-top: 12px;
}

@media (max-width: 860px) {
    .profile-body {
        grid-template-columns: 1fr;
    }
}

.profile-group {
    margin-top: 18px;
}

.profile-group h2 {
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    padding-bottom: 4px;
    margin-bottom: 0;
}

.profile-group .info-table {
    margin-top: 0;
}

/* Marks a row the viewer can see but that is not public. Only ever rendered to
   someone already allowed to see it. */
.field-lock {
    color: var(--muted);
    font-size: 0.7rem;
    vertical-align: super;
}

.tag-list {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
}

.tag {
    display: inline-block;
    padding: 1px 7px;
    font-size: 0.85rem;
    background: var(--bg-alt);
    border: 1px solid var(--border);
}

/* ----- member directory filters ----- */
.directory-filters {
    margin-bottom: 12px;
}

.filter-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px 14px;
}

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

.filter-cell label {
    font-size: 0.8rem;
    color: var(--muted);
    margin: 0;
}

.filter-cell select,
.filter-cell input[type="text"] {
    min-width: 140px;
    padding: 2px 4px;
}

.directory-filters .actions {
    margin: 10px 0 0 0;
}

.filter-group {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-top: 8px;
    padding-top: 8px;
    border-top: 1px dotted var(--border);
}

.filter-group-label {
    flex: 0 0 8.5rem;
    padding-top: 2px;
    font-size: 0.8rem;
    font-weight: bold;
    text-transform: uppercase;
    color: var(--muted);
}

.filter-cell select.is-active {
    border-color: var(--accent);
    background: var(--bg-alt);
    font-weight: bold;
}

/* ----- sortable (partials/_sortable.twig) ----- */
[data-sortable-item] {
    cursor: grab;
}

[data-sortable-item].is-dragging {
    opacity: 0.4;
    cursor: grabbing;
}

.sortable-handle {
    color: var(--muted);
    cursor: grab;
    user-select: none;
    padding-right: 4px;
}

[data-sortable-container].is-saving {
    outline: 1px dashed var(--muted);
}

[data-sortable-container].is-saved {
    outline: 1px solid var(--ok);
}

[data-sortable-container].is-failed {
    outline: 2px solid var(--danger);
}

/* An empty group still needs to be a drop target. */
[data-sortable-container]:empty::after {
    content: attr(data-sortable-empty);
    display: block;
    padding: 10px;
    color: var(--muted);
    font-size: 0.85rem;
    border: 1px dashed var(--border);
}

/* ----- admin field list (draggable) ----- */
.field-group {
    margin-top: 16px;
}

.field-group h2 {
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    border-bottom: 1px solid var(--border);
    padding-bottom: 4px;
    margin-bottom: 6px;
}

.field-list {
    list-style: none;
    margin: 0;
    padding: 0;
    min-height: 2.4rem;
}

.field-row,
.cat-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 5px 8px;
    border: 1px solid var(--border);
    background: var(--field-row-bg);
    margin-bottom: 3px;
}

.field-row.is-disabled {
    opacity: 0.55;
}

.field-row-main,
.cat-row-main {
    flex: 1 1 auto;
    min-width: 0;
}

.field-row-main code,
.cat-row-main code {
    margin-left: 6px;
    font-size: 0.8rem;
}

.field-row-meta,
.cat-row-meta {
    flex: 0 0 auto;
    font-size: 0.85rem;
}

.field-row-actions,
.cat-row-actions {
    flex: 0 0 auto;
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 0.85rem;
}

.field-row-actions form,
.cat-row-actions form {
    display: inline;
    margin: 0;
}

/* The admin category list: a list of lists, one level deep. Rows are the
   field rows above — same drag component, same shape — so only the nesting
   and its drop target are new here. */
.cat-list,
.cat-children {
    list-style: none;
    margin: 0;
    padding: 0;
}

.cat-children {
    /* Indented and always a drop target, even when empty: an empty parent you
       cannot drop into is a sub-forum you cannot create. */
    margin: 0 0 3px 28px;
    min-height: 1.9rem;
}

.cat-danger {
    margin-top: 16px;
}

/* An admin action: what it does on the left, the control on the right.
   One row per decision, so a panel of them reads as a list of choices rather
   than a heap of buttons with sentences drifting between them. */
.admin-actions {
    margin-top: 16px;
}

.admin-actions h2 {
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    margin: 0 0 10px;
}

.admin-action {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 9px 0;
}

/* Rows sit against each other, so they get air rather than a rule between
   them — the panel already has an edge and does not need internal ones. */
.admin-action + .admin-action {
    padding-top: 10px;
}

.admin-action-text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.admin-action-text .muted {
    font-size: 0.85rem;
}

.admin-action form {
    flex: 0 0 auto;
    margin: 0;
}

/* Where a button would be if the action were available. Right-aligned with
   the buttons above it so the column stays a column. */
.admin-action-blocked {
    flex: 0 0 auto;
    font-size: 0.85rem;
}

/* Move-all: target and button are one control. */
.cat-move {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.cat-move select {
    width: auto;
    max-width: 260px;
}

@media (max-width: 700px) {
    .admin-action {
        flex-direction: column;
        align-items: flex-start;
        gap: 8px;
    }
}

.cat-danger h2 {
    font-size: 0.9rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    margin: 0 0 6px;
}

/* A sub-category on the forum front page: the same row, one step in. The
   whole cell moves, not just the link — an indented name with its subheading
   still flush left reads as two different rows. */
.forum-cat-sub td:first-child {
    padding-left: 30px;
}

.forum-cat-sub .forum-cat-name::before {
    content: "└ ";
    color: var(--muted);
    margin-left: -14px;
}

.forum-subforums {
    margin-bottom: 18px;
}

/* While managing an album the tiles become draggable. */
.photo-tile[data-sortable-item] {
    cursor: grab;
}

/* Group heading actions sit on one line: a block-level form would wrap the
   delete link onto its own row. */
.field-group h2 form {
    display: inline;
    margin: 0;
}

.field-group h2 .group-rename {
    display: inline-flex;
    align-items: baseline;
    gap: 6px;
}

.field-group h2 .group-rename input {
    font-size: 0.9rem;
    padding: 2px 4px;
    max-width: 180px;
}

.group-count {
    font-weight: normal;
    font-size: 0.85rem;
}

.group-add {
    margin-top: 18px;
}

/* Shared pager — every paginated list on the site renders through this.
   Numbered rather than "load more": principle 1 rules out infinite scroll,
   and a numbered page is the only kind you can leave and come back to. */
.pager {
    display: flex;
    align-items: center;
    gap: 10px;
    flex-wrap: wrap;
    margin-top: 16px;
    padding: 8px 10px;
    border: 1px solid var(--border);
    background: var(--pager-bg);
    font-size: 13px;
}

.pager-numbers {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.pager-num,
.pager-step {
    padding: 3px 8px;
    border: 1px solid transparent;
    text-decoration: none;
}

.pager-num:hover,
.pager-step:hover {
    border-color: var(--border);
    background: var(--pager-hover-bg);
}

/* The current page is not a link, so it needs to look like the anchor of the
   row rather than just an un-clickable gap in it. */
.pager-num.is-current {
    border-color: var(--border);
    background: var(--accent);
    color: var(--pager-current-fg);
    font-weight: 600;
}

.pager-num.is-current:hover {
    background: var(--accent);
}

/* Ends of the list keep their arrows in place rather than collapsing, so the
   numbers do not shift sideways as you page through. */
.pager-step.is-off {
    opacity: 0.35;
    cursor: default;
}

.pager-gap {
    padding: 3px 2px;
    opacity: 0.5;
}

.pager-count {
    margin-left: auto;
}

/* 0.B.3 — honeypot decoy field. Never visible, never focusable: a human
   cannot tick it, so any submitted value marks a bot. Off-screen rather
   than display:none so naive bots that skip hidden elements still walk
   into the decoy. */
.hp {
    position: absolute !important;
    left: -9999px !important;
    width: 1px !important;
    height: 1px !important;
    opacity: 0 !important;
    pointer-events: none !important;
    border: 0 !important;
    padding: 0 !important;
    margin: 0 !important;
}

/* ----- member directory: card grid (the lurker's main surface) ----- */
.member-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
    gap: 14px;
    margin-top: 12px;
}
.member-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    padding: 14px 12px 16px;
    border: 1px solid var(--border);
    background: var(--tile-bg);
    color: var(--text);
    text-decoration: none;
}
.member-card:hover {
    border-color: var(--accent);
    background: var(--card-hover-bg);
    color: var(--card-hover-fg);
    text-decoration: none;
}
.member-card-avatar {
    width: 120px;
    height: 120px;
    object-fit: cover;
    border: 1px solid var(--border);
    margin-bottom: 10px;
}
.member-card-name {
    font-weight: bold;
    line-height: 1.2;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--card-heading);
}
.member-card:hover .member-card-name { color: var(--card-heading-hover); }
.member-card-loc {
    font-size: 0.8rem;
    color: var(--muted);
    margin-top: 2px;
}
.member-card-chips {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
    margin-top: 8px;
}
.member-chip {
    font-size: 0.72rem;
    padding: 1px 6px;
    border: 1px solid var(--border);
    background: var(--chip-bg);
    color: var(--chip-fg);
    white-space: nowrap;
}

/* Sidebar suggestion overflow fix: the @handle sub-line had no ellipsis, so it
   spilled under the add button in the 194px rail. */
.sidebar-person-row form .btn {
    flex-shrink: 0;
    white-space: nowrap;
}

/* Directory filters: search + collapse toggle on one line, panel hidden. */
.filter-topbar {
    display: flex;
    align-items: flex-end;
    gap: 8px;
    flex-wrap: wrap;
}
.filter-topbar .filter-cell { flex: 1; min-width: 180px; }
.filter-topbar input { width: 100%; }
.filter-toggle { display: inline-flex; align-items: center; gap: 6px; }
.filter-badge {
    display: inline-block;
    min-width: 16px;
    padding: 0 4px;
    background: var(--accent);
    color: var(--on-accent);
    font-size: 0.72rem;
    text-align: center;
    border-radius: 2px;
}
.filter-panel { margin-top: 12px; }

/* ----- admin insights ----- */
.insight-tiles {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin: 8px 0 4px;
}
.insight-tile {
    flex: 1 1 120px;
    border: 1px solid var(--border);
    background: var(--bg-alt);
    padding: 10px 12px;
    text-align: center;
}
.insight-tile-n { display: block; font-size: 1.6rem; font-weight: bold; }
.insight-tile-l { display: block; font-size: 0.8rem; color: var(--muted); }
/* Horizontal bars: a coloured fill + the number, no charting library. */
.insight-funnel td, .insight-activity td { position: relative; }
.insight-bar {
    display: inline-block;
    height: 14px;
    background: var(--accent);
    vertical-align: middle;
    min-width: 1px;
    opacity: 0.55;
}
.insight-bar-n { margin-left: 6px; font-variant-numeric: tabular-nums; }
.insight-activity th { width: 96px; font-weight: normal; color: var(--muted); }

/* ----- admin: red-tinted navbar (last, so it wins over the dark navbar) ----- */
/* The staff skin is driven by ONE thing: the body class. Everything that
   marks a page as staff work is rendered always and revealed here, so a
   swapped page can put the marks on and take them off without the server
   re-rendering the shell (F98). The alternative — the server emitting these
   per page — cannot work when only #main is replaced. */
.admin-banner,
.logo-admin-suffix { display: none; }
body.layout-admin .admin-banner { display: block; }
body.layout-admin .logo-admin-suffix { display: inline; }
/* And the members' furniture steps back: a work surface is not a page of the
   site, so the brand spine and the legal footer sit it out. */
body.layout-admin .brand-wordmark,
body.layout-admin .app-foot { display: none; }

body.layout-admin .admin-banner {
    background: var(--admin-banner-bg);
    color: var(--admin-banner-fg);
    border: 1px solid var(--admin-banner-border);
    padding: 6px 10px;
    margin-bottom: 10px;
    font-size: 0.95rem;
    font-weight: bold;
}

/* ----- admin appeal thread ----- */
.appeal-msg {
    margin-bottom: 10px;
    padding: 8px 10px;
    border: 1px solid var(--border);
    background: var(--bg-alt);
}
/* Staff messages get a faint red tint so the two sides read apart at a glance;
   applicant stays the neutral panel tone. Both inherit the shell's light text. */
body.layout-admin .appeal-msg-staff { background: var(--admin-appeal-staff-bg); border-color: var(--admin-appeal-staff-border); }
.appeal-msg-meta { font-size: 0.85rem; margin-bottom: 4px; }
.appeal-msg-body { white-space: pre-wrap; }

/* Moderation card removing itself after an async approve/reject. */
.media-card.htmx-swapping {
    opacity: 0;
    transition: opacity 0.15s ease;
}

/* Rich toast: a thumbnail + actor line, the body clickable through to the item. */
.toast-rich { padding: 8px; gap: 8px; }
.toast-thumb {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    object-fit: cover;
    border: 1px solid var(--border);
    background: var(--toast-thumb-bg);
}
.toast-body {
    flex: 1;
    min-width: 0;
    color: inherit;
    text-decoration: none;
    display: block;
}
a.toast-body:hover { text-decoration: none; }
a.toast-body:hover .toast-actor { color: var(--accent); }
.toast-icon { margin-right: 4px; }
.toast-actor { font-weight: bold; }

/* Notification thumbnail (rich inbox rows). */
.notification-thumb {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    object-fit: cover;
    border: 1px solid var(--border);
}
.notification-icon { margin-right: 4px; }

/* ============================ SGO THEME START ============================
   Structural rules for the member/admin shell: the glass panels, chrome
   composition (transparent navbar, content-on-grey), the boot watermark, the
   vertical brand wordmark, and the Nunito font. These only CONSUME tokens —
   the palette itself lives in themes/default.css (the admin-red overrides are
   in that file too, on body.layout-admin). No colour tokens are defined here. */

@font-face {
    font-family: 'Nunito';
    src: url('../fonts/nunito.woff2') format('woff2');
    font-weight: 200 1000;         /* variable axis — weight is a second accent */
    font-style: normal;
    font-display: swap;
}

/* Arial wearing Nunito's measurements. On a hard reload the fallback paints
   first and Nunito swaps in a beat later; untuned, that swap changed every
   text width by ~2% — enough to shunt the right-anchored navbar cluster
   sideways. size-adjust is the canvas-measured Nunito/Arial width ratio, the
   vertical overrides are Nunito's metrics rescaled by it, so the swap lands
   on the same pixels and moves nothing. */
@font-face {
    font-family: 'Nunito-fallback';
    src: local('Arial');
    size-adjust: 102.2%;
    ascent-override: 98.9%;
    descent-override: 34.5%;
    line-gap-override: 0%;
}

/* Minimalist icon font: Tabler Icons 3.31.0 (MIT, github.com/tabler/tabler-icons),
   subset to 78 glyphs — the woff2 is ~13KB.

   The subset is deliberately WIDER than what is used today. It grew from 21
   the first time an admin screen wanted three icons that were not in it, and
   picking a wrong-but-present glyph is how an icon set starts lying. What it
   is not is the whole font: all 5880 Tabler icons subset to **756KB**, which
   is 57× this file for glyphs nothing will ever name — the same trade this
   project already refused when it traced the boot placeholder into 1.7KB of
   SVG rather than shipping the 1.7MB PNG.

   TO ADD AN ICON: a .icon-* class pointing at a codepoint outside the list
   below renders a blank box. Download the full Tabler webfont (the upstream
   tabler-icons.woff2 plus its CSS, which is where names map to codepoints),
   add the codepoint to the list, and regenerate with the WHOLE list — this
   replaces the file rather than adding to it, and dropping one silently
   breaks whatever used it:

     pyftsubset tabler-icons.woff2 --flavor=woff2 \
       --output-file=public/assets/fonts/gsgo-icons.woff2 \
       --unicodes=10096,ea03,ea05,ea06,ea0b,ea16,ea19,ea1f,\
ea25,ea2e,ea35,ea53,ea54,ea5e,ea5f,ea60,\
ea61,ea62,ea67,ea6a,ea70,ea7a,ea94,ea95,\
ea96,ea99,ea9a,eaa5,eaa6,eaad,eabc,eabe,\
eabf,eac1,eac5,eac7,eade,eae1,eae2,eae5,\
eaef,eaf2,eb04,eb0a,eb0b,eb0d,eb13,eb1c,\
eb1e,eb20,eb24,eb2e,eb41,eb47,eb49,eb4b,\
eb4c,eb4d,eb55,eb5a,eb62,eb6b,eb77,eb8b,\
eba8,ebaa,ebf2,ec42,ec9c,ece9,ecf0,ecf9,\
ed5f,ed62,edba,ee88,efbe,f0ad

   Then check the navbar dropdown still has its three icons: a regeneration
   that loses a glyph looks fine on the page you were working on. */
@font-face {
    font-family: 'gsgo-icons';
    src: url('../fonts/gsgo-icons.woff2') format('woff2');
    font-weight: normal;
    font-style: normal;
    font-display: block;
}

/* Icon glyphs are drawn with currentColor, so they inherit the link colour
   wherever they sit — no theme token needed. */
.icon {
    font-family: 'gsgo-icons';
    font-weight: normal;
    font-style: normal;
    line-height: 1;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}
/* In use today. */
.icon-power::before   { content: "\eb0d"; }
.icon-logout::before  { content: "\eba8"; }
.icon-bell::before    { content: "\ea35"; }
.icon-camera::before  { content: "\ea54"; }
.icon-heart::before   { content: "\eabe"; }
.icon-help::before    { content: "\eabf"; }
.icon-home::before    { content: "\eac1"; }
.icon-message::before { content: "\eaef"; }
.icon-settings::before{ content: "\eb20"; }
.icon-user::before    { content: "\eb4d"; }
.icon-users::before   { content: "\ebf2"; }
/* Forum controls. Each toggle has both of its faces, because a button that
   says "Öffnen" under a padlock is telling the member two different things. */
.icon-bell-off::before { content: "\ece9"; }
.icon-lock::before     { content: "\eae2"; }
.icon-lock-open::before{ content: "\eae1"; }
.icon-pin::before      { content: "\ec9c"; }
.icon-pin-off::before  { content: "\ed5f"; }
.icon-trash::before    { content: "\eb41"; }
.icon-restore::before  { content: "\eb77"; }
.icon-quote::before    { content: "\efbe"; }
.icon-edit::before     { content: "\eb04"; }
.icon-flag::before     { content: "\eaa6"; }

/* In the subset, ready to be used — see the note above before adding more. */
.icon-adjustments::before    { content: "\ea03"; }
.icon-alert-circle::before   { content: "\ea05"; }
.icon-alert-triangle::before { content: "\ea06"; }
.icon-archive::before        { content: "\ea0b"; }
.icon-archive-off::before    { content: "\f0ad"; }
.icon-arrow-down::before     { content: "\ea16"; }
.icon-arrow-left::before     { content: "\ea19"; }
.icon-arrow-right::before    { content: "\ea1f"; }
.icon-arrow-up::before       { content: "\ea25"; }
.icon-arrows-sort::before    { content: "\eb5a"; }
.icon-ban::before            { content: "\ea2e"; }
.icon-calendar::before       { content: "\ea53"; }
.icon-check::before          { content: "\ea5e"; }
.icon-checks::before         { content: "\ebaa"; }
.icon-chevron-down::before   { content: "\ea5f"; }
.icon-chevron-left::before   { content: "\ea60"; }
.icon-chevron-right::before  { content: "\ea61"; }
.icon-chevron-up::before     { content: "\ea62"; }
.icon-circle-check::before   { content: "\ea67"; }
.icon-circle-x::before       { content: "\ea6a"; }
.icon-clock::before          { content: "\ea70"; }
.icon-copy::before           { content: "\ea7a"; }
.icon-device-floppy::before  { content: "\eb62"; }
.icon-dots::before           { content: "\ea95"; }
.icon-dots-vertical::before  { content: "\ea94"; }
.icon-download::before       { content: "\ea96"; }
.icon-eraser::before         { content: "\eb8b"; }
.icon-external-link::before  { content: "\ea99"; }
.icon-eye::before            { content: "\ea9a"; }
.icon-eye-check::before      { content: "\ee88"; }
.icon-eye-off::before        { content: "\ecf0"; }
.icon-filter::before         { content: "\eaa5"; }
.icon-folder::before         { content: "\eaad"; }
.icon-hash::before           { content: "\eabc"; }
.icon-info-circle::before    { content: "\eac5"; }
.icon-key::before            { content: "\eac7"; }
.icon-layout-grid::before    { content: "\edba"; }
.icon-link::before           { content: "\eade"; }
.icon-list::before           { content: "\eb6b"; }
.icon-mail::before           { content: "\eae5"; }
.icon-menu-2::before         { content: "\ec42"; }
.icon-minus::before          { content: "\eaf2"; }
.icon-photo::before          { content: "\eb0a"; }
.icon-plus::before           { content: "\eb0b"; }
.icon-refresh::before        { content: "\eb13"; }
.icon-search::before         { content: "\eb1c"; }
.icon-send::before           { content: "\eb1e"; }
.icon-shield::before         { content: "\eb24"; }
.icon-star::before           { content: "\eb2e"; }
.icon-star-off::before       { content: "\ed62"; }
.icon-tag::before            { content: "\10096"; }
.icon-upload::before         { content: "\eb47"; }
.icon-user-check::before     { content: "\eb49"; }
.icon-user-off::before       { content: "\ecf9"; }
.icon-user-plus::before      { content: "\eb4b"; }
.icon-user-x::before         { content: "\eb4c"; }
.icon-x::before              { content: "\eb55"; }

/* Rounded humanist sans everywhere, in place of Arial. */
body,
body h1, body h2, body h3, body h4,
#navbar, .nav-user-name,
.badge,
input, button, select, textarea {
    font-family: 'Nunito', 'Nunito-fallback', 'Segoe UI', system-ui, sans-serif;
}

/* Hierarchy from weight (green + weight together, like the reference). */
body.layout-app h1 { font-weight: 800; letter-spacing: -0.01em; }
body.layout-app h2 { font-weight: 700; }
body.layout-app h3 { font-weight: 700; }

/* Active nav: green word + green edge on a quiet grey — NOT a solid green slab
   (the base rule fills with var(--accent), which is now green). */
body.layout-app .sidebar-nav a.is-active,
body.layout-app .sidebar-nav a.is-active:hover {
    background: var(--sidebar-active-bg);
    color: var(--accent);
    border-left: 3px solid var(--accent);
    font-weight: 800;
    text-decoration: none;
}

/* Names and handles: weight + link-green where they are links. */
body.layout-app .media-card-meta strong,
body.layout-app .member-card-name,
body.layout-app .dash-member-name,
body.layout-app #navbar .nav-user-name,
body.layout-app .activity-count { font-weight: 700; }

/* Primary / accept actions go green, like the reference's btn-success. */
body.layout-app .btn-ok,
body.layout-app .btn-accept {
    background: var(--accent);
    color: #ffffff;
    border-color: #007a00;
    font-weight: 700;
}
body.layout-app .btn-ok:hover,
body.layout-app .btn-accept:hover {
    background: #007e00;
    color: #ffffff;
}

/* Count badge: dark pill on light — attention via contrast, not colour. */
body.layout-app .badge-alert {
    background: #23232a;
    color: #f4f4f5;
    border-color: transparent;
    font-weight: 700;
}

/* Section eyebrows: green, letter-spaced — the reference's green headings. */
body.layout-app .profile-group h2,
body.layout-app .profile-gallery h2,
body.layout-app .dash-activity h2 {
    font-weight: 700;
    letter-spacing: 0.06em;
    color: var(--accent);
}

/* Panels lift off the light ground so it never turns to mush. */
body.layout-app .panel,
body.layout-app .panel-inset,
body.layout-app .media-card,
body.layout-app .album-card,
body.layout-app .dash-activity,
body.layout-app .notification-list,
body.layout-app .member-card {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

/* --- Brand furniture ------------------------------------------------------ */

/* Boot: faint, full content height, anchored to the right edge. */
body.layout-app #main-wrap::before {
    top: var(--nav-h);
    bottom: 0;
    right: 0;
    left: auto;
    width: min(46vw, 720px);
    height: auto;
    background: url("../images/boot-bg.png") right center / contain no-repeat;
    mix-blend-mode: multiply;
    opacity: 0.12;
}

/* Big vertical "SGO" wordmark, bottom-left, reading bottom→top. Decorative
   only (aria-hidden in the template); sits on the fixed watermark layer behind
   content. Point-of-entry sets the text; --long shrinks the legible full name
   so it still fits the viewport height. */
body.layout-app .brand-wordmark {
    position: fixed;
    left: 6px;
    bottom: 14px;
    /* Sideways Latin reading bottom→top (reference's corner mark). writing-mode
       keeps the box a narrow on-screen column; rotate(180) flips it to read up.
       (rotate(-90) on a block box swings it off-screen left.) */
    writing-mode: vertical-rl;
    text-orientation: sideways;
    transform: rotate(180deg);
    font-family: 'Nunito', 'Nunito-fallback', 'Segoe UI', system-ui, sans-serif;
    font-weight: 800;
    font-size: 72px;
    line-height: 1;
    letter-spacing: 0.05em;
    color: #2a2a31;
    opacity: 0.5;
    white-space: nowrap;
    pointer-events: none;
    user-select: none;
    /* Above the sidebar (90), below the navbar (100) — overlays the sidebar's
       empty foot like the reference's corner wordmark. */
    z-index: 95;
}
body.layout-app .brand-wordmark--long { font-size: 56px; letter-spacing: 0.01em; }

/* Below the shell cap the gutter collapses to 0 and the corner mark would
   sit on the sidebar's links — keep it for wide screens only. */
@media (max-width: 1680px) {
    body.layout-app .brand-wordmark { display: none; }
}

/* --- Chrome port: the reference's whole shell composition ----------------- */

/* Navbar + sidebar blend into the grey ground (reference: transparent navbar,
   same-grey side-nav), separated by hairlines rather than slabs. */
/* Member navbar sits on the light header slab; admin keeps its own red bar (below). */
body.layout-app:not(.layout-admin) #navbar {
    background: var(--header-bg);
}
body.layout-app #sidebar {
    background: var(--page-bg);
}
body.layout-app .sidebar-nav a:hover {
    background: var(--sidebar-hover-bg);
}

/* Content sits directly on the grey; the white surfaces are the panels/cards
   inside, not a big #main slab (the reference composition — content drifts over
   the boot, panels carry the readable white). */
body.layout-app #main-wrap { background: transparent; }
body.layout-app #main {
    background: transparent;
    border: 0;
    /* No top padding: --content-gap on #main-wrap is the single thing standing
       between the navbar and the content, so the column starts where the
       sidebar's first heading does. A second offset here would push it off. */
    padding: 0;
    /* Cap the column so wide screens leave right whitespace for the boot,
       left-aligned like the reference's offset grid. The navbar reads the same
       token, so the two edges cannot drift apart. */
    max-width: var(--content-w);
    margin-right: auto;
}

/* No navbar override here on purpose. The bar spans the shell (`left`/`right`
   at the gutter, in the base #navbar rule), which is now the same thing as
   ending where the content ends — that is what --shell-max is derived for.
   Pulling it in to --content-right instead would stop it 12px short of the
   shell edge, leaving #main-wrap's scrollbar hanging past the end of the bar. */

/* The admin banner is the one thing inside #main-wrap that is not inside #main,
   so capping the column left it behind: it ran the full width of the wrap, ~305px
   past both the bar above it and the table below it, and the admin shell kept the
   lost-right-edge look the member shell had just been fixed out of. Same token as
   #main, so the three edges move together. */
body.layout-admin .admin-banner {
    max-width: var(--content-w);
    margin-right: auto;
}

/* Boot watermark: the isolated muddy boot (public/assets/images/scheiss-nora.jpg),
   anchored bottom-right. It sits on pure white, so mix-blend-mode:multiply drops
   the background for free; the CSS filter desaturates it to a muted watermark.
   Anchored to the viewport, not to the shell (`position: fixed` in the base rule,
   `right: 0` here): it is furniture for the whole window, and pinning it to the
   shell's gutter instead makes it slide about with the shell as the window is
   resized, which reads as part of the layout coming loose. */
body.layout-app #main-wrap::before {
    top: auto;
    bottom: 0;
    right: 0;
    left: auto;
    width: min(46vw, 644px);
    height: min(96vh, 920px);
    background: url("../images/scheiss-nora.jpg") right bottom / contain no-repeat;
    mix-blend-mode: multiply;
    filter: grayscale(0.05) blur(1.0px);
    opacity: 0.20;
}

/* Page title as a big, airy heading (reference's letter-spaced name band). */
body.layout-app #main > h1:first-child,
body.layout-app .profile-ident h1 {
    font-weight: 800;
    letter-spacing: 0.01em;
    font-size: 1.9rem;
}

/* --- Glass refinement: the reference's translucent, rounded, airy panels ---
   The single defining trait of the reference is that panels are SEE-THROUGH —
   the boot shows through them. Solid cards miss the whole look. */

/* Boot reads clearly through the glass (member + admin). The wordmark's size,
   opacity and colour all live in the single .brand-wordmark rule above. */
body.layout-app #main-wrap::before { opacity: 0.20; }

/* Rounded corners everywhere (the reference is rounded, not 2005-sharp). */
body.layout-app .panel,
body.layout-app .panel-inset,
body.layout-app .media-card,
body.layout-app .album-card,
body.layout-app .member-card,
body.layout-app .dash-activity,
body.layout-app .notification-list,
body.layout-app .profile-header,
body.layout-app .profile-group,
body.layout-app .btn,
body.layout-app input,
body.layout-app select,
body.layout-app textarea { border-radius: 8px; }

/* Translucent white surfaces so the boot + grey show through (the glassy look). */
body.layout-app .panel,
body.layout-app .dash-activity,
body.layout-app .member-card,
body.layout-app .media-card,
body.layout-app .album-card,
body.layout-app .notification-list,
body.layout-app .profile-header,
body.layout-app .profile-group,
body.layout-app .panel-inset {
    background: rgba(234, 234, 238, 0.62);
    -webkit-backdrop-filter: blur(2px);
    backdrop-filter: blur(2px);
    border: 1px solid rgba(255, 255, 255, 0.32);
    box-shadow: 0 2px 7px rgba(0, 0, 0, 0.10);
}

/* Photo cards must clip to the rounded corner. */
body.layout-app .media-card,
body.layout-app .album-card {
    overflow: hidden;
    box-shadow: 4px 5px 12px rgba(0,0,0,0.25);
}

/* Profile header reads as the reference's big translucent name band. */
body.layout-app .profile-header {
    padding: 16px 20px;
    gap: 18px;
    align-items: center;
    margin-bottom: 18px;
}
body.layout-app .profile-ident h1 { font-size: 2.2rem; letter-spacing: 0.03em; }

/* Profile info sections become glass cards with a header band on top. */
body.layout-app .profile-group { padding: 12px; margin-top: 16px; }
body.layout-app .profile-group .info-table { background: transparent; margin: 0; }
body.layout-app .profile-group > h2,
body.layout-app .dash-activity > h2 {
    margin: -12px -12px 10px -12px;
    padding: 8px 14px;
    border-radius: 8px 8px 0 0;
    background: rgba(255, 255, 255, 0.32);
    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
    color: var(--accent);
    letter-spacing: 0.06em;
    font-weight: 700;
}
body.layout-app .dash-activity { padding: 12px; }

/* The album section heading stays a simple green eyebrow (grid, not a card). */
body.layout-app .profile-gallery > h2 {
    background: none;
    border: 0;
    padding: 0 0 4px;
    margin: 0 0 6px;
    border-bottom: 2px solid var(--accent);
}

/* Airier rhythm between sections, like the reference. */
body.layout-app .dash-section { margin-top: 22px; }

/* --- Admin identity: same light theme, accent family swapped to RED. The red
   token overrides live in themes/default.css (body.layout-admin); the rules
   below are the component-level admin chrome that those tokens drive. */
/* Approve / accept stay green even in admin — colour carries meaning. */
body.layout-admin .btn-ok,
body.layout-admin .btn-accept {
    background: #009100;
    border-color: #007a00;
    color: #ffffff;
}
/* Sidebar attention rows, readable on the light admin sidebar. */
body.layout-admin .sidebar-nav a.nav-alert {
    background: #f4dede;
    color: #9a2323;
    border-left-color: #b21f1f;
}
body.layout-admin .sidebar-nav a.nav-alert:hover { background: #eecfcf; }

/* Admin navbar joins the light theme (the old dark-red bar left the right-side
   items unreadable). Identity now comes from a red underline + red links + the
   solid red "Administrationsmodus" banner below. */
body.layout-admin #navbar {
    background: var(--header-bg);
    border-bottom: 2px solid #b21f1f;
    color: var(--text);
}
body.layout-admin #navbar .site-logo { color: var(--heading); }
body.layout-admin #navbar .nav-user-name { color: var(--text); }
/* ----- admin: backups (0.C.2) -----
   Tinted from the existing state tokens via color-mix, so the panel follows the
   theme instead of introducing a second palette nobody remembers to update. */
.backup-alerts { margin: 10px 0 14px; }
.backup-alert {
    border: 1px solid var(--border);
    border-left-width: 4px;
    padding: 8px 10px;
    margin-bottom: 6px;
    background: var(--bg-alt);
}
.backup-alert--alert {
    border-left-color: var(--danger);
    background: color-mix(in srgb, var(--danger) 10%, var(--main-bg));
}
.backup-alert--warn {
    border-left-color: var(--warn);
    background: color-mix(in srgb, var(--warn) 12%, var(--main-bg));
}
.backup-alert--ok {
    border-left-color: var(--ok);
    background: color-mix(in srgb, var(--ok) 10%, var(--main-bg));
}
.backup-actions { display: flex; flex-wrap: wrap; gap: 6px; margin: 8px 0 4px; }
.backup-actions form { margin: 0; }
.backup-job {
    border: 1px solid var(--border);
    background: var(--panel-inset-bg);
    padding: 8px 10px;
    margin: 10px 0;
}
.backup-job--running { border-left: 4px solid var(--info); }
.backup-job-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; }
.backup-output,
.backup-cmd {
    background: var(--code-bg);
    border: 1px solid var(--code-border);
    color: var(--code-fg);
    padding: 8px 10px;
    margin: 8px 0 0;
    overflow-x: auto;
    white-space: pre;
    font-size: 0.85rem;
}
.backup-cmd { margin: 8px 0; }
.backup-tag {
    display: inline-block;
    font-size: 0.72rem;
    padding: 0 5px;
    margin-left: 4px;
    border: 1px solid var(--border);
    background: var(--chip-bg);
    color: var(--chip-fg);
}
.backup-tag--dim { color: var(--muted); }
.backup-result--ok { color: var(--ok); }
.backup-result--bad { color: var(--danger); font-weight: bold; }
.backup-broken { color: var(--danger); }

/* ============================= SGO THEME END ============================= */

/* ===== Forum + markup =====================================================
   The forum is tables and panels like the rest of the shell; almost nothing
   here is new furniture, which is the point — a forum that looked like a
   different site would read as bolted on. */

.forum-cat-name { font-weight: bold; }
.forum-cat-blurb { font-size: 0.9rem; margin-top: 2px; }
.forum-thread-by { font-size: 0.85rem; margin-top: 2px; }
.info-table td.num, .info-table th.num { text-align: right; width: 6em; }

.crumbs { font-size: 0.9rem; margin-bottom: 8px; }
.crumbs span[aria-hidden] { margin: 0 4px; color: var(--muted); }

/* ----- the head of a thread ----------------------------------------------
   What the thread is, on the left; what can be done to it, on the right. One
   surface a step darker than the posts (--forum-head-bg vs --panel-bg), which
   is the whole trick: enough to stop the header dissolving into the first
   reply, not so much that it becomes a second kind of post.

   An album's head shares the block (gallery/partials/_show.twig wears the
   same two classes); the .album-head class exists only so the album page can
   join the rule rather than forking it — the F85 argument, one definition. */

.forum-thread-head,
.album-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 10px 16px;
    margin: 0 0 14px;
    padding: 12px 14px;
    background: var(--forum-head-bg);
    border: 1px solid var(--border);
}

/* min-width: 0 lets a long title wrap instead of shoving the buttons off the
   panel — the titles here are German compound nouns. */
.forum-thread-ident {
    min-width: 0;
    flex: 1 1 20rem;
}

/* The header owns its spacing; the h1 and the meta line brought their own
   margins from the page defaults and produced the empty band this replaced. */
.forum-thread-head h1 {
    margin: 0 0 2px;
    padding-bottom: 0;
}

.forum-thread-head p {
    margin: 0;
}

.forum-thread-head .badge {
    vertical-align: middle;
    margin-left: 6px;
}

/* Right-aligned, and wrapping right-aligned too, so a moderator's four
   buttons break to a second line under the first rather than stretching the
   header taller on the title side. */
.forum-thread-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: 6px;
    margin-left: auto;
}

/* A category's head is a thread's head with a different noun in it, so it
   shares the block and only says where it differs. */
.crumbs + .forum-cat-head {
    margin-top: -4px;
}

/* The count sits with the name but does not compete with it. */
.forum-cat-count {
    font-weight: 400;
    color: var(--muted);
}

/* Sub-categories: a row of links, sized like the thing they are — a way in,
   not a dataset. The count rides along so the row answers "is there anything
   in there" without a table to say it. */
.subcats {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    margin: 0 0 14px;
}

.subcats-label {
    font-size: 0.85rem;
}

/* Landing on #friends: the inline script in the section marks it before paint,
   so the friends panel is up on the first frame instead of after Alpine's
   deferred script arrives. Alpine's x-show takes over from there — these rules
   only have to be right for that first frame. */
.profile-panels.is-friends > [x-show="tab === 'albums'"] {
    display: none;
}

/* The profile's right-hand panel: one slot, two collections, a tab bar on top.
   The bar reuses .tabs/.tab — the same underline the rest of the site uses to
   say "you are looking at this one". */
.profile-panels {
    min-width: 0;
}

.profile-panels .tabs {
    border-bottom: none;
    margin-bottom: 6px;
}

.profile-panels .tab .forum-cat-count {
    font-size: 0.85em;
}

/* Friend cards inside the profile panel, where the column is narrower than
   the member directory it was drawn for: smaller tiles, so eight faces fit
   the way six album covers do. */
.member-grid--tight {
    grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
    gap: 10px;
}

.member-grid--tight .member-card {
    padding: 6px;
}

.member-grid--tight .member-card-avatar {
    width: 56px;
    height: 56px;
}

/* A door, not a status chip. It was a grey pill with a grey number in it,
   which reads as a label *about* this page rather than a way off it — so the
   name takes the link colour and some weight, a folder says what is behind
   it, and the border picks up the accent so the eye finds it among greys. */
.subcat {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px 4px 10px;
    font-size: 0.95rem;
    font-weight: 600;
    color: var(--link);
    background: var(--panel-bg);
    border: 1px solid var(--accent);
    border-radius: 999px;
}

.subcat:hover {
    color: var(--on-accent);
    background: var(--accent);
    border-color: var(--accent-edge);
}

.subcat .icon {
    font-size: 0.9em;
    opacity: 0.75;
}

/* The count keeps its own shape inside the pill: the name stays the thing you
   read, the number stays the thing you check.

   Round rather than oval — min-width matches the line box, so one digit gets
   a circle and two get a stadium — and the padding is deliberately lopsided.
   Nunito's digits overhang their advance to the right (measured at 12px: the
   advance is 7.2px, the ink reaches 8px), so a box padded evenly renders the
   number a hair right of centre. 0.03em off the left and onto the right puts
   the *ink* in the middle, which is the only middle anyone can see. */
.subcat-count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.45em;
    box-sizing: border-box;
    font-size: 0.8rem;
    font-weight: 700;
    padding: 0 0.33em 0 0.27em;
    border-radius: 999px;
    color: var(--muted);
    background: var(--bg-alt);
}

.subcat:hover .subcat-count {
    color: var(--accent);
    background: var(--on-accent);
}

/* Sized here rather than with .btn-sm: that class is 11px !important, built
   for a dense admin table, and it is too small to carry a German label and an
   icon. These want to read as smaller than a primary action, not as fine
   print. */
.forum-thread-actions .btn {
    font-size: 0.85rem;
    padding: 4px 10px;
}

.forum-post { margin-bottom: 10px; }

/* The marker left where staff removed a post. Quiet and short on purpose: it
   is there so the replies around it still make sense, not to memorialise the
   removal. Keeps the anchor, so a link to the post still lands in the right
   place in the conversation. */
.forum-post--removed {
    padding: 8px 16px;
    background: var(--panel-inset-bg);
    color: var(--muted);
    font-size: 0.9rem;
}

.forum-post--removed .removed-note {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0;
}

.forum-post--removed .icon {
    opacity: 0.7;
}
.forum-post-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
/* The AUTHOR link only — a direct child of the header. It is a flex row
   because it carries an avatar beside a name. Written as `.forum-post-head a`
   it also caught the permalink nested in .forum-post-when, turning that into a
   flex container too and pushing the age onto a line of its own. */
.forum-post-head > a { display: flex; align-items: center; gap: 8px; text-decoration: none; }
/* One line, always. The date, its "#" and the age are one fact about the post
   — broken across two lines they read as two, and the header goes ragged as
   each post's age wraps at a different width. There is room: the span is
   pushed right by margin-left:auto in a header whose left side is one name. */
.forum-post-when {
    margin-left: auto;
    font-size: 0.85rem;
    white-space: nowrap;
}

/* The permalink. The "#" is the affordance — it is what tells a reader the
   timestamp is an address and not just a date — so it is dimmer than the date
   at rest and comes up to the link colour with it on hover, rather than
   sitting there as a stray character. */
.post-permalink {
    color: var(--muted);
    text-decoration: none;
}

.post-permalink:hover {
    color: var(--link);
}

.permalink-hash {
    margin-right: 5px;
    opacity: 0.6;
}

.post-permalink:hover .permalink-hash {
    opacity: 1;
}

/* "(vor 3 Tagen)" — the quick read. The exact timestamp next to it is the
   precise one; this only has to be glanceable, so it never competes. */
.post-age {
    margin-left: 4px;
}

/* ----- a post's own actions ----------------------------------------------
   Right-aligned and away from the prose, so the eye reading replies down the
   left never runs into them. `gap` owns the spacing: these four are a link, a
   link, a one-button form and a <details>, and letting each bring its own
   margin is what produced the stray hole in front of "Melden". */
.forum-post-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: flex-end;
    align-items: center;
    gap: 4px 14px;
    margin-top: 10px;
}

.forum-post-actions .inline-form {
    margin-right: 0;
}

/* One control, whatever element it happens to be. The <summary> needs the
   list-marker removed or it keeps the disclosure triangle and stops matching
   the links beside it. */
.post-action {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    font-size: 0.9rem;
    color: var(--muted);
    cursor: pointer;
    text-decoration: none;
    list-style: none;
}

.post-action::-webkit-details-marker {
    display: none;
}

.post-action:hover {
    color: var(--link);
}

/* Red on the destructive one only, and only for the word — a row where three
   of four controls are coloured is a row with no emphasis in it. */
.post-action--danger:hover {
    color: var(--danger);
}

.post-action .icon {
    font-size: 1em;
    opacity: 0.85;
}

/* Long words in a post must not push the column wide — a pasted URL is the
   usual culprit and the layout is a fixed shell (principle 7). */
.forum-post-body { overflow-wrap: anywhere; }
/* An inline embed is context inside a conversation, not the viewing surface:
   uncapped, one photo is taller than the screen and the replies either side of
   it stop being one thread. Principle 7 says never cap the photo — and this
   does not: the image links to /m/{handle}, which is the full one, and that
   view is uncapped. What is capped here is the chrome around a discussion. */
.forum-post-body img {
    max-width: 100%;
    max-height: 460px;
    width: auto;
    height: auto;
}
/* An embedded photo opens in the lightbox, so it says so on hover — the same
   cursor the gallery's own tiles use. */
.markup-img { display: inline-block; cursor: zoom-in; }

.markup-quote {
    margin: 8px 0;
    padding: 6px 10px;
    border-left: 3px solid var(--accent);
    background: var(--panel-inset-bg);
}

/* Who is being quoted, from [quote=name]. Its own line above the words, and
   quieter than them: the attribution is context for the quote, and the quote
   is context for the reply — neither should outweigh what was actually
   written underneath. */
.markup-quote cite {
    display: block;
    margin-bottom: 4px;
    font-style: normal;
    font-weight: bold;
    font-size: 0.9rem;
    color: var(--muted);
}

/* The toolbar is enhancement: with JavaScript off it is a row of inert
   buttons above a textarea that still works, so it is styled quietly rather
   than as a promise of an editor. */
.markup-toolbar { display: flex; gap: 4px; align-items: center; margin-bottom: 4px; }
.markup-toolbar button {
    min-width: 28px;
    padding: 2px 8px;
    background: var(--btn-bg);
    border: 1px solid var(--border);
    cursor: pointer;
    font-size: 0.9rem;
}
.markup-toolbar button:hover { background: var(--btn-hover-bg); }
.markup-toolbar-sep { flex: 1; }
.markup-hint { font-size: 0.8rem; margin-top: 4px; }

/* The paper. Fixed height and scrolled to its own bottom after every update,
   so the newest line sits against the field below it and earlier text rides
   upward out of view — the typewriter arrangement, where the paper moves and
   the writing point stays where your hands are. A preview that grew downward
   would push the textarea off the screen, which is the opposite of useful. */
.markup-preview {
    margin-bottom: 8px;
    max-height: 340px;
    overflow-y: auto;
    padding: 8px 12px;
    border: 1px solid var(--border);
    background: var(--panel-inset-bg);
    /* Anchored at the bottom: when content grows, the browser keeps the view
       pinned there instead of drifting. The explicit scroll after the swap is
       still what does the work — this stops the wobble in between. */
    overflow-anchor: auto;
}
.markup-preview-head {
    font-size: 0.85rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--muted);
    margin-bottom: 4px;
    position: sticky;
    top: -8px;
    background: var(--panel-inset-bg);
    padding: 4px 0 2px;
}
/* Wide and short: the field is a line to type on, the paper above is where the
   text is read. */
.markup-editor textarea {
    width: 100%;
    /* The global input rule caps every field at 360px, which is right for a
       username and wrong for a paragraph. This is the one field on the site
       that is a writing surface. */
    max-width: none;
    min-height: 7.5rem;
    resize: vertical;
}

/* ----- reports ------------------------------------------------------------
   A <details> rather than a modal: no JavaScript, no focus trap, and an
   accidental click opens a small box instead of a dialog over what you were
   reading. */
.report-control { display: inline-block; margin-left: 10px; }
.report-control > summary { cursor: pointer; list-style: none; font-size: 0.85rem; }
.report-control > summary::-webkit-details-marker { display: none; }
.report-form {
    margin-top: 8px;
    padding: 10px 12px;
    border: 1px solid var(--border);
    background: var(--panel-inset-bg);
    max-width: 30rem;
}
.report-intro { font-size: 0.85rem; margin-bottom: 6px; }
.report-form textarea, .report-form select { width: 100%; }

.report-row { margin-bottom: 10px; }
.report-row-head { display: flex; gap: 8px; align-items: baseline; margin-bottom: 6px; }
.report-subject { margin-bottom: 6px; }
/* The reported words, shown as text. Never rendered — see admin/reports.twig. */
.report-excerpt {
    margin: 6px 0;
    padding: 6px 10px;
    border-left: 3px solid var(--border);
    background: var(--panel-inset-bg);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    font-size: 0.9rem;
}
.report-note { font-size: 0.9rem; }
.report-actions { display: flex; gap: 8px; margin-top: 8px; }

.tabs { display: flex; gap: 12px; margin-bottom: 12px; }
.tab { padding: 2px 0; text-decoration: none; border-bottom: 2px solid transparent; }
.tab.is-current { border-bottom-color: var(--accent); font-weight: bold; }

/* Workbench upload drop zone: dashed until a drag arrives, accent while one
   hovers, dimmed while the queue runs. Deliberately tall — it is a target,
   not a form row. Below it, the queue: one row per file with a live bar and
   a verdict, so no upload ever lands in the void. */
.drop-zone {
    border: 2px dashed #999;
    border-radius: 6px;
    min-height: 180px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: .25rem;
    padding: 1rem;
    text-align: center;
    margin-bottom: .5rem;
    cursor: pointer;
    transition: border-color .15s ease, border-style .15s ease, background-color .15s ease;
}
.drop-zone.is-dragover {
    border-color: var(--accent);
    border-style: solid;
    background: color-mix(in srgb, var(--accent) 8%, var(--main-bg));
}
.drop-zone.is-uploading {
    cursor: default;
    opacity: .85;
}
.upload-queue {
    list-style: none;
    margin: 0 0 .5rem;
    padding: 0;
}
.upq {
    display: grid;
    grid-template-columns: 1fr auto auto;
    gap: .25rem .75rem;
    align-items: baseline;
    border: 1px solid var(--border);
    border-radius: 4px;
    background: var(--panel-inset-bg);
    padding: .25rem .5rem;
    margin-bottom: .25rem;
    font-size: .9rem;
}
.upq-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.upq-size { font-size: .85rem; }
.upq-state { font-size: .85rem; white-space: nowrap; }
.upq.is-done .upq-state { color: var(--flash-success-text, #006600); font-weight: bold; }
.upq.is-fail .upq-state { color: var(--flash-error-text, #990000); font-weight: bold; white-space: normal; }
.upq-bar {
    grid-column: 1 / -1;
    height: 6px;
    border: 1px solid var(--border);
    border-radius: 3px;
    background: var(--main-bg);
    overflow: hidden;
}
.upq-fill {
    height: 100%;
    width: 0;
    background: var(--accent);
    transition: width .2s ease;
}
.upload-summary { margin: 0 0 .5rem; }
.upload-results {
    margin-bottom: .5rem;
}

/* Workbench bulk toolbar (album workbench round) */
.workbench-bulk {
    display: flex;
    flex-wrap: wrap;
    gap: .75rem;
    align-items: center;
    justify-content: space-between;
    padding: .5rem .75rem;
}
.workbench-bulk-form {
    display: inline-flex;
    gap: .5rem;
    align-items: center;
    margin: 0;
}
.workbench-bulk-form select {
    max-width: 200px;
}
.workbench-check {
    position: absolute;
    top: .35rem;
    left: .35rem;
    z-index: 1;
    cursor: pointer;
}
.workbench-check input {
    cursor: pointer;
}
.photo-tile {
    position: relative;
}
[x-cloak] {
    display: none !important;
}
