/* ---- themes -------------------------------------------------------------
   Every colour on this site comes from the variables below, so a theme is
   just another block of them -- no per-theme rules anywhere else in this
   file. The set is applied by static/themes.js, which stamps
   data-theme="<name>" on <html> from the visitor's own localStorage; it is
   never sent to the server and never leaves the browser.

   The block below is the default (also what an unset data-theme gets, so
   the site is fully styled before any JS runs). Named themes further down
   override it via :root[data-theme="..."].

   Adding a theme means adding one block there and one entry to THEMES in
   themes.js -- nothing else. Any variable a theme leaves out simply falls
   through to the default, so a partial theme is valid, not broken. */
:root, html {
  color-scheme: var(--color-scheme);
  --bg: #14161a;
  --bg-raised: #1c1f25;
  --bg-card: #20242b;
  --border: #2e333c;
  --text: #e6e8eb;
  --text-dim: #9aa2ad;
  --link: #7cb7ff;
  --accent: #ff8a5c;
  --accent-text: #17140f;
  --danger: #ff8a80;
  --tag-bg: #2a3040;
  --focus-ring: #7cb7ff;
  /* The accent at low opacity, for the pinned-Feel wash. A variable
     rather than a hardcoded rgba() of the default accent, which is what
     it used to be -- that stayed orange no matter what the theme did. */
  --accent-soft: rgba(255, 138, 92, 0.12);
  /* Panel/modal drop shadow. Near-black is right on a dark theme and far
     too heavy on a light one, so themes get to soften it. */
  --shadow: rgba(0, 0, 0, 0.35);
  --shadow-strong: rgba(0, 0, 0, 0.5);
  /* Greentext stays green in every theme -- it's chan convention, not
     decoration -- but the exact shade has to shift to stay legible
     against a light background. */
  --green-text: #7fbf5f;
  /* Whether this theme is light or dark, declared to the browser rather
     than merely implied by its colours.
     Two things read it. The browser renders everything it draws ITSELF to
     match -- Firefox's scrollbars, native form controls, autofill, the
     canvas behind the page -- which until now stayed in dark mode on the
     five light themes here. And dark-mode extensions use it to decide
     whether a site already handles this, which is the polite half of the
     darkreader-lock meta tag in templates.layout. */
  --color-scheme: dark;

  /* ---- chrome -----------------------------------------------------------
     Colour alone cannot make a theme. Every theme here used to be a hue
     swap over identical furniture -- same font, same 10px corners on
     everything, same flat fills, same hairline borders -- so they all came
     out looking like one site wearing different paint, which is a fair
     description of "half-assed".

     What actually separates Aero from Aqua from a green terminal is none of
     those hues: it is the typeface, how round the corners are, whether a
     surface is flat or has a gradient down it, how a button is shaped and
     lit, and how heavy the borders are. The variables below are those
     things, so a theme can commit to a look instead of tinting one.

     Every default here reproduces the current appearance exactly, so a
     theme that sets none of them is unchanged rather than broken -- the
     same fall-through the colours have always had. */
  --font-ui: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --letter-spacing: normal;
  /* Corner radius, in three sizes: cards/panels, controls, and the little
     badges. A theme that wants hard corners sets all three to 0 and gets
     them everywhere at once. */
  --radius: 10px;
  --radius-sm: 6px;
  --radius-xs: 4px;
  --border-width: 1px;
  /* background-image on the header and on card surfaces. "none" is a flat
     fill of --bg-raised / --bg-card, which is what every theme had. */
  --header-image: none;
  --surface-image: none;
  --card-shadow: none;
  /* Buttons, which carry more of a look than anything else on the page --
     Aero's gloss and Aqua's capsule are both mostly button. --btn-text is
     separate from --accent-text on purpose: a theme whose buttons are pale
     needs dark text on them while ::selection still wants light text on the
     accent, and those are genuinely different questions. */
  --btn-image: none;
  --btn-radius: 6px;
  --btn-border: none;
  --btn-shadow: none;
  --btn-text: var(--accent-text);
  --input-shadow: none;
  --header-shadow: none;
  /* Text fields. Defaults to the page background, which is what every dark
     theme wants and what this has always done -- but a light-chrome theme
     needs white fields over a grey page, and those are different colours
     for the first time. */
  --input-bg: var(--bg);
  /* The DEFAULT button -- the one Enter triggers. Both Aqua and Aero
     singled it out (Aqua's pulsed blue, Aero's carried a glow ring), and
     it is one of the details people actually remember about each. Falls
     through to the ordinary button chrome, so a theme that does not care
     gets exactly what it had. */
  --btn-default-image: var(--btn-image);
  --btn-default-border: var(--btn-border);
  --btn-default-text: var(--btn-text);
  --btn-default-shadow: var(--btn-shadow);
  /* Body text glow. Only a CRT theme wants this, and a CRT theme is not
     recognisable without it -- phosphor bleed is the effect. */
  --text-shadow: none;
  /* Text sitting ON chrome (the header, the toolbar strip) rather than on
     a page. Aluminium UI of this era embossed it with a 1px light shadow;
     on a dark theme there is no such thing, hence the neutral default. */
  --chrome-text-shadow: none;

  /* ---- state and detail -------------------------------------------------
     The pre-flat era spent most of its craft on things a flat design does
     not have at all: a button that visibly sinks when pressed, a field that
     lights up when focused, a scrollbar with a modelled thumb, a title
     strip across the top of every panel. None of that is colour either, and
     without it a "skeuomorphic" theme is still just a gradient. */

  /* Pressed. Defaults to no change, which is what a flat theme wants. */
  --btn-active-image: var(--btn-image);
  --btn-active-shadow: var(--btn-shadow);
  /* Focused field. Windows 7's blue glow and Aqua's blue halo are both
     this one property, and both are instantly recognisable. */
  --input-focus-shadow: var(--input-shadow);

  /* The strip across the top of a post -- its title bar. Off by default:
     --card-head-margin pulls it out to the card's edges only when a theme
     asks, so a flat theme keeps the plain inset head it always had. */
  --card-head-image: none;
  --card-head-bg: transparent;
  --card-head-border: none;
  --card-head-margin: 0 0 0.55rem;
  --card-head-padding: 0;
  --card-head-radius: 0;

  /* Badges: the Feel number, tags, the poster chip. Flat fills by default. */
  --badge-image: none;
  --badge-border: none;
  --badge-shadow: none;

  /* Toolbar strip (the Feels/Hr bar) and the nav links, which in this era
     were chrome in their own right rather than plain text. */
  --toolbar-image: none;
  --toolbar-bg: transparent;
  --toolbar-border: none;
  --toolbar-radius: 0;
  --nav-hover-image: none;
  --nav-hover-bg: transparent;
  --nav-hover-text: var(--text);
  --nav-radius: 0;
  /* A segmented-control theme needs room for its capsule; a plain-text one
     wants the tight original. */
  --nav-padding: 0.25rem 0.1rem;
  /* The strip the links sit in. A segmented control is a bordered track
     with its segments inside it, so a theme that wants one needs to dress
     the container as well as the items. */
  --nav-strip-bg: transparent;
  --nav-strip-image: none;
  --nav-strip-border: none;
  --nav-strip-radius: 0;
  --nav-strip-shadow: none;
  --nav-strip-padding: 0;
  --nav-strip-gap: 1.1rem;
  /* The segment you are on. Defaults to the hover treatment, so a theme
     gets a sane selected state for free the moment it defines a hover. */
  --nav-active-image: var(--nav-hover-image);
  --nav-active-bg: var(--nav-hover-bg);
  --nav-active-text: var(--nav-hover-text);
  --nav-active-shadow: none;

  /* The two gutter panels -- the spell bar and the notification feed.
     They were carrying --bg-card and a border and nothing else, so on a
     themed page they read as two flat rectangles beside furniture that had
     grown depth. */
  --panel-image: var(--surface-image);
  --panel-shadow: var(--card-shadow);
  --panel-radius: var(--radius);

  /* Scrollbars. Completely unstyled until now, which meant every theme --
     terminal, Aero, Aqua -- had the same modern OS scrollbar down the side
     of it, quietly undoing the illusion on the one element that is on
     screen the entire time. */
  --scrollbar-size: 14px;
  --scrollbar-track: var(--bg-raised);
  --scrollbar-track-image: none;
  --scrollbar-thumb: var(--border);
  --scrollbar-thumb-image: none;
  --scrollbar-thumb-hover: var(--text-dim);
  --scrollbar-thumb-border: 3px solid transparent;
  --scrollbar-radius: 8px;

  /* Hover. Aero's blue bloom around a control you are pointing at is one
     of its two most-copied details (the other is the focus glow), and Aqua
     answered the same question by simply darkening the capsule. Neither is
     expressible as `filter: brightness()`, which is all this had. */
  --btn-hover-image: var(--btn-image);
  --btn-hover-shadow: var(--btn-shadow);

  /* Dropdowns. A pop-up menu was its own control in both systems and looks
     nothing like a button in either. */
  --select-image: none;
  --select-bg: var(--bg);
  --select-border: 1px solid var(--border);
  --select-radius: var(--radius-sm);
  --select-shadow: none;
  /* The arrow. appearance:none removes the native one, so every theme has
     to supply something -- and in this era the arrow was not a glyph beside
     the text, it was its own button welded to the right end of the control.
     Aqua's is a blue square with a stacked up/down pair; Windows had a flat
     chevron in a light well. */
  --select-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 2 L6 7 L11 2' fill='none' stroke='%23888' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --select-arrow-size: 0.75rem auto;
  --select-arrow-pos: right 0.55rem center;
  --select-padding-right: 1.9rem;

  /* The search field, which both systems drew as a capsule with a
     magnifier rather than as an ordinary text box. */
  --search-radius: var(--radius-sm);
  --search-icon: none;
  --search-padding-left: 0.7rem;

  /* Progress bars -- the hero panel's HP/Armor/Mana. Aqua's striped blue
     progress bar is possibly the single most recognisable widget of the
     era, and this is the only place on the site shaped like one. */
  --progress-bg: var(--bg);
  --progress-border: 1px solid var(--border);
  --progress-radius: 4px;
  --progress-shadow: none;
  /* Overlaid ON the existing per-bar colour rather than replacing it, so a
     theme adds texture without having to restate three gradients. */
  --progress-overlay: none;

  /* Checkboxes and radios. Left native until now, which meant the one
     control on the page that ignored the theme entirely -- a stock white
     box with a blue tick, on the green terminal, on Aero, on everything.
     The defaults below are a plain themed box, so no theme is worse off
     than the native one it replaces. */
  --check-size: 1rem;
  --check-bg: var(--bg);
  --check-image: none;
  --check-border: 1px solid var(--border);
  --check-radius: 3px;
  --check-shadow: none;
  --check-on-bg: var(--accent);
  --check-on-image: none;
  --check-on-border: 1px solid var(--accent);
  --check-mark: var(--accent-text);

  /* The hero panel's three bars. Hardcoded red/grey/blue until now, so
     they stayed exactly the same on the green terminal, on Aero and on
     High Contrast -- three of the few genuinely coloured shapes on the
     page, ignoring the theme entirely.

     They still have to read as three DIFFERENT things, which is the
     constraint: a monochrome theme cannot separate them by hue, so it
     separates them by intensity instead. Defaults reproduce the previous
     colours exactly. */
  --bar-hp: linear-gradient(90deg, #ff5c5c, #ff8a5c);
  --bar-armor: linear-gradient(90deg, #8b93a1, #b8bec7);
  --bar-mana: linear-gradient(90deg, #5c8aff, #7cb7ff);
}

/* Soup Dark -- the default. Given its own block rather than living in
   :root, so :root can stay the neutral fall-through every theme inherits
   from and this one's opinions are as visible as anybody else's.

   Flat, modern, quiet: no gradients, generous corners, and just enough
   shadow that a card sits above the page instead of being a differently
   coloured rectangle drawn on it. */
:root[data-theme="dark"] {
  --card-shadow: 0 1px 2px rgba(0, 0, 0, 0.28);
  --btn-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);

  --scrollbar-track: #14161a;
  --scrollbar-thumb: #363c46;
  --scrollbar-thumb-hover: #4a515e;
  --btn-active-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.35);
  --color-scheme: dark;
}

/* Light. The one theme that inverts the whole relationship between text
   and background, so it's also the one that proves the variables really
   do carry every colour: shadows lighten, greentext darkens to stay
   legible on white, and --accent-text flips to light-on-orange. */
:root[data-theme="light"] {
  --bg: #f4f5f7;
  --bg-raised: #ffffff;
  --bg-card: #ffffff;
  --border: #d3d7de;
  --text: #1a1d23;
  --text-dim: #5c646f;
  --link: #1d68c9;
  --accent: #d9541e;
  --accent-text: #fff6f1;
  --danger: #c0341c;
  --tag-bg: #e6e9ef;
  --focus-ring: #1d68c9;
  --accent-soft: rgba(217, 84, 30, 0.10);
  --shadow: rgba(15, 20, 30, 0.12);
  --shadow-strong: rgba(15, 20, 30, 0.20);
  --green-text: #2f7d32;

  /* Airy rather than merely inverted: the cards are white on a grey page,
     so they need a real (if faint) shadow to separate, and slightly softer
     corners suit a light surface. */
  --radius: 12px;
  --card-shadow: 0 1px 3px rgba(15, 20, 30, 0.10);
  --btn-shadow: 0 1px 2px rgba(15, 20, 30, 0.14);

  --scrollbar-track: #eceef2;
  --scrollbar-thumb: #c3c8d1;
  --scrollbar-thumb-hover: #a8aeb9;
  --btn-active-shadow: inset 0 2px 4px rgba(15, 20, 30, 0.22);
  --color-scheme: light;
}

/* Green terminal. Phosphor green on near-black, one hue throughout --
   the dimmed text is the same green at lower intensity rather than a
   grey, which is what makes it read as a CRT instead of a dark theme
   that happens to have green links. */
:root[data-theme="hacker"] {
  --bg: #000600;
  --bg-raised: #01120a;
  --bg-card: #01160c;
  --border: #0b5c2e;
  --text: #33ff66;
  --text-dim: #1f9c46;
  --link: #7cffb0;
  --accent: #00ff41;
  --accent-text: #001a06;
  --danger: #ff5f56;
  --tag-bg: #06301a;
  --focus-ring: #00ff41;
  --accent-soft: rgba(0, 255, 65, 0.10);
  --shadow: rgba(0, 255, 65, 0.14);
  --shadow-strong: rgba(0, 255, 65, 0.22);
  --green-text: #9dff6a;

  /* A terminal, so: monospace throughout, no rounding anywhere, hairline
     borders, no shadows, and the faint horizontal scanlines of a phosphor
     display over every surface. The glow is what actually sells it -- a
     green-on-black page without bleed just reads as a colour scheme. */
  --font-ui: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
  --letter-spacing: 0.02em;
  --radius: 0;
  --radius-sm: 0;
  --radius-xs: 0;
  --btn-radius: 0;
  --btn-border: 1px solid #1f7a2f;
  --surface-image: repeating-linear-gradient(180deg,
      rgba(51, 255, 102, 0.035) 0 1px, transparent 1px 3px);
  --header-image: repeating-linear-gradient(180deg,
      rgba(51, 255, 102, 0.05) 0 1px, transparent 1px 3px);
  --text-shadow: 0 0 4px rgba(51, 255, 102, 0.35);

  /* A terminal's scrollbar is a character cell, not a pill: square, hard,
     and drawn in the same phosphor as everything else. */
  --scrollbar-size: 12px;
  --scrollbar-track: #001200;
  --scrollbar-track-image: repeating-linear-gradient(180deg,
      rgba(51, 255, 102, 0.05) 0 1px, transparent 1px 3px);
  --scrollbar-thumb: #1f7a2f;
  --scrollbar-thumb-hover: #33ff66;
  --scrollbar-thumb-border: 1px solid #33ff66;
  --scrollbar-radius: 0;
  /* Square, inset, hard -- pressed reads as the cell inverting. */
  --btn-active-shadow: inset 0 0 0 1px #33ff66;
  --input-focus-shadow: inset 0 0 0 1px #33ff66, 0 0 6px rgba(51, 255, 102, 0.45);
  --card-head-border: 1px solid #1f7a2f;
  --card-head-margin: -1.1rem -1.2rem 0.75rem;
  --card-head-padding: 0.4rem 1.2rem;
  --badge-border: 1px solid #1f7a2f;

  /* A terminal check is a bracketed cell -- square, unfilled, and the mark
     is drawn in the same phosphor as the text. */
  --check-radius: 0;
  --check-bg: #001200;
  --check-border: 1px solid #1f7a2f;
  --check-on-bg: #001200;
  --check-on-border: 1px solid #33ff66;
  --check-mark: #33ff66;

  /* One phosphor, three intensities. A monochrome display cannot separate
     these by hue, so it separates them the way a real one did. */
  --bar-hp: linear-gradient(90deg, #0d5c1c, #33ff66);
  --bar-armor: linear-gradient(90deg, #0a3d14, #1f7a2f);
  --bar-mana: linear-gradient(90deg, #1f7a2f, #9dffbc);
  --color-scheme: dark;
}

/* Amber CRT -- the other half of the terminal pair. Same idea as hacker,
   warm monochrome instead of green. */
:root[data-theme="amber"] {
  --bg: #0d0800;
  --bg-raised: #191006;
  --bg-card: #1d1307;
  --border: #5a3a10;
  --text: #ffb642;
  --text-dim: #b57a24;
  --link: #ffd591;
  --accent: #ffa31a;
  --accent-text: #1a0f00;
  --danger: #ff6b52;
  --tag-bg: #3a2409;
  --focus-ring: #ffa31a;
  --accent-soft: rgba(255, 163, 26, 0.10);
  --shadow: rgba(255, 163, 26, 0.14);
  --shadow-strong: rgba(255, 163, 26, 0.22);
  --green-text: #c8d34a;

  /* The same CRT chassis as Green Terminal, in amber -- deliberately
     identical in construction, because the two were the same hardware with
     a different phosphor and pretending otherwise would be inventing
     history. */
  --font-ui: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", "Liberation Mono", monospace;
  --letter-spacing: 0.02em;
  --radius: 0;
  --radius-sm: 0;
  --radius-xs: 0;
  --btn-radius: 0;
  --btn-border: 1px solid #8a5a12;
  --surface-image: repeating-linear-gradient(180deg,
      rgba(255, 176, 0, 0.04) 0 1px, transparent 1px 3px);
  --header-image: repeating-linear-gradient(180deg,
      rgba(255, 176, 0, 0.06) 0 1px, transparent 1px 3px);
  --text-shadow: 0 0 4px rgba(255, 176, 0, 0.32);

  --scrollbar-size: 12px;
  --scrollbar-track: #140b00;
  --scrollbar-track-image: repeating-linear-gradient(180deg,
      rgba(255, 176, 0, 0.05) 0 1px, transparent 1px 3px);
  --scrollbar-thumb: #8a5a12;
  --scrollbar-thumb-hover: #ffb000;
  --scrollbar-thumb-border: 1px solid #ffb000;
  --scrollbar-radius: 0;
  --btn-active-shadow: inset 0 0 0 1px #ffb000;
  --input-focus-shadow: inset 0 0 0 1px #ffb000, 0 0 6px rgba(255, 176, 0, 0.45);
  --card-head-border: 1px solid #8a5a12;
  --card-head-margin: -1.1rem -1.2rem 0.75rem;
  --card-head-padding: 0.4rem 1.2rem;
  --badge-border: 1px solid #8a5a12;

  --check-radius: 0;
  --check-bg: #140b00;
  --check-border: 1px solid #8a5a12;
  --check-on-bg: #140b00;
  --check-on-border: 1px solid #ffb000;
  --check-mark: #ffb000;

  --bar-hp: linear-gradient(90deg, #8a5a12, #ffb000);
  --bar-armor: linear-gradient(90deg, #5c3c0c, #8a5a12);
  --bar-mana: linear-gradient(90deg, #8a5a12, #ffd98a);
  --color-scheme: dark;
}

/* Midnight -- deep indigo with a violet accent. The quiet one. */
:root[data-theme="midnight"] {
  --bg: #0b0f1e;
  --bg-raised: #131a30;
  --bg-card: #161e38;
  --border: #2a3560;
  --text: #dfe3f5;
  --text-dim: #8f9ac4;
  --link: #8fb4ff;
  --accent: #a78bfa;
  --accent-text: #140d24;
  --danger: #ff7b9c;
  --tag-bg: #232e55;
  --focus-ring: #a78bfa;
  --accent-soft: rgba(167, 139, 250, 0.14);
  --shadow: rgba(0, 0, 0, 0.45);
  --shadow-strong: rgba(0, 0, 0, 0.6);
  --green-text: #7fd97f;

  /* Soft and deep. The only theme that gradients its own surfaces without
     imitating an operating system: a barely-there lift down each card, big
     corners, and shadows with enough spread to read as depth rather than
     as an outline. */
  --radius: 14px;
  --surface-image: linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0));
  --card-shadow: 0 2px 10px rgba(0, 0, 0, 0.35);
  --btn-radius: 9px;
  --btn-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);

  --scrollbar-track: #0b1020;
  --scrollbar-thumb: #26304d;
  --scrollbar-thumb-hover: #3a4770;
  --scrollbar-radius: 999px;
  --card-head-image: linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0));
  --card-head-margin: -1.1rem -1.2rem 0.75rem;
  --card-head-padding: 0.55rem 1.2rem;
  --card-head-radius: 14px 14px 0 0;
  --btn-active-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.45);

  --check-radius: 4px;
  --check-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
  --color-scheme: dark;
}

/* Paper -- warm, low-contrast light theme for actually reading a long
   thread, as opposed to Light, which is a plain bright UI. */
:root[data-theme="paper"] {
  --bg: #efe7d7;
  --bg-raised: #f8f2e6;
  --bg-card: #f8f2e6;
  --border: #d6c9ae;
  --text: #33291b;
  --text-dim: #6e6047;
  --link: #8a5a1f;
  --accent: #a8641c;
  --accent-text: #fdf7ec;
  --danger: #a52f22;
  --tag-bg: #e3d8c0;
  --focus-ring: #a8641c;
  --accent-soft: rgba(168, 100, 28, 0.12);
  --shadow: rgba(80, 60, 30, 0.14);
  --shadow-strong: rgba(80, 60, 30, 0.24);
  --green-text: #4a6f21;

  /* Built for reading rather than for looking at: a serif face, small
     corners, hairline rules and no shadows at all. Nothing here should
     announce itself -- a page of text on warm stock, with the furniture
     got out of the way. */
  --font-ui: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
  --radius: 3px;
  --radius-sm: 3px;
  --radius-xs: 2px;
  --btn-radius: 3px;
  --card-shadow: none;
  --btn-shadow: none;

  /* A rule under the head, not a strip across it -- this theme is a page,
     and a page separates with a line. */
  --card-head-border: 1px solid #ddd2bb;
  --card-head-margin: 0 0 0.6rem;
  --card-head-padding: 0 0 0.4rem;
  --scrollbar-size: 12px;
  --scrollbar-track: #efe7d6;
  --scrollbar-thumb: #cbbfa6;
  --scrollbar-thumb-hover: #b0a288;
  --scrollbar-radius: 2px;
  --btn-active-shadow: inset 0 2px 3px rgba(90, 70, 40, 0.25);

  /* Ink on stock: an unfilled square with the mark drawn in the text
     colour, the way a form printed on paper is filled in. */
  --check-radius: 2px;
  --check-bg: #fbf6ea;
  --check-on-bg: #fbf6ea;
  --check-on-border: 1px solid #6b5836;
  --check-mark: #3a2f1c;

  /* Printed inks rather than screen colours -- nothing here should glow. */
  --bar-hp: linear-gradient(90deg, #9c3b28, #c26a4e);
  --bar-armor: linear-gradient(90deg, #8a7b60, #b8ab92);
  --bar-mana: linear-gradient(90deg, #3a5f8a, #7d9dbe);

  --color-scheme: light;
}

/* Vapor -- the loud one, to match the novelty BBCode tags. */
:root[data-theme="vapor"] {
  --bg: #1a0b2e;
  --bg-raised: #2a1247;
  --bg-card: #2f1450;
  --border: #6c2fa8;
  --text: #f6e8ff;
  --text-dim: #c39ae0;
  --link: #59f7f0;
  --accent: #ff5fd2;
  --accent-text: #24052a;
  --danger: #ff6b8a;
  --tag-bg: #451c6e;
  --focus-ring: #59f7f0;
  --accent-soft: rgba(255, 95, 210, 0.15);
  --shadow: rgba(255, 95, 210, 0.20);
  --shadow-strong: rgba(89, 247, 240, 0.25);
  --green-text: #7dffb8;

  /* The one theme allowed to be loud. Oversized corners, a two-hue wash
     across every surface, gradient buttons and a glow instead of a shadow
     -- the palette was already garish and the chrome may as well agree
     with it. */
  --radius: 16px;
  --radius-sm: 10px;
  --surface-image: linear-gradient(155deg, rgba(255, 95, 210, 0.10), rgba(89, 247, 240, 0.05));
  --header-image: linear-gradient(90deg, rgba(255, 95, 210, 0.16), rgba(89, 247, 240, 0.12));
  --card-shadow: 0 0 0 1px rgba(255, 95, 210, 0.18), 0 6px 22px rgba(89, 247, 240, 0.10);
  --btn-image: linear-gradient(120deg, #ff5fd2, #59f7f0);
  --btn-radius: 999px;
  --btn-text: #24052a;
  --btn-shadow: 0 2px 12px rgba(255, 95, 210, 0.35);

  --scrollbar-size: 12px;
  --scrollbar-track: #1a0b2e;
  --scrollbar-thumb: #ff5fd2;
  --scrollbar-thumb-image: linear-gradient(180deg, #ff5fd2, #59f7f0);
  --scrollbar-thumb-hover: #59f7f0;
  --scrollbar-radius: 999px;
  --scrollbar-thumb-border: 2px solid transparent;
  --card-head-image: linear-gradient(90deg, rgba(255, 95, 210, 0.22), rgba(89, 247, 240, 0.12));
  --card-head-margin: -1.1rem -1.2rem 0.8rem;
  --card-head-padding: 0.55rem 1.2rem;
  --card-head-radius: 16px 16px 0 0;
  --btn-active-shadow: inset 0 3px 10px rgba(36, 5, 42, 0.5);
  --input-focus-shadow: 0 0 0 2px rgba(89, 247, 240, 0.5), 0 0 14px rgba(255, 95, 210, 0.4);

  --check-radius: 5px;
  --check-on-image: linear-gradient(135deg, #ff5fd2, #59f7f0);
  --check-on-border: 1px solid #59f7f0;
  --check-mark: #24052a;

  --bar-hp: linear-gradient(90deg, #ff2d8a, #ff5fd2);
  --bar-armor: linear-gradient(90deg, #6c2fa8, #c39ae0);
  --bar-mana: linear-gradient(90deg, #2fb6f7, #59f7f0);
  --color-scheme: dark;
}

/* High contrast -- pure black/white with a yellow accent, for anyone who
   finds the default's greys too soft. Not a novelty theme; kept simple
   and maximally legible on purpose. */
:root[data-theme="contrast"] {
  --bg: #000000;
  --bg-raised: #0a0a0a;
  --bg-card: #0d0d0d;
  --border: #6a6a6a;
  --text: #ffffff;
  --text-dim: #d0d0d0;
  --link: #7fd4ff;
  --accent: #ffd400;
  --accent-text: #000000;
  --danger: #ff6f6f;
  --tag-bg: #262626;
  --focus-ring: #ffd400;
  --accent-soft: rgba(255, 212, 0, 0.15);
  --shadow: rgba(0, 0, 0, 0.8);
  --shadow-strong: rgba(0, 0, 0, 0.9);
  --green-text: #7dff7d;

  /* Everything the other themes use to suggest depth is exactly what makes
     an interface hard to read for the people this is for: gradients lower
     contrast against text, shadows blur an edge, and a rounded corner is a
     softer boundary than a square one. So all of it is off, and the
     borders double in weight to draw the edges outright. */
  --radius: 0;
  --radius-sm: 0;
  --radius-xs: 0;
  --btn-radius: 0;
  --border-width: 2px;
  --btn-border: 2px solid var(--text);
  --surface-image: none;
  --header-image: none;
  --card-shadow: none;
  --btn-shadow: none;
  --text-shadow: none;

  /* Nothing modelled, nothing rounded, nothing blurred -- a scrollbar you
     can find without looking for it. */
  --scrollbar-size: 18px;
  --scrollbar-track: #000000;
  --scrollbar-thumb: #ffffff;
  --scrollbar-thumb-hover: #ffd400;
  --scrollbar-thumb-border: 2px solid #000000;
  --scrollbar-radius: 0;
  --card-head-border: 2px solid #6a6a6a;
  --card-head-margin: -1.1rem -1.2rem 0.75rem;
  --card-head-padding: 0.45rem 1.2rem;
  --btn-active-shadow: inset 0 0 0 3px #ffd400;
  --input-focus-shadow: 0 0 0 3px #ffd400;

  /* Filled solid and outlined heavily -- checked must be obvious at a
     glance, not a subtle tint. */
  --check-size: 1.15rem;
  --check-radius: 0;
  --check-bg: #000000;
  --check-border: 2px solid #ffffff;
  --check-on-bg: #ffd400;
  --check-on-border: 2px solid #ffffff;
  --check-mark: #000000;

  /* No gradients and no mid-tones: three flat fills that are each
     unmistakable against black at a glance. */
  --bar-hp: #ff6f6f;
  --bar-armor: #d0d0d0;
  --bar-mana: #ffd400;
  --color-scheme: dark;
}

/* ---- base / typography ------------------------------------------------ */

* { box-sizing: border-box; }

/* Percentage, not a fixed px, so this still respects the user's own
   browser font-size preference -- 118.75% of a 16px default is ~19px.
   Every font-size in this file is rem/em, so bumping this one value
   scales the whole site proportionally. */
html { font-size: 118.75%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  letter-spacing: var(--letter-spacing);
  text-shadow: var(--text-shadow);
  font-size: 1rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-weight: 700;
  line-height: 1.25;
  margin: 1.75rem 0 0.75rem;
}
h2 { font-size: 1.4rem; }
h3 { font-size: 1.15rem; }
p { margin: 0 0 0.9rem; }

a { color: var(--link); text-decoration: none; text-underline-offset: 0.15em; }
a:hover { text-decoration: underline; }

/* Consistent, visible keyboard-focus ring on every interactive element,
   instead of relying on (and possibly losing) the browser default. */
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible,
summary:focus-visible, .mod-btn:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  border-radius: 4px;
}

::selection { background: var(--accent); color: var(--accent-text); }

/* Checkboxes and radios, themed rather than native.
   .mc-check is excluded: the moderation console ships its own stylesheet
   and its own accent-color treatment, and two systems styling one control
   is how it ends up looking like neither. */
input[type="checkbox"]:not(.mc-check),
input[type="radio"]:not(.mc-check) {
  appearance: none;
  -webkit-appearance: none;
  width: var(--check-size);
  height: var(--check-size);
  margin: 0;
  flex: none;
  display: inline-grid;
  place-content: center;
  vertical-align: -0.15em;
  background-color: var(--check-bg);
  background-image: var(--check-image);
  border: var(--check-border);
  border-radius: var(--check-radius);
  box-shadow: var(--check-shadow);
  cursor: pointer;
}
input[type="radio"]:not(.mc-check) { border-radius: 50%; }

input[type="checkbox"]:not(.mc-check)::before,
input[type="radio"]:not(.mc-check)::before {
  content: "";
  /* Scaled to nothing rather than hidden, so the mark has somewhere to
     come from -- checked is a transform, which stays crisp at any size. */
  transform: scale(0);
  transition: transform 0.08s ease-out;
}
input[type="checkbox"]:not(.mc-check)::before {
  width: 0.6em;
  height: 0.6em;
  /* A real tick rather than a glyph: a font-dependent character would be a
     different shape in every theme that changes --font-ui. */
  clip-path: polygon(14% 46%, 0 60%, 40% 100%, 100% 22%, 86% 8%, 38% 72%);
  background-color: var(--check-mark);
}
input[type="radio"]:not(.mc-check)::before {
  width: 0.45em;
  height: 0.45em;
  border-radius: 50%;
  background-color: var(--check-mark);
}
input[type="checkbox"]:not(.mc-check):checked,
input[type="radio"]:not(.mc-check):checked {
  background-color: var(--check-on-bg);
  background-image: var(--check-on-image);
  border: var(--check-on-border);
}
input[type="checkbox"]:not(.mc-check):checked::before,
input[type="radio"]:not(.mc-check):checked::before { transform: scale(1); }
input[type="checkbox"]:not(.mc-check):focus-visible,
input[type="radio"]:not(.mc-check):focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Scrollbars. On screen the entire time and previously untouched, so every
   theme -- terminal, Aero, Aqua -- had the same modern OS scrollbar down
   its side quietly contradicting everything else on the page.

   -webkit- only, because that is the only vendor prefix that can model a
   thumb; Firefox gets scrollbar-color, which is the two flat colours it
   supports and is still better than nothing. */
* {
  scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
::-webkit-scrollbar {
  width: var(--scrollbar-size);
  height: var(--scrollbar-size);
}
::-webkit-scrollbar-track {
  background-color: var(--scrollbar-track);
  background-image: var(--scrollbar-track-image);
}
::-webkit-scrollbar-thumb {
  background-color: var(--scrollbar-thumb);
  background-image: var(--scrollbar-thumb-image);
  border-radius: var(--scrollbar-radius);
  /* A transparent border inset from the track is what gives the thumb its
     margin without a second element to put it on. */
  border: var(--scrollbar-thumb-border);
  background-clip: padding-box;
}
::-webkit-scrollbar-thumb:hover { background-color: var(--scrollbar-thumb-hover); }
::-webkit-scrollbar-corner { background-color: var(--scrollbar-track); }

/* ---- header / nav ------------------------------------------------------ */

.site-header {
  text-shadow: var(--chrome-text-shadow);
  background-color: var(--bg-raised);
  background-image: var(--header-image);
  border-bottom: var(--border-width) solid var(--border);
  box-shadow: var(--header-shadow);
  /* Above the content it shadows, or the drop shadow a title-bar theme
     casts lands underneath the first card and is invisible.
     Deliberately z-index 1, not something larger: the gutter panels
     (.hero-panel, .spell-column, .mana-events-sidebar) are absolutely
     positioned against the PAGE and their top edges sit inside the
     header's band, so they only stayed visible because they paint later in
     DOM order. Giving the header a z-index at all beat that, and hid the
     hero behind the nav on every theme. They take 2; nothing here needs
     more room than that. */
  position: relative;
  z-index: 1;
}
/* Same max-width/margin/horizontal-padding recipe as .site-main below, so
   the logo/nav line up with the page content instead of sitting flush
   against the browser edge while everything under it is inset. */
.site-header-inner {
  display: flex;
  align-items: center;
  gap: 1.25rem;
  max-width: 980px;
  margin: 0 auto;
  padding: 0.85rem 1.25rem;
  flex-wrap: wrap;
}
/* The site name used to live here as .brand; it now sits on the stat bar
   immediately left of the Feels/Hr counters -- see .stat-bar-brand. */
.site-nav {
  display: flex;
  gap: var(--nav-strip-gap);
  flex-wrap: wrap;
  background-color: var(--nav-strip-bg);
  background-image: var(--nav-strip-image);
  border: var(--nav-strip-border);
  border-radius: var(--nav-strip-radius);
  box-shadow: var(--nav-strip-shadow);
  padding: var(--nav-strip-padding);
}
.nav-link.is-active {
  color: var(--nav-active-text);
  background-color: var(--nav-active-bg);
  background-image: var(--nav-active-image);
  box-shadow: var(--nav-active-shadow);
}
.nav-link {
  color: var(--text-dim);
  font-weight: 600;
  font-size: 0.82rem;
  padding: var(--nav-padding);
  /* Reserved at all times, just transparent -- so adding color on hover
     doesn't change the element's box height and shift the text. */
  border-bottom: 2px solid transparent;
}
.nav-link { border-radius: var(--nav-radius); }
.nav-link:hover {
  color: var(--nav-hover-text);
  text-decoration: none;
  border-bottom-color: var(--accent);
  background-color: var(--nav-hover-bg);
  background-image: var(--nav-hover-image);
}

/* Hero panel -- decorative video-game-style status card, living in the
   empty gutter beside the centered 980px content column (same max-width
   soupchan uses for .site-main/.site-header-inner) rather than competing
   for room *inside* the header row. position:absolute (not fixed) so it
   sits at a fixed point in the page and scrolls away normally like
   anything else -- not pinned to the viewport. Only shown once there's
   actually enough gutter width to fit it without clipping off the edge
   of the screen (see the max-width media query below) -- if there's no
   free space, it just doesn't render, rather than overlapping real
   content. HP/Armor/Mana all start at the anonymous default (1000/0/0)
   server-side and mana.js fills in the real values once /mana/status
   resolves (docs/mana-design.md §10), same as the rest of the Mana
   widget. */
.hero-panel {
  position: absolute;
  /* Above .site-header, which carries z-index 1 so its drop shadow lands
     over the content -- see the note there. This panel's top edge sits
     inside the header's band, so without this it disappears behind the
     nav on any theme whose header is not transparent. */
  z-index: 2;
  top: 1rem;
  /* Shares its left edge and width with .spell-column below it -- the two
     read as one column down the left gutter, so they have to be expressed
     the same way or they drift apart. Both are px for the reason spelled
     out on .spell-column: the gutter they live in is (100vw - 980px) / 2
     and the breakpoint that hides them is a px one, so sizing these in rem
     let a changed root font (Brave's minimum font size, a page zoom, an
     accessibility extension) push them wider than the space available.
     -692 = the content column's own left edge (50% - 490px) minus the
     202px .spell-column is offset by; 190px matches its width. */
  left: calc(50% - 692px);
  width: 190px;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 10px;
  padding: 0.7rem 0.8rem;
  box-shadow: 0 6px 18px var(--shadow);
}
.hero-name-row { display: flex; align-items: baseline; gap: 0.4rem; }
.hero-name { font-weight: 700; font-size: 0.85rem; color: var(--text); }
.hero-role {
  font-size: 0.63rem;
  font-weight: 700;
  color: var(--text-dim);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: var(--tag-bg);
  border-radius: 4px;
  padding: 0.05rem 0.35rem;
}
.hero-bar {
  position: relative;
  height: 1.05rem;
  border-radius: var(--progress-radius);
  background: var(--progress-bg);
  border: var(--progress-border);
  box-shadow: var(--progress-shadow);
  overflow: hidden;
}
.hero-bar-fill { position: absolute; inset: 0; width: 0; border-radius: 3px; transition: width 0.4s ease; }
/* The texture a theme lays over the fill -- Aqua's diagonal stripes, Aero's
   gloss sweep. A separate layer so a theme never has to restate the three
   per-bar colours below just to add a highlight to them. */
.hero-bar-fill::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--progress-overlay);
  pointer-events: none;
}
.hero-bar-hp .hero-bar-fill { background: var(--bar-hp); }
.hero-bar-armor .hero-bar-fill { background: var(--bar-armor); }
.hero-bar-mana .hero-bar-fill { background: var(--bar-mana); }
.hero-bar-label {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.6rem;
  font-weight: 700;
  color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.95), 0 0 4px rgba(0, 0, 0, 0.7);
  letter-spacing: 0.01em;
  white-space: nowrap;
  pointer-events: none;
}

/* The left-gutter column (docs/mana-design.md §10): the spell bar, with
   the Site Tagline beneath it. The tagline flows directly under the bar
   however tall it renders, and stays put when the bar is hidden (no
   signed-in identity) -- a Site Tagline is a site-wide effect everyone
   sees, not a caster-only one. The bar itself is populated by mana.js
   from GET /mana/spells; empty/hidden until a signed-in identity's
   status resolves, since casting requires Mana.

   An earlier attempt to align this with <main>'s *top* was reverted for
   overlapping .hero-panel, which occupies the same gutter from top:1rem
   down to roughly 7rem. Aligning with the post box instead (see
   --post-box-top on .site-main) clears the hero panel comfortably --
   the post box starts well below it -- which is what makes nesting
   inside main viable now where it wasn't before.

   position:absolute, not fixed, so it scrolls away with the page like
   .hero-panel does rather than trailing the viewport down a long Feel. */
.spell-column {
  position: absolute;
  z-index: 2;
  /* Top edge of the post box, and left of .site-main's own left edge --
     both relative to .site-main now, so neither has to track the
     header's height the way the old page-absolute top/left pair did. */
  top: var(--post-box-top);
  /* Horizontal geometry is in px, NOT rem, on purpose. The gutter this
     has to fit inside is (100vw - .site-main's 980px max-width) / 2, and
     the breakpoint below that hides the column is a px one -- both are
     rem-independent, so sizing the column in rem let the two drift apart
     the moment anything changed the root font size. That is exactly what
     Brave (and font-size-bumping privacy/accessibility extensions
     generally) does: at the old -12rem/11rem, a root font any larger than
     the page's own 19px made the column wider than the gutter it sits in,
     so the viewport clipped its left edge and the bar collapsed into a
     narrow scrolling box. At the 1401px the breakpoint lets through, the
     gutter is (1401 - 980) / 2 = 210px; 12 + 190 = 202px fits with room
     to spare, and now stays fitting whatever the root font is.

     .hero-panel above sits directly on top of this and uses the same two
     numbers -- keep them in step, or the gutter stops reading as one
     column. */
  left: -202px;
  width: 190px;
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
}
/* Beats .site-main > * + *, whose margin would otherwise be added to the
   top offset above (margins do shift an absolutely positioned box). */
.site-main > .spell-column { margin-top: 0; }
.spell-bar {
  background-image: var(--panel-image);
  box-shadow: var(--panel-shadow);
  border-radius: var(--panel-radius);
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  /* Deliberately NOT capped with a max-height + overflow-y: auto. It used
     to be (so a long spell list couldn't push the tagline off-screen), but
     the cap was calc(100vh - 14rem) -- part viewport, part root font --
     which meant an inflated root font shrank the box and grew its contents
     at the same time, tipping the bar into its own scroller. Since the
     column is absolutely positioned and scrolls away with the page rather
     than being pinned to the viewport, an expanded category can simply run
     down the page and be reached by scrolling normally, which is both
     nicer to use and has nothing left to mis-measure. */
}
.spell-bar[hidden] { display: none; }
.spell-bar-category summary {
  cursor: pointer;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.3rem 0.5rem;
}
.spell-bar-category { margin-bottom: 0.2rem; }
.spell-bar-list { display: flex; flex-direction: column; gap: 0.25rem; padding: 0.3rem 0 0; }
.spell-btn {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.35rem 0.5rem;
  font-size: 0.72rem;
  cursor: pointer;
  text-align: left;
}
.spell-btn:hover { border-color: var(--accent); }
.spell-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.spell-btn-head { display: flex; justify-content: space-between; gap: 0.4rem; font-weight: 600; }
.spell-btn-cost { color: var(--text-dim); white-space: nowrap; font-weight: 400; }
.spell-btn-desc { color: var(--text-dim); font-size: 0.66rem; font-weight: 400; line-height: 1.3; }
.spell-btn.is-targeting { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent) inset; }
.spell-cancel-btn {
  background: var(--bg-card);
  border: 1px solid var(--danger);
  color: var(--danger);
  border-radius: 6px;
  padding: 0.35rem 0.5rem;
  font-size: 0.72rem;
  cursor: pointer;
}

/* Targeting mode (sidebar Post spells): crosshair cursor everywhere,
   valid targets (post cards) light up on hover so it's obvious where a
   click will land. */
body.spell-targeting-mode { cursor: crosshair; }
body.spell-targeting-mode .post-card { cursor: crosshair; }
body.spell-targeting-mode .post-card:hover {
  outline: 2px dashed var(--accent);
  outline-offset: 2px;
}

/* Spell params panel -- collects hours/color/text for spells that need
   them (docs/mana-design.md §10, v2's "adjustable based on what they want
   to spend" duration pricing). */
.spell-params-panel {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 0.5rem 0.6rem;
  font-size: 0.75rem;
  margin-bottom: 0.3rem;
}
.spell-params-title { font-weight: 700; }
.spell-params-row { display: flex; align-items: center; justify-content: space-between; gap: 0.4rem; }
.spell-params-row input[type="text"] { flex: 1; min-width: 0; }
.spell-params-cost { color: var(--text-dim); font-size: 0.7rem; }
.spell-params-btn-row { display: flex; gap: 0.4rem; }
.spell-params-btn-row button {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.3rem;
  font-size: 0.72rem;
  cursor: pointer;
}
.spell-params-btn-row button:first-child { border-color: var(--accent); }

/* Per-post spell icon + menu (docs/mana-design.md §10, v2 Post spells) --
   the icon sits in each Feel's own post-head; clicking it opens a menu of
   just that post's Post spells, target already implied. */
.post-spell-icon-btn { margin-left: auto; }
.post-spell-menu {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 0.5rem 0.6rem;
  margin-top: 0.5rem;
  font-size: 0.78rem;
}
.post-spell-menu-btn {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.35rem 0.5rem;
  font-size: 0.78rem;
  text-align: left;
  cursor: pointer;
}
.post-spell-menu-btn:hover { border-color: var(--accent); }
.post-spell-menu-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.post-spell-menu-close {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 6px;
  padding: 0.3rem 0.5rem;
  font-size: 0.75rem;
  cursor: pointer;
}

/* Right-sidebar recent-activity feed. position:absolute, same as
   .spell-bar above -- scrolls away normally with the page. */
.mana-events-sidebar {
  background-image: var(--panel-image);
  box-shadow: var(--panel-shadow);
  border-radius: var(--panel-radius);
  position: absolute;
  z-index: 2;
  top: 1rem;
  /* px for the same reason .spell-column is -- this sits in the mirror
     image of that gutter (50% of the viewport puts it at the centre, +490
     walks out to the right edge of the 980px content column), so a rem
     width here overflowed the gutter under an inflated root font too.
     12 + 186 = 198px against the 210px the breakpoint guarantees. */
  left: calc(50% + 490px + 12px);
  width: 186px;
  /* Deliberately NOT capped with a max-height + overflow-y: auto, for the
     same reason .spell-bar above isn't. The cap was calc(100vh - 2rem),
     which put a scroller inside a box that is already absolutely
     positioned and already scrolls away with the page -- so the reader got
     two scrollbars for one list, and the inner one hid its own overflow
     behind a thumb most people never touch. The feed is bounded by
     constants.EVENTS_FEED_LIMIT instead, which bounds the height at the
     source rather than hiding it. Anything past the fold simply uses the
     page's own scrollbar. */
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 0.7rem 0.8rem;
  box-shadow: 0 6px 18px var(--shadow);
}
.mana-events-sidebar[hidden] { display: none; }
.mana-events-title {
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
  margin-bottom: 0.4rem;
}
.mana-event-row {
  font-size: 0.72rem;
  color: var(--text-dim);
  padding: 0.3rem 0;
  /* Pinned rather than left to the browser's "normal" so the row's line
     spacing is a known quantity and does not shift with the font stack. */
  line-height: 1.45;
  border-top: 1px solid var(--border);
  /* Anchors the dismiss ×, and the right padding is what stops a long
     message running underneath it. */
  position: relative;
  padding-right: 1rem;
}
.mana-event-row:first-child { border-top: none; }

/* Dismiss ×, top right of every notification. Deliberately faint until the
   row is hovered: it sits on every row in the feed, so at full contrast a
   dozen of them would read as the loudest thing in the sidebar.

   NO explicit height, and that is the point. An earlier version gave this
   a height of one text line box and matched line-height, reasoning that the
   glyph would then centre on the message's first line. It does not: a
   <button> centres its content vertically inside its own box irrespective
   of line-height, so a box taller than the glyph pushes the × down into the
   middle of the row rather than aligning it with anything.

   Sized to the glyph instead -- line-height 1 and no height -- so the box IS
   the ×, and `top` alone decides where it sits. If it needs nudging, that is
   the single number to change. */
/* A plain link in the row's top-right corner. Deliberately an <a> and not a
   <button>: a button brings the UA's own padding, border, background and
   internal content-centring, and fighting all four off was what made this
   so hard to place. A link is an inline box that goes where it is put. */
.mana-event-dismiss {
  position: absolute;
  /* 4px, established by the operator actually looking at it in a browser --
     which is the only way this was ever going to be settled. Left as a px
     rather than converted to rem so it stays the plain measured number. */
  top: 4px;
  right: 0;
  color: var(--text-dim);
  text-decoration: none;
  opacity: 0.4;
  cursor: pointer;
}
.mana-event-row:hover .mana-event-dismiss { opacity: 0.8; }
.mana-event-dismiss:hover { opacity: 1; color: var(--text); }
/* Keyboard users never hover, so the control has to surface on focus too. */
.mana-event-dismiss:focus-visible { opacity: 1; outline: 1px solid var(--border); }

/* Sits beside Reply inside .mana-event-actions -- two one-word controls on
   one line read as a pair of actions on the message. */
.mana-block-btn {
  display: inline;
  margin-left: 0.6rem;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.7rem;
  color: var(--text-dim);
  text-decoration: underline;
  cursor: pointer;
}
.mana-block-btn:hover { color: var(--danger, var(--text)); }
.mana-block-btn:disabled { opacity: 0.5; cursor: default; }

/* Small confirm overlay. Rides .chan-modal-backdrop for the dimmed
   background and .chan-modal for the card, and only narrows it -- a
   40rem box around one sentence reads as an error, not a question. */
.confirm-modal {
  width: 24rem;
  gap: 0.9rem;
}
.confirm-modal-text {
  font-size: 0.9rem;
  line-height: 1.45;
  color: var(--text);
}
.confirm-modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 0.5rem;
}
.confirm-modal-btn {
  font: inherit;
  font-size: 0.85rem;
  padding: 0.35rem 0.9rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
}
.confirm-modal-btn:hover { border-color: var(--text-dim); }
.confirm-modal-btn-danger {
  border-color: var(--danger, var(--border));
  color: var(--danger, var(--text));
}

/* Blocked-senders list, inside the settings cog menu. */
.settings-blocks-toggle {
  display: block;
  font-size: 0.75rem;
  color: var(--text-dim);
  text-decoration: none;
  cursor: pointer;
}
.settings-blocks-toggle:hover { color: var(--text); }
.settings-blocks-list { margin-top: 0.35rem; }
.settings-block-row {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.3rem 0;
  border-top: 1px solid var(--border);
}
.settings-block-row:first-child { border-top: none; }
/* The message is the whole point of the row, so it takes the space and the
   unblock link stays a fixed-width afterthought on the right. */
.settings-block-msg {
  flex: 1;
  font-size: 0.7rem;
  color: var(--text-dim);
  word-break: break-word;
}
.settings-block-msg.is-missing { font-style: italic; opacity: 0.7; }
.settings-block-undo {
  flex: none;
  font-size: 0.7rem;
  color: var(--text-dim);
  text-decoration: underline;
  cursor: pointer;
}
.settings-block-undo:hover { color: var(--accent, var(--text)); }

/* "Clear all", under the list. Separated by a rule so it reads as an action
   on the whole feed rather than as another notification.

   A <form> wrapping a <button> now, rather than a button built in script:
   the separating rule and the spacing belong to the form, the type belongs
   to the button. [hidden] is spelled out because the display:block above it
   would otherwise win. */
.mana-events-clear {
  display: block;
  margin-top: 0.4rem;
  padding: 0.25rem 0 0;
  border-top: 1px solid var(--border);
}
.mana-events-clear[hidden] { display: none; }
.mana-events-clear-btn {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.68rem;
  color: var(--text-dim);
  text-align: right;
  cursor: pointer;
}
.mana-events-clear-btn:hover { color: var(--text); }
.mana-events-clear-btn:disabled { opacity: 0.5; cursor: default; }

/* Whisper replies. The Reply button sits under its message rather than
   inline with it: the feed is 186px wide and a message runs to three or
   four lines, so a floated button would either wrap awkwardly or squeeze
   the text into a column. */
/* The line that holds Reply and Block. It is a block so the pair start a
   new line under the message instead of running on from its last word --
   which is what happened when only the buttons were made inline. */
.mana-event-actions { margin-top: 0.3rem; }

/* The message itself is a block for the same reason: an inline span lets
   whatever follows continue its final line. */
.mana-event-text { display: block; }

/* The attachment gets its own line too, rather than sharing one with the
   message or the controls. */
.mana-event-image-link { display: block; }

.mana-reply-btn {
  display: inline;
  padding: 0;
  border: none;
  background: none;
  font: inherit;
  font-size: 0.7rem;
  color: var(--accent, var(--text-dim));
  text-decoration: underline;
  cursor: pointer;
}
.mana-reply-btn:hover { color: var(--text); }
.mana-reply-box { margin-top: 0.35rem; }
.mana-reply-input {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  font-size: 0.72rem;
  padding: 0.3rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 6px;
  resize: vertical;
}
.mana-reply-actions {
  display: flex;
  gap: 0.3rem;
  margin-top: 0.25rem;
}
.mana-reply-send, .mana-reply-cancel {
  font: inherit;
  font-size: 0.68rem;
  padding: 0.2rem 0.45rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-card);
  color: var(--text);
  cursor: pointer;
}
.mana-reply-send:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
.mana-reply-file {
  display: block;
  margin-top: 0.3rem;
  font-size: 0.62rem;
  max-width: 100%;
}
.mana-reply-attach-note {
  font-size: 0.62rem;
  color: var(--text-dim);
  margin-top: 0.15rem;
}
/* A whisper attachment in the feed. Capped hard: the sidebar is 186px and
   an attachment is a private aside, not the point of the row. */
/* Extra-long feels: everything past the free limit sits behind this, so a
   bought post takes no more room in the list than an ordinary one until a
   reader opens it. */
.long-feel { margin-top: 0.25rem; }
.long-feel-more {
  cursor: pointer;
  font-size: 0.78rem;
  color: var(--text-dim);
  list-style: none;
}
.long-feel-more::-webkit-details-marker { display: none; }
.long-feel-more::before { content: "\25B8 "; }
.long-feel[open] .long-feel-more::before { content: "\25BE "; }
.long-feel-more:hover { color: var(--text); }
.long-feel-rest { margin-top: 0.25rem; }

/* A ritual already owned: still listed, so it's clear you have it, but
   visibly not a thing to buy again. */
.spell-btn-owned {
  opacity: 0.65;
  cursor: default;
}
.spell-btn-owned .spell-btn-cost {
  color: var(--accent, var(--text-dim));
  font-weight: 700;
}

.long-post-toggle {
  margin-left: 0.5rem;
  font: inherit;
  font-size: 0.72rem;
  padding: 0.1rem 0.4rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg-card);
  color: var(--text-dim);
  cursor: pointer;
}
.long-post-toggle:hover { color: var(--text); }
.long-post-toggle.is-on {
  color: var(--text);
  border-color: var(--accent, var(--border));
}

.mana-event-image {
  display: block;
  margin-top: 0.3rem;
  max-width: 100%;
  max-height: 140px;
  border-radius: 6px;
  border: 1px solid var(--border);
  cursor: zoom-in;
}
.mana-event-image:hover { border-color: var(--accent, var(--text-dim)); }

/* Below this, the gutter is narrower than the panel needs -- hide rather
   than clip or overlap the (centered, fixed-width) content column. */
@media (max-width: 1400px) {
  /* .spell-column, not .spell-bar -- hiding only the inner bar would
     leave the tagline stranded in the too-narrow gutter. */
  .hero-panel, .spell-column, .mana-events-sidebar { display: none; }
}

/* ---- Post/Site spell visuals (docs/mana-design.md §10, v2) -------------- */

.pin-badge {
  background: var(--accent);
  color: var(--accent-text);
  border-radius: 4px;
  padding: 0.1rem 0.45rem;
  font-size: 0.68rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.bb-feel-message {
  margin-top: 0.4rem;
  font-size: 0.78rem;
  font-style: italic;
  color: var(--text-dim);
  border-left: 2px solid var(--accent);
  padding-left: 0.5rem;
}

/* Site Tagline -- #site-tagline-slot, now sitting under the spell bar in
   the left gutter (it used to share the stat-bar row with the Feels/Hr
   counters); filled in by mana.js after polling GET /mana/site_effects,
   empty when no tagline spell is active. */
.site-tagline {
  font-size: 0.85rem;
  color: var(--text-dim);
  line-height: 1.35;
  overflow-wrap: anywhere; /* narrow column -- don't let one long word overflow it */
}
.site-tagline:empty { display: none; }

/* Site Background -- a low, fixed-opacity color wash behind all content
   ("blended in", per the spell's own description) rather than a flat
   replacement, so the real page underneath always stays legible. */
.site-bg-overlay {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0.08;
}

/* Mana widget (docs/mana-design.md) -- occupies the header slot the theme
   toggle used to. Empty (no button, no markup) whenever Turnstile isn't
   configured, so an un-configured deploy shows nothing here at all. */
.header-icons { margin-left: auto; display: flex; align-items: center; gap: 0.5rem; }
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  /* Overrides the generic `button { background: var(--accent) }` reset --
     without this, every real <button> using .icon-btn (settings gear,
     reply-notify) inherits an orange background unconditionally, since
     .icon-btn never used to set its own background at all. The search
     icon is an <a>, not a <button>, so it never had this problem. */
  background: none;
  border: none;
  padding: 0.4rem;
  border-radius: 6px;
  min-width: 1.9rem;
  min-height: 1.9rem;
}
.icon-btn:hover { color: var(--text); background: var(--tag-bg); text-decoration: none; }
.icon-btn[hidden] { display: none; }
.mana-widget { position: relative; }
.settings-widget { position: relative; }
.reply-notify { position: relative; }
.reply-notify-btn {
  font-size: 0.78rem;
  font-weight: 700;
  min-width: 1.6rem;
  padding: 0.35rem 0.5rem;
}
.reply-notify-btn.has-new { color: var(--accent); background: var(--tag-bg); }
.mana-btn {
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.45rem 0.8rem;
  font-size: 0.9rem;
  font-weight: 600;
  cursor: pointer;
  min-height: 2.25rem;
}
.mana-btn:hover { border-color: var(--accent); }

/* Signed-out header controls: "Enable Mana" (create/claim a user) sitting
   next to "Login" (get back into one you already have). Styled as a plain
   text link rather than a second button so it reads as the secondary,
   already-have-one path instead of competing with the primary action. */
.mana-signed-out-row { display: flex; align-items: center; gap: 0.6rem; }
.mana-login-link {
  background: none;
  border: none;
  padding: 0;
  color: var(--text-dim);
  font-size: 0.85rem;
  font-weight: 600;
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.mana-login-link:hover { color: var(--accent); }
.mana-login-error { color: var(--danger); font-size: 0.8rem; }
.mana-login-error[hidden] { display: none; }

.mana-panel {
  display: flex;
  align-items: center;
  gap: 0.6rem;
}
.mana-status-text { color: var(--text-dim); font-size: 0.85rem; }
.mana-error { color: var(--danger); }

.mana-menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 10;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.9rem;
  width: 23rem;
  max-width: 90vw;
  box-shadow: 0 8px 24px var(--shadow);
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  font-size: 0.85rem;
}
/* [hidden] on its own loses to the ".mana-menu { display: flex }" rule
   above -- both are specificity (0,1,0), and an author stylesheet rule
   beats the browser's built-in [hidden] rule at a specificity tie. This
   selector is (0,2,0), so it actually wins and the menu is truly hidden
   until JS clears the attribute. Same fix as .new-badge elsewhere. */
.mana-menu[hidden] { display: none; }

.settings-menu {
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  z-index: 10;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.9rem;
  width: 24rem;
  max-width: 92vw;
  max-height: 80vh;
  overflow-y: auto;
  box-shadow: 0 8px 24px var(--shadow);
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
  font-size: 0.85rem;
}
.settings-menu[hidden] { display: none; }
/* Wider than it was: the menu now carries the theme picker and a custom
   CSS editor, and a code textarea at 16rem is unusable. */
.settings-toggle-row { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
.settings-hint { color: var(--text-dim); font-size: 0.75rem; line-height: 1.4; }
.settings-menu hr { border: none; border-top: 1px solid var(--border); margin: 0.2rem 0; width: 100%; }

/* Theme picker + custom CSS (static/themes.js). */
.settings-theme-section { display: flex; flex-direction: column; gap: 0.4rem; }
.settings-theme-row { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
.settings-theme-select {
  flex: 1;
  min-width: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.3rem 0.4rem;
  font-size: 0.8rem;
}
.settings-css-details summary {
  cursor: pointer;
  font-weight: 600;
  color: var(--text-dim);
  font-size: 0.8rem;
  padding: 0.2rem 0;
}
.settings-css-textarea {
  width: 100%;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.5rem;
  /* Monospace and horizontally scrollable rather than wrapped -- this is
     code, and soft-wrapped CSS is much harder to read back. */
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.72rem;
  line-height: 1.45;
  white-space: pre;
  overflow-wrap: normal;
  overflow-x: auto;
  resize: vertical;
  margin-top: 0.35rem;
}
.settings-css-row { display: flex; gap: 0.4rem; margin-top: 0.4rem; }
.settings-css-row button {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.3rem;
  font-size: 0.75rem;
  cursor: pointer;
}
.settings-css-row button:first-child { border-color: var(--accent); }
.settings-css-status { color: var(--text-dim); font-size: 0.72rem; min-height: 1em; margin-top: 0.3rem; }
.mana-menu-id code { background: var(--tag-bg); padding: 0.1rem 0.4rem; border-radius: 4px; word-break: break-all; }
.mana-menu-weight { color: var(--text-dim); }
.mana-menu-hint { color: var(--text-dim); font-size: 0.78rem; }
.mana-menu hr { border: none; border-top: 1px solid var(--border); margin: 0.2rem 0; }
.mana-menu-form { display: flex; flex-direction: column; gap: 0.4rem; }
.mana-menu-form input {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.45rem 0.6rem;
  font: inherit;
  font-size: 0.85rem;
}
/* The identity ID is 32 hex chars -- monospace + a smaller size keeps the
   full value visible in the field instead of scrolling out of view. */
.mana-id-input {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: 0.72rem !important;
  letter-spacing: -0.01em;
}
.mana-menu-form button {
  padding: 0.4rem 0.7rem;
  font-size: 0.85rem;
}
.mana-menu-btn-row { display: flex; gap: 0.4rem; }
.mana-menu-btn-row button { flex: 1; }

/* Identity export overlay (shown once, right after Set Password) -- see
   mana.js's showExportOverlay. */
.identity-export-backdrop {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}
.identity-export-box {
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 10px;
  padding: 1.2rem;
  width: 24rem;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  box-shadow: 0 12px 32px var(--shadow-strong);
}
.identity-export-title { font-weight: 700; font-size: 1.05rem; }
.identity-export-hint { color: var(--text-dim); font-size: 0.8rem; }
.identity-export-field {
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.5rem 0.7rem;
  font-size: 0.85rem;
  word-break: break-all;
}
.identity-export-box > button { padding: 0.5rem; font-size: 0.85rem; }
.identity-export-email-row { display: flex; gap: 0.4rem; }
.identity-export-email-row input {
  flex: 1;
  min-width: 0;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.45rem 0.6rem;
  font-size: 0.85rem;
}
.identity-export-close {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
}
.mana-btn-danger {
  width: 100%;
  background: transparent;
  border: 1px solid var(--danger);
  color: var(--danger);
  font-size: 0.82rem;
}
.mana-btn-danger:hover { background: var(--danger); color: var(--bg-card); border-color: var(--danger); }

.stat-bar {
  text-shadow: var(--chrome-text-shadow);
  background-color: var(--toolbar-bg);
  background-image: var(--toolbar-image);
  border: var(--toolbar-border);
  border-radius: var(--toolbar-radius);
  color: var(--text-dim);
  font-size: 0.9rem;
  margin: 0.5rem 0 1.25rem;
  display: flex;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
}
/* The site name, sitting immediately left of the stats (it used to be
   .brand up in the header nav). Carries the old .brand a styling. */
.stat-bar-brand {
  font-weight: 800;
  font-size: 1.3rem;
  color: var(--text);
  letter-spacing: -0.01em;
}
.stat-bar-brand:hover { text-decoration: none; }

/* Muted explanatory note under a heading or table (help, secret board,
   filter test). These used to borrow class="stat-bar" purely for its
   colour and size, which quietly broke them: .stat-bar is the index's
   stats ROW -- a flex layout that, above 1401px, is pinned to exactly one
   line's height with overflow:hidden so it can't push the post box out of
   alignment with the spell column. Any note longer than one line was
   therefore clipped at the bottom, which is what made the help page look
   truncated. Same look, no layout baggage. */
.page-note {
  color: var(--text-dim);
  font-size: 0.9rem;
  margin: 0.5rem 0 1.25rem;
  line-height: 1.6;
  max-width: 68ch;
}
/* The counters sit at the right edge of the row, opposite the site name.
   margin-left:auto rather than justify-content:space-between so the
   alignment still holds on pages that render the bar without a brand. */
.stat-bar-stats { margin-left: auto; }

/* Above that breakpoint the spell column is visible and keyed to
   --post-box-top, which assumes the stat bar is exactly one line tall --
   so don't let it wrap to two and push the post box out of alignment. A
   long enough [site] title is the only realistic way that happens at
   this width; it now ellipsizes instead. Wrapping stays allowed below
   the breakpoint, where the row is tight and nothing is aligned to it.
   Placed after the .stat-bar rules above on purpose: a media query adds
   no specificity, so from earlier in the file the plain flex-wrap: wrap
   would simply win. */
@media (min-width: 1401px) {
  /* Height is pinned, not merely assumed: --post-box-top treats this row
     as exactly one line, so nothing in it may be allowed to grow the row
     and push the post box out of alignment with the spell column. A long
     [site] title is the realistic case -- the brand shrinks and
     ellipsizes (min-width: 0 is what lets a flex item shrink below its
     content and actually ellipsize), while the counters keep their
     natural width on one line. */
  .stat-bar { flex-wrap: nowrap; height: calc(1.3rem * 1.6); overflow: hidden; }
  .stat-bar-brand { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .stat-bar-stats { flex: none; white-space: nowrap; }
}


.alert-banner {
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-left: 4px solid var(--accent);
  color: var(--text);
  padding: 0.7rem 1rem;
  border-radius: 8px;
  margin: 0 0 1rem;
  font-size: 0.95rem;
}

.site-main {
  max-width: 980px;
  margin: 0 auto;
  padding: 1.5rem 1.25rem 3rem;
  /* Containing block for .spell-column in the left gutter. */
  position: relative;

  /* Where the post box's top edge falls, measured from the top of this
     element's padding box: its own padding-top, then the stat bar --
     margin-top, one line's height (the .stat-bar-brand line box, since
     that's the tallest thing on the row), and the margin below it, which
     collapses with the .site-main > * + * gap to whichever is larger.
     The spell column keys its own top off this, so the two line up by
     construction rather than by the hand-tuned pixel value they kept
     drifting apart on whenever the header or stat bar changed height. */
  --post-box-top: calc(1.5rem + 0.5rem + (1.3rem * 1.6) + 1.25rem);
}
/* Generic top-level rhythm: any two adjacent direct children of .site-main
   get the same gap .thread-list/.reply-list already use between cards --
   covers the OP card / reply-list / reply-form stack on /replies/ (which
   sat flush against each other, having no gap of their own) without
   needing page-specific margin rules. */
.site-main > * + * { margin-top: 1.1rem; }

/* ---- forms -------------------------------------------------------------- */

.post-form {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  background-color: var(--bg-card);
  background-image: var(--surface-image);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  padding: 1.1rem;
  margin-bottom: 1.5rem;
}
.post-form textarea, .post-form input[type="text"], .post-form input[type="password"] {
  background: var(--input-bg);
  border: var(--border-width) solid var(--border);
  color: var(--text);
  border-radius: var(--radius-sm);
  box-shadow: var(--input-shadow);
  padding: 0.6rem 0.7rem;
  font: inherit;
  /* font-size stays >=16px so mobile Safari doesn't auto-zoom on focus */
  line-height: 1.5;
}
.post-form textarea:focus, .post-form input:focus {
  border-color: var(--focus-ring);
  box-shadow: var(--input-focus-shadow);
  outline: none;
}
.post-form textarea::placeholder, .post-form input::placeholder { color: var(--text-dim); opacity: 1; }
.post-form textarea { min-height: 8rem; resize: vertical; }
.form-row { display: flex; gap: 0.6rem; flex-wrap: wrap; }
.form-row input { flex: 1; min-width: 10rem; }
.char-counter { font-size: 0.82rem; color: var(--text-dim); }

/* "What are you linking to". Server-rendered visible and hidden by
   static/posts.js while the box holds no link -- see
   templates.link_description_row for why that way round. [hidden] needs
   saying explicitly because .post-form is a flex container, and a flex
   child's display beats the UA's [hidden] rule. */
.link-desc-row { display: block; }
.link-desc-row[hidden] { display: none; }
.link-desc-input { width: 100%; box-sizing: border-box; }
/* The inline edit box's copy. It is not inside a .post-form, so it needs
   the field styling the post forms get from their container. */
.post-edit-link-desc {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.4rem 0.5rem;
  font: inherit;
  font-size: 0.85rem;
  margin-top: 0.4rem;
}
.post-edit-link-desc[hidden] { display: none; }

.bbcode-toolbar { display: flex; flex-wrap: wrap; gap: 0.35rem; align-items: center; }
.bbcode-btn {
  height: 2rem;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0 0.6rem;
  font-size: 0.78rem;
  cursor: pointer;
}
.bbcode-btn:hover { border-color: var(--accent); }
select {
  appearance: none;
  -webkit-appearance: none;
  background-color: var(--select-bg);
  /* Arrow first so it paints over the surface; the surface repeats behind. */
  background-image: var(--select-arrow), var(--select-image);
  background-repeat: no-repeat, repeat;
  background-position: var(--select-arrow-pos), 0 0;
  background-size: var(--select-arrow-size), auto;
  border: var(--select-border);
  border-radius: var(--select-radius);
  box-shadow: var(--select-shadow);
  color: var(--text);
  font: inherit;
  padding: 0.25rem var(--select-padding-right) 0.25rem 0.55rem;
}

/* The search field. Same input element as any other, told apart by name --
   which is exactly how both systems told them apart too: a search box was
   its own control, not a text box with a different label. */
input[type="text"][name="search"] {
  border-radius: var(--search-radius);
  background-image: var(--search-icon);
  background-repeat: no-repeat;
  background-position: left 0.55rem center;
  background-size: 0.85rem auto;
  padding-left: var(--search-padding-left);
}

.bbcode-size-select {
  height: 2rem;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  font-size: 0.78rem;
  padding: 0 0.4rem;
}
/* Font color -- an orange "A" rather than the browser's native color
   swatch, so it reads as a toolbar button matching the others in both
   look and height. The real <input type="color"> is still there
   underneath, just invisible and stretched over the whole button --
   clicking anywhere on it opens the native picker. */
.bbcode-color-btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 2rem;
  width: 2rem;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}
.bbcode-color-btn:hover { border-color: var(--accent); }
.bbcode-color-letter {
  color: var(--accent);
  font-weight: 800;
  font-size: 0.95rem;
  pointer-events: none;
}
.bbcode-color-btn input[type="color"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  width: 100%;
  height: 100%;
  border: none;
  padding: 0;
  cursor: pointer;
}
/* The smiley. An icon among text labels, so it needs the flex centring the
   letter buttons get from their own text -- and the same 2rem box, or it
   would sit a pixel or two off the row. */
.bbcode-emote-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2rem;
  padding: 0;
}
.bbcode-emote-btn svg { display: block; }

/* ---- emote picker ---------------------------------------------------------
   One overlay for the page, opened by whichever post box's smiley was
   clicked. Same dialog furniture as .ban-overlay and the identity export
   box: fixed backdrop at z-index 100, a panel with a fixed height whose
   BODY is the part that scrolls, so the search field and the category tabs
   stay put while two thousand emotes move under them. */
.emote-picker {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.55);
}
.emote-picker[hidden] { display: none; }
.emote-picker-panel {
  display: flex;
  flex-direction: column;
  width: 32rem;
  max-width: 100%;
  /* Capped against the viewport as well as fixed, so it cannot grow taller
     than a phone screen and put its own scrollbar out of reach. */
  height: 28rem;
  max-height: 100%;
  background: var(--bg-raised);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 10px 40px var(--shadow-strong);
  overflow: hidden;
}
.emote-picker-head {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  padding: 0.6rem;
  border-bottom: 1px solid var(--border);
}
.emote-picker-search {
  flex: 1;
  min-width: 0;
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.4rem 0.6rem;
  font: inherit;
  font-size: 0.85rem;
}
.emote-picker-close {
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 1.3rem;
  line-height: 1;
  padding: 0 0.3rem;
  cursor: pointer;
}
.emote-picker-close:hover { color: var(--text); }
.emote-picker-tabs {
  display: flex;
  gap: 0.25rem;
  overflow-x: auto;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--border);
  /* The nine standard categories plus ours do not fit across a phone; this
     row scrolls sideways rather than wrapping into three lines and eating
     the grid's height. */
  scrollbar-width: thin;
}
.emote-tab {
  flex: 0 0 auto;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 999px;
  padding: 0.2rem 0.6rem;
  font-size: 0.72rem;
  white-space: nowrap;
  cursor: pointer;
}
.emote-tab:hover { color: var(--text); border-color: var(--accent); }
.emote-picker-body {
  flex: 1;
  overflow-y: auto;
  padding: 0.6rem;
}
/* Two thousand tiles is a lot of layout to do at once. content-visibility
   lets the browser skip everything below the fold until it is scrolled to,
   and the intrinsic size keeps the scrollbar roughly honest in the
   meantime. Paired with loading="lazy" on the images themselves, opening
   the picker costs about a screenful of work rather than all of it. */
.emote-section {
  content-visibility: auto;
  contain-intrinsic-size: auto 12rem;
}
.emote-section[hidden] { display: none; }
.emote-section-title {
  margin: 0.6rem 0 0.4rem;
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-dim);
}
.emote-section:first-child .emote-section-title { margin-top: 0; }
.emote-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 0.15rem;
}
.emote {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.05rem;
  height: 2.2rem;
  min-width: 2.2rem;
  background: none;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 0 0.2rem;
  cursor: pointer;
}
.emote img {
  width: 1.55rem;
  height: 1.55rem;
  /* The two-image pairings are the only emotes wider than one tile, so
     they shrink rather than pushing the button out of the row. */
  min-width: 0;
  object-fit: contain;
  pointer-events: none;
}
.emote[hidden] { display: none; }
.emote:hover { background: var(--accent-soft); border-color: var(--accent); }
.emote-picker-foot {
  padding: 0.45rem 0.6rem;
  border-top: 1px solid var(--border);
  color: var(--text-dim);
  font-size: 0.75rem;
  /* Shows the hovered emote's shortcode. Fixed to one line so the panel
     does not change height as the pointer moves across the grid. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.bbcode-poll-panel {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.6rem;
}
.bbcode-poll-panel[hidden] { display: none; }
.bbcode-poll-question, .bbcode-poll-option {
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.4rem 0.6rem;
  font: inherit;
  font-size: 0.85rem;
}
.bbcode-poll-options { display: flex; flex-direction: column; gap: 0.3rem; }
.bbcode-poll-add-option, .bbcode-poll-create, .bbcode-poll-cancel {
  align-self: flex-start;
  background: var(--bg-card);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.35rem 0.7rem;
  font-size: 0.8rem;
  cursor: pointer;
}
.bbcode-poll-create { border-color: var(--accent); }
.bbcode-poll-row { display: flex; gap: 0.5rem; }

button, .mod-btn {
  background-color: var(--accent);
  background-image: var(--btn-image);
  color: var(--btn-text);
  border: var(--btn-border);
  border-radius: var(--btn-radius);
  box-shadow: var(--btn-shadow);
  padding: 0.5rem 1rem;
  min-height: 2.35rem;
  cursor: pointer;
  font: inherit;
  font-weight: 600;
  font-size: 0.95rem;
}
/* Ghost buttons: the ones that are a word you can click, not a control --
   dismiss crosses, "hide", "show", the icon buttons in the header.
   They each already clear the primary background and border, but a theme
   can now add a gradient and a raised shadow to `button`, and neither of
   those is something thirteen separate rules should have to know about.
   Stated once, here, so adding a chrome variable never sprouts little
   embossed boxes around every inline link-button on the site. */
.icon-btn, .post-hide-btn, .post-hidden-show, .post-edit-btn, .mark-all-read-btn,
.backlink-arrow, .mana-events-clear-btn, .mana-reply-btn, .mana-block-btn,
.mana-login-link, .identity-export-close, .post-spell-menu-close,
.bb-poll-results-btn, .settings-block-undo {
  background-image: none;
  box-shadow: none;
  border: none;
}

/* The submit button on a post form is the default action -- see
   --btn-default-image. */
.post-form button[type="submit"] {
  background-image: var(--btn-default-image);
  border: var(--btn-default-border);
  color: var(--btn-default-text);
  box-shadow: var(--btn-default-shadow);
}

button:hover, .mod-btn:hover {
  filter: brightness(1.1);
  background-image: var(--btn-hover-image);
  box-shadow: var(--btn-hover-shadow);
}
button:active, .mod-btn:active {
  filter: brightness(0.95);
  background-image: var(--btn-active-image);
  box-shadow: var(--btn-active-shadow);
}
button:disabled, .mod-btn:disabled { cursor: default; opacity: 0.6; filter: none; }

/* Secondary/inline moderation buttons read as UI chrome, not primary CTAs --
   quieter styling keeps them from competing with "Post a Feel" / "Reply". */
.admin-controls .mod-btn, .ban-disclosure summary.mod-btn {
  background: var(--bg-raised);
  color: var(--text);
  border: 1px solid var(--border);
  font-size: 0.82rem;
  padding: 0.3rem 0.7rem;
  min-height: 1.9rem;
  font-weight: 600;
}
.admin-controls .mod-btn:hover, .ban-disclosure summary.mod-btn:hover { border-color: var(--text-dim); filter: none; }
.admin-controls button[value="delete"] { color: var(--danger); border-color: var(--danger); }

/* ---- posts --------------------------------------------------------------- */

.thread-list, .reply-list { display: flex; flex-direction: column; gap: 1.1rem; }

/* /new_replies/ -- new posts grouped by their parent thread. */
.mark-all-read-btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 6px;
  padding: 0.4rem 0.9rem;
  font-size: 0.82rem;
  cursor: pointer;
  margin-bottom: 1.25rem;
}
.mark-all-read-btn:hover { border-color: var(--accent); color: var(--text); }
.new-replies-groups { display: flex; flex-direction: column; gap: 1.75rem; }
.new-replies-group { display: flex; flex-direction: column; gap: 0.75rem; }
.new-replies-group-head {
  font-size: 0.85rem;
  font-weight: 700;
  color: var(--text-dim);
  padding-bottom: 0.4rem;
  border-bottom: 1px solid var(--border);
}
.new-replies-group-head a { font-weight: 700; }
.new-replies-group .thread-list { padding-left: 1rem; border-left: 2px solid var(--border); }

.post-card {
  position: relative;
  background-color: var(--bg-card);
  background-image: var(--surface-image);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  padding: 1.1rem 1.2rem;
}
.post-card.op { border-left: 3px solid var(--accent); }
.post-card.is-pinned {
  background: var(--accent-soft);
  border-color: var(--accent);
}
/* nowrap, unlike the old wrapping head: the backlink strip below is the
   one element here that's meant to absorb the leftover width and scroll
   its own overflow, which it can only do if the row refuses to wrap and
   hand it a second line instead. Everything else in the row is
   flex: none. */
.post-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: nowrap;
  /* Its own title bar when a theme asks for one: the margin is what pulls
     the strip out to the card's edges, and defaults to a plain bottom gap
     so a flat theme keeps the inset head it always had. */
  margin: var(--card-head-margin);
  padding: var(--card-head-padding);
  background-color: var(--card-head-bg);
  background-image: var(--card-head-image);
  border-bottom: var(--card-head-border);
  border-radius: var(--card-head-radius);
}
.post-head > *:not(.post-backlinks) { flex: none; }
.poster-badge {
  background-color: var(--tag-bg);
  background-image: var(--badge-image);
  border: var(--badge-border);
  box-shadow: var(--badge-shadow);
  color: var(--text);
  border-radius: var(--radius-xs);
  padding: 0.15rem 0.55rem;
  font-size: 0.8rem;
  font-weight: 700;
}
.post-time { color: var(--text-dim); font-size: 0.85rem; white-space: nowrap; }
.post-edited { color: var(--text-dim); font-size: 0.75rem; font-style: italic; }

/* The Feel's own number, which replaced the "Anonymous" badge that used to
   lead the head -- every post said Anonymous, so it distinguished nothing,
   while this is both the post's identity and the token you need to reply
   to it. Clicking inserts ">>123" into the reply box (custom.js). */
.post-no {
  background: var(--tag-bg);
  color: var(--text);
  border-radius: 4px;
  padding: 0.15rem 0.55rem;
  font-size: 0.8rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.post-no:hover { color: var(--accent); text-decoration: none; }

/* Backlink strip: every post that replied to this one, as ">>123" chips.
   Takes the leftover width of the head row and scrolls horizontally
   inside it, since a popular Feel collects far more replies than fit --
   the arrows either side are revealed by custom.js only when the track
   actually overflows, and scroll it. */
.post-backlinks {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 0.2rem;
}
.post-backlinks-track {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  gap: 0.4rem;
  overflow-x: auto;
  scroll-behavior: smooth;
  /* The arrows are the intended control; the native bar would add a
     second one plus a reserved gutter inside an already-short row. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.post-backlinks-track::-webkit-scrollbar { display: none; }
.backlink, .post-ref {
  color: var(--accent);
  font-size: 0.78rem;
  font-weight: 600;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.backlink:hover, .post-ref:hover { text-decoration: underline; }
.backlink-arrow {
  flex: none;
  background: none;
  border: none;
  color: var(--text-dim);
  font-size: 1rem;
  line-height: 1;
  padding: 0 0.15rem;
  cursor: pointer;
}
.backlink-arrow:hover { color: var(--accent); }
.backlink-arrow[hidden] { display: none; }

/* Cross-site chan reference ("/4chan/g/76543210") and the modal that opens
   the copy soupchan holds of it -- see chanlinks.py / chan.js. Styled
   distinctly from a local ">>123" reference: they behave differently
   (one scrolls the page, one opens a window onto another site) so they
   shouldn't look identical. */
.chan-ref {
  color: var(--text);
  background: var(--tag-bg);
  border-radius: 4px;
  padding: 0.05rem 0.4rem;
  font-size: 0.9em;
  font-weight: 600;
  white-space: nowrap;
}
.chan-ref:hover { color: var(--accent); text-decoration: none; }

.chan-modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1.5rem;
}
.chan-modal {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 10px;
  padding: 1rem 1.2rem;
  width: 40rem;
  max-width: 100%;
  max-height: 80vh;
  overflow-y: auto;
  box-shadow: 0 12px 40px var(--shadow-strong);
}
.chan-modal-head { display: flex; align-items: baseline; gap: 0.6rem; flex-wrap: wrap; }
.chan-modal-ref { font-weight: 700; }
.chan-modal-meta, .chan-modal-fetched { color: var(--text-dim); font-size: 0.78rem; }
.chan-modal-subject { font-weight: 700; color: var(--accent); }
.chan-modal-status { color: var(--text-dim); }
/* pre-wrap: the body is plain text carrying the source post's own line
   breaks and greentext, which are most of what a quoted chan post is. */
.chan-modal-body {
  white-space: pre-wrap;
  overflow-wrap: break-word;
  line-height: 1.6;
  border-left: 2px solid var(--border);
  padding-left: 0.8rem;
}
.chan-modal-image-link, .chan-modal-source { font-size: 0.85rem; font-weight: 600; }
.chan-modal-foot { display: flex; align-items: baseline; gap: 0.8rem; flex-wrap: wrap; }
.chan-modal-close {
  align-self: flex-start;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.35rem 0.8rem;
  font-size: 0.82rem;
  cursor: pointer;
}
.chan-modal-close:hover { border-color: var(--accent); }

/* Hover preview over a ">>123" reference or a backlink chip -- the real
   rendered post, fetched from /api/post.json. Positioned by custom.js. */
.post-preview {
  position: absolute;
  z-index: 40;
  max-width: 32rem;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 0.6rem 0.8rem;
  box-shadow: 0 8px 24px var(--shadow);
  font-size: 0.9rem;
  pointer-events: none;
}
.post-preview-head { color: var(--text-dim); font-size: 0.75rem; margin-bottom: 0.3rem; }
.post-preview-body { overflow-wrap: break-word; max-height: 18rem; overflow: hidden; }

/* Post-submission edit box (posting.EDIT_WINDOW_SECONDS). */
.post-edit-btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 6px;
  padding: 0.15rem 0.5rem;
  margin-left: 0.6rem;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}
.post-edit-btn:hover { border-color: var(--accent); color: var(--accent); }
.post-edit-box { display: flex; flex-direction: column; gap: 0.4rem; margin-top: 0.5rem; }
.post-edit-box textarea {
  background: var(--bg);
  border: 1px solid var(--accent);
  color: var(--text);
  border-radius: 6px;
  padding: 0.6rem;
  font: inherit;
  min-height: 6rem;
  resize: vertical;
}
.post-edit-row { display: flex; align-items: center; gap: 0.5rem; }
.post-edit-row button {
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.3rem 0.7rem;
  font-size: 0.8rem;
  cursor: pointer;
}
.post-edit-row button:first-child { border-color: var(--accent); }
.post-edit-countdown { color: var(--text-dim); font-size: 0.78rem; margin-left: auto; }
.post-edit-error { color: var(--danger); font-size: 0.8rem; }
.capcode { color: var(--danger); font-weight: 700; font-size: 0.8rem; }
.tag-badge {
  background-color: var(--tag-bg);
  background-image: var(--badge-image);
  border: var(--badge-border);
  box-shadow: var(--badge-shadow);
  color: var(--text-dim);
  border-radius: var(--radius-xs);
  padding: 0.15rem 0.55rem;
  font-size: 0.8rem;
  font-weight: 600;
}
.tag-badge:hover { color: var(--text); text-decoration: none; }

.post-body {
  font-size: 1.02rem;
  line-height: 1.65;
  white-space: normal;
  overflow-wrap: break-word;
  /* Comfortable reading measure even inside a wide card -- long single-line
     posts still wrap at a legible line length instead of stretching edge
     to edge of a 980px container. */
  max-width: 68ch;
}
/* A flex row rather than inline children separated by collapsed
   whitespace, which is what put "hide" hard against the reply count with a
   single space between them. */
.post-foot {
  margin-top: 0.65rem;
  display: flex;
  align-items: center;
  gap: 0.9rem;
  flex-wrap: wrap;
}

.reply-count, .parent-link { font-size: 0.85rem; font-weight: 600; }

/* Hidden by default; custom.js adds .is-visible + sets the count once it
   knows this thread has been seen before with fewer replies than it has
   now. Empty/.is-visible-less elements stay display:none so there's never
   an empty pill flashing before JS runs. */
.new-badge {
  display: none;
  position: absolute;
  top: -0.55rem;
  right: -0.55rem;
  align-items: center;
  background: var(--accent);
  color: var(--accent-text);
  border-radius: 999px;
  padding: 0.25rem 0.65rem;
  font-size: 0.78rem;
  font-weight: 700;
  line-height: 1;
  box-shadow: 0 1px 3px var(--shadow);
}
.new-badge:hover { color: var(--accent-text); text-decoration: none; }
.new-badge.is-visible { display: inline-flex; }

.audio-badge {
  display: inline-flex;
  align-items: center;
  color: var(--accent);
  background: var(--tag-bg);
  border-radius: 4px;
  padding: 0.15rem 0.4rem;
}

/* Sound only -- present in the DOM (so it actually plays) but takes up no
   visible space and is unreachable by mouse/keyboard/screen readers. */
.audio-embed {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  opacity: 0;
  pointer-events: none;
  border: 0;
}

.admin-controls { margin-top: 0.75rem; display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: flex-start; }
.inline-form { display: inline-flex; gap: 0.4rem; align-items: center; }
.ban-disclosure summary { cursor: pointer; list-style: none; }
.ban-disclosure summary::-webkit-details-marker { display: none; }
.ban-form { margin-top: 0.5rem; flex-wrap: wrap; }
.ban-form input[type="text"] {
  background: var(--bg); border: 1px solid var(--border); color: var(--text);
  border-radius: 6px; padding: 0.35rem 0.5rem; font: inherit; font-size: 0.85rem;
}

/* ---- pagination ----------------------------------------------------------- */

.pager { display: flex; gap: 0.35rem; align-items: center; margin: 1.5rem 0; flex-wrap: wrap; font-size: 0.9rem; }
.pager-link, .pager-current, .pager-gap, .pager-prev, .pager-next {
  padding: 0.3rem 0.65rem;
  border-radius: 6px;
  min-height: 2rem;
  display: inline-flex;
  align-items: center;
}
.pager-link:hover, .pager-prev:hover, .pager-next:hover { background: var(--bg-card); text-decoration: none; }
.pager-current { background: var(--accent); color: var(--accent-text); font-weight: 700; }
.pager-gap { color: var(--text-dim); }
.pager-prev, .pager-next { font-weight: 600; color: var(--text); }

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

.mod-table { width: 100%; border-collapse: collapse; margin: 1.25rem 0; font-size: 0.92rem; }
.mod-table th {
  border-bottom: 2px solid var(--border);
  padding: 0.5rem 0.6rem;
  text-align: left;
  font-weight: 700;
  color: var(--text-dim);
  text-transform: uppercase;
  font-size: 0.75rem;
  letter-spacing: 0.04em;
}
.mod-table td { border-bottom: 1px solid var(--border); padding: 0.55rem 0.6rem; text-align: left; }
.mod-table tr:hover td { background: var(--bg-card); }
.mod-image-thumb { max-width: 8rem; max-height: 8rem; border-radius: 6px; display: block; }
.mod-tag { background: var(--tag-bg); color: var(--text-dim); border-radius: 4px; padding: 0.1rem 0.4rem; font-size: 0.78rem; font-weight: 600; }

/* ---- filter test (mod-only dry run, views/filter_test.py) --------------- */

.filter-test-result {
  margin: 0.75rem 0 1.5rem;
  padding: 0.75rem 1rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  background: var(--bg-card);
  font-size: 0.88rem;
}
.filter-test-result.is-clean { border-color: #4caf7a; }
.filter-test-result.is-rejected { border-color: var(--danger); }
.filter-test-verdict { font-weight: 700; margin-bottom: 0.4rem; }
.filter-test-result.is-clean .filter-test-verdict { color: #4caf7a; }
.filter-test-result.is-rejected .filter-test-verdict { color: var(--danger); }
.filter-test-result ul { margin: 0.2rem 0; padding-left: 1.2rem; color: var(--text-dim); font-size: 0.85rem; }

.help-table code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.1rem 0.4rem;
  font-size: 0.88rem;
}

.mod-pills { display: flex; gap: 0.6rem; margin-bottom: 1.25rem; }
.mod-pills a { padding: 0.4rem 0.9rem; border-radius: 6px; background: var(--bg-card); border: 1px solid var(--border); color: var(--text); font-weight: 600; font-size: 0.9rem; }
.mod-pills a:hover { text-decoration: none; border-color: var(--text-dim); }
.mod-pills a.active { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }

/* ---- memes / stats -------------------------------------------------------- */

.meme-list { padding-left: 1.5rem; line-height: 1.8; }
.meme-score { color: var(--text-dim); }

.stats-chart { width: 100%; height: auto; color: var(--accent); }
.chart-label { font-size: 0.7rem; fill: var(--text-dim); }

.rules-list { line-height: 1.9; font-size: 1.02rem; padding-left: 1.4rem; }
.rules-list li { margin-bottom: 0.4rem; }

/* ---- bbcode --------------------------------------------------------------- */

.green-text { color: var(--green-text); }
/* A rendered :shortcode:. An <img>, not a character -- see soupchan/emoji.py
   for why. Sized in em so it tracks whatever text it sits in ([big], [small]
   and the size tag all still work on it), with width and height both set so
   the line does not reflow as the images arrive. */
.bb-emote {
  display: inline-block;
  width: 1.35em;
  height: 1.35em;
  vertical-align: -0.25em;
  /* Emote pairs (:eggplant_splash:) are two images that should read as one
     emote, so they sit tighter to each other than to surrounding words. */
  margin: 0 0.02em;
  object-fit: contain;
}
.bb-small { font-size: 0.75em; }
.bb-big { font-size: 1.5em; }
.bb-blink { animation: bb-blink 1s steps(1) infinite; }
@keyframes bb-blink { 50% { opacity: 0; } }
.bb-shake { display: inline-block; animation: bb-shake 0.2s infinite; }
@keyframes bb-shake {
  0% { transform: translate(3px, -2px) rotate(-2deg); }
  10% { transform: translate(-4px, 3px) rotate(2deg); }
  20% { transform: translate(4px, 2px) rotate(-1deg); }
  30% { transform: translate(-3px, -4px) rotate(2deg); }
  40% { transform: translate(3px, 3px) rotate(-2deg); }
  50% { transform: translate(-4px, -2px) rotate(1deg); }
  60% { transform: translate(4px, -3px) rotate(-2deg); }
  70% { transform: translate(-3px, 2px) rotate(2deg); }
  80% { transform: translate(3px, -3px) rotate(-1deg); }
  90% { transform: translate(-4px, 4px) rotate(2deg); }
  100% { transform: translate(3px, -2px) rotate(-2deg); }
}
@media (prefers-reduced-motion: reduce) {
  .bb-blink, .bb-shake { animation: none; }
}
.bb-spoiler { background: #000; color: #000; border-radius: 3px; padding: 0 3px; }
.bb-spoiler:hover, .bb-spoiler:focus { color: #fff; }
.bb-glow {
  color: var(--accent);
  animation: bb-glow 1.6s ease-in-out infinite;
}
@keyframes bb-glow {
  0%, 100% { text-shadow: 0 0 4px var(--accent), 0 0 10px var(--accent); }
  50% { text-shadow: 0 0 10px var(--accent), 0 0 22px var(--accent), 0 0 32px var(--accent); }
}
@media (prefers-reduced-motion: reduce) {
  .bb-glow { animation: none; text-shadow: 0 0 8px var(--accent); }
}

/* Mana-gated spells (docs/mana-design.md §10) -- cosmetic only, on purpose. */
.bb-sparkle {
  background: linear-gradient(90deg, var(--text) 0%, var(--text) 40%, #fff 50%, var(--text) 60%, var(--text) 100%);
  background-size: 250% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: bb-sparkle 2.5s linear infinite;
}
@keyframes bb-sparkle {
  0% { background-position: 200% 0; }
  100% { background-position: -50% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .bb-sparkle { animation: none; color: var(--text); background: none; }
}
.bb-crown {
  display: inline-block;
  filter: drop-shadow(0 0 3px #ffd76a);
}
.bb-blood {
  color: #a10000;
  font-weight: 700;
  text-shadow: 0 0 3px #5c0000;
  animation: bb-blood-drip 3s ease-in infinite;
}
@keyframes bb-blood-drip {
  0% {
    text-shadow: 2px 3px 0 #7a0000, -3px 4px 0 #7a0000, 1px 2px 0 #7a0000, 0 0 3px #5c0000;
  }
  60% {
    text-shadow: 2px 15px 2px rgba(122, 0, 0, 0.55), -3px 20px 2px rgba(122, 0, 0, 0.35),
      1px 11px 2px rgba(122, 0, 0, 0.5), 0 0 3px #5c0000;
  }
  100% {
    text-shadow: 2px 3px 0 #7a0000, -3px 4px 0 #7a0000, 1px 2px 0 #7a0000, 0 0 3px #5c0000;
  }
}
@media (prefers-reduced-motion: reduce) {
  .bb-blood { animation: none; text-shadow: 2px 4px 0 #7a0000, -2px 4px 0 #7a0000; }
}

.bb-ticker {
  display: block;
  width: 100%;
  overflow: hidden;
  white-space: nowrap;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 0.3rem 0;
  margin: 0.4rem 0;
}
.bb-ticker-inner {
  display: inline-block;
  padding-left: 100%;
  animation: bb-ticker-scroll 10s linear infinite;
}
@keyframes bb-ticker-scroll {
  from { transform: translateX(0); }
  to { transform: translateX(-100%); }
}
@media (prefers-reduced-motion: reduce) {
  .bb-ticker { overflow-x: auto; }
  .bb-ticker-inner { animation: none; padding-left: 0; }
}

.bb-img { display: block; margin: 0.5rem 0; }
.bb-img-thumb {
  max-width: 16rem;
  max-height: 16rem;
  border-radius: 8px;
  border: 1px solid var(--border);
  cursor: zoom-in;
  display: block;
}
.bb-img-pending { color: var(--text-dim); font-size: 0.82rem; font-style: italic; }

.bb-poll {
  display: block;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.7rem 0.8rem;
  margin: 0.5rem 0;
  max-width: 26rem;
}
.bb-poll-question { font-weight: 700; margin-bottom: 0.5rem; }
.bb-poll-vote-form { display: flex; flex-direction: column; gap: 0.4rem; }
.bb-poll-option-row { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; font-size: 0.9rem; }
.bb-poll-btn-row { display: flex; gap: 0.5rem; margin-top: 0.3rem; }
.bb-poll-vote-btn {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 6px;
  padding: 0.4rem 0.9rem;
  font-size: 0.85rem;
  cursor: pointer;
}
.bb-poll-vote-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.bb-poll-result-row {
  position: relative;
  height: 1.6rem;
  border-radius: 4px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  margin-bottom: 0.35rem;
  overflow: hidden;
}
.bb-poll-result-row.is-your-vote { border-color: var(--accent); }
.bb-poll-result-bar { position: absolute; inset: 0; width: 0; background: var(--tag-bg); }
.bb-poll-result-label {
  position: relative;
  display: flex;
  align-items: center;
  height: 100%;
  padding: 0 0.6rem;
  font-size: 0.82rem;
}
.bb-poll-footer { color: var(--text-dim); font-size: 0.75rem; margin-top: 0.2rem; }
.bb-poll-results-btn {
  margin-top: 0.5rem; /* "Back to voting" case -- standalone, not in .bb-poll-btn-row */
  background: none;
  color: var(--text-dim);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 0.35rem 0.8rem;
  font-size: 0.78rem;
  cursor: pointer;
}
.bb-poll-btn-row .bb-poll-results-btn { margin-top: 0; }
.bb-poll-results-btn:hover { border-color: var(--accent); color: var(--text); }

.bb-rainbow { animation: bb-rainbow 3s linear infinite; }
@keyframes bb-rainbow {
  0% { color: #ff3b3b; }
  16% { color: #ff9f3b; }
  33% { color: #d9e021; }
  50% { color: #3bff5e; }
  66% { color: #3bcaff; }
  83% { color: #a25bff; }
  100% { color: #ff3b3b; }
}

.bb-matrix {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: #00ff41;
  text-shadow: 0 0 4px #00ff41, 0 0 1px #00ff41;
}

.bb-glitch { display: inline-block; animation: bb-glitch-jitter 2.5s infinite; }
@keyframes bb-glitch-jitter {
  0%, 92%, 100% { transform: translate(0, 0); text-shadow: none; }
  93% { transform: translate(-2px, 1px); text-shadow: 2px 0 #ff3b6a, -2px 0 #3bd7ff; }
  95% { transform: translate(2px, -1px); text-shadow: -2px 0 #ff3b6a, 2px 0 #3bd7ff; }
  97% { transform: translate(-1px, 0); text-shadow: 1px 0 #ff3b6a, -1px 0 #3bd7ff; }
}

.bb-vhs {
  display: inline-block;
  text-shadow: 1px 0 rgba(255, 0, 90, 0.5), -1px 0 rgba(0, 220, 255, 0.5);
  animation: bb-vhs-flicker 1.4s infinite;
}
@keyframes bb-vhs-flicker {
  0%, 100% { opacity: 1; }
  10% { opacity: 0.85; }
  20% { opacity: 1; }
  55% { opacity: 0.92; }
  57% { opacity: 1; }
}

.bb-ghost { display: inline-block; animation: bb-ghost-drift 4s ease-in-out infinite; }
@keyframes bb-ghost-drift {
  0%, 100% { opacity: 0.4; transform: translateY(0); }
  50% { opacity: 0.95; transform: translateY(-3px); }
}

.bb-decay { display: inline-block; color: #b0a184; animation: bb-decay-rot 4s ease-in-out infinite; }
@keyframes bb-decay-rot {
  0%, 100% { filter: saturate(1) brightness(1); color: #b0a184; }
  50% { filter: saturate(0.2) brightness(0.75); color: #5c5342; }
}

.bb-possessed { display: inline-block; animation: bb-possessed-flash 3.4s infinite; }
@keyframes bb-possessed-flash {
  0%, 6%, 9%, 34%, 38%, 71%, 75%, 100% { color: inherit; text-shadow: none; }
  7%, 8%, 35%, 37%, 72%, 74% { color: #ff0022; text-shadow: 0 0 8px #ff0022; }
}

.bb-neon {
  color: var(--accent);
  text-shadow: 0 0 4px var(--accent), 0 0 10px var(--accent);
  animation: bb-neon-flicker 2.8s infinite;
}
@keyframes bb-neon-flicker {
  0%, 18%, 22%, 25%, 53%, 57%, 100% {
    opacity: 1;
    text-shadow: 0 0 4px var(--accent), 0 0 10px var(--accent), 0 0 18px var(--accent);
  }
  20%, 24%, 55% { opacity: 0.4; text-shadow: none; }
}

.bb-electric {
  display: inline-block;
  color: #6fe3ff;
  text-shadow: 0 0 4px #6fe3ff, 0 0 10px #2fb8ff;
  animation: bb-electric-jitter 0.15s infinite;
}
@keyframes bb-electric-jitter {
  0%, 100% { transform: translate(0, 0); }
  25% { transform: translate(0.5px, -0.5px); }
  50% { transform: translate(-0.5px, 0.5px); }
  75% { transform: translate(0.5px, 0.5px); }
}

.bb-hologram {
  display: inline-block;
  color: #7fe9ff;
  background-image: repeating-linear-gradient(
    rgba(127, 233, 255, 0.12) 0px, rgba(127, 233, 255, 0.12) 1px, transparent 2px, transparent 3px
  );
  text-shadow: 0 0 6px rgba(127, 233, 255, 0.7);
  animation: bb-hologram-flicker 2.2s infinite;
}
@keyframes bb-hologram-flicker {
  0%, 100% { opacity: 0.92; transform: translateY(0); }
  50% { opacity: 1; transform: translateY(-1px); }
  62% { opacity: 0.75; }
}

.bb-spin-letter { display: inline-block; animation: bb-spin-anim 1.8s linear infinite; }
@keyframes bb-spin-anim {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.bb-bounce { display: inline-block; animation: bb-bounce-anim 0.9s ease-in-out infinite; }
@keyframes bb-bounce-anim {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

.bb-pulse { display: inline-block; animation: bb-pulse-anim 1.1s ease-in-out infinite; }
@keyframes bb-pulse-anim {
  0%, 100% { transform: scale(1); }
  25% { transform: scale(1.12); }
  40% { transform: scale(1); }
  55% { transform: scale(1.08); }
}

.bb-redacted { background: #000; color: transparent; border-radius: 2px; padding: 0 2px; }

.bb-wave span { display: inline-block; animation: bb-wave-bob 1.2s ease-in-out infinite; }
@keyframes bb-wave-bob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-6px); }
}

.bb-rot13 { cursor: help; border-bottom: 1px dotted var(--text-dim); position: relative; }
.bb-rot13:hover, .bb-rot13:focus { color: transparent; outline: none; }
.bb-rot13:hover::after, .bb-rot13:focus::after {
  content: attr(data-real);
  position: absolute;
  left: 0;
  top: 0;
  color: var(--text);
}

.bb-typewriter {
  display: inline-block;
  border-right: 2px solid var(--accent);
  padding-right: 2px;
  animation: bb-typewriter-caret 0.8s step-end infinite;
}
@keyframes bb-typewriter-caret {
  0%, 100% { border-color: var(--accent); }
  50% { border-color: transparent; }
}

.bb-scramble {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: var(--accent);
}

@media (prefers-reduced-motion: reduce) {
  .bb-rainbow, .bb-glitch, .bb-vhs, .bb-ghost, .bb-decay, .bb-possessed,
  .bb-neon, .bb-electric, .bb-hologram, .bb-spin-letter, .bb-bounce, .bb-pulse,
  .bb-wave span, .bb-typewriter {
    animation: none;
  }
}

.bb-code {
  background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
  padding: 0.7rem 0.8rem; overflow-x: auto;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
  font-size: 0.9rem; line-height: 1.55;
}

blockquote {
  margin: 0.75rem 0;
  padding: 0.4rem 0.9rem;
  border-left: 3px solid var(--border);
  color: var(--text-dim);
}

.site-footer { text-align: center; color: var(--text-dim); padding: 2.5rem 0 2rem; font-size: 0.85rem; }
.site-version { font-size: 0.7rem; opacity: 0.6; margin-top: 0.3rem; }


@media (max-width: 600px) {
  .site-header-inner { flex-direction: column; align-items: stretch; gap: 0.75rem; }
  h2 { font-size: 1.25rem; }
  .post-card { padding: 0.9rem; }
  .post-body { max-width: none; }
}


/* A flagged row in the moderation forensics tables. Deliberately a tint
   rather than a loud red: these are leads for a human to check, not
   verdicts, and styling them as alarms would encourage acting on them
   without reading the numbers beside them. */
.mod-table tr.mod-flagged td {
  background: color-mix(in srgb, var(--accent, #c05621) 14%, transparent);
  font-weight: 600;
}

/* ---- ban confirmation overlay (static/modban.js) -------------------------
   The ban form deletes every post from an IP scope by default, so this is
   the last thing standing between one click and dozens of posts the
   moderator never saw. Styled as a real dialog rather than a toast: it
   should be hard to dismiss by accident and impossible to miss. */
.ban-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: rgba(0, 0, 0, 0.55);
}
.ban-overlay[hidden] { display: none; }
.ban-overlay-box {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  width: 46rem;
  max-width: 100%;
  /* Fixed height, and the LIST inside is what scrolls -- see below. */
  max-height: 82vh;
  background: var(--bg-card);
  border: 1px solid var(--accent);
  border-radius: 10px;
  padding: 1rem 1.2rem;
  box-shadow: 0 12px 40px var(--shadow-strong);
}
.ban-overlay-title {
  font-weight: 700;
  font-size: 1.05rem;
}
.ban-overlay-summary {
  font-size: 0.88rem;
  line-height: 1.45;
}
/* The one place on this site where an inner scrollbar is the right answer:
   this is a fixed-height dialog, and letting a 400-post list size the box
   would put the Cancel and Confirm buttons off the bottom of the screen --
   on the single most destructive action in the panel. */
.ban-overlay-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg);
}
.ban-overlay-note, .ban-overlay-empty {
  padding: 0.6rem 0.7rem;
  font-size: 0.8rem;
  color: var(--text-dim);
}
.ban-overlay-post {
  padding: 0.5rem 0.7rem;
  border-bottom: 1px solid var(--border);
}
.ban-overlay-post:last-child { border-bottom: none; }
.ban-overlay-post-head {
  display: flex;
  gap: 0.6rem;
  align-items: baseline;
  flex-wrap: wrap;
  margin-bottom: 0.2rem;
}
.ban-overlay-post-id { font-weight: 700; font-size: 0.8rem; }
.ban-overlay-post-meta { color: var(--text-dim); font-size: 0.72rem; }
.ban-overlay-post-body {
  font-size: 0.82rem;
  white-space: pre-wrap;
  overflow-wrap: break-word;
}
.ban-overlay-actions {
  display: flex;
  gap: 0.6rem;
  justify-content: flex-end;
}
.ban-overlay-confirm {
  background: var(--danger, #b03030);
  color: #fff;
  border-color: var(--danger, #b03030);
}
.ban-overlay-confirm[disabled] { opacity: 0.5; cursor: not-allowed; }

/* ---- a Feel whose opening post was hidden -------------------------------
   The thread survives because its replies belong to other people, so this
   stands in for the OP's text -- which is never rendered at all, rather
   than rendered and hidden with CSS. See templates.redacted_body: blurred
   content is still served, and a site hiding something illegal behind a
   filter is still hosting it.

   Styled as an obvious absence rather than a subtle one: a moderator's
   removal should read as a removal, not as a rendering fault. */
.post-redacted {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  padding: 0.6rem 0.7rem;
  border: 1px dashed var(--border);
  border-radius: 8px;
  background: var(--bg);
}
.post-redacted-mark {
  display: block;
  height: 0.7rem;
  border-radius: 3px;
  /* Reads as struck-out text at a glance while being no text at all --
     there is nothing here to squint at, select, or recover. */
  background: repeating-linear-gradient(
    90deg,
    var(--text-dim) 0, var(--text-dim) 2.2rem,
    transparent 2.2rem, transparent 2.7rem
  );
  opacity: 0.35;
}
.post-redacted-note {
  font-size: 0.78rem;
  color: var(--text-dim);
  font-style: italic;
}

/* Feels inside the ban confirmation list. Hiding a reply affects only its
   author; hiding a Feel changes a thread other people are still talking
   in, so it is marked out rather than left to blend into the list. */
.ban-overlay-post.is-feel {
  background: color-mix(in srgb, var(--accent, #c05621) 10%, transparent);
  border-left: 3px solid var(--accent, #c05621);
}
.ban-overlay-feel-badge {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  padding: 0.05rem 0.3rem;
  border-radius: 3px;
  background: var(--accent, #c05621);
  color: #fff;
}
.ban-overlay-warn {
  padding: 0.6rem 0.7rem;
  font-size: 0.8rem;
  border-bottom: 1px solid var(--border);
  background: color-mix(in srgb, var(--accent, #c05621) 8%, transparent);
}
.ban-overlay-check {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.85rem;
  cursor: pointer;
}

.hidden-batch { margin: 1rem 0; }
.hidden-batch-head {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  flex-wrap: wrap;
  font-size: 0.85rem;
  margin-bottom: 0.35rem;
}

/* ---- "this is you" on the ban form --------------------------------------
   The 2026-08-16 accident was a click on the wrong post card: the ban
   controls are identical on every card, and the neighbouring one was the
   operator's own. This is the last thing between that click and its
   consequences, so it is deliberately loud -- red, first in the dialog,
   and it changes the confirmation wording underneath it. */
.ban-overlay-self {
  border: 2px solid #c0392b;
  border-radius: 8px;
  background: #c0392b;
  color: #fff;
  padding: 0.7rem 0.85rem;
}
.ban-overlay-self[hidden] { display: none; }
.ban-overlay-self-title {
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  margin-bottom: 0.35rem;
}
.ban-overlay-self-body {
  font-size: 0.85rem;
  line-height: 1.45;
}
.ban-overlay-self-body + .ban-overlay-self-body { margin-top: 0.4rem; opacity: 0.9; }
/* The whole dialog picks up the alarm, so it cannot be mistaken for the
   ordinary confirmation at a glance. */
.ban-overlay-box.is-self { border-color: #c0392b; }
.ban-overlay-box.is-self .ban-overlay-check { color: #c0392b; font-weight: 700; }

/* ---- the leaving-soupchan interstitial (views/outbound.py) --------------- */
.out-panel {
  max-width: 40rem;
  margin: 2rem auto;
  padding: 1.2rem 1.4rem;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.out-title { font-size: 1.15rem; font-weight: 700; margin-bottom: 0.5rem; }
.out-row {
  display: flex;
  gap: 0.7rem;
  padding: 0.55rem 0;
  border-top: 1px solid var(--border);
  font-size: 0.88rem;
}
.out-label { flex: 0 0 8rem; color: var(--text-dim); }
.out-value { flex: 1 1 auto; overflow-wrap: anywhere; }
/* The poster's own words about the destination. Italic and quoted so it
   reads as somebody's claim rather than as something the site is
   asserting -- the row above it is a fact, this one is a quotation. */
.out-claim { font-style: italic; }
/* A domain nobody has posted before is the warning worth colouring. */
.out-row.out-warn { background: color-mix(in srgb, #c0392b 12%, transparent); }
.out-actions { display: flex; gap: 0.6rem; margin-top: 1rem; flex-wrap: wrap; }
/* The button that leaves is deliberately not the prettiest thing on the
   page -- going is a choice, not the default. */
.out-go { border-color: var(--accent); }
.out-blocked { border-color: #c0392b; }
.bb-img-removed { color: var(--text-dim); font-size: 0.82rem; font-style: italic; }

/* ---- hidden posts + keyword filter (client-side only) -------------------
   Both live entirely in this browser's localStorage, like the theme: the
   server is never told what anyone has hidden, which is the privacy-
   preserving option and the one that needs no account.

   A filtered post COLLAPSES rather than disappearing. Removing it outright
   would leave a reader unable to tell a quiet thread from a filtered one,
   and would silently hide replies to it -- the stub keeps the thread
   readable and puts the decision back in the reader's hands. */
.post-card.is-user-hidden > *:not(.post-hidden-stub) { display: none; }
.post-card.is-user-hidden {
  padding: 0.5rem 0.9rem;
  opacity: 0.75;
}
.post-hidden-stub {
  display: flex;
  gap: 0.6rem;
  align-items: center;
  font-size: 0.78rem;
  color: var(--text-dim);
}
.post-hidden-reason { flex: 1 1 auto; overflow-wrap: anywhere; }
.post-hidden-show {
  flex: none;
  background: none;
  border: none;
  font: inherit;
  font-size: 0.78rem;
  color: var(--link);
  text-decoration: underline;
  cursor: pointer;
  padding: 0;
}
/* The per-post hide control: a minimise button at the right end of the post
   head, beside the spell icon -- where a window keeps its own. */
.post-hide-btn {
  flex: none;
  padding: 0.15rem;
  min-width: 1.4rem;
  min-height: 1.4rem;
  /* Pushed to the right end. When the spell icon is present it takes this
     job instead (it comes first and carries its own auto margin), and the
     adjacency rule below stops the two splitting the free space between
     them and drifting apart. */
  margin-left: auto;
}
/* Pulled back against the minimise bar so the two read as one cluster of
   post controls rather than two unrelated glyphs. Negative, because
   .post-head carries a 0.6rem flex gap that applies between every child --
   this cancels most of it and leaves the pair tighter with each other than
   either is with the rest of the row, which is what makes them group.
   Still a real gap, though: they do quite different things.

   It also cancels the auto margin the heart carries on its own. Both
   elements want to sit at the right end, and two auto margins would split
   the slack between them and push them apart -- so whichever comes FIRST
   keeps its auto and the second one gives its up. The pair is minimise
   then heart, in that order (mana.js appends). */
.post-hide-btn + .post-spell-icon-btn { margin-left: -0.4rem; }
.post-hide-btn:hover { color: var(--danger); background: var(--tag-bg); }

.settings-filter-textarea {
  width: 100%;
  box-sizing: border-box;
  background: var(--bg);
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 6px;
  padding: 0.4rem 0.5rem;
  font: inherit;
  font-size: 0.8rem;
  resize: vertical;
}
/* "Your own CSS is overriding this theme." Deliberately loud: it is the one
   piece of state that silently outranks every theme, and it was previously
   only discoverable by unfolding a panel nobody unfolds. */
.settings-css-active {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin: 0.5rem 0;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--danger);
  border-radius: var(--radius-sm);
  background: var(--accent-soft);
  font-size: 0.75rem;
  color: var(--text);
}
.settings-css-drop {
  flex: none;
  padding: 0.15rem 0.5rem;
  min-height: 0;
  font-size: 0.72rem;
}

.settings-filter-row { display: flex; gap: 0.5rem; margin-top: 0.4rem; }
.settings-filter-status { font-size: 0.75rem; color: var(--text-dim); margin-top: 0.3rem; }

/* ---- moderation image gallery (contact sheet) ---------------------------
   A grid, not a list: the job is to scan hundreds quickly and spot the one
   that should not be there. */
.gal-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 0.8rem;
  margin-top: 0.8rem;
}
.gal-item {
  margin: 0;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.gal-item img {
  display: block;
  width: 100%;
  height: 9rem;
  object-fit: cover;
  background: var(--bg);
}
.gal-item figcaption {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  padding: 0.4rem 0.5rem;
  font-size: 0.72rem;
}
.gal-meta { color: var(--text-dim); }
