/* ==========================================================================
   picks.css - the picks tab (anime/manga detail)

   Scope: the layout the picks-tab.ftlh macro itself renders - the root
   section, the mode rail's page-level spacing (the rail chrome itself is
   `.mh-rail` / `.mh-rail__seg` / `.mh-rail__tab` from components.css, not
   redefined here) plus the rail's own "recommend a title" trigger, the hero
   card (backdrop/poster/title/status line), and the progress indicator
   (segment track + pinned prev/next). The thread and endorse-modal sections
   further down this file are a SEPARATE owner (picks-thread.js /
   endorse-modal.js) - edit the hero/rail/progress sections above the strip-
   transition block and leave those alone.

   All new classes carry the mh-pick prefix (project convention: a generic
   class name silently collides with bespoke local rules of the same name).
   ========================================================================== */

.mh-pick-tab {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

/* Mid-transition: strips off pointer events from every control so a click
   during the crossfade cannot read a target off a node that is still
   mid-flight (opacity: 0 alone still takes clicks). Set/cleared by
   picks-carousel.js around a strip transition. */
.mh-pick-tab.is-transitioning .mh-rail__tab,
.mh-pick-tab.is-transitioning .mh-pick-seg,
.mh-pick-tab.is-transitioning .mh-pick-nav-btn,
.mh-pick-tab.is-transitioning .mh-pick-hero-cta {
    pointer-events: none;
}

/* Count-forward mode selector (owner: "the numbers ARE the interface").
   Three centred columns, each carrying its own count above its label, so the
   engine/community split is readable without switching modes.

   Deliberately NOT the shared .mh-rail component: a rail reads as a
   container, and a container is precisely what this tab is shedding. */
.mh-pick-modes {
    display: flex;
    flex-wrap: nowrap;
    align-items: flex-end;
    /* Start-safe centring - see the spacer block after .mh-pick-seg-track > *. */
    justify-content: flex-start;
    gap: clamp(16px, 4vw, 48px);
    margin-bottom: 10px;
    /* Chip/mode rows scroll rather than stack on narrow screens (house rule);
       the scrollbar itself stays hidden. */
    overflow-x: auto;
    scrollbar-width: none;
}

.mh-pick-modes::-webkit-scrollbar {
    display: none;
}

.mh-pick-mode {
    display: flex;
    flex-shrink: 0;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 4px 4px 7px;
    border: none;
    border-bottom: 2px solid transparent;
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    transition: color var(--dur-base) var(--ease-standard), border-color var(--dur-base) var(--ease-standard);
}

.mh-pick-mode__count {
    font-size: clamp(1.3rem, 0.9rem + 1.1vw, 1.85rem);
    font-weight: var(--weight-bold);
    line-height: var(--leading-none);
    letter-spacing: -0.02em;
}

.mh-pick-mode__label {
    font-size: 0.78rem;
    white-space: nowrap;
}

.mh-pick-mode:hover {
    color: var(--text);
}

/* Compound selector rather than a same-specificity sibling rule: an
   .is-active that ties with the base resolves by SOURCE ORDER, which a later
   refactor can reshuffle without anyone noticing the active state went
   half-applied. */
.mh-pick-mode.mh-pick-mode.is-active {
    color: var(--text);
    border-bottom-color: var(--brand-blue);
}

.mh-pick-mode.is-active .mh-pick-mode__count {
    color: var(--brand-blue);
}

/* Centred section headers. The honeycomb and the vouch thread ran together
   with no visible boundary (owner), and a centred label is the lightest
   divider that does not reintroduce a box. */
.mh-pick-sechead {
    position: relative;
    margin: 36px 0 18px;
    padding-bottom: 13px;
    color: var(--text);
    font-size: clamp(1.05rem, 0.85rem + 0.7vw, 1.4rem);
    font-weight: var(--weight-bold);
    letter-spacing: -0.01em;
    line-height: var(--leading-snug);
    text-align: center;
}

/* A short centred rule under the label. This is the weight a section header
   would normally get from sitting on a panel - carried by one 46px mark
   instead, so the section reads as separated without a container coming
   back to divide it. */
.mh-pick-sechead::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 50%;
    width: 46px;
    height: 3px;
    border-radius: var(--radius-pill);
    background: var(--brand-gradient);
    transform: translateX(-50%);
}

/* A header with nothing beneath it is worse than no header. :has() lets the
   header answer for its own sibling, instead of JS having to remember to
   toggle a second element every time the band's contents change. */
.mh-pick-sechead:has(+ .mh-pick-viewers:not(.has-content)) {
    display: none;
}

/* Hero stage - reserves the space the current pick renders into. Sized so
   the tab does not jump when the first pick loads in. position: relative +
   overflow: hidden makes this the containing block for the backdrop's
   inset: 0 and clips its transform: scale(1.1) blur overscan.

   --mh-pick-hero-h is the single source of truth for the fluid hero height
   (owner: "the hero area might have to get a bit more taller and imposing"
   to make room for the floating-island stack below) - declared once here
   and inherited by .mh-pick-slide further down, so the two never drift out
   of sync the way two independently-maintained literal values would. The
   640px breakpoint at the bottom of this file overrides ONLY this custom
   property, not the min-height declaration itself. */
.mh-pick-hero {
    --mh-pick-hero-h: clamp(400px, 34vw, 560px);
    position: relative;
    min-height: var(--mh-pick-hero-h);
    overflow: hidden;
    /* No border, no surface fill, no radius: the backdrop's own edge mask
       (below) dissolves the art into the page, and a 1px border around a
       dissolving edge is exactly the container outline being removed. */
    background: transparent;
}

/* ==========================================================================
   Hero slide - one pick's card, staged as a floating-island composition
   (owner direction, 2026-08-23): "the poster will be in the middle and act
   like a floating island... poster first, then title, and other meta
   stuff... give them all some form of shadow and if possible even
   perspective so they look like they stack on top of the hero area."

   The poster is ALWAYS centred (the left/right-offset composition this
   file briefly carried was reversed by the owner - see .mh-pick-poster
   below) with a blurred, enlarged copy of the same art filling the space
   either side ("blurred wings" - .mh-pick-backdrop.is-poster). The banner
   case (a real bannerImageUrl exists) never renders a poster at all - see
   3 in the task brief this composition implements - so it stays a plain
   crisp full-bleed image with nothing to elevate.

   `perspective` lives here, one level up from the elevated layers
   themselves (.mh-pick-poster / .mh-pick-meta), because CSS 3D transforms
   need their PARENT to hold the perspective, not the transformed element
   itself. overflow: hidden (already needed for the backdrop's scale
   overscan) also keeps the layers' subtle forward translateZ from ever
   spilling past the slide's own edge. */
.mh-pick-slide {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    width: 100%;
    min-height: var(--mh-pick-hero-h);
    padding: 28px 24px 24px;
    box-sizing: border-box;
    overflow: hidden;
    text-align: center;
    perspective: 1000px;
}

/* Bottom-anchored legibility scrim for the centered meta block below -
   dense directly behind the title/rank and falling off quickly above that,
   so a real banner stays crisp everywhere else on the card (removing the
   backdrop blur was deliberate - the scrim carries legibility instead of
   re-blurring the image, per "keep it as light as it can be while the
   title stays readable"). Literal black rather than a themed token: this
   is a media scrim, and design-tokens.css deliberately keeps
   --modal-scrim and --modal-bg-media literal black for the same reason (a
   scrim exists to make the media legible, not to carry the theme). */
.mh-pick-slide::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.88) 0%, rgba(0, 0, 0, 0.62) 22%, rgba(0, 0, 0, 0.22) 46%, rgba(0, 0, 0, 0) 68%);
}

.mh-pick-backdrop {
    position: absolute;
    inset: 0;
    z-index: 0;
    background-size: cover;
    background-position: center;
}

/* Edge blend (owner: the banner "should not look like it's in a container").

   The mask goes on the BACKDROP, never on .mh-pick-hero. Masking the hero
   would fade its children too - the title, rank and poster are the one thing
   that must stay at full strength, and they sit in the same stacking box.

   Two gradients composited with intersect: left/right dissolve so the art has
   no vertical edge, and the bottom dissolves into the page so the card has no
   floor. The top stays hard because the carousel's own chrome sits above it
   and a fade there just looks like a rendering fault. */
.mh-pick-backdrop {
    --mh-bd-edge: 9%;
    --mh-bd-floor: 62%;
    -webkit-mask-image:
        linear-gradient(to right, transparent 0, #000 var(--mh-bd-edge), #000 calc(100% - var(--mh-bd-edge)), transparent 100%),
        linear-gradient(to bottom, #000 0, #000 var(--mh-bd-floor), transparent 100%);
    -webkit-mask-composite: source-in;
    mask-image:
        linear-gradient(to right, transparent 0, #000 var(--mh-bd-edge), #000 calc(100% - var(--mh-bd-edge)), transparent 100%),
        linear-gradient(to bottom, #000 0, #000 var(--mh-bd-floor), transparent 100%);
    mask-composite: intersect;
}

/* Banner present - crisp, full-bleed, no blur/tint/scale of its own. The
   slide's own bottom scrim (::before above) is the only legibility
   treatment it gets, kept as light as it can be while the title stays
   readable. This is the COMMON case: 73.8% of actual recommendation
   targets carry a banner. */
.mh-pick-backdrop.is-banner {
    background-position: center;
}

/* No banner - the poster art doubles as the backdrop. Positioned toward
   the upper third (poster art usually has the subject high) rather than
   dead centre, and given the MINIMUM blur that hides the upscale rather
   than the heavy duotone every backdrop used to get regardless of whether
   it needed hiding. NOT mix-blend-mode - a blend mode on any descendant of
   a preserve-3d context flattens the context, and the flattened content
   keeps hit-testing perfectly while it stops painting. */
.mh-pick-backdrop.is-poster {
    background-position: center 22%;
    filter: blur(16px) saturate(1.2) brightness(0.68);
    transform: scale(1.08);
    /* Wider fade than the banner case, for a geometric reason rather than an
       aesthetic one: the scale above pushes this element ~4% past each edge
       of the hero's overflow clip, and a mask is applied in the element's OWN
       coordinate space before being scaled with it. A fade tuned for an
       unscaled backdrop therefore has its first ~3.7% cropped away and meets
       the visible edge already ~40% opaque - which is the hard edge the mask
       exists to remove. Widening lets the ramp COMPLETE inside the visible
       window. */
    --mh-bd-edge: 20%;
    --mh-bd-floor: 54%;
}

/* The "blurred wings" treatment for the no-banner case (see heroMarkup) -
   a poster is 2:3 against this hero's ~4:1 frame, so covering the hero's
   width the way a banner does would blow the poster up to ~7x its height
   and show nothing but a blurred fragment of the title lettering, not
   artwork. .mh-pick-backdrop.is-poster (above) is the WINGS - it stays
   full-bleed and heavily blurred, filling the frame with the artwork's own
   colour only. This element is the SUBJECT sitting on top of the wings:
   natural aspect ratio (width auto against height: 100%), full hero
   height, centred horizontally, crisp - no blur of its own.

   THE FLOATING ISLAND (owner direction, 2026-08-23, reversing the
   left/right-offset composition this rule briefly carried): the poster
   goes back to dead centre - box-shadow plus a forward translateZ (the
   slide's own `perspective` above is what makes translateZ read as depth)
   make it look like a physical card resting above the blurred backdrop
   rather than printed on it. Two shadow tiers stacked (a tight one plus a
   soft, far-thrown one) rather than a single value - the same "real drop
   shadow" the title/meta layers below get, just heaviest here since the
   poster is the NEAREST layer in the stack.

   Renders ONLY when there is no banner AND a real poster URL exists (see
   heroMarkup) - when a banner exists it already IS the backdrop, and a
   missing poster degrades to the wings alone rather than standing a
   generic placeholder icon in for real artwork.

   z-index: 0, same tier as .mh-pick-backdrop and painted after it in
   source order (so it sits above the wings), but still BELOW the slide's
   scrim (::before, z-index 1) and .mh-pick-meta (z-index 2) - the exact
   same stack a real banner sits in, so the title/rank overlay it with the
   identical scrim and text-shadow treatment regardless of which case this
   is. max-width: 100% plus .mh-pick-slide's own overflow: hidden keep it
   from ever overflowing the hero horizontally; height: 100% means it can
   never exceed the hero's height either.

   The forward translateZ is static elevation only - it lifts the poster
   clear of the banner so it reads as a floating island. Nothing moves it. */
.mh-pick-poster {
    position: absolute;
    top: 0;
    left: 50%;
    z-index: 0;
    height: 100%;
    width: auto;
    max-width: 100%;
    transform: translateX(-50%) translateZ(46px);
    box-shadow: var(--shadow-md), var(--shadow-2xl);
    pointer-events: none;
}

/* The text/CTA island - everything below the poster in the stack ("poster
   first, then title, and other meta stuff", owner). translateZ (less than
   the poster's, so it reads as sitting a little further back) plus
   filter: drop-shadow (box-shadow does nothing useful on a flex column with
   no background of its own - drop-shadow shadows the actual painted content:
   the title glyphs, the badge pills, the avatar stack, the CTA circle) are
   this layer's own elevation. Same translate/transition contract as the
   poster above, at a smaller travel distance (see picks-carousel.js) - the
   base plane of the stack should move the least. */
.mh-pick-meta {
    position: relative;
    z-index: 2;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    width: 100%;
    max-width: 560px;
    min-width: 0;
    color: var(--text-on-brand);
    transform: translateZ(18px);
    filter: drop-shadow(var(--shadow-lg));
}

.mh-pick-rank {
    margin: 0;
    color: var(--text-on-brand);
    opacity: 0.85;
    font-size: 0.75rem;
    font-weight: var(--weight-medium);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    /* Legibility over bright artwork now that the backdrop blur is gone -
       paired with the strengthened scrim above, not a replacement for it. */
    text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

/* The largest element on the card, by owner direction - everything else in
   .mh-pick-meta steps down in size from here. clamp() rather than a fixed
   size so it scales with the hero instead of overflowing a narrow one.

   The title is the stack's SECOND tier (poster nearest, title next, the
   rest of .mh-pick-meta furthest), and carries a third, longer-thrown
   shadow layer below so it reads as sitting a little proud of its siblings
   (rank/badges/status line/CTA) rather than flush with them. */
.mh-pick-title {
    margin: 0;
    color: var(--text-on-brand);
    font-size: clamp(1.5rem, 1rem + 2.5vw, 2.25rem);
    font-weight: var(--weight-bold);
    line-height: var(--leading-snug);
    /* Same reasoning as .mh-pick-rank above - crisp banners need the text
       itself defended, not just the scrim behind it. The third, softer
       layer is the elevation shadow the two tight ones above it don't
       carry on their own. */
    text-shadow: 0 2px 6px rgba(0, 0, 0, 0.85), 0 1px 2px rgba(0, 0, 0, 0.9), 0 12px 20px rgba(0, 0, 0, 0.5);
}


/* The clear-logo stand-in for .mh-pick-title (picks-carousel.js's
   logoMarkup()) - rendered as its PRECEDING sibling, never a replacement in
   the DOM, so .mh-pick-title survives as the readable fallback if the image
   never loads. Sits inside .mh-pick-meta like the title did, so it inherits
   the same layered elevation stack, with no extra depth rule needed here.

   max-width/max-height both capped - a clear logo's aspect ratio is
   unpredictable (some are near-square, some are a thin wordmark), so
   constraining only one axis lets the other blow out the hero or shrink to
   nothing. */
.mh-pick-logo {
    display: block;
    max-width: min(360px, 100%);
    max-height: clamp(64px, 11vw, 140px);
    width: auto;
    height: auto;
    object-fit: contain;
    filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.85)) drop-shadow(0 12px 20px rgba(0, 0, 0, 0.5));
}

/* Hides the text title for exactly as long as the logo image is present and
   precedes it in the DOM - see logoMarkup()'s doc comment in
   picks-carousel.js. If the logo 404s, utils.js's global data-remove-on-error
   handler removes the <img>, this selector stops matching, and the title
   reappears with no JS on this side of the fix. */
.mh-pick-logo ~ .mh-pick-title {
    display: none;
}

/* titleEnglish, shown only when it differs from the native title - same
   "subtitle when it differs" convention as anime-detail.ftlh / manga-detail.ftlh. */
.mh-pick-subtitle {
    margin: 0;
    color: var(--text-on-brand);
    opacity: 0.7;
    font-size: 0.85rem;
    font-style: italic;
}

/* isSameFranchise/isCurrentlyAiring - the only badges left in the hero
   after the match-reason chips and the similarity meter were stripped back
   (owner direction: "we don't need all that extra information, the
   visuals need to carry the presentation"). Same horizontal-scroll house
   rule as the chip row it replaces, though with at most two badges it will
   rarely need to actually scroll. */
.mh-pick-badges {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    /* Start-safe centring - see the spacer block after .mh-pick-seg-track > *. */
    justify-content: flex-start;
    max-width: 100%;
    gap: 8px;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.mh-pick-badges::-webkit-scrollbar {
    display: none;
}

/* isSameFranchise - its own accent (brand purple, via color-mix so it works
   against var(--brand-purple)'s hex value with no bare-rgb token needed),
   distinct from the plain translucent-white match-reason chips beside it. */
.mh-pick-franchise {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    gap: 4px;
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--brand-purple) 35%, transparent);
    color: var(--text-on-brand);
    font-size: 0.75rem;
    font-weight: var(--weight-medium);
    white-space: nowrap;
}

.mh-pick-franchise i {
    font-size: 0.68rem;
}

/* isCurrentlyAiring (media-aware label, see airingMarkup) - the semantic
   --success token, same color-mix pattern as .mh-pick-modal-row-badge.is-yours
   further down this file. The dot is a plain background circle, not an
   animation, so it needs no prefers-reduced-motion guard. */
.mh-pick-airing {
    display: inline-flex;
    flex-shrink: 0;
    align-items: center;
    gap: 6px;
    padding: 4px 10px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--success) 30%, transparent);
    color: var(--text-on-brand);
    font-size: 0.75rem;
    font-weight: var(--weight-medium);
    white-space: nowrap;
}

.mh-pick-airing span {
    flex-shrink: 0;
    width: 6px;
    height: 6px;
    border-radius: var(--radius-circle);
    background: var(--success);
}

/* Compact rating+status line, below the title and smaller than everything
   above it - the only "extra information" the hero keeps, per statusLine()
   in picks-carousel.js. Renders only when at least one half exists; the
   divider only appears when both do. */
.mh-pick-status-line {
    display: flex;
    flex-wrap: wrap;
    row-gap: 4px;
    align-items: center;
    gap: 8px;
    color: var(--text-on-brand);
    opacity: 0.85;
    font-size: 0.8rem;
}

/* The rating IS a number - styled numerals beside a star, not a sentence. */
.mh-pick-rating {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

.mh-pick-rating i {
    color: var(--warning);
    font-size: 0.75rem;
}

.mh-pick-rating-value {
    font-variant-numeric: tabular-nums;
    font-weight: var(--weight-semibold);
}

.mh-pick-status-sep {
    flex-shrink: 0;
    width: 1px;
    height: 12px;
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.3);
}

/* Icon PLUS the status name (owner). An icon alone asks the viewer to
   decode a glyph, and the data-tooltip it used to carry only answers on
   hover, which is no answer at all on a phone. */
.mh-pick-watch-status {
    display: inline-flex;
    align-items: center;
    gap: 5px;
}

.mh-pick-watch-status i {
    font-size: 0.78rem;
}

/* The name is a phrase ("Plan to watch"), and breaking it across two lines
   inside a one-line meta strip reads as two separate facts. */
.mh-pick-watch-status__label {
    white-space: nowrap;
}

/* Icon-only, per this build's control convention (data-tooltip carries the
   label; picks-carousel.js sets aria-label too). Overrides .btn's default
   rectangular padding down to a circular 44x44 touch target. */
.mh-pick-hero-cta {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    min-width: 44px;
    min-height: 44px;
    margin-top: 4px;
    padding: 0;
    border-radius: var(--radius-circle);
    font-size: 1rem;
}

/* ==========================================================================
   Strip transition overlay (picks-strips.js). N full-size stacked copies of
   the OUTGOING backdrop, each clipped to a horizontal band with clip-path
   percentages. Percentages resolve against the border box, so this is
   correct at every width with no resize handler and no cached rect - which
   is why it replaces the reference implementation's background-position
   pixel arithmetic against a hardcoded container height.

   The strips are an overlay that LEAVES: picks-strips.js paints the incoming
   pick underneath FIRST (its completion callback runs before this layer is
   even built) and nothing in this block sets opacity on the pick itself. A
   decoration must never own visibility - a thrown handler or a frozen frame
   loop must still leave a fully rendered pick on screen, never a blank hero.

   z-index: 3 sits one above .mh-pick-meta's 2 so the leaving bands always
   cover the freshly-rendered pick's text/CTA, not just its backdrop/poster.
   ========================================================================== */
.mh-pick-strips {
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
}

/* The strips carry the OUTGOING art, so at rest they must wear the same edge
   blend the backdrop does - otherwise the transition begins by snapping two
   hard vertical edges into a composition that had none.

   Registered with @property so it can actually interpolate: an unregistered
   custom property inside a gradient is substituted as a token, not as a
   typed value, and transitioning it does nothing at all. Registration is
   what makes the fade real rather than a jump at the end.

   It fades to 0 as the band leaves (owner: "so it's not awkwardly still on
   the strip as it animates") - by the time a band is travelling, a soft edge
   riding along with it reads as a rendering artefact rather than as depth.
   Faster than the slide itself so it is gone early in the move. */
@property --mh-strip-edge {
    syntax: '<percentage>';
    inherits: true;
    initial-value: 9%;
}

.mh-pick-strip {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center;
    will-change: transform;
    /* Soft start, long tail (owner: "cinematic and graceful, not too fast,
       not too slow") rather than the old symmetric ease-in-out.

       The duration is PUBLISHED by picks-strips.js onto the layer, so
       STRIP_DURATION_MS is the single authority and the 1700ms here is only
       a fallback. It used to be a hand-kept duplicate: both files carried
       the number and both comments said they must stay equal, which is a
       description of the hazard rather than a defence against it - and the
       CSS copy silently governed what played while the JS copy governed only
       the cleanup timer. */
    -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--mh-strip-edge), #000 calc(100% - var(--mh-strip-edge)), transparent 100%);
    mask-image: linear-gradient(to right, transparent 0, #000 var(--mh-strip-edge), #000 calc(100% - var(--mh-strip-edge)), transparent 100%);
    transition:
        transform var(--mh-pick-strip-duration, 1700ms) var(--ease-out),
        --mh-strip-edge 420ms linear;
}

.mh-pick-strips.is-leaving .mh-pick-strip {
    --mh-strip-edge: 0%;
}

.mh-pick-strips.is-leaving .mh-pick-strip:nth-child(odd) {
    transform: translateX(105%);
}

.mh-pick-strips.is-leaving .mh-pick-strip:nth-child(even) {
    transform: translateX(-105%);
}

/* Reduced motion: picks-strips.js checks the same media query and calls the
   completion callback directly without ever building this layer (matches
   how hero-carousel-core.js:613 gates its own pan) - this rule is a second
   line of defense so a strip layer somehow present still renders inert. */
@media (prefers-reduced-motion: reduce) {
    .mh-pick-strip {
        transition: none;
    }

    .mh-pick-strips {
        display: none;
    }
}

/* Position indicator strip under the hero, with pinned prev/next arrows
   flanking a horizontally-scrolling segment track (#mhPickSegTrack) - the
   arrows must stay put while the track scrolls (375px: the track already
   scrolls horizontally for a long spine, house rule), so overflow now
   lives on the track alone rather than on this whole row. */
.mh-pick-progress {
    display: flex;
    align-items: center;
    gap: 4px;
}

/* Hidden (not just left empty) when there is nothing to page through. The
   prev/next arrows are static template markup, not rebuilt per render, so
   an empty view has to hide the whole bar rather than leaving two arrows
   that go nowhere - toggled as a class by render() in picks-carousel.js,
   not the [hidden] attribute: this rule's own unconditional display: flex
   is an author rule and always outranks the UA [hidden] rule regardless
   of specificity. */
.mh-pick-progress.is-empty {
    display: none;
}

.mh-pick-seg-track {
    display: flex;
    align-items: center;
    /* Start-safe centring - see the spacer block after .mh-pick-seg-track > *. */
    justify-content: flex-start;
    flex: 1 1 auto;
    min-width: 0;
    gap: 8px;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
}

.mh-pick-seg-track::-webkit-scrollbar {
    display: none;
}

.mh-pick-seg-track > * {
    flex-shrink: 0;
}

/* START-SAFE CENTRING for the three scrolling rows in this file
   (.mh-pick-modes, .mh-pick-badges, .mh-pick-seg-track). justify-content:
   center on an overflowing scroller pushes the first items past the start
   edge, where they cannot be scrolled to. Each row is flex-start instead, and
   these zero-width spacers carry auto margins that centre the row while it
   fits and collapse to 0 once it overflows. */
.mh-pick-modes::before,
.mh-pick-modes::after,
.mh-pick-badges::before,
.mh-pick-badges::after,
.mh-pick-seg-track::before,
.mh-pick-seg-track::after {
    content: '';
}

.mh-pick-modes::before,
.mh-pick-badges::before,
.mh-pick-seg-track::before {
    margin-left: auto;
}

.mh-pick-modes::after,
.mh-pick-badges::after,
.mh-pick-seg-track::after {
    margin-right: auto;
}

/* Prev/next - pinned outside the scrolling track (see .mh-pick-progress
   above) so they never scroll away with the segments. Inert during a
   strip transition via the same .is-transitioning rule the segments use. */
.mh-pick-nav-btn {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: none;
    border-radius: var(--radius-circle);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    transition: background var(--dur-base) var(--ease-standard), color var(--dur-base) var(--ease-standard);
}

.mh-pick-nav-btn:hover {
    background: var(--surface-hover);
    color: var(--text);
}

/* A single pick in the current view (e.g. "From the community" with one
   endorsement) has nowhere for prev/next to go - render() in
   picks-carousel.js sets aria-disabled rather than the native disabled
   attribute (a data-tooltip on a genuinely disabled control never fires -
   this codebase has been bitten by that before), so the click handler is
   what actually stops the wrap, and this rule is what makes "disabled"
   visible rather than just inert. pointer-events: none doubles as
   suppressing the now-pointless "Previous/Next pick" tooltip on hover. */
.mh-pick-nav-btn[aria-disabled="true"] {
    opacity: 0.35;
    cursor: default;
    pointer-events: none;
}

/* One segment. The clickable box is a full 44x44 touch target; the visible
   pill is a smaller centered ::after so the strip does not read as chunky
   at desktop width. */
.mh-pick-seg {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: none;
    background: transparent;
    cursor: pointer;
}

.mh-pick-seg::after {
    content: '';
    display: block;
    width: 22px;
    height: 6px;
    border-radius: var(--radius-pill);
    background: var(--surface-2);
    transition: background var(--dur-base) var(--ease-standard);
}

.mh-pick-seg:hover::after {
    background: var(--surface-hover);
}

.mh-pick-seg.is-active::after {
    background: var(--brand-blue);
}

@media (max-width: 640px) {
    /* No min-height override here on purpose - .mh-pick-slide inherits
       --mh-pick-hero-h from .mh-pick-hero, which is what the SECOND
       640px block below (near .mh-pick-thread) overrides. One custom
       property, read in both places, instead of two literal values that
       can drift apart. */
    .mh-pick-slide {
        padding: 20px 16px 16px;
    }

    .mh-pick-title {
        font-size: clamp(1.25rem, 6vw, 1.6rem);
    }

    .mh-pick-logo {
        max-height: clamp(48px, 16vw, 88px);
    }
}

/* ==========================================================================
   Endorsement thread - fixed-height scroll region under the active pick.

   The height is FIXED, not min-height: a thread that grows and shrinks as
   the carousel moves would reflow the tab under the user's cursor, and
   picks get compared side by side, which needs the page to hold still.
   The list scrolls INSIDE this box; the box itself never changes size.

   No panel: owner direction (2026-08-22) is that neither the populated
   thread nor its empty state sits inside a tinted/elevated background box.
   The mount itself carries no background or border - only the fixed height
   and the flex plumbing that drives the scroll region and the centered
   empty state below.
   ========================================================================== */
.mh-pick-thread {
    display: flex;
    flex-direction: column;
    gap: 12px;
    height: 360px;
}

/* Shares .mh-pick-endorsement's own centering (width: 100%, max-width: 840px,
   margin: 0 auto) rather than sitting flush with .mh-pick-thread's full
   width, so the heading's left edge lines up with the card below it instead
   of the container's. */
.mh-pick-thread-head {
    flex-shrink: 0;
    width: 100%;
    max-width: 840px;
    margin: 0 auto;
    color: var(--text);
    font-size: 0.95rem;
    font-weight: var(--weight-semibold);
}

.mh-pick-thread-list {
    display: flex;
    flex-direction: column;
    flex: 1;
    /* Without this a flex child cannot shrink below its content's height,
       so overflow-y: auto would never actually trigger a scrollbar. */
    min-height: 0;
    margin: 0;
    /* Room for the cards' hard offset shadow (right/bottom) and the corner
       badge poking above the top card - overflow-y: auto clips the x-axis
       too, at the PADDING edge, so this is what keeps both un-clipped. */
    padding: 14px 6px 4px 0;
    overflow-y: auto;
    overflow-x: hidden;
    list-style: none;
}

/* ==========================================================================
   The endorsement card - a dark "ticket stub" sticker, not a chat row. The
   owner's reference was a white card with a hard offset shadow, a dotted
   inset border and a skewed corner badge; light backgrounds are disallowed
   here, so this is that structural language re-rendered on the dark surface
   scale - a --surface-2 card, the offset shadow kept literal black (design
   language keeps shadows/scrims literal black under any theme), the dotted
   border as a faint light-dot inset, and the corner badge in brand blue.
   (The card also carried a speech-bubble tail via ::before - removed per
   owner direction; nothing else depended on it.)
   ========================================================================== */
/* Contained, not edge-to-edge - a full-width card reads as a bar and loses
   the offset shadow/dotted border's "physical sticker" quality, which
   only works with real margin around the object. Capped well under the
   thread's own width and centered with auto margins; below the cap (already
   the case at 375px, and any narrower thread column) the card is simply
   fluid, so nothing here fights the 375px rule. */
.mh-pick-endorsement {
    position: relative;
    flex-shrink: 0;
    width: 100%;
    max-width: 840px;
    margin: 0 auto 18px;
    padding: 18px 16px 14px;
    border-radius: var(--radius-md);
    background: var(--surface-2);
    /* Hard offset, no blur - a pasted-on sticker, not a floating card. */
    box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.55);
}

.mh-pick-endorsement:last-child {
    margin-bottom: 4px;
}

/* A dead-straight column of identical cards loses the reference's
   pasted-on quality - a small alternating nudge reads as stickers dropped
   slightly askew rather than a rigid list. Gated to widths comfortably
   above the card's own 840px cap (never activates below it, so there is
   always real slack on both sides for the nudge plus the hard shadow) -
   below that the thread is narrow enough that the contained-card fix
   alone is the win, and a nudge here would risk fighting the 6px shadow
   clearance on .mh-pick-thread-list instead of complementing it. */
@media (min-width: 960px) {
    .mh-pick-endorsement:nth-child(odd) {
        transform: translateX(-10px);
    }

    .mh-pick-endorsement:nth-child(even) {
        transform: translateX(10px);
    }
}

/* Ticket-stub perforation, inset from the card's own edge - a real border
   here would double up with the card edge itself, so this is a separate
   decorative layer. */
.mh-pick-endorsement::after {
    content: '';
    position: absolute;
    inset: 6px;
    border: 1.5px dotted rgba(var(--fg-rgb), 0.14);
    border-radius: calc(var(--radius-md) - 4px);
    pointer-events: none;
}

/* The viewer's own endorsement - a brand-tinted surface and a brand-tinted
   shadow instead of the plain black one, so it reads as "yours" before the
   corner badge is even read. */
.mh-pick-endorsement.is-mine {
    background: color-mix(in srgb, var(--brand-blue) 9%, var(--surface-2));
    box-shadow: 5px 5px 0 rgba(var(--brand-blue-rgb), 0.45);
}

/* Skewed corner badge - earns its place as a real signal (the viewer's own
   take), never an invented ranking. A parallelogram via clip-path rather
   than a transform skew, so the label text itself stays upright/legible. */
.mh-pick-mine-badge {
    position: absolute;
    top: -10px;
    right: 16px;
    z-index: 1;
    padding: 5px 16px 5px 12px;
    clip-path: polygon(14% 0, 100% 0, 86% 100%, 0% 100%);
    background: var(--brand-blue);
    color: var(--text-on-brand);
    font-size: 0.68rem;
    font-weight: var(--weight-bold);
    font-style: italic;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
}

.mh-pick-endorsement-head {
    display: flex;
    align-items: center;
    gap: 12px;
    margin: 0 0 12px;
}

.mh-pick-avatar {
    flex-shrink: 0;
    width: 52px;
    height: 52px;
    border-radius: var(--radius-circle);
    background: var(--surface-3);
    object-fit: cover;
    box-shadow: 2px 2px 0 rgba(0, 0, 0, 0.4);
}

.mh-pick-endorsement-who {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

/* A <button> now, not a <span> - it opens the shared user-info modal. The UA
   button chrome is reset rather than each property being fought individually,
   and the hover colour is what tells you it is pressable at all. */
.mh-pick-endorser {
    display: block;
    max-width: 100%;
    padding: 0;
    border: none;
    background: transparent;
    overflow: hidden;
    color: var(--text);
    cursor: pointer;
    font-family: inherit;
    font-size: 1rem;
    font-weight: var(--weight-bold);
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    transition: color var(--dur-base) var(--ease-standard);
}

.mh-pick-endorser:hover,
.mh-pick-endorser:focus-visible {
    color: var(--brand-blue);
}

/* Small, uppercase, letter-spaced and muted - deliberately quiet next to
   the bold username above it. */
.mh-pick-when {
    color: var(--text-dim);
    font-size: 0.68rem;
    font-weight: var(--weight-medium);
    text-transform: uppercase;
    letter-spacing: 0.08em;
}

/* The endorser's own rating + watch status (endorserRating/endorserWatchStatus)
   - a third line beside the username/timestamp above it, same "render only
   what exists, divider only when both do" contract as .mh-pick-status-line
   in the hero (statusLine(), picks-carousel.js), just smaller and muted:
   this is secondary information next to a person's name, not a headline
   number. Sentence case, not the uppercase .mh-pick-when treatment - a
   humanized status ("Completed") reads oddly in small caps. */
.mh-pick-endorser-meta {
    display: flex;
    align-items: center;
    gap: 6px;
    margin-top: 2px;
    color: var(--text-dim);
    font-size: 0.72rem;
    font-weight: var(--weight-medium);
}

.mh-pick-endorser-rating {
    display: inline-flex;
    align-items: center;
    gap: 3px;
}

.mh-pick-endorser-rating i {
    color: var(--warning);
    font-size: 0.68rem;
}

.mh-pick-endorser-sep {
    flex-shrink: 0;
    width: 1px;
    height: 10px;
    background: var(--border-subtle);
}

/* The writing is the content here, not a chat row - generous size and
   line-height, full-strength foreground rather than the secondary tone. */
.mh-pick-endorsement-text {
    margin: 0 0 14px;
    color: var(--text);
    font-size: 1rem;
    font-weight: var(--weight-semibold);
    line-height: var(--leading-relaxed);
    /* Endorsement text is free-form user writing with no guaranteed break
       points (a URL, a long word) - let it wrap instead of overflowing the
       card. */
    overflow-wrap: anywhere;
}

.mh-pick-endorsement-actions {
    display: flex;
    align-items: center;
    gap: 8px;
    padding-top: 12px;
    border-top: 1px solid var(--border-subtle);
}

/* Owner actions sit LEFT (the default above); a like pushes RIGHT (owner).
   The row only ever holds one of the two - ownerActions() OR likeButton()
   in picks-thread.js - so which one is PRESENT is the honest discriminator.
   Keying on that rather than on a modifier class means there is no flag for
   the renderer to forget to set. */
.mh-pick-endorsement-actions:has(.mh-pick-like) {
    justify-content: flex-end;
}

/* Circular like (owner). Compound selector so it outranks .btn-ghost:
   a single class TIES with it and would then resolve on source order,
   which a later refactor can reshuffle without anyone noticing the shape
   went half-applied. */
.btn.mh-pick-like {
    display: inline-flex;
    position: relative;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    border-radius: var(--radius-circle);
    font-size: 0.9rem;
}

/* The count as a BADGE on the circle's edge (owner: beside it, the two "don't
   look synonymous"). Absolutely positioned so it rides the control instead of
   sitting in flow beside it - which also keeps the 44px circle circular.

   It is INSIDE the button, so the count is part of the same hit target. A
   figure that looks attached to a control but is not clickable is worse than
   one that looks detached.

   tabular-nums so the badge does not change width as the number ticks under
   an optimistic like. */
.mh-pick-like-count {
    position: absolute;
    right: -2px;
    bottom: -2px;
    min-width: 18px;
    padding: 1px 5px;
    border-radius: var(--radius-pill);
    background: var(--surface-3);
    color: var(--text-secondary);
    font-size: 0.68rem;
    font-variant-numeric: tabular-nums;
    line-height: var(--leading-snug);
}

.mh-pick-like.is-liked .mh-pick-like-count {
    background: var(--danger);
    color: var(--text-on-brand);
}

/* Icon-only, matching the hero's mh-pick-hero-cta treatment - a plain
   44x44 circle, no visible label, tooltip + aria-label carry the meaning.
   Only ever rendered on the viewer's OWN card (see ownerActions() in
   picks-thread.js) - there is deliberately no like button there, since
   self-likes are rejected server-side. */
.mh-pick-edit,
.mh-pick-delete {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    width: 44px;
    height: 44px;
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    border-radius: var(--radius-circle);
    font-size: 0.9rem;
}

.mh-pick-like.is-liked {
    color: var(--danger);
    border-color: var(--danger);
}

.mh-pick-like.is-liked i {
    color: var(--danger);
}

.mh-pick-delete:hover {
    background: var(--danger);
    border-color: var(--danger);
    color: var(--text-on-brand);
}

.mh-pick-railadd {
    flex-shrink: 0;
    align-self: flex-start;
    min-height: 44px;
}

/* ==========================================================================
   Empty state - shown instead of an endorsement list, either for a
   specific pick with none yet, or (via renderInvite) when there is no
   active pick at all. Built on the shared .mh-empty component
   (components.css) rather than a bespoke panel - owner direction is no
   tinted background box here either. This is the tab's most-seen state
   (only ~11.4% of anime carry any engine recommendation, and the DB holds
   a single user endorsement today), so it is the default experience, not
   a fallback, and gets real presence through type/spacing/a clear CTA
   instead of a box. */
.mh-pick-invite {
    width: 100%;
}

.mh-pick-invite .mh-pick-add {
    align-self: center;
}

@media (max-width: 640px) {
    /* Taller than the pre-island 240px, but bounded well short of the
       desktop range (400-560px) so a phone-width hero does not push the
       rest of the tab off screen - "keep it sane at 375px" (owner). */
    .mh-pick-hero {
        --mh-pick-hero-h: clamp(300px, 62vw, 380px);
    }

    .mh-pick-thread {
        height: 300px;
    }

    .mh-pick-avatar {
        width: 40px;
        height: 40px;
    }
}

/* ==========================================================================
   Endorse modal - body-mounted picker + compose dialog (endorse-modal.js).

   Reuses .mh-modal-overlay / .mh-modal-dialog (modal.css, loaded globally
   by desktop-navigation.ftlh) for the fixed positioning, scrim and
   enter/exit animation - these rules only widen the dialog and lay out its
   own content. The <link> that loads this stylesheet is a permanent element
   the picks-tab macro renders into the page, not a fragment that can be
   torn down out from under a body-mounted element.
   ========================================================================== */
.mh-pick-modal-dialog {
    position: relative;
    max-width: 480px;
    text-align: left;
}

.mh-pick-modal-close {
    position: absolute;
    top: 12px;
    right: 12px;
    width: 32px;
    height: 32px;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-circle);
    background: transparent;
    color: rgba(var(--fg-rgb, 255, 255, 255), 0.6);
    font-size: 0.85rem;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background 0.25s var(--ease-spring), color 0.25s var(--ease-spring), transform 0.25s var(--ease-spring);
}

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

.mh-pick-modal-subtitle {
    margin: 0 0 16px;
    color: var(--text-secondary);
    font-size: 0.85rem;
    line-height: var(--leading-relaxed);
}

.mh-pick-modal-search {
    box-sizing: border-box;
    width: 100%;
    padding: 10px 14px;
    margin-bottom: 12px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.05);
    color: var(--text);
    font-size: 0.875rem;
}

.mh-pick-modal-search:focus {
    outline: none;
    border-color: var(--border-brand);
    box-shadow: var(--ring-blue);
}

.mh-pick-modal-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
    max-height: 360px;
    overflow-y: auto;
}

.mh-pick-modal-row {
    display: flex;
    /* MUST stay 0. This row is a flex item in a 360px-capped scrolling column,
       and a flex item defaults to flex-shrink: 1 - so 35 rows worth of content
       against that cap compressed every row to its 44px min-height and the
       56px poster spilled out of it. The list scrolls; the rows do not
       negotiate. */
    flex-shrink: 0;
    align-items: center;
    gap: 12px;
    min-height: 44px;
    padding: 8px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    background: var(--surface-2);
    color: var(--text);
    text-align: left;
    cursor: pointer;
    transition: background var(--dur-base) var(--ease-standard);
}

.mh-pick-modal-row:hover {
    background: var(--surface-hover);
}

.mh-pick-modal-row-img {
    flex-shrink: 0;
    width: 40px;
    height: 56px;
    border-radius: var(--radius-sm);
    object-fit: cover;
}

.mh-pick-modal-row-body {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.mh-pick-modal-row-title {
    overflow: hidden;
    color: var(--text);
    font-size: 0.88rem;
    font-weight: var(--weight-medium);
    text-overflow: ellipsis;
    white-space: nowrap;
}

.mh-pick-modal-row-badge {
    align-self: flex-start;
    padding: 2px 8px;
    border-radius: var(--radius-pill);
    font-size: 0.7rem;
    font-weight: var(--weight-medium);
}

.mh-pick-modal-row-badge.is-engine {
    background: rgba(var(--brand-blue-rgb), 0.16);
    color: var(--brand-blue);
}

.mh-pick-modal-row-badge.is-yours {
    background: color-mix(in srgb, var(--success) 16%, transparent);
    color: var(--success);
}

.mh-pick-modal-empty {
    margin: 0;
    padding: 24px 8px;
    color: var(--text-secondary);
    font-size: 0.85rem;
    text-align: center;
}

/* Subject card. Compose is launched from a hero built entirely out of artwork,
   and arriving at a bare text row read as a different feature - so the poster
   returns here as a blurred, scrimmed backdrop behind the same facts. Painted
   by wireCompose() through Utils.setBackgroundImage, and only ever from a real
   URL: that helper refuses data: by design, and a blurred placeholder SVG
   would carry nothing anyway. */
.mh-pick-modal-subject {
    position: relative;
    display: flex;
    align-items: center;
    gap: 14px;
    overflow: hidden;
    padding: 12px;
    margin-bottom: 14px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-lg);
    background: var(--surface-grad);
}

/* Scaled past its own box so the blur's soft edge never exposes a transparent
   border. No opacity here: the dimming belongs entirely to the scrim below. */
.mh-pick-modal-subject-bg {
    position: absolute;
    inset: 0;
    z-index: 0;
    background-position: center;
    background-size: cover;
    filter: blur(22px) saturate(1.25);
    transform: scale(1.25);
}

/* The scrim is a SIBLING layer on the card, deliberately NOT the backdrop's
   own ::after. Nested inside the backdrop it would inherit that element's
   filter and be multiplied by its opacity, making the one layer responsible
   for keeping the title legible the weakest thing in the stack - and it would
   have failed silently, looking merely "a bit washed out" over pale artwork.
   A media scrim stays literal black under any theme (house rule). */
.mh-pick-modal-subject::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    background: linear-gradient(90deg, rgba(0, 0, 0, 0.9), rgba(0, 0, 0, 0.6));
    pointer-events: none;
}

/* Both real children are NAMED rather than covered by a `> *` convenience
   rule: a blanket selector would also catch the backdrop and outrank its own
   positioning, dropping it into the flex row (invariant #83). */
.mh-pick-modal-subject-img,
.mh-pick-modal-subject-body {
    position: relative;
    z-index: 2;
}

.mh-pick-modal-subject-img {
    flex-shrink: 0;
    width: 56px;
    height: 80px;
    border-radius: var(--radius-sm);
    object-fit: cover;
    box-shadow: var(--shadow-md);
}

.mh-pick-modal-note {
    display: flex;
    align-items: center;
    gap: 8px;
    margin: 0 0 12px;
    padding: 8px 12px;
    border-radius: var(--radius-md);
    background: rgba(var(--brand-blue-rgb), 0.1);
    color: var(--text-secondary);
    font-size: 0.8rem;
}

.mh-pick-modal-label {
    display: block;
    margin-bottom: 6px;
    color: var(--text-secondary);
    font-size: 0.8rem;
    font-weight: var(--weight-medium);
}

.mh-pick-modal-textarea {
    box-sizing: border-box;
    width: 100%;
    min-height: 100px;
    padding: 10px 14px;
    margin-bottom: 16px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-md);
    background: rgba(var(--fg-rgb, 255, 255, 255), 0.05);
    color: var(--text);
    font-size: 0.875rem;
    font-family: 'Poppins', sans-serif;
    line-height: var(--leading-relaxed);
    resize: vertical;
}

.mh-pick-modal-textarea:focus {
    outline: none;
    border-color: var(--border-brand);
    box-shadow: var(--ring-blue);
}

/* Actions sit on ONE row with Back at the leading edge and the commit pair at
   the trailing edge, sharing a baseline. Back used to float above the title on
   its own axis, which is what made it read as unaligned.

   ONE home for this row. It was declared twice - here and again ~290 lines
   below - so the gap: 12px written here was already dead, overridden by the
   later copy's 8px while still reading as live (invariant #33). The spacer
   child is what pushes Cancel/Post to the trailing edge, so justify-content
   only decides the layout when Back is absent. */
.mh-pick-modal-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 16px;
}

.mh-pick-modal-actions .btn {
    min-height: 44px;
}

@media (max-width: 640px) {
    .mh-pick-modal-dialog {
        max-width: calc(100vw - 40px);
    }

    .mh-pick-modal-list {
        max-height: 50vh;
    }

    .mh-pick-modal-actions {
        flex-direction: column-reverse;
    }

    .mh-pick-modal-actions .btn {
        width: 100%;
    }

    /* The spacer's whole job is pushing the commit pair to the trailing edge
       of ONE row. Stacked by column-reverse it would instead grow into a band
       of empty vertical space, so it stops participating here. */
    .mh-pick-modal-actions-spacer {
        display: none;
    }
}

/* ==========================================================================
   Vouch thread tools - search + sort
   ========================================================================== */

/* Only rendered when a thread has more than one vouch (picks-thread.js), so
   this never appears over a single card it could not usefully sift. */
.mh-pick-thread-tools {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
    margin-bottom: 14px;
}

.mh-pick-thread-search {
    position: relative;
    flex: 1 1 220px;
    min-width: 0;
}

.mh-pick-thread-search i {
    position: absolute;
    top: 50%;
    left: 12px;
    color: var(--text-secondary);
    font-size: 0.8rem;
    transform: translateY(-50%);
    /* The icon sits over the field; clicks belong to the input beneath it. */
    pointer-events: none;
}

.input.mh-pick-thread-q {
    width: 100%;
    min-height: 40px;
    padding-left: 34px;
}

/* type="search" paints a UA clear button that does not match this theme. */
.mh-pick-thread-q::-webkit-search-cancel-button {
    -webkit-appearance: none;
    appearance: none;
}

/* custom-select.js WRAPS the select in .cs-wrap and hides the native one, so
   after enhancement the flex child of the tools row is the WRAP, not the
   select. Sizing only the select left the wrap at its default flex sizing,
   which took a full row and pushed the search box onto its own line. */
.mh-pick-thread-tools .cs-wrap {
    /* An EXPLICIT basis, not "0 0 auto". A basis of auto resolves from the
       width property, and .cs-wrap carries width: 100% - so the wrap claimed
       the whole line and pushed the search box onto its own row. A stated
       basis ignores width entirely. */
    flex: 0 0 210px;
}

.mh-pick-thread-sort {
    flex: 0 0 auto;
    min-height: 40px;
}

/* Shown when a search matches nothing - distinct from the tab's own empty
   state, which means "nobody has vouched", not "your filter excluded them". */
.mh-pick-thread-none {
    margin: 0;
    padding: 22px 12px;
    color: var(--text-secondary);
    font-size: 0.9rem;
    text-align: center;
}

/* Nothing in the thread may exceed its column (owner).

   Only max-width is set here, deliberately. A blanket rule over descendants
   that also touched `position` or `display` would outrank the specific rules
   it sits beside - that is how an absolutely positioned child (the like
   badge) ends up back in flow. max-width alone cannot do that.

   min-width: 0 on the flex children is the other half: a flex item's default
   min-width is auto, so a long unbroken string pushes the item wider than its
   container no matter what max-width says. */
.mh-pick-thread {
    max-width: 100%;
}

/* Replaced content only - things with an intrinsic size that ignores their
   container. NOT a blanket over every descendant: ".mh-pick-thread *" ties
   with ".mh-pick-endorsement" on specificity and wins on source order, which
   silently replaced the card's own 840px cap with 100% and stretched every
   card across the full 1657px thread. The blanket looked harmless because it
   set only max-width - but max-width is precisely what the card relied on.

   Text is kept inside its column by min-width: 0 and overflow-wrap below,
   which is what actually held the 120-character URL. */
.mh-pick-thread img,
.mh-pick-thread video,
.mh-pick-thread iframe,
.mh-pick-thread svg,
.mh-pick-thread pre,
.mh-pick-thread table {
    max-width: 100%;
}

.mh-pick-thread-list,
.mh-pick-endorsement,
.mh-pick-endorsement-head,
.mh-pick-endorsement-actions {
    min-width: 0;
}

/* A pasted URL or an unbroken title wraps rather than widening the card. */
.mh-pick-endorsement-text,
.mh-pick-thread-head,
.mh-pick-endorser-name {
    overflow-wrap: anywhere;
}

/* "Add yours" - shares the mode column's two-line structure (a mark where
   their count sits, a label beneath) so it reads as part of the row rather
   than a lone circle parked next to it.

   Kept distinct from the filters by colour and a divider, not by shape: it
   carries brand colour against the modes' muted foreground, and it has no
   role="tab" or aria-selected, so it is never announced as a fourth option. */
.mh-pick-railadd {
    display: flex;
    flex-shrink: 0;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    margin-left: 4px;
    padding: 4px 4px 7px 16px;
    border: none;
    border-left: 1px solid var(--border-subtle);
    border-bottom: 2px solid transparent;
    background: transparent;
    color: var(--brand-blue);
    cursor: pointer;
    transition: color var(--dur-base) var(--ease-standard);
}

/* Sized to the mode count's optical height so the two labels share a
   baseline, rather than matching its font-size (an icon and a numeral of the
   same size do not sit the same). */
.mh-pick-railadd__mark {
    display: flex;
    align-items: center;
    justify-content: center;
    height: clamp(1.3rem, 0.9rem + 1.1vw, 1.85rem);
    font-size: 1.05rem;
    line-height: var(--leading-none);
}

.mh-pick-railadd__label {
    font-size: 0.78rem;
    white-space: nowrap;
}

.mh-pick-railadd:hover {
    color: var(--text);
}

/* Scroll sentinel for the vouch list. Needs real height and its own line: a
   zero-height element can never intersect, so a sentinel that collapses is a
   list that silently stops revealing. list-style is cleared because it is an
   <li> for valid markup inside the <ul>, not a visible row. */
.mh-pick-thread-sentinel {
    flex-shrink: 0;
    height: 1px;
    margin: 0;
    padding: 0;
    list-style: none;
}

/* Picker filters - status, type and order beside the existing search. */
.mh-pick-modal-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-bottom: 12px;
}

/* An EXPLICIT basis: custom-select.js wraps each select in .cs-wrap, which
   carries width: 100%, and a basis of auto would resolve from that width and
   give every filter its own row. */
.mh-pick-modal-filters .cs-wrap {
    flex: 1 1 130px;
    min-width: 0;
}

.mh-pick-modal-filter {
    width: 100%;
    min-height: 38px;
}

/* Row context: type, status, the viewer's own rating, the site score.

   Scrolls rather than wraps, per the house rule for horizontal fact rows -
   and now that the facts are bar-separated, wrapping would also strand a
   leading separator at the start of the second line, which reads as a stray
   mark rather than a divider. Children need flex-shrink: 0 or they compress
   into each other instead of overflowing (house rule). */
.mh-pick-modal-row-meta {
    display: flex;
    flex-wrap: nowrap;
    align-items: center;
    gap: 10px;
    margin-top: 2px;
    overflow-x: auto;
    scrollbar-width: none;
    -ms-overflow-style: none;
    color: var(--text-secondary);
    font-size: 0.74rem;
}

.mh-pick-modal-row-meta::-webkit-scrollbar {
    display: none;
}

.mh-pick-modal-row-meta > span {
    flex-shrink: 0;
}

/* Vertical rule between meta facts (owner). Both builders of this strip
   (endorse-modal.js picker row and compose subject) emit plain <span>s, so
   the separator lives in CSS and fixes both at once rather than being
   threaded through two JS join() calls. Drawn INSIDE the following item
   with a matching margin, so the parent gap sits evenly on either side of
   the bar instead of bunching it against one neighbour. */
.mh-pick-modal-row-meta > span + span::before {
    content: "";
    display: inline-block;
    width: 1px;
    height: 10px;
    margin-right: 10px;
    background: var(--border-subtle);
    vertical-align: -1px;
}

/* The viewer's own rating is the one figure here that is theirs, so it is the
   only one that carries colour. */
.mh-pick-modal-row-meta .is-yours {
    color: var(--warning);
}

/* ---- compose step ------------------------------------------------------ */

/* Subject block: poster beside title and the same four facts the picker row
   shows, so the context does not evaporate between choosing a title and
   writing about it. */
.mh-pick-modal-subject-body {
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 0;
}

.mh-pick-modal-subject-title {
    color: var(--text);
    font-size: 1.05rem;
    font-weight: var(--weight-bold);
    line-height: var(--leading-snug);
    overflow-wrap: anywhere;
}

/* Character counter. Neutral while there is room, warm as the cap nears, and
   danger only once it is reached - the colour is the warning the bare number
   is not. */
.mh-pick-modal-count {
    margin: 6px 0 0;
    color: var(--text-secondary);
    font-size: 0.76rem;
    font-variant-numeric: tabular-nums;
    text-align: right;
    transition: color var(--dur-base) var(--ease-standard);
}

.mh-pick-modal-count.is-near {
    color: var(--warning);
}

.mh-pick-modal-count.is-full {
    color: var(--danger);
}

.mh-pick-modal-actions-spacer {
    flex: 1 1 auto;
}

.mh-pick-modal-back {
    flex-shrink: 0;
}

/* ---- long vouches: clamp + read more ----------------------------------- */

/* Applied by picks-thread.js only to cards it MEASURED as overflowing, and
   only after they have rendered unclamped - so a card is never hidden waiting
   for script to release it.

   -webkit-line-clamp needs the -webkit-box display and vertical orient to do
   anything at all; the line count comes from JS (--mh-clamp-lines) so the
   number lives in one place.

   The last line fades out with a mask rather than a gradient overlay: an
   overlay would need a positioned child, and a child inside a -webkit-box is
   treated as another flex line - it would be clamped along with the text it
   was meant to sit over. */
.mh-pick-endorsement-text.is-clamped {
    /* The -webkit- pair still supplies the ELLIPSIS, but it does not bound the
       box: Chrome blockifies display: -webkit-box to flow-root, so the element
       lays out every line and the clamp only paints a marker. Measured here
       before the fix - computed display flow-root, scrollHeight equal to
       clientHeight, so the overflow test could never fire.

       max-height is what actually bounds it, and overflow: clip with
       overflow-clip-margin: content-box moves the clip edge off the padding
       box so the cut lands on a line boundary rather than revealing the top
       sliver of the next line. clip over hidden also avoids creating a scroll
       container. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: var(--mh-clamp-lines, 4);
    max-height: calc(var(--mh-clamp-lines, 4) * var(--leading-relaxed) * 1em);
    overflow: clip;
    overflow-clip-margin: content-box;
    -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
    mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
}

/* A real control, not a bare link: pill, hairline border, and its own hover
   state, so it reads as something to press rather than as more prose. */
.mh-pick-readmore {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: 32px;
    margin: -4px 0 14px;
    padding: 4px 14px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-pill);
    background: transparent;
    color: var(--text-secondary);
    cursor: pointer;
    font-size: 0.78rem;
    font-weight: var(--weight-semibold);
    transition:
        background var(--dur-base) var(--ease-standard),
        border-color var(--dur-base) var(--ease-standard),
        color var(--dur-base) var(--ease-standard);
}

.mh-pick-readmore:hover {
    border-color: var(--brand-blue);
    background: color-mix(in srgb, var(--brand-blue) 12%, transparent);
    color: var(--text);
}

/* ONE chevron that rotates - never two icons swapped, which makes the
   control's width jump as the label changes. */
.mh-pick-readmore i {
    font-size: 0.7rem;
    transition: transform var(--dur-base) var(--ease-standard);
}

.mh-pick-readmore[aria-expanded="true"] i {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .mh-pick-readmore,
    .mh-pick-readmore i {
        transition: none;
    }
}

/* The tools share the CARD's column, not the thread's full width - the cards
   are 840px and centred, so a full-width control row above them read as
   belonging to something else (owner). Same width, same centring, same left
   edge as .mh-pick-endorsement and .mh-pick-thread-head. */
.mh-pick-thread-tools,
.mh-pick-thread-none {
    width: 100%;
    max-width: 840px;
    margin-left: auto;
    margin-right: auto;
}

/* ---- per-endorser identity --------------------------------------------- */

/* A card whose author set profile colours wears them: the hard offset shadow
   takes their colour instead of flat black, so a thread of vouches reads as a
   set of different people rather than one repeated component.

   Only .has-colour, set by picks-thread.js when the author genuinely picked
   one. Applying a fallback colour to everyone would erase the distinction
   this exists to draw. */
.mh-pick-endorsement.has-colour {
    box-shadow: 5px 5px 0 color-mix(in srgb, var(--mh-en-color) 45%, rgba(0, 0, 0, 0.55));
}

/* The colour is theirs; the surface stays the site's. A tint this light keeps
   the card legible against every hue a user might pick - a saturated fill
   would make white text fail on the pale end of the range. */
.mh-pick-endorsement.has-colour::before {
    content: '';
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: linear-gradient(135deg,
        color-mix(in srgb, var(--mh-en-color) 9%, transparent),
        transparent 60%);
    pointer-events: none;
}

/* "Your take" carries the owner's colours too, as a gradient when they set an
   accent and a flat fill when they did not. */
.mh-pick-endorsement.has-colour .mh-pick-mine-badge {
    background: linear-gradient(135deg,
        var(--mh-en-color),
        var(--mh-en-accent, var(--mh-en-color)));
}

/* The avatar ring picks it up as well - the smallest place identity shows,
   and the one people scan first. */
.mh-pick-endorsement.has-colour .mh-pick-avatar {
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--mh-en-color) 70%, transparent);
}
