/* =============================================================
   components.css — shared component patterns (MitsuHub)
   Built on design-tokens.css; link this IMMEDIATELY AFTER it.
   See docs/superpowers/component-css-migration.md for sweep guide.
   ============================================================= */

/* ==========================================================================
   PAGE GUTTER — .mh-container
   ==========================================================================
   The house page shell. Replaces bare `class="container"`, which was
   Bootstrap's and brought Bootstrap's gutter with it: `--bs-gutter-x` is
   1.5rem, so every one of those elements got 12px rather than the house
   15px, and pages that set only `max-width`/`margin` never overrode it.
   Nested, it compounded — the anime detail overview sat 24px in (measured).

   `padding-inline`, not the `padding` shorthand, so a page can set its own
   vertical padding with `padding-block` without silently wiping the gutter.
   A page-level rule using the shorthand WILL wipe it — that is the one
   thing not to do here.

   Pair it with a place-specific class that carries the vertical rhythm:
       <div class="mh-container overview-container">
   ========================================================================== */
.mh-container {
    margin-inline: auto;
    max-width: 100%;
    padding-inline: var(--container-pad);
}

/* A container inside a container has already been paid for. Without this the
   gutter compounds with depth and content drifts further in the deeper it
   sits. Two classes deep, so it outranks any single-class page rule. */
.mh-container .mh-container {
    padding-inline: 0;
}

/* ---- Scrollbar: global dark default ---- */
/* Matches the documented standard: thin, dark, no browser default chrome.
   Template inline rules (hides / themed thumbs) load after this file and
   therefore OVERRIDE it — intentional hides and themed scrollbars keep working. */
*::-webkit-scrollbar {
    width: 6px;
    height: 6px;
}
*::-webkit-scrollbar-track {
    background: transparent;
}
*::-webkit-scrollbar-thumb {
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.15);
    border-radius: var(--radius-xs);
}
*::-webkit-scrollbar-thumb:hover {
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.25);
}
* {
    scrollbar-width: thin;
    scrollbar-color: rgba(var(--fg-rgb, 255, 255, 255), 0.15) transparent;
}

/* Scrollbar utilities */
.scroll-hidden {
    scrollbar-width: none;
    -ms-overflow-style: none;
}
.scroll-hidden::-webkit-scrollbar {
    display: none;
}
.scroll-thin::-webkit-scrollbar {
    width: 4px;
    height: 4px;
}

/* ---- Buttons ---- */
.btn {
    display: inline-flex;
    align-items: center;
    /* Load-bearing: align-items centers the label on the CROSS axis only. Without
       justify-content, the main axis defaults to flex-start, so any .btn that is
       stretched (a flex parent's default align-items: stretch) or given a width
       left-aligns its label and dumps the free space on the right. Content-sized
       buttons have no free space, so this is a no-op for them. */
    justify-content: center;
    gap: 8px;
    border: none;
    border-radius: var(--radius-sm);
    padding: 10px 20px;
    font-weight: var(--weight-medium);
    cursor: pointer;
    transition: background var(--transition-base), background-color var(--transition-base), border var(--transition-base), border-color var(--transition-base), box-shadow var(--transition-base), color var(--transition-base), filter var(--transition-base), opacity var(--transition-base), transform var(--transition-base);
    text-decoration: none;
}
.btn-primary {
    background: var(--brand-blue);
    color: #fff;
}
.btn-primary:hover {
    background: var(--brand-blue-dark);
    transform: translateY(-1px);
    box-shadow: var(--lift-blue);
}
.btn-danger {
    background: var(--danger);
    color: #fff;
}
.btn-danger:hover {
    background: var(--danger-dark);
}
.btn-ghost {
    background: var(--glass-bg);
    color: var(--text);
    border: 1px solid var(--border);
}
.btn-ghost:hover {
    background: var(--surface-hover);
}
/* Brand-gradient primary button (the ~half of .btn-primary sites that use a gradient). */
.btn-gradient {
    background: var(--brand-gradient);
    color: #fff;
}
/* Pill shape. A modifier, not a colour: it composes with any of the variants
   above (.btn.btn-ghost.btn-pill, .btn.btn-primary.btn-pill). Added because
   pill-shaped buttons were being hand-rolled with a local border-radius each
   time one was wanted - the shared system should carry the shape (owner
   directive: if a new thing could be shared, make it shared). */
.btn-pill {
    border-radius: var(--radius-pill);
}
.btn-gradient:hover {
    transform: translateY(-1px);
    box-shadow: var(--lift-blue);
}

/* ---- Button variants the tail was hand-rolling ----
   Added 2026-08-25 from a MEASURED reading of 677 hand-rolled `*-btn` classes,
   not from a guess at what a button system should have. #95: a scale drawn
   without measuring fails silently, because the ladder cannot express what
   people reach for and the low adoption then reads as a discipline problem.

   The fill axis was already covered (primary / danger / ghost / gradient).
   What the tail actually reached for and could not find was SIZE, an OUTLINE
   with no fill, and a CIRCULAR icon button.

   Two corrections the measurement made to the obvious answer:

   - the raw padding histogram's small cluster is 6-8px x 12-14px, but the
     three files that already define `.btn-sm` by name all chose `8px 16px`
     at ~0.81rem, and they agree with each other. The people who named the
     thing are better evidence than the histogram, so those are the values.
   - the circular variant is NOT called `.btn-icon`. That name is already
     taken locally and means three different things: a rounded square with
     `padding: 10px` (my-social-links-tab), an 18x18 box (home), and a 48px
     circle (anime-music). Claiming it centrally would add a fourth meaning
     and page-local CSS would win anyway, so the shared circle is
     `.btn-circle` - a free name that says what it is (#31, #59). */

/* Disabled. The component had NO disabled state, which is why the tail carries
   262 `:disabled` rules of its own - and it is the reason a class cannot simply
   be swapped for `.btn` without checking: the swap would drop the only thing
   making a dead button look dead. `opacity: 0.5` is the measured house value
   (41 uses, against 15 each for 0.6 and 0.4).

   The hover lift is cancelled here too: `:hover` still matches a disabled
   button in CSS, so without this a disabled `.btn-primary` still rises and
   glows under the cursor while refusing to do anything. */
.btn:disabled,
.btn[disabled],
.btn.is-disabled {
    opacity: 0.5;
    cursor: not-allowed;
}
.btn:disabled:hover,
.btn[disabled]:hover,
.btn.is-disabled:hover {
    transform: none;
    box-shadow: none;
    filter: none;
}

.btn-sm {
    padding: 8px 16px;
    font-size: 0.8125rem;
    gap: 6px;
}
.btn-lg {
    padding: 12px 24px;
    font-size: 0.95rem;
    gap: 10px;
}

/* Outline: a border and no fill. 50 classes in the tail are transparent AND
   bordered, which `.btn-ghost` does not cover - ghost carries a glass fill. */
.btn-outline {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-secondary);
}
.btn-outline:hover {
    background: var(--surface-hover);
    border-color: var(--border-strong);
    color: var(--text);
}

/* Circular icon button. 67 tail classes are a circle with width == height;
   the measured sizes are 32 / 36 / 40 / 44, so 36 is the base and the size
   variants take the ends.

   Written as compound selectors (`.btn-circle.btn-sm`) rather than relying on
   source order: `.btn-sm` above sets padding, and at equal specificity the
   later rule wins, which is the trap that makes a variant apply SOME of what
   it declares (#33). */
.btn-circle {
    width: 36px;
    height: 36px;
    padding: 0;
    gap: 0;
    border-radius: var(--radius-circle);
    flex-shrink: 0;
}
.btn-circle.btn-sm {
    width: 32px;
    height: 32px;
    padding: 0;
    font-size: 0.8125rem;
}
.btn-circle.btn-lg {
    width: 44px;
    height: 44px;
    padding: 0;
    font-size: 0.95rem;
}

/* Every component must work at 375px and touch targets are at least 44x44.
   A 32px or 36px circle does not meet that, and the tail has been shipping
   32px as its most common icon size. Growing the HIT AREA on coarse pointers
   keeps the drawn size on a mouse and satisfies the rule on a finger. */
@media (pointer: coarse) {
    .btn-circle,
    .btn-circle.btn-sm {
        min-width: 44px;
        min-height: 44px;
    }
}

/* ---- Custom select (dark) ---- REMOVED, deliberately.
   This rule skinned a NATIVE <select class="custom-select">, and not one template
   ever used it that way: dark <select> styling in this codebase goes through
   data-custom-select -> custom-select.js -> .cs-wrap.
   Meanwhile SEVEN live sites use <div class="custom-select"> as the WRAPPER of a
   hand-rolled button+menu widget (admin/staff applications, assign-role, members;
   watch-party party-settings x2; watchlist creator-dashboard). None of them
   re-declared background/border/padding, so this rule leaked a phantom chevron
   glyph, an extra border and 8px/32px padding onto every one of those widgets --
   invariant #31 exactly, from a class with zero real adopters.
   The dark-select component still exists; it is .cs-wrap (custom-select.js) and
   the namespaced .mhdd-* dropdown below. */

/* ---- Input (dark) ---- */
.input {
    background: var(--glass-bg);
    border: 1px solid var(--border);
    color: #fff;
    border-radius: var(--radius-sm);
    padding: 10px 12px;
}
.input:focus {
    outline: none;
    border-color: var(--border-brand);
    box-shadow: var(--ring-blue);
}

/* ---- Native control defaults, killed globally (2026-08-18) ---- */
/* Same reasoning as the dark-scrollbar default above: the house rule is that no
   default browser UI shows through on a black page, and enforcing that per-page
   had drifted badly — an audit found 38 number inputs still painting the white
   up/down spinners (16 of them on the anime/manga detail tabs, the highest
   traffic surface in the app) against 25 that had been fixed locally, plus 19
   bare date pickers rendering a white calendar glyph. Per-class rules could
   never win that race: every new number input started out wrong and only got
   fixed once somebody noticed. This is the floor for ALL of them; a page that
   genuinely wants the spinner can still opt back in with its own rule. */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button {
    -webkit-appearance: none;
    appearance: none;
    margin: 0;
}

input[type="number"] {
    -moz-appearance: textfield;
    appearance: textfield;
}

/* Renders the picker glyph and the native popup dark instead of white-on-black.
   Applied to the control only, never the page, so nothing else is themed. */
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="month"],
input[type="week"] {
    color-scheme: dark;
}

/* ---- Glass (blur bucketed to 3 token sizes) ---- */
/* Bucket boundaries: blur < 10px → .glass-sm, 10–15px → .glass/.glass-md, ≥ 16px → .glass-lg */
.glass,
.glass-md {
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur-md));
    -webkit-backdrop-filter: blur(var(--glass-blur-md));
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
}
.glass-sm {
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur-sm));
    -webkit-backdrop-filter: blur(var(--glass-blur-sm));
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
}
.glass-lg {
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur-lg));
    -webkit-backdrop-filter: blur(var(--glass-blur-lg));
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xl);
}

/* ---- Spinner ---- */
@keyframes spin {
    to {
        transform: rotate(360deg);
    }
}
.spinner {
    width: 40px;
    height: 40px;
    border: 3px solid var(--border-subtle);
    border-top-color: var(--brand-blue);
    border-radius: var(--radius-circle);
    animation: spin 0.8s linear infinite;
}

/* ---- Empty state (shared with the <@empty.show/> FreeMarker macro) ---- */
.mh-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    gap: 16px;
    padding: 60px 20px;
    margin: 0 auto;
    max-width: 100%;
    /* A flex ITEM sizes to its content, and max-width cannot make it grow, so
       inside a flex parent an empty state rendered as a ~400px message floating
       in a 1,356px section. Inert wherever the parent is not flex. */
    flex: 1 1 100%;
    box-sizing: border-box;
    /* Dropped into a CSS grid, an empty state is a grid ITEM: it lands in a
       single column track and reads as a cramped, left-hugging message rather
       than a centred one (the gallery grid showed exactly this). Spanning every
       column fixes it wherever it is a grid child, and the declaration is
       simply ignored everywhere else. A container that genuinely wants a
       narrower span still wins - its own rule loads after this sheet. */
    grid-column: 1 / -1;
}
/* Redundant since the base rule went to max-width 100% (2026-07-17 bring-up);
   kept as an explicit marker for callers that request the wide variant. */
.mh-empty--wide {
    max-width: 100%;
}
/* Bare glyph, no circle: the background disc made every sub-pixel glyph
   offset visible (FA ink is never perfectly centered in its em box), so
   the chrome was removed rather than fought. Keep this a plain icon. */
.mh-empty__icon {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-secondary);
    font-size: 2.25rem;
    line-height: var(--leading-none);
    opacity: 0.85;
}
.mh-empty__icon i {
    display: block;
    line-height: var(--leading-none);
}
.mh-empty__title {
    margin: 0;
    font-size: 1.25rem;
    font-weight: var(--weight-semibold);
    color: var(--text);
    line-height: var(--leading-snug);
}
.mh-empty__body {
    margin: 0;
    font-size: 0.95rem;
    font-weight: var(--weight-normal);
    color: var(--text-secondary);
    line-height: var(--leading-normal);
    max-width: 360px;
}
.mh-empty__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 10px;
    margin-top: 8px;
}
.mh-empty__cta,
.mh-empty__cta-secondary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 10px 20px;
    border-radius: var(--radius-sm);
    font-family: inherit;
    font-size: 0.95rem;
    font-weight: var(--weight-medium);
    line-height: var(--leading-tight);
    text-decoration: none;
    cursor: pointer;
    transition: background var(--dur-base) var(--ease-standard), border var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard), box-shadow var(--dur-base) var(--ease-standard), color var(--dur-base) var(--ease-standard), font-size var(--dur-base) var(--ease-standard), min-height var(--dur-base) var(--ease-standard), padding var(--dur-base) var(--ease-standard), transform var(--dur-base) var(--ease-standard);
    min-height: 44px;
    box-sizing: border-box;
}
.mh-empty__cta {
    color: var(--text);
    border: none;
    box-shadow: var(--lift-blue);
}
.mh-empty__cta:hover,
.mh-empty__cta:focus-visible {
    transform: translateY(-1px);
    box-shadow: var(--lift-blue);
    color: var(--text);
    text-decoration: none;
}
.mh-empty__cta-secondary {
    background: var(--glass-bg);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.85);
    border: 1px solid var(--border);
}
.mh-empty__cta-secondary:hover,
.mh-empty__cta-secondary:focus-visible {
    background: var(--surface-hover);
    border-color: rgba(var(--fg-rgb, 255, 255, 255), 0.2);
    color: var(--text);
    text-decoration: none;
}

/* Compact variant — sidebar panels, sub-tabs, narrow grids */
.mh-empty--compact {
    padding: 32px 16px;
    gap: 12px;
}
.mh-empty--compact .mh-empty__icon {
    width: 56px;
    height: 56px;
    font-size: 1.4rem;
}
.mh-empty--compact .mh-empty__title {
    font-size: 1.05rem;
}
.mh-empty--compact .mh-empty__body {
    font-size: 0.875rem;
}
.mh-empty--compact .mh-empty__cta,
.mh-empty--compact .mh-empty__cta-secondary {
    padding: 8px 16px;
    font-size: 0.875rem;
    min-height: 40px;
}

/* Card variant — the empty state IS the panel rather than sitting inside one.
   Three pages had grown their own copy of exactly this (staff-positions, the
   public profile's quotes section, the watchlist add-empty grid); this is that
   card, once. Composes with every other variant, since it only paints a
   surface. */
.mh-empty--card {
    background: var(--surface-grad);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-xl);
}

/* Tone variants — a tinted icon CHIP, for empty states whose reason has a
   semantic colour: a failed load is not the same event as an empty inbox, and
   the chip says which before the title is read.

   Lifted from the friends zone, which had this treatment on nine surfaces as
   inline `style="background: rgba(239, 68, 68, 0.12); color: #ef4444;"` - six
   hues, hardcoded, with --brand-blue and --brand-purple each appearing BOTH as
   a var() and as their own literal on different rows. Sharing it turns six
   hand-written hues into five named tones off the canonical palette.

   Written as a compound selector so it outranks `.mh-empty__icon` outright
   rather than tying with it and depending on source order (#33).

   The tint goes through a CHANNEL token, never rgba() of a colour token:
   rgba(var(--danger), .12) is invalid and drops the whole declaration with no
   error at all (#94). Every channel used here is defined globally in
   design-tokens.css - a tone that resolved on some pages and silently
   vanished on others is the exact failure this component exists to end. */
.mh-empty--tone-neutral .mh-empty__icon,
.mh-empty--tone-info .mh-empty__icon,
.mh-empty--tone-success .mh-empty__icon,
.mh-empty--tone-warning .mh-empty__icon,
.mh-empty--tone-danger .mh-empty__icon,
.mh-empty--tone-brand .mh-empty__icon {
    width: 80px;
    height: 80px;
    border-radius: var(--radius-circle);
    margin: 0 auto;
    font-size: 2rem;
    opacity: 1;
    background: rgba(var(--mh-empty-tone-rgb), 0.12);
    color: rgb(var(--mh-empty-tone-rgb));
}
.mh-empty--tone-neutral { --mh-empty-tone-rgb: var(--fg-rgb); }
.mh-empty--tone-info { --mh-empty-tone-rgb: var(--info-rgb); }
.mh-empty--tone-success { --mh-empty-tone-rgb: var(--success-rgb); }
.mh-empty--tone-warning { --mh-empty-tone-rgb: var(--warning-rgb); }
.mh-empty--tone-danger { --mh-empty-tone-rgb: var(--danger-rgb); }
.mh-empty--tone-brand { --mh-empty-tone-rgb: var(--brand-purple-rgb); }

/* Neutral is the one tone whose foreground must not be its own tint at full
   strength - white text on a white-6% chip is the default look, not an accent. */
.mh-empty--tone-neutral .mh-empty__icon {
    color: var(--text-muted);
}

/* Compact + tone. This has to restate the compact sizes rather than rely on
   them: `.mh-empty--compact .mh-empty__icon` above ties with the tone rule at
   equal specificity and loses on source order, so without this a compact
   toned empty state would wear the full-size 80px chip (#33). The numbers are
   the compact ones verbatim - a chip that sized itself differently would be a
   second scale for the same slot. */
.mh-empty--compact.mh-empty--tone-neutral .mh-empty__icon,
.mh-empty--compact.mh-empty--tone-info .mh-empty__icon,
.mh-empty--compact.mh-empty--tone-success .mh-empty__icon,
.mh-empty--compact.mh-empty--tone-warning .mh-empty__icon,
.mh-empty--compact.mh-empty--tone-danger .mh-empty__icon,
.mh-empty--compact.mh-empty--tone-brand .mh-empty__icon {
    width: 56px;
    height: 56px;
    font-size: 1.4rem;
}

/* Entrance variant — OPT-IN, and it animates transform ONLY.
   `.mh-empty` has never had an entrance on purpose: a decoration that owns its
   own visibility is how a fully-rendered page ends up blank when the animation
   does not run (CLAUDE.md #58 - a backgrounded tab freezing the frame loop, a
   cancelled rAF, a throw earlier in init). Keeping opacity out of the keyframe
   removes that failure entirely: if this never runs, the empty state is simply
   there, unmoved. `both` holds the end state, never the from-state. */
.mh-empty--enter {
    animation: mhEmptyRise var(--dur-slow) var(--ease-out) both;
}
@keyframes mhEmptyRise {
    from { transform: translateY(12px); }
    to   { transform: none; }
}
@media (prefers-reduced-motion: reduce) {
    .mh-empty--enter {
        animation: none;
    }
}

/* Mobile responsiveness */
@media (max-width: 480px) {
    .mh-empty {
        padding: 40px 16px;
        gap: 14px;
    }
    .mh-empty__icon {
        width: 64px;
        height: 64px;
        font-size: 1.5rem;
    }
    .mh-empty__title {
        font-size: 1.125rem;
    }
    .mh-empty__body {
        font-size: 0.9rem;
    }
    .mh-empty__actions {
        flex-direction: column;
        width: 100%;
        align-items: stretch;
    }
    .mh-empty__cta,
    .mh-empty__cta-secondary {
        width: 100%;
    }
}

/* Honor reduced-motion preference */
@media (prefers-reduced-motion: reduce) {
    .mh-empty__icon::before {
        animation: none;
    }
    .mh-empty__cta,
    .mh-empty__cta-secondary {
        transition: none;
    }
    .mh-empty__cta:hover,
    .mh-empty__cta:focus-visible,
    .mh-empty__cta-secondary:hover,
    .mh-empty__cta-secondary:focus-visible {
        transform: none;
    }
}

/* ---- MhDropdown — shared namespaced custom dropdown widget ---- */
/* Prefix .mhdd-* chosen per invariant #31 (namespace collision avoidance).
   The messages-panel slow-mode picker (messages-info-panel.ftlh) owns its
   own local dropdown classes — the two widgets share ZERO CSS class names
   by design. See the task-1 rename report for detail.                     */
.mhdd {
    position: relative;
}

.mhdd-trigger {
    width: 100%;
    padding: 14px 18px;
    padding-right: 48px;
    border-radius: var(--radius-md);
    background: var(--glass-bg);
    border: 1px solid var(--border-strong);
    color: var(--text);
    font-size: 0.875rem;
    font-family: inherit;
    cursor: pointer;
    transition: background var(--transition-base), border-color var(--transition-base);
    text-align: left;
    display: flex;
    align-items: center;
    gap: 10px;
    position: relative;
}

.mhdd-trigger:hover {
    border-color: rgba(139, 92, 246, 0.3);
    background: var(--surface-hover);
}

.mhdd.open .mhdd-trigger {
    border-color: var(--brand-purple);
    background: rgba(139, 92, 246, 0.15);
}

/* Icon in the trigger button */
.mhdd-icon {
    font-size: 1.125rem;
    line-height: var(--leading-none);
    flex-shrink: 0;
}

/* Trigger label text */
.mhdd-text {
    flex: 1;
}

/* Placeholder state: muted colour when no value is selected */
.mhdd-text--placeholder {
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.7);
}

/* Chevron: positioned at the right of the trigger, rotates on open */
.mhdd-chevron {
    position: absolute;
    right: 18px;
    top: 50%;
    transform: translateY(-50%);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.7);
    transition: transform var(--transition-normal);
    pointer-events: none;
}

.mhdd.open .mhdd-chevron {
    transform: translateY(-50%) rotate(180deg);
    color: var(--brand-purple);
}

/* Menu panel */
.mhdd-menu {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    right: 0;
    background: linear-gradient(135deg, #1e1e32 0%, #1a1a2e 100%);
    border: 1px solid rgba(139, 92, 246, 0.3);
    border-radius: var(--radius-md);
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    transition: max-height var(--transition-normal), opacity var(--transition-normal);
    z-index: 100;
    box-shadow: var(--shadow-2xl);
}

.mhdd.open .mhdd-menu {
    max-height: 320px;
    overflow-y: auto;
    opacity: 1;
}

/* intentional — themed purple scrollbar for the dropdown menu */
.mhdd-menu::-webkit-scrollbar {
    width: 6px;
}

.mhdd-menu::-webkit-scrollbar-thumb {
    background: rgba(139, 92, 246, 0.3);
    border-radius: var(--radius-md);
}

/* Individual item rows */
.mhdd-item {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px;
    cursor: pointer;
    transition: background var(--transition-base), border-bottom var(--transition-base);
    border-bottom: 1px solid rgba(var(--fg-rgb, 255, 255, 255), 0.05);
}

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

.mhdd-item:hover {
    background: rgba(139, 92, 246, 0.15);
}

/* Keyboard focus gets the hover cue: the items carry a roving tabindex now. */
.mhdd-item:focus-visible {
    background: rgba(139, 92, 246, 0.15);
    outline: 2px solid rgba(139, 92, 246, 0.6);
    outline-offset: -2px;
}

.mhdd-item.selected {
    background: rgba(139, 92, 246, 0.25);
}

/* Icon box inside each item (overrides the trigger's .mhdd-icon rule) */
.mhdd-item .mhdd-icon {
    width: 32px;
    height: 32px;
    border-radius: var(--radius-sm);
    background: var(--glass-bg);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    flex-shrink: 0;
}

/* Item label text */
.mhdd-label {
    flex: 1;
    font-size: 0.875rem;
    color: var(--text);
}

/* Check-mark shown on the selected item */
.mhdd-check {
    color: var(--brand-purple);
    opacity: 0;
    transition: opacity var(--transition-base);
    flex-shrink: 0;
}

.mhdd-item.selected .mhdd-check {
    opacity: 1;
}

/* ==========================================================================
   SCREEN-READER-ONLY TEXT (.sr-only, .sr-only-focusable)
   ==========================================================================
   Available to assistive technology, absent from the visual design.

   This lives here because it was MISSING. Ten templates carry
   `class="sr-only"` and seven of them define the rule in their own inline
   <style>; the other three simply rendered the text on screen. The class came
   from Bootstrap, which most pages no longer load, so the label a sighted user
   was never meant to see — "Post Title", "Password must be at least 8
   characters" — was sitting in the layout. Nothing errors when a utility class
   resolves to nothing, which is why it survived.

   Additive by construction: components.css loads in <head> and a template's
   inline <style> loads later, so the seven pages that define their own keep
   theirs unchanged. Only the pages with no definition gain one.

   `clip` is the deprecated property and `clip-path` the current one. Both are
   here on purpose — `clip` is what several of the existing inline copies use,
   and dropping it would make this rule weaker than the ones it backstops.
   ========================================================================== */

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border-width: 0;
}

/* A skip link is sr-only until it takes focus, then it must be SEEN — it
   exists for keyboard users, and one that stays hidden while focused is worse
   than none, because the tab order stops somewhere invisible. */
.sr-only-focusable:focus,
.sr-only-focusable:focus-visible {
    position: fixed;
    top: 8px;
    left: 8px;
    width: auto;
    height: auto;
    overflow: visible;
    clip: auto;
    clip-path: none;
    white-space: normal;
    z-index: 9999;
    padding: 8px 16px;
    background: var(--brand-blue);
    color: #fff;
    text-decoration: none;
    border-radius: var(--radius-sm);
}

/* ==========================================================================
   LINKS INSIDE PROSE (.mh-text-link)
   ==========================================================================
   A link in a sentence reads by weight and brightness, never the browser's or
   Bootstrap's underlined blue (owner decision 2026-09-14). Markup contract:

       <p>... head to <a class="mh-text-link" href="...">profile settings</a> ...</p>

   A class rather than a bare `a` rule on purpose: the nav re-links this file
   in the body, so a bare selector would outrank every page's own link colours. */
.mh-text-link {
    color: var(--text-primary);
    font-weight: var(--weight-semibold);
    text-decoration: none;
    transition: color var(--dur-fast) ease;
}

.mh-text-link:hover {
    color: var(--brand-blue);
}

.mh-text-link:focus-visible {
    outline: 2px solid var(--brand-blue);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
}

/* ==========================================================================
   "VIEW ALL" SECTION LINKS (.mh-viewall)
   ==========================================================================
   The canonical "there's more of this elsewhere" affordance: accent text +
   an arrow that darts right on hover. Markup contract:

       <a class="mh-viewall" href="...">Label <i class="fas fa-arrow-right" aria-hidden="true"></i></a>

   (a <span> around the label is fine; the icon is always fa-arrow-right —
   chevrons are reserved for expand/collapse.)

   Appearance lives HERE and only here; sections own PLACEMENT via their own
   container rules (margins, alignment, flex-shrink). Hover moves ONLY the
   arrow (transform — no layout shift, right-aligned headers stay stable).

   Accent: override --mh-viewall-accent in a zone whose palette isn't brand
   blue (e.g. --msg-accent, --accent-light). Profile pages inherit their
   theme automatically via the --profile-primary/--accent-color fallbacks;
   the Vibrant dynamic theme overrides live in profile-dynamic-theme.ftlh.

   --pill: the contained variant for sections that need visual weight
   (dense toolbars, links over busy imagery). Chrome derives from
   currentColor, so it follows the accent — and frosts white on hover. Use
   sparingly; the bare link is the default. */
.mh-viewall {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: var(--mh-viewall-accent, var(--profile-primary, var(--accent-color, var(--brand-blue))));
    font-size: 0.8125rem;
    font-weight: var(--weight-semibold);
    text-decoration: none;
    white-space: nowrap;
    transition: color 0.25s ease;
    /* 44px touch floor (house rule). Measured 44x20 when it carries a label, so
       it missed the floor vertically everywhere; and 43x44 on /users, where a
       mobile rule drops the label and leaves the arrow alone - so BOTH axes
       need the floor, not just the one the first measurement happened to catch. */
    min-height: 44px;
    min-width: 44px;
    justify-content: center;
}

.mh-viewall:hover {
    color: #ffffff;
    text-decoration: none;
}

.mh-viewall i {
    font-size: 0.85em;
    transition: transform var(--dur-slow) var(--ease-spring);
}

.mh-viewall:hover i {
    transform: translateX(4px);
}

.mh-viewall--pill {
    padding: 8px 16px;
    border-radius: var(--radius-pill);
    border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
    background: color-mix(in srgb, currentColor 9%, transparent);
    /* Explicit here too, not only on the base class — the pill must never
       underline even when a page's own anchor rules outrank .mh-viewall
       (owner request 2026-08-17). */
    text-decoration: none;
    transition: color 0.25s ease, border-color 0.25s ease, background 0.25s ease;
}

.mh-viewall--pill:hover {
    border-color: color-mix(in srgb, currentColor 45%, transparent);
    background: color-mix(in srgb, currentColor 12%, transparent);
    text-decoration: none;
}

/* --inline: the door that IS the text. For a count or a noun set inside a running
   sentence, where an arrow and a separate colour would read as a control bolted on
   beside the prose rather than as the prose being the link. Keeps .mh-viewall's
   semantics and destination and inherits the surrounding type, so the sentence still
   reads as a sentence. The caller owns any hover treatment for its own text
   (a gradient-clipped <em> needs its fill flipped, not its color set). */
.mh-viewall--inline {
    display: inline;
    gap: 0;
    color: inherit;
    font-size: inherit;
    font-weight: inherit;
    white-space: normal;
}

/* --block: the door that IS the row. For a threshold at the end of a run, where the
   link is the full-width boundary you arrive at rather than a word inside a sentence.
   Same semantics and destination as the bare link; the caller owns its own ground,
   typography and any art behind it. */
.mh-viewall--block {
    display: flex;
    align-items: center;
    justify-content: space-between;
    width: 100%;
    font-size: inherit;
    font-weight: inherit;
    white-space: normal;
}

/* ==========================================================================
   SECTION HEADERS (.mh-sechead)
   ==========================================================================
   The canonical anatomy for a content-section header. This unifies the
   GRAMMAR, not one pixel-identical look — sections differ in what they put
   in the slots, never in the slot rules. Markup contract:

       <div class="mh-sechead">
           <div class="mh-sechead__main">
               <h2 class="mh-sechead__title">
                   <i class="mh-sechead__icon fas fa-..." aria-hidden="true"></i>
                   <span>Title</span>
                   <span class="mh-sechead__count">12</span>   (optional)
               </h2>
               <p class="mh-sechead__sub">Subtitle</p>          (optional)
           </div>
           <div class="mh-sechead__actions">…toggles, .mh-viewall…</div>
       </div>

   ICON POLICY (tiered): top-level page sections carry ONE accent-colored
   icon before the title; nested sub-section headers carry none. If a script
   swaps the title text, give IT an inner <span id> — never setText the
   whole <h2>, that wipes the icon.

   --centered: the stacked hero-style variant (title block centered,
   actions centered below). Same anatomy, second sanctioned layout.

   Mobile contract (<=640px): the row variant stacks in DOM order —
   title group first, actions after — so section identity always lands
   before its controls. No per-section `order:` hacks.

   Accent: --mh-sechead-accent, defaulting through the same chain as
   .mh-viewall (profile theme → page accent → brand blue). */
.mh-sechead {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    flex-wrap: wrap;
    margin-bottom: 24px;
}

.mh-sechead__main {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.mh-sechead__title {
    display: flex;
    align-items: center;
    gap: 10px;
    margin: 0;
    font-size: clamp(1.25rem, 2.5vw, 1.6rem);
    font-weight: var(--weight-bold);
    color: var(--text);
    line-height: var(--leading-snug);
}

/* The mark sits in a tinted tile rather than floating beside the title. A TINT of
   the zone accent, not a solid fill: solid competes with the title next to it,
   while a tint reads as a slot the icon lives in. color-mix keeps it honest across
   palettes — a fixed alpha disappears on a pale accent and shouts on a saturated
   one. Every zone keeps its own accent through --mh-sechead-accent. */
.mh-sechead__icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    flex-shrink: 0;
    border-radius: var(--radius-md);
    color: var(--mh-sechead-accent, var(--profile-primary, var(--accent-color, var(--brand-blue))));
    background: color-mix(in srgb, currentColor 15%, transparent);
    border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
    font-size: 0.85rem;
}

.mh-sechead__count {
    font-size: 0.8125rem;
    font-weight: var(--weight-semibold);
    padding: 3px 11px;
    border-radius: var(--radius-pill);
    color: var(--mh-sechead-accent, var(--profile-primary, var(--accent-color, var(--brand-blue))));
    background: color-mix(in srgb, currentColor 12%, transparent);
    line-height: var(--leading-normal);
}

.mh-sechead__sub {
    margin: 0;
    font-size: 0.9rem;
    color: var(--text-muted, rgba(var(--fg-rgb, 255, 255, 255), 0.55));
    font-weight: var(--weight-normal);
}

.mh-sechead__actions {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-shrink: 0;
}
/* ==========================================================================
   .mh-subsec — SECTION LABEL AT PANEL SCALE

   .mh-sechead is the PAGE-level header: a clamp()ed 1.25-1.6rem title, a 30px
   icon tile, 24px of margin. Inside a modal or a side panel that is far too
   loud, and every such surface had been hand-rolling a small uppercase caption
   instead. This is that caption, once.

   Markup contract:

       <div class="mh-subsec__label">
           <span>Roles</span>            <- required; carries the accent rule
           <a class="mh-viewall" ...>    <- optional trailing control
       </div>

   The accent rule reads --mh-subsec-accent, so a surface showing one person's
   or one entity's content can tint its sections without redefining anything.
   Falls back through the same chain --mh-sechead-* uses.
   ========================================================================== */
.mh-subsec + .mh-subsec {
    margin-top: 26px;
}

.mh-subsec__label {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border-subtle);
    font-size: 0.72rem;
    font-weight: var(--weight-bold);
    text-transform: uppercase;
    letter-spacing: 0.10em;
    color: var(--text-secondary);
}

.mh-subsec__label > span:first-child {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
}

/* The eye needs a hard edge to break a column into sections; a dim uppercase
   string on its own does not provide one. */
.mh-subsec__label > span:first-child::before {
    content: '';
    width: 3px;
    height: 13px;
    border-radius: var(--radius-pill);
    background: var(--mh-subsec-accent, var(--profile-primary, var(--accent-color, var(--brand-blue))));
    flex-shrink: 0;
}

/* ==========================================================================
   .mh-hscroll — THE HOUSE CHIP ROW

   Pill and chip rows scroll horizontally on small screens rather than wrapping
   into a stack, and the scrollbar is hidden. That rule was being re-implemented
   per surface; this is the shared form.

   Children need no class - they are simply not allowed to shrink, because a
   squashed chip is worse than one you scroll to.
   ========================================================================== */
.mh-hscroll {
    display: flex;
    flex-wrap: nowrap;
    overflow-x: auto;
    gap: 8px;
    padding-bottom: 2px;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.mh-hscroll::-webkit-scrollbar {
    display: none;
}

.mh-hscroll > * {
    flex-shrink: 0;
}

/* ==========================================================================
   .mh-rail — THE SECTION HEADER CONSOLE

   A section header's right-hand slot routinely carries three DIFFERENT KINDS
   of thing at once:

       state controls  — tabs/toggles, which change WHAT you are looking at
       facts           — counts and meta, which DESCRIBE what you are looking at
       navigation      — a "view all", which LEAVES for more of it

   Rendered as bare flex siblings 12px apart they read as three unrelated
   objects, because nothing expresses that they are different in kind — the eye
   cannot tell where the facts stop and the action starts. The rail binds them
   into ONE instrument with ONE ground, ONE border and ONE radius, and uses a
   hairline divider to say "different kind of thing" WITHOUT making it a
   separate object.

   Zone order is fixed everywhere so the shape is learnable:

       <div class="mh-sechead__actions mh-rail">
         <div class="mh-rail__zone mh-rail__seg">   … controls
         <div class="mh-rail__zone mh-rail__facts"> … facts
         <a   class="mh-rail__zone mh-rail__go mh-viewall"> … navigation
       </div>

   Any subset is valid — a rail of just facts + go is the common case. Use the
   BARE .mh-viewall inside a rail, never .mh-viewall--pill: the rail is already
   the container, and a pill inside it is a box inside a box.

   Zones are opted in BY CLASS rather than matched with `.mh-rail > *`. A
   blanket child rule that sets display/position silently re-flows any
   out-of-flow child (a tooltip, a badge), which is a mistake this codebase has
   already paid for once.
   ========================================================================== */
.mh-rail {
    display: flex;
    align-items: stretch;   /* equal-height zones, so dividers run edge to edge */
    gap: 0;                 /* the dividers separate the zones, not whitespace */
    flex-shrink: 0;
    max-width: 100%;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.03);
    /* clip a selected segment's fill to the rail's corners; auto on x so a rail
       that outgrows a narrow header scrolls as ONE unit instead of wrapping into
       the loose row this component exists to replace */
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.mh-rail::-webkit-scrollbar {
    display: none;
}

.mh-rail__zone {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;         /* scroll rather than squash (house rule) */
    padding: 7px 14px;
    white-space: nowrap;
    /* 44px touch floor (house rule): the zone is the tap target when it carries a
       single control (e.g. .mh-rail__go), and measured 34px. */
    min-height: 44px;
}

/* Two divider weights, because there are two levels here: a ZONE break says
   "different kind of thing", a CELL break inside a segment says "another option
   of the same kind". One weight for both would flatten the rail into an
   undifferentiated grid of cells. */
.mh-rail__zone + .mh-rail__zone {
    border-left: 1px solid var(--border);
}

/* Facts: annotation, not a control. Muted and lighter than anything clickable
   beside them, so weight alone tells you which parts respond to a click. */
.mh-rail__facts {
    font-size: 0.8125rem;
    font-weight: var(--weight-medium);
    color: var(--text-muted, rgba(var(--fg-rgb, 255, 255, 255), 0.55));
    gap: 14px;
}

/* No margin here on purpose. A fact that mixes an icon with text is a flex box
   at the site, and a flex box DROPS the leading whitespace of an anonymous text
   item — so "<i/> 29 posts" renders as "29posts" unless the fact sets its own
   gap. Spacing inside a fact belongs to the fact; the zone only spaces facts
   from each other. */
.mh-rail__facts i {
    opacity: 0.75;
}

/* Segment holder: the buttons supply their own padding so each fills its cell
   edge to edge and a selected segment reads as part of the rail. */
.mh-rail__seg {
    padding: 0;
    gap: 0;
}

.mh-rail__tab {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 7px 16px;
    border: 0;
    background: none;
    font-family: inherit;
    font-size: 0.8125rem;
    font-weight: var(--weight-semibold);
    letter-spacing: 0.02em;
    color: var(--text-muted, rgba(var(--fg-rgb, 255, 255, 255), 0.55));
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.25s ease, background 0.25s ease;
    /* 44px touch floor (house rule): 7px padding on a 13px label measured 33px. */
    min-height: 44px;
}

.mh-rail__tab + .mh-rail__tab {
    border-left: 1px solid var(--border-subtle);
}

.mh-rail__tab:hover {
    color: var(--text);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.04);
}

/* Selected segment: a TINT of its own accent rather than a solid fill, matching
   the icon-tile decision — solid competes with the label it carries, a tint
   reads as the cell being lit. Each button may set --mh-rail-accent so a
   two-media toggle can keep one hue per side. */
.mh-rail__tab.is-active {
    color: var(--mh-rail-accent, var(--profile-primary, var(--accent-color, var(--brand-blue))));
    background: color-mix(in srgb, currentColor 16%, transparent);
}

.mh-rail__tab i {
    font-size: 0.9em;
    opacity: 0.8;
}

.mh-rail__tab.is-active i {
    opacity: 1;
}

/* Terminal navigation zone. Always last, so the arrow lands at the rail's end
   and "there is more this way" is always in the same place. */
.mh-rail__go {
    text-decoration: none;
}

.mh-rail__go:hover {
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.05);
    text-decoration: none;
}

/* --stacked: TWO ROWS, because one long rail is not a shape.

   A rail carrying a media toggle AND category tabs AND a view-more reads as one
   undifferentiated band, and the two control groups in it are not peers:
   anime-vs-manga changes what the whole shelf IS, while the category tabs pick
   within it. Stacking says that. The parent choice takes the first row alone;
   the categories and the way out share the second, split by the rail's own
   divider.

       ROW 1   [ Anime | Manga ]
       ROW 2   [ Top | Popular | Airing | Upcoming ] | View More >

   THE SIZING IS THE WHOLE DIFFICULTY, and both obvious spellings fail. Measured
   on the home Discover rail:

     grid + minmax(0, 1fr)   first column resolved to 0px, every zone crushed to
                             44px, 270px of page overflow.
     flex-wrap + basis:100%  a percentage basis against a content-sized container
                             resolved to 0, so the rail computed 2px wide and all
                             three zones stacked on separate rows.

   Both because this rail's width comes from its CONTENT -- .mh-sechead__actions
   sits in a column flex container that centres rather than stretches, so there
   is no definite width for a percentage or a 1fr to resolve against. Hence
   content-sized columns plus an explicit `width: max-content`, which gives the
   grid the intrinsic size the flex version got for free.

   Keyed on child ORDER, not on any page's class names, so the modifier belongs
   to the component: the first child is promoted to a full-width row, everything
   after it shares the second in markup order.

   The zone divider rule (.mh-rail__zone + .mh-rail__zone) is a DOM-order
   selector, so whichever zone opens row 2 would otherwise carry a LEFT border --
   a vertical rule floating at the start of a line. It trades that for a top
   border, which is the line between the rows. Everything after it keeps its left
   divider: that is the vertical bar between the tabs and the way out. */
.mh-rail--stacked {
    display: grid;
    grid-template-columns: auto auto;
    justify-content: start;
    width: max-content;
    max-width: 100%;
    /* The base rail scrolls as one unit; a rail two rows tall must not, or the
       second row scrolls out from under the first. */
    overflow-x: visible;
}

.mh-rail--stacked > :first-child {
    grid-column: 1 / -1;
    /* The row is full-width, but the CONTROL in it is not -- the media toggle
       measured 514px of row holding ~215px of buttons, so it sat hard left with
       300px of dead space beside it. Centring the pair is what makes the two
       rows read as one instrument rather than a wide empty bar above a full
       one. Set on the ZONE, whose children are the buttons; the row itself
       still spans, so the divider beneath it runs the rail's full width. */
    justify-content: center;
}

.mh-rail--stacked > :nth-child(2) {
    border-left: 0;
    border-top: 1px solid var(--border);
}

/* Anything sharing row 2 takes the top rule as well, so the line runs the rail's
   full width instead of stopping at the divider. */
.mh-rail--stacked > :nth-child(n + 3) {
    border-top: 1px solid var(--border);
}

/* A rail with nothing after the categories: let them use the whole row rather
   than leaving a dead column where a view-more would have been. */
.mh-rail--stacked > :nth-child(2):last-child {
    grid-column: 1 / -1;
}

/* ONE COLUMN ON A PHONE, so the way out gets a row of its own.

   Two rows is right on a wide screen and wrong on a narrow one. MEASURED at
   375px: the tab row held its 273px and the view-more was squeezed to 61px
   against the 114px it needs -- so "View More" overlapped "Upcoming" rather
   than either one giving way. The rail cannot solve that by sharing a row
   differently; there is not enough room for both.

   So on a phone each zone takes its own row: the parent choice, the categories,
   then the way out. That reads as the same three tiers the desktop rail has,
   stacked.

   NOT solved by dropping the labels to icons, which was the other option. This
   rail already decided that question in the other direction at this exact
   breakpoint -- .amp-header-controls .mh-rail__tab i is display:none below
   768px, keeping the words. A flame for Popular is legible enough; a broadcast
   tower for Airing and a calendar for Upcoming are guesses.

   The categories row also becomes a real scroller here, which is the house rule
   for a row of pills: it survives 320px, and a fifth category later, without
   squashing anything. It could not scroll while it shared a row, because the
   grid track it sat in was what got squeezed. */
@media (max-width: 768px) {
    .mh-rail--stacked {
        grid-template-columns: minmax(0, 1fr);
    }

    /* Every zone now OPENS a row, so none of them carries a left divider; the
       top border is the line between rows. */
    .mh-rail--stacked > :nth-child(n + 2) {
        border-left: 0;
        border-top: 1px solid var(--border);
    }

    /* Pills scroll rather than squash (house rule). The zone's children already
       set flex-shrink: 0, so this is the container half of that contract. */
    .mh-rail--stacked > :nth-child(2) {
        overflow-x: auto;
        scrollbar-width: none;
        -ms-overflow-style: none;
    }

    .mh-rail--stacked > :nth-child(2)::-webkit-scrollbar {
        display: none;
    }
}

/* A centered header (--centered / the <=640px stack) puts the rail on its own
   line; keep it from spanning edge to edge and looking like a toolbar. */
@media (max-width: 640px) {
    .mh-rail {
        max-width: 100%;
    }
}

.mh-sechead--centered {
    flex-direction: column;
    align-items: center;
    text-align: center;
}

.mh-sechead--centered .mh-sechead__main {
    align-items: center;
}

/* Stacking on a phone is right; CENTRING is not. This block used to set
   align-items/text-align: center, which made every section header on the site
   read as --centered below 640px - turning an OPT-IN variant into the default
   for the 41 templates that never asked for it, and leaving their titles on
   no shared left edge. Titles now keep the house left edge; the ACTIONS slot
   stays centred so a lone "View all" reads as a deliberate control rather
   than a stranded link. */
@media (max-width: 640px) {
    .mh-sechead {
        flex-direction: column;
        align-items: stretch;
        gap: 14px;
    }

    .mh-sechead__main {
        align-items: flex-start;
    }

    /* CENTRED, BUT SAFELY.

       Plain `center` is correct for a loose row of controls and WRONG the moment the
       actions are a .mh-rail, because the rail is a scroll container: centring an
       overflowing scroller splits the overflow across BOTH edges, and the start half
       is then unreachable - scrollLeft is already 0 and cannot go negative. Measured
       on the home page at 375px: 429px of rail in a 333px box put the first tab
       ('Top') at x = -80, permanently off-screen.

       `safe` falls back to start-alignment exactly when the content overflows, which
       is the case that breaks. The plain declaration above it is the fallback for
       engines that do not parse the keyword - they keep today's behaviour rather than
       losing the centring entirely. */
    .mh-sechead__actions {
        justify-content: center;
        justify-content: safe center;
    }

    /* The opt-in variant ties with the rules above at equal specificity and
       would lose on source order, so it is re-asserted as a COMPOUND selector
       (0-2-0) and wins regardless of where it sits in the file. */
    .mh-sechead.mh-sechead--centered {
        align-items: center;
        text-align: center;
    }

    .mh-sechead.mh-sechead--centered .mh-sechead__main {
        align-items: center;
    }
}

/* --hero: the sanctioned LARGER tier for a page's flagship discovery
   sections (e.g. home Discover / Featured Watchlists). Two tiers exist by
   design; per-section font-size forks do not. */
.mh-sechead--hero .mh-sechead__title {
    font-size: var(--mh-sechead-title-size, clamp(1.7rem, 3.5vw, 2.2rem));
    font-weight: var(--weight-extrabold);
    letter-spacing: -0.02em;
}

/* ==========================================================================
   Comment notices — dismissable banners above a comment composer.

   Shared, not template-local, because the same banner appears on every comment
   surface. Namespaced `cnotice-` so it cannot collide with a template's own
   `.notice-*` or `.banner-*` classes (invariant #31).
   ========================================================================== */
.cnotice-stack {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 12px;
}

.cnotice {
    /* Each type carries its own accent through one variable, so a new variant is three
       colour declarations rather than a new copy of the whole block. */
    --cn-accent: var(--brand-blue);
    --cn-tint: rgba(52, 152, 219, 0.13);

    position: relative;
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 14px 15px;
    border-radius: var(--radius-lg);
    overflow: hidden;
    border: 1px solid var(--border-subtle);
    /* Tinted toward its own colour on the left and fading out, so the type is legible
       from the surface itself rather than only from a 3px rail. */
    background:
        linear-gradient(100deg, var(--cn-tint) 0%, rgba(var(--fg-rgb, 255, 255, 255), 0.02) 62%),
        var(--surface-2);
}

/* The rail is drawn rather than bordered, so it can carry a gradient. */
.cnotice::before {
    content: '';
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    width: 3px;
    background: linear-gradient(180deg, var(--cn-accent), transparent 140%);
}

.cnotice--success {
    --cn-accent: var(--success);
    --cn-tint: rgba(46, 204, 113, 0.13);
}

.cnotice--warn {
    --cn-accent: #f5c451;
    --cn-tint: rgba(245, 196, 81, 0.13);
}

/* Oversized glyph bleeding off the right edge. Decorative and deliberately faint: it
   gives each type a silhouette you can recognise before reading a word, and sits under
   the text rather than behind it because the copy never reaches that far. */
.cnotice__ghost {
    position: absolute;
    /* Pushed further off-edge than it looks like it needs: at -14px the glyph sat
       directly behind the dismiss button and muddied it. Most of the shape belongs
       outside the card, leaving a recognisable sliver rather than a whole icon. */
    right: -30px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 6rem;
    line-height: var(--leading-none);
    color: var(--cn-accent);
    opacity: 0.07;
    pointer-events: none;
}

.cnotice__badge {
    width: 34px;
    height: 34px;
    flex-shrink: 0;
    border-radius: 11px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 0.9rem;
    color: var(--cn-accent);
    background: color-mix(in srgb, var(--cn-accent) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--cn-accent) 34%, transparent);
}

.cnotice__body {
    flex: 1;
    min-width: 0;
    position: relative;
}

.cnotice__title {
    display: block;
    font-size: 0.84rem;
    font-weight: var(--weight-semibold);
    color: #fff;
    letter-spacing: 0.01em;
}

.cnotice__text {
    display: block;
    font-size: 0.76rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.62);
    line-height: var(--leading-normal);
    word-break: break-word;
}

/* Site-wide close treatment: circular, rotates and reddens on hover. */
.cnotice__x {
    position: relative;
    width: 26px;
    height: 26px;
    flex-shrink: 0;
    padding: 0;
    border-radius: var(--radius-circle);
    border: 1px solid var(--border);
    background: transparent;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.4);
    font-size: 0.7rem;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    transition: background 0.25s var(--ease-spring), color 0.25s var(--ease-spring), transform 0.25s var(--ease-spring);
}

.cnotice__x:hover {
    transform: rotate(90deg);
    background: rgba(231, 76, 60, 0.9);
    color: #fff;
}

@media (max-width: 480px) {
    /* The ghost is the first thing to go: at this width it competes with the copy
       instead of framing it. */
    .cnotice__ghost {
        display: none;
    }

    .cnotice {
        padding: 12px 13px;
        gap: 10px;
    }
}


/* ---- CLUB_TIER: a milestone card, not a line of text ---- */
.cnotice-tier {
    display: block;
    padding: 0;
    overflow: hidden;
    border-left-width: 1px;
    border-color: var(--border);
    background-color: var(--surface-2);
    background-size: cover;
    /* Anchored RIGHT: the mask clears on that side, so the part of the banner worth
       seeing should be the part that ends up there. */
    background-position: right center;
}

.cnotice-tier__head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 14px 10px;
}

.cnotice-tier__crest {
    width: 44px;
    height: 44px;
    flex-shrink: 0;
    border-radius: var(--radius-md);
    object-fit: cover;
    border: 1px solid rgba(var(--fg-rgb, 255, 255, 255), 0.16);
    background: var(--surface-3);
}

.cnotice-tier__crest--initial {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 1.1rem;
    font-weight: var(--weight-bold);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.55);
}

.cnotice-tier__titles {
    flex: 1;
    min-width: 0;
}

.cnotice-tier__eyebrow {
    display: block;
    font-size: 0.6rem;
    font-weight: var(--weight-bold);
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.4);
}

.cnotice-tier__journey {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex-wrap: wrap;
}

.cnotice-tier__from {
    font-size: 0.82rem;
    font-weight: var(--weight-medium);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.4);
}

.cnotice-tier__arrow {
    font-size: 0.62rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.3);
}

.cnotice-tier__to {
    font-size: 1.05rem;
    font-weight: var(--weight-bold);
    color: #fff;
}

/* Tier accent, matching the leaderboard palette: one progression, every surface. */
.cnotice-tier--NEW .cnotice-tier__to { color: #8395a7; }
.cnotice-tier--RISING .cnotice-tier__to { color: var(--brand-blue); }
.cnotice-tier--ESTABLISHED .cnotice-tier__to { color: #1abc9c; }
.cnotice-tier--THRIVING .cnotice-tier__to { color: var(--brand-purple); }

.cnotice-tier--LEGENDARY .cnotice-tier__to {
    background: linear-gradient(135deg, #f5c451, var(--brand-purple));
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
}

/* Perks read as one line of values divided by hairlines, not a row of pills (owner, 2026-09-13). */
.cnotice-tier__perks {
    display: flex;
    flex-wrap: nowrap;
    padding: 0 14px 14px;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    -webkit-overflow-scrolling: touch;
}

.cnotice-tier__perks::-webkit-scrollbar {
    display: none;
}

.cnotice-tier__perk {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
    white-space: nowrap;
    padding: 0 12px;
    font-size: 0.7rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.55);
}

.cnotice-tier__perk:first-child {
    padding-left: 0;
}

.cnotice-tier__perk + .cnotice-tier__perk {
    border-left: 1px solid var(--border-subtle);
}

.cnotice-tier__perk i {
    font-size: 0.68rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.4);
}

.cnotice-tier__perk b {
    color: #fff;
    font-weight: var(--weight-bold);
}

/* ==========================================================================
   GIF PICKER — BASE STATE ONLY
   --------------------------------------------------------------------------
   The picker's full styling lives with the comment system, which arrives as a
   FRAGMENT (episode discussions load it over AJAX). The modal itself is
   appended to document.body by comment-gif-handler.js and is never removed —
   so when the fragment is torn down, the element outlives the stylesheet that
   hides it and lays out as an ordinary ~400px block at the end of the page.
   That is the "huge empty space below the footer".

   Only the out-of-flow BASE state lives here, globally, so the modal can never
   fall back into document flow no matter which page it is stranded on. The
   fragment's own rules still own everything else, including `.open` — they are
   more specific in file order there, and this sets nothing they need to undo
   beyond what `.gif-picker-modal.open` already overrides.
   ========================================================================== */
.gif-picker-modal {
    position: fixed;
    inset: 0;
    z-index: 10000;
    opacity: 0;
    visibility: hidden;
}

.gif-picker-modal.open {
    opacity: 1;
    visibility: visible;
}

/* ==========================================================================
   NAV RAILS (.topnav [data-nav-rail])
   ==========================================================================
   The top nav is a STACK, not a single bar. Anything belonging to the page's top
   chrome -- the up-navigation row, the CMS context header -- registers as a rail
   and is adopted into nav.topnav as a full-width row beneath the main one.

   Why: before this, each pinned itself with position:fixed/sticky at a hardcoded
   73px, the nav's logged-out desktop height. None could see the others, so the
   up-nav strip and .cms-header both claimed the same band -- and every offset was
   wrong the moment the nav's height changed, which it does when the avatar row
   wraps, on mobile, or when a rail is present at all. As real children they just
   stack, and the nav's own box becomes the single measured truth.

   Register with data-nav-rail="<order>"; lower sits higher. The adopter lives in
   the nav itself, so it runs whether or not any given rail is on the page.

   A rail paints NO ground of its own. The nav already has three stacked paint
   layers -- its own `background`, the ::before readability gradient, and the
   ::after blur that the mega/search panels switch on -- and all three are
   `inset: 0` on .topnav, so they span the FULL stack the moment a rail is a
   child of it. Giving a rail its own background put a flat slab on top of that:
   at the top of the page the nav was a soft gradient while the rail was a hard
   0.55, and on scroll the nav went to 0.88 while the rail stayed put. Two
   surfaces disagreeing about the same scroll state.

   Inheriting instead of imitating means the whole stack fades, solidifies and
   blurs as ONE element, in every state, with no second implementation to keep
   in sync. The hairline is the only thing a rail contributes. */
.topnav [data-nav-rail] {
    /* Bleed through the nav's own 15px padding: the hairline then runs edge
       to edge, and the rail's content lands on the SAME 15px gutter as the
       logo above it -- stacking rail padding on nav padding put it 30px in. */
    width: calc(100% + var(--container-pad) * 2);
    margin: 0 calc(var(--container-pad) * -1);
    display: flex;
    align-items: center;
    /* 34px, down from 45. A rail is a 13px text row; 40px min-height plus 6px
       padding was sized like a button bar. Chrome went 73 -> 118px with one
       rail, and the cheapest third of that back costs nothing. Mobile restores
       a 44px target below -- see the media query. */
    min-height: 34px;
    padding: 4px var(--container-pad);
    background: transparent;
    /* Collapse animates height, so it must clip while shrinking. `clip` rather
       than `hidden`: hidden makes this a scroll container, which re-anchors any
       descendant sticky/view() timeline to a scrollport that never scrolls
       (CLAUDE.md #40). */
    overflow: clip;
    transition:
        height var(--dur-base) var(--ease-standard),
        min-height var(--dur-base) var(--ease-standard),
        padding-top var(--dur-base) var(--ease-standard),
        padding-bottom var(--dur-base) var(--ease-standard),
        border-top-width var(--dur-base) var(--ease-standard),
        opacity var(--dur-fast) linear;
    /* --border, not --border-subtle: this is a STRUCTURAL divider between bands
       of chrome, not a card edge. At 0.06 alpha over the nav's dark ground the
       separation was invisible in practice, which defeats the point of stacking
       them as distinct rows. */
    border-top: 1px solid var(--border);
    box-sizing: border-box;
}

/* The readability gradient is tuned for a ~73px nav and fades to fully
   transparent at its bottom edge. Stretched over a 118px or 180px stack that
   leaves the LOWEST row sitting on nothing, which is unreadable over hero
   artwork -- the exact problem the gradient exists to solve, reintroduced by
   the stack being taller. So when the nav carries rails, the gradient holds a
   floor instead of reaching zero. It still FADES, so the nav keeps its
   signature soft top edge rather than becoming a slab.
   :has() rather than a JS class: the adopter moves rails in during parse, and a
   class would have to be kept in sync with that. This cannot drift. */
/* ── Scroll collapse ─────────────────────────────────────────────────────────
   OPT-IN, per rail. A rail that carries ACTIONS must never hide: collapsing
   .cms-header would take Submit and Preview away from an editor scrolling a
   long form, which is a worse bug than the height it saves. Only rails marked
   data-nav-rail-collapse="scroll" participate -- today that is the up-nav,
   whose content is one link the user can always get back by scrolling up.

   The rail is shown at the top of the document (where you land, and where you
   are most likely to want "up"), hides as you read downward, and returns the
   instant you scroll up -- a gesture users already make, so the affordance
   teaches itself rather than needing to be documented.

   `height` is driven by --rail-h, which the nav measures, because a transition
   from `auto` does not animate. */
.topnav [data-nav-rail][data-nav-rail-collapse] {
    height: var(--rail-h, auto);
}
.topnav.rails-collapsed [data-nav-rail][data-nav-rail-collapse] {
    height: 0;
    min-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    border-top-width: 0;
    opacity: 0;
}

/* Restores the 44px touch target the thinning gave up. Desktop is a pointer
   surface where 34px is comfortable; a finger is not a cursor. */
@media (max-width: 768px) {
    .topnav [data-nav-rail] {
        min-height: 44px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .topnav [data-nav-rail] {
        transition: none;
    }
}

.topnav:has([data-nav-rail])::before {
    /* Overhang the nav's bottom edge so the fade FINISHES in open page, not at
       the chrome boundary. Holding a floor to the last pixel (the first attempt
       here) keeps the lowest row readable but ends the scrim on a hard line --
       the nav's whole character at rest is that its bottom edge dissolves. With
       30px of overhang the gradient is still ~0.5 where the bottom rail's text
       sits and reaches zero below the chrome, so readability and the soft edge
       stop being a trade.
       Beats `inset: 0` from the base rule on specificity (0,2,1 vs 0,1,1), not
       source order -- base.ftlh's inline <style> loads after this file. */
    bottom: -30px;
    background: linear-gradient(
        180deg,
        rgba(0, 0, 0, 0.74) 0%,
        rgba(0, 0, 0, 0.68) 30%,
        rgba(0, 0, 0, 0.62) 55%,
        rgba(0, 0, 0, 0.52) 79%,
        rgba(0, 0, 0, 0.00) 100%
    );
}

/* Out-of-flow fallback for the frame before adoption, and for any page with no
   nav: never let an unadopted rail push a full-bleed hero down. */
/* Never fires today: all 83 up-nav carriers render the nav, so every rail is
   adopted. Kept so a future nav-less page degrades to a positioned bar rather
   than an invisible transparent strip -- and this one DOES need its own ground,
   precisely because there is no nav painting behind it. */
[data-nav-rail]:not(.topnav [data-nav-rail]) {
    position: fixed;
    top: var(--chrome-bottom, 80px);
    left: 0;
    right: 0;
    z-index: 999;
    background: rgba(0, 0, 0, 0.82);
    backdrop-filter: blur(var(--glass-blur-md));
    -webkit-backdrop-filter: blur(var(--glass-blur-md));
}

/* ==========================================================================
   UP-NAVIGATION LEAD (.mh-upnav-lead)
   ==========================================================================
   The back/up control, sitting in the nav's MAIN ROW beside the logo.

   It used to be a full-width rail. That cost 35px of chrome height on every
   page carrying one, which in turn invalidated the ~86 pages whose top offset
   is a hardcoded guess at the nav's height. The main row already has roughly
   900px of unused space between the logo and the nav links, so the control is
   free here -- and unlike a collapsing rail it is always visible, which matters
   most in a native shell where there is no browser back at all.

   AFTER the logo, never before: only about half the pages have an up target, so
   a control to the logo's left would shift the logo's x position from page to
   page and make it jump as you navigate.

   Namespaced mh- because .back-link, .back-btn and .back-button are ALL already
   local classes across the templates (#31).

   Spec: docs/design/up-navigation.md */
.mh-upnav-lead {
    display: inline-flex;
    align-items: center;
    /* Fallback is deliberate. --spacing-xs IS defined in design-tokens.css's :root and
       the live server response contains it, yet DevTools reports it unresolved on this
       element — a discrepancy I could not reproduce from the source. An unresolved
       var() does not degrade, it drops the WHOLE declaration (CLAUDE.md #95e), so the
       gap would silently vanish rather than fall back to something sane. The literal
       matches the token, so this changes nothing when the token resolves. */
    gap: var(--spacing-xs, 4px);
    flex-shrink: 0;
    /* 36px: comfortable for a pointer, and short enough to sit inside the nav's
       existing row without adding to its height. The mobile rule below takes it
       to a 44px touch target. */
    min-height: 36px;
    padding: 0;
    /* The nav is justify-content: space-between, so simply inserting a second
       child made it a THIRD distribution point and parked the control at x=464,
       mid-nav, unattached to anything. An auto margin absorbs all free space
       before justify-content gets any, so the logo and this control pack left
       together and everything after it stays pinned right -- which is where the
       nav's own items already were. */
    margin-right: auto;
    /* NO pill. The first version was a bordered capsule and it fought the
       wordmark: the logo is angular and heavy, the capsule soft and light, so
       two competing shapes sat 8px apart with nothing relating them. The
       standing directive covers exactly this -- a pill suits ambient metadata,
       but next to an identity mark it reads as clutter, and the fix is to
       restructure the zone rather than restyle the pill.
       So: a hairline divider, then plain text. "Brand | where you are" is one
       zone with a clear hierarchy, and it defers to the logo instead of
       competing with it. */
    color: var(--text-secondary);
    font-size: 0.8125rem;
    font-weight: var(--weight-semibold);
    text-decoration: none;
    white-space: nowrap;
    transition: color var(--transition-fast);
}

/* The divider is the control's own ::before, not a separate element, so it can
   never be left behind when the control is absent -- on the ~85 pages with no up
   target there is simply nothing here. Shorter than the row on purpose: a
   full-height rule would read as a hard column break. */
.mh-upnav-lead::before {
    content: '';
    flex-shrink: 0;
    width: 1px;
    height: 22px;
    margin: 0 14px;
    background: var(--border);
}

.mh-upnav-lead:hover {
    color: var(--text);
    text-decoration: none;
}

.mh-upnav-lead i {
    font-size: 0.75rem;
    transition: transform var(--transition-fast);
}

/* Transform only -- the label must not shift, or the whole nav row jitters. */
.mh-upnav-lead:hover i {
    transform: translateX(-3px);
}

/* Below ~600px the nav row genuinely runs out of width, so the control becomes
   icon-only and square. The label is kept for as long as it fits because the
   destination NAME is the point: a page declares its parent so the control can
   never promise somewhere it will not go, and an unlabelled arrow gives that
   up. aria-label carries the destination in both states. */
@media (max-width: 600px) {
    .mh-upnav-lead__label {
        display: none;
    }
    /* The divider stays -- it is what separates the arrow from the wordmark --
       but its gutters tighten, because at 375px 28px of margin is real estate
       the nav does not have. */
    .mh-upnav-lead::before {
        margin: 0 10px;
    }
    .mh-upnav-lead i {
        /* Restores the touch target the label was providing. */
        min-width: 32px;
        text-align: center;
    }
}

@media (max-width: 768px) {
    .mh-upnav-lead {
        min-height: 44px;
    }
}

/* ==========================================================================
   BULK SELECT — shared multi-select engine (static/js/bulk-select.js)
   --------------------------------------------------------------------------
   Every class is mh- prefixed. The collection page already defines its own
   .cd-select-checkbox, and a template's inline <style> loads AFTER this file,
   so an unprefixed shared name would be silently overridden per-property by
   whichever stylesheet wins the cascade (invariant #31).

   The checkbox overlay is injected by JS into whatever wrapper a surface
   nominates, so it positions against that wrapper without assuming anything
   else about it.
   ========================================================================== */

.mh-select-checkbox {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 5;
    width: 26px;
    height: 26px;
    border-radius: var(--radius-circle);
    border: 2px solid rgba(var(--fg-rgb, 255, 255, 255), 0.55);
    background: rgba(0, 0, 0, 0.55);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    transition: background var(--dur-base) ease, border-color var(--dur-base) ease, transform var(--dur-base) ease;
}

.mh-select-checkbox i {
    font-size: 0.75rem;
    color: #fff;
    opacity: 0;
    transition: opacity var(--dur-fast) ease;
}

/* The engine adds .mh-selectable to each item it decorates, so this file
   never has to name a surface's own card classes — the containing block for
   the absolute overlay follows the engine rather than the page. */
.mh-selectable {
    position: relative;
    cursor: pointer;
    transition: outline var(--transition-fast), transform var(--transition-fast);
}

.mh-selectable:hover {
    outline: 2px solid rgba(52, 152, 219, 0.4);
    outline-offset: -2px;
}

/* --------------------------------------------------------------------------
   Carried verbatim from collection-bulk-select.js's stylesheet, which this
   engine supersedes. Each of these suppresses an affordance that would
   otherwise fight selection, and none is obvious from first principles — the
   reason they are here is that the shipped feature needed them.
   -------------------------------------------------------------------------- */

/* A grid card's hover drawer must not open while selecting: a click is a
   selection, not a drawer reveal. */
body.mh-select-active .grid-card-hover-drawer {
    display: none !important;
}

/* The status badge's popover would swallow the click before it reaches the
   card. The badge stays visible as a label, inert. */
body.mh-select-active .status-badge-container {
    pointer-events: none;
}

/* ...but the row's own action buttons stay reachable, which the engine's
   click handler also honours via its [data-action] bail-out. */
body.mh-select-active .status-action-btn {
    pointer-events: auto;
}

.is-mh-selected .mh-select-checkbox {
    background: var(--brand-blue);
    border-color: var(--brand-blue);
    transform: scale(1.05);
}

.is-mh-selected .mh-select-checkbox i {
    opacity: 1;
}

.is-mh-selected {
    outline: 2px solid var(--brand-blue);
    outline-offset: 2px;
    border-radius: var(--radius-md);
}

/* --------------------------------------------------------------------------
   The action drawer
   -------------------------------------------------------------------------- */

.mh-select-drawer {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(var(--mh-bottom-nav-space, 0px) + 16px);
    z-index: 1200;
    display: flex;
    align-items: center;
    gap: 16px;
    max-width: calc(100vw - 30px);
    padding: 12px 16px;
    border-radius: var(--radius-lg);
    background: var(--glass-bg);
    backdrop-filter: blur(var(--glass-blur, 20px));
    -webkit-backdrop-filter: blur(var(--glass-blur, 20px));
    border: 1px solid var(--border);
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
}

.mh-select-drawer__info {
    display: flex;
    align-items: center;
    gap: 12px;
    flex-shrink: 0;
}

.mh-select-drawer__count {
    font-weight: var(--weight-medium);
    font-size: 0.875rem;
    color: var(--text);
    white-space: nowrap;
}

.mh-select-drawer__link {
    background: none;
    border: none;
    padding: 0;
    font-size: 0.8125rem;
    font-weight: var(--weight-medium);
    color: var(--brand-blue);
    cursor: pointer;
    white-space: nowrap;
    transition: color var(--dur-base) ease;
}

.mh-select-drawer__link:hover {
    color: var(--brand-blue-dark);
}

.mh-select-drawer__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: nowrap;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.mh-select-drawer__actions::-webkit-scrollbar {
    display: none;
}

.mh-select-action {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 9px 14px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.06);
    color: var(--text);
    font-size: 0.8125rem;
    font-weight: var(--weight-medium);
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--dur-base) ease, border-color var(--dur-base) ease, opacity var(--dur-base) ease, transform var(--dur-base) ease;
}

.mh-select-action:hover:not(:disabled) {
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.12);
    transform: translateY(-1px);
}

.mh-select-action:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.mh-select-action--danger:hover:not(:disabled) {
    background: var(--danger);
    border-color: var(--danger);
}

.mh-select-drawer__done {
    flex-shrink: 0;
    width: 32px;
    height: 32px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-circle);
    border: 1px solid var(--border);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.06);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.75);
    cursor: pointer;
    transition: background 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.mh-select-drawer__done:hover {
    transform: rotate(90deg);
    background: rgba(231, 76, 60, 0.9);
    color: #fff;
}

/* ==========================================================================
   PROGRESS TIMELINE — the session log / rewatch history modal
   --------------------------------------------------------------------------
   The overlay is appended to document.body by progress-timeline.js and never
   removed, so its base out-of-flow state MUST live here rather than in any
   page's own <style> block. Invariant #84: a body-appended element that
   depends on a fragment's stylesheet survives that fragment's teardown, falls
   back to position:static, and adds a screen of scrollable nothing to the end
   of the document.
   ========================================================================== */

.mh-timeline-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    margin-left: 6px;
    padding: 0;
    border: none;
    border-radius: var(--radius-circle);
    background: transparent;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.4);
    font-size: 11px;
    cursor: pointer;
    transition: background var(--dur-base) ease, color var(--dur-base) ease;
}

.mh-timeline-btn:hover {
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.08);
    color: var(--brand-blue);
}

/* A title in WATCHING with no activity for months. Marked quietly — a loud
   treatment across a dozen cards reads as an accusation and gets ignored.
   A hairline accent, not a colour wash: the card must still read as a normal
   entry the user can ignore. */
.is-mh-stale {
    box-shadow: inset 2px 0 0 rgba(var(--amber-500-rgb), 0.55);
}

.mh-stale-chip {
    display: inline-flex;
    align-items: center;
    margin-top: 6px;
    padding: 4px 10px;
    border-radius: var(--radius-pill, 999px);
    border: 1px solid rgba(var(--amber-500-rgb), 0.35);
    background: rgba(var(--amber-500-rgb), 0.12);
    color: #fbbf24;
    font-size: 0.7rem;
    font-weight: var(--weight-medium);
    white-space: nowrap;
    cursor: pointer;
    transition: background var(--dur-base) ease, border-color var(--dur-base) ease;
}

.mh-stale-chip:hover {
    background: rgba(var(--amber-500-rgb), 0.22);
    border-color: rgba(var(--amber-500-rgb), 0.55);
}

@media (max-width: 768px) {
    .mh-stale-chip {
        min-height: 32px;
    }
}

.mh-timeline-overlay {
    position: fixed;
    inset: 0;
    z-index: 1400;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 15px;
    background: rgba(0, 0, 0, 0.7);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}

/* The [hidden] attribute is how the modal is closed, so the display rule is
   scoped with :not([hidden]). An unconditional author `display: flex` beats
   the UA's [hidden] rule regardless of specificity, which silently turns
   every `el.hidden = true` into a no-op. */
.mh-timeline-overlay[hidden] {
    display: none;
}

.mh-timeline {
    width: 100%;
    max-width: 460px;
    max-height: 80vh;
    display: flex;
    flex-direction: column;
    border-radius: var(--radius-lg);
    background: var(--surface-grad);
    border: 1px solid var(--border);
    box-shadow: 0 24px 70px rgba(0, 0, 0, 0.65);
    overflow: hidden;
}

.mh-timeline__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 16px 20px;
    border-bottom: 1px solid var(--border-subtle);
    flex-shrink: 0;
}

.mh-timeline__title {
    margin: 0;
    font-size: 1rem;
    font-weight: var(--weight-semibold);
    color: #fff;
}

.mh-timeline__close {
    width: 32px;
    height: 32px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-circle);
    border: 1px solid var(--border);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.05);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.7);
    cursor: pointer;
    transition: background 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), color 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.mh-timeline__close:hover {
    transform: rotate(90deg);
    background: rgba(231, 76, 60, 0.9);
    color: #fff;
}

.mh-timeline__body {
    padding: 16px 20px 20px;
    overflow-y: auto;
    scrollbar-width: thin;
}

.mh-timeline__loading {
    display: flex;
    justify-content: center;
    padding: 32px 0;
}

.mh-timeline__summary {
    margin: 0 0 14px 0;
    font-size: 0.8rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.5);
}

.mh-timeline__run {
    padding: 14px 0;
    border-bottom: 1px solid var(--border-subtle);
}

.mh-timeline__run:last-child {
    border-bottom: none;
    padding-bottom: 0;
}

.mh-timeline__run-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 4px;
}

.mh-timeline__run-name {
    font-size: 0.925rem;
    font-weight: var(--weight-semibold);
    color: #fff;
}

.mh-timeline__badge {
    flex-shrink: 0;
    padding: 3px 9px;
    border-radius: var(--radius-pill, 999px);
    font-size: 0.7rem;
    font-weight: var(--weight-medium);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.08);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.7);
}

.mh-timeline__badge--completed {
    background: rgba(16, 185, 129, 0.18);
    color: #34d399;
}

.mh-timeline__badge--dropped {
    background: rgba(231, 76, 60, 0.18);
    color: #f87171;
}

.mh-timeline__badge--in_progress {
    background: rgba(52, 152, 219, 0.18);
    color: #60a5fa;
}

.mh-timeline__dates {
    margin: 0 0 12px 0;
    font-size: 0.8rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.5);
}

.mh-timeline__stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
    gap: 10px;
}

.mh-timeline__stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 9px 10px;
    border-radius: var(--radius-sm);
    background: var(--glass-bg);
    border: 1px solid var(--border-subtle);
}

.mh-timeline__stat-value {
    font-size: 0.95rem;
    font-weight: var(--weight-semibold);
    color: #fff;
    font-variant-numeric: tabular-nums;
}

.mh-timeline__stat-label {
    font-size: 0.7rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.45);
}

.mh-timeline__note {
    margin: 10px 0 0 0;
    font-size: 0.72rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.4);
    font-style: italic;
}

.mh-timeline__empty {
    text-align: center;
    padding: 28px 12px;
}

.mh-timeline__empty i {
    font-size: 1.75rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.2);
    margin-bottom: 12px;
}

.mh-timeline__empty-title {
    margin: 0 0 6px 0;
    font-size: 0.95rem;
    font-weight: var(--weight-semibold);
    color: #fff;
}

.mh-timeline__empty-body {
    margin: 0;
    font-size: 0.8rem;
    line-height: var(--leading-normal);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.5);
}

@media (max-width: 768px) {
    .mh-timeline {
        max-height: 85vh;
    }

    .mh-timeline__close {
        width: 44px;
        height: 44px;
    }

    .mh-timeline-btn {
        width: 32px;
        height: 32px;
    }
}

/* --------------------------------------------------------------------------
   The status picker opened by the "Set status" action.
   A custom component rather than a native <select>, per the project rule that
   a dropdown is a button + panel; it also has to be usable at 375px, which a
   native select inside a fixed drawer is not.
   -------------------------------------------------------------------------- */

.mh-select-picker-overlay {
    position: fixed;
    inset: 0;
    z-index: 1300;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 15px;
    background: rgba(0, 0, 0, 0.6);
    backdrop-filter: blur(4px);
    -webkit-backdrop-filter: blur(4px);
}

.mh-select-picker {
    width: 100%;
    max-width: 320px;
    padding: 20px;
    border-radius: var(--radius-lg);
    background: var(--surface-2);
    border: 1px solid var(--border);
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
}

.mh-select-picker__title {
    margin: 0 0 14px 0;
    font-size: 1rem;
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.mh-select-picker__caption {
    margin: -8px 0 14px 0;
    font-size: 0.78rem;
    line-height: 1.45;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.45);
}

.mh-select-picker__options {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.mh-select-picker__option {
    width: 100%;
    min-height: 44px;
    padding: 11px 14px;
    text-align: left;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: var(--glass-bg);
    color: #fff;
    font-size: 0.9rem;
    font-weight: var(--weight-medium);
    cursor: pointer;
    transition: background var(--dur-base) ease, border-color var(--dur-base) ease;
}

.mh-select-picker__option:hover,
.mh-select-picker__option:focus-visible {
    background: var(--brand-blue);
    border-color: var(--brand-blue);
    outline: none;
}

.mh-select-picker__cancel {
    width: 100%;
    min-height: 44px;
    margin-top: 12px;
    padding: 10px 14px;
    border-radius: var(--radius-sm);
    border: 1px solid var(--border);
    background: transparent;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.7);
    font-size: 0.875rem;
    font-weight: var(--weight-medium);
    cursor: pointer;
    transition: background var(--dur-base) ease, color var(--dur-base) ease;
}

.mh-select-picker__cancel:hover {
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.05);
    color: #fff;
}

/* --------------------------------------------------------------------------
   Mobile — the drawer becomes a full-width sheet clearing the bottom nav.
   --mh-bottom-nav-space is published by mobile-navigation.ftlh: 0 on desktop,
   the bar's real height on mobile, 0 again when the user minimises it.
   -------------------------------------------------------------------------- */

@media (max-width: 768px) {
    .mh-select-drawer {
        left: 15px;
        right: 15px;
        transform: none;
        max-width: none;
        flex-wrap: wrap;
        gap: 10px;
        padding: 12px 14px;
        border-radius: 14px;
    }

    .mh-select-drawer__info {
        flex: 1 1 100%;
        justify-content: space-between;
    }

    .mh-select-drawer__actions {
        flex: 1 1 auto;
        min-width: 0;
    }

    .mh-select-action {
        min-height: 44px;
        padding: 10px 14px;
    }

    .mh-select-drawer__done {
        width: 44px;
        height: 44px;
    }

    .mh-select-drawer__link {
        min-height: 44px;
        display: inline-flex;
        align-items: center;
    }

    .mh-select-checkbox {
        width: 30px;
        height: 30px;
    }
}

/* ── Inline rate-limit notice ────────────────────────────────────────────────
   Rendered by the shared handler in utils.js next to whichever control carries
   data-ratelimit-inline. Hosted on .cnotice--warn so it inherits the accent
   rail, tint and spacing every other notice on the site already has; only the
   bits specific to a countdown live here.

   Namespaced mh- because this is a global component class, and a generic name
   here would collide with the many bespoke local classes of the same shape.

   No entrance animation on purpose. An opacity-based reveal would make the
   animation a precondition for the message existing, and this message appears
   exactly when something already went wrong. */
.mh-ratelimit {
    margin-top: 10px;
    /* The notice belongs to the control it explains, so it must not inherit a
       parent flex row's stretch and end up beside the button. */
    align-self: stretch;
}

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

.mh-ratelimit-countdown {
    font-size: 0.86em;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

/* The hourglass is the only moving part, and it stops for reduced motion. */
.mh-ratelimit .fa-hourglass-half {
    color: #f5c451;
    animation: mh-ratelimit-turn 2.4s ease-in-out infinite;
}

@keyframes mh-ratelimit-turn {
    0%,
    70%,
    100% {
        transform: rotate(0deg);
    }
    80%,
    95% {
        transform: rotate(180deg);
    }
}

@media (prefers-reduced-motion: reduce) {
    .mh-ratelimit .fa-hourglass-half {
        animation: none;
    }
}

/* 44x44 touch floor - .mh-timeline-btn measured 32x32 in a 5px row. Unlike the
   .mh-viewall / .mh-rail floors above (which are unconditional), this one is
   scoped to COARSE pointers: the timeline buttons sit in a dense progress row and
   growing them on desktop would change that row on every list page. */
@media (pointer: coarse) {
    .mh-timeline-btn {
        min-width: 44px;
        min-height: 44px;
    }
}

/* 44x44 touch floor, part 2 - the two SHARED components measured below it.

   .btn is the base button and carries no height of its own: padding 10px/20px
   over a ~17px line box lands it at 36px, so EVERY .btn on the site (37 templates,
   154 uses) was 8px under the house floor. It is not a watch-party defect; that is
   just where it was first measured.

   .mh-empty--compact deliberately drops its CTAs to 40px. That compactness is the
   point of the variant on a desktop card, so it keeps it there and gives the floor
   back to a thumb - the variant stays, the undercut does not.

   Both are COARSE-only, so desktop density is unchanged. */
@media (pointer: coarse) {
    .btn {
        min-height: 44px;
    }

    .mh-empty--compact .mh-empty__cta,
    .mh-empty--compact .mh-empty__cta-secondary {
        min-height: 44px;
    }
}

/* 44x44 touch floor, part 3 - Swiper navigation arrows.

   These are Swiper vendor class names, but ten templates each style them
   independently rather than sharing a rule, and every one lands at 40x40. One
   coarse-only floor here reaches all ten; patching them file by file would leave
   the eleventh short the day someone adds a carousel. */
@media (pointer: coarse) {
    .swiper-button-prev,
    .swiper-button-next {
        min-width: 44px;
        min-height: 44px;
    }
}
/* ==========================================================================
   AUTHOR-LINE PILL - the one geometry for every badge, flair, tag and chip
   that renders beside a username
   --------------------------------------------------------------------------
   Twelve independently hand-rolled pills can land on an author line: the staff
   badge, the chat Admin/Creator badges, the spoiler tag, the watch-party unit
   and topic chips, four post-type flairs (prediction, quiz, this-or-that,
   shipping), the gacha "Drafted" badge, the club tag chip and the collapsed
   comment's reason pill. They agreed on nothing - vertical padding ran 1px to
   3px, radius was --radius-sm, --radius-md or --radius-pill depending on the
   author, font-size ran 0.6rem to 0.7rem, weight was 500 or 600, two different
   box models were in play, and five carried their own margin-left on top of
   their container's gap. Measured on one comment header before this block:
   NINE distinct heights between 18px and 24.93px, and nine distinct tops. The
   row sat on no shared baseline.

   THE SET IS DERIVED, NOT LISTED. Each author line is scoped by what it is
   NOT - the username, the separators, the timestamp. Anything else a zone
   drops in is a pill and inherits the geometry without its author knowing this
   block exists. A hand-typed class list is always one behind (CLAUDE.md #73)
   and the club tag chip proves it: written after the other eight, it had to
   hand-align itself to "the badge's exact 18px box".

   GEOMETRY ONLY. Every pill keeps its own background, border and colour - that
   is its identity. Case is deliberately NOT unified: uppercase at this size is
   a severity signal that SPOILER and a staff role earn and an opinion flair
   does not, and several flairs carry user-authored text that must not be
   shouted.

   TO OVERRIDE, redefine the custom properties on a scope - never re-declare the
   longhands, which now sit at high specificity because of the :not() chains.
   .mh-authorpill is the opt-in for a pill that lives somewhere else entirely.
   ========================================================================== */
:root {
    --authorpill-height: 20px;
    --authorpill-pad-x: 10px;
    --authorpill-gap: 5px;
    --authorpill-font-size: 0.6875rem;
}

.mh-authorpill,
.staff-badge-pill,
/* .comment-header-avatar is the stacked comment layout's avatar, not a pill: as an
   unlisted header child it was dressed with pill height and padding, and this chain's
   specificity overrode the rule that hides it in the side layout, so both avatars showed.
   .comment-header-pills is the SCROLLING ROW that holds the pills, not a pill.
   Without this exclusion the row was dressed as one - fixed pill height, pill
   padding - and, because pills must not shrink, it could not shrink either, so
   instead of scrolling it pushed the header 48px past the viewport at 375px.
   The second selector is what keeps the real pills styled now that they are
   grandchildren of the header rather than direct children. */
.comment-header > :not(.comment-author):not(.comment-header-sep):not(.comment-date):not(.comment-time):not(.skeleton):not(.staff-badge-container):not(.comment-header-pills):not(.comment-header-avatar),
.comment-header .comment-header-pills > :not(.comment-header-sep):not(.comment-date):not(.comment-time):not(.skeleton):not(.staff-badge-container),
.comment-header .comment-author > :not(.username-link):not(.skeleton):not(.staff-badge-container),
.message-header .header-group > :not(.message-username):not(.staff-badge-container),
.msg-author-row > :not(.msg-author):not(.msg-timestamp):not(.staff-badge-container),
.comment-collapse-bar .collapse-reason-pill {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    height: var(--authorpill-height);
    padding: 0 var(--authorpill-pad-x);
    gap: var(--authorpill-gap);
    margin: 0;
    border-radius: var(--radius-pill);
    font-size: var(--authorpill-font-size);
    font-weight: var(--weight-semibold);
    white-space: nowrap;
    flex-shrink: 0;
    vertical-align: middle;
    /* box-sizing is load-bearing: five of the twelve draw a 1px border and
       seven do not, and there is no global border-box reset in this codebase. */
    /* line-height is load-bearing too - an author line inherits 1.6, which adds
       ~5px to every pill and defeats the fixed height. */
    line-height: var(--leading-none);
}

.mh-authorpill > i,
.staff-badge-pill > i,
.comment-header > :not(.comment-author):not(.comment-header-sep):not(.comment-date):not(.comment-time):not(.skeleton):not(.comment-header-pills):not(.comment-header-avatar) > i,
.comment-header .comment-header-pills > :not(.comment-header-sep):not(.comment-date):not(.comment-time):not(.skeleton) > i,
.comment-header .comment-author > :not(.username-link):not(.skeleton) > i,
.message-header .header-group > :not(.message-username) > i,
.msg-author-row > :not(.msg-author):not(.msg-timestamp) > i,
.comment-collapse-bar .collapse-reason-pill > i {
    font-size: 0.9em;
    line-height: var(--leading-none);
}

/* The staff badge's icon is an inline SVG sized in px, not an <i>. Scoped to
   the pill so the role list inside its tooltip keeps the larger 12px icon. */
.staff-badge-pill .staff-badge-svg-icon {
    width: 11px;
    height: 11px;
}

/* Every author line is a flex row with its own gap, so the badge wrapper must
   not add a second margin on top of it. */
.staff-badge-container {
    margin: 0;
}

/* --------------------------------------------------------------------------
   TOUCH TARGETS. Two author-line pills are real controls: the staff badge
   (its role tooltip) and the gacha "Drafted" badge (jumps to the Draft
   Gallery). At 20px they sit under the 44px floor, so each gets an expanded
   touch band from a transparent ::after that costs ZERO layout - the row does
   not grow by a pixel. 16px up into the comment card's own top padding, 8px
   down, which stops 4px clear of the comment text 12px below; 4px each side,
   exactly half the 8px gap, so neighbouring bands touch and never overlap.

   The staff badge's band lives on its CONTAINER because the pill has
   overflow: hidden for its shimmer and would clip its own hit area. The pill
   is raised above the band (z-index in staff-badge-styles) so a pointer over
   the pill still hits the PILL and every :hover state it owns still fires.

   Scoped to the comment header: these are the surroundings that were measured.
   The same badge in chat and messages keeps its existing behaviour.
   -------------------------------------------------------------------------- */
.comment-header .staff-badge-container,
.comment-header .gacha-draft-badge {
    position: relative;
}

.comment-header .staff-badge-container::after,
.comment-header .gacha-draft-badge::after {
    content: '';
    position: absolute;
    top: -16px;
    right: -4px;
    bottom: -8px;
    left: -4px;
    z-index: 0;
}

/* ==========================================================================
   RAIL CARD + RAIL ACTIONS  (.mh-rail-*)
   --------------------------------------------------------------------------
   A sidebar module: a titled shell holding a vertical list of actions, each
   an icon tile + label + one-line caption. Shared rather than page-local
   because it now has two consumers (the post-view rail's shortcuts and the
   community board's rail), and a hand-rolled second copy would carry none of
   this one's states and drift on the next change.

   Works as an <a> or a <button>. A toggle action carries aria-pressed and is
   dressed from THAT, so the accessible state and the visual state cannot
   disagree - there is no separate styling attribute to keep in sync.

   Remaining legacy consumer: the post-view rail's other two modules still use
   .connected-head (post-view-macros/connected-posts.ftlh) for a head styled
   identically to .mh-rail-card__head. Migrating those two is a follow-up.
   ========================================================================== */
.mh-rail-card {
    background: var(--surface-grad);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    overflow: hidden;
}

.mh-rail-card__head {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 14px 16px;
    border-bottom: 1px solid var(--border-subtle);
}

.mh-rail-card__head-icon {
    width: 36px;
    height: 36px;
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-md);
    font-size: 0.9rem;
    background: rgba(243, 156, 18, 0.13);
    color: var(--warning);
}

.mh-rail-card__head-title {
    font-size: 1rem;
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.mh-rail-card__body {
    display: flex;
    flex-direction: column;
    padding: 8px;
    gap: 4px;
}

.mh-rail-action {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    /* The 44px floor is the touch target, not a look: these rails go full
       width below their fold breakpoint, where the actions are thumbed. */
    min-height: 56px;
    padding: 8px 10px;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: transparent;
    color: inherit;
    font-family: inherit;
    text-align: left;
    text-decoration: none;
    cursor: pointer;
    transition: background var(--transition-base), border-color var(--transition-base), transform var(--dur-fast) var(--ease-out);
}

.mh-rail-action:hover {
    background: var(--glass-bg);
    border-color: var(--border-subtle);
}

.mh-rail-action:active {
    transform: scale(0.97);
}

.mh-rail-action:focus-visible {
    outline: 2px solid var(--brand-blue);
    outline-offset: 2px;
}

.mh-rail-action[disabled] {
    opacity: 0.6;
    cursor: default;
}

.mh-rail-action__icon {
    flex-shrink: 0;
    width: 38px;
    height: 38px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--radius-md);
    font-size: 0.9rem;
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.05);
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.6);
    transition: background var(--transition-base), color var(--transition-base);
}

.mh-rail-action__icon--brand {
    background: rgba(52, 152, 219, 0.13);
    color: var(--brand-blue);
}

.mh-rail-action__icon--purple {
    background: rgba(155, 89, 182, 0.13);
    color: var(--brand-purple);
}

.mh-rail-action__icon--gold {
    background: rgba(243, 156, 18, 0.13);
    color: var(--warning);
}

.mh-rail-action__text {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.mh-rail-action__label {
    font-size: 0.85rem;
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.mh-rail-action__sub {
    font-size: 0.68rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.38);
}

.mh-rail-action__go {
    margin-left: auto;
    flex-shrink: 0;
    font-size: 0.7rem;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.25);
    transition: transform var(--transition-base), color var(--transition-base);
}

.mh-rail-action:hover .mh-rail-action__go {
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.5);
    transform: translateX(2px);
}

/* An engaged toggle is dressed from aria-pressed, so a saved/active item is
   readable from the rail at a glance without a second state attribute. */
.mh-rail-action[aria-pressed="true"] .mh-rail-action__icon {
    background: rgba(243, 156, 18, 0.16);
    color: var(--warning);
}

/* The current destination in a navigation rail. */
.mh-rail-action.is-current {
    background: rgba(52, 152, 219, 0.10);
    border-color: rgba(52, 152, 219, 0.28);
}

@media (prefers-reduced-motion: reduce) {
    .mh-rail-action:active {
        transform: none;
    }

    .mh-rail-action:hover .mh-rail-action__go {
        transform: none;
    }
}

/* ==========================================================================
   SKELETON SHIMMER  (--mh-skel-* + @keyframes mh-skel-shimmer)
   --------------------------------------------------------------------------
   THE core loading shimmer. Before this the app carried ~40 hand-rolled shimmer
   keyframes and ~13 separate pulse keyframes, so every skeleton loaded at its
   own speed, easing and intensity - and one of them ran a per-block opacity
   pulse UNDERNEATH a sweep, which read as the blocks stretching.

   Applied through VARIABLES rather than a utility class, deliberately. Most
   skeleton markup here is built in JS string templates in a different file from
   the CSS that styles it, so a class-based utility reached only about half of
   them and silently left the rest as static grey blocks. A zone applies:

       --mh-skel-base: <its own tint>;
       background: var(--mh-skel-base) var(--mh-skel-sheen-image)
                   0 0 / 200% 100% no-repeat;
       animation: mh-skel-shimmer var(--mh-skel-dur) linear infinite;

   so the MATERIAL and the MOTION are single-sourced while each zone keeps its
   own geometry and intensity. .mh-skel below is the same thing as a class, for
   new markup that can carry one.

   Mechanism is the background-position sweep the majority of the pre-existing
   shimmers already used - the house idiom promoted, not a new one invented. It
   needs no pseudo-element, no positioning context and no overflow clipping,
   which is what makes it safe to drop onto an arbitrary block.
   ========================================================================== */
:root {
    --mh-skel-base: rgba(var(--fg-rgb, 255, 255, 255), 0.06);
    --mh-skel-dur: 1.6s;
    --mh-skel-sheen-image: linear-gradient(
        90deg,
        transparent 0%,
        rgba(var(--fg-rgb, 255, 255, 255), 0.13) 50%,
        transparent 100%
    );
}

.mh-skel {
    background: var(--mh-skel-base) var(--mh-skel-sheen-image) 0 0 / 200% 100% no-repeat;
    animation: mh-skel-shimmer var(--mh-skel-dur) linear infinite;
    /* A skeleton stands in for content that is not there yet, so it must never
       be readable or focusable while it waits. */
    color: transparent;
    user-select: none;
    pointer-events: none;
}

@keyframes mh-skel-shimmer {
    from {
        background-position: 200% 0;
    }
    to {
        background-position: -200% 0;
    }
}

/* Reduced motion keeps the PLACEHOLDER and drops the sweep. Blanking the sheen
   variable stops every skeleton in the app at once, including the zone rules
   that apply the shimmer without the class. */
@media (prefers-reduced-motion: reduce) {
    :root {
        --mh-skel-sheen-image: none;
    }

    .mh-skel {
        animation: none;
    }
}

/* ==========================================================================
   .mh-resultcount — THE ANSWER TO WHAT THE TOOLBAR JUST ASKED

   Five pages rendered their result count as a sentence — "Showing 0 companies"
   — in body text beside a control. That reads as a CAPTION: the eye files it
   with the chrome and skips it, when it is in fact the one number the whole
   filter bar exists to produce.

   A figure with its unit set under it gives the number somewhere to sit and a
   weight that matches its importance. Lifted verbatim from the music browse
   page, where the treatment was first worked out; that page now CONSUMES this
   instead of owning a private copy, so there is one definition rather than a
   reference implementation plus four imitations.

       <div class="mh-resultcount">
         <span class="mh-resultcount__num">1,284</span>
         <span class="mh-resultcount__unit">series</span>
       </div>

   The unit carries whatever qualifies the figure — "series", "of 40 watchlists",
   "watchlists found" — because it is the slot the eye reaches second.

   Namespaced, per the rule for anything in this file: `.results-count` was
   already defined page-locally on five surfaces, and a global rule under that
   name would have silently reached every one of them.
   ========================================================================== */
.mh-resultcount {
    display: flex;
    align-items: baseline;
    gap: 8px;
    min-width: 0;
}

.mh-resultcount__num {
    font-size: 1.6rem;
    font-weight: var(--weight-bold);
    line-height: var(--leading-none);
    color: var(--text-primary);
    /* Tabular figures: the count changes as filters apply, and proportional
       digits make it jitter sideways on every keystroke. */
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.02em;
}

.mh-resultcount__unit {
    font-size: 0.74rem;
    font-weight: var(--weight-semibold);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
}

/* ==========================================================================
   .mh-adrail — THE HOST'S SPACING FOR AN AD, HELD BY THE HOST

   AdSlotSpacingContractTest pins that no ad VARIANT declares a margin, and it
   is right: a unit carrying its own spacing imposes it on every page it is
   dropped into, and on a grid or flex parent it ADDS to the gap rather than
   replacing it. That test's javadoc records an earlier version asserting the
   opposite for leaderboard/in-article/sidebar, calls that the wrong call, and
   notes the argument is superficially persuasive and will be made again.

   It was made again here, and the contract caught it. Recording the measurement
   that motivated it, because the PROBLEM is real even though that fix was not:
   the leaderboard's gap to its nearest neighbour was 0px on home (both slots),
   browse-anime, browse-manga, companies and publications, and 0px BELOW the
   grid on the character and person lists. The sections either side supply their
   own internal padding while the ad supplies none, so it butts onto a
   neighbour's padding edge and reads as part of it.

   So the spacing goes on a WRAPPER the host owns, never on the slot. The ad
   unit stays portable and margin-free; the page decides how much air it gets.
   That is the "page-level rule that puts the slot on the same rhythm as its
   siblings" the contract names as a sanctioned mechanism — one class rather
   than seven page rules, because the answer was the same on all seven.

       <div class="mh-container mh-adrail">
           <@ads.leaderboard slotId="…" />
       </div>
   ========================================================================== */
.mh-adrail {
    margin-block: var(--spacing-xl);
}

/* No ad, no air. The ad gate renders NOTHING for supporters and when ads are off, which left
   an empty wrapper still holding its margins: on home, logged in as a supporter, that was 64px
   of blank space under the Hot Posts card on top of the next section's own padding (measured).
   The spacing belongs to the ad, so it goes when the ad does. */
.mh-adrail:not(:has(*)) {
    display: none;
}

/* ==========================================================================
   .mh-i-anime / .mh-i-manga — THE TWO DOMAINS, DRAWN RATHER THAN BORROWED

   Anime was fa-tv and manga was whichever of fa-book / fa-book-open / fa-scroll
   the author reached for. Both were wrong in the same way: they name a DEVICE
   or an OBJECT, not the medium.

   fa-tv was the sharper problem, because TV is also one of the anime FORMAT
   values (TV / Movie / OVA / ONA / Special) and the format badge uses that same
   glyph. One icon meant two things, so a Movie card showed a television. The
   two meanings are now separate icons, and the format badge keeps fa-tv, where
   a television is exactly right.

   DELIVERED AS A MASK, not an inline <svg> macro, and that is forced by the
   call sites: of 194, most are a class NAME rather than markup -- `icon:
   'fas fa-tv'` in a JS object, a bare 'fa-tv' composed into a class later, a
   string spliced into innerHTML. A macro reaches none of those. A class reaches
   all of them, and a currentColor mask inherits colour and font-size exactly
   the way the Font Awesome glyph it replaces did, so no call site needs to know
   anything changed.

   Self-sufficient on purpose: the class sets its own box, so it renders
   correctly whether or not a leftover `fas` is still sitting beside it.
   ========================================================================== */
.mh-i-anime,
.mh-i-manga {
    display: inline-block;
    /* 1.125em, not 1em, and the number is MEASURED rather than chosen. At 1em
       these sat visibly smaller than the Font Awesome glyphs beside them,
       because a FA icon's box is wider than its font-size: measured live
       2026-09-10, fa-th-large at 12px renders 13.21px wide (1.101em) and
       fa-trophy at 11.7px renders 13.42px (1.147em). Heights already matched
       (both equal the font-size); only the mass was short.

       1.125em is the middle of that measured range and is also FA's own default
       advance. The box stays SQUARE because the art is square - matching FA's
       width while keeping height at 1em would stretch the mask. */
    width: 1.125em;
    height: 1.125em;
    /* Font Awesome's own optical baseline, so swapping one for the other does
       not shift the text it sits in. */
    vertical-align: -0.125em;
    background-color: currentColor;
    -webkit-mask: var(--mh-i) no-repeat center;
    mask: var(--mh-i) no-repeat center;
    -webkit-mask-size: contain;
    mask-size: contain;
    /* Only transform animates -- see the state block below. */
    transition: transform 0.22s var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}

.mh-i-anime {
    --mh-i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill-rule='evenodd' d='M5 4h14a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H5a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3Zm0 2a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H5Z'/%3E%3Cpath d='M10 8.5v7l6-3.5z'/%3E%3C/svg%3E");
}

.mh-i-manga {
    --mh-i: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M11 7.2v11.4C9.5 17.6 7.6 17 5.5 17c-.9 0-1.7.1-2.5.3V5.8c.8-.2 1.6-.3 2.5-.3 2.1 0 4 .6 5.5 1.7z'/%3E%3Cpath d='M13 7.2v11.4c1.5-1 3.4-1.6 5.5-1.6.9 0 1.7.1 2.5.3V5.8c-.8-.2-1.6-.3-2.5-.3-2.1 0-4 .6-5.5 1.7z'/%3E%3C/svg%3E");
}

/* STATE ONLY, NEVER AT REST.

   These land in tab rails, section headers, nav and cards -- dozens per page.
   Anything that moves while idle is noise at that density, and it costs a
   composite on every frame for an icon nobody is looking at. So they hold still
   until the thing carrying them is hovered or becomes the selected one.

   Transform only, because a mask cannot animate its own interior: there is no
   way to move just the play triangle without a second layered element, and an
   extra pseudo-element per icon is not worth it at this count. */
:where(a, button, label, [role="tab"], [role="button"]):hover > .mh-i-anime,
:where(a, button, label, [role="tab"], [role="button"]):hover > .mh-i-manga,
.mh-i-anime:hover,
.mh-i-manga:hover {
    transform: scale(1.14);
}

.is-active > .mh-i-anime,
.active > .mh-i-anime,
[aria-selected="true"] > .mh-i-anime,
[aria-current="true"] > .mh-i-anime {
    animation: mh-i-anime-cue 0.42s var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}

.is-active > .mh-i-manga,
.active > .mh-i-manga,
[aria-selected="true"] > .mh-i-manga,
[aria-current="true"] > .mh-i-manga {
    animation: mh-i-manga-cue 0.42s var(--ease-standard, cubic-bezier(0.4, 0, 0.2, 1));
}

/* No opacity in either keyframe. An entrance that animates opacity makes the
   animation a precondition for the icon existing, and anything that stops the
   frame loop then leaves a blank space (#58). These only move. */
@keyframes mh-i-anime-cue {
    0%   { transform: scale(1); }
    45%  { transform: scale(1.22); }
    100% { transform: scale(1); }
}

@keyframes mh-i-manga-cue {
    0%   { transform: rotate(0deg); }
    40%  { transform: rotate(-7deg); }
    100% { transform: rotate(0deg); }
}

@media (prefers-reduced-motion: reduce) {
    .mh-i-anime,
    .mh-i-manga {
        transition: none;
        animation: none !important;
    }

    :where(a, button, label, [role="tab"], [role="button"]):hover > .mh-i-anime,
    :where(a, button, label, [role="tab"], [role="button"]):hover > .mh-i-manga,
    .mh-i-anime:hover,
    .mh-i-manga:hover {
        transform: none;
    }
}

/* ==========================================================================
   .mh-score-mark — "THIS SCORE IS OURS"

   Owner decision 2026-09-12: only a MitsuHub-sourced score is marked. A MAL score
   looks exactly as it always has, so nothing changes on any surface until titles
   graduate (30 in-house votes), and a mark that appears on every card would say
   nothing. Rendered server-side by <@sc.mark source=.../> and client-side by
   Utils.scoreMark(source), which emit identical markup - one design, two
   renderers (#34), so neither may style it locally.

   Sized in em, not rem, on purpose: it sits beside score text that ranges from the
   hero's small star rating to the info card's large numeral, and should read as a
   suffix of whichever number it follows.

   Static. It can appear dozens of times per page (#58, and the .mh-i-* note above):
   anything that moved at rest would be noise at that density.
   ========================================================================== */
.mh-score-mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin-left: 0.35em;
    padding: 0.1em 0.4em;
    border-radius: var(--radius-xs);
    background: var(--brand-gradient);
    color: var(--text-on-brand);
    font-size: 0.62em;
    font-weight: var(--weight-bold);
    letter-spacing: 0.04em;
    line-height: var(--leading-tight);
    vertical-align: middle;
    white-space: nowrap;
    cursor: help;
    user-select: none;
}

/* ==========================================================================
   UNLOCK CARD - shared/macros/unlock-card.ftlh
   What a capability needs before it opens, as a checklist, with progress on the one
   numeric row. The checks come from the same service the server gate evaluates, so the
   card cannot promise a rule the server does not enforce. First user: the News tab's
   writer gate. The watchlist Creator card and the club founding gate are the same idea
   hand-built, and can move onto this.
   ========================================================================== */
.mh-unlock {
    position: relative;
    border-radius: var(--radius-lg);
    margin-bottom: 24px;
    padding: clamp(18px, 3vw, 28px);
    background:
        radial-gradient(circle at 0% 0%, rgba(155, 89, 182, 0.16), transparent 55%),
        radial-gradient(circle at 100% 100%, rgba(52, 152, 219, 0.16), transparent 55%),
        rgba(var(--fg-rgb, 255, 255, 255), 0.02);
    border: 1px solid rgba(155, 89, 182, 0.22);
}

.mh-unlock__head {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 18px;
}

.mh-unlock__icon {
    flex-shrink: 0;
    width: 48px;
    height: 48px;
    border-radius: var(--radius-md);
    background: linear-gradient(135deg, var(--brand-purple), var(--brand-blue));
    display: flex;
    align-items: center;
    justify-content: center;
    color: #fff;
    font-size: 20px;
    box-shadow: var(--lift-purple);
}

.mh-unlock__heading {
    min-width: 0;
}

.mh-unlock__title {
    font-size: 1.15rem;
    font-weight: var(--weight-bold);
    color: var(--text);
    margin: 0 0 4px;
}

.mh-unlock__sub {
    font-size: 0.875rem;
    color: var(--text-secondary);
    margin: 0;
}

.mh-unlock__checks {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.mh-unlock__check {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    padding: 12px 14px;
    border-radius: var(--radius-md);
    background: var(--glass-bg);
    border: 1px solid var(--border-subtle);
}

.mh-unlock__check-icon {
    flex-shrink: 0;
    margin-top: 3px;
    font-size: 1rem;
    color: var(--text-muted);
}

.mh-unlock__check.is-met .mh-unlock__check-icon {
    color: var(--success);
}

.mh-unlock__check-text {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
    flex: 1;
}

.mh-unlock__check-label {
    font-size: 0.9rem;
    font-weight: var(--weight-semibold);
    color: var(--text);
}

.mh-unlock__check.is-met .mh-unlock__check-label {
    color: var(--text-secondary);
}

.mh-unlock__check-detail {
    font-size: 0.8125rem;
    line-height: var(--leading-normal);
    color: var(--text-secondary);
}

.mh-unlock__progress {
    width: 100%;
    height: 6px;
    margin-top: 6px;
    background: var(--border-subtle);
    border-radius: var(--radius-xs);
    overflow: hidden;
}

.mh-unlock__progress-fill {
    height: 100%;
    background: linear-gradient(90deg, var(--brand-purple), var(--brand-blue));
    border-radius: var(--radius-xs);
}

.mh-unlock__progress-label {
    font-size: 0.75rem;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
}

.mh-unlock__alt {
    margin: 16px 0 0;
    font-size: 0.8125rem;
    color: var(--text-secondary);
}

.mh-unlock__alt-link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 44px;
    color: var(--brand-blue);
    font-weight: var(--weight-semibold);
    text-decoration: none;
}

.mh-unlock__alt-link i {
    font-size: 0.75em;
    transition: transform var(--dur-base) var(--ease-out);
}

@media (hover: hover) and (pointer: fine) {
    .mh-unlock__alt-link:hover i {
        transform: translateX(3px);
    }
}

.mh-unlock__alt-link:focus-visible {
    outline: 2px solid var(--brand-blue);
    outline-offset: 2px;
    border-radius: var(--radius-xs);
}

/* ==========================================================================
   THEME SONG TYPE PALETTE - one colour per theme type, on every surface
   Owner decision 2026-09-15: openings, endings and insert songs read the same
   colour wherever a type badge appears (anime page, theme cards, artist page,
   person credits, collections, theme detail). Before this each page picked its
   own pair, so no single insert colour could avoid colliding with some page's
   ending colour.

   Two layers, because every surface already owns its badge GEOMETRY:
   - .mh-theme-type--op / --ed / --in / --other only set the channel. Put one
     on any badge and read rgb(var(--mh-theme-type-rgb)).
   - .mh-theme-type-badge is the default tinted text badge for surfaces that
     have no geometry of their own.
   Every endpoint below is defined in design-tokens.css (#94: one missing
   channel silently drops the whole declaration).
   ========================================================================== */
.mh-theme-type--op {
    --mh-theme-type-rgb: var(--brand-blue-rgb);
}

.mh-theme-type--ed {
    --mh-theme-type-rgb: var(--brand-purple-rgb);
}

.mh-theme-type--in {
    --mh-theme-type-rgb: var(--amber-500-rgb);
}

.mh-theme-type--other {
    --mh-theme-type-rgb: var(--fg-rgb);
}

.mh-theme-type-badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border-radius: var(--radius-xs);
    border: 1px solid rgba(var(--mh-theme-type-rgb, var(--fg-rgb)), 0.25);
    background: rgba(var(--mh-theme-type-rgb, var(--fg-rgb)), 0.15);
    color: rgb(var(--mh-theme-type-rgb, var(--fg-rgb)));
    font-size: 0.6875rem;
    font-weight: var(--weight-bold);
    letter-spacing: 0.04em;
    line-height: var(--leading-none);
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   CONTENT WARNING COVER - spoiler and NSFW theme videos (content-warning-cover.js)
   Owner decision 2026-09-15: a flagged video never plays and its frame never
   shows until the viewer clicks through. The cover sits over the player or
   card that hosts it; the host only needs to be a positioned box, which
   .mh-content-cover-host guarantees without overriding a host that is already
   absolutely or fixed positioned.
   ========================================================================== */
.mh-content-cover-host:not([style*="position"]) {
    position: relative;
}

.mh-content-cover {
    position: absolute;
    inset: 0;
    z-index: var(--z-raised);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    border-radius: inherit;
    background: rgba(0, 0, 0, 0.72);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    text-align: center;
    animation: mh-content-cover-in var(--dur-base) var(--ease-out);
}

.mh-content-cover__inner {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    max-width: 320px;
}

.mh-content-cover__icon {
    font-size: 1.5rem;
    color: rgb(var(--amber-500-rgb));
}

.mh-content-cover__title {
    font-size: 1rem;
    font-weight: var(--weight-bold);
    color: var(--text);
}

.mh-content-cover__text {
    margin: 0;
    font-size: 0.8125rem;
    color: var(--text-secondary);
}

.mh-content-cover__reveal {
    min-height: 44px;
    margin-top: 4px;
}

.mh-content-cover__reveal:active {
    transform: scale(0.97);
}

.mh-content-cover__reveal:focus-visible {
    outline: 2px solid var(--brand-blue);
    outline-offset: 2px;
}

.mh-content-cover--compact {
    padding: 8px;
}

.mh-content-cover--compact .mh-content-cover__icon {
    font-size: 1rem;
}

.mh-content-cover--compact .mh-content-cover__title {
    font-size: 0.75rem;
}

.mh-content-cover--compact .mh-content-cover__reveal {
    min-height: 32px;
    padding: 4px 10px;
    font-size: 0.75rem;
}

/* A thumbnail of a flagged video: blurred, with a small tag, until revealed. */
.mh-content-blur {
    filter: blur(14px);
    transform: scale(1.08);
}

.mh-content-tag {
    position: absolute;
    top: 6px;
    right: 6px;
    z-index: var(--z-raised);
    padding: 2px 6px;
    border-radius: var(--radius-xs);
    background: rgba(0, 0, 0, 0.7);
    color: rgb(var(--amber-500-rgb));
    font-size: 0.625rem;
    font-weight: var(--weight-bold);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    pointer-events: none;
}

@keyframes mh-content-cover-in {
    from {
        transform: scale(0.98);
    }
    to {
        transform: none;
    }
}

@media (prefers-reduced-motion: reduce) {
    .mh-content-cover {
        animation: none;
    }
}
