/**
 * WPMediaVerse — Frontend Styles
 *
 * Base styles for templates, shortcodes, and unstyled blocks.
 * Mobile-first. RTL-ready via [dir="rtl"] overrides.
 * Dark mode via BuddyNext [data-theme="dark"] token cascade.
 *
 * @package WPMediaVerse
 */

/* ==========================================================================
   0. Design Tokens
   ========================================================================== */

:root {
    /* Typography — BuddyNext first, then the THEME, via inherit.
       --font-body / --font-display are defined by BuddyNext and by nothing
       else, so on every other theme these tokens fell through to a hardcoded
       Apple system stack, and the plugin imposed that stack on top of whatever
       the theme had set. MediaVerse pages read in a different typeface from the
       site around them.
       `inherit` is the fix rather than a chain of --wp--preset--font-family--*
       names: a preset list only covers the themes someone thought to name, and
       silently misses the next one. Inheriting takes the theme's own font on
       ANY theme, block or classic, with nothing to maintain. */
    --mvs-font: var(--font-body, inherit);
    --mvs-font-heading: var(--font-display, var(--mvs-font));
    /* Font-size scale — single source of truth for every font-xs/sm/md/lg
       callsite (137 of them, Free + Pro + admin). Was only in the never-enqueued
       src/shared/design-tokens.css, so bare uses silently fell back to inherited size. */
    --mvs-font-xs: 12px;
    --mvs-font-sm: 14px;
    --mvs-font-md: 16px;
    --mvs-font-lg: 18px;

    /* Justified-row height for the `original` grid layout. Mirrors Pro's
       --mvs-fl-row-height (Flickr), whose justified-rows technique this layout
       now shares - see the .mvs-grid--original block for why. */
    --mvs-grid-row-height: 240px;

    /* Page width for every MediaVerse surface. The same 1200px used to be
       hardcoded on four wrappers and a banner, so a site owner on a wide theme
       had to override five rules in Additional CSS and would still drift the
       moment a sixth surface was added. One token, one override:
       :root { --mvs-content-width: 1600px; } in a child theme widens the lot,
       and the surfaces stay consistent with each other by construction.
       Basecamp 10313587813. */
    --mvs-content-width: 1200px;

    /* Per-item aspect ratio for those justified rows. Every tile overrides
       this inline (TemplateHelpers::grid_item_ar_style() server-side,
       card-builders.js for appended cards); the value here is the 3:2
       fallback for an item with no stored dimensions.

       Declared in :root deliberately. The CSS token-contract gate reads a
       var() with no definition as a phantom token, and it is right to - it
       cannot tell an instance property from a typo, and the typo is the
       common case. Giving it a real definition satisfies the contract
       honestly instead of adding an allowlist entry that would blind the
       gate to the next real one. */
    --mvs-ar: 1.5;

    /* Brand — prefer theme BUTTON tokens first so the plugin's primary
       buttons (Follow / Post / Add Media (+) / .mvs-btn--primary / .mvs-fab)
       match the theme's own button palette. Fall back to the theme's
       primary/link color, then to the plugin's hard-coded brand color.
         BuddyX, BuddyX Pro       → --button-background-color
         BuddyBoss                → --bb-primary-button-background
         Reign + generic themes   → --color-theme-primary
       Without this cascade, Follow/Post/+ buttons were always the
       brand/link color regardless of how the theme styled its own
       buttons — visibly off-palette next to native theme buttons.
       --mvs-on-primary picks up the theme's button text color via the
       same chain. */
    --mvs-primary: var(--reign-site-button-bg-color, var(--button-background-color, var(--bb-primary-button-background, var(--color-theme-primary, var(--brand, #0073aa)))));
    --mvs-primary-hover: var(--reign-site-button-bg-hover-color, var(--button-background-hover-color, var(--bb-primary-button-background-regular-hover, var(--color-theme-primary, var(--brand-hover, #005a87)))));
    --mvs-on-primary: var(--reign-site-button-text-color, var(--button-text-color, var(--bb-primary-button-text-regular, #ffffff)));

    /* ── A CONTRAST FLOOR WE OWN ──────────────────────────────────────────
     *
     * Everything above derives from the theme, which means our AA compliance
     * was decided by whichever theme the buyer installed. Measured: the primary
     * call-to-action passes on Reign (4.51) and FAILS on Astra (4.29). Both
     * halves of that pair come from the theme, so when a theme's own button
     * colours do not meet AA, we inherited the failure (Basecamp 10252222099).
     *
     * `--mvs-primary-fill` nudges the accent 10% toward black. That alone does
     * NOT guarantee the floor and was never going to: measured across candidate
     * brand colours, a 15% darken still leaves white text at 2.00 on yellow and
     * 2.88 on lime. Darkening enough to fix those would change the buyer's
     * brand colour, which is the one thing they would notice and did not ask
     * for.
     *
     * `--mvs-on-primary-auto` is what actually guarantees it: relative colour
     * syntax reads the fill's OKLCH lightness and returns white below the
     * threshold, black above it. The hue is never touched — only the text
     * flips. Measured on the same candidates, fill + auto foreground:
     *
     *   Reign 5.38 · Astra 6.15 · yellow 11.78 · lime 8.09 · pink 5.19
     *   navy 17.09 · grey 6.41          (minimum 5.19, all comfortably AA)
     *
     * The 10% nudge stays because it buys headroom on the dark accents that
     * were merely scraping past — Reign goes 4.51 -> 5.38.
     *
     * Fallback: browsers without relative colour syntax keep the theme's own
     * button text colour, i.e. exactly today's behaviour. Nobody is worse off
     * than before; supporting browsers are better off.
     */
    --mvs-primary-fill: color-mix(in srgb, var(--mvs-primary) 90%, #000);
    /*
     * Declared straight, with NO @supports guard, and that is deliberate.
     * Chrome discards `@supports (color: oklch(from red l c h))` at the at-rule
     * parser while `CSS.supports()` returns true for the same string — measured,
     * the guarded rule never reached the CSSOM at all. The guard is also
     * unnecessary: a custom property a browser cannot parse becomes
     * guaranteed-invalid, so every consumer's own `var(..., fallback)` takes
     * over. Old browsers therefore keep exactly today's behaviour for free.
     */
    /*
     * The lightness at which black and white text contrast EQUALLY against a
     * fill. Not a taste value: WCAG contrast is (L1+0.05)/(L2+0.05), so the
     * crossover is at relative luminance sqrt(0.05 * 1.05) - 0.05 = 0.1791,
     * which is the grey #757575, whose OKLab lightness is 0.5624.
     *
     * It was 0.62 - a guess, and one that sat a whisker above this plugin's own
     * default accent (#6366f1, L 0.5854), so the default landed on the wrong
     * side of its own threshold. Measured over 4096 colours spread across the
     * sRGB cube, scoring each rule against true WCAG luminance:
     *
     *     0.62,   operands reversed   3739 wrong picks   3703 of them under 4.5:1
     *     0.62,   operands correct     357 wrong picks    317 under 4.5:1
     *     0.5624, operands correct     149 wrong picks    116 under 4.5:1
     *
     * Read it, never retype it (Coding Rule 22). admin.css declares its own copy
     * because the two stylesheets do not reliably load together.
     */
    --mvs-contrast-flip-l: 0.5624;
    --mvs-on-primary-auto: oklch(from var(--mvs-primary-fill) clamp(0, (var(--mvs-contrast-flip-l) - l) * 1000, 1) 0 0);
    /*
     * Text on --mvs-accent, same mechanism and the same ordering requirement.
     * It MUST be declared here, before the @supports block below, not with the
     * other accent tokens further down: custom properties are same-specificity,
     * so a later plain declaration beats the earlier conditional one. Declared
     * after it, the @supports override never applied.
     *
     * This line shipped with its operands REVERSED - (l - threshold) instead of
     * (threshold - l) - so it returned white on light fills and black on dark
     * ones, the exact inverse of the rule. #6366f1 happened to come out right
     * anyway, which is why it survived: the wrong formula and a threshold on
     * the wrong side of the default cancelled out on that one colour.
     * Basecamp 10300601366.
     *
     * Fixed by correcting the direction AND moving the threshold to the real
     * crossover (see --mvs-contrast-flip-l above). Neither alone is enough:
     * correcting the direction at 0.62 regresses #6366f1 from black 4.70 to
     * white 4.47, which is why this sat unfixed. Together, #6366f1 stays black
     * at 4.70 and the pale fills stop getting white text at 1.28-1.43:1.
     *
     * Verified in-browser against contrast-color() on the same fills:
     *   #6366f1 black · #ffd400 black · #a7f3d0 black · #ff2d8f black
     *   #0b0b0b white          - 5 of 5 agree.
     */
    --mvs-on-accent: oklch(from var(--mvs-accent) clamp(0, (var(--mvs-contrast-flip-l) - l) * 1000, 1) 0 0);
}

/*
 * Lightness is not luminance, and the line above can only read lightness.
 * At high chroma the two disagree: #ff2d8f reads as "dark" in OKLCH (below the
 * 0.62 threshold, so the rule picks white) while its actual WCAG luminance is
 * 0.202, where black is the better partner. Measured: white 4.16, black 5.04.
 * A saturated brand accent is exactly where a threshold on `l` mispredicts.
 *
 * No amount of retuning 0.62 fixes that, because the error is the axis, not the
 * number. `contrast-color()` does the real luminance comparison, so where the
 * browser has it, hand the decision over. Sweeping 24 hues x 4 chromas x 12
 * lightnesses, choosing the better of black/white never fell below 4.62:1 --
 * so this is not a tradeoff worth exposing as a setting, it is just the
 * calculation done right.
 *
 * Unguarded above, guarded here, and the asymmetry is deliberate: an unparsable
 * custom property is guaranteed-invalid and falls through to each consumer's
 * own var() fallback, but an unparsable VALUE inside a supported property would
 * simply lose. Verified in-browser that this at-rule reaches the CSSOM, since
 * the note above records Chrome discarding a similar @supports for relative
 * colour while CSS.supports() claimed it was fine.
 */
@supports (color: contrast-color(red)) {
    :root {
        --mvs-on-primary-auto: contrast-color(var(--mvs-primary-fill));
        /* Same treatment for the accent fill. --mvs-accent is NOT redefined in
           the dark block (verified: 0 occurrences in the dark :root), so this
           one declaration serves both modes. */
        --mvs-on-accent: contrast-color(var(--mvs-accent));
    }
}

:root {

    /* The brand colour AS TEXT, dark enough to pass AA.
     *
     * --mvs-primary is tuned to be a button BACKGROUND, where white sits on it
     * and passes easily. The same value used as text on a light page does not:
     * measured 4.36:1 on the dashboard rail link and the active tab label,
     * under the 4.5 floor for normal text (Basecamp 10253005724).
     *
     * Clamping lightness keeps the theme's hue - a site's brand still reads as
     * its brand - while guaranteeing the contrast. Text only; the button
     * background keeps the unmodified colour, which is why this is a separate
     * token rather than a change to --mvs-primary.
     *
     * The dark block below clamps in the opposite direction (max(l, 0.72)):
     * against a dark surface the accent must be LIGHTENED to clear the floor,
     * just as it is darkened against a light one. Same shape, opposite bound. */
    --mvs-primary-text: oklch(from var(--mvs-primary) min(l, 0.48) c h);

    /*
     * The primary button's colours, resolved ONCE.
     *
     * `.mvs-btn--primary` is declared in four stylesheets across the two
     * plugins — here, shared-ui-frame.css (modals), bp-integration.css
     * (#buddypress, where a class-only rule loses to the theme's
     * `#buddypress button`) and Pro's gamification.css (which wins on the
     * competition pages by load order). The three extra copies exist for
     * specificity and load order, which are real problems; restating the
     * COLOUR in each of them was not. It meant the contrast-floor fix had to
     * be made four times, and whichever copy someone forgets is the one that
     * ships broken — which is exactly how a `color: var(--mvs-surface)` (a
     * page-surface token used as button text, black-on-brand in dark mode)
     * survived in shared-ui-frame.css long after the same mistake was found
     * and commented elsewhere.
     *
     * The other three now reference these four tokens and declare no colour of
     * their own, so the formula lives here and changing it changes every
     * surface. They keep a plain `var(--mvs-primary)` fallback so a context
     * that somehow loads without this file degrades to the theme colour rather
     * than to nothing — a fallback, deliberately not a second copy of the
     * formula. Basecamp 10252281364.
     */
    --mvs-btn-primary-bg: var(--mvs-primary-fill, var(--mvs-primary));
    --mvs-btn-primary-fg: var(--mvs-on-primary-auto, var(--mvs-on-primary, #ffffff));
    --mvs-btn-primary-bg-hover: var(--mvs-primary-hover);
    --mvs-btn-primary-fg-hover: var(--mvs-on-primary, #ffffff);

    --mvs-primary-rgb: 0, 115, 170;
    --mvs-danger: var(--red, #d63638);
    --mvs-danger-rgb: 214, 54, 56;
    --mvs-success: var(--green, #44b700);
    --mvs-success-rgb: 68, 183, 0;
    --mvs-warning: #b58105;
    --mvs-warning-rgb: 181, 129, 5;

    /*
     * Text-only variants of the semantic colours, same split as
     * --mvs-primary-text above and for the same reason: each is used BOTH as a
     * fill (light text on top) and as text on its own tint, and those two jobs
     * want opposite adjustments.
     *
     * Measured on the drive chips against the COMPOSITED tint — the number that
     * matters, since the fill is an alpha over the row, not an opaque colour:
     *
     *   before      light    dark
     *   PDF         3.94     2.78
     *   DIR/word    3.90     6.82
     *
     * So light needed darkening and dark needed lightening; the dark block
     * below does the second half. Changing --mvs-danger itself was the tempting
     * one-liner and is wrong in both directions: it fills the Delete button,
     * where white sits on it at 4.73:1, and moving it either way breaks that.
     * (Basecamp 10253005724.)
     */
    --mvs-danger-text: oklch(from var(--mvs-danger) min(l, 0.45) c h);
    --mvs-success-text: oklch(from var(--mvs-success) min(l, 0.45) c h);

    /* Semantic backgrounds — derived tints (work in light + dark via alpha) */
    --mvs-info-bg: rgba(var(--mvs-primary-rgb), 0.08);
    --mvs-info-bg-light: rgba(var(--mvs-primary-rgb), 0.04);
    --mvs-info-border: rgba(var(--mvs-primary-rgb), 0.30);
    --mvs-success-bg: rgba(var(--mvs-success-rgb), 0.10);
    --mvs-warning-bg: rgba(var(--mvs-warning-rgb), 0.12);
    --mvs-danger-bg: rgba(var(--mvs-danger-rgb), 0.10);

    /* Extended semantic tokens — promoted from former PHANTOM uses (used via
       var() across the plugin but never defined, so they silently froze on a
       light hardcoded fallback and broke in dark mode). Defined here = single
       source of truth, dark-mode-safe. */
    --mvs-bg-subtle: var(--mvs-surface-2);     /* subtle panel fill */
    --mvs-on-danger: #ffffff;                  /* text on danger (red both modes) */
    /* Text on the warning and accent fills. These existed nowhere, so every
       badge and chip painted on them froze `color: #fff` inline and inherited
       whatever contrast the fill happened to give. Measured on this theme:
       white on --mvs-warning #b58105 = 3.44:1 and on --mvs-accent #6366f1 =
       4.47:1 - both under the 4.5 AA floor, the accent by a hair.
       Warning takes DARK text: it is a light-ish amber in light mode and
       lifts further to #f0b73a in dark, so white loses in both. Accent uses
       the same relative-colour trick as --mvs-on-primary-auto so it flips
       with the fill instead of being frozen at a value that just fails. */
    --mvs-on-warning: #1f1500;                 /* dark text on amber, both modes */
    /* Emphasis tier — see the border contract below. Mixing toward --mvs-text
       means this strengthens in BOTH modes off one declaration: dark redefines
       --mvs-border and --mvs-text, so the mix re-resolves lighter there. */
    --mvs-border-strong: color-mix(in oklab, var(--mvs-border), var(--mvs-text) 30%);
    --mvs-accent: #6366f1;                      /* indigo accent (boosts/highlights) */
    --mvs-accent-2: #ec4899;                     /* secondary accent (stories-ring gradient end) */
    --mvs-accent-soft: rgba(99, 102, 241, 0.12); /* translucent → works light + dark */

    /* ── Theme palette mapping (THE single source of truth) ──────────────────
       Each --mvs-* token inherits from the active theme's own variable so the
       plugin matches the theme in BOTH light and dark mode (the theme flips its
       own vars). Chain order = Reign → BuddyX / BuddyX Pro (--global-* /
       --color-theme-*) → BuddyNext (--text-* / --bg-*) → hardcoded fallback.
       Only the active theme's var resolves; the rest are absent. To support a
       new theme, add its var name to these chains here — nowhere else. The dark
       block below repeats the SAME chains with dark hardcoded fallbacks (only
       hit by generic themes that signal dark but don't theme their own vars). */

    /* Surfaces */
    --mvs-bg: var(--reign-site-body-bg-color, var(--color-theme-white-box, var(--bg, #ffffff)));
    --mvs-surface: var(--reign-site-body-bg-color, var(--global-body-lightcolor, var(--bg-subtle, #f6f7f7)));
    --mvs-surface-2: var(--reign-site-body-bg-color, var(--global-body-lightcolor, var(--bg-hover, #eeeeee)));
    --mvs-overlay: rgba(0, 0, 0, 0.72);
    --mvs-bg-muted: var(--reign-site-body-bg-color, var(--global-body-lightcolor, #f9fafb));

    /* Text */
    --mvs-text: var(--reign-site-body-text-color, var(--global-title-color, var(--text-1, #1e1e1e)));
    --mvs-text-secondary: var(--reign-site-alternate-text-color, var(--global-font-color, var(--text-2, #646970)));
    --mvs-text-muted: var(--reign-site-alternate-text-color, var(--color-meta, var(--text-3, #a7aaad)));

    /* ── Borders: THREE TIERS, ONE JOB EACH ─────────────────────────────────
       Before 2.4.0 every box drew the same line: a text input, the card holding
       it, and the panel holding that were all `1px solid var(--mvs-border)`. So
       nesting produced three identical edges and nothing receded — the member
       counted boxes instead of reading content. Pick by JOB, never by taste:

         --mvs-border-light   STRUCTURE — cards, panels, sections, rows,
                              dividers. Neutral and quiet; it separates, it does
                              not ask to be looked at.
         --mvs-border         CONTROLS  — input, select, textarea, button, chip,
                              dropzone. Theme-tinted: a visible edge here means
                              "you can interact with me".
         --mvs-border-strong  EMPHASIS  — active/selected state, table head rule.

       Two hard rules that keep this from drifting back:
       1. A bordered box MUST NOT contain another bordered box. Where nesting is
          real, the OUTER one drops its border and expresses depth with
          background + spacing instead.
       2. Semantic borders (success/warning/danger, brand) are NOT part of this
          ladder — never swap one for a neutral tier, it destroys the meaning. */
    --mvs-border: var(--reign-site-border-color, var(--global-border-color, var(--border, #dcdcde)));
    --mvs-border-light: var(--border-soft, #e9e9ea);
    /* Elevation — BuddyNext-first (soft, layered --bn-shadow-*), with a matching
       standalone fallback. Shared Wbcom Community-OS standard: components pick a
       token, never a literal box-shadow, so elevation is uniform across the suite. */
    --mvs-shadow-sm: var(--bn-shadow-sm, 0 1px 2px rgba(16, 24, 40, 0.04), 0 2px 4px rgba(16, 24, 40, 0.04));
    --mvs-shadow-md: var(--bn-shadow-md, 0 2px 4px rgba(16, 24, 40, 0.04), 0 8px 16px rgba(16, 24, 40, 0.06));
    --mvs-shadow-lg: var(--bn-shadow-lg, 0 8px 16px rgba(16, 24, 40, 0.06), 0 24px 48px rgba(16, 24, 40, 0.08));

    /* Radii — BuddyNext first, then hardcoded fallback */
    --mvs-radius-sm: var(--r-sm, 6px);
    --mvs-radius-md: var(--r-md, 8px);
    --mvs-radius-lg: var(--r-lg, 12px);
    --mvs-radius-pill: var(--r-full, 100px);

    /*
     * Breakpoints — DOCUMENTATION ONLY. A @media query cannot read a custom
     * property, so nothing consumes these; they exist so that "the tablet
     * breakpoint" means one thing to the next person.
     *
     * They previously did not. This block said tablet=768 / mobile=480 while
     * src/shared/design-tokens.css said tablet=1024 / mobile=767 — the same two
     * names, different numbers, and whichever file you happened to read decided
     * what you wrote. That is how this codebase ended up with rules at both 767
     * and 768, and at both 1023 and 1024 (Basecamp 10252222205). Both blocks now
     * carry the list below.
     *
     *   480px  small phone                       (13 rules)
     *   640px  phone — the most-used breakpoint  (24 Free / 38 Pro)
     *   768px  tablet boundary; below it is 767  (17 rules)
     *  1024px  top of tablet; desktop is 1025+   (5 rules)
     *   782px  WordPress admin's own breakpoint — core's number, not ours
     *
     * The contract asks for three and there are five. Collapsing them is a
     * deliberate redesign, not a tidy-up, and is NOT what this card did — the
     * fix here was to stop the same name meaning two things.
     */
    --mvs-bp-phone-sm: 480px;
    --mvs-bp-phone: 640px;
    --mvs-bp-tablet: 768px;
    --mvs-bp-desktop: 1025px;

    /* ─── Mobile UX guideline tokens (see docs/MOBILE_UX_GUIDELINE.md §2.2) ── */

    /* Touch target floor — Apple HIG / WCAG 2.5.5 (AAA). Every interactive
       element below the desktop breakpoint must clear this in both axes. */
    --mvs-touch-min: 44px;

    /* Spacing scale — mirrors theme.json so PHP and CSS share one ramp. */
    --mvs-space-1: 0.25rem;  /*  4px */
    --mvs-space-2: 0.5rem;   /*  8px */
    --mvs-space-3: 1rem;     /* 16px — base gutter */
    --mvs-space-4: 1.5rem;   /* 24px */
    --mvs-space-5: 2rem;     /* 32px */
    --mvs-space-6: 3rem;     /* 48px */
    --mvs-space-7: 4rem;     /* 64px */

    /* What the page pins at the top of the viewport. Every sticky MVS surface
       adds one of these to its own offset so it parks BELOW that chrome
       instead of under it. A plugin cannot hardcode a theme's header height,
       so the numbers come from measurement —
       assets/js/frontend/sticky-top.js rewrites them on load/resize/scroll.
       0 is the honest default and the value that STAYS on a theme with no
       sticky header, so those themes keep the original offsets exactly.

       Two of them, because our own sticky surfaces stack on each other:

       --mvs-sticky-top    CHROME ONLY — the WP admin bar and the THEME's
                           header. Nothing of ours counts toward it.
                           Read by .mvs-dashboard-tabs and .mvs-dashboard-rail.
       --mvs-sticky-stack  Chrome PLUS our own pinned surfaces sitting above
                           the bulk bar (at <=860px the dashboard tab strip is
                           one). Read by .mvs-bulk-bar.

       The split is what stops a surface chasing itself: a single variable that
       the tab strip both read and contributed to would creep down the page one
       frame at a time. Each variable is measured with its own consumer
       excluded. Basecamp 10320911387. */
    --mvs-sticky-top: 0px;
    --mvs-sticky-stack: 0px;

    /* Z-index layers — never use bare integers in plugin CSS.
       The overlay-and-above band sits in the 9990s: theme sticky headers run
       high (Reign #masthead is 999/1000, BuddyBoss ~991), and at the old
       200–500 an MVS dropdown/modal/toast painted BEHIND the pinned header
       (Basecamp 10240335491). The band clears common theme headers while
       staying under the WP admin bar (99999), which must remain on top.
       Ceiling note: a theme with a fixed header at 99999 (e.g. Divi) would
       still win; that is a deliberate stop short of the admin bar rather than
       an oversight. Page chrome that legitimately sits below a theme header
       (base/sticky/fab) stays low. */
    --mvs-z-base:    1;
    --mvs-z-sticky:  50;
    --mvs-z-fab:     100;
    --mvs-z-overlay: 9990;
    --mvs-z-modal:   9991;
    --mvs-z-toast:   9992;
    --mvs-z-tooltip: 9993;

    /* Motion */
    --mvs-duration-fast: 120ms;
    --mvs-duration-base: 200ms;
    --mvs-duration-slow: 320ms;
    --mvs-easing: cubic-bezier(0.2, 0, 0, 1);

    /* FAB sizing + safe area so floating elements never overlap content. */
    --mvs-fab-size:   48px;
    --mvs-fab-offset: 16px;
    --mvs-fab-safe:   calc(var(--mvs-fab-size) + var(--mvs-fab-offset) * 2);
}

/* Dark mode: theme-controlled only (no prefers-color-scheme).
   Honors every Wbcom Community-OS dark signal:
   - [data-theme="dark"] on <html> (generic themes)
   - [data-bn-theme="dark"] on <html> (BuddyNext)
   - [data-bx-mode="dark"] on <html> (Reign / BuddyX / BuddyX Pro 5.1+)
   - .dark-mode on <html> or <body> (legacy BuddyX)
   When a parent theme is active its own --color-* vars cascade through the
   :root mapping above, so we only set hardcoded values for sites
   without a dark-mode-aware parent theme. */
[data-theme="dark"],
[data-bn-theme="dark"],
[data-bx-mode="dark"],
html.dark-mode,
body.dark-mode {
    /* Same Reign chain as :root, but with DARK hardcoded fallbacks. Reign flips
       its own --reign-site-* vars for dark, so those still track the theme.

       The generic --global-* / --color-* fallbacks are deliberately NOT repeated
       in this block. Themes such as BuddyX declare them once at :root with
       light-mode values (--global-font-color: #0f172a, --global-body-lightcolor:
       #f1f5f9 -- note the name) and never flip them for dark, so chaining
       through them meant the dark fallbacks below were never reached: dark mode
       resolved to near-black text on a dark surface (~1.3:1) on every
       MediaVerse surface. A theme that genuinely themes dark flips
       --reign-site-* or restyles directly; a readable default beats an
       unreadable "themed" one. */
    --mvs-bg: var(--reign-site-body-bg-color, #121212);
    --mvs-surface: var(--reign-site-body-bg-color, #1e1e1e);
    --mvs-surface-2: var(--reign-site-body-bg-color, #2a2a2a);
    --mvs-bg-muted: var(--reign-site-body-bg-color, #2a2a2a);
    --mvs-text: var(--reign-site-body-text-color, #f0f0f0);
    --mvs-text-secondary: var(--reign-site-alternate-text-color, #b0b0b0);
    --mvs-text-muted: var(--reign-site-alternate-text-color, #888888);
    --mvs-border: var(--reign-site-border-color, #363636);
    --mvs-border-light: #2e2e2e;
    /* Dark mode clamps the OTHER WAY - it does not skip the clamp.
       This used to be `var(--mvs-primary)` on the reasoning that "a theme
       already lightens its brand here (measured 7.98:1)". That was true of the
       accent it was measured against and false of the next one: BuddyX hands
       dark mode #e94560 unchanged and it scores 3.75:1 on --mvs-bg-muted, worse
       than anything the original report found in light mode. Not clamping is
       the same bug as clamping wrongly - it only works for the accents that
       happen to pass. Basecamp 10253005724. */
    --mvs-primary-text: oklch(from var(--mvs-primary) max(l, 0.72) c h);
    --mvs-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.35);
    --mvs-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.45);
    --mvs-shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.55);
    --mvs-warning: #f0b73a;
    /* Danger and success were never lightened for dark mode the way warning
       was, so a PDF chip painted --mvs-danger on a --mvs-danger-bg tint scored
       2.78:1 against the dark surface. The fill tokens stay put; only the text
       variants lift. */
    --mvs-danger-text: oklch(from var(--mvs-danger) max(l, 0.72) c h);
    --mvs-success-text: oklch(from var(--mvs-success) max(l, 0.72) c h);
    --mvs-info-bg: rgba(var(--mvs-primary-rgb), 0.18);
    --mvs-info-bg-light: rgba(var(--mvs-primary-rgb), 0.10);
    --mvs-success-bg: rgba(var(--mvs-success-rgb), 0.18);
    --mvs-warning-bg: rgba(240, 183, 58, 0.15);
    --mvs-danger-bg: rgba(var(--mvs-danger-rgb), 0.18);
}

[data-theme="dark"] .mvs-album-badge,
[data-bn-theme="dark"] .mvs-album-badge,
[data-bx-mode="dark"] .mvs-album-badge,
.dark-mode .mvs-album-badge,
[data-theme="dark"] .mvs-album-badge .mvs-icon svg,
[data-bn-theme="dark"] .mvs-album-badge .mvs-icon svg,
[data-bx-mode="dark"] .mvs-album-badge .mvs-icon svg,
.dark-mode .mvs-album-badge .mvs-icon svg,
[data-theme="dark"] .mvs-grid-item-overlay,
[data-bn-theme="dark"] .mvs-grid-item-overlay,
[data-bx-mode="dark"] .mvs-grid-item-overlay,
.dark-mode .mvs-grid-item-overlay {
    color: var(--mvs-text);
}

[data-theme="dark"] .mvs-empty-state .mvs-icon svg,
[data-bn-theme="dark"] .mvs-empty-state .mvs-icon svg,
[data-bx-mode="dark"] .mvs-empty-state .mvs-icon svg,
.dark-mode .mvs-empty-state .mvs-icon svg {
    color: var(--mvs-text-muted, #6c7781);
}

/* ==========================================================================
   0a. Type Reset — lock down all typography inside MVS templates.
       Follows BuddyNext type scale. Works with or without BuddyNext.
   ========================================================================== */

/* No body font-size here on purpose. body.mvs-page used to set
   var(--font-size-regular, 1rem|inherit), but no theme and not BuddyNext defines
   that token, so it only ever pinned MVS pages to the root size and overrode the
   theme's own (responsive) body size - 17px on Reign phones where the theme uses
   15px. The theme owns body text size. Basecamp 10317724451. */

/* Scope: all MVS template wrappers use these classes.
   font-family set here so BuddyNext's Inter/Plus Jakarta Sans carries through. */
.mvs-single-media,
.mvs-single-album,
.mvs-single-collection,
.mvs-explore-page,
.mvs-documents,
.mvs-dashboard,
.mvs-profile-edit {
    font-family: var(--mvs-font);
    font-size: 0.9375rem;
    line-height: 1.6;
    color: var(--mvs-text);
}

/* Private-media gate shown when the viewer can't see the requested media. */
.mvs-privacy-blocked {
    text-align: center;
    padding: var(--mvs-space-7) var(--mvs-space-4);
}

.mvs-privacy-blocked__icon {
    font-size: 48px;
}

/* Heading reset within any MVS template — uses display font for headings */
.mvs-single-media h1,
.mvs-single-album h1,
.mvs-single-collection h1,
.mvs-explore-page h1,
.mvs-documents h1,
.mvs-dashboard h1,
.mvs-profile-edit h1 {
    font-family: var(--mvs-font-heading);
    font-size: 1.375rem;
    font-weight: 700;
    line-height: 1.3;
    margin: 0 0 0.5em;
}

.mvs-single-media h2,
.mvs-single-album h2,
.mvs-single-collection h2,
.mvs-explore-page h2,
.mvs-documents h2,
.mvs-dashboard h2,
.mvs-profile-edit h2 {
    font-family: var(--mvs-font-heading);
    font-size: 1.25rem;
    font-weight: 700;
    line-height: 1.3;
    margin: 0 0 0.5em;
}

.mvs-single-media h3,
.mvs-single-album h3,
.mvs-single-collection h3,
.mvs-explore-page h3,
.mvs-documents h3,
.mvs-dashboard h3,
.mvs-profile-edit h3 {
    font-family: var(--mvs-font-heading);
    font-size: 1.125rem;
    font-weight: 600;
    line-height: 1.35;
    margin: 0 0 0.5em;
}

.mvs-single-media h4,
.mvs-single-album h4,
.mvs-single-collection h4,
.mvs-explore-page h4,
.mvs-documents h4,
.mvs-dashboard h4,
.mvs-profile-edit h4 {
    font-size: 0.875rem;
    font-weight: 600;
    margin: 0 0 0.5em;
}

/* Form controls do not inherit font-family from an ancestor -- the UA sheet
   gives them its own default -- so MediaVerse buttons/inputs rendered in the
   theme's page fell back to Arial while every div around them used the theme
   font. This used to be scoped to a hand-maintained wrapper list that missed
   .mvs-explore-page, .mvs-documents, .mvs-single-album and
   .mvs-single-collection. body.mvs-page (TemplateLoader::body_class) covers
   every MVS template at once; the footer-portalled shell (lightbox, FAB,
   toast) carries its own copy in shared-ui-frame.css because it renders
   outside these wrappers. Basecamp 10309690361, 10309616378. */
body.mvs-page button,
body.mvs-page input,
body.mvs-page select,
body.mvs-page textarea {
    font-family: inherit;
}

/* Size is a separate job at ZERO specificity. Sharing the rule above made
   `font-size: inherit` (0,1,2) outrank every control's own single-class size
   (.mvs-btn, .mvs-search-mode-btn, .mvs-lightbox-reaction ...), so they all
   rendered at body size. :where() keeps the fallback for controls that set no
   size of their own (inputs would otherwise drop to the UA 13.33px) and lets
   any authored size win. Basecamp 10309690361. */
:where(body.mvs-page) :where(button, input, select, textarea) {
    font-size: inherit;
}

/* Selects: never let a label run under the arrow, and never cut a word in
   half. Themes that restyle selects (Reign: appearance:none plus a background
   chevron 17px in from the edge) leave the UA arrow gutter to the control's own
   padding, and ours was 12px, so the tail of a long option sat under the
   chevron. The privacy labels are deliberately explicit (TemplateHelpers::
   privacy_labels) and can be wider than a phone-width control, so the closed
   state truncates with an ellipsis instead of clipping mid-word; the full label
   is still in the open list. Basecamp 10313754840. */
body.mvs-page select {
    padding-inline-end: 2.25rem;
    text-overflow: ellipsis;
}

/* Keyboard focus must be visible on every MVS form control, whatever the
   theme does: Reign ships select/input :focus { outline: 0 }, and most of our
   fields had no focus style of their own, so tabbing through a modal showed
   nothing (WCAG 2.4.7). Same ring the toolbar controls already use. Kept at
   (0,1,1) through :where() so a component's own focus style still wins, and
   declared after the theme so a tie goes our way. Basecamp 10320619382. */
:where(body.mvs-page) :is(input, select, textarea):focus-visible {
    outline: 2px solid var(--mvs-primary);
    outline-offset: 2px;
}

/* ==========================================================================
   0c. Mobile UX base layer
       Defined by docs/MOBILE_UX_GUIDELINE.md. Lives here so every template
       inherits Apple-quality defaults without per-page CSS overrides.
   ========================================================================== */

/* §3.1 — Universal touch-target floor below the desktop breakpoint.
   Every interactive element clears 44×44 in both axes, expanding via padding
   when the visible chip is smaller.

   1024, not 1023. Desktop starts at 1025 (src/shared/base.css), tablet is
   768-1024, and the block CSS generator emits `max-width: 1024px` for its
   tablet tier — so at exactly 1024px, which is iPad landscape, this floor was
   the ONLY tablet rule in the plugin that switched itself off. The three other
   tablet blocks in this file already say 1024; this one was the outlier
   (Basecamp 10252222205). */
@media (max-width: 1024px) {
    .mvs-page .mvs-btn,
    .mvs-page .mvs-icon-btn,
    .mvs-page .mvs-fab,
    .mvs-page .mvs-tab,
    .mvs-page .mvs-action-icon,
    .mvs-page .mvs-toast-close,
    .mvs-page .mvs-modal-close,
    .mvs-page .mvs-back-link,
    .mvs-page .mvs-tag-pill,
    .mvs-page .mvs-tag-cloud-item,
    .mvs-page .mvs-bulk-check,
    .mvs-single-media .mvs-btn,
    .mvs-single-album .mvs-btn,
    .mvs-profile-edit .mvs-btn,
    .mvs-dashboard .mvs-btn {
        min-height: var(--mvs-touch-min);
        min-width: var(--mvs-touch-min);
    }
    /* Load More is a full-width-ish button whose own padding left it at 40px
       tall. Height only — its width is already well past the floor and forcing
       min-width would fight the centred layout. */
    .mvs-page .mvs-load-more-btn {
        min-height: var(--mvs-touch-min);
    }
    /* Icon-only link wrapping a 40px avatar. Grow the HIT AREA to the floor and
       centre the avatar inside it, rather than scaling the image up — the
       picture should stay 40px, the target should not. Same technique as the
       chips above. Found by the tap-target assertion in
       audit/journeys/customer/08-explore-browse-mobile.md. */
    .mvs-page .mvs-dashboard-rail-head__avatar-link {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: var(--mvs-touch-min);
        min-width: var(--mvs-touch-min);
    }
    /* Small chips/tags expand their hit area via padding instead of growing
       the visible bubble. Tag-cloud filter chips were 36px before this floor. */
    .mvs-page .mvs-tag-pill,
    .mvs-page .mvs-tag-cloud-item {
        display: inline-flex;
        align-items: center;
    }
    /* Upload privacy select sat at 35px — give it the touch-target height
       without forcing the 44px min-width a full-width control would break on. */
    .mvs-page .mvs-upload-privacy {
        min-height: var(--mvs-touch-min);
    }
}

/* §6 — Back link component for every detail page header. */
.mvs-back-link {
    display: inline-flex;
    align-items: center;
    gap: var(--mvs-space-2);
    color: var(--mvs-text-secondary);
    text-decoration: none;
    font-size: 0.875rem;
    line-height: 1;
    padding: var(--mvs-space-2) var(--mvs-space-3) var(--mvs-space-2) var(--mvs-space-2);
    margin-bottom: var(--mvs-space-3);
    border-radius: var(--mvs-radius-md);
    transition: color var(--mvs-duration-fast) var(--mvs-easing),
                background var(--mvs-duration-fast) var(--mvs-easing);
}

.mvs-back-link:hover,
.mvs-back-link:focus-visible {
    color: var(--mvs-primary-text, var(--mvs-primary));
    background: var(--mvs-info-bg-light);
}

.mvs-back-link i,
.mvs-back-link svg {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    /* Hide textual label — keep the chevron only.
       The screen-reader label stays via aria-label on the anchor. */
    .mvs-back-link__label {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
    .mvs-back-link {
        padding: var(--mvs-space-2);
    }
}

/* §7 — FAB safe area: every page that hosts a FAB pads its bottom so content
   never sits behind it. iOS notch supported via env(safe-area-inset-bottom). */
.mvs-page,
.mvs-single-media,
.mvs-single-album,
.mvs-single-collection,
.mvs-profile-edit,
.mvs-explore-page,
.mvs-documents,
.mvs-dashboard {
    padding-bottom: calc(var(--mvs-fab-safe) + env(safe-area-inset-bottom, 0px));
}

/* §4.1/§4.2 — Icon-collapse buttons: text-and-icon control that collapses to
   icon-only below the tablet breakpoint and exposes a CSS-only tooltip. */
.mvs-btn--icon-collapse {
    display: inline-flex;
    align-items: center;
    gap: var(--mvs-space-2);
    position: relative;
}

.mvs-btn--icon-collapse i,
.mvs-btn--icon-collapse svg {
    width: 18px;
    height: 18px;
    flex-shrink: 0;
}

@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    .mvs-btn--icon-collapse .mvs-btn__label {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
    }
    .mvs-btn--icon-collapse {
        padding-inline: var(--mvs-space-2);
        gap: 0;
        justify-content: center;
    }
}

/* CSS-only tooltip: shows on :hover (mouse) and :focus-visible (touch + kb).
   Pages set data-mvs-tooltip="Label" on any element to opt in. */
[data-mvs-tooltip] {
    position: relative;
}

[data-mvs-tooltip]::after {
    content: attr(data-mvs-tooltip);
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%) translateY(4px);
    background: var(--mvs-text);
    color: var(--mvs-bg);
    font-size: 0.75rem;
    line-height: 1;
    padding: var(--mvs-space-2) var(--mvs-space-2);
    border-radius: var(--mvs-radius-sm);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity var(--mvs-duration-fast) var(--mvs-easing),
                transform var(--mvs-duration-fast) var(--mvs-easing);
    z-index: var(--mvs-z-tooltip);
}

[data-mvs-tooltip]:hover::after,
[data-mvs-tooltip]:focus-visible::after {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

@media (prefers-reduced-motion: reduce) {
    [data-mvs-tooltip]::after { transition: none; }
}

/* §-skeleton — Loading shimmer placeholders.
   Drop-in component for any "waiting on REST" surface. Pages add the
   .mvs-skeleton class to a sized box; the shimmer animation pulses across.
   Reduced motion swaps to a static muted background. */
.mvs-skeleton {
    position: relative;
    overflow: hidden;
    background: var(--mvs-surface);
    border-radius: var(--mvs-radius-md);
    isolation: isolate;
}

.mvs-skeleton::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(
        90deg,
        transparent 0,
        rgba(255, 255, 255, 0.4) 50%,
        transparent 100%
    );
    transform: translateX(-100%);
    animation: mvs-skeleton-shimmer 1.4s var(--mvs-easing) infinite;
}

[data-theme="dark"] .mvs-skeleton::after,
[data-bx-mode="dark"] .mvs-skeleton::after,
html.dark-mode .mvs-skeleton::after,
body.dark-mode .mvs-skeleton::after {
    background: linear-gradient(
        90deg,
        transparent 0,
        rgba(255, 255, 255, 0.06) 50%,
        transparent 100%
    );
}

@keyframes mvs-skeleton-shimmer {
    100% {
        transform: translateX(100%);
    }
}

@media (prefers-reduced-motion: reduce) {
    .mvs-skeleton::after {
        animation: none;
    }
}

.mvs-skeleton--text {
    height: 0.875rem;
    margin-bottom: 0.5rem;
    border-radius: var(--mvs-radius-sm);
}

.mvs-skeleton--text:last-child {
    margin-bottom: 0;
    width: 60%;
}

.mvs-skeleton--avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
}

.mvs-skeleton--thumb {
    aspect-ratio: 1 / 1;
    border-radius: var(--mvs-radius-md);
}

.mvs-skeleton-grid {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: var(--mvs-space-2);
}

@media (min-width: 768px) /* --mvs-bp-tablet and up */ {
    .mvs-skeleton-grid {
        grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
        gap: var(--mvs-space-3);
    }
}

/* §5 — Tab strip pattern: horizontal scroll with edge-fade mask + scroll-snap.
   Components opt in by adding the .mvs-tabs-strip class. */
.mvs-tabs-strip {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    -ms-overflow-style: none;
    /* Edge-fade telegraphs that more tabs exist beyond the right edge. */
    -webkit-mask-image: linear-gradient(to right, black calc(100% - 32px), transparent);
    mask-image: linear-gradient(to right, black calc(100% - 32px), transparent);
}

.mvs-tabs-strip::-webkit-scrollbar { display: none; }

.mvs-tabs-strip > * {
    scroll-snap-align: start;
    flex: 0 0 auto;
}

/* When the strip fits without scrolling, drop the mask so the right edge is solid. */
@supports selector(:has(*)) {
    .mvs-tabs-strip:not(:hover):not(:focus-within):where(:not([data-overflow="true"])) {
        /* Re-enabled by JS via [data-overflow="true"] when scrollWidth > clientWidth. */
    }
}

/* ==========================================================================
   0b. Profile Header Card (explore grid filtered by user)
   ========================================================================== */

.mvs-profile-header-card {
    display: flex;
    align-items: flex-start;
    gap: 32px;
    padding: 32px;
    margin-bottom: 24px;
    background: var(--mvs-surface);
    /* Pair the foreground with the background this component sets; without it
       the text inherited the ambient theme colour and went unreadable in dark
       mode (measured 1.07-1.38:1). */
    color: var(--mvs-text);
    border-radius: 12px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}

.mvs-profile-header-avatar {
    width: 120px;
    height: 120px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
    border: 3px solid var(--mvs-surface-2);
}

.mvs-profile-header-info {
    flex: 1;
    min-width: 0;
}

.mvs-profile-header-top {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-bottom: 12px;
    flex-wrap: wrap;
}

.mvs-profile-header-name {
    margin: 0;
    font-size: 1.5rem;
    font-weight: 700;
    /* No color → inherits theme heading color, washes out on the dark surface
       card in dark mode (same bug class as the compete-hub headings). */
    color: var(--mvs-text);
}

.mvs-follow-toggle {
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
}

.mvs-follow-toggle--following {
    background: var(--mvs-surface-2);
    color: var(--mvs-text);
    border: 1px solid var(--mvs-border);
}

.mvs-follow-toggle--following:hover {
    background: var(--mvs-danger-bg);
    color: var(--mvs-danger);
    border-color: var(--mvs-danger);
}

.mvs-follow-toggle:disabled {
    opacity: 0.6;
    cursor: wait;
}

.mvs-message-btn {
    transition: background 0.15s;
}

/* align-items: the follower/following buttons carry padding-block for the 44px
   tap target while the "N media" span does not, so under the default stretch
   the span's content sat ~14px higher than the buttons'. Basecamp 10313672574. */
.mvs-profile-header-stats {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--mvs-space-xs, 6px) 28px;
    font-size: 0.9375rem;
    color: var(--mvs-text-secondary);
    margin-bottom: 12px;
}

/* Each stat stays one unit on one line. The media count is a span and the
   follower counts are buttons; when the row got tight on a phone the buttons
   shrank and broke "6 / followers" over two lines while "6 media" stayed on
   one, so the row read as three layouts. A stat that does not fit now moves
   to the next line whole. Basecamp 10313672574. */
.mvs-profile-header-stats > * {
    flex-shrink: 0;
    white-space: nowrap;
}

.mvs-profile-header-stats span {
    display: flex;
    align-items: baseline;
    gap: 4px;
}

.mvs-profile-header-stats strong {
    color: var(--mvs-text);
    font-size: 1.125rem;
    font-weight: 700;
}

.mvs-profile-header-bio {
    margin: 0;
    color: var(--mvs-text);
    font-size: 0.9rem;
    line-height: 1.6;
    max-width: 600px;
}

@media (max-width: 640px) {
    .mvs-profile-header-card {
        flex-direction: column;
        align-items: center;
        text-align: center;
        gap: 16px;
        padding: 24px 16px;
    }

    .mvs-profile-header-avatar {
        width: 96px;
        height: 96px;
    }

    .mvs-profile-header-top {
        justify-content: center;
    }

    .mvs-profile-header-stats {
        justify-content: center;
        /* 28px between three stats needs ~311px; the phone header column is
           ~288px. The stat buttons already carry 6px inline padding. */
        column-gap: var(--mvs-space-sm, 8px);
    }

    .mvs-profile-header-bio {
        max-width: 100%;
    }
}

/* ==========================================================================
   1. Single Media Template
   ========================================================================== */

.mvs-single-media {
    max-width: 680px;
    margin: 0 auto;
    padding: 20px 16px;
}

.mvs-media-article {
    background: var(--mvs-bg);
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.mvs-media-header {
    padding: 16px var(--mvs-space-4);
}

.mvs-media-header-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 12px;
}

.mvs-media-author-info {
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
}

.mvs-media-author-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    flex-shrink: 0;
}

.mvs-media-author-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.mvs-media-author-name {
    font-weight: 600;
    font-size: 0.9375rem;
    color: var(--mvs-text);
    text-decoration: none;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.mvs-media-author-name:hover {
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-media-date {
    font-size: 0.8125rem;
    color: var(--mvs-text-muted);
}

.mvs-media-sep {
    margin: 0 2px;
}

.mvs-media-title {
    margin: 0;
    font-size: 1.5em;
    line-height: 1.3;
}

/* ==========================================================================
   Document identity line — type, size, privacy, location.
   The drive row already showed all four; this page showed none, so opening a
   document from the list LOST information. Privacy is the one that matters:
   without it a member cannot tell a private contract from a public one, on the
   same screen as a button that can mint a link anyone can open.
   ========================================================================== */

.mvs-media-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--mvs-space-2);
    margin: var(--mvs-space-2) 0 0;
    color: var(--mvs-text-muted);
    font-size: var(--mvs-font-sm);
    line-height: 1.4;
}

/* Separator is drawn, not typed, so the parts stay independently omittable —
   any of type / size / privacy / location can be absent without leaving a
   stranded bullet behind. */
.mvs-media-meta__item + .mvs-media-meta__item::before,
.mvs-media-meta__type + .mvs-media-meta__item::before,
.mvs-media-meta__privacy + .mvs-media-meta__item::before {
    content: "·";
    margin-inline-end: var(--mvs-space-2);
    opacity: 0.55;
}

.mvs-media-meta__item {
    font-variant-numeric: tabular-nums;
}

/* The type chip is a LABEL, not a status, so it stays neutral.
   It deliberately does NOT copy the drive's `--mvs-info-bg` + `--mvs-primary`
   pairing: on this theme `--mvs-primary` is red, so that pair renders red text
   on a blue wash. A neutral chip also lets the privacy chip beside it be the
   only coloured thing on the line, which is the one worth noticing. */
.mvs-media-meta__type {
    display: inline-flex;
    align-items: center;
    padding: 2px 6px;
    border-radius: var(--mvs-radius-sm);
    background: var(--mvs-bg-muted);
    color: var(--mvs-text-secondary);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
}

/* Amber wash carries the "restricted" signal; the TEXT is the normal ink so it
   stays legible. `--mvs-warning` on `--mvs-warning-bg` is the same hue at 15%
   alpha — amber on amber, which measured at almost no contrast in either
   theme. */
.mvs-media-meta__privacy {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: var(--mvs-radius-pill);
    background: var(--mvs-warning-bg);
    color: var(--mvs-text);
    font-size: var(--mvs-font-sm);
    font-weight: 600;
}

/* Sits beside the privacy chip and borrows its shape. Deliberately NOT the
   privacy chip's colour: this is not a restriction, it is a fact about reach,
   and colouring it like a lock would read as a second privacy state. */
.mvs-media-meta__shared {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    background: var(--mvs-surface-2);
    color: var(--mvs-text-secondary);
    border-radius: var(--mvs-radius-pill, 999px);
    padding: 2px var(--mvs-space-sm, 8px);
    font-size: 0.8125rem;
}

.mvs-media-meta__shared svg,
.mvs-media-meta__shared [data-lucide] {
    width: 14px;
    height: 14px;
    color: inherit;
    flex-shrink: 0;
}

.mvs-media-meta__privacy i,
.mvs-media-meta__privacy svg {
    color: var(--mvs-warning);
}

.mvs-media-meta__privacy i,
.mvs-media-meta__privacy svg {
    width: 12px;
    height: 12px;
}

/* The document block lines up with the title above it.
   `.mvs-media-content` is deliberately flush at 0 so a PHOTO can run edge to
   edge in its card — that is right for an image and wrong for a page of text,
   which sat 24px to the left of its own heading. Inset the document wrapper
   only, rather than padding the shared content area and boxing in every image
   on the site. */
.mvs-media-document {
    padding-inline: var(--mvs-space-4);
}

@media (max-width: 640px) {

    .mvs-media-document {
        padding-inline: var(--mvs-space-3);
    }

}

/* Follow steps back on a document page so Download is the only primary. */
.mvs-btn.mvs-follow-btn--quiet {
    background: transparent;
    border-color: var(--mvs-border);
    color: var(--mvs-text-secondary);
    font-weight: 500;
}

.mvs-btn.mvs-follow-btn--quiet:hover,
.mvs-btn.mvs-follow-btn--quiet:focus-visible {
    border-color: var(--mvs-primary);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

/* Media Content */
.mvs-media-content {
    padding: 0;
}

.mvs-media-image {
    width: 100%;
    background: var(--mvs-surface-2);
}

.mvs-media-image img {
    display: block;
    width: 100%;
    height: auto;
}

.mvs-media-video {
    width: 100%;
    background: var(--mvs-overlay);
    /* aspect-ratio is set inline from the media's stored width/height so
     * the frame is reserved on first paint. Without it, the box reflows
     * when metadata/poster/playback transitions happen. */
}

.mvs-media-video video {
    display: block;
    width: 100%;
    height: 100%;
    /* Letterbox the source inside the locked frame instead of stretching
     * — keeps poster and playback at identical dimensions. */
    object-fit: contain;
    background: #000;
}

.mvs-media-audio {
    padding: 16px 24px;
}

.mvs-media-audio audio {
    width: 100%;
}

.mvs-media-description {
    padding: 12px var(--mvs-space-4) 0;
    line-height: 1.6;
    color: var(--mvs-text);
    font-size: 0.9375rem;
}

.mvs-media-description p:first-child {
    margin-top: 0;
}

/* Footer & Tags */

/*
 * ONE HORIZONTAL RHYTHM ON THE MEDIA CARD.
 *
 * The card's four rows each chose their own inline padding — header 24px,
 * content 0 (its own children re-inset to 24), social wrapper 0, footer and
 * comments 16px — so the comment box and the description sat 8px closer to the
 * card's edge than the title, meta, preview and actions above them. Reported as
 * "why is it touching the border": nothing was overflowing, the rows simply
 * disagreed about where the edge is.
 *
 * 24px desktop / 16px mobile, on every row, so a vertical line drawn down the
 * card touches the start of all of them.
 */
.mvs-media-footer {
    padding: 8px var(--mvs-space-4) var(--mvs-space-4);
}

.mvs-media-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}

.mvs-tag {
    display: inline-block;
    padding: 4px 12px;
    background: var(--mvs-info-bg);
    border-radius: 16px;
    font-size: 0.8125rem;
    color: var(--mvs-primary-text, var(--mvs-primary));
    text-decoration: none;
    transition: all 0.2s;
}

.mvs-tag:hover {
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
}

/* ==========================================================================
   2. Album Template
   ========================================================================== */

.mvs-single-album {
    max-width: var(--mvs-content-width, 1200px);
    margin: 0 auto;
    padding: 20px;
}

.mvs-album-article {
    background: var(--mvs-bg);
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    padding: 24px;
}

.mvs-album-header {
    margin-bottom: 24px;
}

.mvs-album-title {
    margin: 0 0 8px;
    font-size: 1.75em;
}

.mvs-album-description {
    color: var(--mvs-text-secondary);
    margin-bottom: 12px;
    line-height: 1.6;
}

.mvs-album-count {
    display: inline-block;
    padding: 4px 12px;
    background: var(--mvs-surface-2);
    border-radius: 16px;
    font-size: 0.85em;
    color: var(--mvs-text-secondary);
}

/* ==========================================================================
   3. Explore / Archive Template
   ========================================================================== */

.mvs-explore-page {
    max-width: var(--mvs-content-width, 1200px);
    margin: 0 auto;
    padding: 20px;
    box-sizing: border-box;
    width: 100%;
}

.mvs-explore-header {
    margin-bottom: 24px;
}

.mvs-explore-header h1 {
    margin: 0;
    font-size: 1.75em;
    /* Sits directly on the THEME's page background, not on a MediaVerse
       surface, so it follows the theme's own text colour. Using --mvs-text made
       it vanish on a theme that signals dark (so --mvs-text goes light) while
       keeping the page background white - BuddyX only darkens its footer. */
    color: inherit;
}

/* ==========================================================================
   4. Media Grid (shared by explore, album, shortcodes)
   ========================================================================== */

.mvs-media-grid {
    display: grid;
    /* The token, with the long-standing 16px as its fallback. The media-grid
       block emits --mvs-grid-gap per instance from its Gap control; that only
       worked while the block shipped its own duplicate .mvs-media-grid rule.
       Reading it here keeps the control working once that duplicate is gone,
       and every surface that sets nothing still gets 16px. */
    gap: var(--mvs-grid-gap, 16px);
    max-width: 100%;
    overflow: hidden;
    box-sizing: border-box;
}

.mvs-cols-2 {
    grid-template-columns: repeat(2, 1fr);
}

.mvs-cols-3 {
    grid-template-columns: repeat(3, 1fr);
}

.mvs-cols-4 {
    grid-template-columns: repeat(4, 1fr);
}

.mvs-cols-5 {
    grid-template-columns: repeat(5, 1fr);
}

@media (max-width: 1024px) {

    .mvs-cols-5 {
        grid-template-columns: repeat(4, 1fr);
    }
}

@media (max-width: 767px) /* below --mvs-bp-tablet */ {

    .mvs-cols-3,
    .mvs-cols-4,
    .mvs-cols-5 {
        grid-template-columns: repeat(2, 1fr);
    }
}

@media (max-width: 480px) {

    .mvs-cols-2,
    .mvs-cols-3,
    .mvs-cols-4,
    .mvs-cols-5 {
        grid-template-columns: 1fr;
    }
}

.mvs-grid-item {
    position: relative;
    overflow: hidden;
    border-radius: 8px;
    background: var(--mvs-surface-2);
    /* Pair the foreground with the background this component sets; without it
       the text inherited the ambient theme colour and went unreadable in dark
       mode (measured 1.07-1.38:1). */
    color: var(--mvs-text);
    aspect-ratio: 1;
    min-width: 0;
}

.mvs-grid-item > a {
    display: block;
    width: 100%;
    height: 100%;
}

.mvs-grid-item img,
.mvs-grid-item .mvs-grid-video-preview {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.3s;
    background: #0f1014;
}

.mvs-grid-item:hover img,
.mvs-grid-item:hover .mvs-grid-video-preview {
    transform: scale(1.05);
}

.mvs-grid-item .mvs-grid-video-preview {
    pointer-events: none;
}

.mvs-grid--original .mvs-grid-item img {
    /* Fills the fixed-height link. The justified row already gives the tile the
       width its aspect ratio earns, so `cover` crops nothing visible. */
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.mvs-grid--original .mvs-dashboard-card-thumb {
    aspect-ratio: auto;
}

.mvs-grid--original .mvs-dashboard-card-thumb img {
    height: auto;
    object-fit: contain;
}

/* Original mode = JUSTIFIED ROWS (Flickr / Google Photos): every image keeps
   its native aspect ratio, each row is stretched to the full width, and rows
   read LEFT TO RIGHT.

   This replaced CSS multi-column, which packed tightly but filled column-major:
   with "Newest first" the visible top row was items #1, #5 and #9, so scanning
   the page the way people actually scan it did not give newest-first. The sort
   control, the REST order and the DOM were all correct and only the layout
   disagreed (Basecamp 10298798668).

   The technique is Pro's Flickr layout, which already solved this: each item
   carries its aspect ratio as the `--mvs-ar` instance custom property, and
   flex-grow distributes each row's width in proportion to it. flex-basis is
   derived from the same ratio so a row WRAPS at the right point - with
   flex-basis 0 every tile would land on one line and never wrap.

   Why not CSS grid + `grid-row: span N`: a grid row cannot be sized as a
   function of column width, so a server-computed span is only correct at one
   column count and this layout goes 3 -> 2 -> 1 columns. That needs a JS
   measure pass; this does not. */
.mvs-media-grid.mvs-grid--original {
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    /* 4px, not the 16px the square grid uses, and the difference is not
       cosmetic - it decides how many tiles fit a row.

       A tile's flex-basis is `ratio * row-height`, so three landscape tiles
       want 3 x 360 = 1080px. At a 1100px container the 16px gap made that
       1080 + 32 = 1112 and the row wrapped to TWO tiles, which then stretched
       to fill 1100 - displaying at aspect 2.13 against a natural 1.50 and
       cropping 32% of each image's height. Missing by 12px cost a third of
       the picture. At 4px it is 1080 + 8 = 1088, three tiles fit, and the
       stretch is 1100/1080 - about 2%. Measured, not estimated.

       Same value Pro's Flickr layout uses (--mvs-fl-gap), which is the same
       technique and has the same arithmetic.

       Still `var(--mvs-grid-gap, ...)`: the media-grid block emits that
       property per instance from its Gap control, so an owner who sets a gap
       deliberately still wins - only the DEFAULT changed. */
    gap: var(--mvs-grid-gap, 4px);
}

.mvs-grid--original .mvs-grid-item {
    /* --mvs-ar is emitted per item by TemplateHelpers::grid_item_ar_style()
       (PHP) and window.mvsCardBuilders (JS). The 1.5 fallback is the same
       landscape default Flickr uses when an item has no stored dimensions -
       album and collection covers, for instance. */
    flex-grow: var(--mvs-ar, 1.5);
    flex-shrink: 1;
    flex-basis: calc(var(--mvs-ar, 1.5) * var(--mvs-grid-row-height));
    min-width: 120px;
    max-width: 100%;
    aspect-ratio: auto;
}

/* A lone orphan on the last row would otherwise stretch the full width. */
.mvs-grid--original .mvs-grid-item:last-child {
    max-width: min(100%, calc(var(--mvs-ar, 1.5) * var(--mvs-grid-row-height)));
}

/* The card's <a> wrapper is forced square by `.mvs-feed .mvs-grid-item-link`
   (a later, equal-specificity rule); beat it with a compound selector so the
   link hugs the image.

   DIRECT CHILD only, never `.mvs-grid-item a`. That descendant form also
   matched the author profile link inside .mvs-grid-item-info, which then took
   the full row height: every card rendered 496px tall around a 240px image.
   It was harmless while this rule said `height: auto` and became a bug the
   moment it carried a real height. */
.mvs-media-grid.mvs-grid--original .mvs-grid-item-link,
.mvs-media-grid.mvs-grid--original .mvs-grid-item > a {
    aspect-ratio: auto;
    /* The ROW height lands here, not on .mvs-grid-item: render_grid_item()
       adds a .mvs-grid-item-info author row below this link on some surfaces
       and not others. Fixing the image area keeps every card in a row the same
       height either way; fixing the whole card would clip that row. */
    height: var(--mvs-grid-row-height);
    display: block;
}

/* The author row keeps its natural height - it is a sibling of the media link,
   not part of the image area. */
.mvs-media-grid.mvs-grid--original .mvs-grid-item-info a {
    height: auto;
}

/* List mode: one row per item, thumbnail beside the title. Overrides
   .mvs-media-grid's columns and .mvs-grid-item's square aspect-ratio.
   The title comes OUT of the hover overlay deliberately: a row that reveals
   its title on hover is unreadable on touch, where there is no hover. */
.mvs-media-grid.mvs-grid--list {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

/* COMPOUND selector deliberately. `.mvs-feed .mvs-grid-item` (display:flex,
   ~150 lines below) has the same 0-2-0 specificity and wins on order, so the
   plain `.mvs-grid--list .mvs-grid-item` form lost every time: display stayed
   flex, grid-template-columns was computed but inert, and the thumbnail filled
   the row at 1084px square instead of sitting 96px beside the title. Explore
   carries `mvs-feed` on its container (explore.php), so that was every Explore
   surface - the List option a site owner can pick has never rendered as a list.
   Basecamp 10299239470. */
.mvs-media-grid.mvs-grid--list .mvs-grid-item {
    aspect-ratio: auto;
    display: grid;
    grid-template-columns: 96px 1fr;
    align-items: center;
    gap: 12px;
    padding: 8px;
}

/* The square wrapper is wanted here - it keeps the row thumbnails aligned -
   but it must size to the column, not to the row.

   DIRECT CHILD only, never `.mvs-grid-item a`. The descendant form also matched
   the author profile link inside .mvs-grid-item-info, which sits in the 1fr
   column: at ~956px wide, `aspect-ratio: 1` made it ~956px TALL and dragged the
   whole list row to 988px. Same over-broad selector, same author link, same
   symptom as the one fixed in `--original` above - the two were written apart
   and only one was hardened (Basecamp 10299239470). */
.mvs-media-grid.mvs-grid--list .mvs-grid-item-link,
.mvs-media-grid.mvs-grid--list .mvs-grid-item > a {
    aspect-ratio: 1;
    height: auto;
    border-radius: 6px;
    overflow: hidden;
}

.mvs-grid--list .mvs-grid-item-overlay {
    position: static;
    inset: auto;
    background: none;
    color: inherit;
    opacity: 1;
    justify-content: flex-start;
    /* start, not left: the row reads right-to-left in an RTL locale. */
    text-align: start;
    padding: 0;
}

.mvs-grid--list .mvs-grid-item-title {
    white-space: normal;
    font-weight: 600;
}

@media (max-width: 480px) {

    .mvs-grid--list .mvs-grid-item {
        grid-template-columns: 72px 1fr;
    }
}

/* Justified rows need no column-count breakpoints - the row height is the
   only knob, and fewer/taller items land per row as the screen narrows.
   Same ladder as Pro's Flickr layout. */
@media (max-width: 1024px) {

    :root {
        --mvs-grid-row-height: 180px;
    }
}

@media (max-width: 640px) {

    :root {
        --mvs-grid-row-height: 140px;
    }
}

.mvs-grid-item-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    color: var(--mvs-text-muted);
}

.mvs-grid-item-placeholder .mvs-icon [data-lucide],
.mvs-grid-item-placeholder .mvs-icon svg {
    width: 48px;
    height: 48px;
}

.mvs-grid-item-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.4);
    color: var(--mvs-bg);
    font-size: 0.9em;
    opacity: 0;
    transition: opacity 0.3s;
}

.mvs-grid-item:hover .mvs-grid-item-overlay,
.mvs-grid-item-link:hover .mvs-grid-item-overlay {
    opacity: 1;
}

.mvs-grid-item-stats {
    display: flex;
    gap: 16px;
    font-size: 1.1em;
    font-weight: 700;
}

.mvs-grid-stat {
    display: flex;
    align-items: center;
    gap: 4px;
}

/* Lucide's default <svg> is 24x24, which towers over a two-digit count in the
   thumbnail overlay. Same cap the button rule applies, at the smaller size this
   context wants. `currentColor` is what makes these follow the token palette —
   the thing the emoji they replaced could never do. */
/* Cover placeholders for albums and collections. Same reason as the grid stats:
   a Lucide <svg> ignores the `font-size` these were sized by, so it needs real
   dimensions to keep the presence the placeholder was laid out around. */
.mvs-grid-collection-icon svg,
.mvs-grid-collection-icon [data-lucide],
.mvs-grid-album-icon svg,
.mvs-grid-album-icon [data-lucide] {
    width: 40px;
    height: 40px;
    stroke-width: 1.5;
    color: inherit;
}

.mvs-grid-stat svg,
.mvs-grid-stat [data-lucide] {
    width: 14px;
    height: 14px;
    stroke-width: 2;
    flex-shrink: 0;
    color: inherit;
}

.mvs-grid-item-title {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Instagram-like feed: author row below each card */
.mvs-feed {
    gap: var(--mvs-grid-gap, 6px);
}

.mvs-feed .mvs-grid-item {
    aspect-ratio: auto;
    border-radius: 4px;
    overflow: hidden;
    background: var(--mvs-bg);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.mvs-feed .mvs-grid-item-link {
    display: block;
    aspect-ratio: 1;
    overflow: hidden;
    position: relative;
}

.mvs-feed .mvs-grid-item-overlay {
    pointer-events: none;
}

.mvs-grid-item-info {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    background: var(--mvs-bg);
    font-size: 0.85em;
}

.mvs-grid-avatar {
    border-radius: 50%;
    width: 24px !important;
    height: 24px !important;
}

.mvs-grid-item-author {
    font-weight: 600;
    color: var(--mvs-text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Avatar + name link to the uploader's profile (card #9962508646). Reset the
   default link chrome so it reads like the prior plain text; the name
   underlines + tints on hover/focus as the affordance. min-width:0 keeps the
   inner ellipsis working inside the flex link. */
.mvs-grid-item-author-link {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
    text-decoration: none;
    color: inherit;
}

.mvs-grid-item-author-link:visited {
    color: inherit;
}

.mvs-grid-item-author-link:hover .mvs-grid-item-author,
.mvs-grid-item-author-link:focus-visible .mvs-grid-item-author {
    color: var(--mvs-primary-text, var(--mvs-text));
    text-decoration: underline;
}

.mvs-grid-item-author-link:focus-visible {
    outline: 2px solid var(--mvs-primary, currentColor);
    outline-offset: 2px;
    border-radius: 4px;
}

/* -- Album badge (carousel indicator) in feed */
.mvs-album-badge {
    position: absolute;
    top: 8px;
    right: 8px;
    display: flex;
    align-items: center;
    gap: 2px;
    background: rgba(0, 0, 0, 0.6);
    color: var(--mvs-bg);
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 0.8em;
    font-weight: 600;
    pointer-events: none;
    z-index: 2;
}

.mvs-album-badge .mvs-icon [data-lucide],
.mvs-album-badge .mvs-icon svg {
    width: 16px;
    height: 16px;
    color: var(--mvs-bg);
}

/* -- Gallery badge (multi-image group indicator) */
.mvs-gallery-badge {
    position: absolute;
    top: 8px;
    right: 8px;
    display: flex;
    align-items: center;
    gap: 2px;
    background: rgba(0, 0, 0, 0.6);
    color: var(--mvs-surface);
    padding: 4px 8px;
    border-radius: 4px;
    font-size: 0.8em;
    font-weight: 600;
    pointer-events: none;
    z-index: 2;
}

.mvs-gallery-badge .mvs-icon [data-lucide],
.mvs-gallery-badge .mvs-icon svg {
    width: 16px;
    height: 16px;
    color: var(--mvs-surface);
}

/* ==========================================================================
   5. Pagination
   ========================================================================== */

.mvs-pagination {
    margin-top: 32px;
    text-align: center;
}

.mvs-pagination .nav-links {
    display: flex;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
}

.mvs-pagination .page-numbers {
    display: inline-block;
    padding: 8px 14px;
    background: var(--mvs-surface-2);
    border-radius: 4px;
    text-decoration: none;
    color: var(--mvs-text);
    font-size: 0.9em;
    transition: background 0.2s;
}

.mvs-pagination .page-numbers.current,
.mvs-pagination .page-numbers:hover {
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
}

/* ==========================================================================
   6. Empty State
   ========================================================================== */

.mvs-no-media {
    text-align: center;
    padding: 48px 24px;
    color: var(--mvs-text-muted);
    font-size: 1.1em;
}

/* ==========================================================================
   9. Story Viewer
   ========================================================================== */

.mvs-story-viewer {
    position: relative;
    max-width: 400px;
    margin: 0 auto;
    background: var(--mvs-overlay);
    border-radius: 12px;
    overflow: hidden;
    aspect-ratio: 9/16;
}

.mvs-story-viewer img,
.mvs-story-viewer video {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.mvs-story-progress {
    display: flex;
    gap: 4px;
    padding: 8px 12px;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    z-index: 2;
}

.mvs-story-progress-bar {
    flex: 1;
    height: 3px;
    background: rgba(255, 255, 255, 0.4);
    border-radius: 2px;
    overflow: hidden;
}

.mvs-story-progress-fill {
    height: 100%;
    background: var(--mvs-bg);
    width: 0;
    transition: width linear;
}

.mvs-story-header {
    position: absolute;
    top: 24px;
    left: 12px;
    right: 12px;
    z-index: 2;
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--mvs-bg);
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}

.mvs-story-nav {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 50%;
    z-index: 1;
    cursor: pointer;
    background: none;
    border: none;
}

.mvs-story-nav--prev {
    left: 0;
}

.mvs-story-nav--next {
    right: 0;
}

/* ==========================================================================
   10. Stats Cards
   ========================================================================== */

.mvs-stats-cards {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 16px;
}

.mvs-stat-card {
    background: var(--mvs-bg);
    border-radius: 8px;
    padding: 20px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    text-align: center;
}

.mvs-stat-value {
    display: block;
    font-size: 2em;
    font-weight: 700;
    color: var(--mvs-primary-text, var(--mvs-primary));
    line-height: 1.2;
}

.mvs-stat-label {
    display: block;
    margin-top: 4px;
    font-size: 0.9em;
    color: var(--mvs-text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

/* ==========================================================================
   11. Lock Overlay (Paywall)
   ========================================================================== */

.mvs-lock-overlay {
    position: relative;
    border-radius: 8px;
    overflow: hidden;
}

.mvs-lock-preview {
    filter: blur(8px);
    pointer-events: none;
}

.mvs-lock-prompt {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    background: rgba(255, 255, 255, 0.95);
    padding: 32px 40px;
    border-radius: 12px;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
    z-index: 2;
    max-width: 360px;
    width: 90%;
}

.mvs-lock-icon {
    font-size: 36px;
    color: var(--mvs-primary-text, var(--mvs-primary));
    margin-bottom: 12px;
}

.mvs-lock-title {
    font-size: 1.2em;
    font-weight: 600;
    margin-bottom: 8px;
}

.mvs-lock-message {
    color: var(--mvs-text-secondary);
    font-size: 0.9em;
    margin-bottom: 16px;
    line-height: 1.5;
}

.mvs-lock-button {
    display: inline-block;
    padding: 10px 24px;
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
    border: none;
    border-radius: 6px;
    font-size: 1em;
    cursor: pointer;
    text-decoration: none;
    transition: background 0.2s;
}

.mvs-lock-button:hover {
    background: var(--mvs-primary-hover);
    color: var(--mvs-bg);
}

/* ==========================================================================
   12. Social Interactions Bar (for Step 47d)
   ========================================================================== */

.mvs-reactions {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}

.mvs-social-bar button {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    background: none;
    border: 1px solid var(--mvs-border);
    border-radius: 20px;
    cursor: pointer;
    font-size: 0.9em;
    color: var(--mvs-text-secondary);
    transition: all 0.2s;
}

.mvs-social-bar button:hover {
    background: none;
    border: 1px solid var(--mvs-primary);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-social-bar__actions {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
}

.mvs-social-bar button.active {
    background: var(--mvs-btn-primary-bg);
    border-color: var(--mvs-primary);
    color: var(--mvs-btn-primary-fg);
}

.mvs-social-bar .mvs-count {
    font-weight: 600;
}

.mvs-social-bar button.mvs-view-count {
    color: var(--mvs-text-muted);
    font-size: 0.85em;
}

.mvs-social-actions {
    display: flex;
    align-items: center;
    padding: 8px 24px 12px;
}

.mvs-social-actions-left {
    display: flex;
    align-items: center;
    gap: 8px;
}

.mvs-social-actions .mvs-view-count {
    flex: 1;
    text-align: center;
}

.mvs-social-actions-right {
    display: flex;
    align-items: center;
    gap: 6px;
}

.mvs-social-actions button {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    background: none;
    border: 1px solid var(--mvs-border);
    border-radius: 20px;
    cursor: pointer;
    font-size: 0.9em;
    color: var(--mvs-text-secondary);
    transition: all 0.2s;
}

.mvs-social-actions button:hover,
.mvs-social-actions button:focus {
    background: none;
    color: var(--mvs-primary-text, var(--mvs-primary));
    border: 1px solid var(--mvs-primary);
}

.mvs-social-actions button.active {
    background: var(--mvs-btn-primary-bg);
    border-color: var(--mvs-primary);
    color: var(--mvs-btn-primary-fg);
}

@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    /* Hide the view count on mobile to reclaim space — it's a passive metric. */
    .mvs-single-media .mvs-social-actions .mvs-view-count {
        display: none;
    }
}

/* ==========================================================================
   13. Comments Section (for Step 47d)
   ========================================================================== */

.mvs-comments-section {
    padding: 12px var(--mvs-space-4) var(--mvs-space-4);
    border-top: 1px solid var(--mvs-border-light);
}

.mvs-comments-title {
    font-size: 0.9375rem;
    font-weight: 600;
    margin: 0 0 12px;
    color: var(--mvs-text);
}

.mvs-comment-form {
    display: flex;
    gap: 8px;
    margin-bottom: 16px;
}

.mvs-comment-form textarea {
    flex: 1;
    padding: 10px 12px;
    background-color: var(--mvs-bg);
    border: 1px solid var(--mvs-border);
    border-radius: 8px;
    resize: none;
    min-height: 40px;
    font-family: inherit;
    font-size: 0.875rem;
}

.mvs-comment-form button {
    padding: 10px 16px;
    background: var(--mvs-primary);
    color: var(--mvs-btn-primary-fg, #fff);
    border: none;
    border-radius: 6px;
    cursor: pointer;
    font-size: 0.9em;
    align-self: flex-end;
    transition: background 0.2s;
}

.mvs-comment-form button:hover {
    background: var(--mvs-primary-hover);
}

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

.mvs-comment-item {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 0;
    border-bottom: 1px solid var(--mvs-border-light, #e5e7eb);
}

.mvs-comment-item:last-child {
    border-bottom: none;
}

.mvs-comment-avatar-link {
    flex-shrink: 0;
    display: block;
    line-height: 0;
    text-decoration: none;
}

.mvs-comment-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
}

.mvs-comment-body-wrap {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.mvs-comment-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    flex-wrap: wrap;
}

.mvs-comment-author-link {
    text-decoration: none;
    color: inherit;
}

.mvs-comment-author-link:hover .mvs-comment-author {
    text-decoration: underline;
}

.mvs-comment-author {
    font-weight: 600;
    font-size: 14px;
    color: var(--mvs-text, #111827);
}

.mvs-comment-date {
    font-size: 12px;
    color: var(--mvs-text-muted, #6b7280);
    white-space: nowrap;
    flex-shrink: 0;
}

.mvs-comment-text {
    font-size: 14px;
    line-height: 1.55;
    color: var(--mvs-text, #111827);
    word-wrap: break-word;
    overflow-wrap: anywhere;
}

.mvs-comment-actions {
    display: flex;
    gap: 6px;
    margin-top: 4px;
}

.mvs-comment-actions .mvs-btn--small {
    padding: 4px 10px;
    font-size: 12px;
}

/* ==========================================================================
   14. Dashboard
   ========================================================================== */

.mvs-dashboard {
    max-width: var(--mvs-content-width, 1200px);
    margin: 0 auto;
    padding: 20px;
}

.mvs-dashboard-header h1 {
    margin: 0 0 20px;
    font-size: 1.75em;
}

.mvs-dashboard-tabs {
    display: flex;
    gap: 0;
    border-bottom: 2px solid var(--mvs-border-light);
    margin-bottom: 24px;
}

.mvs-dashboard-tab {
    /* 12px padding on a 0.95em label lands at ~37px, under the 40px contract.
       The earlier tap-target sweep missed these because it measured buttons and
       .mvs-btn, and a tab is neither - it is an <a class="mvs-dashboard-tab">
       (QA re-verify, Basecamp 10252222115: 7 tabs measured 36.6-37.6px).
       min-height enforces the floor without changing the visual padding on
       surfaces where the label already pushes it past 40px. */
    min-height: var(--mvs-touch-min, 40px);
    display: inline-flex;
    align-items: center;
    padding: 12px 24px;
    background: none;
    border: none;
    border-radius: 0;
    border-bottom: 2px solid transparent;
    margin-bottom: -2px;
    cursor: pointer;
    font-size: 0.95em;
    font-weight: 600;
    color: var(--mvs-text);
    opacity: 0.75;
    transition: all 0.2s;
}

.mvs-dashboard-tab:hover,
.mvs-dashboard-tab:focus,
.mvs-dashboard-tab.active {
    opacity: 1;
}

.mvs-dashboard-tab:hover,
.mvs-dashboard-tab:focus {
    color: var(--mvs-primary-text, var(--mvs-primary));
    background: none;
    border: none;
    border-bottom: 2px solid var(--mvs-primary);
    border-radius: 0;
}

.mvs-dashboard-tab:first-child {
    padding-left: 0;
}

.mvs-dashboard-tab.active {
    /* Label is text -> AA-safe variant. The underline is not text, so it keeps
       the unmodified brand colour and stays visually true to the theme. */
    color: var(--mvs-primary-text, var(--mvs-primary));
    border-bottom-color: var(--mvs-primary);
}

/* Theme defense. Some themes (e.g. BuddyX) style bare <button> with a filled
   accent background + full border + radius, which turns these flat tabs into
   chunky colored buttons. Re-assert the intended look so the dashboard reads
   the same on any theme. !important is the standard pattern for a frontend
   plugin defending its own controls inside arbitrary themes. Bug #10019404777.

   IT WAS ALSO DEFENDING A DESIGN THAT NO LONGER EXISTS. Written for the
   horizontal strip, it pinned `border-bottom: 2px solid transparent`,
   `border-radius: 0` and `background: none` with !important — so when the
   sections became a vertical rail, every rule the rail wrote for its own
   appearance lost to armour the plugin had aimed at itself. That is why rail
   items rendered as red underlined links with no active fill and no
   inline-start marker: not the theme winning, the plugin's own past winning.
   Diagnosed by computed style, after `display: flex` from a rule applied while
   `border: 0` from the SAME rule did not.

   What is invariant across both designs stays here. What is per-design moves
   into the breakpoint that owns that design — because below 861px the strip IS
   horizontal, and there an underline is the right marker. */
.mvs-dashboard-tabs .mvs-dashboard-tab {
    background: none !important;
    border: 0 !important;
    box-shadow: none !important;
    /* The theme also forces button text white; without re-asserting the text
       color the label turns white-on-light = invisible once the fill is gone. */
    color: var(--mvs-text) !important;
}

/* Horizontal strip: the active marker is an underline. */
@media (max-width: 860px) {

    .mvs-dashboard-tabs .mvs-dashboard-tab {
        border-bottom: 2px solid transparent !important;
        border-radius: 0 !important;
    }

    .mvs-dashboard-tabs .mvs-dashboard-tab:hover,
    .mvs-dashboard-tabs .mvs-dashboard-tab:focus,
    .mvs-dashboard-tabs .mvs-dashboard-tab.active {
        border-bottom-color: var(--mvs-primary) !important;
        /* Text -> AA-safe variant. These theme-defense rules carry !important,
           so they are where the tab label's colour is actually decided; setting
           the token on the non-important rules alone left it at 4.36:1. */
        color: var(--mvs-primary-text, var(--mvs-primary)) !important;
    }
}

/* Rail: the active marker is the inline-start edge plus a surface fill. An
   underline down a rail reads as a rule under one row, which is what it looked
   like. Declared at the same weight as the armour above, or the armour wins
   again — the whole point of this block. */
/* ── Dashboard tab strip: the scrolling form ───────────────────────────────
 *
 * `max-width: 860px`, NOT 768. These rules lived inside the mobile block and
 * the vertical rail that replaces them starts at `min-width: 861px`, so 769px
 * to 860px was covered by NEITHER: the strip fell back to the base
 * `display: flex` with no wrap and no overflow, and its content — 912px wide
 * with the full tab set — simply spilled out of the container with no way to
 * scroll to it. Three tabs were unreachable across a 78px band of desktop
 * widths, and the page itself did not scroll, so nothing looked wrong.
 *
 * Found by walking every distinct breakpoint at desktop rather than by reading
 * the CSS, which is the only way this shows up: each rule is correct on its own
 * and the bug lives in the gap between them (Basecamp 10252222205).
 *
 * The two boundaries now meet exactly at 860/861. If either moves, move both.
 */
@media (max-width: 860px) {
    .mvs-dashboard-tabs {
        position: sticky;
        /* Below the theme's chrome, not under it. At `top: 0` this strip pinned
           at 0-70 while Reign's header held 0-81 at z-index 999 against our 50,
           so the whole strip was invisible and every tab hit-tested to the site
           logo — the same defect as the bulk bar, on the control that navigates
           between dashboard sections.
           --mvs-sticky-top is CHROME ONLY and deliberately excludes this strip;
           reading a variable this element also contributed to would walk it
           down the page a frame at a time. Basecamp 10320911387. */
        top: var(--mvs-sticky-top, 0px);
        z-index: var(--mvs-z-sticky);
        background: var(--mvs-bg);
        padding: 8px 0;
        border-bottom: 1px solid var(--mvs-border-light);
        /* Inline axis ONLY. `margin: 0 -20px` bled the sticky bar edge-to-edge
           and, being a shorthand, silently reset the base rule's
           `margin-bottom: 24px` — so the toolbar underneath sat flush against
           the tab bar and its focus ring overlapped the border (Basecamp
           10248974871). margin-inline also survives RTL, which the shorthand
           did not. */
        margin-inline: -20px;
        padding-left: 20px;
        padding-right: 20px;
        overflow-x: auto;
        white-space: nowrap;
        -webkit-overflow-scrolling: touch;
        scroll-snap-type: x proximity;
        scrollbar-width: none;
        -ms-overflow-style: none;
        /* Edge-fade so users see there are more tabs to swipe to. */
        -webkit-mask-image: linear-gradient(to right,
            black 0,
            black calc(100% - 24px),
            transparent 100%);
        mask-image: linear-gradient(to right,
            black 0,
            black calc(100% - 24px),
            transparent 100%);
    }

    .mvs-dashboard-tabs::-webkit-scrollbar { display: none; }

    .mvs-dashboard-tabs > .mvs-dashboard-tab {
        scroll-snap-align: start;
    }
}

@media (min-width: 861px) {

    .mvs-dashboard-tabs .mvs-dashboard-tab {
        /* No leading marker. The active row was carrying four signals at once -
           a 2px accent bar, a fill, an accent label and a heavier weight - and
           the bar had to square off its own leading corners to stop reading as
           a detached stick. The fill and the accent label say "active" on their
           own, so the bar and the corner hack both go. Every row loses the same
           2px, so nothing shifts between states. */
        border-radius: var(--mvs-radius-sm) !important;
        color: var(--mvs-text-secondary) !important;
    }

    /* Hover and active are washes derived from the FOREGROUND, not from
       --mvs-surface. That token resolved to rgb(35,38,47) on Reign dark - the
       exact colour of the rail behind it - so the fill these states are built
       on was not being painted at all. On the active row that left a lone
       floating marker with nothing to anchor it. currentColor is always
       visible on whatever surface a host supplies, and on the active row it
       is the accent, so the wash agrees with the marker.

       These carry !important because the non-important copies of the same
       rules further down are out-specified here; setting the fill there had
       no effect at all until it was set where the cascade ends. */
    .mvs-dashboard-tabs .mvs-dashboard-tab:hover,
    .mvs-dashboard-tabs .mvs-dashboard-tab:focus {
        background: rgba(128, 128, 128, 0.1) !important;
        background: color-mix(in srgb, currentColor 8%, transparent) !important;
        color: var(--mvs-text) !important;
    }

    .mvs-dashboard-tabs .mvs-dashboard-tab.active {
        background: rgba(128, 128, 128, 0.14) !important;
        background: color-mix(in srgb, currentColor 12%, transparent) !important;
        color: var(--mvs-primary-text, var(--mvs-primary)) !important;
    }

    /* A group with no heading still shows its break. */
    .mvs-dashboard-tabs .mvs-dashboard-tab--group-start {
        border-block-start: 1px solid var(--mvs-border-light) !important;
    }
}

/*
 * Every tab's content sits in the SAME wrapper.
 *
 * The panel used to be a bare `min-height` — transparent, no padding, no edge —
 * so each section's content sat directly on the page background and looked
 * unframed next to the rail and streak banner beside it, which are both carded.
 * Whether a screen looked finished depended on whether its content happened to
 * bring its own card: Media did (the upload zone), Albums, Collections,
 * Favorites, Documents and Edit profile did not.
 *
 * Matches the rail's treatment deliberately — same border, radius and surface
 * token — so the two columns read as one interface rather than two.
 *
 * Geometry was already uniform across desktop / iPad / mobile (identical x,
 * width and padding at 1440 / 820 / 390); what was missing was the frame.
 */
/* The panel is a LAYOUT REGION, not a card — it holds cards that draw their own
   border, and an outline here made every tab read as a box inside a box inside a
   box. Depth comes from the tinted ground + padding instead (border contract
   rule 1: a bordered box must not contain another bordered box). */
.mvs-dashboard-panel {
    min-height: 200px;
    padding: var(--mvs-space-4);
    /* Same card as the rail. The panel already carried the fill and the radius
       but no edge, so beside a bordered rail it read as unframed content rather
       than the other half of the layout - most visible on Profile and Documents,
       which have no toolbar of their own to give them a top edge. One border per
       column, and it lands on the shared panel class so every section gets it,
       including any a future release adds. */
    border: 1px solid var(--mvs-border-light);
    border-radius: var(--mvs-radius-md);
    background: var(--mvs-bg-subtle);
}


@media (max-width: 640px) {
    /* Tighter on a phone: a full-width card with desktop padding wastes the
       narrowest axis, which is the one that matters there. */
    .mvs-dashboard-panel {
        padding: var(--mvs-space-3);
    }
}

/* Interactivity API uses [hidden] attribute for show/hide */
.mvs-dashboard-panel[hidden] {
    display: none !important;
}

.mvs-dashboard-actions {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 20px;
}

/* .mvs-btn canonical definition lives in the "Shared button styles" block
 * further down (around line 4727). The legacy block that was here — with
 * different padding (10px 20px vs 0.625rem 1.25rem) — has been removed so
 * button dimensions are deterministic and don't drift based on stylesheet
 * load order. Danger + small modifiers remain here as they're only defined
 * in this place. */

.mvs-btn--danger {
    background: var(--mvs-danger);
    color: var(--mvs-on-danger, #ffffff);
    border: none;
}

.mvs-btn--danger:hover,
.mvs-btn--danger:focus,
.mvs-btn--danger:visited {
    background: var(--mvs-danger);
    color: var(--mvs-on-danger, #ffffff);
    filter: brightness(0.92);
    border: none;
}

.mvs-btn--small {
    padding: 6px 12px;
    font-size: 0.8em;
}

/* Dashboard Grid — default responsive auto-fill when no column-count class
   is applied; when the template emits `.mvs-cols-N` alongside it (as driven
   by the mvs_grid_columns setting), the compound-selector overrides below
   win by source order and honour the chosen column count. */
.mvs-dashboard-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--mvs-grid-gap, 16px);
}

.mvs-dashboard-grid.mvs-cols-2 { grid-template-columns: repeat(2, 1fr); }
.mvs-dashboard-grid.mvs-cols-3 { grid-template-columns: repeat(3, 1fr); }
.mvs-dashboard-grid.mvs-cols-4 { grid-template-columns: repeat(4, 1fr); }
.mvs-dashboard-grid.mvs-cols-5 { grid-template-columns: repeat(5, 1fr); }

/* Narrow-viewport collapse mirrors the .mvs-media-grid behaviour so the
   user never ends up with a 5-col grid squished on mobile. */
@media (max-width: 1024px) {
    .mvs-dashboard-grid.mvs-cols-5 { grid-template-columns: repeat(4, 1fr); }
}
@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    .mvs-dashboard-grid.mvs-cols-3,
    .mvs-dashboard-grid.mvs-cols-4,
    .mvs-dashboard-grid.mvs-cols-5 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 480px) {
    .mvs-dashboard-grid.mvs-cols-2,
    .mvs-dashboard-grid.mvs-cols-3,
    .mvs-dashboard-grid.mvs-cols-4,
    .mvs-dashboard-grid.mvs-cols-5 { grid-template-columns: 1fr; }
}

.mvs-dashboard-card {
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--mvs-bg);
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
    transition: box-shadow 0.2s;
}

.mvs-dashboard-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/* Bulk-select overlay checkbox on each card. Hidden until hover/selection so
   it stays out of the way, but always keyboard-reachable. */
.mvs-bulk-check {
    position: absolute;
    top: var(--mvs-space-2);
    inset-inline-start: var(--mvs-space-2);
    z-index: 3;
    /* 40px minimum tap target (Wbcom responsive contract). The check glyph
       stays 18px; the box carries the hit area. */
    width: 40px;
    height: 40px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 2px solid var(--mvs-on-primary);
    border-radius: var(--mvs-radius-sm);
    background: rgba(0, 0, 0, 0.45);
    color: transparent;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.15s, background 0.15s;
}

.mvs-dashboard-card:hover .mvs-bulk-check,
.mvs-bulk-check:focus-visible,
.mvs-bulk-check--on {
    opacity: 1;
}

.mvs-bulk-check--on {
    background: var(--mvs-primary);
    border-color: var(--mvs-primary);
    color: var(--mvs-on-primary);
}

.mvs-bulk-check svg {
    width: 18px;
    height: 18px;
}

.mvs-dashboard-card--selected {
    outline: 2px solid var(--mvs-primary);
    outline-offset: -2px;
}

/* Sticky action bar shown while ≥1 card is selected. */
.mvs-bulk-bar {
    position: sticky;
    /* Clears everything already pinned above it, then the usual gap. A flat
       var(--mvs-space-2) parked the bar at ~8px, which on any theme with a
       sticky header (Reign, the theme we ship with, pins 0-90 at 1440 and 0-81
       at 390) is UNDERNEATH it: the count, the album picker and the Add button
       were all covered, and elementFromPoint at each control returned the
       theme's header link. A member could not click the very controls the bar
       exists for.
       The --stack variable, not --top: at <=860px the dashboard tab strip is
       pinned between the theme header and this bar, and the bar has to clear
       BOTH. --mvs-sticky-stack is 0 when nothing is pinned, so a theme without
       a sticky header keeps the old offset exactly. Basecamp 10320911387. */
    top: calc(var(--mvs-sticky-stack, 0px) + var(--mvs-space-2));
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--mvs-space-2);
    margin-block-end: var(--mvs-space-3);
    padding: var(--mvs-space-2) var(--mvs-space-3);
    background: var(--mvs-bg);
    border: 1px solid var(--mvs-border-light);
    border-radius: var(--mvs-radius-md);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}

.mvs-bulk-count {
    font-weight: 600;
    color: var(--mvs-text);
    margin-inline-end: auto;
}

.mvs-bulk-privacy {
    /* Was a 160px cap: every privacy label is wider (174-272px), so even the
       default "Public: anyone can see" was clipped. Size to the label, never
       wider than the bar. Basecamp 10313754840. */
    width: auto;
    max-width: 100%;
}

@media (max-width: 480px) {
    .mvs-bulk-bar {
        flex-direction: column;
        align-items: stretch;
    }

    .mvs-bulk-count {
        margin-inline-end: 0;
    }

    .mvs-bulk-privacy {
        /* Full row on a phone. width:auto would size it to its (long) label
           and push the arrow past the bar's edge. */
        width: 100%;
    }
}

.mvs-dashboard-card-thumb {
    position: relative;
    display: block;
    aspect-ratio: 1;
    background: var(--mvs-surface-2);
    overflow: hidden;
    text-decoration: none;
    color: inherit;
}

.mvs-dashboard-card-thumb img:not([hidden]),
.mvs-dashboard-card-thumb video:not([hidden]) {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.mvs-dashboard-card-thumb img[hidden],
.mvs-dashboard-card-thumb video[hidden] {
    display: none !important;
}

.mvs-dashboard-card-body {
    padding: 12px;
    flex: 1;
    display: flex;
    flex-direction: column;
}

.mvs-dashboard-card-title {
    display: block;
    font-weight: 600;
    font-size: 0.9em;
    margin: 0 0 6px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    color: var(--mvs-text);
    text-decoration: none;
}

a.mvs-dashboard-card-title:hover {
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-dashboard-card-meta {
    font-size: 0.8em;
    color: var(--mvs-text-muted);
    margin-bottom: 8px;
}

.mvs-dashboard-card-actions {
    display: flex;
    gap: 6px;
    margin-top: auto;
    padding-top: 8px;
}

.mvs-privacy-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 0.7em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.mvs-privacy-badge--public {
    background: var(--mvs-success-bg);
    color: var(--mvs-success);
}

.mvs-privacy-badge--members {
    background: var(--mvs-warning-bg);
    color: var(--mvs-warning);
}

.mvs-privacy-badge--private {
    background: var(--mvs-danger-bg);
    color: var(--mvs-danger);
}

/* Load More */
.mvs-load-more-wrap {
    text-align: center;
    margin-top: 24px;
}

/* ==========================================================================
   Panel toolbar — the one shape above every list or grid.
   Emitted by TemplateHelpers::render_panel_toolbar(). Used by the four
   dashboard panels (client-driven) and the document drive (GET form), so the
   two rendering engines look like one product.
   ========================================================================== */

.mvs-panel-toolbar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--mvs-space-2);
    margin-bottom: var(--mvs-space-4);
}

.mvs-panel-toolbar__search {
    /* Takes the slack so the selects keep their natural width; the floor stops
       it collapsing to nothing once several filters are present. */
    flex: 1 1 12rem;
    min-width: 0;
    min-height: var(--mvs-touch-min);
    padding: 0 var(--mvs-space-3);
    border: 1px solid var(--mvs-border);
    border-radius: var(--mvs-radius-md);
    /* !important fights BuddyX's bx-tokens-applied.css, whose form rule is
       `input:not([type="submit"]):not([type="button"]):not([type="checkbox"])
       :not([type="radio"]):not([type="range"])` — specificity (0,5,1), which no
       sane class selector beats. Without this the search box stayed white on a
       dark dashboard while the select beside it went dark, because the theme's
       grouped selector only carries that weight on its `input` branch.
       Colour only: layout above is left to lose normally. */
    background: var(--mvs-surface) !important;
    color: var(--mvs-text) !important;
    font-size: var(--mvs-font-md);
}

.mvs-panel-toolbar__select {
    min-height: var(--mvs-touch-min);
    /* Width is ours to decide, not the theme's. Left unconstrained, a theme
       that styles `select { width: 100% }` — Astra does — stretched these to
       the full content column (1160px each) and stacked them, while on Reign
       they sat at ~110px. Anything we leave to the theme is only correct by
       luck on the themes we happen to test. */
    width: auto;
    min-width: 9rem;
    max-width: 100%;
    padding: 0 var(--mvs-space-3);
    border: 1px solid var(--mvs-border);
    border-radius: var(--mvs-radius-md);
    background: var(--mvs-surface);
    color: var(--mvs-text);
    font-size: var(--mvs-font-sm);
}

.mvs-panel-toolbar__count {
    color: var(--mvs-text-muted);
    font-size: var(--mvs-font-sm);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.mvs-panel-toolbar__search:focus-visible,
.mvs-panel-toolbar__select:focus-visible {
    outline: 2px solid var(--mvs-primary);
    outline-offset: 2px;
}

@media (max-width: 640px) {

    .mvs-panel-toolbar__search {
        /* The search owns the first row on a phone; the selects wrap beneath it
           rather than each being squeezed to a few characters wide. */
        flex-basis: 100%;
    }

    .mvs-panel-toolbar__select {
        flex: 1 1 auto;
    }

}

/* Dashboard panel loading state.
   Every panel's `loading` flag was already being SET by the store and read by
   nothing, so a slow first request rendered an entirely blank panel: the grid
   was empty and the empty state was deliberately suppressed. This is the other
   half of that pair, and it is the same markup in all four panels so they say
   "loading" the same way. */
.mvs-dashboard-loading {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--mvs-space-2);
    padding: var(--mvs-space-5) var(--mvs-space-3);
    color: var(--mvs-text-muted);
    font-size: var(--mvs-font-sm);
}

.mvs-dashboard-loading__spinner {
    width: 18px;
    height: 18px;
    border: 2px solid var(--mvs-border);
    /* One side inherits the accent so the ring reads as spinning. */
    border-top-color: var(--mvs-primary);
    border-radius: 50%;
    animation: mvs-dashboard-spin 0.7s linear infinite;
}

@keyframes mvs-dashboard-spin {
    to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {

    .mvs-dashboard-loading__spinner {
        /* The label still says "Loading…", so removing the spin costs nothing. */
        animation: none;
    }

}

/* ==========================================================================
   15. Modal
   ========================================================================== */

.mvs-modal-overlay {
    position: fixed;
    inset: 0;
    background: var(--mvs-overlay);
    z-index: var(--mvs-z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    /* Allow taps outside the panel to dismiss without scrolling the body. */
    overscroll-behavior: contain;
}

.mvs-modal {
    background: var(--mvs-bg);
    border-radius: var(--mvs-radius-lg);
    width: 100%;
    max-width: 560px;
    max-height: 85vh;
    overflow-y: auto;
    box-shadow: var(--mvs-shadow-lg);
    position: relative;
}

/* §11 — Bottom-sheet on mobile.
   Modal slides up from the bottom edge with a drag handle, max 90vh,
   safe-area-inset honored for iOS home indicator. Animation respects
   prefers-reduced-motion. */
@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    .mvs-modal-overlay {
        align-items: flex-end;
        padding: 0;
    }

    .mvs-modal {
        max-width: 100%;
        max-height: 90vh;
        border-radius: var(--mvs-radius-lg) var(--mvs-radius-lg) 0 0;
        padding-bottom: env(safe-area-inset-bottom, 0);
        animation: mvs-modal-slide-up var(--mvs-duration-base) var(--mvs-easing) both;
    }

    /* Drag handle pill — pure CSS, no JS dismiss yet but gives the affordance. */
    .mvs-modal::before {
        content: "";
        display: block;
        width: 36px;
        height: 4px;
        background: var(--mvs-text-muted);
        opacity: 0.5;
        border-radius: var(--mvs-radius-pill);
        margin: 8px auto 0;
        flex-shrink: 0;
    }
}

@keyframes mvs-modal-slide-up {
    from {
        transform: translateY(100%);
    }
    to {
        transform: translateY(0);
    }
}

@media (prefers-reduced-motion: reduce) {
    .mvs-modal {
        animation: none !important;
    }
}

.mvs-modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px 24px;
    border-bottom: 1px solid var(--mvs-border-light);
}

.mvs-modal-header h2 {
    margin: 0;
    font-size: 1.2em;
}

.mvs-modal-close {
    background: none;
    border: none;
    font-size: 1.5em;
    cursor: pointer;
    color: var(--mvs-text-muted);
    padding: 0;
    line-height: 1;
}

.mvs-modal-close:hover {
    background: none;
    border: none;
    color: var(--mvs-text);
}

.mvs-modal-body {
    padding: 24px;
}

.mvs-modal-footer {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    padding: 16px 24px;
    border-top: 1px solid var(--mvs-border-light);
}

/* Form Fields */
.mvs-field {
    margin-bottom: 16px;
}

.mvs-field label {
    display: block;
    margin-bottom: 4px;
    font-weight: 600;
    font-size: 0.9em;
    color: var(--mvs-text);
}

.mvs-field input[type="text"],
.mvs-field textarea,
.mvs-field select {
    width: 100%;
    padding: 8px 12px;
    background-color: var(--mvs-bg);
    border: 1px solid var(--mvs-border);
    border-radius: 6px;
    font-size: 0.9em;
    font-family: inherit;
    box-sizing: border-box;
}

.mvs-field textarea {
    resize: vertical;
    min-height: 80px;
}

/* Same clipping as .mvs-dashboard-upload-row: 200px leaves ~156px of text
   room, under the 207px/239px the longest privacy labels need. The modal is
   560px wide, so 100% is safe. Basecamp 10309750901. */
.mvs-field select {
    max-width: 100%;
}

/* ==========================================================================
   16. Tag Input
   ========================================================================== */

.mvs-tag-input-wrap {
    position: relative;
}

.mvs-tag-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 6px;
    border: 1px solid var(--mvs-border-light);
    border-radius: 6px;
    min-height: 38px;
    align-items: center;
    cursor: text;
}

.mvs-tag-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
    border-radius: 12px;
    font-size: 0.8em;
}

.mvs-tag-pill-remove {
    background: none;
    border: none;
    color: var(--mvs-bg);
    cursor: pointer;
    font-size: 1em;
    padding: 0;
    line-height: 1;
    opacity: 0.8;
}

.mvs-tag-pill-remove:hover {
    opacity: 1;
}

.mvs-tag-text-input {
    border: none !important;
    outline: none;
    padding: 2px 4px !important;
    font-size: 0.9em;
    min-width: 80px;
    flex: 1;
}

.mvs-tag-text-input:focus-visible {
    outline: 2px solid var(--mvs-primary, #0073aa);
    outline-offset: -2px;
}

.mvs-tag-text-input:focus-visible {
    outline: 2px solid var(--mvs-primary);
    outline-offset: -2px;
    border-radius: 2px;
}

.mvs-tag-autocomplete {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    background: var(--mvs-bg);
    border: 1px solid var(--mvs-border-light);
    border-top: none;
    border-radius: 0 0 6px 6px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    z-index: 10;
    max-height: 160px;
    overflow-y: auto;
}

.mvs-tag-autocomplete-item {
    padding: 8px 12px;
    cursor: pointer;
    font-size: 0.9em;
}

.mvs-tag-autocomplete-item:hover {
    background: var(--mvs-info-bg);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

/* ==========================================================================
   17. Confirm Dialog
   ========================================================================== */

.mvs-confirm-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 100001;
    display: flex;
    align-items: center;
    justify-content: center;
}

.mvs-confirm {
    background: var(--mvs-bg);
    border-radius: 12px;
    padding: 28px;
    max-width: 380px;
    width: 90%;
    text-align: center;
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
}

.mvs-confirm p {
    margin: 0 0 20px;
    font-size: 1em;
    color: var(--mvs-text);
}

.mvs-confirm-actions {
    display: flex;
    justify-content: center;
    gap: 8px;
}

.mvs-report-reason-select {
    display: block;
    width: 100%;
    padding: 8px 12px;
    box-sizing: border-box;
    margin: 12px 0;
    font-size: 14px;
    border: 1px solid var(--mvs-border);
    border-radius: 6px;
    background-color: var(--mvs-surface-2);
    color: var(--mvs-text);
}

/* ==========================================================================
   18. Toast Notification - owned by shared-ui-frame.css, which styles the
   toast element that shared-ui-frame.php renders.
   ========================================================================== */

/* ==========================================================================
   Interactivity API [hidden] overrides
   Elements with display:flex/fixed need explicit [hidden] rules because
   data-wp-bind--hidden toggles the HTML hidden attribute.
   ========================================================================== */

/* Any MediaVerse element the Interactivity API hides with the HTML `hidden`
   attribute stays hidden, whatever display value its own rule sets.

   This replaces a hand-maintained allowlist of ten selectors. The allowlist
   missed `.mvs-bulk-bar` and `.mvs-dashboard-loading`, so on any theme that
   does not ship a `[hidden]` reset — Astra, for one — members saw a bulk
   Delete bar reading "0 selected" and a permanent "Loading…" under a finished
   grid. It looked correct on Reign only by luck: Reign ships
   `[hidden]{display:none}` and enqueues after us, and at equal specificity
   source order decided it. An enumerated list guarantees the next control
   added is missed the same way.

   Matched on our own namespace, so no theme element is touched. */
[class*="mvs-"][hidden],
[class*="mvs-"] [hidden] {
    display: none !important;
}

/* ==========================================================================
   19. Media Picker (for album creation)
   ========================================================================== */

.mvs-media-picker {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
    gap: 8px;
    max-height: 300px;
    overflow-y: auto;
    padding: 8px;
    border: 1px solid var(--mvs-border-light);
    border-radius: 6px;
    margin-top: 8px;
}

.mvs-media-picker-item {
    position: relative;
    aspect-ratio: 1;
    border-radius: 4px;
    overflow: hidden;
    cursor: pointer;
    border: 2px solid transparent;
    transition: border-color 0.2s;
}

.mvs-media-picker-item.selected {
    border-color: var(--mvs-primary);
}

/* The picker reuses the grid's MARKUP classes, but the grid's sizing is scoped
   to `.mvs-grid-item`, so inside a picker cell a video/audio tile matched no
   sizing rule at all and rendered ragged next to the images. Sized here for
   every child the picker can hold, not just `img` (Basecamp 10190678139). */
.mvs-media-picker-item img,
.mvs-media-picker-item video,
.mvs-media-picker-item .mvs-grid-video-preview,
.mvs-media-picker-item .mvs-grid-item-placeholder {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

/* `.mvs-grid-item-placeholder--video` pins `aspect-ratio: 16 / 9`, which cannot
   fill a square cell that clips its overflow. Inside the picker the cell decides
   the shape. */
.mvs-media-picker-item .mvs-grid-item-placeholder--video,
.mvs-media-picker-item .mvs-grid-item-placeholder--audio {
    aspect-ratio: auto;
}

.mvs-media-picker-check {
    position: absolute;
    top: 4px;
    right: 4px;
    width: 22px;
    height: 22px;
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
    border-radius: 50%;
    display: none;
    align-items: center;
    justify-content: center;
    font-size: 0.75em;
    font-weight: 700;
}

.mvs-media-picker-item.selected .mvs-media-picker-check {
    display: flex;
}

/* ==========================================================================
   20. Owner Actions (on single pages)
   ========================================================================== */

.mvs-owner-actions {
    display: inline-flex;
    gap: 6px;
}

.mvs-collection-card-info .mvs-owner-actions {
    display: flex;
    margin-left: 0;
    padding: 12px 0 0;
}

/* Inline edit form on single pages */
.mvs-inline-edit {
    padding: 16px 24px;
    border-top: 1px solid var(--mvs-border-light);
    background: var(--mvs-surface-2);
}

.mvs-inline-edit .mvs-field {
    margin-bottom: 12px;
}

/* Two-column row inside the inline-edit form — used for paired controls
   (Privacy + slug-regenerate checkbox) so they share one row instead
   of stacking. Collapses to single column at ≤640px. */
.mvs-inline-edit .mvs-field-row {
    display: flex;
    gap: 12px;
    margin-bottom: 12px;
    align-items: flex-end;
}

.mvs-inline-edit .mvs-field-row .mvs-field {
    margin-bottom: 0;
}

.mvs-inline-edit .mvs-field--inline {
    flex: 1 1 0;
    min-width: 0;
}

.mvs-inline-edit .mvs-field-row .mvs-field--checkbox {
    flex: 0 0 auto;
    padding-bottom: 10px;
}

.mvs-inline-edit .mvs-field--checkbox label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-weight: 500;
    cursor: pointer;
    font-size: 13px;
    margin: 0;
    white-space: nowrap;
}

.mvs-inline-edit .mvs-field--checkbox input[type="checkbox"] {
    width: auto;
    margin: 0;
    flex-shrink: 0;
}

@media (max-width: 640px) {
    .mvs-inline-edit .mvs-field-row {
        flex-direction: column;
        align-items: stretch;
    }

    .mvs-inline-edit .mvs-field-row .mvs-field--checkbox {
        padding-bottom: 0;
    }
}

.mvs-inline-edit-actions {
    display: flex;
    gap: 8px;
    margin-top: 12px;
}

/* ==========================================================================
   21. Explore Search + Tag Cloud
   ========================================================================== */

.mvs-explore-search {
    margin-bottom: 24px;
}

.mvs-tag-cloud {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 24px;
}

/* On a phone the cloud wraps to as many rows as there are tags — a site with
   22 tags pushed the first media 680px down the page, so Explore opened on a
   screenful of filters with no media in sight. One horizontally scrollable row
   instead: 312px of chips becomes ~56px and the grid is visible immediately.
   Same pattern Pro already uses for .mvs-competition-tabs. */
@media (max-width: 640px) {
    .mvs-tag-cloud {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        padding-bottom: 4px;
    }

    .mvs-tag-cloud::-webkit-scrollbar {
        display: none;
    }

    /* Chips must not shrink to fit, or the labels compress instead of scrolling. */
    .mvs-tag-cloud-item {
        flex: 0 0 auto;
    }
}

.mvs-tag-cloud-item {
    display: inline-block;
    padding: 4px 14px;
    background: var(--mvs-surface-2);
    border-radius: 16px;
    font-size: 0.85em;
    color: var(--mvs-text);
    text-decoration: none;
    transition: background 0.2s, color 0.2s;
}

/*
 * Hover/focus on a filter chip. Background and colour ONLY — never a property
 * that changes the box.
 *
 * This selector previously had no declaration block of its own: an edit removed
 * the body and left `.mvs-tag-cloud-item:hover,` dangling above a comment, so
 * the parser joined it to the `.mvs-sr-only` rule below and hovering any chip
 * applied `position:absolute; width:1px; height:1px; clip:rect(0,0,0,0)`. The
 * chip visually vanished, the row reflowed, the pointer left the chip, it came
 * back — the "tabs overlap and the tags jitter" report (Basecamp 10259608476).
 * Keep a real block here; a bodiless selector silently adopts the next rule.
 */
.mvs-tag-cloud-item:hover,
.mvs-tag-cloud-item:focus-visible {
    background: rgba(var(--mvs-primary-rgb, 0, 115, 170), 0.08);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

/*
 * Visually hidden, still announced. Ours on purpose: `.screen-reader-text` is
 * WordPress core's class and its presence depends on the THEME defining it. A
 * label that becomes visible on a theme that does not would be worse than the
 * placeholder-only field it replaced — and "the theme will have it" is the
 * assumption this audit kept finding to be wrong.
 */
.mvs-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;
}

/*
 * The foreground must move WITH the background. This set `--mvs-text` — the
 * body text colour — on top of the primary fill, so the selected chip scored
 * 2.05 in light mode and 1.13 in dark, i.e. the one filter a member has chosen
 * was the least readable thing on the page (Basecamp 10252222011).
 *
 * `--mvs-on-primary` is the token that exists for exactly this: whatever reads
 * correctly on the accent, resolved from the theme. Same pairing the attention
 * badge uses. Renders on Explore, /media-tag/ archives and member profiles, so
 * this one rule covers three surfaces.
 */
.mvs-tag-cloud-item.active {
    background: var(--mvs-primary);
    color: var(--mvs-on-primary, #fff);
}

/* ==========================================================================
   22. Upload Form Enhancement
   ========================================================================== */

.mvs-upload-fields {
    margin-top: 16px;
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.mvs-upload-fields input[type="text"],
.mvs-upload-fields textarea {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid var(--mvs-border);
    border-radius: 6px;
    font-size: 0.9em;
    font-family: inherit;
    box-sizing: border-box;
}

.mvs-upload-fields textarea {
    resize: vertical;
    min-height: 60px;
}

/* ==========================================================================
   23. Dashboard Inline Upload
   ========================================================================== */

.mvs-dashboard-upload {
    margin-bottom: 24px;
    background: var(--mvs-bg, #fff);
    border: 1px solid var(--mvs-border-light, #dcdcde);
    border-radius: 12px;
    padding: 20px;
}

.mvs-dashboard-dropzone {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    margin: 0 0 1rem;
    padding: 28px 20px;
    border: 2px dashed var(--mvs-border);
    border-radius: 8px;
    cursor: pointer;
    transition: border-color 0.2s, background 0.2s;
    background: var(--mvs-surface-2);
}

.mvs-dashboard-dropzone:hover {
    border-color: var(--mvs-primary);
    background: var(--mvs-info-bg);
}

.mvs-dashboard-dropzone.mvs-drag-active {
    border-color: var(--mvs-primary);
    background: var(--mvs-info-bg);
    border-style: solid;
}

.mvs-dashboard-dropzone-icon {
    font-size: 1.5em;
    line-height: 1;
}

.mvs-dashboard-dropzone-label {
    font-size: 0.95em;
    color: var(--mvs-text-secondary);
}

.mvs-dashboard-upload-fields {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin-top: 12px;
    padding: 12px;
    background: var(--mvs-surface-2);
    border-radius: 6px;
}

.mvs-dashboard-upload-fields input[type="text"],
.mvs-dashboard-upload-fields textarea,
.mvs-dashboard-upload-fields select {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid var(--mvs-border);
    border-radius: 6px;
    font-size: 0.9em;
    font-family: inherit;
    box-sizing: border-box;
}

.mvs-dashboard-upload-fields textarea {
    resize: vertical;
    min-height: 60px;
}

.mvs-dashboard-upload-row {
    display: flex;
    gap: 10px;
}

.mvs-dashboard-upload-row input {
    flex: 1;
}

/* Privacy labels are sentences ("Only me: hidden from everyone else" measures
   239px). A closed <select> clips rather than wraps or ellipsizes, so any fixed
   cap truncates the selected option -- size to content instead, bounded by the
   row. Basecamp 10313754840. */
.mvs-dashboard-upload-row select {
    width: auto;
    max-width: 100%;
}

.mvs-dashboard-upload-status {
    margin-top: 12px;
    font-size: 0.9em;
    color: var(--mvs-primary-text, var(--mvs-primary));
    font-weight: 600;
}

.mvs-dashboard-upload-status:empty {
    display: none;
}

/* Staged-upload review step: selecting files reveals this instead of
   uploading immediately, so metadata can be filled first. */
.mvs-dashboard-upload-review {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-top: 12px;
    padding: 10px 12px;
    /* Sits INSIDE the bordered upload card — the tinted ground already separates
       it, so an outline here would be the third edge in a row (border contract
       rule 1). */
    background: var(--mvs-bg-subtle, #f6f7f8);
    border-radius: 6px;
}

/* Respect the HTML [hidden] attribute set by data-wp-bind--hidden. The
   display:flex above is a class rule, so it outranks the UA stylesheet's
   [hidden]{display:none} and the review bar stayed on screen with zero files
   staged — the "Upload 0 file(s)" button was visible AND clickable. Same
   guard as .mvs-dashboard-panel[hidden] and the messaging panels. */
.mvs-dashboard-upload-review[hidden] {
    display: none;
}

.mvs-dashboard-upload-review-label {
    font-size: 0.9em;
    color: var(--mvs-text-muted, #555);
}

.mvs-dashboard-upload-review-actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;
}

@media (max-width: 640px) {
    .mvs-dashboard-upload-review {
        flex-direction: column;
        align-items: stretch;
    }

    .mvs-dashboard-upload-review-actions {
        justify-content: flex-end;
    }
}

@media (max-width: 600px) {
    .mvs-dashboard-dropzone {
        flex-direction: column;
        padding: 20px;
    }

    .mvs-dashboard-upload-row {
        flex-direction: column;
    }

    .mvs-dashboard-upload-row select {
        max-width: 100%;
    }
}

/* ==========================================================================
   15. Empty State (shared)
   ========================================================================== */

.mvs-empty-state {
    text-align: center;
    padding: 48px 24px;
    color: var(--mvs-text-secondary);
}

.mvs-empty-state .mvs-icon [data-lucide],
.mvs-empty-state .mvs-icon svg {
    width: 48px;
    height: 48px;
    color: var(--mvs-border-light);
    display: block;
    margin: 0 auto 12px;
}

.mvs-empty-state p {
    font-size: 1em;
    margin: 8px 0 0;
    color: var(--mvs-text-muted);
}

/* Section 16 "BuddyPress Profile Media Tab" — removed. Rules were
 * unscoped duplicates of scoped versions already in bp-integration.css:
 *   .mvs-bp-profile-actions     → bp-integration.css section 'Action /
 *                                  toolbar row'
 *   .mvs-bp-dropzone +           → bp-integration.css section 'Upload
 *     .mvs-bp-dropzone-text        surface'
 *   .mvs-bp-upload-status +      → bp-integration.css section 'Upload
 *     --success                    surface'
 *   .mvs-bp-upload-wrap          → see dedicated rules below (this file
 *                                  keeps its copy because
 *                                  templates/album.php reuses the class
 *                                  in a non-BP context).
 */

/* Section 24 "BuddyPress Activity Media" — all rules moved to
 * bp-integration.css (see "Activity stream — media images" and
 * "Activity stream — multi-image grid" sections). The broad
 * `img:not(.avatar):not(.emoji)` catch-all was also narrowed to match
 * only /media/ permalinks during the move, so theme-injected classless
 * images (Reign's .bp-group-preview-cover, etc.) no longer get
 * constrained by our rules. Section header retained as placeholder for
 * the pending sequential renumbering in a later commit. */

/* Activity form controls (attach-media button, preview grid, privacy
 * select, upload state) moved to bp-integration.css. See that file's
 * "Activity form — attach-media button, preview grid, privacy select"
 * section. Only the shared @keyframes stays here since it's referenced
 * by multiple non-BP spinners too. */
@keyframes mvs-spin {
    to {
        transform: rotate(360deg);
    }
}

/* ── 25. Activity Media Lightbox ── (moved to frontend.css Lightbox
 *         social sidebar section; see bottom of file).
 *
 * Sections 26–28 (Inline Album Creation / Inline Upload / Single Album
 * View) moved to bp-integration.css for the BP-only rules
 * (.mvs-bp-album-form, .mvs-bp-album-msg, .mvs-bp-back-link,
 * .mvs-btn-primary, .mvs-btn-secondary).
 *
 * The .mvs-bp-upload-* classes remain here, unscoped, because
 * templates/album.php (non-BP single-album view) reuses the same
 * markup for its "Add Media" inline upload panel. Renaming to
 * .mvs-inline-upload-* would be cleaner but is deferred to avoid
 * touching PHP/JS in this CSS-only migration.
 */

.mvs-bp-upload-wrap {
    margin: 16px 0;
    padding: 20px;
    background: var(--mvs-bg);
    border: 1px solid var(--mvs-border-light);
    border-radius: 8px;
}

/* The dropzone itself. Every sibling above was already unscoped here for
 * templates/album.php; the dropzone was the one that was left behind and
 * only existed under `#buddypress .mvs-bp-screen` in bp-integration.css.
 * On the single-album page that meant no border, no padding, no
 * min-height -- a bare line of text instead of a box, so users aimed at
 * a target that wasn't drawn and dropped on the page background (which
 * navigates the tab to the file). Basecamp #10134243697, cause B. */
.mvs-bp-dropzone {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 20px 24px;
    border: 2px dashed var(--mvs-border);
    border-radius: 8px;
    background: var(--mvs-surface-2);
    text-align: center;
    cursor: pointer;
    transition: border-color 0.2s, background 0.2s;
}

.mvs-bp-dropzone.is-dragover {
    border-color: var(--mvs-primary);
    background: color-mix(in srgb, var(--mvs-primary) 5%, var(--mvs-surface-2));
}

.mvs-bp-dropzone-text {
    margin: 0;
    color: var(--mvs-text-secondary);
    font-size: 14px;
}

/* Cloud-upload icon anchors the dropzone, so scale it up from the
 * 16px body-button default. */
.mvs-bp-dropzone svg,
.mvs-bp-dropzone [data-lucide] {
    width: 24px;
    height: 24px;
    stroke-width: 1.75;
    color: var(--mvs-text-secondary);
    flex-shrink: 0;
}

/* Mirrors the BP-scoped mobile override in bp-integration.css so the
 * non-BP single-album dropzone gets the same treatment. Mobile is part of
 * the definition of done, not a follow-up. */
@media (max-width: 640px) {
    .mvs-bp-dropzone {
        padding: 16px;
    }
}

.mvs-bp-upload-preview {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 12px;
}

.mvs-bp-upload-thumb {
    width: 80px;
    text-align: center;
}

.mvs-bp-upload-thumb img {
    width: 80px;
    height: 80px;
    object-fit: cover;
    border-radius: 6px;
    border: 1px solid var(--mvs-border-light);
}

.mvs-bp-upload-thumb-name {
    display: block;
    margin-top: 4px;
    font-size: 11px;
    color: var(--mvs-text-secondary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mvs-bp-upload-form-actions {
    margin-top: 12px;
}

.mvs-bp-upload-status {
    margin-top: 10px;
    padding: 8px 12px;
    border-radius: 6px;
    font-size: 13px;
    background: var(--mvs-info-bg);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-bp-upload-status--success {
    background: var(--mvs-success-bg);
    color: var(--mvs-success);
}

.mvs-bp-upload-status--error {
    background: var(--mvs-danger-bg);
    color: var(--mvs-danger-text);
}

/* Used only by templates/album.php (single-album view, Add Media panel). */
.mvs-album-upload-section {
    margin: 16px 0;
}

/* Section 26 "Group Media Sub-Tabs" — removed. Unscoped duplicates of
 * scoped versions already in bp-integration.css section 'Sub-tab row
 * (Media / Albums)':
 *   .mvs-bp-sub-tabs, .mvs-bp-sub-tab, .mvs-bp-sub-tab:hover,
 *   .mvs-bp-sub-tab.active
 *
 * Section 27 "Activity Media Thumbnails (Video/Audio)" — moved to
 * bp-integration.css section 'Activity stream — video thumbnails,
 * placeholders, audio cards', scoped under #buddypress. All
 * .mvs-activity-media--video / --audio / --placeholder rules plus the
 * play icon, vid-link, audio icon/info/title/meta supporting classes.
 */

/* ==========================================================================
   28. Enhanced Single Media Player
   ========================================================================== */

/* `.mvs-media-video video` rule lives in section "Media Content" — single
 * source of truth so width/height/object-fit can't drift. Border-radius
 * applies to the wrapper so it clips the letterbox bars too. */
.mvs-media-video {
    border-radius: 4px;
    overflow: hidden;
}

.mvs-audio-info {
    padding: 12px 24px 0;
    display: flex;
    gap: 8px;
    align-items: center;
    font-size: 0.9em;
    color: var(--mvs-text-secondary);
}

.mvs-audio-artist {
    font-weight: 600;
    color: var(--mvs-text);
}

.mvs-audio-album::before {
    content: "\00b7";
    margin: 0 4px;
}

.mvs-media-audio audio {
    display: block;
    width: 100%;
    margin-top: 8px;
}

/* ==========================================================================
   29. Album Grid Items (Video/Audio)
   ========================================================================== */

.mvs-grid-item {
    position: relative;
}

/* Direct child only: the media permalink. A descendant selector here also hit
   .mvs-grid-item-author-link, overriding its display:flex (specificity 0,1,1
   vs 0,1,0) so the avatar and name stacked and the name never ellipsized.
   Same trap as the two cases documented above. Basecamp 10312738180. */
.mvs-grid-item > a {
    display: block;
    position: relative;
    text-decoration: none;
    color: inherit;
}

.mvs-grid-play-icon {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 48px;
    height: 48px;
    background: rgba(0, 0, 0, 0.55);
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    z-index: 2;
    pointer-events: none;
    transition: background 0.2s, transform 0.2s;
}

.mvs-grid-play-icon svg {
    width: 22px;
    height: 22px;
    /* Optical centering — the play triangle isn't centered in its bounding box. */
    margin-left: 2px;
}

.mvs-grid-item a:hover .mvs-grid-play-icon {
    background: rgba(0, 0, 0, 0.8);
    transform: translate(-50%, -50%) scale(1.05);
}

/* Unified placeholder frame — audio + video + generic all share aspect ratio
   and centering so grids never collapse based on media type. Specific
   gradients layered via the --video / --audio / --generic modifiers. */
.mvs-grid-item-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    aspect-ratio: 1;
    position: relative;
    color: rgba(255, 255, 255, 0.9);
    background: linear-gradient(135deg, #1a2332, #0a0a0a);
}

.mvs-grid-item-placeholder svg {
    width: 34%;
    height: 34%;
    max-width: 72px;
    max-height: 72px;
    opacity: 0.9;
}

.mvs-grid-item-placeholder--video {
    background: linear-gradient(135deg, #0d2137, #0a0a0a);
    aspect-ratio: 16 / 9;
}

.mvs-grid-item-placeholder--audio {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: #fff;
}

/* A favorited DOCUMENT has no thumbnail and is not an image, so every other
   branch on the card thumb was false and the tile rendered as a blank
   rectangle (Basecamp 10268223103). Audio already had this; documents did not.

   No background fill. A tint was tried and measured on the rendered pixel:
   both a --mvs-surface-* token and a currentColor mix resolved to the card's
   own colour (rgb(250,251,253) on both), so it painted nothing while looking
   like it did something. The glyph alone is the affordance, and it reads
   cleanly on the card. The glyph is sized up from its 20px default because
   here it IS the tile, not an icon sitting beside a filename. */
.mvs-grid-item-placeholder--document {
    color: var(--mvs-text-muted);
}

.mvs-grid-item-placeholder--document .mvs-doc-glyph {
    --mvs-doc-glyph-size: 48px;
}

/* Audio fallback card — used when an audio upload has no embedded album
   art. Lays out a centered icon, title, and meta line so the cell carries
   information at-a-glance instead of just a music glyph. */
.mvs-audio-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 16px 18px;
    text-align: center;
    color: #fff;
}

.mvs-audio-card__waveform {
    display: block;
    width: 100%;
    max-width: 240px;
    height: 64px;
    color: rgba(255, 255, 255, 0.85);
    flex-shrink: 0;
}

.mvs-audio-card__waveform svg {
    width: 100%;
    height: 100%;
    display: block;
    max-width: none;
    max-height: none;
    opacity: 1;
}

.mvs-audio-card__title {
    font-weight: 600;
    line-height: 1.3;
    max-width: 100%;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-word;
}

.mvs-audio-card__meta {
    font-size: 0.85em;
    opacity: 0.85;
    line-height: 1.3;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

@media (max-width: 640px) {
    .mvs-audio-card {
        padding: 12px 14px;
        gap: 6px;
    }
    .mvs-audio-card__waveform {
        height: 44px;
        max-width: 200px;
    }
}

.mvs-grid-item-placeholder--generic {
    background: linear-gradient(135deg, #2b3544, #1a1f2b);
}

/* Document tile — the cell a document takes in a collection grid.
 *
 * A LIGHT, bordered card rather than the dark gradient the generic
 * placeholder uses. The dark tile is right for media that has a picture we
 * could not produce (a video still, a missing thumbnail): it reads as a
 * photograph slot. A document has no picture and never will, so a photo slot
 * is the wrong metaphor — the reported bug was literally "a black tile with a
 * tiny generic icon". This says "file" instead of "image we lost".
 *
 * Token-driven, so it inverts in dark mode; the gradient above is a literal
 * and cannot. */
.mvs-grid-item-placeholder--document {
    background: var(--mvs-surface-2, #f5f6f8);
    border: 1px solid var(--mvs-border-light, #e2e5ea);
    color: var(--mvs-text-secondary, #5c6472);
}

.mvs-doc-card {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 16px 18px;
    text-align: center;
    width: 100%;
    height: 100%;
    box-sizing: border-box;
}

.mvs-doc-card__glyph {
    --mvs-doc-glyph-size: 44px;
    color: var(--mvs-text-secondary, #5c6472);
    opacity: 0.9;
}

.mvs-doc-card__title {
    font-weight: 600;
    line-height: 1.3;
    max-width: 100%;
    color: var(--mvs-text, #1a1f2b);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    text-overflow: ellipsis;
    word-break: break-word;
}

.mvs-doc-card__meta {
    font-size: 0.82em;
    color: var(--mvs-text-muted, #7a8494);
}

/* A document opened in the media lightbox (Basecamp 10248528902). A document has
   no displayable image, so instead of a broken <img> it renders as a centered
   card; the lightbox chrome around it (Open / Download) still reaches the file. */
.mvs-lightbox-document.mvs-doc-card {
    width: min(340px, 80vw);
    height: auto;
    gap: 14px;
    padding: 40px 32px;
    background: var(--mvs-bg, #ffffff);
    border-radius: 12px;
}

.mvs-lightbox-document .mvs-doc-card__glyph {
    --mvs-doc-glyph-size: 72px;
}

.mvs-lightbox-document .mvs-doc-card__title {
    font-size: 1.05em;
}

@media (max-width: 480px) {
    .mvs-doc-card {
        gap: 6px;
        padding: 10px 12px;
    }

    .mvs-doc-card__glyph {
        --mvs-doc-glyph-size: 32px;
    }
}

.mvs-grid-audio-icon,
.mvs-grid-generic-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    color: inherit;
    line-height: 0;
}

.mvs-grid-audio-icon svg,
.mvs-grid-generic-icon svg {
    width: 34%;
    height: 34%;
    max-width: 72px;
    max-height: 72px;
}

/* ==========================================================================
   30. Playlist UI
   ========================================================================== */

.mvs-playlist {
    max-width: 700px;
    margin: 20px auto;
    background: var(--mvs-surface-2);
    border: 1px solid var(--mvs-border-light);
    border-radius: 8px;
    overflow: hidden;
}

.mvs-playlist-player {
    padding: 16px 20px;
    background: var(--mvs-text);
}

.mvs-playlist-player audio {
    width: 100%;
}

.mvs-playlist-now-playing {
    padding: 10px 20px;
    background: var(--mvs-text);
    color: var(--mvs-bg);
    font-size: 0.95em;
    font-weight: 600;
    min-height: 20px;
}

.mvs-playlist-tracks {
    list-style: none;
    margin: 0;
    padding: 0;
}

.mvs-playlist-track {
    display: flex;
    align-items: center;
    border-bottom: 1px solid var(--mvs-border-light);
    transition: background 0.15s;
}

.mvs-playlist-track:last-child {
    border-bottom: none;
}

.mvs-playlist-track:hover {
    background: var(--mvs-surface-2);
}

.mvs-playlist-track--active {
    background: var(--mvs-info-bg);
}

.mvs-playlist-track--active:hover {
    background: var(--mvs-info-bg-light);
}

.mvs-playlist-track-btn {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    background: none;
    border: none;
    cursor: pointer;
    text-align: left;
    font: inherit;
    color: inherit;
    min-width: 0;
}

.mvs-playlist-track-num {
    width: 28px;
    text-align: center;
    color: var(--mvs-text-muted);
    font-size: 0.85em;
    flex-shrink: 0;
}

.mvs-playlist-track--active .mvs-playlist-track-num {
    color: var(--mvs-primary-text, var(--mvs-primary));
    font-weight: 700;
}

.mvs-playlist-track-info {
    flex: 1;
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.mvs-playlist-track-title {
    font-weight: 500;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mvs-playlist-track-artist {
    font-size: 0.82em;
    color: var(--mvs-text-muted);
}

.mvs-playlist-track-duration {
    color: var(--mvs-text-muted);
    font-size: 0.85em;
    flex-shrink: 0;
}

.mvs-playlist-track-link {
    padding: 12px 14px;
    color: var(--mvs-text-muted);
    text-decoration: none;
    font-size: 1.1em;
}

.mvs-playlist-track-link:hover {
    color: var(--mvs-primary-text, var(--mvs-primary));
}

/* ==========================================================================
   27. Collections & Rule Builder
   ========================================================================== */

.mvs-collection-card-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 14px;
    background: var(--mvs-surface-2);
    border-bottom: 1px solid var(--mvs-border-light);
}

/* --- Collection Single Page --- */

.mvs-single-collection {
    max-width: var(--mvs-content-width, 1200px);
    margin: 0 auto;
    padding: 24px 16px 0;
}

/* Info Card */
.mvs-collection-card-info {
    background: var(--mvs-bg);
    border: 1px solid var(--mvs-border-light);
    border-radius: 12px;
    padding: 24px 28px;
    margin-bottom: 24px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
}

.mvs-collection-card-title {
    margin: 0 0 12px;
    font-size: 1.6em;
    font-weight: 700;
    line-height: 1.3;
    color: var(--mvs-text);
}

.mvs-collection-card-meta {
    display: flex;
    align-items: center;
    gap: 14px;
    flex-wrap: wrap;
    font-size: 0.9em;
    color: var(--mvs-text-secondary);
}

.mvs-collection-meta-author {
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.mvs-collection-meta-author a {
    color: var(--mvs-danger);
    text-decoration: none;
    font-weight: 500;
}

.mvs-collection-meta-author a:hover {
    text-decoration: underline;
}

.mvs-collection-avatar {
    border-radius: 50%;
    width: 24px;
    height: 24px;
}

.mvs-collection-meta-text {
    color: var(--mvs-text-secondary);
}

.mvs-collection-card-desc {
    margin-top: 14px;
    padding-top: 14px;
    border-top: 1px solid var(--mvs-surface-2);
    color: var(--mvs-text-secondary);
    font-size: 0.95em;
    line-height: 1.6;
}

.mvs-collection-card-desc p {
    margin: 0;
}

/* Search bar — reuses .mvs-explore-search layout */
.mvs-collection-search {
    margin-bottom: 24px;
}

.mvs-collection-search form {
    display: flex;
    gap: 0;
}

.mvs-collection-search-input {
    flex: 1;
    padding: 10px 16px;
    border: 1px solid var(--mvs-border);
    border-radius: 8px 0 0 8px;
    font-size: 0.95em;
    background: var(--mvs-bg);
    transition: border-color 0.2s;
}

.mvs-collection-search-input:focus {
    outline: none;
    border-color: var(--mvs-primary);
    box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

.mvs-collection-search-btn {
    padding: 10px 20px;
    border: 1px solid var(--mvs-primary);
    border-left: 0;
    border-radius: 0 8px 8px 0;
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
    font-size: 0.95em;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.2s;
}

.mvs-collection-search-btn:hover {
    background: var(--mvs-primary-hover);
}

/* Rules display (inline in meta) */
.mvs-collection-card-meta .mvs-rule-pill {
    padding: 3px 10px;
    border-radius: 16px;
    font-size: 0.85em;
    background: var(--mvs-surface-2);
    color: var(--mvs-text-secondary);
    font-weight: 500;
}

.mvs-collection-type-badge {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 10px;
    font-size: 0.75em;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    background: var(--mvs-info-bg);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-collection-count {
    font-size: 0.85em;
    color: var(--mvs-text-secondary);
}

.mvs-collection-rules-preview {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin-top: 6px;
}

.mvs-rule-pill {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 0.75em;
    background: rgba(var(--mvs-primary-rgb), 0.15);
    color: var(--mvs-primary-text, var(--mvs-primary));
    border: 1px solid rgba(var(--mvs-primary-rgb), 0.30);
}

/* Type toggle */
.mvs-collection-type-toggle {
    display: flex;
    gap: 0;
    border: 1px solid var(--mvs-border);
    border-radius: 6px;
    overflow: hidden;
    width: fit-content;
}

.mvs-toggle-btn {
    padding: 8px 20px;
    border: none;
    background: var(--mvs-bg);
    cursor: pointer;
    font-size: 0.9em;
    color: var(--mvs-text-secondary);
    transition: background 0.15s, color 0.15s;
}

.mvs-toggle-btn:not(:last-child) {
    border-right: 1px solid var(--mvs-border);
}

.mvs-toggle-btn.active {
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
}

.mvs-field-hint {
    margin-top: 6px;
    font-size: 0.85em;
    color: var(--mvs-text-muted);
    font-style: italic;
}

/* Rule builder */
.mvs-rules-builder {
    margin-top: 16px;
}

.mvs-rules-builder>label {
    display: block;
    font-weight: 600;
    margin-bottom: 10px;
}

.mvs-rules-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 12px;
}

.mvs-rule-row {
    display: flex;
    gap: 8px;
    align-items: center;
}

.mvs-rule-row select,
.mvs-rule-row input {
    padding: 8px 10px;
    border: 1px solid var(--mvs-border);
    border-radius: 4px;
    font-size: 0.9em;
}

.mvs-rule-key {
    min-width: 140px;
    flex: 0 0 auto;
}

.mvs-rule-value,
.mvs-rule-value-text {
    flex: 1;
    min-width: 120px;
}

.mvs-rule-remove {
    flex: 0 0 auto;
    width: 32px;
    height: 32px;
    border: 1px solid var(--mvs-border);
    border-radius: 4px;
    background: var(--mvs-bg);
    color: var(--mvs-danger);
    font-size: 1.2em;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s;
}

.mvs-rule-remove:hover,
.mvs-rule-remove:focus {
    background: var(--mvs-danger-bg);
    color: var(--mvs-danger);
    border-color: var(--mvs-danger);
}

.mvs-rules-preview {
    margin-top: 12px;
    padding: 10px 14px;
    background: var(--mvs-success-bg);
    border-radius: 6px;
    font-size: 0.9em;
    color: var(--mvs-success);
    font-weight: 500;
}

/* Wide modal variant for collection rule builder */
.mvs-modal--wide {
    max-width: 680px;
}

@media (max-width: 600px) {
    .mvs-rule-row {
        flex-wrap: wrap;
    }

    .mvs-rule-key {
        min-width: 100%;
    }
}

/* Section 27 "Theme Compatibility — Reign / BuddyBoss / Generic"
 * removed. Audit summary:
 *
 *  - .theme-flavor scoped rules — dead: no PHP/JS/template in the plugin
 *    emits .theme-flavor, and no theme adds it to <body>. Every rule was
 *    a no-op.
 *  - .mvs-bp-upload-btn scoped rules — dead: the upload button is
 *    emitted with id="mvs-bp-upload-btn" and class="mvs-btn", never with
 *    class="mvs-bp-upload-btn". Rules were also no-ops.
 *  - .buddyboss-theme .mvs-upload-dropzone / .mvs-upload-block —
 *    .mvs-upload-* only appears inside the media-upload Gutenberg block,
 *    which doesn't render inside BP surfaces; the rule never matched.
 *    Additionally, a dangling `.theme-flavor` selector with no rule body
 *    on the line above was silently hijacking this selector list, so the
 *    rules were doubly broken.
 *  - .buddyboss-theme #mvs-activity-media-btn — redundant with the
 *    consolidated #mvs-activity-media-btn rule now in bp-integration.css
 *    (same display/align/gap values).
 *  - .wb-reign-theme #whats-new-form #mvs-activity-media-btn margin-left
 *    — the only live theme-compat rule. Moved to bp-integration.css
 *    (Theme compatibility section) with the selector updated to scope
 *    on .mvs-activity-media-btn-wrap since that's now the flex row
 *    anchor (button no longer lays out on its own).
 *  - .mvs-bp-upload-btn, #mvs-activity-media-btn { max-width: 100% } —
 *    the button now lives inside .mvs-activity-media-btn-wrap which
 *    already has width: 100%; box-sizing: border-box on all button
 *    children via box-sizing: border-box inheritance from the baseline
 *    reset in bp-integration.css. No longer needed.
 */

/* ==========================================================================
   26. Album Placeholder
   ========================================================================== */

.mvs-grid-item-placeholder--album {
    background: linear-gradient(135deg, #6c757d, #495057);
    aspect-ratio: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}

.mvs-grid-album-icon {
    font-size: 36px;
    opacity: 0.8;
}

.mvs-grid-item-placeholder--collection {
    background: linear-gradient(135deg, #4a90d9, #357abd);
    aspect-ratio: 1;
    display: flex;
    align-items: center;
    justify-content: center;
}

.mvs-grid-collection-icon {
    font-size: 36px;
    opacity: 0.8;
}

.mvs-collection-card .mvs-collection-type-badge {
    position: absolute;
    top: 8px;
    right: 8px;
    background: rgba(0, 0, 0, 0.7);
    color: #fff;
    padding: 2px 8px;
    border-radius: 4px;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
}

.mvs-collection-card .mvs-dashboard-card-thumb {
    position: relative;
}

.mvs-collection-card .mvs-dashboard-card-meta {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mvs-collection-card .mvs-collection-rules-preview {
    max-height: 52px;
    overflow: hidden;
}

/* ==========================================================================
   27. Notification Bell
   ========================================================================== */

.mvs-dashboard-header {
    display: flex;
    align-items: center;
    gap: 16px;
}

.mvs-notification-bell {
    position: relative;
    cursor: pointer;
    margin-left: auto;
    user-select: none;
}

.mvs-notification-bell-icon {
    font-size: 1.4em;
}

.mvs-notification-badge {
    position: absolute;
    top: -6px;
    right: -8px;
    background: var(--mvs-danger);
    color: var(--mvs-bg);
    font-size: 11px;
    font-weight: 700;
    min-width: 18px;
    height: 18px;
    line-height: 18px;
    text-align: center;
    border-radius: 9px;
    padding: 0 4px;
}

.mvs-notification-dropdown {
    position: absolute;
    top: 100%;
    right: 0;
    width: 340px;
    max-height: 400px;
    overflow-y: auto;
    background: var(--mvs-bg, #fff);
    border: 1px solid var(--mvs-border-light, #ddd);
    border-radius: 8px;
    box-shadow: var(--mvs-shadow-lg, 0 8px 32px rgba(0, 0, 0, 0.18));
    z-index: 998;
    margin-top: 8px;
}

.mvs-notification-dropdown-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid var(--mvs-border-light);
    /* Pin the header while the notification list scrolls (the parent
       .mvs-notification-dropdown is the overflow-y:auto scroll container). */
    position: sticky;
    top: 0;
    background: var(--mvs-bg, #fff);
    z-index: 1;
}

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

.mvs-notification-item {
    padding: 10px 16px;
    border-bottom: 1px solid var(--mvs-surface-2);
    font-size: 0.9em;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.mvs-notification-item:last-child {
    border-bottom: none;
}

.mvs-notification-unread {
    background: var(--mvs-info-bg);
}

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

.mvs-notification-empty {
    padding: 20px 16px;
    text-align: center;
    color: var(--mvs-text-muted);
    margin: 0;
}

/* ==========================================================================
   28. Comment Edit/Delete Actions
   (Base layout rules for .mvs-comment-header/.mvs-comment-avatar/
   .mvs-comment-actions live in section 13. Only edit-form-specific
   styles belong here.)
   ========================================================================== */

.mvs-comment-edit-form {
    margin-top: 6px;
}

.mvs-comment-edit-textarea {
    width: 100%;
    padding: 8px;
    border: 1px solid var(--mvs-border);
    border-radius: 4px;
    font-size: 0.9em;
    resize: vertical;
    box-sizing: border-box;
}

.mvs-comment-edit-actions {
    display: flex;
    gap: 6px;
    margin-top: 6px;
}

/* ==========================================================================
   29. Follow Button
   ========================================================================== */

.mvs-follow-btn-wrap {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
}

.mvs-follow-btn {
    border-radius: 8px;
    padding: 6px 16px;
    font-size: 0.8125rem;
    font-weight: 600;
    transition: all 0.2s;
    background: var(--mvs-primary);
    /* Always use --mvs-on-primary (#fff) here; never --mvs-bg which flips
       to dark in dark-mode themes and becomes unreadable on the red bg. */
    color: var(--mvs-on-primary, #ffffff);
    border: 1px solid var(--mvs-primary);
}

.mvs-follow-btn:hover {
    background: var(--mvs-primary-hover);
    border-color: var(--mvs-primary-hover);
    color: var(--mvs-on-primary, #ffffff);
}

.mvs-follow-btn.active {
    background: var(--mvs-bg);
    color: var(--mvs-text);
    border-color: var(--mvs-border);
}

.mvs-follow-btn.active:hover {
    background: var(--mvs-surface-2);
    border-color: var(--mvs-border);
}

/* ==========================================================================
   30. Album Cover Picker
   ========================================================================== */

/* `.mvs-media-picker-item` is declared in §19 with its full box, including
   `position: relative`. The duplicate rule that used to sit here restated only
   that one property — Coding Rule 12 forbids duplicate class rules. */

.mvs-media-picker-cover-btn {
    position: absolute;
    inset-block-end: 4px;
    inset-inline-start: 4px;
    background: rgba(0, 0, 0, 0.7);
    color: var(--mvs-bg);
    border: none;
    border-radius: 4px;
    padding: 2px 8px;
    min-block-size: 24px; /* WCAG 2.2 target-size floor on fine pointers; 44px on touch below. */
    font-size: 11px;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.2s;
    z-index: 3;
}

.mvs-media-picker-item:hover .mvs-media-picker-cover-btn,
.mvs-media-picker-cover-btn:focus-visible {
    opacity: 1;
}

/* Revealed on keyboard focus but drew no ring (the theme strips button
   outlines), so a keyboard user could not see which thumbnail's button was
   active. Basecamp 10320657228. */
.mvs-media-picker-cover-btn:focus-visible {
    outline: 2px solid var(--mvs-primary);
    outline-offset: 2px;
}

/* A hover-only reveal is unreachable on touch: there is no hover on a phone or
   tablet, so "Set Cover" was invisible and effectively untappable — while the
   hint directly above the picker instructs the member to click it. On a coarse
   pointer the button is simply always visible (Basecamp 10190678139). */
@media (hover: none), (pointer: coarse) {
    /* Two classes to outrank the global `[class*="mvs-"] button` 44px touch
       floor, which otherwise grew the chip back to 44px. */
    .mvs-media-picker-item .mvs-media-picker-cover-btn {
        opacity: 1;
        min-height: 24px;
    }

    /* The touch floor is met by the HIT AREA, not the chip. Growing the chip
       itself to 44px covered about 40% of a small tile (Basecamp 10320784172),
       so the chip keeps its 24px corner size and this invisible layer extends
       what a finger can hit. It grows upward, toward the tile's centre, and
       only 4px (the chip's own offset) at the edges, so it never spills past
       the tile into its neighbour. */
    .mvs-media-picker-cover-btn::before {
        content: "";
        position: absolute;
        inset-block: calc(24px - var(--mvs-touch-min, 44px)) -4px;
        inset-inline: -4px;
        min-inline-size: var(--mvs-touch-min, 44px);
    }
}

.mvs-media-picker-cover-badge {
    position: absolute;
    top: 4px;
    left: 4px;
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
    border-radius: 4px;
    padding: 2px 8px;
    font-size: 11px;
    font-weight: 700;
    z-index: 3;
}

.mvs-media-picker-cover {
    outline: 3px solid var(--mvs-primary);
    outline-offset: -3px;
}

/* ==========================================================================
   27. Grid Pagination
   ========================================================================== */

.mvs-grid-pagination {
    margin-top: 24px;
    text-align: center;
}

.mvs-grid-pagination-info {
    display: block;
    margin-bottom: 12px;
    font-size: 13px;
    color: var(--mvs-text-secondary);
}

.mvs-grid-pagination-links ul {
    display: flex;
    justify-content: center;
    gap: 4px;
    list-style: none;
    margin: 0;
    padding: 0;
    flex-wrap: wrap;
}

.mvs-grid-pagination-links li {
    margin: 0;
}

.mvs-grid-pagination-links a,
.mvs-grid-pagination-links span {
    display: inline-block;
    padding: 8px 14px;
    border: 1px solid var(--mvs-border-light);
    border-radius: 4px;
    text-decoration: none;
    font-size: 13px;
    color: var(--mvs-text);
    background: var(--mvs-bg);
    transition: all 0.15s;
}

.mvs-grid-pagination-links a:hover {
    background: var(--mvs-surface-2);
    border-color: var(--mvs-border-light);
}

.mvs-grid-pagination-links .current {
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
    border-color: var(--mvs-primary);
    font-weight: 600;
}

.mvs-grid-pagination-links .dots {
    border-color: transparent;
    background: transparent;
}

/* ==========================================================================
   28. Upload Form Fixes
   ========================================================================== */

.mvs-upload-block {
    max-width: 680px;
    margin: 0 auto;
}

.mvs-upload-dropzone {
    border: 2px dashed var(--mvs-border);
    border-radius: 8px;
    padding: 40px 20px;
    text-align: center;
    cursor: pointer;
    transition: all 0.2s;
    background: var(--mvs-surface-2);
}

.mvs-upload-dropzone:hover,
.mvs-upload-dropzone.mvs-dragover {
    border-color: var(--mvs-primary);
    background: var(--mvs-info-bg);
}

.mvs-upload-icon {
    color: var(--mvs-text-muted);
    margin-bottom: 12px;
}

.mvs-upload-text {
    margin: 0 0 16px;
    font-size: 14px;
    color: var(--mvs-text-secondary);
}

.mvs-upload-input {
    display: none;
}

.mvs-upload-privacy {
    margin-top: 12px;
    padding: 6px 12px;
    border: 1px solid var(--mvs-border);
    border-radius: 4px;
    font-size: 13px;
}

.mvs-upload-fields {
    margin-top: 16px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

/* Text fields only -- a bare `input` here also caught the "share as story"
   checkbox and stretched it to the full row width. This block duplicates the
   .mvs-upload-fields rules at ~3644 with different values; the two should be
   consolidated, but that is a wider cleanup than this fix.
   Basecamp 10313183837. */
.mvs-upload-fields input[type="text"],
.mvs-upload-fields textarea {
    width: 100%;
    padding: 8px 12px;
    border: 1px solid var(--mvs-border);
    border-radius: 4px;
    font-size: 13px;
    box-sizing: border-box;
}

/* Section 29 "BP Activity Media Button Alignment" — all rules moved to
 * bp-integration.css. Section header retained as a placeholder so
 * renumbering in a later commit stays mechanical. */

/* ==========================================================================
   30. Single Media Page Fixes
   ========================================================================== */

.mvs-media-content img {
    max-width: 100%;
    height: auto;
    border-radius: 4px;
}

/* ONE declaration. There were two, at identical specificity with no media
   query between them — `12px 24px` earlier in the file and this one later —
   so the first was dead code and its `justify-content: space-between` never
   applied to anything. The horizontal value is 24px so the bar's contents line
   up with the title and body of the card it closes; at 16px it sat 8px to
   their left on every media page. */
.mvs-social-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
    padding: 10px var(--mvs-space-4);
    border-top: 1px solid var(--mvs-border-light);
}

.mvs-reactions {
    display: flex;
    gap: 4px;
}

.mvs-reaction-btn {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    padding: 5px 10px;
    border: 1px solid var(--mvs-border);
    border-radius: 20px;
    background: var(--mvs-bg);
    cursor: pointer;
    font-size: 14px;
    transition: all 0.15s;
}

.mvs-reaction-btn:hover,
.mvs-reaction-btn.active {
    background: var(--mvs-info-bg);
    border-color: var(--mvs-primary);
}

.mvs-reaction-btn .mvs-count {
    font-size: 12px;
    color: var(--mvs-text-secondary);
}

.mvs-social-bar button.active .mvs-count {
    color: var(--mvs-bg);
}

/* Read-only reactions for logged-out users */
.mvs-reactions--readonly .mvs-reaction-btn {
    cursor: default;
    opacity: 0.7;
    pointer-events: none;
}

.mvs-favorite-btn {
    padding: 5px 12px;
    border: 1px solid var(--mvs-border);
    border-radius: 20px;
    background: var(--mvs-bg);
    cursor: pointer;
    font-size: 13px;
    transition: all 0.15s;
}

.mvs-favorite-btn:hover,
.mvs-favorite-btn.active {
    background: var(--mvs-danger-bg);
    border-color: var(--mvs-danger);
    color: var(--mvs-danger);
}

.mvs-share-btn {
    padding: 5px 12px;
    border: 1px solid var(--mvs-border);
    border-radius: 20px;
    background: var(--mvs-bg);
    cursor: pointer;
    font-size: 13px;
    transition: all 0.15s;
}

.mvs-share-btn:hover {
    border-color: var(--mvs-primary);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-view-count {
    margin-left: auto;
    font-size: 0.8125rem;
    color: var(--mvs-text-muted);
}

/* Single Media Responsive */
@media (max-width: 680px) {
    .mvs-single-media {
        padding: 0;
    }

    .mvs-media-article {
        border-radius: 0;
        border-left: none;
        border-right: none;
    }
}

@media (max-width: 480px) {
    .mvs-media-header {
        padding: 12px var(--mvs-space-3);
    }

    .mvs-media-title {
        font-size: 1.25em;
    }

    .mvs-social-bar {
        padding: 10px 12px;
        gap: 6px;
    }

    .mvs-reaction-btn {
        padding: 4px 8px;
        font-size: 13px;
    }

    .mvs-comments-section {
        padding: 10px var(--mvs-space-3) var(--mvs-space-3);
    }

    .mvs-media-description {
        padding: 10px var(--mvs-space-3) 0;
    }

    .mvs-media-footer {
        padding: 6px var(--mvs-space-3) var(--mvs-space-3);
    }
}

/* ==========================================================================
   31. Explore Page Grid Fixes
   ========================================================================== */

/* Section 31 held two EMPTY media blocks (767 and 480). They shipped nothing
   and inflated the breakpoint count that made this file look like it had more
   responsive behaviour than it does. Removed rather than kept as placeholders. */

/* ==========================================================================
   32. Profile Header (Dashboard) & Profile Edit
   ========================================================================== */

/* The profile CARD that lived above the rail is gone — its avatar, name, bio
   and buttons became the rail head plus the `profile` section, so its rules go
   with it rather than lingering as selectors nothing emits (Coding Rule 12).
   The edit FORM's own styling continues below; only the card is removed. */

.mvs-dashboard-profile-edit-btn {
    flex-shrink: 0;
}

/* Inline Edit Form */
/* `padding-top: 0.5rem` used to live here. It predated the panel wrapper and,
   being a later rule of equal specificity, out-specified it — Edit profile was
   the one tab whose content started higher than every other. Removed rather
   than zeroed, so the panel's own padding applies on all four sides like every
   other tab. */

.mvs-profile-form-inline {
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.mvs-profile-field-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
}

.mvs-profile-form-actions {
    display: flex;
    gap: 0.5rem;
}

/* Standalone Profile Edit Page */
.mvs-profile-edit {
    max-width: 600px;
    margin: 2rem auto;
    padding: 0 1rem;
}

.mvs-profile-edit h2 {
    font-size: 1.5rem;
    margin-bottom: 1.5rem;
}

/* Messages */
.mvs-profile-message {
    padding: 0.75rem 1rem;
    border-radius: 6px;
    margin-bottom: 1rem;
    font-size: 0.875rem;
}

.mvs-profile-message--success {
    background: var(--mvs-success-bg);
    color: var(--mvs-success);
    border: 1px solid #c3e6cb;
}

.mvs-profile-message--error {
    background: var(--mvs-danger-bg);
    color: var(--mvs-danger);
    border: 1px solid #f5c6cb;
}

/* Avatar Section */
.mvs-profile-avatar-section {
    display: flex;
    align-items: center;
    gap: 1.5rem;
    margin-bottom: 2rem;
    padding: 1.5rem;
    background: var(--mvs-surface-2);
    border-radius: 12px;
}

.mvs-profile-avatar-preview {
    flex-shrink: 0;
}

.mvs-profile-avatar-img {
    width: 96px;
    height: 96px;
    border-radius: 50%;
    object-fit: cover;
    border: 3px solid var(--mvs-surface);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

.mvs-profile-avatar-actions {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.mvs-profile-avatar-upload-label {
    cursor: pointer;
    display: inline-block;
}

.mvs-profile-avatar-input {
    display: none;
}

.mvs-profile-avatar-remove {
    font-size: 0.8125rem;
    color: var(--mvs-danger);
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
    text-align: left;
}

.mvs-profile-avatar-remove:hover {
    text-decoration: underline;
}

.mvs-profile-avatar-hint {
    font-size: 0.75rem;
    color: var(--mvs-text-muted);
    margin: 0;
}

/* Form Fields */
.mvs-profile-form {
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
}

.mvs-profile-field {
    display: flex;
    flex-direction: column;
    gap: 0.375rem;
}

.mvs-profile-field label {
    font-weight: 600;
    font-size: 0.875rem;
    color: var(--mvs-text);
}

.mvs-profile-field input[type="text"],
.mvs-profile-field textarea {
    padding: 0.625rem 0.75rem;
    border: 1px solid var(--mvs-border);
    border-radius: 6px;
    font-size: 0.9375rem;
    font-family: inherit;
    transition: border-color 0.2s;
    width: 100%;
    box-sizing: border-box;
}

.mvs-profile-field input[type="text"]:focus,
.mvs-profile-field textarea:focus {
    border-color: var(--mvs-primary);
    outline: none;
    box-shadow: 0 0 0 2px rgba(0, 115, 170, 0.15);
}

.mvs-profile-field textarea {
    resize: vertical;
    min-height: 80px;
}

/* Actions */
.mvs-profile-actions {
    margin-top: 0.5rem;
}

/*
 * Colour floor for the button base.
 *
 * `.mvs-btn` carries layout only; every colour lives on a modifier
 * (--primary / --secondary / --danger). That is fine until a call site prints
 * the base class alone — "Add Media" on an album and "Save" on the collection
 * form both do — and then the button falls through to the browser's UA styling
 * (`buttonface` / `ButtonText`). UA button colours answer to `color-scheme`,
 * not to our tokens, so on a dark theme the pill stays light while the theme
 * repaints the label: white on white (Basecamp 10259504401).
 *
 * `:where()` keeps this at ZERO specificity, so every modifier still wins no
 * matter which stylesheet it lives in or what order the sheets load — this is
 * a floor, not a default that has to be fought. Pro's own base rules
 * (gamification.css, dashboard-connectors.css) set no colours either and are
 * covered by the same floor.
 */
:where(.mvs-btn) {
    background: var(--mvs-surface-2);
    color: var(--mvs-text);
}

/* Shared button styles (used across profile and dashboard) */
.mvs-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.375rem;
    padding: 0.625rem 1.25rem;
    border-radius: 6px;
    font-size: 0.875rem;
    font-weight: 600;
    cursor: pointer;
    border: 1px solid transparent;
    transition: background 0.2s, border-color 0.2s;
    text-decoration: none;
    line-height: 1.4;
}

/* Icon sizing inside any .mvs-btn. Lucide's default <svg> is 24x24; at
 * that size it towers over 14px label text. Cap to 16px and inherit
 * the button color so the icon pairs properly with the label. The
 * `[data-lucide]` selector covers the brief moment before
 * lucide.createIcons() swaps the <i> placeholder for <svg>. */
.mvs-btn svg,
.mvs-btn [data-lucide] {
    width: 16px;
    height: 16px;
    stroke-width: 2;
    flex-shrink: 0;
    color: inherit;
}

/* Small-variant buttons use smaller labels, so scale the icon down too. */
.mvs-btn.mvs-btn--small svg,
.mvs-btn.mvs-btn--sm svg,
.mvs-btn.mvs-btn--small [data-lucide],
.mvs-btn.mvs-btn--sm [data-lucide] {
    width: 14px;
    height: 14px;
}

.mvs-btn--primary {
    /* --mvs-btn-primary-* is defined once in the token block above and is what
       the other three copies of this button reference. Never put a theme
       surface/bg token in `color` here; those flip dark in dark-mode themes. */
    background: var(--mvs-btn-primary-bg);
    color: var(--mvs-btn-primary-fg);
    border-color: var(--mvs-btn-primary-bg);
}

.mvs-btn--primary:hover,
.mvs-btn--primary:focus,
.mvs-btn--primary:visited {
    background: var(--mvs-btn-primary-bg-hover);
    color: var(--mvs-btn-primary-fg-hover);
}

.mvs-btn--primary:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}

/* Secondary buttons are a NEUTRAL surface, deliberately distinct from the red
   primary. Do NOT borrow the theme's --button-* tokens here: BuddyX/Reign set
   --button-background-color to their PRIMARY button color (often red), which
   made the secondary button render identical to primary with mismatched text.
   The plugin's own --mvs-* surface/text tokens already flip for dark mode. */
.mvs-btn.mvs-btn--secondary {
    background: var(--mvs-surface-2);
    color: var(--mvs-text);
    border: 1px solid var(--mvs-border);
}

.mvs-btn.mvs-btn--secondary:hover,
.mvs-btn.mvs-btn--secondary:focus {
    background: var(--mvs-border-light);
    color: var(--mvs-text);
    border-color: var(--mvs-border);
}

/* Secondary button icons keep a neutral, theme-agnostic color — some themes
   expose --button-text-color in red/accent, which leaked into lucide SVG fill
   (e.g. Report flag rendered red). Matches the lightbox action convention. */
.mvs-btn.mvs-btn--secondary svg,
.mvs-btn.mvs-btn--secondary [data-lucide] {
    color: var(--mvs-text-secondary);
}

.mvs-btn.mvs-btn--secondary:hover svg,
.mvs-btn.mvs-btn--secondary:focus svg,
.mvs-btn.mvs-btn--secondary:hover [data-lucide],
.mvs-btn.mvs-btn--secondary:focus [data-lucide] {
    color: var(--mvs-text);
}

.mvs-btn--text {
    background: none;
    border: none;
    padding: 0.25rem 0.5rem;
    border-radius: 4px;
    color: var(--mvs-text-secondary);
    transition: background 0.15s, color 0.15s;
}

.mvs-btn--text:hover {
    background: rgba(214, 54, 56, 0.08);
    color: var(--mvs-danger);
    border: none;
}

/* Block/unblock toggle: de-emphasized next to Follow/Message; danger tint when
   active or hovered so the destructive action reads clearly. */
.mvs-block-toggle {
    color: var(--mvs-text-secondary);
}

.mvs-block-toggle:hover,
.mvs-block-toggle:focus,
.mvs-block-toggle--blocked {
    color: var(--mvs-danger);
}

/* Mobile */
@media (max-width: 480px) {
    .mvs-profile-avatar-section {
        flex-direction: column;
        text-align: center;
    }

    .mvs-profile-avatar-actions {
        align-items: center;
    }

    .mvs-profile-avatar-remove {
        text-align: center;
    }

    .mvs-profile-field-row {
        grid-template-columns: 1fr;
    }
}

/* ==========================================================================
   33a. Search Tabs & User Search
   ========================================================================== */

.mvs-search-bar {
    display: flex;
    align-items: center;
    background: var(--mvs-bg);
    /* Pair the foreground with the background this component sets; without it
       the text inherited the ambient theme colour and went unreadable in dark
       mode (measured 1.07-1.38:1). */
    color: var(--mvs-text);
    border: 1.5px solid var(--mvs-border);
    border-radius: 100px;
    padding: 5px 5px 5px 6px;
    gap: 0;
    transition: border-color 0.2s, box-shadow 0.2s;
    max-width: 600px;
}

.mvs-search-bar:focus-within {
    border-color: var(--mvs-primary);
    box-shadow: 0 0 0 3px rgba(0, 115, 170, 0.12);
}

.mvs-search-mode {
    display: flex;
    background: var(--mvs-surface-2);
    /* Pair the foreground with the background this component sets; without it
       the text inherited the ambient theme colour and went unreadable in dark
       mode (measured 1.07-1.38:1). */
    color: var(--mvs-text);
    border-radius: 100px;
    padding: 2px;
    flex-shrink: 0;
    gap: 2px;
}

.mvs-search-mode-btn {
    padding: 5px 14px;
    border: none;
    background: transparent;
    border-radius: 100px;
    cursor: pointer;
    font-size: 0.8125rem;
    font-weight: 500;
    color: var(--mvs-text-secondary);
    transition: background 0.15s, color 0.15s;
    white-space: nowrap;
}

.mvs-search-mode-btn.active {
    background: var(--mvs-bg);
    color: var(--mvs-primary-text, var(--mvs-primary));
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12);
}

.mvs-search-field {
    display: flex;
    align-items: center;
    flex: 1;
    padding: 0 12px;
    gap: 8px;
    min-width: 0;
}

.mvs-search-icon {
    width: 16px;
    height: 16px;
    color: var(--mvs-text-muted);
    flex-shrink: 0;
}

.mvs-search-field input {
    flex: 1;
    border: none;
    outline: none;
    background: transparent;
    font-size: 0.9375rem;
    color: var(--mvs-text);
    padding: 6px 0;
    min-width: 0;
}

/* The reset above loses to the theme. Reign styles every text input with
   `input[type="text"], input[type="search"], ... { border: 1px solid; }` at
   specificity (0,1,1) - identical to `.mvs-search-field input`, so the tie is
   broken by load order and the theme's sheet loads last. The result is a third
   box drawn inside the search pill. Matching the type attribute takes us to
   (0,2,1), which wins on merit rather than with !important. */
.mvs-search-field input[type="text"],
.mvs-search-field input[type="search"] {
    border: none;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
}

.mvs-search-field input#mvs-search-input:focus {
    outline: none;
}

.mvs-search-field input:focus-visible {
    outline: 2px solid var(--mvs-primary, #0073aa);
    outline-offset: -2px;
}

.mvs-search-field input:focus-visible {
    outline: 2px solid var(--mvs-primary);
    outline-offset: -2px;
    border-radius: 2px;
}

.mvs-search-field input::placeholder {
    color: var(--mvs-text-muted);
}

.mvs-user-search-results {
    margin-top: 12px;
}

.mvs-user-search-empty {
    text-align: center;
    color: var(--mvs-text-secondary);
    padding: 16px;
}

.mvs-user-card {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    border: 1px solid var(--mvs-border-light);
    border-radius: 8px;
    margin-bottom: 8px;
    text-decoration: none;
    transition: border-color 0.2s, box-shadow 0.2s;
}

.mvs-user-card:hover {
    border-color: var(--mvs-primary);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}

.mvs-user-card-avatar {
    border-radius: 50%;
    object-fit: cover;
}

.mvs-user-card-info {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.mvs-user-card-info strong {
    font-size: 0.9375rem;
    color: var(--mvs-text);
}

.mvs-user-card-info span {
    font-size: 0.8125rem;
    color: var(--mvs-text-muted);
}

/* Login prompt links styled like buttons */
.mvs-login-prompt {
    opacity: 0.7;
    cursor: pointer;
    text-decoration: none;
}

.mvs-login-to-comment,
.mvs-comment-denied {
    text-align: center;
    padding: 12px;
    color: var(--mvs-text-secondary);
}

/* --mvs-primary is a FILL colour; as text it measures 4.36:1, under the 4.5:1
   AA floor. --mvs-primary-text is the same hue darkened to pass (Basecamp
   10253005724). */
.mvs-login-to-comment a {
    color: var(--mvs-primary-text, var(--mvs-primary));
}

/* ==========================================================================
   33. Logged-Out CTA Banner
   ========================================================================== */

.mvs-logged-out-banner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    /* Fixed purple gradient in both modes — was var(--mvs-bg), which flips to
       near-black in dark mode (dark text on purple). Keep it white. */
    color: #fff;
    padding: 14px 24px;
    border-radius: 8px;
    margin: 0 auto 20px;
    max-width: var(--mvs-content-width, 1200px);
    position: relative;
}

.wb-grid>.mvs-logged-out-banner {
    flex: 0 0 100%;
}

.mvs-logged-out-banner__content {
    font-size: 0.9375rem;
}

.mvs-logged-out-banner__content strong {
    font-weight: 700;
}

.mvs-logged-out-banner__actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;
    margin-right: 15px;
}

.mvs-logged-out-banner__actions .mvs-btn--primary {
    /* White button on the purple banner in both modes — was var(--mvs-bg),
       which flips dark in dark mode and breaks the intended white pill. */
    background: #fff;
    /* #4c51bf, not #667eea. The banner's own indigo scored 3.66 on the white
       pill — a fail, on the single control that decides whether a visitor
       joins at all (Basecamp 10252222150). Same hue family, just dark enough:
       6.49. Not `--mvs-primary`, because this pill sits on a fixed purple
       gradient that does not follow the theme, so its text must not either. */
    color: #4c51bf;
    border: none;
}

.mvs-logged-out-banner__actions .mvs-btn--secondary {
    background: transparent;
    /* Fixed white, for the reason the comment above already gives: the banner
       is a fixed purple gradient in BOTH modes, and `var(--mvs-bg)` flips to
       near-black in dark — dark text on purple. The primary button was fixed
       for exactly this and the secondary was left behind. */
    color: #fff;
    border: 1px solid rgba(255, 255, 255, 0.6);
}

.mvs-logged-out-banner__close {
    background: none;
    border: none;
    /* Full white, not 70% — at 0.7 on the gradient this was the faintest
       control on a banner shown to every visitor. */
    color: #fff;
    font-size: 20px;
    cursor: pointer;
    /* Was 20x28. A dismiss control needs to be hittable, and this one sits in
       a corner where a thumb is least accurate. Centring the glyph in a
       touch-sized box costs no layout, since the button is absolutely
       positioned. */
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: var(--mvs-touch-min, 44px);
    min-height: var(--mvs-touch-min, 44px);
    padding: 0;
    line-height: 1;
    position: absolute;
    top: 8px;
    right: 4px;
}

.mvs-logged-out-banner__close:hover {
    /* Not `var(--mvs-bg)`: same dark-mode flip as the buttons above. */
    color: #fff;
    background: none;
    border: none;
}

@media (max-width: 600px) {
    .mvs-logged-out-banner {
        flex-direction: column;
        text-align: center;
        padding: 16px 20px 20px;
    }
}

/* ==========================================================================
   34. Frontend Empty States
   ========================================================================== */

.mvs-empty-state-frontend {
    text-align: center;
    padding: 48px 24px;
    color: var(--mvs-text-secondary);
}

.mvs-empty-state-icon {
    display: block;
    font-size: 48px;
    margin-bottom: 12px;
    line-height: 1;
    opacity: 0.7;
}

/* These were emoji sized by `font-size`. A Lucide <svg> ignores that and ships
   at 24x24, so it needs explicit dimensions to keep the 48px presence the empty
   state was designed around. `currentColor` is the point of the swap: the glyph
   now takes the text colour and follows dark mode, which an emoji never could
   (Basecamp 10253006229). */
.mvs-empty-state-icon svg,
.mvs-empty-state-icon [data-lucide] {
    width: 48px;
    height: 48px;
    stroke-width: 1.5;
    color: inherit;
    margin-inline: auto;
}

.mvs-empty-state-frontend h3 {
    margin: 0 0 8px;
    font-size: 1.125rem;
    color: var(--mvs-text);
}

.mvs-empty-state-frontend p {
    margin: 0 0 16px;
    font-size: 0.9375rem;
    color: var(--mvs-text-secondary);
}

/* -----------------------------------------------------------------------
   Plugin-branded 404 (mvs-empty-state-frontend--404)
   Uses the same design language as the auth gate — centered card with a
   gradient accent, circular icon halo, and popular-tag row below the
   primary CTA.
   ----------------------------------------------------------------------- */
.mvs-archive--404 {
    display: flex;
    justify-content: center;
    padding: clamp(32px, 8vw, 96px) 16px;
}

.mvs-empty-state-frontend--404 {
    position: relative;
    width: 100%;
    max-width: 560px;
    margin: 0;
    padding: clamp(32px, 5vw, 56px) clamp(24px, 4vw, 48px);
    background: var(--mvs-surface, #fff);
    border-radius: var(--mvs-radius-lg, 16px);
    box-shadow:
        0 1px 2px rgba(15, 23, 42, 0.04),
        0 18px 48px -12px rgba(15, 23, 42, 0.18);
    overflow: hidden;
    box-sizing: border-box;
}

.mvs-empty-state-frontend--404::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 4px;
    background: linear-gradient(90deg,
        var(--mvs-primary, #0073aa) 0%,
        #6a5acd 50%,
        #ff6aa6 100%);
}

.mvs-empty-state-frontend__glyph {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 88px;
    height: 88px;
    margin: 0 auto 20px;
    border-radius: 50%;
    background: rgba(var(--mvs-primary-rgb, 0, 115, 170), 0.08);
    color: var(--mvs-primary-text, var(--mvs-primary));
    z-index: 1;
}

.mvs-empty-state-frontend__glyph svg {
    width: 44px;
    height: 44px;
    stroke-width: 1.75;
}

.mvs-empty-state-frontend__title {
    margin: 0 0 12px;
    font-family: var(--mvs-font-heading, inherit);
    font-size: clamp(22px, 2.6vw, 28px);
    font-weight: 700;
    line-height: 1.25;
    color: var(--mvs-text, #1e1e1e);
}

.mvs-empty-state-frontend__lede {
    margin: 0 auto 28px;
    max-width: 420px;
    font-size: 15px;
    line-height: 1.55;
    color: var(--mvs-text-secondary, #646970);
}

.mvs-empty-state-frontend--404 .mvs-empty-state-actions {
    display: flex;
    justify-content: center;
    margin-bottom: 24px;
}

.mvs-empty-state-frontend--404 .mvs-btn--primary {
    min-width: 200px;
    padding: 12px 28px;
    font-size: 15px;
    font-weight: 600;
}

.mvs-empty-state-frontend__tags-label {
    margin: 12px 0 10px;
    font-size: 13px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--mvs-text-muted, #a7aaad);
}

.mvs-empty-state-frontend--404 .mvs-empty-state-tags {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    justify-content: center;
}

.mvs-empty-state-frontend--404 .mvs-tag-cloud-item {
    padding: 6px 14px;
    border-radius: var(--mvs-radius-pill, 100px);
    background: var(--mvs-surface-2, #f6f7f7);
    color: var(--mvs-text, #1e1e1e);
    font-size: 13px;
    text-decoration: none;
    transition: background 150ms ease, color 150ms ease;
}

.mvs-empty-state-frontend--404 .mvs-tag-cloud-item:hover,
.mvs-empty-state-frontend--404 .mvs-tag-cloud-item:focus-visible {
    background: rgba(var(--mvs-primary-rgb, 0, 115, 170), 0.08);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

@media (max-width: 640px) {
    .mvs-archive--404 {
        padding: 24px 12px 48px;
    }
    .mvs-empty-state-frontend--404 {
        padding: 28px 20px 32px;
        box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -8px rgba(15, 23, 42, 0.15);
    }
    .mvs-empty-state-frontend__glyph {
        width: 72px;
        height: 72px;
        margin-bottom: 16px;
    }
    .mvs-empty-state-frontend__glyph svg {
        width: 40px;
        height: 40px;
    }
    .mvs-empty-state-frontend--404 .mvs-btn--primary {
        width: 100%;
        min-width: 0;
    }
}

/* ==========================================================================
   34b. Profile Completion Prompt
   ========================================================================== */

.mvs-profile-prompt {
    display: flex;
    align-items: center;
    gap: 10px;
    background: var(--mvs-warning-bg);
    border: 1px solid var(--mvs-warning);
    border-radius: 6px;
    padding: 10px 16px;
    margin-bottom: 16px;
    font-size: 0.875rem;
    position: relative;
}

.mvs-profile-prompt-icon {
    font-size: 20px;
    flex-shrink: 0;
}

.mvs-profile-prompt-text {
    flex: 1;
    color: var(--mvs-warning);
}

.mvs-profile-prompt-text .mvs-dashboard-profile-edit-btn {
    background: transparent;
    color: var(--mvs-warning);
    font-weight: 700;
    text-decoration: underline;
    border: 0;
    padding: 0;
}

.mvs-profile-prompt-text .mvs-dashboard-profile-edit-btn:hover,
.mvs-profile-prompt-text .mvs-dashboard-profile-edit-btn:focus {
    background: transparent;
    color: var(--mvs-primary-text, var(--mvs-primary));
    border: 0;
    padding: 0;
    box-shadow: none;
}

.mvs-profile-prompt-close {
    background: none;
    border: none;
    font-size: 18px;
    cursor: pointer;
    color: var(--mvs-warning);
    padding: 4px;
    line-height: 1;
}

.mvs-profile-prompt-close:hover,
.mvs-profile-prompt-close:focus {
    color: var(--mvs-primary-text, var(--mvs-primary));
    background: none;
    border: none;
}

/* ==========================================================================
   34b. Touch target floor — one rule, not a list
   ========================================================================== */

/* Every interactive plugin control clears --mvs-touch-min on a touch viewport.
   This replaces per-surface selector lists: messaging.css raised five chat
   controls by name, reported "0 under the floor", and the profile-prompt close
   still shipped at 17x22 because it renders only for a member with no profile
   and so was never on screen during that sweep (card 10285849953).

   Same reasoning as the [hidden] rule above: an allowlist guarantees the next
   control added is missed too.

   min-height only. A blanket min-width overflows controls that are meant to
   stretch or sit inline — the chat tab row is the documented case — so width
   floors stay with the icon-only controls that need them. Height is the
   dimension these misses actually fail on. */
@media (max-width: 640px) {

	[class*="mvs-"] button,
	button[class*="mvs-"],
	[class*="mvs-"] [role="button"],
	a[class*="mvs-btn"] {
		min-height: var(--mvs-touch-min, 44px);
	}

	/* Icon-only controls have no text to give them width. */
	.mvs-profile-prompt-close {
		min-width: var(--mvs-touch-min, 44px);
	}
}

/* ==========================================================================
   35. Accessibility — Focus & Reduced Motion
   ========================================================================== */

/* Visible focus ring on all interactive elements */
.mvs-btn:focus-visible,
.mvs-reaction-btn:focus-visible,
.mvs-favorite-btn:focus-visible,
.mvs-share-btn:focus-visible,
.mvs-follow-btn:focus-visible,
.mvs-dashboard-tab:focus-visible,
.mvs-comment-form button:focus-visible,
.mvs-comment-form textarea:focus-visible,
.mvs-tag-cloud-item:focus-visible,
.mvs-notification-bell:focus-visible,
.mvs-modal-close:focus-visible,
.mvs-dashboard-dropzone:focus-visible,
.mvs-load-more-btn:focus-visible {
    outline: 2px solid var(--mvs-primary);
    outline-offset: 2px;
}

/* Focus trap visual — modals */
.mvs-modal:focus-within {
    box-shadow: 0 0 0 4px rgba(0, 115, 170, 0.15);
}

/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}


/* ==========================================================================
   36. Mobile Touch Targets & Responsiveness
   ========================================================================== */

@media (max-width: 767px) /* below --mvs-bp-tablet */ {

    /* Ensure minimum 44px touch targets */
    .mvs-reaction-btn {
        min-width: 44px;
        min-height: 44px;
        padding: 8px 10px;
    }

    .mvs-favorite-btn,
    .mvs-share-btn {
        min-height: 44px;
        padding: 10px 14px;
    }

    .mvs-follow-btn {
        min-height: 44px;
        padding: 10px 16px;
    }

    /* Increase spacing between action buttons */
    .mvs-social-actions {
        gap: 8px;
    }

    .mvs-social-actions-left {
        gap: 8px;
    }

    /* Sticky dashboard tabs — mobile guideline §5.2 */

    .mvs-dashboard-tab {
        min-height: 44px;
    }

    /* Prevent horizontal overflow on long text */
    .mvs-media-title {
        word-break: break-word;
        overflow-wrap: break-word;
    }

    .mvs-media-author-name {
        max-width: 150px;
    }

    .mvs-grid-item-info {
        max-width: 100%;
        overflow: hidden;
    }

    .mvs-grid-item-author {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 120px;
    }

    /* Single media — sticky bottom action bar */
    .mvs-social-bar {
        position: sticky;
        bottom: 0;
        background: var(--mvs-bg);
        border-top: 1px solid var(--mvs-border-light);
        padding: 8px 16px;
        margin: 0 -16px;
        z-index: 5;
    }

    .mvs-social-actions {
        flex-wrap: wrap;
    }

    /* Notification bell touch target */
    .mvs-notification-bell {
        min-width: 44px;
        min-height: 44px;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    /* Upload dropzone larger on mobile */
    .mvs-dashboard-dropzone {
        min-height: 120px;
    }

    /* Tag cloud horizontal scroll */
    .mvs-tag-cloud {
        overflow-x: auto;
        white-space: nowrap;
        -webkit-overflow-scrolling: touch;
        padding-bottom: 8px;
    }

    .mvs-tag-cloud-item {
        min-height: 36px;
        padding: 6px 12px;
    }

    /* Modal full-screen on mobile */
    .mvs-modal {
        width: 100%;
        max-width: 100%;
        height: 100%;
        max-height: 100%;
        border-radius: 0;
        margin: 0;
    }

    .mvs-modal-body {
        max-height: calc(100vh - 120px);
        overflow-y: auto;
    }
}

/* Bottom navigation bar for mobile logged-in users */
@media (max-width: 600px) {
    .mvs-explore-header h1 {
        font-size: 1.25rem;
    }

    .mvs-explore-search input {
        font-size: 16px;
        /* Prevent zoom on iOS */
    }

    .mvs-media-grid.mvs-cols-3,
    .mvs-media-grid.mvs-cols-4,
    .mvs-media-grid.mvs-cols-5 {
        grid-template-columns: repeat(2, 1fr);
    }

    .mvs-dashboard-grid {
        grid-template-columns: repeat(2, 1fr);
    }

    .mvs-dashboard-card-actions {
        flex-wrap: wrap;
    }

    /* Comment form stacked on mobile */
    .mvs-comment-form {
        flex-direction: column;
    }

    .mvs-comment-form button {
        align-self: flex-end;
    }
}

/* ==========================================================================
   RTL Support
   ========================================================================== */

[dir="rtl"] [dir="rtl"] [dir="rtl"] [dir="rtl"] [dir="rtl"] .mvs-grid-item-info {
    flex-direction: row-reverse;
}

[dir="rtl"] .mvs-grid-item-overlay {
    text-align: right;
}

[dir="rtl"] .mvs-search-bar {
    flex-direction: row-reverse;
}

[dir="rtl"] .mvs-search-field {
    flex-direction: row-reverse;
}

[dir="rtl"] .mvs-activity-media-remove {
    right: auto;
    left: 4px;
}

[dir="rtl"] .mvs-explore-header {
    text-align: right;
}

[dir="rtl"] .mvs-tag-cloud {
    direction: rtl;
}

[dir="rtl"] [dir="rtl"] [dir="rtl"] .mvs-logged-out-banner__content {
    text-align: right;
}

[dir="rtl"] .mvs-logged-out-banner__close {
    right: auto;
    left: 16px;
}

[dir="rtl"] .mvs-chat-trigger {
    right: auto;
    left: 20px;
}

[dir="rtl"] .mvs-bp-album-form,
[dir="rtl"] .mvs-bp-album-msg {
    text-align: right;
}

/* ==========================================================================
   Mobile — Search Bar Responsive Fixes
   ========================================================================== */

@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    .mvs-search-bar {
        max-width: 100%;
    }

    .mvs-search-mode-btn {
        padding: 5px 10px;
        font-size: 0.75rem;
    }
}

@media (max-width: 480px) {
    .mvs-explore-header h1 {
        font-size: 1.25rem;
    }

    .mvs-search-bar {
        border-radius: var(--mvs-radius-lg);
        padding: 4px;
    }

    .mvs-search-mode {
        display: none;
        /* Collapsed on very small screens — defaults to media mode */
    }
}


/* ── Lightbox social sidebar ───────────────────────────────────────── */
.mvs-lightbox-overlay {
    position: fixed;
    inset: 0;
    z-index: 100000;
    background: rgba(0, 0, 0, 0.92);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 2.5vh 2.5vw;          /* ~5% breathing room on every edge so the */
    box-sizing: border-box;        /* lightbox doesn't bleed into theme chrome */
    animation: mvs-fade-in 0.2s ease;
}

.mvs-lightbox {
    position: relative;
    display: flex;
    width: 100%;
    height: 100%;
    max-width: 95vw;               /* explicit caps reinforce the inset padding */
    max-height: 95vh;
    /* Container background matches the inner surfaces so the rounded corners
       never expose a lighter sliver when --mvs-surface resolves to white in
       a theme that doesn't define a dark variant. The outer overlay scrim
       handles the page-backdrop dimming. */
    background: transparent;
    border-radius: 12px;
    overflow: hidden;
    animation: mvs-slide-up 0.3s ease;
}

.mvs-lightbox-close {
    position: absolute;
    top: 12px;
    right: 12px;
    z-index: 10;
    background: rgba(0, 0, 0, .5);
    border: none;
    border-radius: 50%;
    width: 44px;
    height: 44px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    /* Always-light icon — the backdrop is rgba(0,0,0,.5) regardless of
       theme, so referencing --mvs-surface (which flips dark in dark mode)
       turned the close icon invisible (dark on dark). The lightbox itself
       overlays the page with a dark veil, so #fff is the right fixed
       choice here. */
    color: #fff;
}

.mvs-lightbox-close:hover {
    background: rgba(0, 0, 0, .7);
}

/* Fullscreen toggle — sits just left of the close button, same pill treatment. */
.mvs-lightbox-fullscreen {
    position: absolute;
    top: 12px;
    right: 64px;
    z-index: 10;
    background: rgba(0, 0, 0, .5);
    border: none;
    border-radius: 50%;
    width: 44px;
    height: 44px;
    padding: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    color: #fff;
}

.mvs-lightbox-fullscreen:hover,
.mvs-lightbox-fullscreen:focus-visible {
    background: rgba(0, 0, 0, .7);
}

/* Swap expand/compress icon by state (the fullscreen class rides on .mvs-lightbox). */
.mvs-lightbox-fs-compress {
    display: none;
}

.mvs-lightbox--fullscreen .mvs-lightbox-fs-expand {
    display: none;
}

.mvs-lightbox--fullscreen .mvs-lightbox-fs-compress {
    display: block;
}

/* Fullscreen: fill the viewport and hide the comments sidebar so the media
   gets the whole space (customer request, Basecamp 9822552686). The media
   pane is flex:1, so it expands on its own once the sidebar is gone. */
.mvs-lightbox--fullscreen {
    max-width: 100vw;
    max-height: 100vh;
    border-radius: 0;
}

.mvs-lightbox--fullscreen .mvs-lightbox-sidebar {
    display: none;
}

.mvs-lightbox-close svg {
    width: 20px;
    height: 20px;
}

.mvs-lightbox-loading {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
}

.mvs-lightbox-media {
    flex: 1;
    min-width: 0;
    min-height: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #000;
    position: relative;
}

/* The <picture> wrapper is the actual flex item — the <img> is its child, not
 * a child of .mvs-lightbox-media. Nothing styled `picture` at all, so it had no
 * definite height: `height: 100%` on the img resolved against an indefinite
 * containing block, fell back to `auto`, the img rendered at its intrinsic
 * height, and `.mvs-lightbox { overflow: hidden }` silently cut the overflow.
 * With align-items:center the cut splits top and bottom, which is why tall
 * portraits lost their heads and feet (Basecamp #10153546608). Measured before
 * this rule: a 900x1600 upload rendered a 1351px-tall <picture> inside a 739px
 * media box — 612px clipped.
 *
 * Constraining `picture` gives the img a definite box to contain into. Video
 * and audio are direct children of .mvs-lightbox-media and were never affected,
 * which is the asymmetry that confirmed the diagnosis. */
.mvs-lightbox-media picture {
    display: flex;
    align-items: center;
    justify-content: center;
    /* Definite, not just capped. max-height alone leaves the height
     * indeterminate, so the img's `height: 100%` still resolved to auto and it
     * kept rendering at intrinsic size inside a correctly-sized picture. */
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;
}

.mvs-lightbox-media img {
    /* Sized by the max-* pair, not width/height 100%, so object-fit has a real
     * box to contain into and the image scales DOWN to fit rather than
     * overflowing its parent. */
    width: auto;
    height: auto;
    max-width: 100%;
    max-height: 100%;
    object-fit: contain;
    display: block;
}

/* `display: flex` above beats the HTML `hidden` attribute — the same trap the
 * img/video/audio rule below already documents. shared-ui-frame.php binds
 * data-wp-bind--hidden on the <picture> to swap between image and video items,
 * so without this a hidden <picture> would keep rendering over the video. */
.mvs-lightbox-media picture[hidden] {
    display: none !important;
}

.mvs-lightbox-media img[hidden],
.mvs-lightbox-media video[hidden],
.mvs-lightbox-media audio[hidden] {
    display: none !important;
}

.mvs-lightbox-video {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
    border-radius: 0;
    background: #000;
}

.mvs-lightbox-audio {
    width: min(400px, 80%);
    display: block;
    border-radius: 12px;
    padding: 8px;
    filter: invert(1);
}

.mvs-lightbox-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    background: rgba(255, 255, 255, .85);
    border: none;
    border-radius: 50%;
    width: 44px;
    height: 44px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 2px 8px rgba(0, 0, 0, .2);
}

.mvs-lightbox-nav:hover {
    background: var(--mvs-surface);
}

.mvs-lightbox-nav--prev {
    left: 12px;
}

.mvs-lightbox-nav--next {
    right: 12px;
}

.mvs-lightbox-position {
    position: absolute;
    bottom: 12px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(0, 0, 0, .6);
    color: var(--mvs-surface);
    padding: 4px 12px;
    border-radius: 12px;
    font-size: 13px;
}

/* Sidebar */
.mvs-lightbox-sidebar {
    width: 380px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    border-left: 1px solid var(--mvs-border-light);
    overflow-y: auto;
    background: var(--mvs-surface);
}

.mvs-lightbox-author {
    padding: 14px 16px;
    border-bottom: 1px solid var(--mvs-border-light);
}

.mvs-lightbox-author-link {
    display: flex;
    align-items: center;
    gap: 10px;
    text-decoration: none;
    color: inherit;
}

.mvs-lightbox-author-avatar {
    width: 36px;
    height: 36px;
    border-radius: 50%;
    object-fit: cover;
}

.mvs-lightbox-stats {
    padding: 6px 16px;
    font-size: 13px;
    color: var(--mvs-text-secondary);
}

.mvs-lightbox-reactions {
    display: flex;
    gap: 2px;
    padding: 8px 16px;
    border-bottom: 1px solid var(--mvs-border-light);
}

.mvs-lightbox-reaction {
    background: transparent;
    border: 1px solid transparent;     /* No "color patches" at rest — borders */
    border-radius: 20px;               /* only appear on hover/active state to */
    padding: 4px 10px;                 /* keep the strip clean against the */
    cursor: pointer;                   /* light sidebar background */
    min-block-size: var(--mvs-touch-min, 44px);
    justify-content: center;
    font-size: 14px;
    display: flex;
    align-items: center;
    gap: 4px;
    transition: background .15s, border-color .15s;
}

.mvs-lightbox-reaction>span:empty {
    display: none;
}

.mvs-lightbox-reaction:hover {
    background: var(--mvs-surface-2);
    border-color: var(--mvs-border-light);
}

.mvs-lightbox-reaction.active {
    background: var(--mvs-info-bg);
    border-color: var(--mvs-primary);
    color: var(--mvs-primary-text, var(--mvs-primary));
}

/* Action toolbar — 5 buttons (Favorite / Share / Download / Open / Report)
   share the row evenly. flex:1 + space-between makes them auto-fit the
   sidebar at any width without horizontal scroll. The previous design
   used inline-flex + margin-left:auto on Report which overflowed the
   380 px sidebar (5 × ~70 px + gaps + padding ≈ 414 px). Customer
   reported a horizontal scrollbar on both desktop and mobile. */
.mvs-lightbox-actions {
    display: flex;
    gap: 4px;
    padding: 10px 12px;
    border-bottom: 1px solid var(--mvs-border-light);
    justify-content: space-between;
    align-items: stretch;
    flex-wrap: nowrap;
}

.mvs-lightbox-action {
    display: flex;
    flex: 1 1 0;
    min-width: 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    background: none;
    border: none;
    cursor: pointer;
    font-size: 13px;
    padding: 6px 2px;
    text-align: center;
    border-radius: 6px;
    text-decoration: none;
    color: var(--mvs-text);
    /* Overflow is handled on .mvs-lightbox-action__label, not here. A bare text
       node in a display:flex button becomes an anonymous flex item, which
       text-overflow cannot reach - so the nowrap/overflow/text-overflow trio
       that used to live on this rule silently did nothing and "Download" was
       hard-cut at 56px. Basecamp 10286532431. */
    line-height: 1.15;
}

/* The label owns its own truncation. display:block (not a flex item) is what
   makes text-overflow work at all; max-width keeps it inside the 56px column
   the fixed 6-7 button track allows. Degrades to "Herunterl..." rather than a
   mid-word break or an invisible word, which matters more for translations
   than for English - "Download" only overruns by 1.8px, "Herunterladen" by 30. */
.mvs-lightbox-action__label {
    display: block;
    max-width: 100%;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    /* 11px is what actually fits. Measured in the 380px sidebar: the 6-7 button
       track gives each label a 52px content box, and "Download" - the longest
       English label - needs 57.8px at 13px, 53.4px at 12px, 48.9px at 11px.
       13px was never wide enough; it only looked fine because the clip was
       1.8px. The ellipsis above is the safety net for translations, not for
       English. */
    font-size: 11px;
}

.mvs-lightbox-action:hover,
.mvs-lightbox-action:focus {
    background: var(--mvs-surface-2);
    color: var(--mvs-text);
    border: none;
}

.mvs-lightbox-action.active {
    color: var(--mvs-danger);
}

.mvs-lightbox-action [data-lucide],
.mvs-lightbox-action svg {
    width: 18px;
    height: 18px;
    stroke-width: 2;
}

/* Theme defense for the lightbox controls — keep the reaction pills and action
   buttons clean (no theme-injected fill/border/radius) so the lightbox reads as
   a premium, uncluttered surface on any theme. !important guards against themes
   that style bare <button>. Bug #10019404777. */
.mvs-lightbox-reaction {
    background: transparent !important;
    border: 1px solid transparent !important;
    border-radius: 20px !important;
    box-shadow: none !important;
}

.mvs-lightbox-reaction:hover {
    background: var(--mvs-surface-2) !important;
    border-color: var(--mvs-border-light) !important;
}

.mvs-lightbox-reaction.active {
    background: var(--mvs-info-bg) !important;
    border-color: var(--mvs-primary) !important;
}

.mvs-lightbox-action {
    background: none !important;
    border: 0 !important;
    border-radius: 6px !important;
    box-shadow: none !important;
    color: var(--mvs-text) !important;
}

.mvs-lightbox-action:hover,
.mvs-lightbox-action:focus {
    background: var(--mvs-surface-2) !important;
    color: var(--mvs-text) !important;
}

/* The active (favorited) state must survive the theme-defense guard above —
   without the !important here the guard's color pin wins and the heart never
   changes color even though the state toggles. */
.mvs-lightbox-action.active,
.mvs-lightbox-action.active:hover,
.mvs-lightbox-action.active:focus {
    color: var(--mvs-danger) !important;
}

.mvs-lb-fav.active svg {
    fill: currentColor;
}

/* Fullscreen — when the lightbox enters browser fullscreen via the
   Fullscreen API, override the dialog max sizes so the media fills the
   actual viewport. ESC and the F shortcut both exit. */
.mvs-lightbox:fullscreen {
    width: 100vw;
    height: 100vh;
    max-width: none;
    max-height: none;
    border-radius: 0;
}

.mvs-lightbox:fullscreen .mvs-lightbox-media {
    max-height: calc(100vh - 64px);
}

.mvs-lightbox:fullscreen .mvs-lightbox-media picture,
.mvs-lightbox:fullscreen .mvs-lightbox-media img,
.mvs-lightbox:fullscreen .mvs-lightbox-video {
    max-height: calc(100vh - 64px);
    object-fit: contain;
}

/* High-contrast focus ring on every lightbox action — keyboard a11y for
   the new Download / Fullscreen buttons + the existing Share / Open. */
.mvs-lightbox-action:focus-visible,
.mvs-lightbox-close:focus-visible,
.mvs-lightbox-nav:focus-visible {
    outline: 2px solid var(--mvs-primary, #2271b1);
    outline-offset: 2px;
}

.mvs-lightbox-action--report {
    /* No margin-left:auto — flex:1 + space-between on the parent already
       distributes the 5 buttons evenly. Forcing Report to the right used
       to overflow the sidebar's 380 px width. */
    color: var(--mvs-text-secondary);
    font-size: 13px;
}

.mvs-lightbox-action--report:hover {
    color: var(--mvs-danger);
    background: var(--mvs-danger-bg);
}

/* ─── Lightbox — mobile responsive layout ──────────────────────────────────
   Stack vertically below tablet so the 380 px sidebar doesn't push the
   media off-screen and force a horizontal scrollbar on narrow viewports.
   Action buttons distribute evenly (flex:1) so 5 fit on one row at any
   width without scrolling. */
@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    .mvs-lightbox {
        flex-direction: column;
        max-width: 100vw;
        max-height: 100vh;
        height: 100vh;
        width: 100vw;
        border-radius: 0;
    }

    .mvs-lightbox-media {
        flex: 0 0 auto;
        max-height: 50vh;
        min-height: 240px;
    }

    .mvs-lightbox-sidebar {
        width: 100%;
        flex: 1 1 auto;
        border-left: none;
        border-top: 1px solid var(--mvs-border-light);
    }

    /* 5-button row distributes evenly; no scroll, no margin-left:auto pushing
       Report off-row. Padding tightens so labels fit. */
    .mvs-lightbox-actions {
        gap: 0;
        padding: 8px 4px;
        flex-wrap: nowrap;
        overflow-x: visible;
        justify-content: space-around;
    }

    .mvs-lightbox-action {
        flex: 1 1 0;
        min-width: 0;
        padding: 6px 4px;
        font-size: 12px;
    }

    /* Override --report's margin-left:auto so the 5 buttons distribute
       evenly via space-around above. */
    .mvs-lightbox-action--report {
        margin-left: 0;
        font-size: 12px;
    }

    .mvs-lightbox-action [data-lucide],
    .mvs-lightbox-action svg {
        width: 20px;
        height: 20px;
    }
}

/* Very narrow phones — drop labels to icons-only so 5 still fit. */
@media (max-width: 380px) {
    .mvs-lightbox-action {
        font-size: 0; /* hide the text node */
        padding: 8px 4px;
    }

    .mvs-lightbox-action [data-lucide],
    .mvs-lightbox-action svg {
        width: 22px;
        height: 22px;
    }
}

.mvs-lightbox-desc {
    padding: 10px 16px;
    font-size: 14px;
    line-height: 1.5;
    border-bottom: 1px solid var(--mvs-border-light);
}

.mvs-lightbox-desc strong {
    margin-right: 6px;
}

.mvs-lightbox-comments {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
}

/* Lightbox comment input — bigger AND uses the sidebar's own color tokens
   so it stays readable in any theme that flips dark mode independently of
   our `--mvs-bg` / `--mvs-text` variables. The default rule one block above
   uses `--mvs-bg` for the background which can resolve to white when the
   active theme (e.g. BuddyX Pro dark) doesn't override that var, leaving
   light theme-text rendered against a white box — visually unreadable. */
.mvs-lightbox-sidebar .mvs-comment-form textarea,
.mvs-lightbox-sidebar textarea[placeholder*="comment" i] {
    min-height: 72px;
    line-height: 1.4;
    background-color: var(--mvs-surface);
    color: var(--mvs-text, inherit);
    border-color: var(--mvs-border-light, var(--mvs-border));
}

.mvs-lightbox-sidebar .mvs-comment-form textarea::placeholder,
.mvs-lightbox-sidebar textarea[placeholder*="comment" i]::placeholder {
    color: var(--mvs-text-secondary);
    opacity: 0.85;
}

.mvs-lightbox-comments-heading {
    margin: 0;
    padding: 14px 16px 8px;
    font-size: 13px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: var(--mvs-text-muted, #6b7280);
    border-bottom: 1px solid var(--mvs-border-light, #e5e7eb);
}

.mvs-lightbox-comment-list {
    flex: 1;
    overflow-y: auto;
    padding: 8px 0;
    margin: 0;
    list-style: none;
}

.mvs-lightbox-comment {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--mvs-border-light, #e5e7eb);
}

.mvs-lightbox-comment:last-child {
    border-bottom: none;
}

.mvs-lightbox-comment-avatar-link {
    flex-shrink: 0;
    display: block;
    text-decoration: none;
    line-height: 0;
}

.mvs-lightbox-comment-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    object-fit: cover;
    display: block;
}

.mvs-lightbox-comment-body {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.mvs-lightbox-comment-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px;
    flex-wrap: wrap;
}

.mvs-lightbox-comment-author-link {
    text-decoration: none;
    color: inherit;
}

.mvs-lightbox-comment-author-link:hover .mvs-lightbox-comment-author {
    text-decoration: underline;
}

.mvs-lightbox-comment-author {
    font-weight: 600;
    font-size: 14px;
    color: var(--mvs-text, #111827);
}

.mvs-lightbox-comment-time {
    font-size: 12px;
    color: var(--mvs-text-muted, #6b7280);
    white-space: nowrap;
    flex-shrink: 0;
}

.mvs-lightbox-comment-content {
    margin: 0;
    font-size: 14px;
    line-height: 1.5;
    color: var(--mvs-text, #111827);
    word-wrap: break-word;
    overflow-wrap: anywhere;
}

/* Edit/delete controls under a lightbox comment. Edit-mode Save/Cancel reuse
   .mvs-btn; the own-comment Edit/Delete are micro icon buttons (below). */
.mvs-lightbox-comment-actions,
.mvs-lightbox-comment-edit-actions {
    display: flex;
    gap: var(--mvs-space-2, 0.5rem);
    margin-block-start: var(--mvs-space-2, 0.5rem);
}

/* Micro icon buttons: a small pencil/trash on a member's own comment, not the
   large filled .mvs-btn. Transparent until hover so they read as icons, with a
   comfortable 28px hit area around the 14px glyph. */
.mvs-lightbox-comment-action {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    inline-size: 24px;
    block-size: 24px;
    /* The 24px box is deliberate and stays. The TARGET grows instead, via the
       ::after below — see the note there. */
    position: relative;
    padding: 0;
    border: none;
    background: transparent;
    color: var(--mvs-text-muted, #64748b);
    border-radius: var(--mvs-radius-sm, 6px);
    cursor: pointer;
    transition: background-color 0.15s ease, color 0.15s ease;
}

/*
 * Hit area, not icon size.
 *
 * These measured 24x24 against a 40px floor, but the 24px glyph is a
 * deliberate design choice for this row and enlarging it would rebuild the
 * comment meta line. So the target grows and the button does not.
 *
 * 40x44, not 44x44: the siblings sit 8.5px apart, so a 44px-wide area would
 * overlap its neighbour by ~11px and the later button in the DOM would win
 * every click in the overlap — silently turning "edit" into "delete" near the
 * boundary. 40 wide leaves 0.5px between areas and still clears the floor.
 * Basecamp 10252222115.
 */
.mvs-lightbox-comment-action::after {
    content: '';
    position: absolute;
    inset-block-start: 50%;
    inset-inline-start: 50%;
    translate: -50% -50%;
    inline-size: 40px;
    block-size: var(--mvs-touch-min, 44px);
}

.mvs-lightbox-comment-action:hover,
.mvs-lightbox-comment-action:focus-visible {
    background: var(--mvs-surface-2, rgba(100, 116, 139, 0.12));
    color: var(--mvs-text, #0f172a);
}

.mvs-lightbox-comment-action--danger:hover,
.mvs-lightbox-comment-action--danger:focus-visible {
    color: var(--mvs-danger, #dc2626);
}

.mvs-lightbox-comment-action svg {
    inline-size: 14px;
    block-size: 14px;
}

/* The edit/delete icons line up on the right, under the timestamp, rather
   than floating on the left under the comment text. */
.mvs-lightbox-comment-actions {
    justify-content: flex-end;
    margin-block-start: 0;
}

.mvs-lightbox-comment-edit {
    margin-block-start: var(--mvs-space-2, 0.5rem);
}

.mvs-lightbox-comment-edit-input {
    width: 100%;
    font: inherit;
    padding: var(--mvs-space-2, 0.5rem);
    border: 1px solid var(--mvs-border);
    border-radius: var(--mvs-radius-md, 8px);
    background: var(--mvs-bg);
    color: var(--mvs-text);
    resize: vertical;
}

.mvs-lightbox-no-comments {
    color: var(--mvs-text-muted, #6b7280);
    font-size: 14px;
    text-align: center;
    padding: 32px 16px;
    margin: 0;
}

.mvs-lightbox-view-all-comments {
    display: block;
    text-align: center;
    padding: 12px 16px;
    font-size: 13px;
    font-weight: 600;
    color: var(--mvs-primary-text, var(--mvs-primary));
    text-decoration: none;
    border-top: 1px solid var(--mvs-border-light, #e5e7eb);
}

.mvs-lightbox-view-all-comments:hover {
    background: var(--mvs-bg-muted, #f9fafb);
}

.mvs-lightbox-comment-form {
    display: flex;
    gap: 8px;
    align-items: center;
    /* Composer sits at the top of the thread (Twitter/LinkedIn), so the
       divider goes below it, above the comment list. */
    border-block-end: 1px solid var(--mvs-border-light, #e5e7eb);
    padding: 12px 16px;
    /* Was #fff — the lightbox sidebar is var(--mvs-surface) (dark in dark mode)
       and the input text is var(--mvs-text) (light), so a white bar made the
       typed text invisible. Match the sidebar surface. */
    background: var(--mvs-surface);
}

/*
 * The logged-OUT half of the composer slot.
 *
 * `.mvs-lightbox-comment-form` above and this prompt are the two branches of
 * the same row — one renders for a member, one for a visitor — but only the
 * member branch had a rule. The visitor got a bare `<p>`: no padding, so the
 * text sat flush against the sidebar edge while every other section was inset
 * 16px, and the theme's paragraph font-size, so it rendered larger than the
 * heading it sat under.
 *
 * Same box as the composer, deliberately: padding, divider and surface are
 * copied so the slot occupies identical space whichever branch renders and the
 * sidebar does not reflow between logged-in and logged-out.
 */
.mvs-lightbox-login-prompt {
    margin: 0;
    padding: 12px 16px;
    border-block-end: 1px solid var(--mvs-border-light, #e5e7eb);
    background: var(--mvs-surface);
    font-size: 14px;
    line-height: 1.4;
}

.mvs-lightbox-login-prompt a {
    color: var(--mvs-primary-text, var(--mvs-primary));
    text-decoration: none;
}

.mvs-lightbox-login-prompt a:hover,
.mvs-lightbox-login-prompt a:focus-visible {
    text-decoration: underline;
}

.mvs-lightbox-comment-input {
    flex: 1;
    /* Explicit surface + text tokens (not the theme's input styling) so the
       field is a defined rounded control that stays readable in dark mode. */
    border: 1px solid var(--mvs-border, #d1d5db);
    border-radius: 999px;
    padding: 9px 14px;
    font-size: 14px;
    outline: none;
    background: var(--mvs-surface);
    color: var(--mvs-text, inherit);
    min-width: 0;
}

.mvs-lightbox-comment-input:focus {
    border-color: var(--mvs-primary, #E35656);
}

.mvs-lightbox-comment-input:focus-visible {
    outline: 2px solid var(--mvs-primary, #E35656);
    outline-offset: -2px;
}

.mvs-lightbox-comment-post {
    flex-shrink: 0;
    min-block-size: var(--mvs-touch-min, 44px);
    background: var(--mvs-primary, #E35656);
    border: none;
    color: var(--mvs-btn-primary-fg, #ffffff);
    font-weight: 600;
    padding: 9px 18px;
    border-radius: 999px;
    cursor: pointer;
    font-size: 14px;
    transition: filter 0.15s ease, opacity 0.15s ease;
}

.mvs-lightbox-comment-post:hover:not(:disabled) {
    filter: brightness(0.93);
}

/* Faded primary while empty, not the theme's grey disabled bg. */
.mvs-lightbox-comment-post:disabled {
    opacity: 0.45;
    cursor: default;
}

.mvs-lightbox-comment-post:disabled {
    background: var(--mvs-border-light, #e5e7eb);
    color: var(--mvs-text-muted, #6b7280);
    cursor: default;
}

.mvs-lightbox-comment-post:not(:disabled):hover {
    filter: brightness(0.92);
    /* Must track the base state's token, not a frozen white: the base rule
       reads --mvs-btn-primary-fg (auto-contrast), so a hardcoded #ffffff here
       made the label flip to white on hover on a light brand colour that had
       correctly resolved to dark text at rest. */
    color: var(--mvs-btn-primary-fg, #ffffff);
}

@media (max-width: 767px) /* below --mvs-bp-tablet */ {
    .mvs-lightbox {
        flex-direction: column;
        width: 100vw;
        height: 100vh;
        height: 100dvh;
        border-radius: 0;
    }

    .mvs-lightbox-media {
        flex: 1;
        min-height: 0;
    }

    .mvs-lightbox-sidebar {
        width: 100%;
        flex: 0 0 45%;
        border-left: none;
        border-top: 1px solid var(--mvs-border-light);
    }
}
/* Replace-file row inside edit modal */
.mvs-replace-file-row {
    padding: 0 24px 12px;
    border-top: 1px solid var(--mvs-border-light);
    margin-top: 12px;
    padding-top: 12px;
}

.mvs-replace-file-label {
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}

.mvs-replace-file-hint {
    color: var(--mvs-text-secondary);
    font-size: 12px;
    margin-left: 8px;
}

/* -----------------------------------------------------------------------
   Auth gate — premium empty-state shown on plugin pages that require login
   (e.g. /my-media/ dashboard shortcode while logged out).
   ----------------------------------------------------------------------- */
.mvs-auth-gate {
    display: flex;
    justify-content: center;
    padding: clamp(32px, 8vw, 96px) 16px;
}

.mvs-auth-gate__card {
    position: relative;
    width: 100%;
    max-width: 560px;
    padding: clamp(32px, 5vw, 56px) clamp(24px, 4vw, 48px);
    background: var(--mvs-surface, #fff);
    border-radius: var(--mvs-radius-lg, 16px);
    box-shadow:
        0 1px 2px rgba(15, 23, 42, 0.04),
        0 18px 48px -12px rgba(15, 23, 42, 0.18);
    text-align: center;
    overflow: hidden;
    color: var(--mvs-primary-text, var(--mvs-primary));
    box-sizing: border-box;
}

.mvs-auth-gate__card::before {
    content: "";
    position: absolute;
    inset: 0 0 auto 0;
    height: 4px;
    background: linear-gradient(90deg,
        var(--mvs-primary, #0073aa) 0%,
        #6a5acd 50%,
        #ff6aa6 100%);
}

.mvs-auth-gate__card::after {
    content: "";
    position: absolute;
    width: 280px;
    height: 280px;
    right: -120px;
    top: -140px;
    background: radial-gradient(circle,
        rgba(var(--mvs-primary-rgb, 0, 115, 170), 0.08) 0%,
        transparent 70%);
    pointer-events: none;
    z-index: 0;
}

.mvs-auth-gate__glyph {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 88px;
    height: 88px;
    margin: 0 auto 20px;
    border-radius: 50%;
    background: rgba(var(--mvs-primary-rgb, 0, 115, 170), 0.08);
    color: var(--mvs-primary-text, var(--mvs-primary));
    z-index: 1;
}

.mvs-auth-gate__glyph svg {
    width: 44px;
    height: 44px;
    stroke-width: 1.75;
}

.mvs-auth-gate__title {
    position: relative;
    margin: 0 0 12px;
    font-family: var(--mvs-font-heading, inherit);
    font-size: clamp(22px, 2.6vw, 28px);
    font-weight: 700;
    line-height: 1.25;
    color: var(--mvs-text, #1e1e1e);
    z-index: 1;
}

.mvs-auth-gate__lede {
    position: relative;
    margin: 0 auto 28px;
    max-width: 420px;
    font-size: 15px;
    line-height: 1.55;
    color: var(--mvs-text-secondary, #646970);
    z-index: 1;
}

.mvs-auth-gate__benefits {
    position: relative;
    list-style: none;
    padding: 0;
    margin: 0 0 32px;
    text-align: left;
    display: grid;
    gap: 12px;
    z-index: 1;
}

.mvs-auth-gate__benefits li {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 14px;
    background: var(--mvs-surface-2, #f6f7f7);
    border-radius: var(--mvs-radius-md, 10px);
    font-size: 14px;
    color: var(--mvs-text, #1e1e1e);
    line-height: 1.4;
}

.mvs-auth-gate__benefit-icon {
    flex: 0 0 36px;
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    background: var(--mvs-surface, #fff);
    border-radius: 50%;
    color: var(--mvs-primary-text, var(--mvs-primary));
    line-height: 1;
    box-shadow: 0 1px 2px rgba(15, 23, 42, 0.06);
}

.mvs-auth-gate__benefit-icon svg {
    width: 18px;
    height: 18px;
    stroke-width: 2;
}

.mvs-auth-gate__actions {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    z-index: 1;
}

.mvs-auth-gate__primary {
    min-width: 240px;
    padding: 12px 28px;
    font-size: 15px;
    font-weight: 600;
    letter-spacing: 0.01em;
}

.mvs-auth-gate__secondary {
    font-size: 14px;
    /* --mvs-text-secondary measured 3.52:1 on the gate's own background, under
       the 4.5 floor for text, and the link was 18.2px tall against a 40px
       contract - the earlier pass fixed the PRIMARY call to action on this gate
       and stopped there (QA re-verify, Basecamp 10252222150).
       Body text colour carries the contrast; inline-flex + min-height give the
       control a real hit area without turning it into a button. */
    color: var(--mvs-text, #1e1e1e);
    display: inline-flex;
    align-items: center;
    min-height: var(--mvs-touch-min, 40px);
    text-decoration: none;
    transition: color 150ms ease;
}

.mvs-auth-gate__secondary:hover,
.mvs-auth-gate__secondary:focus-visible {
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-auth-gate__secondary strong {
    color: var(--mvs-primary-text, var(--mvs-primary));
    font-weight: 600;
    text-decoration: underline;
    text-underline-offset: 2px;
}

@media (max-width: 640px) {
    .mvs-auth-gate {
        padding: 24px 12px 48px;
    }
    .mvs-auth-gate__card {
        padding: 28px 20px 32px;
        box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04), 0 8px 24px -8px rgba(15, 23, 42, 0.15);
    }
    .mvs-auth-gate__glyph {
        width: 72px;
        height: 72px;
        margin-bottom: 16px;
    }
    .mvs-auth-gate__glyph svg {
        width: 44px;
        height: 44px;
    }
    .mvs-auth-gate__primary {
        width: 100%;
        min-width: 0;
    }
}

/* Dark-mode auth-gate card. The earlier `prefers-color-scheme` media query
   here was removed (per Jetonomy 2026-04-29 lesson — theme-class is the
   single source of truth for dark mode; OS-level pref races with the theme
   toggle and produces inconsistent state when a user is on a light theme
   with a dark OS or vice versa). Hooks the same dark-mode selector group
   defined at the top of the file so any theme that flips dark mode via
   class also flips this card. */
[data-theme="dark"] .mvs-auth-gate__card,
[data-bx-mode="dark"] .mvs-auth-gate__card,
html.dark-mode .mvs-auth-gate__card,
body.dark-mode .mvs-auth-gate__card {
    background: var(--mvs-surface, #1e1e1e);
    box-shadow:
        0 1px 2px rgba(0, 0, 0, 0.2),
        0 18px 48px -12px rgba(0, 0, 0, 0.55);
}

/* Album cover hint inside FAB upload modal (album mode, files selected). */
.mvs-modal-album-cover-hint {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    margin-top: 8px;
    background: var(--mvs-surface-2, #f5f5f5);
    border-left: 3px solid var(--mvs-primary, #2271b1);
    border-radius: 4px;
    font-size: 13px;
    color: var(--mvs-text-secondary, #555);
}
.mvs-modal-album-cover-hint[hidden] { display: none; }
.mvs-modal-album-cover-hint svg {
    flex-shrink: 0;
    color: var(--mvs-primary-text, var(--mvs-primary));
}
@media (max-width: 640px) {
    .mvs-modal-album-cover-hint { font-size: 12px; padding: 6px 10px; }
}

/* ============================================================================
   Single album page — cover selection overlay (owner only).
   Renders a "Set as cover" button on each image tile and a "Cover" badge on
   the current cover. Pairs with templates/album.php inline JS that PUTs to
   /albums/{id}/cover. set_cover() is atomic (adds the media to the album in
   the same request if not already there), so no client-side ordering needed.
   ============================================================================ */
.mvs-album-item-wrap {
    position: relative;
    display: block;
}

.mvs-album-cover-badge {
    position: absolute;
    top: 8px;
    left: 8px;
    z-index: 2;
    padding: 4px 10px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--mvs-surface, #fff);
    background: var(--mvs-primary, #2271b1);
    border-radius: 999px;
    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
    pointer-events: none;
}

.mvs-album-set-cover {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 6px 10px;
    font-size: 12px;
    font-weight: 500;
    color: var(--mvs-surface, #fff);
    background: rgba(17, 17, 17, 0.72);
    border: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 999px;
    cursor: pointer;
    opacity: 0;
    transform: translateY(-4px);
    transition: opacity 0.15s ease, transform 0.15s ease, background 0.15s ease;
}

.mvs-album-item-wrap:hover .mvs-album-set-cover,
.mvs-album-set-cover:focus-visible {
    opacity: 1;
    transform: translateY(0);
}

.mvs-album-set-cover:hover,
.mvs-album-set-cover:focus-visible {
    background: var(--mvs-primary, #2271b1);
    border-color: var(--mvs-primary, #2271b1);
    outline: none;
}

.mvs-album-set-cover[disabled] {
    opacity: 0.7;
    cursor: progress;
}

.mvs-album-set-cover .mvs-icon [data-lucide],
.mvs-album-set-cover .mvs-icon svg {
    width: 14px;
    height: 14px;
    line-height: 1;
}

@media (max-width: 640px) {
    .mvs-album-set-cover {
        /* Mobile: always visible (no hover). */
        opacity: 1;
        transform: none;
        padding: 4px 8px;
        font-size: 11px;
    }
    .mvs-album-cover-badge {
        padding: 3px 8px;
        font-size: 10px;
    }
}

/* ==========================================================================
   Usage history (mvs_transactions ledger — [mvs_usage_history] / dashboard)
   ========================================================================== */

.mvs-usage-history {
    background: var(--mvs-surface);
    border: 1px solid var(--mvs-border-light);
    border-radius: var(--mvs-radius-lg);
    padding: var(--mvs-space-4);
    color: var(--mvs-text);
}

.mvs-usage-history__title {
    margin: 0 0 var(--mvs-space-3);
    font-size: 1.125rem;
    color: var(--mvs-text);
}

.mvs-usage-history__empty {
    text-align: center;
    padding: var(--mvs-space-5) var(--mvs-space-3);
    color: var(--mvs-text-muted);
}

.mvs-usage-history__empty p {
    margin: 0 0 var(--mvs-space-1);
}

.mvs-usage-history__table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.9375rem;
}

.mvs-usage-history__table th,
.mvs-usage-history__table td {
    text-align: start;
    padding: var(--mvs-space-2) var(--mvs-space-3);
    border-bottom: 1px solid var(--mvs-border-light);
}

.mvs-usage-history__table th {
    color: var(--mvs-text-secondary);
    font-weight: 600;
}

.mvs-usage-history__type {
    display: inline-block;
    padding: 2px 10px;
    border-radius: var(--mvs-radius-pill);
    font-size: 0.8125rem;
    background: var(--mvs-info-bg);
    color: var(--mvs-text);
}

.mvs-usage-history__delta {
    font-variant-numeric: tabular-nums;
    font-weight: 600;
}

.mvs-usage-history__delta--up {
    color: var(--mvs-success);
}

.mvs-usage-history__delta--down {
    color: var(--mvs-danger);
}

.mvs-usage-history__more {
    margin: var(--mvs-space-3) 0 0;
    color: var(--mvs-text-muted);
    font-size: 0.8125rem;
}


/* Blocked members (Edit Profile) — manage/undo blocks. */
.mvs-blocked-members { margin-top: var(--mvs-space-xl, 24px); }
.mvs-blocked-members__title { font-size: var(--mvs-font-lg, 18px); font-weight: 700; margin: 0 0 var(--mvs-space-md, 12px); }
.mvs-blocked-members__empty { color: var(--mvs-text-secondary, #6b7280); font-size: var(--mvs-font-sm, 14px); }
.mvs-blocked-members__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--mvs-space-sm, 8px); }
.mvs-blocked-members__row { display: flex; align-items: center; gap: var(--mvs-space-md, 12px); padding: var(--mvs-space-sm, 8px) var(--mvs-space-md, 12px); border: 1px solid var(--mvs-border, #e5e7eb); border-radius: var(--mvs-radius-md, 8px); }
.mvs-blocked-members__avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.mvs-blocked-members__name { font-weight: 600; }
.mvs-blocked-members__unblock { margin-inline-start: auto; }

/* Profile actions — overflow (⋯) menu + report dialog. */
.mvs-actions-overflow { position: relative; display: inline-block; }
.mvs-actions-menu { position: absolute; inset-inline-end: 0; inset-block-start: calc(100% + 4px); min-inline-size: 160px; background: var(--mvs-bg, #fff); border: 1px solid var(--mvs-border, #e5e7eb); border-radius: var(--mvs-radius-md, 8px); box-shadow: 0 6px 24px rgba(0, 0, 0, 0.12); padding: 4px; z-index: 100; display: flex; flex-direction: column; }
.mvs-actions-menu[hidden] { display: none; }
.mvs-actions-menu__item { display: block; width: 100%; text-align: start; padding: 8px 12px;
    box-sizing: border-box; background: none; border: 0; border-radius: var(--mvs-radius-sm, 4px); cursor: pointer; font-size: var(--mvs-font-sm, 14px); color: var(--mvs-text, #111827); }
.mvs-actions-menu__item:hover, .mvs-actions-menu__item:focus-visible { background: var(--mvs-bg-muted, #f3f4f6); }
.mvs-actions-menu .mvs-block-toggle--blocked { color: var(--mvs-danger, #dc2626); }
/* Report dialog reuses the .mvs-modal component; ensure controls fill the field. */
.mvs-report-reason, .mvs-report-details { width: 100%; }

/* Followers / Following modal (reuses .mvs-modal) + clickable member stats. */
.mvs-member-photos-card__stat--clickable { cursor: pointer; border-radius: var(--mvs-radius-sm, 4px); }
.mvs-member-photos-card__stat--clickable:hover, .mvs-member-photos-card__stat--clickable:focus-visible { background: var(--mvs-bg-muted, #f3f4f6); }
.mvs-follows-tabs { display: flex; gap: var(--mvs-space-md, 12px); }
.mvs-follows-tab { background: none; border: 0; padding: 4px 2px; font-size: var(--mvs-font-md, 16px); font-weight: 600; color: var(--mvs-text-secondary, #6b7280); cursor: pointer; border-bottom: 2px solid transparent; }
.mvs-follows-tab.is-active { color: var(--mvs-text, #111827); border-bottom-color: var(--mvs-accent, #7c3aed); }
.mvs-follows-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--mvs-space-sm, 8px); }
.mvs-follows-row { display: flex; align-items: center; gap: var(--mvs-space-md, 12px); }
.mvs-follows-row__avatar { width: 40px; height: 40px; border-radius: 50%; object-fit: cover; flex-shrink: 0; }
.mvs-follows-row__name { font-weight: 600; }
.mvs-follows-row .mvs-follows-follow { margin-inline-start: auto; }
.mvs-follows-state { text-align: center; color: var(--mvs-text-secondary, #6b7280); padding: var(--mvs-space-xl, 24px) 0; }

/* Clickable follower/following counts in the profile header. */
.mvs-profile-header-stats__link { background: none; border: 0; margin: 0; font: inherit; color: inherit; cursor: pointer; }
.mvs-profile-header-stats__link:hover, .mvs-profile-header-stats__link:focus-visible { text-decoration: underline; }

/* Stat buttons (clickable follower/following counts) reset to look like text.
   They still have to be hittable: padding:0 left them 25px tall against the
   plugin's own --mvs-touch-min of 44px, which is a plausible reason a tester
   reported them as "not clickable" - the control works, it is just a small
   target. Vertical padding rather than a min-height so the text stays inline
   with the rest of the stat row. Basecamp 10277961221. */
button.mvs-follows-open,
.mvs-profile-header-stats__link {
    background: none;
    border: 0;
    font: inherit;
    color: inherit;
    cursor: pointer;
    padding-block: calc( ( var(--mvs-touch-min, 44px) - 1.5em ) / 2 );
    padding-inline: var(--mvs-space-xs, 6px);
    margin: 0;
}

/* Tag-cloud dynamic items wrapper — display:contents keeps the flat flex row while
   making the data-wp-each <template> the sole child of its parent (avoids an
   Interactivity API hydration mismatch from a static sibling). */
.mvs-tag-cloud-items { display: contents; }

/* ---------------------------------------------------------------------------
   Access gate — shown inside .mvs-media-content on the single media page when
   the viewer is not permitted to see protected media (privacy-denied). Keeps
   the same page container; only the media slot is replaced. Token-driven so it
   adapts to dark mode automatically.
   --------------------------------------------------------------------------- */
.mvs-media-gate {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: center;
	text-align: center;
	gap: 0.75rem;
	min-height: 320px;
	padding: 3rem 1.5rem;
	background: var(--mvs-surface-2);
	border: 1px solid var(--mvs-surface-2);
	border-radius: var(--mvs-radius-lg);
}
.mvs-media-gate__glyph {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	width: 64px;
	height: 64px;
	border-radius: 50%;
	background: var(--mvs-surface);
	color: var(--mvs-text-secondary);
}
.mvs-media-gate__glyph svg {
	width: 28px;
	height: 28px;
}
.mvs-media-gate__title {
	margin: 0;
	font-family: var(--mvs-font-heading);
	font-size: 1.25rem;
	font-weight: 600;
	color: var(--mvs-text);
}
.mvs-media-gate__lede {
	margin: 0;
	max-width: 42ch;
	color: var(--mvs-text-secondary);
}
.mvs-media-gate__cta {
	margin-top: 0.5rem;
}

/* ---------------------------------------------------------------------------
 * Document listing — /explore-document
 *
 * ROWS, not tiles. A grid of identical PDF icons carries no information, and a
 * media grid asked to draw a PDF produces a broken tile. A row shows what
 * actually distinguishes one document from another.
 * ------------------------------------------------------------------------- */
.mvs-documents__header {
	display: flex;
	align-items: baseline;
	gap: var(--mvs-space-sm, 8px);
	margin-block-end: var(--mvs-space-lg, 24px);
}

.mvs-documents__count {
	color: var(--mvs-text-muted, #646970);
	margin: 0;
}

.mvs-documents__list {
	list-style: none;
	margin: 0;
	padding: 0;
	border: 1px solid var(--mvs-border-light, #dcdcde);
	border-radius: var(--mvs-radius-md, 8px);
	overflow: hidden;
}

.mvs-documents__row {
	display: grid;
	/* The four metadata tracks are FIXED, not auto.
	 *
	 * With `auto auto auto auto` each track sized to its own row's content, so
	 * every row laid out independently and the columns never lined up - the
	 * date column started at 1147 / 1165 / 1171 / 1171 across four rows, 24px
	 * of drift, and worse on a real site where usernames and file sizes vary
	 * more than a demo's (Basecamp 10253006024). A column has to be the same
	 * width in every row or it is not a column. */
	/* Chip column widened 4rem -> 6.5rem: the longest type labels (PowerPoint,
	   Markdown, ODF Slides/Sheet/Text - 5 of the 11 in DocumentTypes) overran
	   4rem and overlapped the next column. Basecamp 10268100818. */
	grid-template-columns: auto minmax(0, 1fr) 6.5rem 6rem 4.5rem 7rem;
	align-items: center;
	gap: var(--mvs-space-md, 16px);
	padding: var(--mvs-space-md, 16px);
	border-block-end: 1px solid var(--mvs-border-light, #dcdcde);
	background: var(--mvs-surface, #fff);
}

.mvs-documents__row:last-child {
	border-block-end: 0;
}

.mvs-documents__icon {
	display: inline-flex;
	color: var(--mvs-text-muted, #646970);
}

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

.mvs-documents__name {
	font-weight: 600;
	text-decoration: none;
	color: var(--mvs-text, #1d2327);
	/* 40px tap target on touch without stretching the row on desktop. */
	padding-block: 4px;
}

.mvs-documents__name:hover,
.mvs-documents__name:focus-visible {
	text-decoration: underline;
}

.mvs-documents__excerpt,
.mvs-documents__owner,
.mvs-documents__size,
.mvs-documents__date {
	color: var(--mvs-text-muted, #646970);
	font-size: var(--mvs-font-sm, 0.875rem);
}

/* Reserve a slot per metadata field so the columns line up down the list.
 *
 * The row laid its metadata out as a bare flex run pushed right, so every row
 * sized itself independently and each field started wherever the previous
 * one happened to end. Measured across four rows at 1440px: the date column
 * started at 1147 / 1165 / 1171 / 1171 - 24px of drift on a four-row list, and
 * a row with no type chip shifted owner, size and date left into the wrong
 * slots entirely (Basecamp 10253006024).
 *
 * Four rows is the BEST case: the drift is driven by the rendered width of the
 * owner name and the size string, so a real site with mixed usernames and file
 * sizes gets visibly worse. Fixed widths make a row's layout independent of its
 * own content, which is the property a column needs.
 *
 * text-align differs per field on purpose: a size reads right-aligned because
 * the eye compares magnitudes, a name reads left-aligned because it is a word. */
/* Each cell is pinned to ITS OWN track.
 *
 * Without this, a row whose document has no type chip renders five children
 * instead of six, and auto-placement slides owner, size and date one track to
 * the left - so that row's metadata sat under the wrong headings entirely.
 * Explicit placement leaves the chip track empty instead of collapsing it,
 * which is the only way to keep a column stable when a cell is optional.
 *
 * (An earlier attempt used `flex: 0 0 <width>` here. Flex properties are
 * ignored on grid items, so it changed nothing - the row has been display:grid
 * all along.) */
.mvs-documents__icon  { grid-column: 1; }
.mvs-documents__main  { grid-column: 2; }
.mvs-documents__chip  { grid-column: 3; }

.mvs-documents__owner {
	grid-column: 4;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.mvs-documents__size {
	grid-column: 5;
	text-align: end;
	/* Lining figures so sizes compare down the column. */
	font-variant-numeric: tabular-nums;
}

.mvs-documents__date {
	grid-column: 6;
	text-align: end;
	white-space: nowrap;
}

.mvs-documents__excerpt {
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

.mvs-documents__chip {
	display: inline-flex;
	align-items: center;
	/* Containment only. The ellipsis lives on the inner span: this element is
	   display:inline-flex, and text-overflow does not apply to the anonymous
	   flex item a bare text node becomes - so declaring it here did nothing but
	   look reassuring. Same trap as .mvs-lightbox-action. Basecamp 10290723291. */
	min-width: 0;
	max-width: 100%;
	overflow: hidden;
	padding: 2px var(--mvs-space-sm, 8px);
	border-radius: var(--mvs-radius-pill, 999px);
	background: var(--mvs-surface-2, #f0f0f1);
	color: var(--mvs-text-muted, #646970);
	font-size: var(--mvs-font-xs, 0.75rem);
	font-weight: 600;
	letter-spacing: 0.02em;
}

/* The label owns the truncation - a block box is what text-overflow needs. */
.mvs-documents__chip-label {
	display: block;
	max-width: 100%;
	overflow: hidden;
	text-overflow: ellipsis;
	white-space: nowrap;
}

/*
 * Document type colours, owned by Free.
 *
 * They lived only in Pro's document-drive.css, so My Drive showed a red PDF and
 * a blue folder while Explore Documents — a FREE template — rendered every type
 * in the same neutral. Free cannot read a Pro stylesheet, so the palette moves
 * down here beside `DocumentTypes`, which is Free's vocabulary too, and Pro's
 * drive chips consume the same tokens (Basecamp 10263186054).
 *
 * Text variants, not the base semantic colours: these are labels on a tint, and
 * `--mvs-danger` / `--mvs-success` are tuned for use as a fill (10253005724).
 */
.mvs-documents__chip--folder,
.mvs-documents__chip--word,
.mvs-documents__chip--odf_text {
	background: var(--mvs-info-bg);
	color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-documents__chip--pdf {
	background: var(--mvs-danger-bg);
	color: var(--mvs-danger-text, var(--mvs-danger));
}

.mvs-documents__chip--excel,
.mvs-documents__chip--csv,
.mvs-documents__chip--odf_sheet {
	background: var(--mvs-success-bg);
	color: var(--mvs-success-text, var(--mvs-success));
}

.mvs-documents__chip--powerpoint,
.mvs-documents__chip--odf_presentation {
	background: var(--mvs-warning-bg);
	color: var(--mvs-warning);
}

.mvs-documents__pagination {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: var(--mvs-space-md, 16px);
	margin-block-start: var(--mvs-space-lg, 24px);
}

/* Under 640px the metadata columns stop earning their width, so the row stops
 * being a grid entirely.
 *
 * It cannot stay a grid with the meta cells sharing one area: four siblings
 * assigned to the same grid-area STACK on top of each other, which is exactly
 * what the 390px screenshot showed — "PDF", the owner, the size and the date
 * all overprinting in one box. Flex-wrap flows them instead. */
@media (max-width: 640px) {
	.mvs-documents__row {
		display: flex;
		flex-wrap: wrap;
		align-items: flex-start;
		column-gap: var(--mvs-space-sm, 8px);
		row-gap: 4px;
	}

	.mvs-documents__main {
		/* Take the rest of the first line, pushing the meta to the next. */
		flex: 1 1 60%;
	}

	.mvs-documents__excerpt {
		white-space: normal;
	}

	/* The row is the whole visual hit area on a phone, but the LINK is what a
	 * finger aims at, and at desktop line-height it lands at 30px. Raise it past
	 * the 40px floor rather than relying on the row's padding, which is not
	 * clickable. */
	.mvs-documents__name {
		min-height: 40px;
		display: flex;
		align-items: center;
	}

	.mvs-documents__chip,
	.mvs-documents__owner,
	.mvs-documents__size,
	.mvs-documents__date {
		flex: 0 0 auto;
	}

	/* The meta line starts under the icon, not beside it. */
	.mvs-documents__chip {
		margin-inline-start: calc(20px + var(--mvs-space-md, 16px));
	}
}

/* ============================================================================
 * Document card + download button.
 *
 * Free renders this card on the single-document page when Pro is absent, and
 * Pro's viewer reuses the same class names for its tier-3/4 card — one card,
 * one stylesheet, so the two cannot drift apart.
 *
 * The button is styled here rather than reusing `.mvs-btn-primary` because that
 * class is only ever defined inside a `#buddypress` scope: outside BuddyPress it
 * matches nothing and the download rendered as bare text. All four link states
 * are set explicitly — a theme's `a:visited` will happily repaint an anchor
 * button that only declared its resting colour.
 *
 * @since 2.4.0
 * ========================================================================== */

.mvs-media-document {
    width: 100%;
}

.mvs-doc-card {
    display: flex;
    align-items: center;
    gap: var(--mvs-space-3);
    padding: var(--mvs-space-4);
    border: 1px solid var(--mvs-border-light);
    border-radius: var(--mvs-radius-md);
    background: var(--mvs-surface);
}

.mvs-doc-card__icon {
    flex: 0 0 auto;
    width: 2.5rem;
    height: 2.5rem;
    border-radius: var(--mvs-radius-sm);
    background: var(--mvs-bg-muted);
}

.mvs-doc-card__meta {
    display: flex;
    flex-direction: column;
    gap: var(--mvs-space-1);
    min-width: 0;
}

.mvs-doc-card__name {
    overflow-wrap: anywhere;
    color: var(--mvs-text);
    font-weight: 600;
}

.mvs-doc-card__detail {
    color: var(--mvs-text-muted);
    font-size: var(--mvs-font-sm);
}

.mvs-doc-download,
.mvs-doc-download:link,
.mvs-doc-download:visited {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    align-self: flex-start;
    gap: var(--mvs-space-2);
    /* A download is the primary action on this page and has to clear the
       minimum tap target on a phone. */
    min-height: var(--mvs-touch-min);
    padding: var(--mvs-space-2) var(--mvs-space-4);
    border-radius: var(--mvs-radius-md);
    background: var(--mvs-primary);
    color: var(--mvs-on-primary);
    font-size: var(--mvs-font-md);
    font-weight: 600;
    text-decoration: none;
    transition: background var(--mvs-duration-fast) var(--mvs-easing);
}

.mvs-doc-download:hover,
.mvs-doc-download:focus-visible {
    background: var(--mvs-primary-hover);
    color: var(--mvs-on-primary);
    text-decoration: none;
}

.mvs-doc-download:focus-visible {
    outline: 2px solid var(--mvs-primary);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {

    .mvs-doc-download {
        transition: none;
    }
}

@media (max-width: 640px) {

    .mvs-doc-card {
        /* Icon and text stay side by side; the name wraps to its own line only
           when it genuinely cannot fit. */
        flex-wrap: wrap;
    }

    .mvs-doc-download {
        align-self: stretch;
    }
}

/* A registered dashboard tab is an anchor, not a button — its content lives on
   another page. It has to look and size identically to the button tabs beside
   it, or the row reads as two different kinds of control. */
.mvs-dashboard-tab--link {
    display: inline-flex;
    align-items: center;
    text-decoration: none;
}

.mvs-dashboard-tab--link:hover,
.mvs-dashboard-tab--link:focus-visible {
    text-decoration: none;
}


/* ============================================================================
 * Document page tabs — Public / My Drive / Shared with me / Recent.
 *
 * Free renders this nav, so Free styles it. It previously lived in Pro's drive
 * stylesheet, which is only enqueued when the DRIVE renders — so on the Public
 * tab, and for every logged-out visitor, the nav degraded to a bullet list.
 * Markup and its CSS belong to the same plugin (Coding Rule 12).
 *
 * @since 2.4.0
 * ========================================================================== */


.mvs-documents__tabs ul {
	display: flex;
	flex-wrap: wrap;
	gap: var(--mvs-space-1);
	margin: 0 0 var(--mvs-space-4);
	padding: 0;
	list-style: none;
	border-bottom: 1px solid var(--mvs-border-light);
}

.mvs-documents__tab {
	display: inline-flex;
	align-items: center;
	min-height: var(--mvs-touch-min);
	padding: 0 var(--mvs-space-4);
	border-bottom: 2px solid transparent;
	color: var(--mvs-text-muted);
	font-weight: 500;
	text-decoration: none;
}

.mvs-documents__tab:hover,
.mvs-documents__tab:focus-visible {
	color: var(--mvs-text);
}

.mvs-documents__tab.is-active {
	border-bottom-color: var(--mvs-primary);
	color: var(--mvs-primary-text, var(--mvs-primary));
}



/* ============================================================================
 * My Media — vertical rail.
 *
 * The dashboard began with four tabs and carries eight. A horizontal row stops
 * working somewhere around six: it wraps to two lines, the wrap point moves
 * with the theme, and nothing groups. A rail with headings makes eight items
 * readable and leaves an obvious place for the ninth.
 *
 * @since 2.4.0
 * ========================================================================== */

/* The grid goes on `.mvs-dashboard__body`, which holds ONLY the rail and the
   panels. Putting it on `.mvs-dashboard` swept the streak bar, the profile
   header and the completion notice into the rail column too — every child of a
   grid is a grid item, including the ones that were never meant to be. */
@media (min-width: 861px) {

    .mvs-dashboard__body {
        display: grid;
        grid-template-columns: 13rem minmax(0, 1fr);
        /* Two rows, and the SECOND one absorbs the slack. Without this both
           rows shared the panel's height, so a tall panel pushed the sections
           box a long way down the page and left the rail looking broken in
           two. The head is auto — as tall as a name and a link — and
           everything left over goes to the sections column. */
        grid-template-rows: auto 1fr;
        gap: var(--mvs-space-5);
        align-items: start;
    }

    /* ONE card for the whole rail. The identity and the sections used to be two
       grid items, each with its own border, radius and fill, separated by the
       grid's --mvs-space-5 row gap - two boxes down the left edge instead of
       one column. The wrapper is the grid item now; it spans both rows, holds
       the card, and takes the sticky that used to live on the nav (sticky on
       the nav alone would have slid the sections out from under the identity
       and torn the card in half on scroll). */
    .mvs-dashboard-rail {
        position: sticky;
        /* Chrome offset + the original gap. The rail is tall, so a sticky
           header only ever buried its top ~66px rather than the whole card —
           less visible than the bulk bar's failure, the same cause.
           --mvs-sticky-top (chrome only): the rail is ours, so it must not
           count toward the value it reads. Basecamp 10320911387. */
        top: calc(var(--mvs-sticky-top, 0px) + var(--mvs-space-4));
        grid-column: 1;
        grid-row: 1 / span 2;
        display: flex;
        flex-direction: column;
        border: 1px solid var(--mvs-border-light);
        border-radius: var(--mvs-radius-md);
        background: var(--mvs-bg-subtle);
    }

    /* Both children go borderless inside it. A hairline separates the identity
       from the sections - one line, not a second box. */
    .mvs-dashboard-rail .mvs-dashboard-rail-head {
        margin-bottom: 0;
        border: 0;
        border-block-end: 1px solid var(--mvs-border-light);
        border-radius: 0;
        background: transparent;
    }

    .mvs-dashboard-tabs {
        display: flex;
        flex-direction: column;
        gap: 2px;
        padding: var(--mvs-space-2);
        border: 0;
        background: transparent;
        /* The base rule is the horizontal tab BAR, which needs 24px under it to
           clear the panel. In the rail the nav is the last thing inside the
           card, so that margin became 24px of empty card below "Edit profile". */
        margin-bottom: 0;
    }

    /* Every panel stacks into the same cell; only the active one is visible,
       so the column is as tall as whichever is showing. Spanning both rail rows
       so the panel starts level with the member's name rather than level with
       the first section below it. */
    .mvs-dashboard-panel {
        grid-column: 2;
        grid-row: 1 / span 2;
        min-width: 0;
    }

    /* `a.` bumps specificity past the base tab rule, which sets a bottom
       border every rail item inherited — a red rule under each row. */
    .mvs-dashboard-tabs a.mvs-dashboard-tab,
    .mvs-dashboard-tabs button.mvs-dashboard-tab {
        justify-content: flex-start;
        gap: var(--mvs-space-2);
        /* The horizontal tab styling puts a border on the bottom of every item,
           which down a rail reads as a rule under each row. All four sides are
           reset here rather than only the active one — that was the red line
           under Favorites and Collections. */
        border: 0;
        border-radius: var(--mvs-radius-sm);
        padding: var(--mvs-space-2) var(--mvs-space-3);
        /* No min-height reset here. The rail's padding alone lands a row at
           ~37px, and this selector outranks the base tab rule - which is why
           the 40px floor added there was inert on desktop (QA re-verify,
           Basecamp 10252222115). The base rule's --mvs-touch-min now applies. */
        color: var(--mvs-text-secondary);
        font-size: var(--mvs-font-sm);
        font-weight: 500;
        line-height: 1.4;
        text-align: start;
    }

    .mvs-dashboard-tabs .mvs-dashboard-tab:hover {
        background: var(--mvs-surface);
        color: var(--mvs-text);
    }

    .mvs-dashboard-tabs .mvs-dashboard-tab:focus-visible {
        outline: 2px solid var(--mvs-primary);
        outline-offset: -2px;
    }

    /* The horizontal tab styling underlines the active item, which on a rail
       reads as a stray rule under one row. The selector carries the rail so it
       outranks the base tab rule. */
    .mvs-dashboard-tabs a.mvs-dashboard-tab.active,
    .mvs-dashboard-tabs button.mvs-dashboard-tab.active {
        /* The fill lives in the !important block above, which out-specifies
           this rule. */
        color: var(--mvs-primary-text, var(--mvs-primary));
        font-weight: 600;
    }
}


.mvs-dashboard-tabs__group {
    padding: var(--mvs-space-3) var(--mvs-space-3) var(--mvs-space-1);
    color: var(--mvs-text-muted);
    font-size: var(--mvs-font-xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

/* A rail item is an anchor now, so it has to be told not to look like body
   copy — themes style `a` inside content aggressively. */
a.mvs-dashboard-tab,
a.mvs-dashboard-tab:hover,
a.mvs-dashboard-tab:focus-visible {
    text-decoration: none;
}

@media (max-width: 860px) {

    /* Below the rail's width the group headings earn nothing — they cost a
       whole row in a horizontal scroller. */
    .mvs-dashboard-tabs__group {
        display: none;
    }
}


/* The count sits at the far end of a rail item, quiet until you look for it. */
.mvs-dashboard-tab__label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mvs-dashboard-tab__count {
    flex: 0 0 auto;
    color: var(--mvs-text-muted);
    font-size: var(--mvs-font-xs);
    font-variant-numeric: tabular-nums;
}

.mvs-dashboard-tab.active .mvs-dashboard-tab__count {
    color: var(--mvs-primary-text, var(--mvs-primary));
}

.mvs-dashboard-tabs__group:first-child {
    padding-top: var(--mvs-space-1);
}

/* ---------------------------------------------------------------------------
   The rail states its own appearance.

   Everything above sets rail styling inside a min-width media query, so below
   that breakpoint — and anywhere the theme's `a` rules are more specific — the
   items fell back to being links: red, underlined, with the horizontal tab's
   bottom border still under each row. Three items showed it and three did not,
   which is the signature of inherited styling rather than declared styling.

   These rules are unconditional and name every property a theme is likely to
   set on an anchor, so the rail looks the same on any theme. This is the
   "plugin owns its classes" rule (Varun, 2026-08-09): a surface must not
   depend on a property it never declares.
   --------------------------------------------------------------------------- */
.mvs-dashboard-tabs a.mvs-dashboard-tab,
.mvs-dashboard-tabs button.mvs-dashboard-tab {
    display: flex;
    align-items: center;
    gap: var(--mvs-space-2);
    box-sizing: border-box;
    width: 100%;
    /* Declared, not inherited: the theme's link colour and the horizontal
       tab's bottom border are both overwritten here rather than fought
       selector by selector when a screenshot catches them. */
    border: 0;
    border-radius: var(--mvs-radius-sm);
    background: transparent;
    box-shadow: none;
    color: var(--mvs-text-secondary);
    text-decoration: none;
    font-weight: 500;
}

.mvs-dashboard-tabs a.mvs-dashboard-tab:hover,
.mvs-dashboard-tabs a.mvs-dashboard-tab:focus,
.mvs-dashboard-tabs button.mvs-dashboard-tab:hover,
.mvs-dashboard-tabs button.mvs-dashboard-tab:focus {
    border-block-end: 0;
    background: var(--mvs-surface);
    color: var(--mvs-text);
    text-decoration: none;
}

.mvs-dashboard-tabs a.mvs-dashboard-tab.active,
.mvs-dashboard-tabs button.mvs-dashboard-tab.active {
    border-block-end: 0;
    background: var(--mvs-surface);
    /* Text -> AA-safe variant. These theme-defense rules out-specify the base
       .mvs-dashboard-tab.active rule, so setting the token there alone left the
       label at 4.36:1 - the fix has to land where the cascade actually ends. */
    color: var(--mvs-primary-text, var(--mvs-primary));
    font-weight: 600;
}

/* The count is a quantity, not part of the name — a pill at the far end reads
   as one and stops "Documents 6" scanning as a title. */
.mvs-dashboard-tabs .mvs-dashboard-tab__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 1.5rem;
    padding: 0.0625rem var(--mvs-space-1);
    border-radius: 999px;
    background: var(--mvs-border-light, var(--mvs-surface));
    color: var(--mvs-text-muted);
    font-size: var(--mvs-font-xs);
    font-weight: 600;
    font-variant-numeric: tabular-nums;
    line-height: 1.5;
}

.mvs-dashboard-tabs .mvs-dashboard-tab.active .mvs-dashboard-tab__count {
    background: var(--mvs-primary);
    color: var(--mvs-on-primary);
}

/* ---------------------------------------------------------------------------
   The rail head: who you are, and the way to your public profile.

   Unconditional, for the same reason the rail items are: a theme that paints
   anchors will paint these, and declaring appearance only inside a min-width
   query is what left half the rail looking like links. Every property a theme
   is likely to set on an anchor is named here.
   --------------------------------------------------------------------------- */
.mvs-dashboard-rail-head {
    display: flex;
    align-items: center;
    gap: var(--mvs-space-3);
    margin-bottom: var(--mvs-space-3);
    padding: var(--mvs-space-3);
    border: 1px solid var(--mvs-border-light);
    border-radius: var(--mvs-radius-md);
    background: var(--mvs-bg-subtle);
}

.mvs-dashboard-rail-head__avatar {
    display: block;
    width: 40px;
    height: 40px;
    border-radius: 999px;
    object-fit: cover;
}

.mvs-dashboard-rail-head__avatar-link {
    display: block;
    flex: 0 0 auto;
    border: 0;
    background: none;
    box-shadow: none;
    line-height: 0;
    text-decoration: none;
}

.mvs-dashboard-rail-head__id {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

/* WRAPS rather than truncating. A 13rem rail minus a 40px avatar cut
   "journey-member" to "journey-mem…" — and a name is the one string on this
   page where the end matters, because that is where people put the digits that
   tell two accounts apart. Two lines are cheaper than the wrong name. */
.mvs-dashboard-rail-head__name {
    color: var(--mvs-text);
    font-size: var(--mvs-font-sm);
    font-weight: 600;
    line-height: 1.3;
    overflow-wrap: anywhere;
}

.mvs-dashboard-rail-head__link {
    border: 0;
    background: none;
    box-shadow: none;
    /* Text, so it takes the AA-safe variant - the raw brand measured 4.36:1. */
    color: var(--mvs-primary-text, var(--mvs-primary));
    font-size: var(--mvs-font-xs);
    text-decoration: none;
}

.mvs-dashboard-rail-head__link:hover,
.mvs-dashboard-rail-head__link:focus-visible {
    color: var(--mvs-primary-text, var(--mvs-primary));
    text-decoration: underline;
}

/* Below the rail breakpoint the rail is a horizontal scroller, so the head
   stops being a column header and becomes a line above it. */
@media (max-width: 860px) {

    /* Below the rail breakpoint there is no column to draw, so the wrapper
       steps out of the layout entirely and the head + scroller behave exactly
       as they did before it existed. */
    .mvs-dashboard-rail {
        display: contents;
    }

    .mvs-dashboard-rail-head {
        margin-bottom: var(--mvs-space-2);
    }
}

/* A group that earned no heading still gets its break. Drawn on the item that
   starts it, because the rail is a flat list of items — there is no element
   standing for "the group" to hang a border on.

   The border itself lives with the theme-defense block, which is the only
   place that can outrank its own `border: 0 !important`; spacing is free to
   live here. One border, one implementation. */
.mvs-dashboard-tabs .mvs-dashboard-tab--group-start {
    margin-block-start: var(--mvs-space-2);
    padding-block-start: var(--mvs-space-2);
}


/* =============================================================================
 *  Document type glyphs — ONE mechanism for every document surface.
 *
 *  Used by the BP activity card, the profile Documents row, and the grid tile
 *  a document takes in a collection. They previously disagreed: one drew a
 *  Lucide <i>, one hardcoded an empty box, and the grid drew a PHOTO icon on a
 *  spreadsheet.
 *
 *  A CSS MASK rather than an <svg> or a Lucide element, because the surfaces
 *  do not share a JS environment. Activity content is baked into
 *  bp_activity.content and re-rendered by whatever theme or feed it lands in,
 *  where Lucide is not enqueued (measured: `window.lucide === undefined` on the
 *  member activity stream) and where kses strips `data-lucide` on save anyway.
 *  A mask needs nothing but this stylesheet.
 *
 *  Painted with `background-color: currentColor`, so the glyph takes the
 *  colour of whatever it sits in and inverts with dark mode for free — a
 *  background-image with a baked fill would stay one colour in both themes.
 *
 *  Artwork is the Lucide set the rest of the product draws (v0.460.0), read
 *  back from the live hydrator so the shapes match exactly.
 *  Size with --mvs-doc-glyph-size at the call site.
 * ========================================================================== */
.mvs-doc-glyph {
    display: block;
    flex: 0 0 auto;
    width: var(--mvs-doc-glyph-size, 20px);
    height: var(--mvs-doc-glyph-size, 20px);
    background-color: currentColor;
    -webkit-mask-image: var(--mvs-doc-glyph);
    mask-image: var(--mvs-doc-glyph);
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    -webkit-mask-position: center;
    mask-position: center;
    -webkit-mask-size: contain;
    mask-size: contain;
}

.mvs-doc-glyph--file-text {
    --mvs-doc-glyph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M15%202H6a2%202%200%200%200-2%202v16a2%202%200%200%200%202%202h12a2%202%200%200%200%202-2V7Z%22%2F%3E%3Cpath%20d%3D%22M14%202v4a2%202%200%200%200%202%202h4%22%2F%3E%3Cpath%20d%3D%22M10%209H8%22%2F%3E%3Cpath%20d%3D%22M16%2013H8%22%2F%3E%3Cpath%20d%3D%22M16%2017H8%22%2F%3E%3C%2Fsvg%3E");
}

.mvs-doc-glyph--file-type {
    --mvs-doc-glyph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M15%202H6a2%202%200%200%200-2%202v16a2%202%200%200%200%202%202h12a2%202%200%200%200%202-2V7Z%22%2F%3E%3Cpath%20d%3D%22M14%202v4a2%202%200%200%200%202%202h4%22%2F%3E%3Cpath%20d%3D%22M9%2013v-1h6v1%22%2F%3E%3Cpath%20d%3D%22M12%2012v6%22%2F%3E%3Cpath%20d%3D%22M11%2018h2%22%2F%3E%3C%2Fsvg%3E");
}

.mvs-doc-glyph--file-spreadsheet {
    --mvs-doc-glyph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M15%202H6a2%202%200%200%200-2%202v16a2%202%200%200%200%202%202h12a2%202%200%200%200%202-2V7Z%22%2F%3E%3Cpath%20d%3D%22M14%202v4a2%202%200%200%200%202%202h4%22%2F%3E%3Cpath%20d%3D%22M8%2013h2%22%2F%3E%3Cpath%20d%3D%22M14%2013h2%22%2F%3E%3Cpath%20d%3D%22M8%2017h2%22%2F%3E%3Cpath%20d%3D%22M14%2017h2%22%2F%3E%3C%2Fsvg%3E");
}

.mvs-doc-glyph--presentation {
    --mvs-doc-glyph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M2%203h20%22%2F%3E%3Cpath%20d%3D%22M21%203v11a2%202%200%200%201-2%202H5a2%202%200%200%201-2-2V3%22%2F%3E%3Cpath%20d%3D%22m7%2021%205-5%205%205%22%2F%3E%3C%2Fsvg%3E");
}

.mvs-doc-glyph--file {
    --mvs-doc-glyph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M15%202H6a2%202%200%200%200-2%202v16a2%202%200%200%200%202%202h12a2%202%200%200%200%202-2V7Z%22%2F%3E%3Cpath%20d%3D%22M14%202v4a2%202%200%200%200%202%202h4%22%2F%3E%3C%2Fsvg%3E");
}

.mvs-doc-glyph--file-code {
    --mvs-doc-glyph: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22black%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M10%2012.5%208%2015l2%202.5%22%2F%3E%3Cpath%20d%3D%22m14%2012.5%202%202.5-2%202.5%22%2F%3E%3Cpath%20d%3D%22M14%202v4a2%202%200%200%200%202%202h4%22%2F%3E%3Cpath%20d%3D%22M15%202H6a2%202%200%200%200-2%202v16a2%202%200%200%200%202%202h12a2%202%200%200%200%202-2V7z%22%2F%3E%3C%2Fsvg%3E");
}



/* ── Tap targets ───────────────────────────────────────────────────────────
 *
 * The member-side controls the audit measured under the 40px contract:
 * mvs-search-mode-btn 66x26, mvs-tag-cloud-item 44x31, mvs-dashboard-tab
 * 202x37, mvs-dashboard-rail-head__link 81x19 (Basecamp 10252222115).
 *
 * One rule on the component bases rather than per instance. `min-height` only
 * raises, and `align-items: center` keeps the label optically centred instead
 * of sitting at the top of a taller box.
 */
.mvs-search-mode-btn,
.mvs-tag-cloud-item,
.mvs-dashboard-tab,
.mvs-dashboard-rail-head__link {
    display: inline-flex;
    align-items: center;
    min-height: var(--mvs-touch-min);
}

/*
 * Upload form fields.
 *
 * These four carried NO rule of their own — every pixel of their size came
 * from whatever the active theme does to a bare input. On the QA baseline that
 * produced 42 / 60 / 36 / 42 for four fields in one form: the tags input under
 * the 40px floor, and visibly shorter than the title input beside it. On
 * another theme the numbers are different and so is which one is wrong.
 *
 * The floor is ours; the flavour stays the theme's — this sets a minimum and
 * nothing else, so a theme's padding, border and font still come through.
 * Basecamp 10252222115.
 */
.mvs-upload-meta-title,
.mvs-upload-meta-tags,
.mvs-upload-meta-privacy {
    min-height: var(--mvs-touch-min);
}
