/**
 * BPMB UI — the shared design system (handle: `bpmb-ui-helpers`).
 *
 * THE SINGLE SOURCE OF TRUTH FOR EVERY `--bpmb-*` TOKEN.
 *
 * Pairs with public/js/bpmb-ui.js. Every other stylesheet in the pair (Free
 * public/editor/shortcodes/single/admin, and all 11 Pro stylesheets) declares
 * `bpmb-ui-helpers` as a dependency and CONSUMES these tokens. None of them may
 * declare a `--bpmb-*` token of its own.
 *
 * Before 4.0.0 the same tokens were re-declared in 15 files: 672 root-level
 * declarations, 164 distinct tokens, 49 exact duplicates and 21 genuine
 * CONFLICTS (the same token with different values in different files, so the
 * winner depended on stylesheet load order). Pro carried a whole parallel
 * `--bpmbp-*` set on top. That is all collapsed here.
 *
 * TOKEN LAYERS (spec 09 §4)
 *   Layer 1  Host theme / WordPress  -> --bx-* (BuddyX), --reign-* (Reign),
 *                                       --global-*, --wp--preset--*
 *   Layer 2  Wbcom semantic          -> --bpmb-*   (this file, :root below)
 *   Layer 3  Component locals        -> --bpmb-<component>-* on the component's
 *                                       own selector (see the component layer)
 *
 * THEME ADOPTION IS DELIBERATE. Spec 09 §4 says to drop the `--reign-*`
 * coupling and honor themes only via `--wp--preset--*`. We do NOT: Reign and
 * BuddyX are Wbcom's own flagship themes and the majority of installs, and
 * dropping adoption would stop the plugin matching its host theme's colours.
 * Instead each semantic colour keeps the full chain
 *      BuddyX -> Reign/global -> --wp--preset--* -> canonical hex
 * and only the TERMINAL fallback is reconciled to the spec 09 §A4 palette
 * (primary #2563eb, success #16a34a, danger #dc2626, warning #d97706). On a
 * themed site the theme still wins; on a standalone site you get the canonical
 * palette. Best of both, and no regression for the flagship themes.
 *
 * SPACING/RADIUS/TYPE keep their shipped values (space-md 16px, not spec 09's
 * 12px). Every existing component was authored against this scale; retuning it
 * re-flows all of them at once. The tighter spec scale lands with the component
 * rewrites (Phase 3c/3d), where each surface can be verified individually.
 *
 * @package BuddyPress_Member_Blog
 * @since   3.4.2
 * @since   4.0.0 Became the single token source; added the component library.
 */

/*--------------------------------------------------------------
# Layer 2 — semantic tokens (declared ONCE, here)
--------------------------------------------------------------*/
:root {

    /*
     * --- Brand / primary ------------------------------------
     *
     * THE SEAM. These eight tokens (primary, primary-hover, text-primary,
     * text-secondary, bg-primary, bg-secondary, border-color, border-color-dark)
     * are the ONLY ones a theme is expected to set. Everything tinted or shaded
     * below derives from them with color-mix, so one override recolours the ramp
     * in-hue instead of leaving a coral button on a blue tint.
     *
     * Semantic colours (success/danger/warning/info/scheduled) and the Google SERP
     * preview colours deliberately do NOT derive from the seam and are not
     * overridable — a "delete" button that is not red is a safety defect, and a
     * recoloured search preview is a wrong preview. See bpmb_color_tokens().
     *
     * `--wp--preset--color--accent` comes before `--primary`: TT4 and TT5 ship
     * `accent` and have no `primary` slug at all, so the old chain fell straight
     * through to the hex on the two most common block themes.
     */
    --bpmb-primary: var(--bx-color-accent, var(--color-theme-primary, var(--reign-colors-theme, var(--wp--preset--color--accent, var(--wp--preset--color--primary, #2563eb)))));
    /*
     * THE HOVER FILL — and its last resort has to follow the accent, not a blue we chose.
     *
     * The chain is right: a theme that states its own button-hover colour should win, that is
     * what the seam is for. What was wrong is where the chain ENDED. `#1d4ed8` is our default
     * blue, so on any theme that sets neither variable — a block theme, most generic themes —
     * a red accent button turned BLUE under the cursor. Exactly the "coral button on a blue
     * tint" the comment above this block warns about, three lines under the warning.
     *
     * The fallback is kept in its own token so the dark block can flip its direction without
     * disturbing the theme chain: hovering DARKENS on a light page and LIGHTENS on a dark one,
     * which is what themes themselves do (BuddyX Pro's own hover is #ff8989 against a #ff6b6b
     * accent). Overriding `--bpmb-primary-hover` directly in the dark block would have thrown
     * the theme's own value away.
     */
    --bpmb-primary-hover-fallback: #1d4ed8;
    --bpmb-primary-hover-fallback: color-mix(in srgb, var(--bpmb-primary) 85%, #000);
    --bpmb-primary-hover: var(--bx-color-button-bg-hover, var(--reign-accent-hover-color, var(--bpmb-primary-hover-fallback)));

    /*
     * Derived from the seam. Each token keeps its old literal on the line above as
     * the pre-color-mix fallback — the same two-declaration pattern already used by
     * --bpmb-primary-filled below.
     */
    --bpmb-primary-dark: #1d4ed8;
    --bpmb-primary-dark: color-mix(in srgb, var(--bpmb-primary) 82%, #000);
    --bpmb-primary-light: #e8f0fb;
    --bpmb-primary-light: color-mix(in srgb, var(--bpmb-primary) 12%, var(--bpmb-bg-primary));
    --bpmb-primary-light-hover: #dbeafe;
    --bpmb-primary-light-hover: color-mix(in srgb, var(--bpmb-primary) 18%, var(--bpmb-bg-primary));
    --bpmb-primary-light-border: #bfdbfe;
    --bpmb-primary-light-border: color-mix(in srgb, var(--bpmb-primary) 32%, var(--bpmb-bg-primary));
    --bpmb-primary-ultra-light: #eff6ff;
    --bpmb-primary-ultra-light: color-mix(in srgb, var(--bpmb-primary) 7%, var(--bpmb-bg-primary));

    --bpmb-secondary: #6b7280;
    --bpmb-secondary-hover: #545b62;
    --bpmb-secondary-light: #f8f9fa;
    --bpmb-secondary-light: color-mix(in srgb, var(--bpmb-text-primary) 3%, var(--bpmb-bg-primary));

    --bpmb-accent: var(--bpmb-primary);
    --bpmb-link-color: var(--bpmb-primary);

    /* --- Semantic status ------------------------------------ */
    --bpmb-success: #16a34a;

    /* TEXT ON A SUCCESS-COLOURED SURFACE — the same problem as `--bpmb-on-danger` below, failing in
     * the opposite mode, which is why neither was noticed.
     *
     * White on this green is 3.3:1. Near-black on it is 6.01:1. `--bpmb-text-light` is white in
     * light mode and (via BuddyX Pro's mapping) near-black in dark, so every filled success pill was
     * FAILING IN LIGHT MODE and passing in dark by accident — while the danger pills next to them
     * did exactly the reverse. A theme's mapping was the only thing deciding either.
     *
     * Fixed by darkening the green for FILLED use, the same move `--bpmb-primary-filled` already
     * makes for the accent and for the same reason, so a white label is safe in both modes and the
     * Accept/Decline pair reads as one control set instead of one white label and one black one.
     * White on the darkened green is 5.1:1. `--bpmb-success` itself is untouched: it is still the
     * right colour for a success ICON or BORDER, where the 3:1 non-text bar applies. */
    --bpmb-on-success: #fff;
    --bpmb-success-filled: #117c38;
    --bpmb-success-filled: color-mix(in srgb, var(--bpmb-success) 78%, #000);

    --bpmb-success-bg: #dcfce7;
    --bpmb-success-light: #ebf9eb;
    --bpmb-success-light-2: #d4edda;
    --bpmb-success-text: #155724;

    --bpmb-danger: #dc2626;

    /* TEXT ON A DANGER-COLOURED SURFACE — and it must NOT be `--bpmb-text-light`.
     *
     * `--bpmb-danger` is a fixed dark red in BOTH modes, so its foreground has to be fixed light in
     * both modes. `--bpmb-text-light` is not: **BuddyX Pro maps it at `:root`** in
     * `bp-plugin-support.css` (`--bpmb-text-light: var(--color-theme-white, #ecf0f1)`), and that
     * theme's "white" resolves to #0a0a0a in dark mode. So the delete button rendered near-black
     * text on the red — measured 4.1:1 against AA's 4.5.
     *
     * The theme's mapping is not wrong for what it is aiming at; most `--bpmb-text-light` uses sit
     * on `--bpmb-primary`, which DOES go pale in dark mode, and there dark text is correct. It only
     * breaks where the background is fixed. Hence a separate token rather than fighting the theme:
     * a name it does not map, for the one case where the foreground must not follow the mode.
     *
     * `--bpmb-on-primary` is the equivalent for the accent, and deliberately flips where this
     * deliberately does not. And per the same reasoning, the darkened hover below keeps the button
     * FILLED: the old hover inverted it to danger-red-on-danger-tint, which measured 2.67:1 in dark
     * and 4.35:1 in light — a hover state less readable than the resting one, in both modes. */
    --bpmb-on-danger: #fff;
    --bpmb-danger-strong: #b91c1c;
    --bpmb-danger-strong: color-mix(in srgb, var(--bpmb-danger) 85%, #000);

    --bpmb-danger-bg: #fee2e2;
    --bpmb-danger-light: #fce8ea;
    --bpmb-danger-light-hover: #fee2e2;
    --bpmb-danger-light-border: #fecaca;
    --bpmb-danger-ultra-light: #fef2f2;
    --bpmb-danger-text: #721c24;

    --bpmb-warning: #d97706;
    --bpmb-warning-bg: #fef3c7;
    --bpmb-warning-light: #fff8e1;
    --bpmb-warning-light-2: #fff3cd;
    --bpmb-warning-text: #92400e;

    --bpmb-info: #2563eb;
    --bpmb-info-bg: #dbeafe;
    --bpmb-info-light: #e7f6f8;
    --bpmb-info-light-2: #d1ecf1;
    --bpmb-info-text: #0c5460;

    --bpmb-scheduled: #6f42c1;
    --bpmb-scheduled-light: #e2d9f3;
    --bpmb-scheduled-light-2: #f3e8ff;
    --bpmb-scheduled-text: #4a2c8a;

    --bpmb-featured: #f59e0b;
    --bpmb-unlimited: #059669;

    /* --- Text ------------------------------------------------ */
    --bpmb-text-primary: var(--bx-color-fg, var(--reign-site-body-text-color, var(--global-font-color, var(--wp--preset--color--contrast, #111827))));
    --bpmb-text-secondary: var(--bx-color-fg-muted, var(--reign-site-alternate-text-color, var(--global-font-color, #6b7280)));
    /*
     * Derived, and a contrast fix at the same time. As literals these measured
     * 2.56:1 and 2.54:1 on white — both below WCAG AA — and they assumed a white
     * surface, so on a dark theme they were computed against the wrong ground.
     * Mixing toward the actual surface tracks both modes.
     */
    /*
     * 85%, not 78%. At 78% this resolved to rgb(119,119,119) on white — 4.48:1, missing AA by
     * two hundredths, which is a fail with no margin for a theme whose secondary text sits a
     * shade lighter than BuddyX's. Measured on the composer's upload hint, the smallest real
     * use of it. 85% gives 5.41:1 and is still clearly a step lighter than `--bpmb-text-secondary`
     * (8.06:1), which is the only job this token has.
     */
    --bpmb-text-tertiary: #767676;
    --bpmb-text-tertiary: color-mix(in srgb, var(--bpmb-text-secondary) 85%, var(--bpmb-bg-primary));
    --bpmb-text-muted: #9ca3af;
    --bpmb-text-muted: color-mix(in srgb, var(--bpmb-text-secondary) 85%, var(--bpmb-bg-primary));
    --bpmb-text-light: #fff;
    --bpmb-text-dark: #111827;
    --bpmb-text-color: var(--bpmb-text-primary);

    /* --- Surfaces -------------------------------------------- */
    --bpmb-bg-primary: var(--bx-color-bg-elevated, var(--color-theme-white-box, var(--reign-site-sections-bg-color, var(--wp--preset--color--base, #fff))));
    --bpmb-bg-secondary: var(--bx-color-bg-muted, var(--reign-site-secondary-bg-color, var(--global-body-lightcolor, #f8f9fa)));
    --bpmb-bg-tertiary: #f2f2f2;
    --bpmb-bg-tertiary: color-mix(in srgb, var(--bpmb-text-primary) 6%, var(--bpmb-bg-primary));
    --bpmb-bg-hover: rgba(0, 0, 0, 0.05);
    --bpmb-bg-active: rgba(0, 0, 0, 0.08);
    --bpmb-bg-light: #f8fafc;
    --bpmb-bg-light: color-mix(in srgb, var(--bpmb-text-primary) 3%, var(--bpmb-bg-primary));
    --bpmb-bg-lighter: #f9fafb;
    --bpmb-bg-lighter: color-mix(in srgb, var(--bpmb-text-primary) 2%, var(--bpmb-bg-primary));
    --bpmb-bg-dark: #f2f4f6;
    --bpmb-bg-dark: color-mix(in srgb, var(--bpmb-text-primary) 7%, var(--bpmb-bg-primary));
    --bpmb-bg-white: #fff;
    --bpmb-bg-calendar: #f8fafc;
    --bpmb-bg-calendar: color-mix(in srgb, var(--bpmb-text-primary) 3%, var(--bpmb-bg-primary));
    --bpmb-white: #fff;

    /* TEXT ON A PRIMARY-COLOURED SURFACE.
     *
     * NOT `--bpmb-white`, and not a raw `#fff`. In dark mode `--bpmb-primary` becomes a LIGHT
     * accent (Reign resolves it to a pale blue), so "white on primary" inverts into white on
     * near-white. Measured on the follow button with Reign's toggle set to dark: 1.89:1, against a
     * WCAG AA requirement of 4.5:1 — effectively unreadable.
     *
     * So the foreground has to flip with the mode, and this token is what does it. Anything sitting
     * on `--bpmb-primary` uses this, never `#fff`. */
    --bpmb-on-primary: #fff;

    /* THE FILLED-BUTTON BACKGROUND — and it is NOT simply `--bpmb-primary`.
     *
     * `--bpmb-primary` is the THEME's accent, whatever the theme happens to pick, and a theme is
     * free to pick an accent that white text cannot sit on. Measured: Reign's blue (#1d76da) carries
     * white at 4.51:1 — just passes. BuddyX's coral (#ef5455) carries it at 3.46:1 — fails, and the
     * follow button has a text label, so it needs 4.5.
     *
     * And there is no single foreground that works everywhere: white fails on the coral, dark text
     * fails on the blue. The luminance of the accent decides, and CSS cannot branch on luminance
     * (`color-contrast()` is not shipped).
     *
     * So darken the accent instead, just enough that white is always safe. `color-mix()` keeps the
     * theme's HUE — a site that picked coral still gets coral, a shade deeper — rather than
     * abandoning the theme for a colour of our own. The plain `--bpmb-primary` line above it is the
     * fallback for a browser without color-mix.
     *
     * Not needed in dark mode: there the accent goes pale and `--bpmb-on-primary` goes dark, which
     * already measures 7.13:1 on BuddyX and 8.66:1 on Reign. */
    --bpmb-primary-filled: var(--bpmb-primary);
    --bpmb-primary-filled: color-mix(in srgb, var(--bpmb-primary) 78%, #000);

    /* THE ACCENT AS INK — accent-coloured TEXT on a page surface.
     *
     * The mirror image of `--bpmb-primary-filled` above, and it needs its own token for the same
     * reason: the theme's raw accent is chosen to be seen, not to be read. Measured as text on the
     * card surface (#f7f7f9): BuddyX Pro's red 3.62:1, BuddyX's coral 3.23:1 — both under the 4.5:1
     * AA needs for body-size text. Reign's blue and our own default clear it, so the failure depends
     * entirely on which accent the site picked, which is exactly what a token is for.
     *
     * Darkened the same way, keeping the theme's hue. Worst case across the accents we ship against
     * — BuddyX, BuddyX Pro, Reign, our default, a Twenty-x green — is coral at 4.97:1.
     *
     * IT LANDS ON THE SAME 78% AS `--bpmb-primary-filled` BY COINCIDENCE, NOT BY SHARED MEANING.
     * That one asks "how dark before WHITE is safe ON it", this one asks "how dark before IT is safe
     * on white". Two questions, two answers that currently agree. Keep them as two declarations so
     * that retuning one cannot silently retune the other.
     *
     * Dark mode resets it to the plain accent (see the dark block): there the accent is already pale
     * against a dark surface — the context link measures 7.13:1 untouched — and darkening it would
     * push it back towards the background. */
    --bpmb-primary-ink: var(--bpmb-primary);
    --bpmb-primary-ink: color-mix(in srgb, var(--bpmb-primary) 78%, #000);
    --bpmb-black: #000;
    --bpmb-btn-white: #fff;

    /* --- Borders --------------------------------------------- */
    --bpmb-border-color: var(--bx-color-border, var(--global-border-color, var(--reign-site-border-color, #e2e8f0)));
    --bpmb-border-color-dark: var(--bx-color-border-strong, #ccc);
    --bpmb-border-color-light: rgba(0, 0, 0, 0.1);
    --bpmb-border-color-input: #d0d5dd;
    --bpmb-border-color-input: color-mix(in srgb, var(--bpmb-border-color) 70%, var(--bpmb-text-primary));
    --bpmb-border-strong: rgba(0, 0, 0, 0.2);
    --bpmb-border-calendar: #e2e8f0;
    --bpmb-border-calendar: var(--bpmb-border-color);

    /* --- Grays (neutral ramp) -------------------------------- */
    --bpmb-gray-50: #f9fafb;
    --bpmb-gray-100: #f8fafc;
    --bpmb-gray-200: #e5e7eb;
    --bpmb-gray-300: #e2e8f0;
    --bpmb-gray-400: #d1d5db;
    --bpmb-gray-500: #9ca3af;
    --bpmb-gray-600: #6b7280;
    --bpmb-gray-700: #374151;
    --bpmb-gray-800: #1f2937;
    --bpmb-gray-900: #334155;
    --bpmb-gray-950: #667085;

    /* --- Overlays -------------------------------------------- */
    --bpmb-overlay-bg-rgb: 255, 255, 255;
    --bpmb-overlay-lightest: rgba(0, 0, 0, 0.03);
    --bpmb-overlay-lighter: rgba(0, 0, 0, 0.05);
    --bpmb-overlay-hover: rgba(0, 0, 0, 0.1);
    --bpmb-overlay-dashed: rgba(0, 0, 0, 0.2);
    --bpmb-overlay-light: rgba(0, 0, 0, 0.3);
    --bpmb-overlay-medium: rgba(0, 0, 0, 0.5);
    --bpmb-overlay-dark: rgba(0, 0, 0, 0.6);
    --bpmb-overlay-heavy: rgba(0, 0, 0, 0.75);

    /* --- Spacing (shipped scale — see header note) ----------- */
    --bpmb-space-xs: 4px;
    --bpmb-space-sm: 8px;
    --bpmb-space-md: 16px;
    --bpmb-space-lg: 24px;
    --bpmb-space-xl: 32px;
    --bpmb-space-2xl: 48px;

    /* --- Radius ---------------------------------------------- */
    --bpmb-radius-sm: 4px;
    --bpmb-radius-md: 6px;
    --bpmb-radius-lg: 10px;
    --bpmb-radius-xl: 16px;
    --bpmb-radius-full: 100px;
    /* Aliases used by the Pro skins — same scale, older names. */
    --bpmb-border-radius-sm: 4px;
    --bpmb-border-radius: 6px;
    --bpmb-border-radius-md: 8px;
    --bpmb-border-radius-lg: 12px;
    --bpmb-border-radius-xl: 20px;

    /* --- Typography ------------------------------------------ */
    --bpmb-font-size-xs: 12px;
    --bpmb-font-size-sm: 13px;
    --bpmb-font-size-md: 14px;
    --bpmb-font-size-base: 15px;
    --bpmb-font-size-lg: 16px;
    --bpmb-font-size-xl: 18px;
    --bpmb-font-size-2xl: 20px;
    --bpmb-font-size-3xl: 22px;

    --bpmb-font-weight-normal: 400;
    --bpmb-font-weight-medium: 500;
    --bpmb-font-weight-semibold: 600;
    --bpmb-font-weight-bold: 700;

    --bpmb-line-height-tight: 1.2;
    --bpmb-line-height-base: 1.5;
    --bpmb-line-height-relaxed: 1.7;

    /* --- Shadows --------------------------------------------- */
    --bpmb-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
    --bpmb-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
    --bpmb-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
    --bpmb-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08);
    --bpmb-shadow-modal: 0 20px 60px rgba(0, 0, 0, 0.2);
    --bpmb-shadow-calendar: 0 10px 40px rgba(0, 0, 0, 0.15);
    --bpmb-shadow-overlay: rgba(0, 0, 0, 0.5);
    --bpmb-shadow-focus: 0 0 0 3px rgba(37, 99, 235, 0.25);

    /* --- Motion ---------------------------------------------- */
    --bpmb-transition-fast: 150ms ease;
    --bpmb-transition-base: 200ms ease;
    --bpmb-transition-normal: 200ms ease;
    --bpmb-transition-slow: 300ms ease;
    --bpmb-transition-slowest: 500ms ease;

    /* --- Z-index --------------------------------------------- */
    --bpmb-z-dropdown: 100;
    --bpmb-z-modal: 1000;
    --bpmb-z-tooltip: 1100;
    --bpmb-z-overlay: 99999;

    /* --- Component-ish singletons used across files ---------- */
    --bpmb-skeleton-base: #f0f0f0;
    --bpmb-skeleton-base: color-mix(in srgb, var(--bpmb-text-primary) 8%, var(--bpmb-bg-primary));
    --bpmb-skeleton-sheen: #e0e0e0;
    --bpmb-skeleton-sheen: color-mix(in srgb, var(--bpmb-text-primary) 14%, var(--bpmb-bg-primary));
    --bpmb-spinner-track: rgba(0, 0, 0, 0.15);
    --bpmb-progress-track: rgba(0, 0, 0, 0.08);

    --bpmb-input-bg: var(--bpmb-bg-primary);
    --bpmb-input-border-color: var(--bpmb-border-color-input);
    --bpmb-input-border-radius: var(--bpmb-radius-md);
    --bpmb-input-focus-border: var(--bpmb-primary);
    --bpmb-input-focus-shadow: var(--bpmb-shadow-focus);
    --bpmb-input-font-size: var(--bpmb-font-size-md);
    --bpmb-input-height: 40px;
    --bpmb-input-padding-x: 12px;
    --bpmb-input-padding-y: 8px;

    /* Google SERP preview (SEO panel) — intentionally literal brand colours. */
    --bpmb-google-title-color: #1a0dab;
    --bpmb-google-url-color: #006621;
    --bpmb-google-desc-color: #545454;
    --bpmb-google-desc-muted: #888;

    /* Calendar + series derived tokens (Pro consumes these). */
    --bpmb-text-calendar: #64748b;
    --bpmb-text-calendar: color-mix(in srgb, var(--bpmb-text-secondary) 92%, var(--bpmb-bg-primary));
    --bpmb-cal-published: var(--bpmb-success);
    --bpmb-cal-published-soft: var(--bpmb-success-bg);
    --bpmb-cal-scheduled: var(--bpmb-scheduled);
    --bpmb-cal-scheduled-soft: var(--bpmb-scheduled-light-2);

    /* THE SAME TWO STATUS COLOURS, AS INK.
     *
     * `--bpmb-cal-published` and `--bpmb-cal-scheduled` are the FULL-STRENGTH status colours: right
     * for a dot, a chip fill or a border, where the bar is 3:1. Used as TEXT they are not: measured
     * in dark mode, the day-add hover put the green on its own 18% tint at 3.89:1, and "Schedule a
     * post" put the violet on the panel at 2.54:1.
     *
     * `--bpmb-success-text` and `--bpmb-scheduled-text` already existed and already flip with the
     * mode - they were simply never wired to the calendar, so the calendar reached for the fill
     * colour and used it as a foreground. These two aliases close that, and keep Pro reading only
     * `--bpmb-cal-*` rather than having to know which underlying ramp each one comes from. */
    --bpmb-cal-published-ink: var(--bpmb-success-text);
    --bpmb-cal-scheduled-ink: var(--bpmb-scheduled-text);
    --bpmb-cal-draft: var(--bpmb-text-muted);
    --bpmb-cal-draft-soft: var(--bpmb-bg-secondary);

    /* Role badges (Pro co-authors). The author badge reuses the primary ramp;
       co-author gets its own violet so the two roles are distinguishable at a
       glance without relying on the label alone. */
    --bpmb-role-author-bg: var(--bpmb-primary-light-hover);
    --bpmb-role-author-fg: var(--bpmb-primary-dark);
    --bpmb-role-coauthor-bg: #f3e8ff;
    --bpmb-role-coauthor-fg: #7c3aed;

    --bpmb-series-primary: var(--bpmb-primary);
    --bpmb-series-primary-hover: var(--bpmb-primary-hover);
    --bpmb-series-success: var(--bpmb-success);
    --bpmb-series-bg: var(--bpmb-bg-primary);
    --bpmb-series-bg-light: var(--bpmb-bg-secondary);
    --bpmb-series-border: var(--bpmb-border-color);
    --bpmb-series-text: var(--bpmb-text-primary);
    --bpmb-series-text-muted: var(--bpmb-text-secondary);
    --bpmb-series-radius: var(--bpmb-radius-lg);
    --bpmb-series-shadow: var(--bpmb-shadow-sm);

    /* --- Icons (Lucide) -------------------------------------- */
    --bpmb-icon-stroke: 1.75;
    --bpmb-icon-xs: 12px;
    --bpmb-icon-sm: 14px;
    --bpmb-icon-md: 16px;
    --bpmb-icon-lg: 20px;
    --bpmb-icon-xl: 24px;

    /* --- Admin surface set (wp-admin only) ------------------- */
    --bpmb-admin-accent: var(--bpmb-primary);
    --bpmb-admin-accent-hover: var(--bpmb-primary-hover);
    --bpmb-admin-accent-light: #eff6ff;
    --bpmb-admin-success: var(--bpmb-success);
    --bpmb-admin-success-light: var(--bpmb-success-bg);
    --bpmb-admin-warn: var(--bpmb-warning);
    --bpmb-admin-warn-light: var(--bpmb-warning-bg);
    --bpmb-admin-danger: var(--bpmb-danger);
    --bpmb-admin-white: #fff;
    --bpmb-admin-surface-1: #fff;
    --bpmb-admin-surface-2: #f8fafc;
    --bpmb-admin-text: #1e293b;
    --bpmb-admin-text-2: #475569;
    --bpmb-admin-text-3: #64748b;
    --bpmb-admin-text-4: #334155;
    --bpmb-admin-text-muted: #94a3b8;
    --bpmb-admin-border: #e2e8f0;
    --bpmb-admin-border-strong: #cbd5e1;
    /*
     * Aliases consumed by Pro's admin stylesheet. These were NEVER declared
     * anywhere and carry no hex fallback, so every rule using them resolved to
     * an invalid value and was dropped by the browser — Pro's admin styling has
     * been silently partly-dead. Defining them here fixes that; they alias the
     * canonical set above rather than introducing new values.
     */
    --bpmb-admin-primary: var(--bpmb-admin-accent);
    --bpmb-admin-primary-dark: var(--bpmb-primary-dark);
    --bpmb-admin-primary-bg: var(--bpmb-admin-accent-light);
    --bpmb-admin-primary-border: var(--bpmb-primary-light-border);
    --bpmb-admin-primary-legacy: var(--bpmb-admin-accent);
    --bpmb-admin-accent-bg: var(--bpmb-admin-accent-light);

    --bpmb-admin-error: var(--bpmb-danger);
    --bpmb-admin-error-dark: #b91c1c;
    --bpmb-admin-error-darker: #991b1b;
    --bpmb-admin-error-bg: var(--bpmb-danger-bg);
    --bpmb-admin-error-border: var(--bpmb-danger-light-border);

    --bpmb-admin-success-bg: var(--bpmb-success-bg);
    --bpmb-admin-success-dark: #15803d;

    --bpmb-admin-warning-bg: var(--bpmb-warning-bg);
    --bpmb-admin-warning-border: #fcd34d;
    --bpmb-admin-warning-icon: var(--bpmb-warning);
    --bpmb-admin-warning-text: var(--bpmb-warning-text);

    --bpmb-admin-info: var(--bpmb-info);
    --bpmb-admin-info-bg: var(--bpmb-info-bg);
    --bpmb-admin-info-text: #1e40af;

    --bpmb-admin-surface: var(--bpmb-admin-surface-1);
    --bpmb-admin-surface-alt: var(--bpmb-admin-surface-2);
    --bpmb-admin-surface-subtle: #f8fafc;
    --bpmb-admin-surface-muted: #f1f5f9;
    --bpmb-admin-surface-hover: #f1f5f9;
    --bpmb-admin-surface-info: var(--bpmb-info-bg);

    --bpmb-admin-text-primary: var(--bpmb-admin-text);
    --bpmb-admin-text-secondary: var(--bpmb-admin-text-2);
    --bpmb-admin-text-dark: #0f172a;
    --bpmb-admin-text-medium: var(--bpmb-admin-text-4);
    --bpmb-admin-text-subtle: var(--bpmb-admin-text-muted);

    --bpmb-admin-border-light: #eef2f6;
    --bpmb-admin-border-muted: #f1f5f9;
    --bpmb-admin-disabled: #cbd5e1;
    --bpmb-admin-conditional-bg: #f8fafc;
    --bpmb-admin-conditional-text: var(--bpmb-admin-text-3);

    --bpmb-admin-danger-dark: #b91c1c;
    --bpmb-admin-danger-light: #fee2e2;
    --bpmb-admin-radius: 10px;
    --bpmb-admin-radius-sm: 6px;
    --bpmb-admin-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
    --bpmb-admin-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.08);
    --bpmb-admin-gap-sm: 8px;
    --bpmb-admin-gap-md: 16px;
    --bpmb-admin-gap-lg: 24px;
    --bpmb-admin-gap-xl: 32px;
    --bpmb-admin-tap: 40px;

    /*
     * Dark palette for the WordPress media modal (and one Editor.js popover
     * rule). A `-dark-` PREFIXED token is an anti-pattern under spec 09 - dark
     * mode should be the same token with a different value - and these hold
     * DARK values unconditionally, so every rule reading them MUST be scoped to
     * `body.bpmb-dark` itself. The media block was not, which is how 4.0.1
     * shipped a dark modal onto light-mode sites.
     *
     * The earlier note here claimed "the Editor.js/Medium chrome consumes these
     * names directly". It does not: every consumer is in the media-modal block
     * of bp-member-blog-editor.css, plus a single `.ce-popover` background.
     * `--bpmb-dark-text-muted` had no consumer at all and is gone.
     */
    --bpmb-dark-bg-primary: #1e1e1e;
    --bpmb-dark-bg-secondary: #2c3338;
    --bpmb-dark-bg-tertiary: #3c434a;
    --bpmb-dark-text-primary: #c3c4c7;

    /* 4.5:1 minimum on --bpmb-dark-bg-secondary, where the sidebar's 12px field
       labels sit. The previous #8c8f94 measured 3.95:1; this is 5.49:1. */
    --bpmb-dark-text-secondary: #a7aaad;
    --bpmb-dark-border-color: #3c434a;
    --bpmb-dark-border-color-light: #50575e;
    --bpmb-dark-accent: #72aee6;
    --bpmb-dark-accent-hover: #a3c9e6;
    --bpmb-dark-success: #46b450;
    --bpmb-dark-shadow: rgba(0, 0, 0, .15);
}

/*--------------------------------------------------------------
# Dark mode — root-level token overrides ONLY (spec 09 §4)
--------------------------------------------------------------
Never a per-component dark rule: dark mode is the SAME token with a different
value. Consolidated here from 491 scattered declarations across 15 files.
Selector list covers Reign (data-bx-mode), BuddyX, and generic conventions. */
:root[data-bx-mode="dark"],
:root[data-theme="dark"],
html.dark,
html.dark-mode,
body.dark-mode,
body.dark-scheme,
.bpmb-dark {
    /* Primary is a LIGHT accent in dark mode, so text on it must be DARK. Measured against Reign's
       dark palette: #1a2028 on the pale-blue accent is ~8.9:1, comfortably AA. */
    --bpmb-on-primary: var(--bpmb-bg-primary, #1a2028);

    /* In dark mode the accent is already pale and the label is dark, so leave the fill alone —
       darkening it here would only muddy it. Measured: 8.66:1 on Reign, 7.13:1 on BuddyX. */
    --bpmb-primary-filled: var(--bpmb-primary);

    /* Same for the accent used as ink: the pale accent on a dark surface already passes (7.13:1 on
       BuddyX Pro), and the light-mode darkening would push it back towards the background. */
    --bpmb-primary-ink: var(--bpmb-primary);

    /*
     * Hover LIGHTENS on a dark page, where light mode darkens.
     *
     * THE WHOLE CHAIN IS REPEATED HERE, and it has to be. Redefining only
     * `--bpmb-primary-hover-fallback` in this block does nothing: `--bpmb-primary-hover` is
     * DECLARED at `:root`, and a custom property's `var()`s are substituted where the property
     * is declared, not where it is read. The fallback token would be re-read at `:root` and this
     * override never consulted — measured, and the dark hover kept darkening.
     *
     * So the theme chain is written out again with the other tail. The theme still wins in both
     * modes; only the last resort differs. Two declarations as everywhere else in this file: the
     * first is what a browser without color-mix gets.
     */
    --bpmb-primary-hover: var(--bx-color-button-bg-hover, var(--reign-accent-hover-color, #60a5fa));
    --bpmb-primary-hover: var(--bx-color-button-bg-hover, var(--reign-accent-hover-color, color-mix(in srgb, var(--bpmb-primary) 85%, #fff)));

    /* THE ACCENT TINTS, AT AN ALPHA RATHER THAN MIXED INTO THE PAGE.
     *
     * Light mode mixes these into `--bpmb-bg-primary` to get an opaque pastel; dark mode wants an
     * alpha instead, so the panel underneath keeps showing through and the tint stays a tint rather
     * than a flat block that fights the surface it sits on.
     *
     * WHAT THEY MUST NOT DO — and did — IS FORGET THE THEME. These three were the plugin's own
     * default blue, hardcoded, so on a red-accent theme a chip rendered the theme's red text on a
     * BLUE tint. Visible on the series landing page in dark mode: the "Up next" chip. Light mode
     * derived them from the accent all along, so it was dark mode alone that dropped the theme.
     *
     * The literal rgba line above each one is the fallback for a browser without color-mix; it
     * keeps the old (blue) behaviour there rather than losing the tint entirely. */
    --bpmb-primary-light: rgba(37, 99, 235, 0.18);
    --bpmb-primary-light: color-mix(in srgb, var(--bpmb-primary) 18%, transparent);
    --bpmb-primary-light-hover: rgba(37, 99, 235, 0.26);
    --bpmb-primary-light-hover: color-mix(in srgb, var(--bpmb-primary) 26%, transparent);
    --bpmb-primary-ultra-light: rgba(37, 99, 235, 0.12);
    --bpmb-primary-ultra-light: color-mix(in srgb, var(--bpmb-primary) 12%, transparent);

    /* Role badges: the light-mode pastels go muddy on a dark surface — tint the
       surface instead, and lift the text to a shade that still passes contrast. */
    --bpmb-role-author-bg: rgba(37, 99, 235, 0.26);
    --bpmb-role-author-fg: #bfdbfe;
    --bpmb-role-coauthor-bg: rgba(124, 58, 237, 0.26);
    --bpmb-role-coauthor-fg: #ddd6fe;

    --bpmb-success-light: rgba(22, 163, 74, 0.18);
    --bpmb-success-light-2: rgba(22, 163, 74, 0.24);
    --bpmb-success-bg: rgba(22, 163, 74, 0.18);
    --bpmb-success-text: #86efac;

    --bpmb-danger-light: rgba(220, 38, 38, 0.18);
    --bpmb-danger-light-hover: rgba(220, 38, 38, 0.26);
    --bpmb-danger-ultra-light: rgba(220, 38, 38, 0.12);
    --bpmb-danger-bg: rgba(220, 38, 38, 0.18);
    --bpmb-danger-text: #fca5a5;

    --bpmb-warning-light: rgba(217, 119, 6, 0.18);
    --bpmb-warning-light-2: rgba(217, 119, 6, 0.24);
    --bpmb-warning-bg: rgba(217, 119, 6, 0.18);
    --bpmb-warning-text: #fcd34d;

    --bpmb-info-light: rgba(37, 99, 235, 0.18);
    --bpmb-info-light-2: rgba(37, 99, 235, 0.24);
    --bpmb-info-bg: rgba(37, 99, 235, 0.18);
    --bpmb-info-text: #93c5fd;

    --bpmb-scheduled-light: rgba(111, 66, 193, 0.2);
    --bpmb-scheduled-light-2: rgba(111, 66, 193, 0.26);
    --bpmb-scheduled-text: #c4b5fd;

    --bpmb-text-tertiary: #94a3b8;
    --bpmb-text-muted: #9ca3af;
    --bpmb-text-dark: #e5e7eb;

    --bpmb-bg-tertiary: rgba(255, 255, 255, 0.08);
    --bpmb-bg-hover: rgba(255, 255, 255, 0.06);
    --bpmb-bg-active: rgba(255, 255, 255, 0.1);
    --bpmb-bg-light: rgba(255, 255, 255, 0.04);
    --bpmb-bg-lighter: rgba(255, 255, 255, 0.03);
    --bpmb-bg-dark: rgba(255, 255, 255, 0.06);
    --bpmb-bg-calendar: rgba(255, 255, 255, 0.04);

    --bpmb-border-color-light: rgba(255, 255, 255, 0.12);
    --bpmb-border-color-input: rgba(255, 255, 255, 0.2);
    --bpmb-border-strong: rgba(255, 255, 255, 0.28);
    --bpmb-border-calendar: rgba(255, 255, 255, 0.12);

    --bpmb-overlay-bg-rgb: 26, 26, 26;
    --bpmb-overlay-lightest: rgba(255, 255, 255, 0.03);
    --bpmb-overlay-lighter: rgba(255, 255, 255, 0.05);
    --bpmb-overlay-hover: rgba(255, 255, 255, 0.1);
    --bpmb-overlay-dashed: rgba(255, 255, 255, 0.2);

    --bpmb-skeleton-base: rgba(255, 255, 255, 0.08);
    --bpmb-skeleton-sheen: rgba(255, 255, 255, 0.16);
    --bpmb-spinner-track: rgba(255, 255, 255, 0.2);
    --bpmb-progress-track: rgba(255, 255, 255, 0.12);

    --bpmb-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
    --bpmb-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.45);
    --bpmb-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
    --bpmb-shadow-hover: 0 4px 12px rgba(0, 0, 0, 0.45);

    --bpmb-google-title-color: #8ab4f8;
    --bpmb-google-url-color: #81c995;
    --bpmb-google-desc-color: #bdc1c6;
    --bpmb-google-desc-muted: #9aa0a6;

    --bpmb-text-calendar: #94a3b8;

    --bpmb-admin-surface-1: #0f172a;
    --bpmb-admin-surface-2: #1e293b;
    --bpmb-admin-text: #f1f5f9;
    --bpmb-admin-text-2: #cbd5e1;
    --bpmb-admin-text-3: #94a3b8;
    --bpmb-admin-text-4: #e2e8f0;
    --bpmb-admin-border: #334155;
    --bpmb-admin-border-strong: #475569;
    --bpmb-admin-accent-light: rgba(37, 99, 235, 0.16);
}

/*--------------------------------------------------------------
# Dark mode — the SEAM tokens, split by who owns the dark palette
--------------------------------------------------------------

The block above overrides 70 tokens and, until 4.0.2, not one of the eight seam
tokens. That was survivable on Reign and BuddyX, where `--bx-color-*` flips
underneath so the seam moved without anything here saying so. It was NOT
survivable anywhere else: a theme that only sets `html.dark` / `body.dark-mode`
(Blocksy, Kadence, GeneratePress and Astra dark add-ons all do) flipped those 70
tokens while the surfaces and text stayed LIGHT. The shipped result was a mixed
state — status text at 1.4-1.9:1 on white, and the translucent-white surfaces
above compositing over white into invisible panels and invisible borders.

It cannot be fixed with one rule, because the two families need opposite
treatment:

  - Reign/BuddyX set their OWN variables and flip them. Chain to those, so the
    plugin keeps the theme's exact dark shades.
  - The generic conventions do not expose a variable we can read. Chaining is
    actively wrong there: `--bx-color-bg-elevated` is still defined at `:root`
    with its LIGHT value, so `var(--bx-color-bg-elevated, #1a2028)` resolves to
    WHITE and the dark fallback never fires. They need literals.

Specificity settles the overlap: `[data-bx-mode="dark"]` is (0,2,0) and beats
`html.dark` at (0,1,1), so a Reign site that also carries a generic class still
gets the theme-aware values regardless of source order. */
:root[data-bx-mode="dark"],
:root[data-theme="dark"] {
    --bpmb-bg-primary: var(--bx-color-bg-elevated, var(--color-theme-white-box, var(--reign-site-sections-bg-color, #1a2028)));
    --bpmb-bg-secondary: var(--bx-color-bg-muted, var(--reign-site-secondary-bg-color, #232b36));
    --bpmb-text-primary: var(--bx-color-fg, var(--reign-site-body-text-color, #e6eaf2));
    --bpmb-text-secondary: var(--bx-color-fg-muted, var(--reign-site-alternate-text-color, #a9b3c4));
    --bpmb-border-color: var(--bx-color-border, var(--reign-site-border-color, #2f3945));
    --bpmb-border-color-dark: var(--bx-color-border-strong, #3d4757);
}

html.dark,
html.dark-mode,
body.dark-mode,
body.dark-scheme,
.bpmb-dark {
    --bpmb-bg-primary: #1a2028;
    --bpmb-bg-secondary: #232b36;
    --bpmb-text-primary: #e6eaf2;
    --bpmb-text-secondary: #a9b3c4;
    --bpmb-border-color: #2f3945;
    --bpmb-border-color-dark: #3d4757;
}

@media (prefers-color-scheme: dark) {
    :root[data-bx-mode="auto"] {
        /* Accent-derived, for the reason spelled out on the `.bpmb-dark` copy above: the status
           tints below are meant to be fixed colours, this one is meant to be the theme's. */
        --bpmb-primary-light: rgba(37, 99, 235, 0.18);
        --bpmb-primary-light: color-mix(in srgb, var(--bpmb-primary) 18%, transparent);
        --bpmb-success-light: rgba(22, 163, 74, 0.18);
        --bpmb-success-bg: rgba(22, 163, 74, 0.18);
        --bpmb-danger-light: rgba(220, 38, 38, 0.18);
        --bpmb-danger-bg: rgba(220, 38, 38, 0.18);
        --bpmb-warning-light: rgba(217, 119, 6, 0.18);
        --bpmb-warning-bg: rgba(217, 119, 6, 0.18);
        --bpmb-info-light: rgba(37, 99, 235, 0.18);
        --bpmb-info-bg: rgba(37, 99, 235, 0.18);
        --bpmb-text-muted: #9ca3af;
        --bpmb-bg-tertiary: rgba(255, 255, 255, 0.08);
        --bpmb-bg-hover: rgba(255, 255, 255, 0.06);
        --bpmb-skeleton-base: rgba(255, 255, 255, 0.08);
        --bpmb-skeleton-sheen: rgba(255, 255, 255, 0.16);
        --bpmb-overlay-bg-rgb: 26, 26, 26;

        --bpmb-role-author-bg: rgba(37, 99, 235, 0.26);
        --bpmb-role-author-fg: #bfdbfe;
        --bpmb-role-coauthor-bg: rgba(124, 58, 237, 0.26);
        --bpmb-role-coauthor-fg: #ddd6fe;
    }
}


/*--------------------------------------------------------------
# Utilities
--------------------------------------------------------------*/
.bpmb-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}

.bpmb-icon {
    display: inline-block;
    flex: 0 0 auto;
    vertical-align: middle;
}

body.bpmb-modal-open {
    overflow: hidden;
}

/*--------------------------------------------------------------
# Button
--------------------------------------------------------------*/
.bpmb-btn {
    --bpmb-btn-pad-y: 8px;
    --bpmb-btn-pad-x: 16px;
    --bpmb-btn-radius: 8px;
    --bpmb-btn-bg: var(--bpmb-bg-secondary, #f1f5f9);
    --bpmb-btn-fg: var(--bpmb-text-primary, #111827);
    --bpmb-btn-border: var(--bpmb-border-color, #e2e8f0);

    /*
     * BORDER-BOX, or the tap target below is only honoured on half the elements.
     *
     * `<button>` is `border-box` in the UA stylesheet, `<a>` is `content-box`. Without this,
     * `min-height: 40px` reads as 40px TOTAL on a button and 40px of CONTENT on a link, which then
     * adds 16px of padding and 2px of border - so the same class rendered 40px and 58px side by
     * side. The admin sheet carried the identical bug and it is what "the buttons are too big"
     * turned out to be.
     */
    box-sizing: border-box;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;

    /* 40px minimum tap target (spec 09 §4). */
    min-height: 40px;
    padding-block: var(--bpmb-btn-pad-y);
    padding-inline: var(--bpmb-btn-pad-x);

    font-size: 14px;
    font-weight: 500;
    line-height: 1.25;
    text-decoration: none;

    color: var(--bpmb-btn-fg);
    background-color: var(--bpmb-btn-bg);
    border: 1px solid var(--bpmb-btn-border);
    border-radius: var(--bpmb-btn-radius);

    cursor: pointer;
    transition: background-color .2s cubic-bezier(.4, 0, .2, 1),
                border-color .2s cubic-bezier(.4, 0, .2, 1),
                color .2s cubic-bezier(.4, 0, .2, 1);
}

.bpmb-btn:hover {
    filter: brightness(.96);
}

.bpmb-btn:focus-visible {
    outline: 2px solid transparent;
    outline-offset: 2px;
    box-shadow: 0 0 0 3px rgba(37, 99, 235, .35);
}

.bpmb-btn[disabled],
.bpmb-btn[aria-disabled="true"] {
    opacity: .6;
    cursor: not-allowed;
}

.bpmb-btn--primary {
    --bpmb-btn-bg: var(--bpmb-primary, #2563eb);
    --bpmb-btn-fg: #fff;
    --bpmb-btn-border: var(--bpmb-primary, #2563eb);
}

.bpmb-btn--secondary {
    --bpmb-btn-bg: transparent;
    --bpmb-btn-fg: var(--bpmb-text-primary, #111827);
    --bpmb-btn-border: var(--bpmb-border-color, #e2e8f0);
}

.bpmb-btn--danger {
    --bpmb-btn-bg: var(--bpmb-danger, #dc2626);
    --bpmb-btn-fg: #fff;
    --bpmb-btn-border: var(--bpmb-danger, #dc2626);
}

.bpmb-btn--ghost {
    --bpmb-btn-bg: transparent;
    --bpmb-btn-border: transparent;
}

/*--------------------------------------------------------------
# Modal — the generic dialog (bpmbModal)
#
# `.bpmb-modal` is owned EXCLUSIVELY by bpmbModal() in bpmb-ui.js, which builds
# the element and adds `.is-active` to reveal it. It is hidden by default here
# (opacity + visibility), so any static markup that borrows this class is
# invisible forever: jQuery .show()/.fadeIn() set `display` and `opacity`, and
# neither can touch `visibility`.
#
# That is exactly how the composer's preview dialog was lost — it was declared
# `class="bpmb-modal"` in the template and opened with .show(). Static dialog
# markup gets its OWN root class (see `.bpmb-preview-modal`).
--------------------------------------------------------------*/
.bpmb-modal {
    --bpmb-modal-width: 520px;
    --bpmb-modal-pad: 20px;
    --bpmb-modal-radius: 12px;
    --bpmb-modal-accent: var(--bpmb-primary, #2563eb);

    position: fixed;
    inset: 0;
    z-index: 100000;

    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;

    opacity: 0;
    visibility: hidden;

    /*
     * Transition opacity ONLY. `visibility` must flip synchronously with
     * .is-active: if it is transitioned it stays `hidden` for the duration of
     * the transition, and a browser refuses to move focus into an invisible
     * element — so the dialog would open with focus stranded on <body>.
     */
    transition: opacity .2s cubic-bezier(.4, 0, .2, 1);
}

.bpmb-modal.is-active {
    opacity: 1;
    visibility: visible;
}

.bpmb-modal--sm { --bpmb-modal-width: 420px; }
.bpmb-modal--lg { --bpmb-modal-width: 720px; }

.bpmb-modal--danger  { --bpmb-modal-accent: var(--bpmb-danger, #dc2626); }
.bpmb-modal--warning { --bpmb-modal-accent: var(--bpmb-warning, #d97706); }
.bpmb-modal--info    { --bpmb-modal-accent: var(--bpmb-info, #2563eb); }

.bpmb-modal__backdrop {
    position: absolute;
    inset: 0;
    background: rgba(15, 23, 42, .55);
}

.bpmb-modal__panel {
    position: relative;
    z-index: 1;

    display: flex;
    flex-direction: column;
    width: 100%;
    max-width: var(--bpmb-modal-width);
    max-height: calc(100vh - 32px);
    overflow: hidden;

    background: var(--bpmb-bg-primary, #fff);
    color: var(--bpmb-text-primary, #111827);
    border-radius: var(--bpmb-modal-radius);
    box-shadow: 0 20px 25px -5px rgba(0, 0, 0, .18), 0 8px 10px -6px rgba(0, 0, 0, .12);

    transform: translateY(8px) scale(.98);
    transition: transform .2s cubic-bezier(.4, 0, .2, 1);
}

.bpmb-modal.is-active .bpmb-modal__panel {
    transform: translateY(0) scale(1);
}

.bpmb-modal__head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;

    padding: var(--bpmb-modal-pad);
    padding-block-end: 0;
}

.bpmb-modal__title {
    margin: 0;
    font-size: 18px;
    font-weight: 600;
    line-height: 1.25;
    color: inherit;
}

/* An empty title still needs the close button to sit correctly. */
.bpmb-modal__title:empty {
    display: none;
}

.bpmb-modal__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;

    /* Icon-only control: keep the 40px target even though the glyph is 18px. */
    width: 40px;
    height: 40px;
    margin-block-start: -8px;
    margin-inline-end: -8px;

    color: var(--bpmb-text-secondary, #6b7280);
    background: none;
    border: 0;
    border-radius: 8px;
    cursor: pointer;
}

.bpmb-modal__close:hover {
    background: var(--bpmb-bg-hover, rgba(0, 0, 0, .05));
    color: var(--bpmb-text-primary, #111827);
}

.bpmb-modal__close:focus-visible {
    outline: 2px solid var(--bpmb-modal-accent);
    outline-offset: 2px;
}

.bpmb-modal__body {
    padding: var(--bpmb-modal-pad);
    overflow-y: auto;
    font-size: 14px;
    line-height: 1.5;
}

.bpmb-modal__message {
    margin: 0;
    color: var(--bpmb-text-secondary, #6b7280);
}

.bpmb-modal__foot {
    display: flex;
    justify-content: flex-end;
    gap: 8px;

    padding: var(--bpmb-modal-pad);
    padding-block-start: 0;
}

/*--------------------------------------------------------------
# Confirm — a destructive-action gate rendered inside a modal
--------------------------------------------------------------*/
.bpmb-confirm {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    text-align: center;
}

.bpmb-confirm__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;

    width: 64px;
    height: 64px;
    border-radius: 9999px;

    color: var(--bpmb-modal-accent, #dc2626);
    /* Tint of the accent, with a hex fallback for browsers without color-mix. */
    background: var(--bpmb-danger-light, #fee2e2);
    background: color-mix(in srgb, var(--bpmb-modal-accent, #dc2626) 12%, transparent);
}

.bpmb-confirm__message {
    margin: 0;
    font-size: 15px;
    line-height: 1.5;
    color: var(--bpmb-text-secondary, #6b7280);
}

/*--------------------------------------------------------------
# Toast
--------------------------------------------------------------*/
.bpmb-toast-container {
    --bpmb-toast-offset: 24px;

    position: fixed;
    inset-block-start: var(--bpmb-toast-offset);
    inset-inline-end: 24px;
    z-index: 100001;

    display: flex;
    flex-direction: column;
    gap: 8px;

    max-width: min(380px, calc(100vw - 32px));
    pointer-events: none;
}

/* Clear the WordPress admin bar so toasts never render underneath it. */
body.admin-bar .bpmb-toast-container {
    --bpmb-toast-offset: 56px;
}

@media screen and (max-width: 782px) {
    body.admin-bar .bpmb-toast-container {
        --bpmb-toast-offset: 70px;
    }
}

.bpmb-toast {
    --bpmb-toast-accent: var(--bpmb-info, #2563eb);

    display: flex;
    align-items: flex-start;
    gap: 10px;

    padding: 12px 14px;
    pointer-events: auto;

    font-size: 14px;
    line-height: 1.45;
    color: var(--bpmb-text-primary, #111827);

    background: var(--bpmb-bg-primary, #fff);
    border: 1px solid var(--bpmb-border-color, #e2e8f0);
    border-inline-start: 3px solid var(--bpmb-toast-accent);
    border-radius: 10px;
    box-shadow: 0 10px 15px -3px rgba(0, 0, 0, .12), 0 4px 6px -4px rgba(0, 0, 0, .08);

    opacity: 0;
    transform: translateY(-6px);
    transition: opacity .2s cubic-bezier(.4, 0, .2, 1),
                transform .2s cubic-bezier(.4, 0, .2, 1);
}

.bpmb-toast.is-active {
    opacity: 1;
    transform: translateY(0);
}

.bpmb-toast.is-leaving {
    opacity: 0;
    transform: translateY(-6px);
}

.bpmb-toast--success { --bpmb-toast-accent: var(--bpmb-success, #16a34a); }
.bpmb-toast--error   { --bpmb-toast-accent: var(--bpmb-danger, #dc2626); }
.bpmb-toast--warning { --bpmb-toast-accent: var(--bpmb-warning, #d97706); }
.bpmb-toast--info    { --bpmb-toast-accent: var(--bpmb-info, #2563eb); }

.bpmb-toast__icon {
    display: inline-flex;
    flex: 0 0 auto;
    color: var(--bpmb-toast-accent);
}

.bpmb-toast__content {
    flex: 1 1 auto;
    min-width: 0;
}

.bpmb-toast__title {
    font-weight: 600;
}

.bpmb-toast__message {
    overflow-wrap: anywhere;
}

.bpmb-toast__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;

    width: 24px;
    height: 24px;

    color: var(--bpmb-text-muted, #9ca3af);
    background: none;
    border: 0;
    border-radius: 6px;
    cursor: pointer;
}

.bpmb-toast__close:hover {
    color: var(--bpmb-text-primary, #111827);
    background: var(--bpmb-bg-hover, rgba(0, 0, 0, .05));
}

.bpmb-toast__close:focus-visible {
    outline: 2px solid var(--bpmb-toast-accent);
    outline-offset: 1px;
}

/*--------------------------------------------------------------
# Skeleton — the LOADING state (spec 09 §3 four-state contract)
--------------------------------------------------------------*/
.bpmb-skeleton-group {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.bpmb-skeleton {
    background: var(--bpmb-skeleton-base, #e9edf2);
    background-image: linear-gradient(
        90deg,
        var(--bpmb-skeleton-base, #e9edf2) 0%,
        var(--bpmb-skeleton-sheen, #f4f6f9) 50%,
        var(--bpmb-skeleton-base, #e9edf2) 100%
    );
    background-size: 200% 100%;
    border-radius: 6px;
    animation: bpmb-skeleton-sheen 1.4s ease-in-out infinite;
}

.bpmb-skeleton--thumb  { width: 100%; aspect-ratio: 16 / 9; border-radius: 10px; }
.bpmb-skeleton--title  { width: 70%;  height: 18px; }
.bpmb-skeleton--meta   { width: 40%;  height: 12px; }
.bpmb-skeleton--text   { width: 100%; height: 12px; }
.bpmb-skeleton--short  { width: 55%; }
.bpmb-skeleton--avatar { width: 40px; height: 40px; border-radius: 9999px; flex: 0 0 auto; }

.bpmb-skeleton-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

/*--------------------------------------------------------------
# Busy state
#
# `.bpmb-loading` is applied by ~13 call sites across both plugins while a request
# is in flight, but until now it only existed as a layout rule in the EDITOR
# stylesheet - `display: inline-flex` and a gap, no visual feedback at all, and not
# loaded on any other screen. So on most surfaces the class was applied, the user
# saw nothing change, and the button still looked clickable.
#
# Defining it here, in the shared sheet every surface already depends on, is what
# makes those existing call sites mean something. Works on a button or a container:
# the element dims, stops taking pointer events, and grows a spinner.
--------------------------------------------------------------*/

/*
 * THE busy state. State only - dim, inert, and a progress cursor. It deliberately
 * paints NO spinner, because this codebase already has a spinner primitive
 * (`.bpmb-spinner`, below) that six call sites inject into the markup themselves.
 * A glyph on the base class collided with those and painted two spinners at once.
 *
 * Want a glyph without touching markup? Add `.bpmb-loading--spinner` as well.
 */
.bpmb-loading {
    opacity: 0.6;
    pointer-events: none;
    cursor: progress;
}

/*
 * Opt-in trailing glyph, for callers that have no `.bpmb-spinner` of their own.
 * Sits after the label rather than replacing it: swapping the text out would change
 * the button's width mid-click and shift whatever is beside it.
 */
.bpmb-loading--spinner::after {
    content: "";
    display: inline-block;
    width: 1em;
    height: 1em;
    /*
     * `flex: 0 0 auto` is load-bearing, not defensive. Most of the buttons this lands on are
     * `inline-flex` (.bpmb-btn, .add-bpmb-category), which makes the pseudo-element a flex ITEM -
     * and a flex item shrinks by default. Measured without it: a spinner 4px wide and 13px tall,
     * i.e. a squashed line rather than a circle.
     */
    flex: 0 0 auto;
    margin-inline-start: 0.5em;
    vertical-align: -0.125em;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: bpmb-spin 0.7s linear infinite;
}

/*
 * Honour reduced-motion: keep the affordance, drop the spin. A pulse still says
 * "working" without animating a rotating element for someone who asked us not to.
 */
@media (prefers-reduced-motion: reduce) {
    .bpmb-loading--spinner::after {
        animation: bpmb-busy-pulse 1.2s ease-in-out infinite;
        border-top-color: currentColor;
    }

    @keyframes bpmb-busy-pulse {
        50% { opacity: 0.25; }
    }
}

/*
 * The ONE rotate keyframe for the whole design system. It used to be copy-pasted six
 * times (twice in buddypress-member-blog-public.css alone, plus the editor, admin,
 * Pro co-authors and Pro analytics stylesheets). Every one of those stylesheets
 * depends on `bpmb-ui-helpers` - Pro's reach it transitively through the virtual
 * `bpmbp-frontend-tokens` handle - so they all resolve this definition.
 */
@keyframes bpmb-spin {
    to { transform: rotate(360deg); }
}

.bpmb-skeleton-card__body {
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.bpmb-skeleton-row {
    display: flex;
    align-items: center;
    gap: 12px;
}

.bpmb-skeleton-row .bpmb-skeleton--text {
    flex: 1 1 auto;
}

@keyframes bpmb-skeleton-sheen {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

/*--------------------------------------------------------------
# Badge
--------------------------------------------------------------*/
.bpmb-badge {
    --bpmb-badge-fg: var(--bpmb-text-secondary, #6b7280);
    --bpmb-badge-bg: var(--bpmb-bg-secondary, #f1f5f9);

    display: inline-flex;
    align-items: center;
    gap: 4px;

    padding: 2px 8px;

    font-size: 12px;
    font-weight: 500;
    line-height: 1.5;
    white-space: nowrap;

    color: var(--bpmb-badge-fg);
    background: var(--bpmb-badge-bg);
    border-radius: 6px;
}

.bpmb-badge--success   { --bpmb-badge-fg: var(--bpmb-success, #16a34a); --bpmb-badge-bg: var(--bpmb-success-light, #dcfce7); }
.bpmb-badge--danger    { --bpmb-badge-fg: var(--bpmb-danger, #dc2626);  --bpmb-badge-bg: var(--bpmb-danger-light, #fee2e2); }
.bpmb-badge--warning   { --bpmb-badge-fg: var(--bpmb-warning, #d97706); --bpmb-badge-bg: var(--bpmb-warning-light, #fef3c7); }
.bpmb-badge--info      { --bpmb-badge-fg: var(--bpmb-info, #2563eb);    --bpmb-badge-bg: var(--bpmb-info-light, #dbeafe); }
.bpmb-badge--featured  { --bpmb-badge-fg: #92400e; --bpmb-badge-bg: #fef3c7; }

/*--------------------------------------------------------------
# Avatar
--------------------------------------------------------------*/
.bpmb-avatar {
    --bpmb-avatar-size: 32px;

    display: inline-block;
    flex: 0 0 auto;
    width: var(--bpmb-avatar-size);
    height: var(--bpmb-avatar-size);
    overflow: hidden;
    border-radius: 9999px;
}

.bpmb-avatar img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}

.bpmb-avatar--sm { --bpmb-avatar-size: 24px; }
.bpmb-avatar--lg { --bpmb-avatar-size: 48px; }

/* Stacked group (co-authors). */
.bpmb-avatar-group {
    display: inline-flex;
    align-items: center;
}

.bpmb-avatar-group .bpmb-avatar + .bpmb-avatar {
    margin-inline-start: -8px;
    box-shadow: 0 0 0 2px var(--bpmb-bg-primary, #fff);
}

/*--------------------------------------------------------------
# States — EMPTY / ERROR / SUCCESS (the other three of the four)
--------------------------------------------------------------*/
.bpmb-empty,
.bpmb-error-state,
.bpmb-success-state {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;

    padding: 40px 20px;
    text-align: center;

    color: var(--bpmb-text-secondary, #6b7280);
    background: var(--bpmb-bg-secondary, #f8fafc);
    border: 1px solid var(--bpmb-border-color, #e2e8f0);
    border-radius: 12px;
}

.bpmb-empty__icon,
.bpmb-error-state__icon,
.bpmb-success-state__icon {
    color: var(--bpmb-text-muted, #9ca3af);
}

/*
 * `--bpmb-danger-text`, not `--bpmb-danger`.
 *
 * `--bpmb-danger` is the SIGNAL colour - for borders, icons and filled buttons, where
 * it sits against a light surface. As body text on the state's own dark surface it
 * measured 3.13:1, under the 4.5:1 AA floor. `--bpmb-danger-text` is the token that
 * flips per mode for exactly this (#721c24 light, #fca5a5 dark).
 */
.bpmb-error-state   { color: var(--bpmb-danger-text, #721c24); }
.bpmb-success-state { color: var(--bpmb-success, #16a34a); }

.bpmb-error-state__icon   { color: currentColor; }
.bpmb-success-state__icon { color: currentColor; }

.bpmb-empty__title,
.bpmb-error-state__title,
.bpmb-success-state__title {
    margin: 0;
    font-size: 16px;
    font-weight: 600;
    color: var(--bpmb-text-primary, #111827);
}

.bpmb-empty__text,
.bpmb-error-state__text,
.bpmb-success-state__text {
    margin: 0;
    font-size: 14px;
    line-height: 1.5;
}

/*--------------------------------------------------------------
# Responsive — spec 09 §4: stack under 480px, full-bleed dialogs
--------------------------------------------------------------*/
@media (max-width: 640px) {
    .bpmb-toast-container {
        inset-block-start: 12px;
        inset-inline: 12px;
        max-width: none;
    }
}

@media (max-width: 480px) {
    .bpmb-modal {
        padding: 0;
        align-items: flex-end;
    }

    .bpmb-modal__panel {
        max-width: none;
        max-height: 90vh;
        border-start-start-radius: var(--bpmb-modal-radius);
        border-start-end-radius: var(--bpmb-modal-radius);
        border-end-start-radius: 0;
        border-end-end-radius: 0;
    }

    .bpmb-modal__foot {
        flex-direction: column-reverse;
    }

    .bpmb-modal__foot .bpmb-btn {
        width: 100%;
    }
}

/*--------------------------------------------------------------
# Reduced motion
--------------------------------------------------------------*/
@media (prefers-reduced-motion: reduce) {
    .bpmb-modal,
    .bpmb-modal__panel,
    .bpmb-toast,
    .bpmb-btn {
        transition: none;
    }

    .bpmb-skeleton {
        animation: none;
    }
}

/*--------------------------------------------------------------
# Post list / grid  — FIXES A SHIPPED LAYOUT BUG
--------------------------------------------------------------
The shortcodes put `bpmb-columns-{n}` on the WRAPPER and then nest the cards
inside `<ul class="bpmb-post-list">`. The wrapper's grid therefore had exactly
ONE child (the <ul>), so `columns=3` never produced 3 columns — every post grid
rendered as a single stacked column. `.bpmb-post-list` and `.bpmb-pagination`
also had ZERO rules anywhere in the plugin, so the <li>s showed raw bullets and
pagination rendered as a bulleted list.

Fix: the <ul> inherits the wrapper's column template, so the columns apply to
the CARDS. Works whether `bpmb-columns-{n}` sits on the wrapper (today) or on
the <ul> itself (spec 02 §91, where the Renderer emits it). */
.bpmb-post-list {
    /*
     * The wrapper is the declared grid (`bpmb-columns-{n}`) but the <ul> is its
     * ONLY child, so the <ul> would otherwise sit in column 1 and subdivide just
     * that column — 3 cards squeezed into a third of the row. Span the whole row
     * first, then adopt the wrapper's column template so the CARDS get it.
     */
    grid-column: 1 / -1;

    display: grid;
    grid-template-columns: inherit;
    gap: var(--bpmb-space-lg);

    margin: 0;
    padding: 0;
    list-style: none;
}

.bpmb-post-list > li {
    margin: 0;
    padding: 0;
    list-style: none;
}

/* A grid container that directly holds cards (authors) keeps working as-is. */
.bpmb-columns-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.bpmb-columns-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.bpmb-columns-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.bpmb-columns-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }

/*
 * The user's `columns` must actually take effect on the discovery lists.
 *
 * The recent / popular / list shortcodes set `bpmb-columns-{n}` on the WRAPPER,
 * but the grid <ul> carries its own `bpmb-columns-3` (grid()'s default, emitted
 * for the direct-call profile / topic-hub templates that have no wrapper). At
 * equal specificity the <ul>'s own class beat the inherited wrapper template, so
 * every list rendered 3 columns and any other `columns` value — including List's
 * default of 1 — was silently ignored.
 *
 * This descendant selector (two classes) outranks the <ul>'s single-class
 * `bpmb-columns-3`, so when a grid <ul> sits inside a columns wrapper the
 * WRAPPER's declared count wins. Direct-call templates have no columns wrapper,
 * so their <ul> keeps its own default untouched.
 */
.bpmb-columns-1 > .bpmb-post-list { grid-template-columns: repeat(1, minmax(0, 1fr)); }
.bpmb-columns-2 > .bpmb-post-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.bpmb-columns-3 > .bpmb-post-list { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.bpmb-columns-4 > .bpmb-post-list { grid-template-columns: repeat(4, minmax(0, 1fr)); }

@media (max-width: 1024px) {
    .bpmb-columns-3,
    .bpmb-columns-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }

    .bpmb-columns-3 > .bpmb-post-list,
    .bpmb-columns-4 > .bpmb-post-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 640px) {
    .bpmb-columns-2,
    .bpmb-columns-3,
    .bpmb-columns-4 { grid-template-columns: minmax(0, 1fr); }

    .bpmb-columns-2 > .bpmb-post-list,
    .bpmb-columns-3 > .bpmb-post-list,
    .bpmb-columns-4 > .bpmb-post-list { grid-template-columns: minmax(0, 1fr); }
}

/*--------------------------------------------------------------
# Pagination
--------------------------------------------------------------
paginate_links( type => 'list' ) emits <ul class="page-numbers">, which had no
styling at all — it rendered as a bulleted list. */
.bpmb-pagination {
    /*
     * Popular / member-list render the pager INSIDE the `bpmb-columns-{n}` grid
     * wrapper (Recent puts it outside). As a grid child it would be trapped in
     * column 1 and wrap onto two lines. Span the full row wherever it lands.
     */
    grid-column: 1 / -1;

    margin-block-start: var(--bpmb-space-lg);
}

.bpmb-pagination ul.page-numbers {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--bpmb-space-xs);

    margin: 0;
    padding: 0;
    list-style: none;
}

.bpmb-pagination ul.page-numbers > li {
    margin: 0;
    padding: 0;
    list-style: none;
}

.bpmb-pagination .page-numbers {
    display: inline-flex;
    align-items: center;
    justify-content: center;

    /* 40px tap target (spec 09 §4). */
    min-width: 40px;
    min-height: 40px;
    padding-inline: var(--bpmb-space-sm);

    font-size: var(--bpmb-font-size-md);
    line-height: 1;
    text-decoration: none;

    color: var(--bpmb-text-primary);
    background: var(--bpmb-bg-primary);
    border: 1px solid var(--bpmb-border-color);
    border-radius: var(--bpmb-radius-md);

    transition: background-color var(--bpmb-transition-fast),
                border-color var(--bpmb-transition-fast),
                color var(--bpmb-transition-fast);
}

.bpmb-pagination a.page-numbers:hover {
    background: var(--bpmb-bg-secondary);
    border-color: var(--bpmb-border-color-dark);
}

.bpmb-pagination a.page-numbers:focus-visible {
    outline: 2px solid var(--bpmb-primary);
    outline-offset: 2px;
}

.bpmb-pagination .page-numbers.current {
    color: var(--bpmb-text-light);
    background: var(--bpmb-primary);
    border-color: var(--bpmb-primary);
    font-weight: var(--bpmb-font-weight-semibold);
}

.bpmb-pagination .page-numbers.dots {
    border-color: transparent;
    background: transparent;
}

/*--------------------------------------------------------------
# Related posts
--------------------------------------------------------------
An <aside> under an article. The GRID is not defined here: it reuses the shared
`.bpmb-columns-{n}` + `.bpmb-post-list` system above, which already collapses to
one column at 640px. A bespoke breakpoint here would be a second answer to a
question that is already answered. */
.bpmb-related-posts {
    margin-block-start: var(--bpmb-space-2xl);
    padding-block-start: var(--bpmb-space-xl);
    border-block-start: 1px solid var(--bpmb-border-color);
}

.bpmb-related-posts-title {
    margin-block-end: var(--bpmb-space-lg);
    color: var(--bpmb-text-primary);
    font-size: var(--bpmb-font-size-lg);
    font-weight: 600;
}

/* ==========================================================================
   Writer profile header

   LIVES HERE, not in the shortcodes stylesheet, because it is the one component
   rendered on TWO surfaces: `/author/{nick}/` (a template takeover) and the
   BuddyPress member "blog" tab (a BP screen). Those two surfaces load different
   stylesheets — the BP profile does not load the shortcodes CSS — and this file is
   the only one BOTH of them load.

   Measured: with the rules in the shortcodes stylesheet, the header rendered
   `display: block` on the BP tab. Avatar, name, stats and follow button all stacked
   into an unstyled column, on the surface a member sees most.
   ========================================================================== */

.bpmb-writer-header {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: var(--bpmb-space-lg);
	flex-wrap: wrap;
	padding: var(--bpmb-space-lg);
	margin-block-end: var(--bpmb-space-lg);
	background: var(--bpmb-bg-secondary);
	border: 1px solid var(--bpmb-border-color);
	border-radius: var(--bpmb-radius-lg);
}

.bpmb-writer-identity {
	display: flex;
	align-items: center;
	gap: var(--bpmb-space-md);
	min-width: 0; /* Lets a long display name ellipsize instead of shoving the follow button off. */
}

.bpmb-writer-avatar img {
	display: block;
	width: 96px;
	height: 96px;
	border-radius: 50%;
	object-fit: cover;
}

.bpmb-writer-meta {
	min-width: 0;
}

.bpmb-writer-name {
	margin: 0 0 var(--bpmb-space-xs);
	color: var(--bpmb-text-primary);
	line-height: 1.2;
}

.bpmb-writer-bio {
	margin: 0 0 var(--bpmb-space-sm);
	color: var(--bpmb-text-secondary);
	/* A bio is user input with no length limit. Two lines, then clamp — an essay in the header
	   would push the post grid below the fold on every profile. */
	display: -webkit-box;
	-webkit-line-clamp: 2;
	-webkit-box-orient: vertical;
	overflow: hidden;
	max-width: 60ch;
}

/* posts · views · followers */
.bpmb-writer-stats {
	display: flex;
	gap: var(--bpmb-space-lg);
	margin: 0;
}

.bpmb-writer-stat {
	display: flex;
	align-items: baseline;
	gap: var(--bpmb-space-xs);
}

.bpmb-writer-stats .bpmb-stat-value {
	margin: 0;
	font-weight: 600;
	color: var(--bpmb-text-primary);
}

.bpmb-writer-stats .bpmb-stat-label {
	margin: 0; /* <dd> has a default margin-inline-start of 40px. */
	color: var(--bpmb-text-secondary);
	font-size: var(--bpmb-font-size-sm);
}

.bpmb-writer-actions {
	flex-shrink: 0;
}

/* Under 640px the header stacks and centres: side-by-side, the avatar plus a two-line bio plus
   three stats leaves the follow button about 60px of width. */
@media screen and (max-width: 640px) {
	.bpmb-writer-header {
		flex-direction: column;
		align-items: center;
		text-align: center;
		padding: var(--bpmb-space-md);
	}

	.bpmb-writer-identity {
		flex-direction: column;
		align-items: center;
	}

	.bpmb-writer-avatar img {
		width: 72px;
		height: 72px;
	}

	.bpmb-writer-stats {
		justify-content: center;
		flex-wrap: wrap;
		gap: var(--bpmb-space-md);
	}

	.bpmb-writer-actions {
		width: 100%;
	}

	.bpmb-writer-actions .bpmb-follow-btn {
		width: 100%;
		justify-content: center;
	}
}

/* ==========================================================================
   Engagement buttons inside a BuddyPress wrapper

   BuddyPress Nouveau resets EVERY button inside `.buddypress-wrap`:

       .bp-nouveau .buddypress-wrap ... button { display: inline-block; padding: 10px 20px; }

   Specificity 0,2,1 — which beats a plain `.bpmb-clap-btn` (0,1,0). So on the member's blog tab
   the clap and bookmark buttons came out `display: block`, 62x48, with 20px of side padding:
   the min-width/min-height got through and nothing else did. Not a theme bug — it is BuddyPress's
   own reset, and it lands on any plugin that renders a button inside its wrapper.

   Scoped high enough to win (0,3,0) and no higher. `!important` would win too, and would also
   stop a site owner restyling these buttons at all.
   ========================================================================== */

.buddypress-wrap .bpmb-post-actions-inline .bpmb-clap-btn,
.buddypress-wrap .bpmb-post-actions-inline .bpmb-bookmark-btn,
.buddypress-wrap .bpmb-writer-actions .bpmb-follow-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: var(--bpmb-space-xs);
	width: auto;
	height: auto;
	min-width: 40px;
	min-height: 40px;
	padding: 0 var(--bpmb-space-sm);
	line-height: 1;
	background: transparent;
	color: var(--bpmb-text-secondary);
	border-radius: 999px;
	cursor: pointer;
}

.buddypress-wrap .bpmb-writer-actions .bpmb-follow-btn {
	/* The follow button is a primary action and reads as one — the clap and bookmark are quiet. */
	padding: 0 var(--bpmb-space-md);
	background: var(--bpmb-primary);
	/* `--bpmb-on-primary`: white in light mode, DARK in dark mode — because `--bpmb-primary` itself
	   becomes a pale accent there and white-on-pale measured 1.89:1. Not `--bpmb-btn-fg`, which is
	   scoped inside `.bpmb-btn` and resolves to nothing out here. */
	color: var(--bpmb-on-primary, #fff);
	border: 0;
}

.buddypress-wrap .bpmb-writer-actions .bpmb-follow-btn.is-following {
	background: transparent;
	color: var(--bpmb-text-primary);
	border: 1px solid var(--bpmb-border-color);
}

/* ==========================================================================
   Topic hub
   Lives beside the writer header, for the same reason: the hub renders on a term-archive
   TAKEOVER and via a shortcode on any page, and those two do not load the same stylesheets.
   ========================================================================== */

.bpmb-topic-header {
	padding-block-end: var(--bpmb-space-md);
	margin-block-end: var(--bpmb-space-lg);
	border-block-end: 1px solid var(--bpmb-border-color);
}

.bpmb-topic-name {
	margin: 0 0 var(--bpmb-space-sm);
	color: var(--bpmb-text-primary);
}

.bpmb-topic-description {
	max-width: 65ch; /* Bringhurst: past ~75 characters the eye loses the line on the return sweep. */
	color: var(--bpmb-text-secondary);
}

.bpmb-topic-description p:last-child {
	margin-block-end: 0;
}

.bpmb-topic-count {
	margin: var(--bpmb-space-sm) 0 0;
	/* `--bpmb-text-secondary`, NOT `--bpmb-text-light`.
	 *
	 * `--bpmb-text-light` means "light-COLOURED text" — for dark backgrounds — not "text for light
	 * mode". Used here it put near-white text on a white page: measured 1.04:1, which is invisible.
	 * A token named for its colour rather than its role is a trap, and this is what falling into it
	 * looks like. */
	color: var(--bpmb-text-secondary);
	font-size: var(--bpmb-font-size-sm);
}

.bpmb-topic-writers {
	margin-block-end: var(--bpmb-space-lg);
}

.bpmb-topic-writers-title {
	margin: 0 0 var(--bpmb-space-md);
	color: var(--bpmb-text-primary);
	font-size: 1.15rem;
}

@media screen and (max-width: 640px) {
	.bpmb-topic-header {
		margin-block-end: var(--bpmb-space-md);
	}
}

/* ==========================================================================
   COMPONENT ARMOUR — our components are OURS, on any theme.

   THE PROBLEM. Themes ship blanket rules for bare elements, and they out-specify a plain class by a
   mile. BuddyX's is real, and it is not subtle:

       button:not([class*="wp-"]):not([class*="customize-"]):not([class*="wc-block-"])
             :not([class*="bn-"]):not([class*="mvs-"]):not(.bx-color-mode-toggle__btn)
             :not(.menu-toggle):not(.menu-close):not(.datepicker button):not([class*="tribe-events"])

   TEN :not()s — specificity (0,10,1). BuddyPress Nouveau has its own. So the SAME clap button
   rendered 40x40 on a shortcode page, 62x48 inside a BuddyPress wrap, and 22px tall on the author
   archive. Three looks for one component, none of them ours, and one of them below the WCAG minimum
   tap target.

   WHY !important, AND WHY IT IS NOT LAZINESS. I first tried to win on specificity by repeating the
   class — (0,4,0). It lost to (0,10,1), and it would lose again the day a theme adds an eleventh
   :not(). Counting selectors is an arms race against every theme that will ever exist, and the
   plugin loses it by default. Short of Shadow DOM, `!important` is the only DETERMINISTIC way for a
   component to keep its own geometry.

   THE CONTRACT THIS CREATES, and it is the important part:

       THE TOKENS ARE THE API. The RULES are ours.

   A site owner restyles these components by setting `--bpmb-primary`, `--bpmb-radius-*`,
   `--bpmb-space-*` — not by out-specifying our CSS. That is a better seam than the one it replaces:
   it is documented, it is stable across releases, and it cannot be broken by us renaming a class.

   AND WE STILL DO NOT TAKE THE PALETTE. `--bpmb-primary` maps to the theme's accent, the FONT is
   inherited (`all: unset` is inherit-for-inherited), and the radius and spacing come from tokens a
   theme can set. The button is the site's colour and the site's typeface, in our shape, at a
   contrast we can actually guarantee. Premium, and still unmistakably part of the site.
   ========================================================================== */

.bpmb-follow-btn,
.bpmb-clap-btn,
.bpmb-bookmark-btn {
	all: unset !important;

	box-sizing: border-box !important;
	display: inline-flex !important;
	align-items: center !important;
	justify-content: center !important;
	gap: var(--bpmb-space-xs) !important;

	/* 40px: a real tap target. WCAG 2.5.8 asks 24 for a link; 40 is the figure for a CONTROL, and
	   these are controls. It is also what the theme kept taking away. */
	min-width: 40px !important;
	min-height: 40px !important;
	padding: 0 var(--bpmb-space-sm) !important;

	font-family: inherit !important;   /* the SITE's typeface — we are not taking that */
	font-size: var(--bpmb-font-size-sm) !important;
	font-weight: 500 !important;
	line-height: 1 !important;
	text-decoration: none !important;
	white-space: nowrap !important;

	border: 1px solid transparent !important;
	border-radius: 999px !important;
	cursor: pointer !important;
}

/* Quiet by default — a clap and a save are small gestures, not calls to action. */
.bpmb-clap-btn,
.bpmb-bookmark-btn {
	color: var(--bpmb-text-secondary) !important;
	background-color: transparent !important;
	border-color: var(--bpmb-border-color) !important;
}

.bpmb-clap-btn:hover,
.bpmb-bookmark-btn:hover {
	color: var(--bpmb-text-primary) !important;
	background-color: var(--bpmb-bg-secondary) !important;
}

/* Follow IS a call to action, so it is filled — with the THEME's accent, darkened just enough that
   its label is readable on any of them (see --bpmb-primary-filled). */
.bpmb-follow-btn {
	padding: 0 var(--bpmb-space-md) !important;
	color: var(--bpmb-on-primary) !important;
	background-color: var(--bpmb-primary-filled) !important;
	border-color: var(--bpmb-primary-filled) !important;
}

/* Already following = a state you can undo, not a thing to press. It steps back. */
.bpmb-follow-btn.is-following {
	color: var(--bpmb-text-primary) !important;
	background-color: transparent !important;
	border-color: var(--bpmb-border-color) !important;
}

.bpmb-follow-btn.is-following:hover {
	color: var(--bpmb-danger) !important;
	border-color: var(--bpmb-danger) !important;
}

/* A visible focus ring. `all: unset` removes the browser's, and a keyboard user who cannot see
   where they are is a keyboard user who cannot use this. */
.bpmb-follow-btn:focus-visible,
.bpmb-clap-btn:focus-visible,
.bpmb-bookmark-btn:focus-visible {
	outline: 2px solid var(--bpmb-primary) !important;
	outline-offset: 2px !important;
}

.bpmb-clap-btn:disabled {
	opacity: .6 !important;
	cursor: default !important;
}

/* The icon keeps its size whatever the theme does to svg. */
.bpmb-follow-btn svg,
.bpmb-clap-btn svg,
.bpmb-bookmark-btn svg {
	width: 18px !important;
	height: 18px !important;
	flex: 0 0 auto !important;
}

/*
 * BOOKMARK STATE SWAP — lives here because this sheet loads everywhere.
 *
 * The button ships BOTH icons and lets CSS pick one, so the JS only has to flip a class
 * and can never leave the button rendering both states or neither. The rules used to sit
 * in bp-member-blog-shortcodes.css, which the single-post branch does not enqueue — so on
 * a single post both the outline and the filled bookmark rendered side by side and the
 * control read as two separate save buttons. It looked intermittent because three
 * unrelated things (related-posts auto-insert, a listing shortcode in the body, the
 * bpmb_enqueue_card_styles filter) pull that sheet in and accidentally fixed it.
 */
.bpmb-bookmark-btn .bpmb-icon-saved,
.bpmb-bookmark-btn.is-saved .bpmb-icon-save {
	display: none;
}

.bpmb-bookmark-btn.is-saved .bpmb-icon-saved {
	display: block;
}

/* -------------------------------------------------------------------------
 * Load more (4.0.1)
 *
 * Sits under a listing, in place of the numbered pager, which the script
 * hides once it takes over. Everything here is token-driven, so it inherits
 * the dark-mode values the rest of this sheet already defines; there is no
 * raw colour and no physical margin, so RTL needs no override.
 * ---------------------------------------------------------------------- */
.bpmb-load-more {
    /*
     * SPAN THE WHOLE GRID, exactly as .bpmb-pagination already does.
     *
     * Half the listing shortcodes put their footer INSIDE the grid wrapper
     * (Popular, List) and half put it outside (Recent, Following). Inside a
     * `display: grid` wrapper an un-spanned child becomes a single CELL — the
     * button rendered 254px wide in the first column of a 3-column grid,
     * sitting where a card should be. `1 / -1` is inert in the block layouts,
     * so one rule covers both placements.
     */
    grid-column: 1 / -1;

    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--bpmb-space-sm, 8px);
    margin-block-start: var(--bpmb-space-lg, 24px);
    text-align: center;
}

/*
 * `hidden` is how the control ships from the server, and a `display: flex`
 * above would beat the UA's `display: none` on the attribute — the button
 * would be visible for every reader whose JavaScript never arrives, which is
 * precisely the reader this feature is not supposed to strand.
 */
.bpmb-load-more[hidden] {
    display: none;
}

.bpmb-load-more__button {
    min-width: 180px;
}

.bpmb-load-more__button.is-loading {
    cursor: progress;
}

.bpmb-load-more__status {
    margin: 0;
    min-height: 1.25em; /* Reserve the line so announcing a message shifts nothing. */
    font-size: var(--bpmb-font-size-sm, 13px);
    color: var(--bpmb-text-muted, #6b7280);
}

@media (max-width: 480px) {
    .bpmb-load-more__button {
        width: 100%;
        min-width: 0;
    }
}
