@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');

/* ─── Design Tokens ──────────────────────────────────────────────────────── */
:root {
    --bg-base:       #0f0e12;
    --bg-nav:        #141320;
    --bg-sidebar:    #13121a;
    --bg-panel:      #151422;
    --bg-card:       #1a1928;
    --bg-hover:      rgba(255,255,255,0.04);
    --bg-active:     rgba(139,132,232,0.09);
    --bg-input:      rgba(255,255,255,0.06);
    --border:        rgba(255,255,255,0.07);
    --border-med:    rgba(255,255,255,0.11);
    /* Readability tune-up: the original --text-dim (#585670, ~35% luminance) was
       legible on titles-adjacent bullets but dropped into hard-to-read territory for
       body-length hint text like the account settings copy. Bumped the two lower
       tiers up one notch each while keeping the 4-level hierarchy intact:
         primary   — titles, active nav                (unchanged)
         secondary — body copy                         (unchanged)
         muted     — nav-btn default, metadata         (+12 luminance)
         dim       — hints, placeholders, faint lines  (+19 luminance) */
    --text-primary:  #e8e5f0;
    --text-secondary:#bbb8cc;
    --text-muted:    #a5a2b8;
    --text-dim:      #8a87a0;
    /* Sub-dim tier below --text-dim — used for separator glyphs and small
       boxed labels (country code chip on the venue card, the · separator
       between photo-attribution segments) where the surrounding text is
       already dim and the secondary glyph needs to recede further without
       disappearing. Matches the value the inline fallbacks were using. */
    --text-very-dim: #677;
    --accent:        #8B84E8;
    --accent-light:  #b0aaf2;
    --accent-bg:     rgba(139,132,232,0.12);
    --accent-border: rgba(139,132,232,0.28);
    --owned:         #52a832;
    --owned-bg:      rgba(82,168,50,0.15);
    --owned-border:  rgba(82,168,50,0.3);
    --danger:        #c04828;
    --danger-bg:     rgba(192,72,40,0.10);
    --danger-border: rgba(192,72,40,0.28);
    --warn:          #b87820;

    /* Sizing — typography base bumped 13→15px for tablet readability; chrome
       kept tight (48px) so the topnav doesn't dominate the viewport. The 44px
       --min-touch token applies to a tap target's clickable area (achieved via
       padding around a smaller visual button), NOT the chrome height itself. */
    --topnav-height: 48px;
    --mobile-header-height: 48px;
    --mobile-footer-height: 56px;
    --min-touch: 44px;

    /* Teal palette for Biography page "concerts only" badges — distinct from the
       primary purple accent (used for collection ownership) and the green --owned
       (used for individual track ownership inside a release). */
    --teal:          #4cafa0;
    --teal-bg:       rgba(76,175,160,0.12);
    --teal-border:   rgba(76,175,160,0.28);
    --teal-light:    #6dd5c4;
}

/* Mobile nav surfaces — hidden on desktop, revealed in the 767px media query. */
.mobile-header { display: none; }
.mobile-footer { display: none; }
.more-drawer { display: none; }
.more-drawer-backdrop { display: none; }

/* ─── Reset / Base ───────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

html, body {
    height: 100%;
    margin: 0;
    padding: 0;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
    font-size: 15px;
    background: var(--bg-base);
    color: var(--text-primary);
    -webkit-font-smoothing: antialiased;
    overflow: hidden;
}

::-webkit-scrollbar { width: 3px; height: 3px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }

a { color: var(--accent-light); }
button { cursor: pointer; border: none; background: none; font: inherit; color: inherit; }

/* Routes.razor's <FocusOnNavigate Selector="h1" /> moves keyboard focus to
   the page's heading on every navigation — accessibility win for screen
   readers. The browser then draws its default focus outline on the focused
   <h1>/<h2>; for multi-line headings (e.g. the landing hero's three-line
   H1) the outline traces the union of inline boxes and produces a
   bracket-shaped artefact. :focus:not(:focus-visible) suppresses only when
   the browser considers the focus non-visible (programmatic focus, mouse
   click) — Tab-key keyboard navigation still gets the default outline via
   :focus-visible. Applies to all <h1>/<h2> across the app. */
h1:focus:not(:focus-visible),
h2:focus:not(:focus-visible) { outline: none; }

/* The landing hero H1 is a three-line display headline. Chrome draws
   the :focus outline as the union of inline boxes, producing a
   parenthesis/bracket artefact at top and bottom. The :focus-visible
   rule above doesn't cover this case: FocusOnNavigate programmatically
   focuses the H1, and Chromium's :focus-visible heuristic still matches
   when the prior interaction was keyboard-initiated. Suppress
   unconditionally on this H1 specifically — the next Tab stop is the
   hero CTA, which retains its own focus-visible ring, so keyboard
   users keep their orientation. */
.landing-hero-headline:focus { outline: none; }

/* ─── Top Nav ────────────────────────────────────────────────────────────── */
.topnav {
    display: flex;
    align-items: center;
    gap: 4px;
    height: var(--topnav-height);
    padding: 0 16px;
    background: var(--bg-nav);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
    position: sticky;
    top: 0;
    z-index: 100;
}

.nav-logo {
    display: inline-flex;
    align-items: center;
    margin-right: 14px;
    text-decoration: none;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
}

.nav-logo img {
    height: 30px;
    width: auto;
    display: block;
}

.nav-logo .logo-dim { color: var(--text-dim); font-weight: 400; }

/* nav-btn: visual button is 32px tall; the 6/14 padding around it gives a
   ~44px clickable area (the actual touch target, even though the visual chip
   is shorter). This is the same trick mobile OS keyboards use — visual button
   is small, hit-test area is generous. */
.nav-btn {
    padding: 6px 14px;
    border-radius: 6px;
    font-size: 14px;
    color: var(--text-muted);
    transition: color 0.15s, background 0.15s;
    background: none;
    border: none;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    line-height: 1;
}

.nav-btn:hover { color: var(--text-primary); background: var(--bg-hover); }
.nav-btn.active { color: var(--accent-light); background: var(--accent-bg); }
.nav-spacer { flex: 1; }

/* ─── Add Hub button + popover (Phase 2 nav restructure) ─────────────────── */
/* Filled accent button to the right of the brand mark — single, obvious entry
   point for "Add an album / concert". Replaces the standalone Scan/Search/
   Discography nav items.  */
.add-hub-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 14px;
    border-radius: 7px;
    background: var(--accent);
    color: #14111c;
    font-size: 14px;
    font-weight: 600;
    border: none;
    cursor: pointer;
    transition: background 0.15s, transform 0.05s;
    margin-right: 8px;
    line-height: 1;
}

.add-hub-btn:hover { background: var(--accent-light); }
.add-hub-btn:active { transform: scale(0.98); }

.add-hub-plus {
    font-size: 18px;
    font-weight: 700;
    line-height: 1;
}

/* Desktop popover — anchored below the topnav, opens on +Add click. Wide enough
   for two-line items with an icon, kept narrow enough that a long destination
   list doesn't dominate the topnav. */
.add-hub-popover {
    position: fixed;
    top: calc(var(--topnav-height) + 8px);
    left: 80px;
    z-index: 270;
    width: 360px;
    max-height: calc(100vh - var(--topnav-height) - 24px);
    overflow-y: auto;
    background: var(--bg-panel);
    border: 1px solid var(--border-med);
    border-radius: 10px;
    box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
    padding: 10px;
}

.add-hub-backdrop {
    position: fixed;
    inset: 0;
    background: transparent;
    z-index: 265;
}

/* Viewport gate (Phase 2 follow-up bug): the page renders BOTH the desktop
   popover and the mobile bottom sheet whenever addHubOpen=true. Hide whichever
   doesn't belong at the current width so they don't double up.
   Threshold: 767px matches the rest of the mobile breakpoints in this file. */
@media (max-width: 767px) {
    .add-hub-popover,
    .add-hub-backdrop { display: none; }
}
@media (min-width: 768px) {
    .bottom-sheet,
    .bottom-sheet-backdrop { display: none; }
}

.add-hub-section-label {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-dim);
    padding: 8px 12px 6px;
}

.add-hub-item {
    display: flex;
    align-items: center;
    width: 100%;
    gap: 14px;
    padding: 12px 14px;
    background: none;
    border: none;
    border-radius: 8px;
    cursor: pointer;
    text-align: left;
    color: var(--text-primary);
    transition: background 0.12s;
    min-height: var(--min-touch);
}

.add-hub-item:hover { background: var(--bg-hover); }

.add-hub-item-icon {
    width: 32px;
    height: 32px;
    border-radius: 8px;
    background: var(--accent-bg);
    color: var(--accent-light);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 18px;
}

.add-hub-item-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
}

.add-hub-item-title {
    font-size: 14px;
    font-weight: 500;
    color: var(--text-primary);
}

.add-hub-item-sub {
    font-size: 12px;
    color: var(--text-dim);
}

.add-hub-sep {
    border: 0;
    border-top: 1px solid var(--border);
    margin: 6px 6px;
}

/* Mobile centre-raised + button. Sits in the middle of the 5-slot footer,
   visually elevated above the bar so the primary "Add" affordance is the
   obvious target on every screen. */
.add-hub-centre-btn {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--accent);
    color: #14111c;
    border: 4px solid var(--bg-base);
    cursor: pointer;
    flex-shrink: 0;
    margin-top: -20px;
    display: flex;
    align-items: center;
    justify-content: center;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
    transition: background 0.15s, transform 0.08s;
}

.add-hub-centre-btn:hover { background: var(--accent-light); }
.add-hub-centre-btn:active { transform: scale(0.95); }

.add-hub-centre-plus {
    font-size: 28px;
    font-weight: 600;
    line-height: 1;
}

/* Bottom sheet — slides up from the bottom on mobile when the centre + is tapped.
   Uses dvh so it respects the keyboard / browser chrome. */
.bottom-sheet-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.5);
    z-index: 280;
    animation: bottom-sheet-fade-in 0.15s ease-out;
}

.bottom-sheet {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 281;
    background: var(--bg-panel);
    border-top: 1px solid var(--border-med);
    border-radius: 16px 16px 0 0;
    padding: 8px 14px max(20px, env(safe-area-inset-bottom));
    max-height: 80dvh;
    overflow-y: auto;
    animation: bottom-sheet-slide-up 0.22s cubic-bezier(0.2, 0.8, 0.2, 1);
    box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.5);
}

.bottom-sheet-handle {
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border-med);
    margin: 6px auto 10px;
}

.bottom-sheet-content {
    display: flex;
    flex-direction: column;
}

@keyframes bottom-sheet-fade-in {
    from { opacity: 0; }
    to { opacity: 1; }
}

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

/* Help button — always visible in chrome. Re-triggers Tier 2 banners and
   Tier 3 hotspots for the current page. */
.help-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 32px;
    padding: 0 12px 0 8px;
    border-radius: 16px;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    color: var(--text-secondary);
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    transition: all 0.15s;
    margin-right: 8px;
    line-height: 1;
}

.help-btn:hover {
    background: var(--bg-hover);
    color: var(--accent-light);
    border-color: var(--accent-border);
}

.help-btn .help-icon {
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--accent-bg);
    color: var(--accent);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    font-weight: 700;
    line-height: 1;
    flex-shrink: 0;
}

.help-btn .help-label { line-height: 1; }

/* Mobile: pill with ?-icon + "Help" text. Matches the desktop affordance so
   first-time users can identify the entry point — a bare ? glyph wasn't
   self-explanatory enough. Width is auto so the pill grows to fit the label;
   border-radius rounds it gracefully. */
.help-btn.mobile-help-btn {
    height: 36px;
    padding: 0 12px 0 10px;
    border-radius: 18px;
    margin-right: 0;
    align-items: center;
    gap: 6px;
}
.help-btn.mobile-help-btn .help-label {
    /* Override the desktop ".help-btn .help-label { line-height: 1 }" only
       insofar as it would inherit display from a generic hide rule. */
    display: inline;
    font-size: 13px;
    line-height: 1;
}
.help-btn.mobile-help-btn .help-icon {
    width: 22px;
    height: 22px;
    font-size: 13px;
}

/* 3-cycle pulse fired when a Tier 2 banner first auto-dismisses, drawing the
   eye toward the Help button as the place where dismissed prompts can be
   re-triggered. */
@keyframes help-btn-pulse {
    0% { box-shadow: 0 0 0 0 rgba(139, 132, 232, 0.7); }
    100% { box-shadow: 0 0 0 16px rgba(139, 132, 232, 0); }
}
.help-btn.pulse-once {
    animation: help-btn-pulse 1.0s ease-out 3;
    border-color: var(--accent);
}

/* Tier 2 onboarding banner — inline prompt that demonstrates a hidden
   interaction. Sits in the page flow above the affected content. */
.onboarding-banner {
    display: flex;
    /* flex-start so the icon and dismiss button align to the top of the
       text block when it wraps to multiple lines. align-items: center looked
       balanced for one-line copy but caused awkward vertical positioning
       (and on some flex layouts apparent clipping) when the text wrapped to
       2-3 lines on a narrow viewport. */
    align-items: flex-start;
    gap: 14px;
    padding: 14px 18px;
    margin: 0 24px 16px;
    border-radius: 8px;
    background: linear-gradient(90deg, var(--accent-bg), rgba(139, 132, 232, 0.04));
    border: 1px solid var(--accent-border);
    flex-shrink: 0;
    animation: onboarding-banner-in 0.28s ease-out;
    /* `overflow: hidden` was previously on the base class for the old
       max-height transition. The dismiss is now a keyframe animation that
       sets its own overflow via the .dismissing class — keeping it on the
       base capped multi-line text height in some browsers. */
}

@keyframes onboarding-banner-in {
    from { opacity: 0; transform: translateY(-6px); }
    to { opacity: 1; transform: translateY(0); }
}

.onboarding-banner-icon {
    font-size: 20px;
    flex-shrink: 0;
}

.onboarding-banner-text {
    flex: 1;
    min-width: 0;
    font-size: 14px;
    color: var(--text-primary);
    line-height: 1.5;
    /* Be explicit — a flex child with no min-width can be forced narrower than
       its content, and any inline-block child (like the inline Owned pill)
       can then push the line off the side. min-width: 0 lets the text shrink
       so wrapping actually happens; word-wrap as a belt-and-braces in case a
       very long token (URL, etc.) wedges the layout. */
    word-wrap: break-word;
    overflow-wrap: anywhere;
}

@media (max-width: 600px) {
    /* Mobile: shrink the banner chrome so the text gets meaningful width. The
       desktop margin (24px each side) and padding (18px each side) leave only
       ~200px of text column on a 375px viewport — with the icon (20px) and
       close button (44px) consuming more, the text wraps awkwardly or gets
       clipped on narrow phones. */
    .onboarding-banner {
        margin: 0 10px 12px;
        padding: 12px 14px;
        gap: 10px;
    }
    .onboarding-banner-text {
        font-size: 13px;
    }
    .onboarding-banner-close {
        width: 36px;
        height: 36px;
        font-size: 20px;
    }
}

/* Inline pill placeholder used inside banner copy (e.g. "click any track marked
   [Owned]" — the badge is shown in-line so the user can match it visually
   to the actual badges in the setlist). */
.onboarding-banner-inline-pill.owned-pill {
    margin-left: 2px;
    margin-right: 2px;
    vertical-align: baseline;
}

/* Add-mode placeholder for concert image upload (P0-5). Add mode has no
   concertId yet, so the SfUploader can't be wired — point users at the next
   step rather than leaving an empty space where they expect a control. */
.concert-image-add-hint {
    padding: 12px 14px;
    background: var(--bg-card);
    border: 1px dashed var(--border-med);
    border-radius: 6px;
    color: var(--text-dim);
    font-size: 13px;
    line-height: 1.4;
}

.onboarding-banner-close {
    width: var(--min-touch);
    height: var(--min-touch);
    border-radius: 50%;
    background: transparent;
    border: none;
    color: var(--text-dim);
    cursor: pointer;
    font-size: 22px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
}

.onboarding-banner-close:hover {
    color: var(--text-primary);
    background: rgba(255, 255, 255, 0.06);
}

/* Dismiss animation: the banner shrinks and translates upward toward the Help
   button position so the eye follows the motion to the destination. The
   numeric translate values target the upper-right corner of typical viewports
   — close enough to read as "going to where the ? Help button lives". */
.onboarding-banner.dismissing {
    animation: onboarding-banner-to-help 0.9s cubic-bezier(0.4, 0.05, 0.3, 1) forwards;
    transform-origin: top right;
    pointer-events: none;
    /* Clip during dismiss so the transform-scale doesn't blow content outside
       the banner box mid-animation. */
    overflow: hidden;
}

@keyframes onboarding-banner-to-help {
    0%   { opacity: 1; transform: translate(0, 0) scale(1); max-height: 200px; }
    20%  { opacity: 1; transform: translate(2%, -8%) scale(0.94); max-height: 200px; }
    80%  { opacity: 0.4; transform: translate(40%, -200%) scale(0.18); max-height: 200px; }
    100% {
        opacity: 0;
        transform: translate(46%, -240%) scale(0.12);
        max-height: 0;
        margin-top: 0;
        margin-bottom: 0;
        padding-top: 0;
        padding-bottom: 0;
        border-color: transparent;
    }
}

/* Tier 3 hotspot — pulsing dot anchored to a UI element whose interactivity
   isn't otherwise obvious. The visible dot is small (10–14px), but the button
   itself is a 44×44 hit target (per --min-touch) so touch users can land on
   it reliably. The dot + pulse render via pseudo-elements centred inside the
   transparent button.

   Position via the host element (relative parent → absolute hotspot). Default
   anchor is top-right of the host; placements that need a different anchor
   can override via inline style. */
.hotspot-dot {
    position: absolute;
    top: -10px;
    right: -10px;
    /* 44×44 hit target. The visible dot is the ::before pseudo-element,
       centred via flex. The negative inset above pulls the centre of the
       hit target close to the host's corner, so the visible dot appears
       to peek out from the corner rather than the corner being the
       button's centre. */
    width: var(--hotspot-size, 44px);
    height: var(--hotspot-size, 44px);
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    cursor: pointer;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    z-index: 5;
}
.hotspot-dot::before {
    /* Visible coloured dot. Size driven by the modifier class below. */
    content: '';
    width: var(--hotspot-dot-size, 14px);
    height: var(--hotspot-dot-size, 14px);
    background: var(--accent);
    border-radius: 50%;
    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.08);
    transition: transform 0.15s ease;
}
.hotspot-dot:hover::before,
.hotspot-dot.is-active::before {
    transform: scale(1.15);
}

.hotspot-dot-sm { --hotspot-dot-size: 10px; }
.hotspot-dot-md { --hotspot-dot-size: 14px; }

.hotspot-pulse {
    /* Pulse ring sits at the same centre as the dot (inside the hit-target
       button), sized to match the visible dot, expanding outward. */
    position: absolute;
    width: var(--hotspot-dot-size, 14px);
    height: var(--hotspot-dot-size, 14px);
    border-radius: 50%;
    background: var(--accent);
    animation: hotspot-pulse 1.6s ease-out infinite;
    pointer-events: none;
}
.hotspot-dot.is-active .hotspot-pulse {
    /* Tooltip is open — the dot has done its discovery job, stop pulsing
       so it doesn't compete with the tooltip's own attention. */
    animation: none;
    opacity: 0;
}

@keyframes hotspot-pulse {
    0% { transform: scale(1); opacity: 0.7; }
    100% { transform: scale(3); opacity: 0; }
}

/* ─── Hotspot tooltip ────────────────────────────────────────────────────── */
/* Pops up on tap (touch) or hover (mouse) of a hotspot dot. Anchored to the
   same host element as the dot via absolute positioning; placed below + right
   of the dot's natural top-right corner. Tap-outside closes via the backdrop;
   "Got it" inside persistently dismisses the hotspot. */
.hotspot-tooltip {
    position: absolute;
    top: 32px;             /* clears the dot + pulse */
    /* Centre the tooltip below the host so it extends equally in both
       directions. With a right-anchored tooltip, hosts near the left edge of
       a scroll-clipped pane (e.g. chips on the concert detail) had the
       tooltip extending past the pane's left boundary and getting clipped.
       Centring halves the worst-case overhang. The viewport-edge media query
       below caps width on small screens. */
    left: 50%;
    transform: translateX(-50%);
    z-index: 1001;
    min-width: 180px;
    max-width: 260px;
    padding: 10px 12px;
    background: var(--bg-elev, var(--bg-card));
    color: var(--text);
    border: 1px solid var(--accent-border, var(--accent));
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
    font-size: 13px;
    line-height: 1.4;
    animation: hotspot-tooltip-in 0.15s ease-out;
}
.hotspot-tooltip-arrow {
    /* Small triangle pointing up at the dot. Two stacked pseudo-shapes give
       the bordered-tooltip effect — outer matches border colour, inner offset
       by 1px matches background. Centred horizontally on the tooltip (which
       is itself centred on the host's horizontal centre). */
    position: absolute;
    top: -7px;
    left: 50%;
    transform: translateX(-50%);
    width: 12px;
    height: 7px;
    overflow: hidden;
}
.hotspot-tooltip-arrow::before,
.hotspot-tooltip-arrow::after {
    content: '';
    position: absolute;
    top: 0;
    left: 1px;
    width: 10px;
    height: 10px;
    transform: rotate(45deg);
    transform-origin: 0 0;
}
.hotspot-tooltip-arrow::before {
    background: var(--accent-border, var(--accent));
}
.hotspot-tooltip-arrow::after {
    top: 1px;
    background: var(--bg-elev, var(--bg-card));
}
.hotspot-tooltip-text {
    margin-bottom: 8px;
    color: var(--text);
}
.hotspot-tooltip-dismiss {
    background: var(--accent-bg);
    color: var(--accent-light);
    border: 1px solid var(--accent-border);
    border-radius: 6px;
    padding: 4px 12px;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    min-height: 32px;
    transition: background 0.15s;
}
.hotspot-tooltip-dismiss:hover {
    background: rgba(139, 132, 232, 0.25);
}

/* Backdrop catches outside taps when a tooltip is open. Transparent — the
   tooltip itself is the focal element. Sits below the tooltip's z-index so
   the tooltip is interactive while the rest of the page is not. */
.hotspot-tooltip-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1000;
    background: transparent;
}

@keyframes hotspot-tooltip-in {
    from { opacity: 0; transform: translateY(-4px); }
    to { opacity: 1; transform: translateY(0); }
}

/* Mobile: cap tooltip width so the centred-below-host anchor doesn't blow
   past the viewport. The centring transform keeps it horizontally aligned
   with the host; max-width prevents overflow on narrow screens. */
@media (max-width: 600px) {
    .hotspot-tooltip {
        max-width: calc(100vw - 32px);
    }
}

@media (prefers-reduced-motion: reduce) {
    .hotspot-pulse { animation: none; }
    .hotspot-tooltip { animation: none; }
}

@media (prefers-reduced-motion: reduce) {
    .hotspot-pulse { animation: none; }
    .help-btn.pulse-once { animation: none; }
    .onboarding-banner { animation: none; }
    .onboarding-banner.dismissing { animation: none; opacity: 0; max-height: 0; }
}

/* ─── App Body ───────────────────────────────────────────────────────────── */
.app-body {
    position: relative;
    height: calc(100vh - var(--topnav-height));
    overflow: hidden;
}

/* ─── View Layouts ───────────────────────────────────────────────────────── */
.view-3pane {
    display: flex;
    height: 100%;
    overflow: hidden;
}

.view-2pane {
    display: flex;
    height: 100%;
    overflow: hidden;
}

/* ─── Panes ──────────────────────────────────────────────────────────────── */
.pane-sidebar {
    width: 240px;
    flex-shrink: 0;
    background: var(--bg-sidebar);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.pane-sidebar--search { width: 320px; }
/* Concerts list rows carry a 28px artist/concert thumbnail before the date badge,
   eating into the artist-name budget at the default 240px. Matches the Artists
   page (.pane-sidebar-full max-width 480px) so long names like "Suicidal Tendencies"
   / "Rage Against The Machine" fit without ellipsis truncation. */
.pane-sidebar--concerts { width: 480px; }

.pane-mid {
    width: 260px;
    flex-shrink: 0;
    background: var(--bg-panel);
    border-right: 1px solid var(--border);
    display: flex;
    flex-direction: column;
    overflow: hidden;
}

.pane-detail {
    flex: 1;
    background: var(--bg-base);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-width: 0;
    /* Establishes a container-query context so .detail-hero can react to the pane's
       actual width rather than the viewport — viewport queries are wrong when the
       pane sits beside other rails that consume horizontal space. */
    container-type: inline-size;
}

.pane-detail-wide {
    flex: 1;
    background: var(--bg-base);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    min-width: 0;
    container-type: inline-size;
}

/* ─── Panel Header ───────────────────────────────────────────────────────── */
.panel-header {
    padding: 12px 16px 10px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.panel-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 8px;
}

.panel-title .count {
    font-size: 12px;
    color: var(--text-dim);
    font-weight: 500;
    padding: 2px 10px;
    border-radius: 12px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    margin-left: 8px;
    flex-shrink: 0;
    /* Ensure a tab-like number sits centred regardless of digit width. */
    min-width: 22px;
    text-align: center;
}

.filter-input-row {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    background: var(--bg-input);
    border-radius: 6px;
    border: 1px solid var(--border);
    min-height: 38px;
}

.filter-input-row .ic { font-size: 13px; color: var(--text-dim); flex-shrink: 0; }

.filter-input-row input {
    font-size: 13px;
    color: var(--text-secondary);
    background: none;
    border: none;
    outline: none;
    width: 100%;
    font-family: inherit;
}

.filter-input-row input::placeholder { color: var(--text-dim); }

.active-filter-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin: 6px 14px;
    padding: 4px 10px;
    font-size: 12px;
    color: var(--accent-light);
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    border-radius: 12px;
}
.active-filter-clear {
    background: none;
    border: none;
    color: var(--accent-light);
    font-size: 14px;
    cursor: pointer;
    padding: 0 2px;
    line-height: 1;
    min-width: var(--min-touch);
    min-height: var(--min-touch);
    display: flex;
    align-items: center;
    justify-content: center;
}

/* ─── Scrollable List ────────────────────────────────────────────────────── */
.list-scroll {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    position: relative;
}

.section-label {
    padding: 12px 16px 4px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
}

/* ─── Artist Row ─────────────────────────────────────────────────────────── */
.artist-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    cursor: pointer;
    transition: background 0.12s;
    min-height: var(--min-touch);
}

.artist-row:hover { background: var(--bg-hover); }
.artist-row.active { background: var(--bg-active); }
.artist-row.active .artist-name { color: var(--accent-light); }

.artist-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-muted);
    overflow: hidden;
    flex-shrink: 0;
}

.artist-avatar img { width: 100%; height: 100%; object-fit: cover; }
.artist-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.artist-name { font-size: 14px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.artist-tag { font-size: 12px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; text-transform: capitalize; }
.artist-count { font-size: 12px; color: var(--text-dim); }

/* ─── Rail content base layout ──────────────────────────────────────────────
   .rail-content holds the panel header + ArtistList. In rail-collapsed mode
   (Wall/Stack views) the absolute-positioned overlay rule below sets its own
   display:flex / flex-direction:column. In default (List) mode there was no
   base rule, so when ArtistList introduced an .artist-list-body wrapper that
   needed `flex: 1` for the inner .list-scroll to scroll, the parent wasn't a
   flex column — the chain broke and the artist list stopped scrolling.
   Setting the base here keeps both modes consistent. */
.rail-content {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

/* ─── Letter Jump Strip ─────────────────────────────────────────────────── */
.artist-list-body {
    display: flex;
    flex: 1;
    min-height: 0;
}
.artist-list-body > .list-scroll { flex: 1; min-width: 0; }

.letter-jump {
    display: flex;
    flex-direction: column;
    position: sticky;
    top: 0;
    z-index: 2;
    padding: 4px 2px;
    align-items: center;
    overflow-y: auto;
    scrollbar-width: none;
}
.letter-jump::-webkit-scrollbar { display: none; }
.letter-jump-btn {
    font-size: 10px;
    padding: 1px 6px;
    background: none;
    border: none;
    color: var(--accent);
    cursor: pointer;
    line-height: 1.4;
    min-width: 22px;
    text-align: center;
}
.letter-jump-btn:active { background: var(--accent-bg); border-radius: 3px; }
.letter-jump-disabled {
    color: var(--text-dim);
    opacity: 0.4;
    cursor: default;
}

@media (max-width: 900px) {
    .letter-jump { display: none; }
}

/* ─── Album / Release Row ────────────────────────────────────────────────── */
.album-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 16px;
    cursor: pointer;
    transition: background 0.12s;
    min-height: var(--min-touch);
    background: linear-gradient(90deg,
        color-mix(in srgb, var(--row-color, transparent) 25%, transparent) 0%,
        transparent 70%);
}

.album-row:hover { background: var(--bg-hover); }
.album-row.active { background: var(--bg-active); }
.album-row.active .album-title { color: var(--accent-light); }

.album-thumb {
    width: 48px;
    height: 48px;
    border-radius: 5px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    object-fit: cover;
    flex-shrink: 0;
}

.album-thumb-placeholder {
    width: 48px;
    height: 48px;
    border-radius: 5px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    color: var(--text-dim);
}

.album-info { flex: 1; min-width: 0; }
.album-title { font-size: 14px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.album-artist-name { font-size: 12px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
.album-meta { font-size: 12px; color: var(--text-dim); margin-top: 3px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.album-title.not-owned { color: var(--text-muted); font-style: italic; }

/* Phase 5 enrichment badges on album meta line */
.album-type-badge {
    display: inline-flex;
    align-items: center;
    padding: 1px 6px;
    height: 16px;
    border-radius: 3px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.03em;
    text-transform: uppercase;
    background: var(--bg-card);
    color: var(--text-dim);
    border: 1px solid var(--border);
    flex-shrink: 0;
    line-height: 1;
}

/* UX review Pattern C — generic per-fact chip on album row meta lines (year,
   country, format, packaging, catalogue). Lower-key than .album-type-badge so
   the enrichment-derived AlbumType still leads visually; same shape so the
   row scans cleanly. Smaller text + subtler border keeps a multi-chip row
   from competing with the album title above it. */
.album-meta-chip {
    display: inline-flex;
    align-items: center;
    padding: 1px 6px;
    height: 16px;
    border-radius: 3px;
    font-size: 11px;
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border-soft, var(--border));
    flex-shrink: 0;
    line-height: 1;
    white-space: nowrap;
}

.album-mixed-badge {
    font-size: 11px;
    flex-shrink: 0;
    opacity: 0.7;
}

.owned-dot {
    width: 7px;
    height: 7px;
    border-radius: 50%;
    background: var(--owned);
    flex-shrink: 0;
}

/* ─── Release Group Row ──────────────────────────────────────────────────── */
.rg-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 16px;
    cursor: pointer;
    transition: background 0.12s;
    min-height: var(--min-touch);
}

.rg-row:hover { background: var(--bg-hover); }
.rg-row.active { background: var(--bg-active); }
.rg-row.active .rg-title { color: var(--accent-light); }

/* Thumbnail on Discography master rows (UX review item 24). Mirrors the
   .album-thumb sizing/treatment so master rows and pressing rows feel
   consistent — only the surrounding row class differs. */
.rg-thumb {
    width: 40px;
    height: 40px;
    border-radius: 4px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    object-fit: cover;
    flex-shrink: 0;
}
.rg-thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
    color: var(--text-dim);
}

.rg-info { flex: 1; min-width: 0; }
.rg-title { font-size: 14px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.rg-title.not-owned { color: var(--text-muted); font-style: italic; }
.rg-year { font-size: 12px; color: var(--text-dim); margin-top: 2px; }

/* ─── Empty State ────────────────────────────────────────────────────────── */
.empty-state {
    flex: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-direction: column;
    gap: 12px;
    color: var(--text-dim);
    font-size: 15px;
    line-height: 1.5;
    padding: 32px 24px;
    text-align: center;
}

.empty-state .empty-icon { font-size: 40px; opacity: 0.4; }
.empty-state .empty-cta {
    margin-top: 8px;
    padding: 10px 18px;
    background: var(--accent-bg);
    color: var(--accent-light);
    border: 1px solid var(--accent-border);
    border-radius: 6px;
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    min-height: var(--min-touch);
}
.empty-state .empty-cta:hover { background: rgba(139,132,232,0.18); }

/* Tier 1 hint shown above the artist list — describes what picking an artist
   unlocks (the cross-reference view). Subtle so it doesn't compete with the
   list itself. */
.artists-empty-preview {
    margin: 0;
    padding: 12px 16px 14px;
    font-size: 13px;
    color: var(--text-dim);
    border-bottom: 1px solid var(--border);
    line-height: 1.45;
}

/* Pattern K (Phase 5c, review item 43): bottom-edge fade on scrollable
   regions where content overflows. Hints "more below" without consuming
   layout space. Apply via utility class so any `.list-scroll`-style container
   can opt in.

   Implementation: `position: sticky; bottom: 0` on the ::after pins the
   gradient to the bottom of the visible scroll viewport. The original
   `position: absolute; bottom: 0` was wrong — for an absolute child of an
   `overflow-y: auto` parent, "bottom" anchors to the content bottom (i.e.
   far off-screen below the visible area) not to the viewport. Sticky
   position participates in flow, so the negative `margin-top` makes the
   pseudo-element overlap the last 28px of content rather than reserving
   extra layout space below it. */
.scroll-fade {
    position: relative;
}

.scroll-fade::after {
    content: '';
    display: block;
    position: sticky;
    bottom: 0;
    left: 0;
    right: 0;
    /* Deeper and more aggressive than the original 28px @ 0.85 — that pair
       was too subtle on the dark theme. 44px overall depth gives enough
       runway to read as a fade; the gradient hits full --bg-panel by 75%
       so the dim is decisive in the lower half rather than spread thinly
       across the whole region. Full opacity removes the residual lightness
       that made the original look like a mistake on this colour scheme. */
    height: 44px;
    margin-top: -44px;
    background: linear-gradient(to bottom, transparent 0%, var(--bg-panel) 75%);
    pointer-events: none;
    transition: opacity 0.2s ease-out;
}

/* Hide the fade when the user has scrolled to (or near) the bottom — otherwise
   the last item is permanently overlaid by the gradient and unreadable. The
   `.scroll-fade-at-end` class is toggled by the scroll-fade JS in App.razor
   (initial state set on mount via MutationObserver + on every scroll event
   via a capture-phase document listener). */
.scroll-fade.scroll-fade-at-end::after {
    opacity: 0;
}

/* ─── Detail Hero ────────────────────────────────────────────────────────── */
/* Three-column layout: covers (auto-width) | info (flex-grow) | optional Your
   Collection panel (~320px). The collection column is rendered conditionally; when
   absent, the layout is effectively two-column. Below 900px the row collapses to a
   stacked column so the panel sits below the info on narrow screens. */
.detail-hero {
    padding: 24px 24px 16px;
    display: flex;
    gap: 20px;
    align-items: flex-start;
    flex-shrink: 0;
    position: relative;
    background: linear-gradient(160deg,
        color-mix(in srgb, var(--cover-color, var(--accent)) 28%, var(--bg-panel)) 0%,
        var(--bg-panel) 52%);
    border-bottom: 1px solid var(--border);
}

/* Stack hero columns when the pane (NOT the viewport) is too narrow to fit all three
   side by side even with the panel at its 360px minimum. Container query: the
   breakpoint is on the pane's width, set up via `container-type: inline-size` on
   .pane-detail*. Matches the .hero-collection breakpoint below. */
@container (max-width: 900px) {
    .detail-hero {
        flex-wrap: wrap;
    }
}

.hero-covers { display: flex; gap: 10px; flex-shrink: 0; align-items: flex-start; }

.cover-main {
    width: 140px;
    height: 140px;
    border-radius: 6px;
    object-fit: cover;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    box-shadow: 0 8px 24px rgba(0,0,0,0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 32px;
    color: var(--text-dim);
}

/* When a cover image's URL 404s, the inline onerror handler tags it with
   .cover-broken and strips src. This rule swaps in the musical-note glyph
   (same as the empty placeholder) so users see a clean fallback rather than
   the browser's broken-image alt-text rendering. */
.cover-main.cover-broken,
.cover-back.cover-broken {
    /* Pseudo content can't replace an <img>'s appearance directly — instead
       we hide the broken-img chrome and overlay the glyph via background. */
    object-fit: contain;
    background: var(--bg-card)
        url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><text x='50%' y='62%' text-anchor='middle' fill='%238a87a0' font-size='12'>%E2%99%AB</text></svg>")
        center / 32% no-repeat;
}
.cover-main.cover-broken { font-size: 0; }
.cover-back.cover-broken { font-size: 0; }

.cover-back {
    width: 92px;
    height: 92px;
    border-radius: 5px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    color: var(--text-dim);
    margin-top: 24px;
    box-shadow: 0 4px 12px rgba(0,0,0,0.4);
    object-fit: cover;
}

/* Hero covers that open the art lightbox on click. Applied to .cover-main and
   .cover-back when the Album has gallery-renderable art. */
.art-clickable { cursor: pointer; transition: opacity 0.15s; }
.art-clickable:hover { opacity: 0.9; }

/* `flex: 1 1 320px` (vs the previous `flex: 1; min-width: 0`) gives info a "wants"
   width of 320 rather than the default basis of 0. Without a basis info absorbed all
   slack and the .hero-collection panel never shrunk — it stayed at 460 right up
   until the container query stacked. With basis 320 + min-width 280, info AND the
   panel shrink together as the pane narrows, then both stack below ~900px container. */
.hero-info { flex: 1 1 320px; min-width: 280px; padding-top: 2px; }
.hero-artist { font-size: 12px; font-weight: 600; color: var(--accent); letter-spacing: 0.05em; text-transform: uppercase; margin-bottom: 4px; }
.hero-artist-link {
    color: inherit;
    text-decoration: none;
    transition: color 0.12s;
}
.hero-artist-link:hover {
    color: var(--accent-light);
    text-decoration: underline;
    text-underline-offset: 3px;
}
.hero-artist-link span { color: var(--accent); }
.hero-title { font-size: 28px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.3px; line-height: 1.2; margin-bottom: 12px; }

.hero-badges { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }

.badge {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 10px;
    border-radius: 5px;
    font-size: 12px;
    font-weight: 500;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    color: var(--text-muted);
}

.badge.owned { background: var(--owned-bg); border-color: var(--owned-border); color: var(--owned); }
.hero-catalog { font-size: 13px; color: var(--text-dim); margin-top: 4px; }

/* Combined release/catalogue/price line — items separated by · so they share
   one line when there's room and wrap gracefully on narrow widths. */
.hero-catalog-row {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: baseline;
}

.hero-catalog-sep { color: var(--text-faint, var(--text-dim)); opacity: 0.6; }

/* Playback pills sitting in the hero (moved from the tracklist toolbar). */
.hero-playback-pills {
    margin-top: 10px;
}

/* ─── Detail Tab Bar ─────────────────────────────────────────────────────── */
.detail-tabs {
    display: flex;
    align-items: center;
    border-bottom: 1px solid var(--border);
    padding: 0 20px;
    flex-shrink: 0;
    gap: 0;
    background: var(--bg-base);
}

.detail-tab {
    padding: 14px 18px 13px;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-muted);
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    transition: color 0.15s, border-color 0.15s;
    background: none;
    border-top: none;
    border-left: none;
    border-right: none;
    cursor: pointer;
    min-height: var(--min-touch);
}

.detail-tab:hover { color: var(--text-primary); }
.detail-tab.active { color: var(--accent-light); border-bottom-color: var(--accent); }
.detail-tab:disabled { color: var(--text-dim); cursor: not-allowed; }
.detail-tabs .tab-spacer { flex: 1; }

.action-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 9px 14px;
    border-radius: 6px;
    font-size: 13px;
    font-weight: 500;
    color: var(--text-muted);
    border: 1px solid transparent;
    transition: all 0.15s;
    margin-left: 4px;
    cursor: pointer;
    background: none;
    min-height: var(--min-touch);
}

.action-btn:hover { color: var(--text-primary); background: var(--bg-hover); border-color: var(--border); }
.action-btn.primary { color: var(--accent-light); border-color: var(--accent-border); background: var(--accent-bg); }
.action-btn.primary:hover { background: rgba(139,132,232,0.18); }
.action-btn.danger { color: var(--danger); }
.action-btn.danger:hover { background: var(--danger-bg); border-color: var(--danger-border); }
.action-btn:disabled { opacity: 0.4; cursor: not-allowed; pointer-events: none; }

/* Icon-only variant — square footprint with a larger glyph. The default action-btn
   was tuned for text labels and looks small/empty when only an icon is shown (e.g. the
   artist hero's Refresh Artist button after the icon-only redesign). The 44×44 box and
   20px glyph meet the touch-target minimum and match the visual weight of the text variants. */
.action-btn-icon {
    width: var(--min-touch);
    height: var(--min-touch);
    padding: 0;
    justify-content: center;
    border: 1px solid var(--border-med);
    font-size: 20px;
    line-height: 1;
}

/* ─── Detail Content ─────────────────────────────────────────────────────── */
.detail-content {
    flex: 1;
    overflow-y: auto;
    /* padding-top is intentionally 0: Chromium clips overflow at the padding
       box (outer padding edge) but anchors position:sticky top:0 to the
       content box (inner padding edge). A non-zero padding-top creates a
       band that is inside the scrollport but above the sticky-header anchor,
       so scrolled rows show through it above the column header. The hero's
       padding-bottom already provides visual breathing room above us. */
    padding: 0 20px 24px;
}

/* ─── Disc Pills ─────────────────────────────────────────────────────────── */
.disc-pills { display: flex; gap: 6px; margin-bottom: 14px; flex-wrap: wrap; }

.disc-pill {
    padding: 3px 12px;
    border-radius: 12px;
    font-size: 11px;
    font-weight: 500;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    color: var(--text-muted);
    transition: all 0.15s;
    cursor: pointer;
}

.disc-pill.active { background: var(--accent-bg); border-color: var(--accent-border); color: var(--accent-light); }
.disc-pill:hover:not(.active) { background: var(--bg-hover); color: var(--text-secondary); }

/* ─── Album playback-pill rows ───────────────────────────────────────────────
   Container that pairs the disc-pill row (multi-disc) with the right-aligned
   album playback pills (Apple Music / Spotify / Amazon Music). For single-disc
   releases the .tracklist-toolbar variant sits just above the tracklist's
   header row instead. Both end up immediately above the tracklist so the
   playback CTAs are visually adjacent to the tracks they relate to — the prior
   position above the hero art strip was easy to overlook. */
/* Shared height of the sticky disc-pills row. Used both as min-height on the
   row itself AND as the top offset of the sticky tracklist <thead>, so the
   two values can never drift apart. Previously the offset was hardcoded at
   56px while the row's natural box was ~28px, leaving a transparent gap
   below the row through which scrolling track rows became visible above the
   column headers. */
.detail-content { --disc-pills-row-h: 44px; }

.disc-pills-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    /* Stick the disc-pills row to the top of the scrolling .detail-content so
       Disc 1 / Disc 2 stay visible while the user scrolls a long tracklist.
       Stacks above the tracklist <thead> (which sticks below it via the
       :has() rule on .detail-content). */
    position: sticky;
    top: 0;
    background: var(--bg-base);
    z-index: 3;
    /* Padding (not margin) so the bottom gap is part of the sticky box's
       border-box — scrolling content can't appear in the padding area
       because the background paints over it. A margin would leave the same
       gap transparent. */
    padding: 4px 0 12px;
    box-sizing: border-box;
    min-height: var(--disc-pills-row-h);
}
/* When the disc-pills land inside .disc-pills-row the row owns the bottom
   margin; nullify the inner one to avoid stacking gaps. */
.disc-pills-row .disc-pills { margin-bottom: 0; }
/* Push the sticky tracklist <thead> below the sticky disc-pills row so
   they stack rather than overlap. Single-disc (no .disc-pills-row) keeps
   the original top:0 from the .tracklist th rule below. */
.detail-content:has(.disc-pills-row) .tracklist th {
    top: var(--disc-pills-row-h);
}

.tracklist-toolbar {
    display: flex;
    justify-content: flex-end;
    margin-bottom: 8px;
}

.album-playback-pills {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    align-items: center;
}

/* ─── Track List ─────────────────────────────────────────────────────────── */
.tracklist { width: 100%; border-collapse: collapse; }

/* Sticky column headers — keep "# / TITLE / TIME" pinned as the user scrolls
   through long tracklists, so the columns stay readable. The .detail-content
   container scrolls independently of the page; sticky position resolves
   relative to that. */
.tracklist th {
    text-align: left;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
    padding: 10px 10px;
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    background: var(--bg-base);
    z-index: 2;
}
.tracklist th.track-dur { text-align: right; }

.tracklist td { padding: 9px 10px; border-bottom: 1px solid rgba(255,255,255,0.03); }
.tracklist tr:hover td { background: var(--bg-hover); }
.track-num { color: var(--text-dim); font-size: 12px; width: 32px; text-align: right; padding-right: 12px; }
.track-title { color: var(--text-secondary); font-size: 14px; }
.track-artist { font-size: 12px; color: var(--text-dim); }
.track-dur { color: var(--text-dim); font-size: 13px; text-align: right; }
.track-writers { font-size: 12px; color: var(--text-dim); font-style: italic; margin-top: 2px; }
/* Total row (<tfoot>) — right-aligned inside the track-dur column so the
   aggregate sits directly under the per-track times.  No border (it's the
   last row), slightly taller top padding for visual separation from the
   final track row. */
.tracklist tfoot .track-total-row td { border-bottom: none; padding-top: 10px; }
.tracklist tfoot .track-total-row .track-dur { font-size: 12px; color: var(--text-dim); }

/* ─── Notes Section ──────────────────────────────────────────────────────── */
.notes-section { margin-top: 18px; border-top: 1px solid var(--border); }

.notes-section > summary {
    list-style: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 0 10px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
    cursor: pointer;
    user-select: none;
    min-height: 38px;
}

.notes-section > summary::-webkit-details-marker { display: none; }
.notes-section > summary::before { content: '›'; font-size: 16px; transition: transform 0.2s; display: inline-block; }
details[open].notes-section > summary::before { transform: rotate(90deg); }
.notes-body { font-size: 14px; color: var(--text-secondary); line-height: 1.6; padding-bottom: 14px; }

/* ─── Search Page ────────────────────────────────────────────────────────── */
.search-inputs {
    padding: 14px 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.search-field {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 12px;
    background: var(--bg-input);
    border-radius: 6px;
    border: 1px solid var(--border);
    min-height: 40px;
}

.search-field input {
    flex: 1;
    background: none;
    border: none;
    outline: none;
    font: inherit;
    color: var(--text-secondary);
    font-size: 14px;
}

.search-field input::placeholder { color: var(--text-dim); }
.search-field label { font-size: 12px; color: var(--text-dim); flex-shrink: 0; width: 52px; }

.search-btn {
    padding: 10px 18px;
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    border-radius: 6px;
    color: var(--accent-light);
    font-size: 14px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s;
    align-self: flex-end;
    min-height: var(--min-touch);
}

.search-btn:hover { background: rgba(139,132,232,0.18); }

.search-filters {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 14px;
    border-bottom: 1px solid var(--border);
    flex-wrap: wrap;
}
.search-filter-select {
    font-size: 13px;
    padding: 6px 28px 6px 10px;
    min-height: var(--min-touch);
    /* color-scheme: dark hints to the browser to render the OS-native option
       list (the dropdown panel) in dark mode — matches the surrounding UI on
       Chrome/Edge/Safari. Without this hint native <select> options inherit
       the page's white-on-dark inputs but render the dropdown panel in OS
       light mode (white background, near-white text → unreadable). */
    color-scheme: dark;
    background-color: var(--bg-elev, var(--bg-card, #2a2748));
    color: var(--text, #e8e6f5);
    border: 1px solid var(--border-strong, var(--border));
    border-radius: 6px;
    flex: 1 1 140px;
    max-width: 220px;
    appearance: none;
    -webkit-appearance: none;
    /* Custom chevron. SVG fill matches --text-dim. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8' fill='none' stroke='%238a86b0' stroke-width='1.6'%3E%3Cpath d='M1 1l5 5 5-5'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 10px 7px;
    cursor: pointer;
}
.search-filter-select:focus {
    outline: 1px solid var(--accent);
    outline-offset: 1px;
    border-color: var(--accent);
}
/* For browsers that don't honour color-scheme on options, give the options
   themselves an explicit dark surface — only Firefox respects this fully,
   but Chrome/Safari at least won't render unreadable light-on-light. */
.search-filter-select option {
    background-color: var(--bg-elev, #1a1830);
    color: var(--text);
}

/* ─── Discography Artist Picker ──────────────────────────────────────────── */
.discog-artist-picker { position: relative; flex: 1; }
.discog-artist-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    z-index: 100;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.4);
    max-height: 320px;
    overflow-y: auto;
}

/* ─── Artist Detail Hero ─────────────────────────────────────────────────── */
.artist-hero {
    padding: 24px 24px 16px;
    display: flex;
    gap: 20px;
    align-items: flex-start;
    flex-shrink: 0;
    background: linear-gradient(180deg, rgba(139,132,232,0.06) 0%, transparent 100%);
    border-bottom: 1px solid var(--border);
}

.artist-photo {
    width: 96px;
    height: 96px;
    border-radius: 8px;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    box-shadow: 0 6px 20px rgba(0,0,0,0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 26px;
    color: var(--text-dim);
    overflow: hidden;
    flex-shrink: 0;
    object-fit: cover;
}

.artist-hero-info { flex: 1; min-width: 0; }
.artist-hero-name { font-size: 20px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.3px; margin-bottom: 6px; }
.artist-hero-meta { font-size: 12px; color: var(--text-dim); margin-bottom: 8px; }

/* ─── Mobile Pane Navigation ────────────────────────────────────────────── */
.mobile-pane-nav { display: none; }
.mobile-back {
    font-size: 12px;
    color: var(--accent-light);
    padding: 4px 0;
    display: flex;
    align-items: center;
    gap: 4px;
    background: none;
    border: none;
    cursor: pointer;
}

/* ─── Responsive (≤767px) ────────────────────────────────────────────────── */
@media (max-width: 767px) {
    /* Desktop topnav is hidden on mobile — replaced by the dedicated
       .mobile-header (top, logo + avatar) and .mobile-footer (bottom, six items
       + More drawer). */
    .topnav { display: none; }

    .mobile-header {
        display: flex;
        align-items: center;
        justify-content: space-between;
        height: var(--mobile-header-height);
        padding: 0 14px;
        background: var(--bg-nav);
        border-bottom: 1px solid var(--border);
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 200;
        gap: 8px;
    }
    /* Right-side cluster: Help + avatar dropdown. Sits flush against the right
       edge so the logo gets the left edge and there's no awkward gap pushing
       Help into the middle of the bar. */
    .mobile-header-actions {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-left: auto;
    }
    .mobile-header-logo {
        background: none;
        border: 0;
        padding: 0;
        display: flex;
        align-items: center;
        cursor: pointer;
    }
    .mobile-header-logo img { height: 34px; width: auto; }

    /* Matches the pre-PR .topnav mobile layout: align-items: center keeps the
       active-state pill sized to the button's content (text + padding), not
       stretched to the full footer height. */
    .mobile-footer {
        display: flex;
        align-items: center;
        justify-content: space-around;
        height: var(--mobile-footer-height);
        background: var(--bg-nav);
        border-top: 1px solid var(--border);
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        z-index: 200;
        padding: 0 4px;
        padding-bottom: env(safe-area-inset-bottom, 0px);
    }
    .mobile-footer .nav-btn {
        flex: 1;
        text-align: center;
        font-size: 12px;
        padding: 8px 4px;
        min-height: var(--min-touch);
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 2px;
        line-height: 1.1;
    }
    /* Icon glyph above text label on mobile bottom nav (Phase 7j review item 44).
       Icons stay subtle — text remains the primary affordance. */
    .mobile-footer .nav-btn .nav-icon {
        font-size: 16px;
        line-height: 1;
    }

    /* Popup-menu styling matches .user-menu-panel: a small floating panel
       anchored above the More button (rightmost item in the footer), rounded
       on all corners with a drop shadow. Same affordance as the avatar
       dropdown so the interaction is familiar. */
    .more-drawer {
        display: block;
        position: fixed;
        bottom: calc(var(--mobile-footer-height) + 6px);
        right: 6px;
        min-width: 180px;
        background: var(--bg-panel);
        border: 1px solid var(--border);
        border-radius: 8px;
        box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
        padding: 6px;
        z-index: 260;
        opacity: 0;
        transform: translateY(6px);
        pointer-events: none;
        transition: opacity 0.15s ease-out, transform 0.15s ease-out;
    }
    .more-drawer.open {
        opacity: 1;
        transform: translateY(0);
        pointer-events: auto;
    }
    /* Transparent backdrop — its only job is tap-outside-to-dismiss. Visible
       backdrop dimming would be heavier than the popup itself warrants. */
    .more-drawer-backdrop {
        display: block;
        position: fixed;
        inset: 0;
        background: transparent;
        z-index: 250;
    }
    .more-drawer-item {
        display: flex;
        align-items: center;
        width: 100%;
        text-align: left;
        background: none;
        border: 0;
        padding: 12px 14px;
        font-size: 14px;
        color: var(--text-muted);
        border-radius: 6px;
        cursor: pointer;
        transition: background 0.1s, color 0.15s;
        min-height: var(--min-touch);
    }
    .more-drawer-item:hover,
    .more-drawer-item:active {
        background: var(--bg-hover);
        color: var(--text-primary);
    }
    /* Visual divider between the navigation items (Discography / Search) and the
       legal links (Privacy / Terms / Legal) in the More drawer. Light hairline,
       no extra vertical weight. */
    .more-drawer-sep {
        border: none;
        border-top: 1px solid var(--border);
        margin: 4px 10px;
    }

    /* App body sits between the mobile header (top) and footer (bottom). */
    .app-body {
        margin-top: var(--mobile-header-height);
        height: calc(100svh - var(--mobile-header-height) - var(--mobile-footer-height));
    }

    /* Mobile header avatar dropdown opens DOWNWARD (header is at the top of the
       viewport). Pinned right so the panel stays on-screen. Overrides the
       existing .user-menu mobile rules below in this file (which assume the
       user-menu sits in a fixed-bottom bar). */
    .mobile-header .user-menu {
        flex: 0 0 auto;
        margin-right: 0;
    }
    .mobile-header .user-menu-toggle {
        flex: 0 0 auto;
        padding: 4px;
    }
    .mobile-header .user-menu-panel {
        top: calc(100% + 4px);
        bottom: auto;
        right: 0;
        left: auto;
    }
    /* Header has limited horizontal space — show only the avatar disc. */
    .mobile-header .user-menu-name,
    .mobile-header .user-menu-caret { display: none; }

    /* Pane layout */
    .view-3pane, .view-2pane { flex-direction: column; overflow: hidden; }
    .pane-sidebar { width: 100%; height: 100%; border-right: none; }
    .pane-mid { width: 100%; height: 100%; border-right: none; }
    .pane-detail, .pane-detail-wide { width: 100%; height: 100%; }

    /* Hide inactive panes */
    .mobile-hidden { display: none !important; }

    /* Mobile back button in pane headers */
    .mobile-pane-nav {
        display: flex;
        align-items: center;
        padding: 8px 14px;
        border-bottom: 1px solid var(--border);
        flex-shrink: 0;
        background: var(--bg-panel);
    }

    /* Album-detail pane: scroll the whole pane on mobile rather than just the
       tracklist inside .detail-content. The desktop layout fixes .detail-hero at
       its natural height and gives .detail-content (the tracklist) the remainder
       of a flex-column with overflow:auto — on a phone the hero eats most of the
       viewport so the tracklist shrinks to ~4 lines. Mirroring the artist-page
       pattern (see line 2317): outer pane scrolls, inner sections become
       auto-height.

       .detail-tabs gets position:sticky so the Refresh / Remove actions stay
       reachable while the user scrolls down through the hero into the tracklist.
       z-index keeps it above the gradient hero background. */
    .pane-detail, .pane-detail-wide {
        overflow-y: auto;
        overflow-x: hidden;
    }
    .detail-content {
        flex: none;
        overflow-y: visible;
    }
    .detail-tabs {
        position: sticky;
        top: 0;
        z-index: 5;
    }
}

/* ─── Artist Detail Layout ───────────────────────────────────────────────── */
.artist-detail-body {
    display: flex;
    flex: 1;
    overflow: hidden;
    min-height: 0;
}

.artist-bio {
    flex: 2;
    min-width: 0;
    padding: 16px 20px 24px;
    overflow-y: auto;
    border-right: 1px solid var(--border);
}

.artist-profile-text {
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.7;
    margin: 0;
}

.artist-discog {
    flex: 1;
    min-width: 220px;
    overflow-y: auto;
    background: var(--bg-panel);
    display: flex;
    flex-direction: column;
}

.discog-header {
    padding: 12px 16px 10px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
    border-bottom: 1px solid var(--border);
    position: sticky;
    top: 0;
    background: var(--bg-panel);
    z-index: 1;
}

.discog-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 0;
    cursor: pointer;
    transition: background 0.12s;
    border-radius: 4px;
}

.discog-item:hover { background: var(--bg-hover); }
.discog-item.active { background: var(--bg-active); }

/* Vertical timeline container */
.discog-timeline {
    position: relative;
    padding: 8px 14px;
    overflow-y: auto;
    flex: 1;
}

/* Connecting line through the dot column */
/* left = container-padding(14) + year-width(38) + gap(8) + dot-half(4) = 64px */
.discog-timeline::before {
    content: '';
    position: absolute;
    left: 64px;
    top: 8px;
    bottom: 8px;
    width: 1px;
    background: var(--border);
    pointer-events: none;
}

.discog-year {
    width: 44px;
    flex-shrink: 0;
    text-align: right;
    font-size: 12px;
    color: var(--text-dim);
    font-weight: 600;
    line-height: 1;
}

.discog-dot {
    width: 10px;
    height: 10px;
    flex-shrink: 0;
    border-radius: 50%;
    background: var(--bg-panel);
    border: 2px solid var(--border-med);
    position: relative;
    z-index: 1;
}

.discog-dot.owned {
    background: var(--owned);
    border-color: var(--owned);
}

.discog-thumb {
    width: 48px;
    height: 48px;
    border-radius: 5px;
    object-fit: cover;
    background: var(--bg-card);
    border: 1px solid var(--border);
    flex-shrink: 0;
}

.discog-thumb-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    color: var(--text-dim);
}

.discog-info { flex: 1; min-width: 0; }
.discog-title { font-size: 14px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.discog-title.not-owned { color: var(--text-muted); font-style: italic; }
.discog-meta { font-size: 12px; color: var(--text-dim); margin-top: 3px; }

/* Shared search icon (SearchIcon.razor). em-sized so it tracks each slot's font-size; vertically
   centred against adjacent text. Colour comes from the slot via currentColor. */
.search-icon { width: 1em; height: 1em; display: inline-block; vertical-align: middle; flex-shrink: 0; }

/* Unowned master row on the Artist-page discography — an <a> to a pre-filled Search rather
   than an expandable owned row. Reset the anchor's default link styling so it reads as a row. */
.discog-item-unowned { text-decoration: none; color: inherit; }
/* Magnifying-glass + chevron: "go look this up" (deliberately NOT a bare chevron, which would
   read as "open this release"). Muted until row hover. */
.discog-search-affordance {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 1px;
    color: var(--text-dim);
    font-size: 13px;
    padding-right: 4px;
    transition: color 0.12s;
}
.discog-item-unowned:hover .discog-search-affordance { color: var(--accent-light); }
.discog-search-chevron { font-size: 15px; line-height: 1; }

/* Footer note while the masters list is materialising in the background (cold miss). The owned
   rows render above it; this just flags that the unowned fill is still on its way. */
.discog-building-note {
    font-size: 12px;
    color: var(--text-dim);
    font-style: italic;
    padding: 8px 14px;
}

/* "Reissue YYYY" marker on an owned row whose pressing post-dates the original release. Neutral
   outline (informational) — deliberately NOT the green Owned pill, which carries ownership meaning. */
.reissue-pill {
    display: inline-block;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.02em;
    color: var(--text-dim);
    border: 1px solid var(--border-med);
    padding: 1px 7px;
    border-radius: 4px;
    margin-left: 8px;
    vertical-align: middle;
}

/* Links tab */
.links-list { list-style: none; padding: 0; margin: 0; }
.links-list li { padding: 12px 0; border-bottom: 1px solid var(--border); min-height: var(--min-touch); display: flex; align-items: center; }
.links-list a { color: var(--accent-light); font-size: 14px; word-break: break-all; }

/* Mobile: stack bio and discog */
@media (max-width: 767px) {
    .artist-detail-body { flex-direction: column; overflow-y: auto; }
    .artist-bio { border-right: none; border-bottom: 1px solid var(--border); overflow-y: visible; flex: 1; }
    .artist-discog { min-width: unset; flex: 1; }
}

/* ─── Blazor Error UI ────────────────────────────────────────────────────── */
#blazor-error-ui {
    background: #3a1010;
    border-top: 1px solid var(--danger-border);
    color: var(--text-secondary);
    bottom: 0;
    box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.4);
    display: none;
    left: 0;
    padding: 0.6rem 1.25rem 0.7rem 1.25rem;
    position: fixed;
    width: 100%;
    z-index: 1000;
}

#blazor-error-ui .dismiss {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbmsiPjwvc3ZnPg==) no-repeat 1rem/1.8rem, var(--danger-bg);
    padding: 1rem 1rem 1rem 3.7rem;
    color: var(--text-primary);
    border: 1px solid var(--danger-border);
    border-radius: 6px;
}

.blazor-error-boundary::after { content: "An error has occurred."; }

/* ─── Loading Progress ───────────────────────────────────────────────────── */
.loading-progress {
    position: relative;
    display: block;
    width: 8rem;
    height: 8rem;
    margin: 20vh auto 1rem auto;
}

.loading-progress circle {
    fill: none;
    stroke: var(--border-med);
    stroke-width: 0.6rem;
    transform-origin: 50% 50%;
    transform: rotate(-90deg);
}

.loading-progress circle:last-child {
    stroke: var(--accent);
    stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
    transition: stroke-dasharray 0.05s ease-in-out;
}

.loading-progress-text {
    position: absolute;
    text-align: center;
    font-weight: bold;
    inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
    color: var(--text-secondary);
}

.loading-progress-text:after { content: var(--blazor-load-percentage-text, "Loading"); }

/* ─── Activity pill ──────────────────────────────────────────────────────── */
/* Replaces the old Syncfusion SfToast: a single surface that shows spinner +
   message during a user-initiated operation, then morphs in place to success
   or error. See Components/Shared/ActivityStack + ActivityPill. */
.activity-stack {
    position: fixed;
    right: 20px;
    bottom: 20px;
    display: flex;
    flex-direction: column-reverse;
    gap: 8px;
    z-index: 1000;
    pointer-events: none;
}

.activity-pill {
    pointer-events: auto;
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 240px;
    max-width: 360px;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    border-radius: 999px;
    padding: 8px 14px 8px 10px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.35), 0 2px 6px rgba(0,0,0,0.25);
    font-size: 13px;
    color: var(--text-primary);
    transform: translateY(8px);
    opacity: 0;
    animation: activity-pill-in 180ms ease-out forwards;
    transition: opacity 240ms ease, transform 240ms ease, border-color 240ms ease;
}
@keyframes activity-pill-in {
    to { transform: translateY(0); opacity: 1; }
}
.activity-pill.state-success { border-color: var(--owned-border); }
.activity-pill.state-error   { border-color: var(--danger-border); }

.activity-pill .activity-icon {
    width: 22px; height: 22px;
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    position: relative;
}

.activity-pill .activity-icon .spinner {
    width: 16px; height: 16px;
    border: 2px solid var(--border-med);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: activity-spin 700ms linear infinite;
}
@keyframes activity-spin { to { transform: rotate(360deg); } }

.activity-pill .activity-icon svg {
    width: 18px; height: 18px;
    opacity: 0;
    transform: scale(0.6);
    transition: opacity 180ms ease, transform 220ms cubic-bezier(.2,.9,.3,1.2);
    position: absolute;
}
.activity-pill.state-success .activity-icon svg.check { opacity: 1; transform: scale(1); color: var(--owned); }
.activity-pill.state-error   .activity-icon svg.cross { opacity: 1; transform: scale(1); color: var(--danger); }

.activity-pill.state-success .activity-icon .spinner,
.activity-pill.state-error   .activity-icon .spinner {
    opacity: 0;
    transition: opacity 140ms ease;
    border-top-color: transparent;
    border-color: transparent;
    animation: none;
}

.activity-pill .activity-label { flex: 1; min-width: 0; line-height: 1.35; }
.activity-pill .activity-label .title { color: var(--text-primary); font-weight: 500; }
.activity-pill .activity-label .title em { font-style: italic; color: var(--text-primary); margin-left: 0.35em; }
.activity-pill .activity-label .detail { color: var(--text-muted); font-size: 12px; margin-top: 1px; }
.activity-pill.state-error .activity-label .title { color: #e8a090; }

.activity-pill .activity-close {
    background: none; border: 0; color: var(--text-muted); cursor: pointer; padding: 4px;
    border-radius: 4px; display: flex; align-items: center; justify-content: center;
    opacity: 0;
    transition: opacity 180ms ease, background 120ms ease, color 120ms ease;
}
.activity-pill:hover .activity-close,
.activity-pill.state-error .activity-close { opacity: 1; }
.activity-pill .activity-close:hover { background: var(--bg-hover); color: var(--text-primary); }

@media (max-width: 600px) {
    .activity-stack { left: 12px; right: 12px; bottom: 12px; }
    .activity-pill { max-width: none; min-width: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .activity-pill { animation: none; transform: none; opacity: 1; }
    .activity-pill .activity-icon .spinner { animation-duration: 1400ms; }
}

/* Form validation */
.valid.modified:not([type=checkbox]) { outline: 1px solid var(--owned); }
.invalid { outline: 1px solid var(--danger); }
.validation-message { color: var(--danger); }

/* ─── Concert Images ─────────────────────────────────────────────────────── */
.concert-image-strip { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; }
.concert-image-thumb { width: 52px; height: 52px; border-radius: 5px; object-fit: cover; border: 1px solid var(--border); }
/* Concert-list row thumbnail. Always rendered (cover image → artist thumb
   → musical-note filler fallback) so date-badge columns line up across
   the whole list. Wrapper cell uses the parent grid to keep the thumb at
   full row height; the thumb itself is 44x44 square with rounded corners. */
.concert-row-thumb-cell {
    display: flex;
    align-items: center;
    justify-content: center;
}
.concert-row-thumb {
    width: 44px;
    height: 44px;
    border-radius: 6px;
    object-fit: cover;
    border: 1px solid var(--border);
    flex-shrink: 0;
}
.concert-row-thumb--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-dim, #9aa);
    font-size: 20px;
    background: rgba(255, 255, 255, 0.04);
}
.concert-edit-image-wrap { position: relative; display: inline-block; }

.concert-image-remove {
    position: absolute;
    top: -6px;
    right: -6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--danger);
    border: none;
    color: white;
    font-size: 11px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 0;
    line-height: 1;
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
}

.concert-image-remove:hover, .concert-image-remove:focus { opacity: 0.85; outline: none; }

.concert-image-thumb--clickable { cursor: pointer; transition: opacity 0.15s; }
.concert-image-thumb--clickable:hover { opacity: 0.8; }

.concert-gallery-overlay {
    position: fixed; inset: 0; z-index: 1000;
    background: rgba(0,0,0,0.85);
    display: flex; align-items: center; justify-content: center;
}
.concert-gallery-content {
    position: relative; display: flex; flex-direction: column; align-items: center;
    max-width: min(90vw, 900px); max-height: 90vh;
}
.concert-gallery-img {
    max-width: 100%; max-height: calc(90vh - 56px);
    border-radius: 6px; object-fit: contain; display: block;
}
.concert-gallery-close {
    position: absolute; top: -36px; right: 0;
    background: none; border: none; color: white; font-size: 22px;
    cursor: pointer; line-height: 1; padding: 4px 8px; opacity: 0.8;
}
.concert-gallery-close:hover { opacity: 1; }
.concert-gallery-nav {
    display: flex; align-items: center; gap: 16px; margin-top: 12px; color: white;
}
.concert-gallery-arrow {
    background: none; border: 1px solid rgba(255,255,255,0.3); color: white;
    font-size: 28px; width: 40px; height: 40px; border-radius: 50%;
    cursor: pointer; display: flex; align-items: center; justify-content: center;
    line-height: 1; padding: 0; transition: background 0.15s;
}
.concert-gallery-arrow:hover:not(:disabled) { background: rgba(255,255,255,0.15); }
.concert-gallery-arrow:disabled { opacity: 0.3; cursor: default; }
.concert-gallery-counter { font-size: 13px; color: rgba(255,255,255,0.7); min-width: 50px; text-align: center; }

/* ─── Concerts ───────────────────────────────────────────────────────────── */
.concert-panel-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 14px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.new-concert-btn {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 5px 11px;
    border-radius: 6px;
    font-size: 12px;
    font-weight: 600;
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    color: var(--accent-light);
    transition: all 0.15s;
    cursor: pointer;
}

.new-concert-btn:hover { background: rgba(139,132,232,0.2); }

.new-concert-dropdown { position: relative; }
/* Transparent full-screen catcher: any click anywhere closes the dropdown. The menu
   itself sits one z-index higher and stops propagation so clicks on its items don't
   bubble through to the backdrop. */
.dropdown-backdrop {
    position: fixed;
    inset: 0;
    background: transparent;
    z-index: 99;
}
.new-concert-menu {
    position: absolute;
    top: calc(100% + 6px);
    right: 0;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 8px 20px rgba(0,0,0,0.35);
    z-index: 100;
    min-width: 180px;
    padding: 4px;
    display: flex;
    flex-direction: column;
}
.new-concert-menu-item {
    background: none;
    border: none;
    color: var(--text-secondary);
    text-align: left;
    padding: 8px 12px;
    font-size: 13px;
    border-radius: 6px;
    cursor: pointer;
    white-space: nowrap;
}
.new-concert-menu-item:hover { background: rgba(255,255,255,0.06); color: var(--text); }

.year-group-head {
    padding: 12px 16px 6px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
}

.concert-row {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: 12px 16px;
    cursor: pointer;
    transition: background 0.12s;
    border-bottom: 1px solid rgba(255,255,255,0.03);
    min-height: var(--min-touch);
}

.concert-row:hover { background: var(--bg-hover); }
.concert-row.active { background: var(--bg-active); border-left: 2px solid var(--accent); padding-left: 14px; }
/* Linked-group bracket (UX review item 19). The accent bar runs the height of
   each row in the group; first / last row of a group get rounded corners and
   a 4px gap to the neighbour, so two adjacent linked groups visually separate
   instead of reading as one big group. The accompanying "(N acts)" tag on the
   first row makes the grouping explicit. */
.concert-row.linked-group-row { border-left: 3px solid var(--accent); margin-left: 4px; padding-left: 13px; }
.concert-row.linked-group-row.active { padding-left: 11px; }
.concert-row.linked-group-first {
    margin-top: 4px;
    border-top-left-radius: 4px;
}
.concert-row.linked-group-last {
    margin-bottom: 4px;
    border-bottom-left-radius: 4px;
}
.linked-group-tag {
    /* `.concert-row` is `flex-direction: column` with default
       `align-items: stretch`, so the pill stretches to row width unless
       align-self overrides. Pin to flex-start so the pill is content-sized. */
    align-self: flex-start;
    display: inline-flex;
    align-items: center;
    margin-top: 4px;
    padding: 1px 6px;
    height: 16px;
    border-radius: 8px;
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--accent-light);
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    flex-shrink: 0;
    line-height: 1;
}

/* Two-column grid: thumb (full row height, centred) | content stack. The
   content stack itself is flex-column so the top line (date + artist +
   tracks) sits above the bottom line (venue + closed badge + setlist.fm
   pill). The grid layout fixes the previous baseline-aligned thumb
   misalignment when only some rows had images. */
.concert-row-grid {
    display: grid;
    grid-template-columns: 44px 1fr;
    gap: 12px;
    align-items: center;
}
.concert-row-content {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}
.concert-row-top {
    display: flex;
    align-items: baseline;
    gap: 10px;
}

.concert-date-badge {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--accent);
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    padding: 3px 8px;
    border-radius: 4px;
    flex-shrink: 0;
}

.concert-artist {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-secondary);
    flex: 1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Track-count chip on concert list rows (review item 32) — flags rich-setlist
   concerts at a glance. Subtle muted treatment so it doesn't compete with the
   date/artist info. */
.concert-track-count {
    flex-shrink: 0;
    font-size: 11px;
    color: var(--text-dim);
    background: var(--bg-card);
    border: 1px solid var(--border);
    padding: 2px 8px;
    border-radius: 10px;
    white-space: nowrap;
}

.concert-venue {
    font-size: 12px;
    color: var(--text-muted);
}

/* Concert detail hero */
.concert-hero {
    padding: 28px 28px 18px;
    background: linear-gradient(135deg, rgba(80,50,180,0.15) 0%, transparent 60%);
    flex-shrink: 0;
}

.concert-hero-date {
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent);
    margin-bottom: 6px;
}

.concert-hero-title {
    font-size: 28px;
    font-weight: 700;
    color: var(--text-primary);
    letter-spacing: -0.4px;
    margin-bottom: 6px;
    /* Title text + inline setlist.fm pill side-by-side; pill anchors to
       baseline of the artist name so the chip reads as attached to the
       artist row rather than floating. */
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 10px;
}
.concert-hero-title-text {
    display: inline;
}

.concert-hero-venue {
    font-size: 15px;
    color: var(--text-muted);
    margin-bottom: 14px;
}

/* setlist.fm attribution chip — used on the concert hero and inline on the concert list row.
   Mirrors the .discog-expand-tag idiom but with a subtle accent treatment so it reads as a
   link. The .source-pill--inline modifier collapses vertical spacing for use in list rows. */
.source-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    border-radius: 4px;
    font-size: 12px;
    color: var(--text-dim);
    background: var(--bg-panel);
    border: 1px solid var(--border);
    text-decoration: none;
    margin-bottom: 12px;
}

.source-pill:hover {
    color: var(--accent-light);
    border-color: var(--accent-border);
    background: var(--accent-bg);
}

.source-pill--inline {
    margin-bottom: 0;
    margin-left: 8px;
    vertical-align: middle;
}

/* Setlist track that the user owns — wraps the title in a link with a small "Owned" pill
   sitting next to it. Used by both the /concerts setlist table and the inline setlist on
   the artist page concert pane, so the styling lives at the global level rather than
   per-component. The pill colours follow the existing .discog-expand-tag.owned-tag idiom
   so the in-library indicators stay visually consistent across album list and setlist. */
/* Persistent underline (review item 33, Phase 5b): owned setlist tracks need
   a touch-discoverable affordance — hover-only didn't tell tablet/mobile users
   the row was navigable. Subtle dotted under-style on the resting state lifts
   to a solid underline on hover. */
.setlist-track-link {
    color: inherit;
    text-decoration: underline dotted var(--accent);
    text-underline-offset: 3px;
    text-decoration-thickness: 1px;
}

.setlist-track-link:hover {
    color: var(--accent-light);
    text-decoration: underline solid var(--accent);
}

/* P0-8: setlist tracks with no recorded title surface as italic placeholder
   text — they were rendering as empty cells before, which read as broken. */
.setlist-track-empty {
    color: var(--text-dim);
    font-style: italic;
}

/* Review item 36: solos / intros / interludes / etc. render in italic so the
   user can scan past them and focus on songs. */
.setlist-track-element .track-title,
.setlist-track.setlist-track-element {
    font-style: italic;
    color: var(--text-muted);
}

/* Review item 28: ENCORE column dropped in favour of an inline section break
   row. The label spans the full width and reads as a header, not a track. */
.setlist-section-break td {
    padding-top: 16px;
    padding-bottom: 6px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--accent);
    border-bottom: 1px solid var(--accent-border);
    background: transparent;
}

.tracklist-setlist tr:hover td { background: var(--bg-hover); }

.owned-pill {
    display: inline-block;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--owned);
    background: var(--owned-bg);
    border: 1px solid var(--owned-border);
    padding: 2px 7px;
    border-radius: 4px;
    margin-left: 8px;
    vertical-align: middle;
    /* Pills are decorative — keep them out of the link's underline on hover. */
    text-decoration: none;
}

/* "Also that night" — peer concerts at the same venue + date. The chip row sits below
   the artist chips on the concert hero and inside the gig-expand-header on the artist
   page concert pane. Visually distinct from artist-chip (subtle outline, no thumb).
   Pattern I (Phase 5b): chevron suffix flags the chips as interactive — touch users
   can't rely on hover to discover navigation. */
.linked-concerts {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    margin-bottom: 14px;
    font-size: 13px;
}

.linked-concerts-label {
    color: var(--text-dim);
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-right: 4px;
}

.linked-concert-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 12px 4px 4px;
    border-radius: 16px;
    font-size: 13px;
    color: var(--text-secondary);
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    text-decoration: none;
    min-height: 30px;
}

.linked-concert-chip-thumb {
    width: 22px;
    height: 22px;
    border-radius: 50%;
    background: var(--bg-hover);
    border: 1px solid var(--border);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    font-weight: 600;
    color: var(--text-dim);
    flex-shrink: 0;
    text-transform: uppercase;
}

.linked-concert-chip-name { line-height: 1; }

.linked-concert-chip::after {
    content: '›';
    color: var(--accent);
    font-weight: 600;
    margin-left: 2px;
}

.linked-concert-chip:hover {
    color: var(--accent-light);
    border-color: var(--accent-border);
    background: var(--accent-bg);
}

/* Bulk-import "also that night" confirmation panel — sits in the detail pane after the
   user has saved a setlist-imported concert, listing co-billed setlists from setlist.fm
   for the same venue/date. */
.linked-import-panel {
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    height: 100%;
    overflow: auto;
}

.linked-import-title {
    font-size: 18px;
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 4px;
}

.linked-import-sub {
    font-size: 13px;
    color: var(--text-muted);
}

.linked-import-list {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.linked-import-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    border-radius: 4px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    cursor: pointer;
}

.linked-import-row:hover {
    border-color: var(--accent-border);
}

.linked-import-row.selected {
    background: var(--accent-bg);
    border-color: var(--accent-border);
}

.linked-import-info {
    flex: 1;
    min-width: 0;
}

.linked-import-artist {
    font-size: 14px;
    color: var(--text-primary);
    font-weight: 600;
}

.linked-import-meta {
    font-size: 11px;
    color: var(--text-dim);
}

.linked-import-actions {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-top: 8px;
}

/* Artist chips */
.artist-chips { display: flex; flex-wrap: wrap; gap: 6px; }

.artist-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px 6px 6px;
    border-radius: 20px;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    font-size: 13px;
    color: var(--text-secondary);
    text-decoration: none;
    transition: all 0.15s;
}

a.artist-chip:hover {
    background: var(--bg-hover);
    color: var(--accent-light);
    border-color: var(--accent-border);
}

.artist-chip-name { line-height: 1; }
.artist-chip-chevron {
    color: var(--accent);
    font-weight: 600;
    margin-left: 2px;
}

.artist-chip-thumb {
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--bg-hover);
    border: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    color: var(--text-dim);
    overflow: hidden;
    flex-shrink: 0;
}

.artist-chip-remove {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    margin-left: 2px;
    background: rgba(255,255,255,0.06);
    border: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 11px;
    color: var(--text-dim);
    transition: all 0.15s;
    cursor: pointer;
    padding: 0;
    line-height: 1;
}

.artist-chip-remove:hover { background: var(--danger-bg); border-color: var(--danger-border); color: var(--danger); }

.add-artist-chip {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 4px 10px;
    border-radius: 20px;
    background: none;
    border: 1px dashed var(--border-med);
    font-size: 12px;
    color: var(--text-dim);
    transition: all 0.15s;
    cursor: pointer;
}

.add-artist-chip:hover { border-color: var(--accent-border); color: var(--accent-light); background: var(--accent-bg); }

/* Concert detail data grid */
.concert-detail-grid {
    display: grid;
    grid-template-columns: 100px 1fr;
    gap: 10px 16px;
    max-width: 480px;
    padding: 4px 0;
}

.detail-label {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--text-dim);
    font-weight: 600;
    line-height: 2;
}

.detail-value { color: var(--text-secondary); line-height: 2; font-size: 14px; }

/* Concert edit form */
.concert-edit-shell {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}

@media (max-width: 767px) {
    .concert-edit-shell {
        height: auto;
        overflow: visible;
    }
}

.concert-form {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.form-field {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.form-field > label {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--text-dim);
    font-weight: 600;
}

.form-input {
    padding: 10px 14px;
    border-radius: 6px;
    background: var(--bg-input);
    border: 1px solid var(--border-med);
    color: var(--text-primary);
    transition: border-color 0.15s;
    font: inherit;
    font-size: 14px;
    outline: none;
    min-height: var(--min-touch);
}

.form-input:focus { border-color: var(--accent-border); }
.form-input::placeholder { color: var(--text-dim); }
select.form-input { cursor: pointer; }
select.form-input option { background: var(--bg-panel); color: var(--text-primary); }

.form-row { display: flex; gap: 12px; }
.form-row .form-field { flex: 1; }

/* Setlist browse list (setlist.fm results in the add-concert flow) */
.setlist-browse-list {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 6px;
    overflow: hidden;
}

.setlist-browse-row {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 9px 14px;
    cursor: pointer;
    transition: background 0.12s;
    border-bottom: 1px solid var(--border);
}

.setlist-browse-row:last-child { border-bottom: none; }
.setlist-browse-row:hover { background: var(--bg-hover); }

/* Artist picker inline dropdown */
.artist-picker-inline {
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    border-radius: 8px;
    padding: 8px;
    margin-top: 4px;
}

.concert-artist-pick-list {
    max-height: 160px;
    overflow-y: auto;
}

.concert-artist-pick {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 8px;
    cursor: pointer;
    transition: background 0.12s;
    border-radius: 5px;
}

.concert-artist-pick:hover { background: var(--bg-hover); }

/* Delete confirmation overlay */
.delete-confirm-overlay {
    position: absolute;
    inset: 0;
    background: rgba(0,0,0,0.6);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
}

.delete-confirm-box {
    background: var(--bg-panel);
    border: 1px solid var(--border-med);
    border-radius: 8px;
    padding: 20px 24px;
    min-width: 240px;
    box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}

/* ─── Artist Picker Modal ─────────────────────────────────────────────────── */
.modal-overlay {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.65);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 100;
}

.modal-card {
    background: var(--bg-panel);
    border: 1px solid var(--border-med);
    border-radius: 12px;
    width: 500px;
    max-height: 82vh;
    display: flex;
    flex-direction: column;
    box-shadow: 0 16px 48px rgba(0,0,0,0.6);
}

.modal-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 18px 22px;
    border-bottom: 1px solid var(--border);
    font-weight: 600;
    font-size: 16px;
    flex-shrink: 0;
}

.modal-close {
    font-size: 24px;
    color: var(--text-dim);
    padding: 8px;
    transition: color 0.15s;
    line-height: 1;
    min-width: var(--min-touch);
    min-height: var(--min-touch);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 6px;
}

.modal-close:hover { color: var(--text-secondary); background: var(--bg-hover); }

.modal-tabs {
    display: flex;
    padding: 0 22px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.modal-tab {
    padding: 0 16px;
    height: var(--min-touch);
    font-size: 13px;
    color: var(--text-dim);
    border-bottom: 2px solid transparent;
    transition: color 0.15s;
    margin-bottom: -1px;
}

.modal-tab:hover { color: var(--text-secondary); }
.modal-tab.active { color: var(--accent-light); border-bottom-color: var(--accent); }

.modal-body {
    padding: 16px 22px;
    overflow-y: auto;
    flex: 1;
}

.modal-search-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 12px;
    background: var(--bg-input);
    border: 1px solid var(--border-med);
    border-radius: 6px;
    margin-bottom: 12px;
    min-height: 40px;
}

.modal-search-bar input { font-size: 14px; color: var(--text-secondary); flex: 1; }
.modal-search-bar input::placeholder { color: var(--text-dim); }

.picker-row {
    display: flex;
    align-items: center;
    gap: 14px;
    padding: 12px 10px;
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.12s;
    min-height: var(--min-touch);
}

.picker-row:hover { background: var(--bg-hover); }

.picker-avatar {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    flex-shrink: 0;
    background: var(--bg-card);
    border: 1px solid var(--border);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-dim);
    overflow: hidden;
}

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

.picker-info { flex: 1; min-width: 0; }

.picker-name {
    font-size: 14px;
    font-weight: 500;
    color: var(--text-secondary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.picker-meta { font-size: 12px; color: var(--text-dim); margin-top: 2px; }

.picker-select-btn {
    padding: 8px 14px;
    font-size: 13px;
    border-radius: 6px;
    border: 1px solid var(--border-med);
    background: var(--bg-card);
    color: var(--text-secondary);
    transition: all 0.15s;
    white-space: nowrap;
    min-height: var(--min-touch);
}

.picker-select-btn:hover { background: var(--accent-bg); color: var(--accent-light); border-color: var(--accent-border); }

.picker-not-in-library {
    font-size: 11px;
    color: var(--text-dim);
    white-space: nowrap;
    padding: 4px 0;
}

.disambig-note {
    font-size: 11px;
    color: var(--text-dim);
    padding: 6px 8px;
    background: var(--bg-card);
    border-radius: 5px;
    border: 1px solid var(--border);
    margin-bottom: 8px;
}

/* ─── Artists list page (full-width sidebar) ─────────────────────────────── */
.view-1pane {
    display: flex;
    height: 100%;
    overflow: hidden;
    background: var(--bg-base);
}

.pane-sidebar-full {
    width: 100%;
    max-width: 480px;
}

/* ─── Artist Detail Page ─────────────────────────────────────────────────── */
.artist-page-layout {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}

/* Breadcrumb bar — used by ArtistDetailPage today, lifted to other pages in Phase 5f.
   Visually a thin secondary chrome line: 32px tall, but the back button itself
   gets its tap padding so the touch target is still ~40px (back button extends
   above and below the visible bar via padding). */
.artist-page-breadcrumb,
.breadcrumb-bar {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 32px;
    padding: 0 20px;
    flex-shrink: 0;
    background: var(--bg-panel);
    border-bottom: 1px solid var(--border);
}

.breadcrumb-back-btn {
    font-size: 13px;
    color: var(--text-dim);
    cursor: pointer;
    transition: color 0.15s;
    background: none;
    border: none;
    padding: 4px 8px 4px 4px;
    margin: -8px 0;
    display: inline-flex;
    align-items: center;
    line-height: 1;
}

.breadcrumb-back-btn:hover { color: var(--accent-light); }
.breadcrumb-sep { font-size: 13px; color: var(--text-dim); }
.breadcrumb-current { font-size: 13px; font-weight: 600; color: var(--text-secondary); }

/* ─── Artist Hero ────────────────────────────────────────────────────────── */
.artist-page-hero {
    display: flex;
    align-items: flex-start;
    gap: 18px;
    padding: 14px 24px 14px;
    background: linear-gradient(160deg,
        color-mix(in srgb, var(--cover-color, var(--accent)) 28%, var(--bg-panel)) 0%,
        var(--bg-panel) 52%);
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.artist-page-photo {
    width: 96px;
    height: 96px;
    border-radius: 10px;
    flex-shrink: 0;
    object-fit: cover;
    border: 1px solid var(--border-med);
}

.artist-page-photo-initial {
    background: var(--bg-card);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 38px;
    font-weight: 800;
    color: var(--text-muted);
}

.artist-page-hero-content { flex: 1; min-width: 0; }

.artist-page-hero-name {
    font-size: 26px;
    font-weight: 800;
    letter-spacing: -0.4px;
    color: var(--text-primary);
    margin-bottom: 6px;
    line-height: 1.15;
}

/* MusicBrainz genre chips under the artist name. Subtle pill style — not as
   prominent as the album type badges; descriptive tags, not categorical
   classifications. Rendered inline on .hero-tag-stats-line, sharing the row
   with the in-library / concerts counts. */
.hero-tag-stats-line {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    /* Row gap kicks in when the line wraps on narrow widths so the second row
       doesn't kiss the first. Column gap is the inline spacing between chips /
       counts / separator bars. */
    gap: 8px 12px;
    margin-bottom: 10px;
}

.artist-hero-tag-chip {
    font-size: 12px;
    color: var(--text-dim);
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    border-radius: 12px;
    padding: 3px 10px;
    text-transform: capitalize;
    line-height: 1.5;
}

.artist-page-bio {
    font-size: 14px;
    color: var(--text-secondary);
    line-height: 1.6;
    margin-bottom: 8px;
    display: -webkit-box;
    /* Clamped to one line so the hero stays compact at 1280×800 and the
       1366×768 recording target — Show more / Show less toggles the full text. */
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

.artist-page-bio.expanded {
    display: block;
    -webkit-line-clamp: unset;
}

.bio-toggle {
    font-size: 13px;
    color: var(--accent-light);
    cursor: pointer;
    background: none;
    border: none;
    padding: 4px 0;
    margin-bottom: 10px;
    transition: color 0.12s;
}

.bio-toggle:hover { color: var(--text-primary); }

/* Hero meta row — stats and links sit on the same line on wide screens, stack
   on narrow ones. Saves ~30px of hero height at desktop widths.
   Container query on .pane-detail width (since the pane width drives layout
   here, not the viewport). */
.hero-meta-row {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 10px;
}

@container (min-width: 720px) {
    .hero-meta-row {
        flex-direction: row;
        align-items: center;
        gap: 16px;
        flex-wrap: wrap;
    }
    .hero-meta-row .artist-page-hero-stats { margin-bottom: 0; }
    .hero-meta-row .artist-page-hero-links { margin-bottom: 0; }
}

/* Outside a .pane-detail container (e.g. ArtistDetailPage which sits in
   .artist-page-layout), use viewport-width media query as a fallback. */
@media (min-width: 1024px) {
    .artist-page-layout .hero-meta-row {
        flex-direction: row;
        align-items: center;
        gap: 18px;
        flex-wrap: wrap;
    }
    .artist-page-layout .hero-meta-row .artist-page-hero-stats { margin-bottom: 0; }
    .artist-page-layout .hero-meta-row .artist-page-hero-links { margin-bottom: 0; }
}

.artist-page-hero-stats {
    display: flex;
    align-items: center;
    gap: 16px;
    margin-bottom: 12px;
}

.hero-stat { font-size: 14px; color: var(--text-dim); }
.hero-stat strong { color: var(--text-muted); font-weight: 600; font-size: 15px; }
.hero-stat-sep { width: 1px; height: 13px; background: var(--border-med); }

.artist-page-hero-links {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin-bottom: 12px;
}

/* Links dropdown — the single chip that opens a panel of external URLs (Apple
   Music + every live MusicBrainz/Wikipedia/official-site link). Discogs stays
   its own standalone pill alongside this; this dropdown collapses the rest so
   the hero doesn't grow a second row of chips at narrow widths. */
.hero-links-dropdown {
    position: relative;
    display: inline-flex;
}

.hero-links-trigger {
    /* Inherits .hero-link-chip layout; only the caret + spacing differ. */
    gap: 6px;
    cursor: pointer;
    background: none;
    font: inherit;
}

.hero-links-caret {
    font-size: 10px;
    line-height: 1;
    color: var(--text-dim);
}

/* Transparent click-catcher behind the panel so any outside-click closes the
   dropdown. Same pattern as .add-hub-backdrop. Z-index lower than the panel
   but higher than ambient hero content. */
.hero-links-backdrop {
    position: fixed;
    inset: 0;
    background: transparent;
    z-index: 50;
}

.hero-links-panel {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    z-index: 51;
    min-width: 200px;
    max-width: 280px;
    background: var(--bg-panel);
    border: 1px solid var(--border-med);
    border-radius: 10px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
    padding: 6px;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.hero-links-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    border-radius: 6px;
    color: var(--text-secondary);
    text-decoration: none;
    font-size: 13px;
    transition: background 0.12s, color 0.12s;
}

.hero-links-item:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.hero-links-item-icon {
    font-size: 12px;
    color: var(--text-dim);
    width: 16px;
    text-align: center;
}

.hero-link-chip {
    font-size: 13px;
    color: var(--text-dim);
    padding: 6px 12px;
    border-radius: 12px;
    border: 1px solid var(--border-med);
    text-decoration: none;
    transition: all 0.12s;
    display: inline-flex;
    align-items: center;
    min-height: 32px;
}

.hero-link-chip:hover {
    color: var(--accent-light);
    border-color: var(--accent-border);
    background: var(--accent-bg);
}

/* Discogs attribution pill — variant of .hero-link-chip rendered as a <button>
   (rather than an <a href>) so taps on touch devices first reveal a "Data
   provided by Discogs" tooltip before navigating. The wrapper exists only to
   anchor the absolute-positioned tooltip relative to the chip. See
   DiscogsPill.razor for the design rationale. */
.hero-link-chip-wrap {
    position: relative;
    display: inline-flex;
}

.discogs-chip {
    /* Strip default <button> chrome so it visually matches the <a>-based chips
       beside it. font: inherit because <button> picks up the browser UA font
       on some platforms (notably Safari) without it. */
    font: inherit;
    cursor: pointer;
    background: transparent;
    display: inline-flex;
    align-items: center;
}

.discogs-tooltip {
    position: absolute;
    bottom: calc(100% + 8px);
    left: 0;
    z-index: 270;
    min-width: 220px;
    padding: 12px 14px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
    font-size: 13px;
    color: var(--text-muted);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.discogs-tooltip-text {
    color: var(--text-muted);
}

.discogs-tooltip-link {
    color: var(--accent-light);
    text-decoration: none;
    font-weight: 500;
}

.discogs-tooltip-link:hover {
    text-decoration: underline;
}

/* Click-catcher behind the tooltip — same role as .more-drawer-backdrop. Sits
   below the tooltip in z-index so the tooltip remains interactive while taps
   anywhere outside dismiss it. */
.tooltip-backdrop {
    position: fixed;
    inset: 0;
    background: transparent;
    z-index: 265;
}

/* P0-2 (review item 2/3): on mobile the bottom-anchored tooltip overflows above
   the album title, hiding it. Open downward instead, and centre on the viewport
   so a short pill near the right edge doesn't push the tooltip off-screen. */
@media (max-width: 767px) {
    .discogs-tooltip {
        bottom: auto;
        top: calc(100% + 8px);
        left: 50%;
        transform: translateX(-50%);
        max-width: calc(100vw - 32px);
    }
}

/* Single pill replacement for the multi-thumbnail strip — saves ~60-80px of
   hero height at every breakpoint. Click opens the existing lightbox. */
.art-strip-pill {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border-radius: 14px;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    color: var(--text-secondary);
    cursor: pointer;
    font-size: 13px;
    margin-top: 10px;
    transition: all 0.12s;
    line-height: 1;
}

.art-strip-pill:hover {
    background: var(--bg-hover);
    color: var(--accent-light);
    border-color: var(--accent-border);
}

.art-strip-pill-icon { font-size: 14px; }

/* Shared across the artist detail hero and the album detail hero — "view all the art" strip
   that opens the lightbox on click. Class name is legacy; semantics are "hero art strip". */
.artist-page-art-strip {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    margin-top: 10px;
}

.art-strip-thumb {
    width: 60px;
    height: 60px;
    border-radius: 5px;
    object-fit: cover;
    border: 1px solid var(--border);
    cursor: pointer;
    transition: opacity 0.15s, border-color 0.15s;
}

.art-strip-thumb:hover { opacity: 0.8; border-color: var(--border-med); }

.art-strip-more {
    height: 60px;
    padding: 0 14px;
    border-radius: 5px;
    border: 1px dashed var(--border-med);
    font-size: 12px;
    color: var(--text-dim);
    cursor: pointer;
    transition: all 0.15s;
    white-space: nowrap;
}

.art-strip-more:hover { border-color: var(--accent-border); color: var(--accent-light); background: var(--accent-bg); }

.artist-page-hero-actions {
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex-shrink: 0;
    padding-top: 4px;
}

/* ─── Two-column layout ──────────────────────────────────────────────────── */
.artist-page-cols {
    display: flex;
    flex: 1;
    overflow: hidden;
    background: var(--bg-base);
}

.artist-page-col {
    display: flex;
    flex-direction: column;
    overflow: hidden;
    flex: 1;
    border-right: 1px solid var(--border);
    background: var(--bg-base);
}

.artist-page-col:last-child { border-right: none; }

.artist-col-header {
    display: flex;
    align-items: center;
    padding: 9px 16px 9px 20px;
    border-bottom: 1px solid var(--border);
    background: var(--bg-panel);
    flex-shrink: 0;
}

.artist-col-title {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.07em;
    color: var(--text-muted);
    flex: 1;
}

.artist-col-count { font-size: 11px; color: var(--text-dim); margin-right: 10px; }

.artist-col-scroll { flex: 1; overflow-y: auto; }

.col-empty-state {
    padding: 32px 20px;
    text-align: center;
    font-size: 12px;
    color: var(--text-dim);
}

/* ─── Discog list (artist page) ──────────────────────────────────────────── */
.discog-list { padding: 8px 14px; }

.discog-expand {
    display: none;
    padding: 6px 14px 8px 64px;
    background: var(--bg-card);
    border-bottom: 1px solid var(--border);
}

.discog-expand.open { display: block; }

.discog-expand-tags { display: flex; flex-wrap: wrap; gap: 5px; }

.discog-expand-tag {
    font-size: 11px;
    color: var(--text-dim);
    padding: 2px 7px;
    border-radius: 3px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
}

.discog-expand-tag.owned-tag { color: var(--owned); background: var(--owned-bg); border-color: var(--owned-border); }

.discog-expand-loading { font-size: 13px; color: var(--text-dim); padding: 10px 0 4px; }

.discog-expand-disc-label {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-dim);
    padding: 10px 0 6px;
}

.discog-expand-tracklist { padding: 6px 0 8px; }

.discog-expand-track {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 0;
    font-size: 14px;
    color: var(--text-secondary);
}

.discog-expand-track-num { width: 24px; text-align: right; flex-shrink: 0; font-size: 12px; color: var(--text-dim); }
.discog-expand-track-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.discog-expand-track-dur { flex-shrink: 0; font-size: 12px; color: var(--text-dim); }

/* Attention pulse applied to a matched track row when arriving from a concert "Owned"
   link. The animation runs once on the element at the moment it mounts; `forwards` keeps
   the row in the faded-out end state so each row fades on its own timeline regardless of
   any render-pass gap between the discography column and the concerts column. */
.deep-link-flash {
    border-radius: 3px;
    padding: 4px 6px;
    animation: deep-link-flash 3s ease-out forwards;
}

@keyframes deep-link-flash {
    0%   { background: var(--accent); color: #fff; outline: 2px solid var(--accent); }
    80%  { background: var(--accent); color: #fff; outline: 2px solid var(--accent); }
    100% { background: transparent; color: inherit; outline-color: transparent; }
}

/* ─── Art gallery (in drawer) ────────────────────────────────────────────── */
.art-gallery { display: flex; flex-direction: column; gap: 10px; padding: 16px; }
.art-gallery img { width: 100%; border-radius: 6px; border: 1px solid var(--border); display: block; }

/* ─── Concerts column ────────────────────────────────────────────────────────── */
.gig-year-marker {
    padding: 8px 20px 6px;
    font-size: 12px;
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-dim);
    background: var(--bg-base);
    border-bottom: 1px solid var(--border);
}

.gig-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 12px 16px 12px 20px;
    cursor: pointer;
    border-bottom: 1px solid var(--border);
    transition: background 0.12s;
    min-height: var(--min-touch);
}

.gig-row:hover { background: var(--bg-hover); }
.gig-row.active { background: var(--bg-active); }

.gig-date-badge {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--accent);
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    padding: 4px 8px;
    border-radius: 4px;
    flex-shrink: 0;
    white-space: nowrap;
}

.gig-row-info { flex: 1; min-width: 0; }
.gig-venue { font-size: 14px; font-weight: 500; color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gig-row.active .gig-venue { color: var(--text-primary); }
.gig-city { font-size: 12px; color: var(--text-dim); margin-top: 2px; }
.gig-track-count { font-size: 12px; color: var(--text-dim); flex-shrink: 0; }

/* ─── Gig expand panel ───────────────────────────────────────────────────── */
.gig-expand {
    display: none;
    background: var(--bg-card);
    border-bottom: 1px solid var(--border-med);
}

.gig-expand.open { display: block; }

.gig-expand-header {
    display: flex;
    align-items: center;
    padding: 11px 16px 10px 20px;
    border-bottom: 1px solid var(--border);
    gap: 10px;
}

.gig-expand-date { font-size: 16px; font-weight: 700; color: var(--text-primary); }
.gig-expand-venue { font-size: 13px; color: var(--text-muted); margin-top: 3px; }
.gig-expand-actions { display: flex; gap: 8px; margin-left: auto; }

.gig-expand-setlist { padding: 12px 20px 16px; }

.setlist-label {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-dim);
    margin-bottom: 10px;
}

.setlist-track {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
    font-size: 14px;
    color: var(--text-secondary);
}

.setlist-track:last-child { border-bottom: none; }
.setlist-track-num { font-size: 12px; color: var(--text-dim); width: 24px; text-align: right; flex-shrink: 0; }

.encore-section-label {
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-dim);
}

.encore-badge {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
    background: var(--accent-bg);
    padding: 2px 7px;
    border-radius: 3px;
    margin-left: 6px;
}

/* ─── Slide-over drawer ──────────────────────────────────────────────────── */
.drawer-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.45);
    z-index: 200;
    display: none;
    backdrop-filter: blur(1px);
}

.drawer-backdrop.open { display: block; }

.slide-drawer {
    position: fixed;
    top: var(--topnav-height);
    right: 0;
    bottom: 0;
    width: 440px;
    background: var(--bg-panel);
    border-left: 1px solid var(--border-med);
    z-index: 201;
    transform: translateX(100%);
    transition: transform 0.22s cubic-bezier(0.4, 0, 0.2, 1);
    display: flex;
    flex-direction: column;
    box-shadow: -8px 0 32px rgba(0,0,0,0.4);
}

.slide-drawer.open { transform: translateX(0); }

.drawer-header {
    display: flex;
    align-items: center;
    padding: 15px 20px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
    gap: 12px;
}

.drawer-title { font-size: 14px; font-weight: 600; color: var(--text-primary); flex: 1; }
.drawer-subtitle { font-size: 12px; color: var(--text-dim); margin-top: 2px; }

.drawer-close {
    font-size: 16px;
    color: var(--text-dim);
    padding: 4px 6px;
    border-radius: 4px;
    cursor: pointer;
    transition: color 0.12s, background 0.12s;
    background: none;
    border: none;
}

.drawer-close:hover { color: var(--text-primary); background: var(--bg-hover); }

.drawer-body {
    flex: 1;
    overflow-y: auto;
}

/* ─── Help panel (modifier on .slide-drawer) ─────────────────────────────── */
/* Reuses .slide-drawer + .drawer-backdrop for the slide-in animation and
   z-index stack. .help-panel widens the panel slightly to give the 16:9
   demo videos room and switches to a full-viewport modal on mobile. */

.slide-drawer.help-panel {
    width: 480px;
}

.help-panel-scroll {
    /* Comfortable inside padding so videos don't kiss the drawer edges. */
    padding: 16px 20px 24px;
}

.help-panel-section-title {
    font-size: 13px;
    font-weight: 600;
    color: var(--text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin: 0 0 12px;
}

.help-panel-item {
    margin: 0 0 20px;
}

.help-panel-video {
    width: 100%;
    display: block;
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
    aspect-ratio: 16 / 9;
    background: var(--bg-base);
}

.help-panel-caption {
    margin: 8px 0 0;
    font-size: 12px;
    line-height: 1.5;
    color: var(--text-muted);
    text-align: center;
}

.help-panel-divider {
    border: none;
    border-top: 1px solid var(--border);
    margin: 20px 0 16px;
}

.help-panel-secondary-cta {
    display: block;
    width: 100%;
    padding: 10px 14px;
    background: none;
    border: 1px solid var(--border-med);
    border-radius: 6px;
    color: var(--text-primary);
    font-size: 13px;
    cursor: pointer;
    transition: background 0.12s, border-color 0.12s;
}

.help-panel-secondary-cta:hover {
    background: var(--bg-hover);
    border-color: var(--accent);
}

@media (max-width: 767px) {
    .slide-drawer.help-panel {
        width: 100%;
        top: 0;
        /* Full-viewport modal on mobile — explicit height/border/radius overrides
           the generic 85vh bottom-sheet `.slide-drawer` mobile rule below.
           Without these, translateY(100%) only shifts the panel by 85vh and the
           top 15vh of help-panel content stays visible at the bottom of the
           viewport, even when the panel is closed. 100dvh excludes the URL bar
           so the close button stays reachable while it's showing. */
        height: 100dvh;
        border-left: none;
        border-top: none;
        border-radius: 0;
        box-shadow: none;
    }
}

/* ─── Artist Page — Mobile ──────────────────────────────────────────────── */
/* Placed after all artist-page-* desktop rules to win the cascade.        */
@media (max-width: 767px) {
    .artist-page-layout { overflow-y: auto; overflow-x: hidden; }

    /* Compact hero: photo inline with name, hide art strip */
    .artist-page-hero { flex-direction: row; flex-wrap: wrap; gap: 14px; padding: 14px 16px; }
    .artist-page-photo { width: 64px; height: 64px; border-radius: 10px; }
    .artist-page-hero-content { flex: 1; min-width: 0; }
    .artist-page-hero-name { font-size: 22px; margin-bottom: 6px; }
    .artist-hero-tag-chip { font-size: 11px; padding: 3px 9px; }
    /* 2-line clamp on mobile (down from 3) — saves ~22px of hero height; the
       full bio is still one tap away via Show more. */
    .artist-page-bio { -webkit-line-clamp: 1; font-size: 13px; margin-bottom: 6px; line-height: 1.45; }
    .artist-page-hero-stats { gap: 12px; margin-bottom: 8px; font-size: 13px; }
    /* Wrap link pills onto multiple rows on mobile rather than horizontal-scrolling them
       — review item 26 noted the cut-off was undiscoverable. */
    .artist-page-hero-links {
        flex-wrap: wrap;
        gap: 6px;
        margin-bottom: 8px;
    }
    /* Pills smaller on mobile — text + padding ate a 280px content column
       at the previous size (font 13px, padding 6px 12px, min-height 32px),
       which left the wikipedia.org / facebook.com chips too wide to share
       a row. Tighter chip sizing fits two-per-row on a 375px viewport. */
    .artist-page-hero-links .hero-link-chip,
    .artist-page-hero-links .hero-link-chip-wrap .hero-link-chip {
        font-size: 12px;
        padding: 4px 10px;
        min-height: 28px;
    }
    /* P0-4 (review item 4): mobile previously hid the art-strip thumbnails entirely
       and only kept the "View all" button. The thumbnails are the discoverability
       affordance — without them users didn't realise gallery existed. Now they
       scroll horizontally instead. */
    .artist-page-art-strip {
        flex-wrap: nowrap;
        overflow-x: auto;
        -webkit-overflow-scrolling: touch;
        scrollbar-width: none;
        padding-bottom: 4px;
    }
    .artist-page-art-strip::-webkit-scrollbar { display: none; }
    .art-strip-thumb {
        flex-shrink: 0;
        width: 56px;
        height: 56px;
    }
    /* Refresh button anchored to the top-right of the hero on mobile, not wrapped onto a
       row of its own at the bottom (which left a strip of dead space). The hero is
       flex-row with align-items:flex-start; absolute positioning relative to the hero
       keeps the button in the corner regardless of how the wrapped content reflows. */
    .artist-page-hero { position: relative; padding-right: 64px; }
    .artist-page-hero-actions {
        position: absolute;
        top: 10px;
        right: 10px;
        width: auto;
        padding-top: 0;
        flex-direction: row;
        flex-wrap: nowrap;
        gap: 8px;
    }

    /* Columns: flow naturally, no fixed heights */
    .artist-page-cols { flex-direction: column; overflow: visible; flex: none; }
    .artist-page-col { overflow: visible; flex: none; border-right: none; border-bottom: 1px solid var(--border); }
    .artist-col-scroll { flex: none; overflow-y: visible; }

    /* Drawer: bottom sheet */
    .slide-drawer { width: 100%; top: auto; left: 0; bottom: 0; height: 85vh; transform: translateY(100%); border-left: none; border-top: 1px solid var(--border-med); border-radius: 12px 12px 0 0; }
    .slide-drawer.open { transform: translateY(0); }
}

/* Very narrow phones (≤400px) — drop the artist-hero content block below the
   photo so the bio / stats / link-pill row get the full hero width instead of
   the ~210px column left beside the 64px photo. The photo still owns row 1 of
   the wrap-flex hero; content takes row 2. Placed AFTER the 767px block so
   its rule wins on phones that match both queries. */
@media (max-width: 400px) {
    .artist-page-hero-content {
        flex-basis: 100%;
    }
}

/* ─── Barcode Scanner ────────────────────────────────────────────────────── */
.scanner-video-container {
    position: relative;
    flex: none;
    width: 100%;
    height: 260px;
    background: #000;
    border-radius: 8px;
    overflow: hidden;
    margin-bottom: 12px;
}

.scanner-video {
    width: 100%;
    height: 100%;
    display: block;
    object-fit: cover;
}

.scanner-video-placeholder {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: #666;
    font-size: 0.875rem;
}

.scanner-controls {
    display: flex;
    gap: 8px;
    margin-bottom: 8px;
}

.scanner-status {
    font-size: 0.8rem;
    color: var(--text-dim, #888);
    margin-bottom: 8px;
    min-height: 1.2em;
}

.scanner-error { color: var(--color-error, #f56565); }

.scanner-warning {
    font-size: 0.78rem;
    line-height: 1.35;
    color: var(--text-secondary, #c0a060);
    background: rgba(192, 160, 96, 0.1);
    border: 1px solid rgba(192, 160, 96, 0.3);
    border-radius: 5px;
    padding: 6px 8px;
    margin-bottom: 6px;
}

.scanner-barcode-edit {
    display: flex;
    align-items: center;
    gap: 6px;
}

.scanner-barcode-input {
    flex: 1;
    padding: 6px 10px;
    font-size: 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 5px;
    color: var(--text-secondary);
    outline: none;
    font-family: inherit;
}

.scanner-barcode-input::placeholder { color: var(--text-dim); }

/* Targeting reticle — guides user to centre the barcode */
.scanner-reticle {
    position: absolute;
    z-index: 1;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 75%;
    height: 45%;
    border: 2px solid rgba(255, 255, 255, 0.75);
    border-radius: 6px;
    pointer-events: none;
    box-shadow: 0 0 0 1000px rgba(0, 0, 0, 0.25);
}

/* ================================================================== */
/* Phase 5: album hero link chips, album-type badge, track listen strip */
/* ================================================================== */

/* Album hero container for service link chips (Apple Music / Amazon Music).
   Mirrors .artist-page-hero-links but on the album detail hero. */
.hero-link-chips {
    display: flex;
    gap: 6px;
    flex-wrap: wrap;
    margin-top: 10px;
}

/* Service-accent "dot" marker inside chips and listen icons. Muted by default;
   brand colour reveals on hover/focus of the parent chip or icon. Never a fill
   on the whole chip — the chip chrome stays product-accent. */
.chip-dot {
    display: inline-block;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    margin-right: 5px;
    background: var(--text-dim);
    vertical-align: middle;
    transition: background 0.12s;
}
.hero-link-chip:hover .chip-dot,
.listen-icon:hover .chip-dot,
.listen-icon:focus-visible .chip-dot {
    background: var(--dot-color, var(--accent));
}

.dot-apple    { --dot-color: #fa2d48; }  /* Apple Music red/pink */
.dot-spotify  { --dot-color: #1db954; }  /* Spotify green */
.dot-youtube  { --dot-color: #ff0000; }  /* YouTube red */
.dot-amazon   { --dot-color: #ff9900; }  /* Amazon orange (reads better than navy @ 6px on dark theme) */
.dot-discogs  { --dot-color: #ff6600; }  /* Discogs orange (the historic site accent — pure black would vanish on the dark theme) */

/* Album-type pill (EP / Compilation / Live / Soundtrack / etc.).
   Uses product accent; never a service brand colour. */
.badge.album-type {
    background: var(--accent-bg);
    border-color: var(--accent-border);
    color: var(--accent-light);
}

/* Track-title row holds the title + optional type badge inline. */
.track-title-row {
    display: flex;
    align-items: center;
    gap: 8px;
}

/* TrackType badge (Video / Data). Small, muted, product accent — draws attention
   to the exceptional case without competing with the title. */
.track-type-badge {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 2px 6px;
    border-radius: 3px;
    border: 1px solid var(--border);
    color: var(--text-dim);
    line-height: 1.4;
}
.track-type-badge.type-video { color: var(--accent-light); border-color: var(--accent-border); }
.track-type-badge.type-data  { color: var(--text-dim); }

/* Track "listen" column — proto-derived column sitting rightmost on each row.
   padding-left is deliberately tight so the strip sits close to the duration text
   in the neighbouring column on its left (default 8px + 8px produced a 16px
   visual gap that read as "floating far from the track length"). */
.tracklist th.track-listen,
.tracklist td.track-listen {
    width: 1%;
    white-space: nowrap;
    text-align: right;
    padding-left: 4px;
}

/* Tighten the duration column's right padding so its text abuts the listen strip. */
.tracklist th.track-dur,
.tracklist td.track-dur {
    padding-right: 4px;
}

/* Default (no audio preview anywhere in this batch) — compact and right-aligned
   so the dots sit flush to the table's right edge and the gap to the time
   column is just the natural cell padding. The 210px reservation kicks in only
   when audio is present (see .tracklist-with-preview below) — without this
   split, an all-dots-no-audio batch left a ~190px gap between time and dots
   under the previous flex-start + min-width:210px combination. */
.track-listen-cluster {
    display: flex;
    align-items: center;
    gap: 8px;
    justify-content: flex-end;
}

/* When at least one track in the batch has an inline audio preview, the
   cluster reserves a 210px slot and left-justifies inside it so the 120px
   <audio> control sits at a stable left edge (rather than extending back
   into the time column on rows that have audio while leaving rows-with-only-
   dots floating mid-column). 210px = 120px preview + 8px gap + ~90px four-icon
   strip — tuned down from 220px which over-reserved space and pushed the
   strip visually far from the duration. */
.tracklist-with-preview .track-listen-cluster {
    justify-content: flex-start;
    min-width: 210px;
}

/* Apple 30-sec preview player.  Compact native controls — scrub + play/pause + time.
   The ugly extras (download, speed, PiP) are suppressed via controlsList and
   disablePictureInPicture on the <audio> element itself.  Width is deliberately
   tight so the column doesn't dominate the row. */
.track-preview {
    width: 120px;
    height: 28px;
}

/* Per-track "Listen on" strip: cluster of service dots that link to search URLs
   (Spotify/YT Music are fire-and-forget; Apple is first-party verified). */
.listen-strip {
    display: flex;
    align-items: center;
    gap: 4px;
}

.listen-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: 1px solid var(--border);
    text-decoration: none;
    transition: border-color 0.12s;
}

.listen-icon:hover,
.listen-icon:focus-visible {
    border-color: var(--accent);
    outline: none;
}

.listen-icon .chip-dot {
    margin: 0;
}

/* Narrow/mobile: keep the per-track listen-strip dots visible (Spotify / YT Music
   ISRC search-deeplinks — 18×18 each, fit comfortably alongside the time column),
   but hide the inline <audio> preview control. The preview player is 120px wide
   and would force the title column to wrap or truncate on phone widths. The
   per-track dots remain useful even with the new album-level pills since they
   target individual tracks rather than the whole album.

   Up to 2026-04-30 this rule hid the entire .track-listen column at <=900px;
   the album-level pills didn't exist then, so hiding everything was acceptable.
   Splitting the rule keeps the small affordance and drops only the bulky one. */
@media (max-width: 900px) {
    .tracklist .track-preview {
        display: none;
    }
    /* Narrow column accommodates just the icon strip (~50px) when audio is
       hidden — tightens the gap between time and the dots so the row doesn't
       feel front-loaded with empty space. */
    .tracklist .track-listen-cluster {
        min-width: 0;
        justify-content: flex-end;
    }
}

/* -------------------------------------------------------------------------
   Account settings page (Phase 2 — identity linking)
   ------------------------------------------------------------------------- */
.account-settings {
    max-width: 720px;
    margin: 0 auto;
    padding: 24px 16px 48px;
    /* --text-secondary (not --text-primary) for the page's base colour: the h1
       and bold subsection titles still use currentColor, so shifting the
       container down one notch softens the whole page's bolded text without
       needing per-element overrides. Hints remain on --text-dim / --text-muted
       and are unchanged. */
    color: var(--text-secondary);
    /* Single-pane content scroll. .app-body has overflow:hidden so each
       flat-layout page has to host its own scroll. See .admin-page for the
       full rationale. */
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
}

.account-header h1 {
    font-size: 22px;
    font-weight: 600;
    margin: 0 0 4px;
}

.account-subtitle {
    color: var(--text-dim);
    font-size: 13px;
    margin: 0 0 24px;
}

.account-section {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 16px 18px;
    margin-bottom: 18px;
}

.account-section-header h2 {
    font-size: 14px;
    font-weight: 600;
    margin: 0 0 4px;
}

.account-section-hint {
    color: var(--text-dim);
    font-size: 12px;
    margin: 0 0 12px;
    line-height: 1.5;
}

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

.identity-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 10px 0;
    border-top: 1px solid var(--border);
}

.identity-row:first-child {
    border-top: 0;
}

.identity-main {
    flex: 1;
    min-width: 0;
}

.identity-provider {
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 2px;
}

.identity-detail {
    display: flex;
    gap: 10px;
    font-size: 12px;
    color: var(--text-dim);
    flex-wrap: wrap;
}

.identity-email-missing {
    font-style: italic;
    opacity: 0.7;
}

.identity-unlink-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-primary);
    padding: 6px 12px;
    border-radius: 6px;
    font-size: 12px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.identity-unlink-btn:hover:not(:disabled) {
    background: var(--bg-input);
    border-color: var(--text-primary);
}

.identity-unlink-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.account-loading,
.account-empty {
    padding: 12px 0;
    color: var(--text-dim);
    font-size: 13px;
}

.account-error {
    margin-top: 12px;
    padding: 10px 12px;
    border-radius: 6px;
    background: rgba(220, 50, 50, 0.12);
    border: 1px solid rgba(220, 50, 50, 0.4);
    color: #ff9898;
    font-size: 12px;
}

/* Phase 3 — GDPR self-service data export. Same visual weight as the
   identity-unlink button so it doesn't read as a primary action; the
   destructive-styled erase button on /admin/users uses a different tone. */
.data-export-btn {
    background: transparent;
    border: 1px solid var(--border);
    color: var(--text-primary);
    padding: 8px 14px;
    border-radius: 6px;
    font-size: 13px;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
}

.data-export-btn:hover:not(:disabled) {
    background: var(--bg-input);
    border-color: var(--text-primary);
}

.data-export-btn:disabled {
    opacity: 0.55;
    cursor: progress;
}

.data-export-error {
    /* Tweak the shared account-error to use the muted info tone rather than
       red — 429 isn't an error, it's a "come back later" message. */
    background: rgba(180, 180, 180, 0.10);
    border-color: rgba(180, 180, 180, 0.32);
    color: var(--text-dim);
}

.link-provider-row {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.link-provider-btn {
    display: inline-block;
    background: var(--bg-input);
    border: 1px solid var(--border);
    color: var(--text-primary);
    padding: 10px 16px;
    border-radius: 6px;
    font-size: 13px;
    text-decoration: none;
    transition: background 0.15s, border-color 0.15s;
}

.link-provider-btn:hover {
    background: var(--border);
    border-color: var(--text-primary);
}

/* Link-flow result banner — shown above the settings content after the OIDC
   callback redirects back with ?linked=true or ?error=... */
.account-banner {
    max-width: 720px;
    margin: 16px auto 0;
    padding: 10px 14px;
    border-radius: 6px;
    font-size: 13px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
}

.account-banner-success {
    background: rgba(50, 180, 80, 0.12);
    border: 1px solid rgba(50, 180, 80, 0.4);
    color: #a9e6b8;
}

.account-banner-error {
    background: rgba(220, 50, 50, 0.12);
    border: 1px solid rgba(220, 50, 50, 0.4);
    color: #ff9898;
}

.account-banner-dismiss {
    background: transparent;
    border: 1px solid currentColor;
    color: inherit;
    padding: 3px 10px;
    border-radius: 4px;
    font-size: 11px;
    cursor: pointer;
}

/* -------------------------------------------------------------------------
   LoginDisplay (navbar account dropdown) — Phase 2 rebuild.
   Uses native <details>/<summary> so the toggle works without JS.
   ------------------------------------------------------------------------- */

/* <details> defaults to display:block which, inside the flex .topnav, would
   stretch to fill width. inline-block sizes to content and lets the navbar's
   flex layout keep its other children on the baseline. position:relative
   anchors the absolutely-positioned .user-menu-panel beneath the summary. */
.user-menu {
    position: relative;
    display: inline-block;
    margin-right: 6px;
    flex-shrink: 0;
}

/* <summary> has display:list-item by default — the default triangle marker
   shows through no matter what. list-style:none + ::marker{content:""} +
   ::-webkit-details-marker{display:none} together cover every browser.
   display:flex forces the children to line up horizontally; without that,
   some browsers still render the summary as list-item with children on new
   lines. */
/* Toggle text uses --text-muted to match .nav-btn; hover/open state lifts to
   --text-primary. Keeps the navbar visually consistent — the account label
   shouldn't pop brighter than "Discography" / "Search" / "Scan" etc. */
.user-menu-toggle {
    list-style: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    background: transparent;
    color: var(--text-muted);
    font-size: 13px;
    border: 1px solid transparent;
    border-radius: 6px;
    user-select: none;
    white-space: nowrap;
    transition: color 0.15s, background 0.15s;
}

.user-menu-toggle::-webkit-details-marker { display: none; }
.user-menu-toggle::marker { content: ""; }

.user-menu-toggle:hover {
    background: var(--bg-input);
    border-color: var(--border);
    color: var(--text-primary);
}

.user-menu[open] > .user-menu-toggle {
    background: var(--bg-input);
    border-color: var(--border);
    color: var(--text-primary);
}

/* SVG sizing belt-and-braces: width/height attributes on the element set the
   intrinsic size; the CSS here reinforces. flex-shrink:0 stops the flex
   layout from squeezing them to 0. */
.user-menu-icon {
    flex-shrink: 0;
    opacity: 0.85;
}

/* Initials avatar — replaces the generic person icon on the authorized toggle.
   Sized off the toggle's font size so it scales naturally on mobile. Background
   uses the brand accent so the disc is recognisable regardless of name length. */
.user-menu-avatar {
    flex-shrink: 0;
    width: 24px;
    height: 24px;
    border-radius: 50%;
    background: var(--accent);
    color: #fff;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.5px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    user-select: none;
    line-height: 1;
}

.user-menu-name {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 180px;
}

.user-menu-caret {
    flex-shrink: 0;
    opacity: 0.6;
    margin-left: 2px;
    transition: transform 0.15s;
}

.user-menu[open] .user-menu-caret {
    transform: rotate(180deg);
}

.user-menu-panel {
    position: absolute;
    right: 0;
    top: calc(100% + 4px);
    min-width: 240px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25);
    padding: 6px;
    z-index: 1000;
}

/* Panel header — name (primary) + "Signed in with X" (dim). Lives at the top of
   the dropdown so the user can confirm which identity / provider they're acting
   as before clicking Sign out (especially relevant during the multi-identity
   parallel-run period). Padding matches .user-menu-item so the header aligns
   visually with the menu items below. */
.user-menu-panel-header {
    padding: 8px 10px 6px;
}

.user-menu-panel-name {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
    line-height: 1.25;
}

.user-menu-panel-provider {
    margin-top: 2px;
    font-size: 11px;
    color: var(--text-dim);
}

/* Matching :visited pair: without the :visited selector, a previously-visited
   /account/settings link inherits the browser's built-in purple visited colour,
   making it stand out against /Log out in the same menu. :visited can only set
   colour/background-colour/etc. (privacy restriction), which is all we need.
   Base colour is --text-muted to match .nav-btn; hover lifts to --text-primary. */
.user-menu-item,
.user-menu-item:visited {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 10px;
    font-size: 13px;
    color: var(--text-muted);
    text-decoration: none;
    border-radius: 6px;
    transition: background 0.1s, color 0.15s;
}

.user-menu-item:hover,
.user-menu-item:visited:hover {
    background: var(--bg-input);
    color: var(--text-primary);
}

.user-menu-item-icon {
    flex-shrink: 0;
    opacity: 0.75;
}

.user-menu-item-subtle,
.user-menu-item-subtle:visited {
    color: var(--text-dim);
    font-size: 12px;
}

/* Sign-out item — danger colour to set it apart from the neutral "Account
   settings" item above. Matches the destructive convention used elsewhere
   (Delete album, Remove sign-in). The :hover lifts the same colour rather
   than switching to --text-primary so the danger affordance stays consistent
   on rollover. */
.user-menu-item-danger,
.user-menu-item-danger:visited {
    color: var(--danger);
}

.user-menu-item-danger:hover,
.user-menu-item-danger:visited:hover {
    background: var(--danger-bg);
    color: var(--danger);
}

.user-menu-divider {
    height: 1px;
    background: var(--border);
    margin: 4px 2px;
}

/* Mobile: .topnav is fixed to the bottom, so the dropdown has to open UPWARD
   (otherwise the panel is off-screen under the navbar). Also pull the user
   menu into the flex:1 + centred layout of the other nav buttons so it
   matches them visually, and hide the provider badge to save horizontal
   space — the icon + name is enough at 11px. */
@media (max-width: 767px) {
    .user-menu {
        flex: 1;
        margin-right: 0;
        display: flex;
    }
    .user-menu-toggle {
        flex: 1;
        justify-content: center;
        font-size: 11px;
        padding: 6px 4px;
    }
    .user-menu-name {
        font-size: 11px;
        max-width: none;
    }
    /* Avatar shrinks slightly on mobile to keep the toggle compact alongside
       the centred name; provider is no longer in the toggle so nothing else
       to hide here. */
    .user-menu-avatar {
        width: 22px;
        height: 22px;
        font-size: 10px;
    }
    .user-menu-panel {
        top: auto;
        bottom: calc(100% + 4px);
        right: 4px;
        left: auto;
        min-width: 200px;
    }
}

/* ─── User-entered album metadata ────────────────────────────────────────────
   "Your collection" hero panel + AlbumRow rating strip + star input/strip primitives.
   Display mode uses Unicode glyphs (★ ½ ☆); edit mode layers ten transparent half-step
   buttons over five star backgrounds so half-stars are clickable and keyboard-navigable. */

.star-strip {
    color: #f5b50a;
    font-size: 14px;
    letter-spacing: 1px;
}

/* ── "Your collection" hero column ──────────────────────────────────────────
   Two render branches:
     • All-edit                 → every field as input simultaneously + Save / Cancel
     • Read / per-field edit    → panel with populated rows in a 2-col grid + the
                                   "+ Field" buttons row for missing fields. Clicking
                                   a value swaps that one row into an input + Done.
   The panel always renders for owned albums even when fully empty — the "+ Field"
   buttons are the discovery affordance for first-time entry.
   Mobile (<=900px): the column stacks below the info via flex-wrap on .detail-hero. */

/* Panel can shrink between 360–460px under flex pressure (info wants more room),
   then stacks below 900px container width. The shrink range gives a smooth transition
   instead of jumping straight from 460-fixed to full-width.
   `color-scheme: dark` here is inherited by descendant native widgets — date pickers,
   selects, scrollbars — so they render in dark theme automatically. Both per-field
   and all-edit date inputs pick this up; no need for filter hacks on the indicator. */
.hero-collection {
    flex: 0 1 460px;
    min-width: 360px;
    color-scheme: dark;
}

@container (max-width: 900px) {
    .hero-collection {
        flex-basis: 100%;
        min-width: 0;
        margin-top: 4px;
    }
}

/* Shared panel chrome — referenced as `.panel` so future panels match without further work. */
.panel {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 12px 14px 14px;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
}

.hero-collection-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 10px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--border);
}

.hero-collection-title {
    font-size: 11px;
    font-weight: 600;
    color: var(--text-muted);
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

.coll-edit-all {
    background: transparent;
    border: 1px solid var(--border);
    border-radius: 4px;
    color: var(--text-dim);
    font-size: 11px;
    cursor: pointer;
    padding: 2px 8px;
}

.coll-edit-all:hover {
    color: var(--accent-light, var(--accent));
    border-color: var(--accent-light, var(--accent));
}

/* Read mode: 2-col grid (Rating | Condition, Amount paid | Where bought, Date purchased
   alone, Comments full-width). Auto-flow handles partial-population sensibly — fields
   that are missing simply don't render. Comments explicitly spans both columns. */
.coll-fields {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 16px;
    row-gap: 8px;
}

/* All-edit mode: single-column form so every input gets full panel width. */
.coll-fields-stacked {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.coll-field-row {
    display: flex;
    flex-direction: column;
    gap: 3px;
    min-width: 0;
}

.coll-field-row.full-width {
    grid-column: 1 / -1;
}

/* The "+ Field" missing-field row sits below the populated grid; spans full width so
   the buttons can wrap naturally. */
.coll-add-missing {
    grid-column: 1 / -1;
}

.coll-field-label {
    color: var(--text-dim);
    font-size: 11px;
}

/* Read-mode value rendered as a button so click-to-edit is a single affordance. The
   border highlight on hover mirrors the demo's "this is editable" hint. */
.coll-field-value {
    background: transparent;
    border: 1px solid transparent;
    border-radius: 4px;
    color: var(--text);
    font-size: 13px;
    text-align: left;
    padding: 3px 6px;
    word-break: break-word;
    cursor: pointer;
    transition: background 0.12s, border-color 0.12s;
}

.coll-field-clickable:hover {
    background: rgba(139, 132, 232, 0.08);
    border-color: rgba(139, 132, 232, 0.4);
}

/* When the field's read-mode value is a star strip, keep the gold colour from .star-strip;
   .coll-field-value's color rule is defined later in this file and would otherwise win on
   tie-break. Higher specificity here (two classes) keeps gold on top. */
.coll-field-value.star-strip {
    color: #f5b50a;
}

.coll-field-comments { white-space: pre-wrap; }

/* Per-field inline edit. The input replaces the value text in the same slot — the row
   stays in its 2-col grid cell so other rows don't shift around when the user clicks
   between fields. Inputs are stripped of the heavy .form-input chrome (border, fill,
   12px padding) and styled like editable text — same vibe as the demo. */
.coll-field-edit {
    display: flex;
    align-items: center;
    gap: 6px;
    min-width: 0;
}

.coll-field-edit-block {
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
}

.coll-field-edit input,
.coll-field-edit select,
.coll-field-edit textarea {
    background: transparent;
    border: none;
    border-bottom: 1px dashed rgba(255, 255, 255, 0.22);
    border-radius: 0;
    outline: none;
    color: var(--text);
    font-size: 13px;
    font-family: inherit;
    padding: 2px 4px;
    line-height: 1.4;
}

.coll-field-edit input:focus,
.coll-field-edit select:focus,
.coll-field-edit textarea:focus {
    border-bottom-color: var(--accent);
}

/* Date input: cap to ~120px so the picker indicator + value fit without padding the
   row out wider than necessary. The per-field cap stays in `.coll-field-edit`; the
   `color-scheme: dark` declaration is hoisted up to .hero-collection (below) so both
   per-field and all-edit date inputs render their picker indicator in dark theme.
   Don't double up with `filter: invert(1)` here — color-scheme already lights the
   indicator and inversion would flip it back to dark. */
.coll-field-edit input[type="date"] {
    width: 120px;
    max-width: 120px;
}

.coll-field-edit textarea {
    resize: vertical;
    min-height: 52px;
    width: 100%;
}

.coll-edit-input {
    flex: 1;
    min-width: 0;
}

.coll-done-btn {
    flex-shrink: 0;
    background: rgba(139, 132, 232, 0.18);
    border: 1px solid rgba(139, 132, 232, 0.5);
    border-radius: 3px;
    color: var(--accent-light, var(--accent));
    font-size: 11px;
    padding: 2px 8px;
    cursor: pointer;
    white-space: nowrap;
}

.coll-done-btn:hover:not(:disabled) {
    background: rgba(139, 132, 232, 0.28);
}

.coll-done-btn:disabled { opacity: 0.6; cursor: default; }

/* Half-step star input. Ten 12×24 transparent buttons overlay five 24px star glyphs
   rendered via ::before. .left/.right shift the pseudo-element so each button only
   shows its own half. .filled flips the colour from dim to gold; a 4.5-star rating
   is the natural emergent result of button 9 filled and button 10 not. The Clear
   button is gone — clicking the same value decrements; clicking below 1 clears. */
.coll-star-input {
    display: inline-flex;
    align-items: center;
    gap: 1px;
    flex-shrink: 0;
}

.coll-star-half {
    width: 12px;
    height: 24px;
    border: none;
    background: transparent;
    padding: 0;
    cursor: pointer;
    position: relative;
    color: rgba(255, 255, 255, 0.22);
    font-size: 18px;
    line-height: 24px;
    overflow: hidden;
}

.coll-star-half::before {
    content: '\2605';   /* ★ */
    position: absolute;
    top: 0;
    width: 24px;
    height: 24px;
    line-height: 24px;
    pointer-events: none;
}

.coll-star-half.left::before  { left: 0; }
.coll-star-half.right::before { left: -12px; }

.coll-star-half.filled { color: #f5b50a; }

.coll-star-half:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
    border-radius: 2px;
}

.coll-amount-input {
    display: flex;
    gap: 6px;
    align-items: center;
}

/* Compact widths for amount + currency — small-fixed sizes match the demo. The number
   input has an explicit max-width so even pasting/typing a long string doesn't blow
   the field up to half the panel. */
.coll-amount-amount {
    width: 70px;
    max-width: 70px;
    flex: 0 0 70px;
}

.coll-amount-currency {
    width: 56px;
    max-width: 56px;
    flex: 0 0 56px;
}

/* "+ Field name" buttons for missing fields. Match the same accent-link weight as
   the empty-state Add details so the affordance reads as the same kind of action. */
.coll-add-missing {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    margin-top: 4px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
}

.coll-add-field-btn {
    background: transparent;
    border: none;
    color: var(--accent-light, var(--accent));
    font-size: 12px;
    cursor: pointer;
    padding: 0;
    text-decoration: underline;
    text-underline-offset: 2px;
}

.coll-add-field-btn:hover { color: var(--accent); }

/* All-edit mode Save/Cancel row, separated from the field grid by a thin rule. */
.coll-save-row {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
    margin-top: 10px;
    padding-top: 8px;
    border-top: 1px solid var(--border);
}

/* Discog-item rating strip on the Artist page album list. Sits right-justified between
   the album info (flex: 1) and the row's right edge. Hidden on narrow viewports — the
   row is already space-pressured and the rating is still accessible on the album detail. */
.discog-item-rating {
    flex-shrink: 0;
    font-size: 13px;
    color: #f5b50a;
    letter-spacing: 1px;
    margin-left: 8px;
}

@media (max-width: 900px) {
    .discog-item-rating { display: none; }
}

/* Star input — ten half-step buttons positioned over five star background cells.
   Each button is a 16x32 strip; .left / .right place them as the left and right
   halves of one star. .filled bumps the colour up to the accent yellow.            */
.star-input {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 4px 0;
}

.star-input .star-half {
    width: 16px;
    height: 32px;
    border: none;
    background: transparent;
    padding: 0;
    cursor: pointer;
    position: relative;
    color: #6a6a6a;
    font-size: 24px;
    line-height: 32px;
    overflow: hidden;
}

.star-input .star-half::before {
    content: "★";
    position: absolute;
    top: 0;
    width: 32px;
    height: 32px;
    line-height: 32px;
}

.star-input .star-half.left::before  { left: 0; }
.star-input .star-half.right::before { left: -16px; }

.star-input .star-half.filled { color: #f5b50a; }

.star-input .star-half:focus-visible {
    outline: 2px solid var(--accent, #8B84E8);
    outline-offset: 2px;
}

.star-input .star-clear {
    margin-left: 8px;
    background: transparent;
    border: 1px solid var(--border, #3a3a3a);
    color: var(--text-dim);
    font-size: 11px;
    padding: 2px 8px;
    border-radius: 4px;
    cursor: pointer;
}

.star-input .star-clear:hover {
    color: var(--text);
    border-color: var(--border-med, #555);
}

/* ─── Legal pages (Privacy, Terms) ───────────────────────────────────────── */
.legal-page {
    max-width: 760px;
    margin: 0 auto;
    padding: 32px 24px 64px;
    height: 100%;
    overflow-y: auto;
    line-height: 1.55;
    color: var(--text-secondary);
}

.legal-page h1 {
    font-size: 24px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 4px;
}

.legal-page h2 {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 28px 0 8px;
}

.legal-page .legal-meta {
    color: var(--text-dim);
    font-size: 12px;
    margin: 0 0 24px;
}

.legal-page p,
.legal-page li {
    margin: 0 0 10px;
}

.legal-page ul {
    margin: 0 0 14px;
    padding-left: 22px;
}

.legal-page a {
    color: var(--accent-light);
}

/* ─── Upgrade / lockout page ─────────────────────────────────────────────────
   Reachable from the route guard on lapsed users, the TrialBanner CTA, and
   the AccountSettings Subscribe/Resubscribe button. Same shape as
   .account-settings — single-pane, viewport-bounded scrolling. */
.upgrade-page {
    max-width: 720px;
    margin: 0 auto;
    padding: 24px 16px 48px;
    color: var(--text-secondary);
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
}

.upgrade-page .upgrade-loading {
    color: var(--text-dim);
    font-size: 13px;
}

.upgrade-page .upgrade-title {
    font-size: 24px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 8px;
}

.upgrade-page .upgrade-summary {
    margin: 0 0 20px;
    line-height: 1.5;
}

.upgrade-page .upgrade-details {
    display: grid;
    grid-template-columns: 140px 1fr;
    gap: 4px 16px;
    margin: 0 0 24px;
    padding: 16px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.upgrade-page .upgrade-details dt {
    font-size: 12px;
    color: var(--text-muted);
    font-weight: 500;
}

.upgrade-page .upgrade-details dd {
    margin: 0;
    color: var(--text-primary);
}

.upgrade-page .upgrade-cta-row {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
    margin: 0 0 16px;
}

.upgrade-page .upgrade-cta {
    padding: 10px 18px;
    border-radius: 6px;
    font-size: 13px;
    text-decoration: none;
    transition: background 0.15s, color 0.15s;
}

.upgrade-page .upgrade-cta.primary {
    background: var(--accent);
    color: var(--text-primary);
}

.upgrade-page .upgrade-cta.primary:hover {
    background: var(--accent-light);
}

.upgrade-page .upgrade-cta.secondary {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    color: var(--text-muted);
}

.upgrade-page .upgrade-cta.secondary:hover {
    color: var(--text-primary);
    background: var(--bg-hover);
}

.upgrade-page .upgrade-data-notice {
    margin: 24px 0 0;
    font-size: 12px;
    color: var(--text-dim);
    line-height: 1.45;
}

/* Trial-vs-Pro entitlement table sits below the pricing cards on /upgrade.
   Re-uses .pricing-entitlements styling defined alongside the /pricing page. */
.upgrade-page .upgrade-entitlements {
    margin: 32px 0 0;
}

.upgrade-page .upgrade-entitlements h2 {
    font-size: 16px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 8px;
}

.upgrade-page .upgrade-entitlements p {
    margin: 0 0 8px;
    font-size: 13px;
    color: var(--text-secondary);
}

/* Pricing cards have their own top margin; tuck them in on /upgrade so they
   sit closer to the summary copy than on /pricing where they're the focal point. */
.upgrade-page .pricing-cards {
    margin-top: 12px;
    margin-bottom: 0;
}

@media (max-width: 600px) {
    .upgrade-page .upgrade-details {
        grid-template-columns: 1fr;
        gap: 2px 0;
    }
    .upgrade-page .upgrade-details dt {
        margin-top: 8px;
    }
    .upgrade-page .upgrade-details dt:first-child {
        margin-top: 0;
    }
}

/* `.nav-legal` removed — Privacy/Terms/Legal now live in Account Settings
   per Phase 2c (review item 34). */
.account-legal-list {
    list-style: none;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
}

.account-legal-list li {
    border-bottom: 1px solid var(--border);
}

.account-legal-list li:last-child {
    border-bottom: none;
}

.account-legal-list a {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 12px 4px;
    color: var(--accent-light);
    text-decoration: none;
    font-size: 14px;
    min-height: 40px;
    transition: color 0.12s;
}

.account-legal-list a::after {
    content: '›';
    color: var(--text-dim);
    font-size: 18px;
    font-weight: 400;
}

.account-legal-list a:hover,
.account-legal-list a:focus-visible {
    color: var(--accent);
    text-decoration: underline;
}

.account-legal-list a:hover::after,
.account-legal-list a:focus-visible::after { color: var(--accent); }

/* Music service visibility panel (May 2026). Two grouped fieldsets — streaming
   services and track previews — each rendering as a vertical stack of
   checkbox + label rows. Reset the browser default fieldset chrome so the
   panel blends with the rest of the account-section visual language. */
.music-service-toggles {
    border: none;
    margin: 0 0 16px 0;
    padding: 0;
}

.music-service-toggles:last-child {
    margin-bottom: 0;
}

.music-service-group-label {
    font-size: 13px;
    font-weight: 600;
    color: var(--text);
    margin: 0 0 4px 0;
    padding: 0;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.music-service-group-hint {
    font-size: 13px;
    color: var(--text-dim);
    margin: 0 0 10px 0;
    line-height: 1.4;
}

.music-service-toggle {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 4px;
    min-height: 40px;
    cursor: pointer;
    border-bottom: 1px solid var(--border);
    color: var(--text);
    font-size: 14px;
    transition: color 0.12s;
}

.music-service-toggle:last-child {
    border-bottom: none;
}

.music-service-toggle:hover {
    color: var(--accent);
}

.music-service-toggle input[type="checkbox"] {
    width: 16px;
    height: 16px;
    accent-color: var(--accent);
    cursor: pointer;
    margin: 0;
    flex: 0 0 auto;
}

@media (max-width: 480px) {
    .legal-page {
        padding: 20px 16px 80px;
    }
    .legal-page h1 { font-size: 20px; }
    .legal-page h2 { font-size: 15px; margin-top: 22px; }
}

/* ─── Landing page (anonymous /) — full redesign ─────────────────────────────
   Vertical stack of full-width sections (hero, three feature blocks, pricing,
   footer). Each section manages its own internal max-width / centering.
   demo-playback.js drives scroll-driven playback on every <video.demo-video>. */

.landing {
    /* Owns the page scroll. .app-body has overflow:hidden so internal scroll
       must happen here, not on body. */
    height: 100%;
    overflow-y: auto;
    background: var(--bg-base);
}

/* Translucent topnav with backdrop blur ONLY when sitting over the landing
   page. Other surfaces keep the solid bg defined on .topnav at line 86. */
body:has(.landing) .topnav {
    background: rgba(14, 14, 14, 0.78);
    backdrop-filter: blur(12px);
    -webkit-backdrop-filter: blur(12px);
    border-bottom-color: rgba(255, 255, 255, 0.06);
}

/* Section base — landing layout uses CSS variables for the in-section content
   width and side gutters so each section block stays consistent. */
.landing > section {
    padding-left: 48px;
    padding-right: 48px;
}

/* ─── Hero ─────────────────────────────────────────────────────────────── */
.landing-hero {
    text-align: center;
    padding-top: 120px;
    padding-bottom: 100px;
    border-bottom: 1px solid var(--border);
    position: relative;
    overflow: hidden;
}

/* Soft purple bloom behind the hero — single absolutely-positioned ::before
   so we don't introduce a separate wrapper. Matches the existing app palette;
   subtle enough to read as ambient glow, not a gradient surface. */
.landing-hero::before {
    content: '';
    position: absolute;
    top: -160px;
    left: 50%;
    transform: translateX(-50%);
    width: 720px;
    height: 720px;
    background: radial-gradient(circle, rgba(139, 132, 232, 0.08) 0%, transparent 70%);
    pointer-events: none;
}

.landing-tagline {
    /* "A personal music biography." — small reassurance above the headline. */
    font-size: 12px;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent-light);
    margin: 0 0 24px;
    position: relative;
}

.landing-hero-headline {
    font-family: 'DM Serif Display', Georgia, serif;
    font-weight: 400;
    font-size: clamp(40px, 7vw, 68px);
    line-height: 1.08;
    letter-spacing: -0.01em;
    color: var(--text-primary);
    margin: 0 0 28px;
    position: relative;
}

.landing-hero-headline em {
    /* Italic + lavender on "Connected" — the differentiator word. */
    font-style: italic;
    color: var(--accent-light);
}

.landing-hero-sub {
    font-size: 17px;
    line-height: 1.65;
    color: var(--text-secondary);
    max-width: 480px;
    margin: 0 auto 44px;
    position: relative;
}

/* ─── CTA group (used in hero AND pricing footer) ───────────────────────── */
.landing-cta-group {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    position: relative;
}

.landing-cta-primary {
    /* Deep saturated purple per spec §11 — white text on it passes WCAG AA.
       --accent (#8B84E8) is lavender and would fail; using a deeper shade
       reserved for white-on-purple CTAs only. Defined here rather than in
       :root so it doesn't accidentally get pulled in by other surfaces. */
    display: inline-block;
    background: #6948e0;
    color: #ffffff;
    padding: 15px 32px;
    border-radius: 8px;
    font-size: 15px;
    font-weight: 500;
    text-decoration: none;
    border: none;
    cursor: pointer;
    transition: background 0.18s, transform 0.12s;
}

.landing-cta-primary:hover {
    background: #7c5cf5;
    transform: translateY(-1px);
}

.landing-cta-primary:active {
    transform: translateY(0);
}

.landing-cta-trust {
    font-size: 12px;
    color: var(--text-dim);
    line-height: 1.55;
    margin: 0;
    max-width: 360px;
    text-align: center;
}

/* ─── Stats band (three-cell feature summary, sits below hero) ──────────
   Subtle elevation panel so the band visually separates from the hero
   above and the Feature 1 section below. Icons are lavender, not the
   saturated CTA purple, per the §11 contrast rule (deep purple is
   reserved for white-text-on-purple surfaces). */
.landing-stats {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 24px;
    align-items: stretch;
    padding: 32px 48px;
    margin: 0 auto;
    max-width: 1200px;
    background: rgba(255, 255, 255, 0.025);
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}

.landing-stats-cell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    padding: 8px 12px;
    text-align: center;
}

.landing-stats-icon {
    color: var(--accent-light);
    flex-shrink: 0;
}

.landing-stats-text {
    margin: 0;
    font-size: 15px;
    line-height: 1.5;
    color: var(--text);
}

/* ─── Feature sections (1 & 2: two-column alternating; 3: paired demos) ── */
.landing-feature {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 64px;
    align-items: center;
    padding-top: 80px;
    padding-bottom: 80px;
    border-bottom: 1px solid var(--border);
    max-width: 1200px;
    margin: 0 auto;
}

/* Feature 2 reverses the layout: demo first in source order would still
   render text-left, demo-right on desktop because grid columns are fixed.
   We use `order` to swap them in-place. Mobile resets via the responsive
   block below. */
.landing-feature-reverse .landing-feature-demo {
    order: -1;
}

.landing-feature-tag {
    font-size: 11px;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent-light);
    margin: 0 0 14px;
}

.landing-feature-headline {
    font-family: 'DM Serif Display', Georgia, serif;
    font-weight: 400;
    font-size: 30px;
    line-height: 1.25;
    color: var(--text-primary);
    margin: 0 0 16px;
}

.landing-feature-body {
    font-size: 15px;
    line-height: 1.75;
    color: var(--text-secondary);
    margin: 0;
    max-width: 480px;
}

.landing-feature-demo {
    min-width: 0;
}

/* Feature 3 — centred headline above a paired demo row. No body paragraph. */
.landing-feature-paired {
    padding-top: 80px;
    padding-bottom: 80px;
    border-bottom: 1px solid var(--border);
    max-width: 1200px;
    margin: 0 auto;
    text-align: center;
}

.landing-feature-paired-head {
    margin: 0 auto 48px;
    max-width: 720px;
}

.landing-feature-paired-head .landing-feature-headline {
    margin-bottom: 0;
}

.landing-feature-paired-demos {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 24px;
}

.landing-feature-paired-item {
    min-width: 0;
}

/* Demo video — class name preserved because demo-playback.js queries on it. */
.demo-video {
    width: 100%;
    display: block;
    border-radius: 12px;
    border: 1px solid var(--border);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
    aspect-ratio: 16 / 9;
    background: var(--bg-panel);
}

/* ─── Pricing band ──────────────────────────────────────────────────────── */
.landing-pricing {
    text-align: center;
    padding-top: 96px;
    padding-bottom: 96px;
    background: var(--bg-panel);
    border-top: 1px solid var(--border);
    border-bottom: 1px solid var(--border);
}

.landing-pricing-heading {
    font-family: 'DM Serif Display', Georgia, serif;
    font-weight: 400;
    font-size: 36px;
    color: var(--text-primary);
    margin: 0 0 10px;
}

.landing-pricing-sub {
    font-size: 15px;
    color: var(--text-secondary);
    margin: 0 0 48px;
}

.landing-pricing-cards {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    max-width: 720px;
    margin: 0 auto 40px;
    text-align: left;
}

.landing-pricing-card {
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 12px;
    padding: 28px 26px;
    position: relative;
}

.landing-pricing-card-featured {
    /* Saturated purple border for the featured (annual) card. */
    border-color: rgba(124, 92, 245, 0.55);
    box-shadow: 0 0 0 1px rgba(124, 92, 245, 0.2);
}

.landing-pricing-badge {
    /* "BEST VALUE" — dark text on lavender purple per spec §11 contrast rule. */
    display: inline-block;
    position: absolute;
    top: -12px;
    left: 50%;
    transform: translateX(-50%);
    padding: 4px 12px;
    background: #a78bfa;
    color: #161324;
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    border-radius: 4px;
    white-space: nowrap;
}

.landing-pricing-price {
    font-family: 'DM Serif Display', Georgia, serif;
    font-size: 28px;
    color: var(--text-primary);
    line-height: 1.2;
    margin: 0 0 4px;
}

.landing-pricing-rate {
    font-size: 13px;
    color: var(--text-muted);
    margin: 0;
}

.landing-pricing-savings {
    font-size: 14px;
    font-weight: 600;
    color: var(--accent-light);
    margin: 12px 0 0;
}

.landing-pricing-bullets {
    list-style: none;
    padding: 0;
    margin: 18px 0 0;
    font-size: 13px;
    color: var(--text-secondary);
    line-height: 1.85;
}

.landing-pricing-bullets li::before {
    content: '✓ ';
    color: var(--accent-light);
    font-weight: 600;
    margin-right: 4px;
}

/* ─── Responsive: single breakpoint at ~720px ───────────────────────────── */
@media (max-width: 720px) {
    .landing > section {
        padding-left: 24px;
        padding-right: 24px;
    }
    .landing-hero {
        padding-top: 72px;
        padding-bottom: 64px;
    }
    .landing-hero-sub { font-size: 15px; }
    /* Stats band: single column with icon left of sentence, vertically
       centred, per spec §5.3 last bullet. */
    .landing-stats {
        grid-template-columns: 1fr;
        gap: 14px;
        padding: 24px 24px;
    }
    .landing-stats-cell {
        flex-direction: row;
        align-items: center;
        text-align: left;
        gap: 16px;
        padding: 6px 4px;
    }
    .landing-feature,
    .landing-feature-paired {
        grid-template-columns: 1fr;
        gap: 32px;
        padding-top: 56px;
        padding-bottom: 56px;
    }
    /* Mobile: text first, demo second for every feature row — reset
       Feature 2's desktop `order: -1` (which puts the demo on the left)
       so source order (text → demo) wins on narrow viewports. Keeps the
       eyebrow + headline + body visible above the fold; the demo follows
       as visual confirmation of what was just described. */
    .landing-feature .landing-feature-demo,
    .landing-feature-reverse .landing-feature-demo { order: 0; }
    .landing-feature-paired-demos { grid-template-columns: 1fr; gap: 20px; }
    .landing-feature-paired-head { margin-bottom: 32px; }
    .landing-pricing {
        padding-top: 64px;
        padding-bottom: 64px;
    }
    .landing-pricing-cards { grid-template-columns: 1fr; max-width: 380px; }
    .landing-cta-primary { padding: 14px 28px; }
}

/* ─── Pricing page (/pricing) ────────────────────────────────────────────── */
.pricing-page {
    /* Match .legal-page: fill the .app-body height (which is viewport-minus-topnav,
       overflow:hidden) and scroll internally. Without this rule the content
       overflows .app-body and is clipped — the page appears not to scroll. */
    max-width: 820px;
    margin: 0 auto;
    padding: 32px 24px 40px;
    height: 100%;
    overflow-y: auto;
    color: var(--text-primary);
}

.pricing-page-header {
    text-align: center;
    margin: 0 0 36px;
}

.pricing-page-header h1 {
    font-size: 28px;
    font-weight: 700;
    margin: 0 0 12px;
}

.pricing-page-subhead {
    font-size: 15px;
    color: var(--text-secondary);
    margin: 0;
}

.pricing-section {
    max-width: 720px;
    margin: 40px auto 0;
}

.pricing-section h2 {
    font-size: 18px;
    font-weight: 600;
    margin: 0 0 12px;
    color: var(--text-primary);
}

.pricing-section h3 {
    font-size: 14px;
    font-weight: 600;
    margin: 18px 0 6px;
    color: var(--text-primary);
}

.pricing-section p {
    font-size: 14px;
    line-height: 1.6;
    color: var(--text-secondary);
    margin: 0 0 12px;
}

.pricing-entitlements {
    width: 100%;
    border-collapse: collapse;
    margin: 14px 0 18px;
    font-size: 14px;
}

.pricing-entitlements th,
.pricing-entitlements td {
    padding: 10px 12px;
    text-align: left;
    border-bottom: 1px solid var(--border);
}

.pricing-entitlements thead th {
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.05em;
    color: var(--text-muted);
}

.pricing-entitlements tbody th {
    font-weight: 500;
    color: var(--text-secondary);
}

.pricing-entitlements tbody td {
    color: var(--text-primary);
}

.pricing-fineprint {
    margin: 8px 0 0;
}

.pricing-fineprint dt {
    font-weight: 600;
    color: var(--text-primary);
    margin: 12px 0 2px;
    font-size: 14px;
}

.pricing-fineprint dd {
    margin: 0 0 4px;
    color: var(--text-secondary);
    font-size: 14px;
    line-height: 1.5;
}

/* Phase 2 soft UK-only notice rendered when the request's CF-IPCountry
   header is non-GB. Sits between the page header and the pricing cards,
   visually quieter than an error banner — this is informational, not a
   blocker (the user can still start a free trial). */
.pricing-notice {
    max-width: 640px;
    margin: 0 auto 24px;
    padding: 12px 16px;
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    border-radius: 8px;
    color: var(--accent-light);
    font-size: 13px;
    text-align: center;
    line-height: 1.5;
}

@media (max-width: 480px) {
    .pricing-page {
        padding: 24px 16px 32px;
    }
    .pricing-page-header h1 { font-size: 24px; }
    .pricing-page-subhead { font-size: 14px; }
    .pricing-section h2 { font-size: 16px; }
}

/* ─── Pricing cards (shared by /, /pricing, /upgrade) ────────────────────── */

/* Compact teaser — used inside the landing page (anonymous /). Intentionally
   lightweight: a deliberate "section, not centre of gravity" per spec §4.1. */
.pricing-teaser {
    margin: 36px auto 0;
    padding: 24px 24px 4px;
    border-top: 1px solid var(--border);
    text-align: center;
}

.pricing-teaser-heading {
    font-size: 18px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 14px;
}

.pricing-teaser-line {
    font-size: 14px;
    color: var(--text-secondary);
    margin: 0 0 6px;
}

.pricing-teaser-tagline {
    font-size: 13px;
    color: var(--text-muted);
    margin: 14px 0 18px;
}

.pricing-teaser-cta {
    display: inline-block;
    padding: 12px 28px;
    background: var(--accent);
    color: var(--text-primary);
    border-radius: 8px;
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
    transition: background 0.15s, transform 0.05s;
}

.pricing-teaser-cta:hover {
    background: var(--accent-light);
    color: var(--bg-base);
}

.pricing-teaser-cta:active {
    transform: translateY(1px);
}

.pricing-teaser-detail-link {
    margin: 16px 0 0;
    font-size: 12px;
}

.pricing-teaser-detail-link a {
    color: var(--text-dim);
    text-decoration: underline;
}

.pricing-teaser-detail-link a:hover {
    color: var(--text-muted);
}

/* Full two-card layout — used on /pricing and /upgrade. */
.pricing-cards {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    align-items: stretch;
    max-width: 760px;
    margin: 0 auto;
}

.pricing-card {
    position: relative;
    display: flex;
    flex-direction: column;
    padding: 28px 24px 24px;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    border-radius: 12px;
}

/* Yearly is visually emphasised — accent border, slight scale up on desktop. */
.pricing-card-yearly {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent) inset;
}

.pricing-card-tag {
    position: absolute;
    top: -10px;
    left: 50%;
    transform: translateX(-50%);
    padding: 3px 10px;
    background: var(--accent);
    color: var(--bg-base);
    font-size: 10px;
    font-weight: 700;
    letter-spacing: 0.06em;
    border-radius: 4px;
}

.pricing-card-price {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 4px 0 12px;
}

.pricing-card-amount {
    font-size: 34px;
    font-weight: 700;
    color: var(--text-primary);
    line-height: 1.1;
}

.pricing-card-period {
    font-size: 14px;
    color: var(--text-muted);
}

.pricing-card-equivalent {
    font-size: 13px;
    color: var(--text-secondary);
    margin: 0 0 6px;
}

.pricing-card-saving {
    font-size: 13px;
    color: var(--accent-light);
    font-weight: 500;
    margin: 0 0 14px;
}

.pricing-card-bullets {
    list-style: none;
    padding: 0;
    margin: 0 0 22px;
    flex: 1; /* push CTA to bottom so cards line up */
}

.pricing-card-bullets li {
    position: relative;
    padding-left: 22px;
    margin: 0 0 8px;
    font-size: 13px;
    color: var(--text-secondary);
}

.pricing-card-bullets li::before {
    content: "✓";
    position: absolute;
    left: 0;
    top: 0;
    color: var(--accent-light);
    font-weight: 700;
}

.pricing-card-cta {
    display: block;
    padding: 12px 0;
    text-align: center;
    background: var(--bg-hover);
    color: var(--text-primary);
    border-radius: 8px;
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
    transition: background 0.15s, transform 0.05s;
}

.pricing-card-cta:hover {
    background: var(--accent-light);
    color: var(--bg-base);
}

.pricing-card-cta:active {
    transform: translateY(1px);
}

.pricing-card-cta-primary {
    background: var(--accent);
}

/* Disabled / launching-soon variant. Rendered as a <span> (non-clickable)
   when the IsStripeCheckoutEnabled flag is off, so authenticated users see
   the pricing detail but no active subscribe button. */
.pricing-card-cta-disabled {
    background: var(--bg-hover);
    color: var(--text-muted);
    cursor: not-allowed;
    font-style: italic;
}

.pricing-card-cta-disabled:hover {
    background: var(--bg-hover);
    color: var(--text-muted);
}

@media (max-width: 767px) {
    .pricing-cards {
        grid-template-columns: 1fr;
        gap: 28px; /* extra gap because the RECOMMENDED tag overlaps the top edge */
    }
}

@media (max-width: 480px) {
    .pricing-teaser {
        padding: 20px 16px 4px;
    }
    .pricing-card {
        padding: 24px 20px 20px;
    }
    .pricing-card-amount {
        font-size: 30px;
    }
}

/* Suppress the default Blazor "#blazor-error-ui" overlay on anonymous-eligible
   public pages. blazor.web.js still binds to the element (we still render it
   from MainLayout) but the framework's reconnect/error notifications would
   otherwise show a developer-flavoured "An unhandled error has occurred" bar
   on /, /pricing, /terms, /privacy — surfaces a first-time visitor sees
   before they trust the app. The :has() parent selector keys off the
   per-page wrapper classes already in place (.landing-page, .pricing-page,
   .legal-page) so no layout / route-tracking code changes are needed.
   Authenticated routes (which never carry these wrapper classes) still see
   the error UI normally. */
body:has(.landing) #blazor-error-ui,
body:has(.pricing-page) #blazor-error-ui,
body:has(.legal-page) #blazor-error-ui {
    display: none !important;
}

/* ─── Site footer (public pages: /, /pricing, /terms, /privacy) ──────────── */
.site-footer {
    margin: 40px auto 0;
    padding: 16px 24px 24px;
    font-size: 12px;
    color: var(--text-dim);
    text-align: center;
}

.site-footer a {
    color: var(--text-dim);
    text-decoration: none;
}

.site-footer a:hover {
    color: var(--text-muted);
}

.site-footer-sep {
    margin: 0 8px;
    color: var(--text-dim);
}

@media (max-width: 480px) {
    .site-footer {
        /* Stack the links vertically on narrow screens — separators become
           inline dots that wrap naturally with line-height padding. */
        line-height: 1.9;
    }
}

/* ─── Welcome Landing (authenticated, empty library) ────────────────────── */
.welcome-landing {
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 32px 24px;
}
.welcome-content {
    max-width: 480px;
    text-align: center;
}
.welcome-icon {
    font-size: 48px;
    margin-bottom: 12px;
    color: var(--accent);
}
.welcome-title {
    font-size: 22px;
    font-weight: 600;
    color: var(--text);
    margin: 0 0 12px;
}
.welcome-body {
    font-size: 14px;
    color: var(--text-muted);
    line-height: 1.6;
    margin: 0 0 20px;
}
.welcome-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: center;
    margin-bottom: 20px;
}
.welcome-cta {
    display: inline-block;
    padding: 10px 20px;
    background: var(--accent);
    color: #fff;
    font-size: 14px;
    font-weight: 500;
    border-radius: 8px;
    text-decoration: none;
    min-height: var(--min-touch);
    transition: opacity 0.15s;
}
.welcome-cta:hover { opacity: 0.85; }
.welcome-cta-secondary {
    background: var(--accent-bg);
    color: var(--accent-light);
    border: 1px solid var(--accent-border);
}
.welcome-hint {
    font-size: 12px;
    color: var(--text-dim);
    line-height: 1.5;
    margin: 0;
}

/* ─── PR4 admin surface ──────────────────────────────────────────────────────
   Mirrors .account-settings shape (max-width container, padded layout, card
   sections) but wider (1100px vs 720px) to comfortably fit the users table. */

.admin-page {
    max-width: 1100px;
    margin: 0 auto;
    padding: 24px 16px 48px;
    color: var(--text-secondary);
    /* .app-body owns viewport-height clipping (the 2/3-pane views rely on it
       for their internal scroll panes); single-pane content pages have to
       provide their own scroll container or content beyond the viewport is
       unreachable. height: 100% fills .app-body exactly; horizontal centring
       still works because max-width + margin: 0 auto are independent of
       height. The wide-table case has its own .admin-users-table-wrap with
       overflow-x — keeping overflow-x: hidden here avoids a double-scrollbar
       race when the table wrap is wider than the viewport. */
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;
}

.admin-header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 16px;
    margin-bottom: 18px;
}

.admin-header h1 {
    font-size: 22px;
    font-weight: 600;
    margin: 0;
    color: var(--text-primary);
}

.admin-tabs {
    display: flex;
    gap: 4px;
}

/* Tab anchor styling — :visited paired so a previously-visited tab doesn't
   inherit the browser's purple visited colour (mirrors the .user-menu-item
   trick). */
.admin-tab,
.admin-tab:visited {
    padding: 6px 12px;
    font-size: 13px;
    color: var(--text-muted);
    text-decoration: none;
    border-radius: 6px;
    border: 1px solid transparent;
    transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.admin-tab:hover,
.admin-tab:visited:hover {
    color: var(--text-primary);
    background: var(--bg-input);
}

.admin-tab-active,
.admin-tab-active:visited {
    color: var(--text-primary);
    background: var(--bg-input);
    border-color: var(--border);
}

.admin-loading,
.admin-empty {
    padding: 24px;
    text-align: center;
    color: var(--text-dim);
    font-size: 13px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
}

.admin-error {
    padding: 12px 16px;
    margin: 12px 0;
    background: var(--danger-bg);
    border: 1px solid var(--danger-border);
    color: var(--danger);
    border-radius: 6px;
    font-size: 13px;
}

/* Lockout card for non-admins who somehow reach /admin/* — also rendered
   briefly during the SubscriptionStatus-loading window before the page
   knows whether the caller is admin. */
.admin-locked {
    max-width: 480px;
    margin: 60px auto;
    padding: 28px 24px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    text-align: center;
}

.admin-locked h1 {
    font-size: 18px;
    font-weight: 600;
    margin: 0 0 8px;
    color: var(--text-primary);
}

.admin-locked p {
    color: var(--text-dim);
    font-size: 13px;
    margin: 0;
}

/* ── Users table ────────────────────────────────────────────────────────── */

/* Outer wrapper provides horizontal scroll on mobile so the table can keep
   its min-width without breaking the page layout. */
.admin-users-table-wrap {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    overflow-x: auto;
    /* Subtle inner shadow on scroll edges hints at off-screen content;
       cheap way to telegraph "scroll me" on touch devices. */
    -webkit-overflow-scrolling: touch;
}

.admin-users-table {
    width: 100%;
    min-width: 720px;
    border-collapse: collapse;
    font-size: 13px;
}

.admin-users-table th {
    text-align: left;
    padding: 10px 14px;
    color: var(--text-dim);
    font-weight: 600;
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    border-bottom: 1px solid var(--border);
    white-space: nowrap;
    background: var(--bg-card);
}

.admin-users-table td {
    padding: 12px 14px;
    color: var(--text-secondary);
    border-top: 1px solid var(--border);
    vertical-align: middle;
}

/* Engagement count columns (Albums / Concerts). Right-aligned tabular numerics
   in a narrow column so the integers line up and don't widen the already-wide
   table; the wrapper's horizontal scroll covers any overflow on mobile. */
.admin-users-table .admin-count-col {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.admin-users-table tbody tr {
    transition: background 0.1s;
}

.admin-users-table tbody tr:hover {
    background: var(--bg-hover);
}

/* Flex container so multiple action buttons (Edit sub + Promote/Revoke)
   sit side-by-side with consistent spacing, and wrap cleanly on narrow
   widths instead of forcing the column wider. justify-content: flex-end
   right-aligns within the cell so buttons hug the row's right edge across
   varying button-label widths. */
.admin-users-table .admin-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: flex-end;
    align-items: center;
}

.admin-emails {
    color: var(--text-muted);
    font-size: 12px;
    word-break: break-all;
    max-width: 280px;
}

.admin-meta-missing {
    color: var(--text-dim);
    font-style: italic;
    font-size: 12px;
}

.admin-flag-yes {
    color: var(--accent);
    font-weight: 600;
}

/* Status pill — colour-coded for quick scanning. Same colour conventions
   used elsewhere in the app: accent for trial / in-progress, owned (green)
   for active / good, warn (amber) for suspended / attention, danger (red)
   for cancelled / expired. */
.admin-status {
    display: inline-block;
    padding: 3px 10px;
    border-radius: 12px;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    white-space: nowrap;
}

.admin-status-trial {
    background: var(--accent-bg);
    color: var(--accent);
    border: 1px solid var(--accent-border);
}

.admin-status-active {
    background: var(--owned-bg);
    color: var(--owned);
    border: 1px solid var(--owned-border);
}

.admin-status-suspended {
    background: rgba(184, 120, 32, 0.15);
    color: var(--warn);
    border: 1px solid rgba(184, 120, 32, 0.3);
}

.admin-status-cancelled,
.admin-status-expired {
    background: var(--danger-bg);
    color: var(--danger);
    border: 1px solid var(--danger-border);
}

/* Row-level action button — visually distinct from .nav-btn so it reads
   as a clickable affordance inside table cells. Spacing between siblings
   comes from the parent's `gap` (flex container in .admin-actions and
   .admin-edit-actions) rather than per-button margins, so rows don't pick
   up phantom vertical padding from button bottoms.
   min-width keeps the button group's left edge consistent across rows
   regardless of label length ("Close" vs "Edit subscription") — without
   it, right-aligned button groups have ragged left edges as labels vary. */
.admin-action-btn {
    padding: 9px 14px;
    font-size: 13px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    white-space: nowrap;
    text-align: center;
    min-width: 140px;
    min-height: var(--min-touch);
    transition: background 0.1s, color 0.15s, border-color 0.15s;
}

.admin-action-btn:hover:not(:disabled) {
    background: var(--accent-bg);
    border-color: var(--accent-border);
    color: var(--accent);
}

.admin-action-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Save button gets the accent treatment up-front so it reads as the
   primary affordance in the inline editor's action row. */
.admin-action-btn-primary {
    background: var(--accent-bg);
    border-color: var(--accent-border);
    color: var(--accent);
}

.admin-action-btn-primary:hover:not(:disabled) {
    background: var(--accent);
    color: #fff;
    border-color: var(--accent);
}

/* Destructive action — Phase 3 erasure button. Red tone keeps the
   irreversible action visually distinct from the Edit/Promote/Revoke row
   buttons. Same shape as .admin-action-btn so the row aligns. */
.admin-action-btn-danger {
    background: rgba(220, 50, 50, 0.10);
    border-color: rgba(220, 50, 50, 0.45);
    color: #ff9898;
}

.admin-action-btn-danger:hover:not(:disabled) {
    background: rgba(220, 50, 50, 0.22);
    border-color: rgba(220, 50, 50, 0.7);
    color: #ffb8b8;
}

/* Phase 3 erasure confirmation modal. Backdrop dims the page; the inner
   card holds the title + body + type-to-confirm input + action row. The
   click handler on the backdrop closes the modal; @onclick:stopPropagation
   on the inner card prevents inside-clicks from bubbling. */
.admin-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    z-index: 9000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
}

.admin-modal {
    background: var(--bg-panel, #1a1a1a);
    border: 1px solid var(--border);
    border-radius: 10px;
    max-width: 540px;
    width: 100%;
    padding: 24px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
    color: var(--text-primary);
}

.admin-modal-title {
    margin: 0 0 12px;
    font-size: 18px;
    color: #ff9898;
}

.admin-modal-body {
    margin: 0 0 12px;
    font-size: 13px;
    color: var(--text-muted);
    line-height: 1.5;
}

.admin-modal-body code {
    background: var(--bg-input);
    padding: 2px 6px;
    border-radius: 4px;
    color: var(--text-primary);
}

.admin-modal-input {
    width: 100%;
    padding: 9px 12px;
    font-size: 14px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text-primary);
    margin: 8px 0 12px;
    box-sizing: border-box;
}

.admin-modal-error {
    margin: 0 0 12px;
    padding: 10px 12px;
    border-radius: 6px;
    background: rgba(220, 50, 50, 0.12);
    border: 1px solid rgba(220, 50, 50, 0.4);
    color: #ff9898;
    font-size: 12px;
}

.admin-modal-success {
    margin: 0 0 12px;
    padding: 10px 12px;
    border-radius: 6px;
    background: rgba(50, 180, 80, 0.10);
    border: 1px solid rgba(50, 180, 80, 0.4);
    color: #bef0c4;
    font-size: 12px;
}

.admin-modal-actions {
    display: flex;
    gap: 10px;
    justify-content: flex-end;
    margin-top: 8px;
}

/* Page-level subnote: small dim hint text above the table. Used for the
   "changes take effect on next sign-in" caveat. */
.admin-subnote {
    margin: 0 0 14px;
    color: var(--text-dim);
    font-size: 12px;
    font-style: italic;
}

/* Phase 2 admin filter bar — email substring + status dropdown above the
   users table. Wraps on narrow viewports to avoid the controls overflowing. */
.admin-filter-bar {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: end;
    margin: 0 0 16px;
}

.admin-filter-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.admin-filter-label {
    font-size: 12px;
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.admin-filter-input {
    padding: 6px 10px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text-primary);
    font-size: 13px;
    min-width: 200px;
}

.admin-filter-clear {
    height: 32px;
    align-self: end;
}

.admin-stripe-customer code {
    font-size: 11px;
    color: var(--text-muted);
    background: var(--bg-panel);
    padding: 2px 6px;
    border-radius: 4px;
    white-space: nowrap;
}

/* ── Inline editor row (OverrideSubscription) ────────────────────────────── */

.admin-edit-row > td {
    /* Flush the editor card against the parent row — visually a continuation
       rather than a new table row. The accent left-stripe ties the editor to
       the in-progress edit and signals "this is the row you just clicked on"
       even after scrolling the parent row off-screen. The colspan + padding
       pattern is standard for "expandable detail" tables. */
    padding: 0;
    background: var(--bg-card);
    border-top: none;
    border-left: 3px solid var(--accent);
}

/* Editor card: vertical stack with a clear "Edit subscription" heading,
   labelled-row form fields, and right-aligned action buttons at the
   bottom. The 480px max-width on the form keeps it readable rather than
   stretching field controls across a wide table. */
.admin-edit-form {
    padding: 16px 14px 14px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 540px;
}

.admin-edit-heading {
    margin: 0 0 6px;
    font-size: 13px;
    font-weight: 600;
    color: var(--text-primary);
    /* Slight lowering of contrast vs the page-level h1 — this is a
       subform header, not a page header. */
    opacity: 0.85;
}

/* Each field is one horizontal row: [label] [input grows] [Clear].
   220px label width keeps labels left-aligned across the three date rows
   so inputs visually line up vertically. */
.admin-edit-field {
    display: grid;
    grid-template-columns: 160px 1fr auto;
    align-items: center;
    gap: 12px;
}

.admin-edit-field label {
    font-size: 12px;
    color: var(--text-muted);
    font-weight: 500;
}

.admin-edit-input {
    padding: 6px 8px;
    font-size: 13px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text-primary);
    /* Native date pickers in dark themes look awful in some browsers;
       color-scheme: dark hints the platform to invert the calendar UI. */
    color-scheme: dark;
    /* Cap input width so dates don't stretch to fill the entire form
       width — ~180px is enough for "dd/mm/yyyy" and the picker glyph. */
    max-width: 220px;
    width: 100%;
}

.admin-edit-input:focus {
    outline: none;
    border-color: var(--accent-border);
    background: var(--bg-panel);
}

.admin-edit-input:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/* Native <select> chevron on dark theme renders as a default OS glyph (often
   a black arrow against our dark input bg) and the dropped list inherits
   light-mode option styling on some browsers. Reset appearance + draw a
   themed chevron via inline SVG; explicitly theme options for the rare
   browser that doesn't honour color-scheme on the option list. Applies to
   both the inline editor's Status / Plan selects and the page-top filter
   bar's status dropdown. */
select.admin-edit-input,
select.admin-filter-input {
    appearance: none;
    -webkit-appearance: none;
    -moz-appearance: none;
    padding-right: 30px;
    background-image: url("data:image/svg+xml;charset=UTF-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23a8a4c1'%3E%3Cpath d='M3 6l5 5 5-5z'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 8px center;
    background-size: 12px;
    cursor: pointer;
}

select.admin-edit-input option,
select.admin-filter-input option {
    background: var(--bg-panel);
    color: var(--text-primary);
}

/* Clear toggle sits inline to the right of the input, label-style. */
.admin-edit-clear {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 11px;
    color: var(--text-muted);
    font-weight: 400;
    cursor: pointer;
    user-select: none;
}

.admin-edit-clear input[type="checkbox"] {
    cursor: pointer;
    margin: 0;
}

/* Status field is the only non-date field; reuses the same grid columns
   so the dropdown's left edge aligns with the date inputs below it. The
   third grid column is empty (no Clear toggle) — the auto-sized track
   collapses to zero so spacing stays consistent. */
.admin-edit-field-status .admin-edit-input {
    max-width: 220px;
}

.admin-edit-help {
    margin: 4px 0 0;
    color: var(--text-dim);
    font-size: 11px;
    font-style: italic;
    /* Indent to align with the input column so the help reads as
       belonging to the form group rather than the page. */
    padding-left: 172px;
}

.admin-edit-actions {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-top: 6px;
}

/* Action buttons inside the editor get their min-width inherited from
   .admin-action-btn — already wide enough for "Save" / "Cancel" labels. */

/* Mobile: stack labels above inputs so the form doesn't crush into a
   single column with truncated labels. */
@media (max-width: 600px) {
    .admin-edit-field {
        grid-template-columns: 1fr;
        gap: 4px;
    }
    .admin-edit-field label {
        font-size: 11px;
        text-transform: uppercase;
        letter-spacing: 0.5px;
        color: var(--text-dim);
    }
    .admin-edit-help {
        padding-left: 0;
    }
}

/* "Currently using XXX" hint to the right of the storage-quota input.
   Mirrors the muted label style — same column slot the Clear checkbox
   uses on the date rows, so the form maintains a consistent grid rhythm. */
.admin-edit-meta {
    font-size: 11px;
    color: var(--text-dim);
    white-space: nowrap;
}

@media (max-width: 600px) {
    .admin-edit-meta {
        white-space: normal;
    }
}

/* ── Image-storage quota panel (Account Settings page) ─────────────────────
   Lives inside an .account-section so spacing/typography inherit. The
   progress bar is a slim track + fill — fill colour swaps to a warning
   tone when usage crosses 90% so the user sees the cliff coming. */
.storage-quota {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.storage-quota-bar {
    height: 8px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 4px;
    overflow: hidden;
}

.storage-quota-fill {
    height: 100%;
    background: var(--accent);
    transition: width 0.25s ease;
}

.storage-quota-fill.near-limit {
    background: #c0392b;
}

.storage-quota-summary {
    margin: 0;
    font-size: 13px;
    color: var(--text-primary);
}

.storage-quota-percent {
    color: var(--text-dim);
    margin-left: 4px;
}

.storage-quota-meta {
    margin: 0;
    font-size: 12px;
    color: var(--text-muted);
}

.storage-quota-upgrade {
    margin-top: 4px;
    align-self: flex-start;
    font-size: 12px;
    color: var(--accent);
    text-decoration: none;
}

.storage-quota-upgrade:hover {
    text-decoration: underline;
}

/* Inline storage hint that sits above the SfUploader on the concert-edit
   form. Single muted line — the rich panel lives on /account/settings. */
.concert-image-quota-hint {
    font-size: 12px;
    color: var(--text-dim);
    margin-bottom: 6px;
}

/* Pager — minimal centred pair of buttons + count */
.admin-pager {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 12px;
    padding: 16px 12px 0;
}

.admin-pager-btn {
    padding: 6px 12px;
    font-size: 12px;
    background: var(--bg-input);
    border: 1px solid var(--border);
    border-radius: 6px;
    color: var(--text-muted);
    cursor: pointer;
    transition: background 0.1s, color 0.15s;
}

.admin-pager-btn:hover:not(:disabled) {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.admin-pager-btn:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

.admin-pager-info {
    color: var(--text-dim);
    font-size: 12px;
}

/* ── Stats grid ─────────────────────────────────────────────────────────── */

.admin-stat-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
    gap: 12px;
    margin-bottom: 18px;
}

/* Conversion cards are wider than the by-status cards because they carry a
   "PR5 placeholder" footnote — the wider min-width prevents wrap on the note. */
.admin-stat-grid-conversions {
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.admin-stat-card {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 14px 16px;
}

.admin-stat-label {
    font-size: 11px;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.5px;
    margin-bottom: 6px;
}

.admin-stat-value {
    font-size: 28px;
    font-weight: 600;
    color: var(--text-primary);
    line-height: 1.1;
}

.admin-stat-note {
    font-size: 11px;
    color: var(--text-dim);
    margin-top: 8px;
    font-style: italic;
}

/* ── Mobile ─────────────────────────────────────────────────────────────── */

@media (max-width: 767px) {
    .admin-page {
        padding: 16px 8px 32px;
    }
    .admin-header {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .admin-tabs {
        justify-content: stretch;
    }
    .admin-tab {
        flex: 1;
        text-align: center;
    }
    /* The table itself keeps min-width:720px and the wrapper scrolls.
       Reducing cell padding tightens it slightly so less swiping is needed. */
    .admin-users-table th,
    .admin-users-table td {
        padding: 10px 10px;
    }
    .admin-emails {
        max-width: 200px;
    }
    .admin-stat-value {
        font-size: 22px;
    }
}

/* ─── Biography Page ─────────────────────────────────────────────────────── */
.biog-page {
    padding: 24px 28px 40px;
    overflow-y: auto;
    height: 100%;
}

.biog-title {
    font-size: 22px;
    font-weight: 500;
    color: var(--text-primary);
    margin: 0 0 4px;
}

.biog-subtitle {
    font-size: 13px;
    color: var(--text-dim);
    margin-bottom: 18px;
}

.biog-divider {
    border: none;
    border-top: 1px solid var(--border);
    margin: 10px 0;
}

.biog-section-title {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 10px;
}

.biog-empty {
    font-size: 12px;
    color: var(--text-dim);
    padding: 8px 0;
}

/* Hero metric cards. auto-fit + minmax keeps the grid responsive — collapses
   from 6 columns at desktop width down to 2 columns at mobile width. */
/* Non-mobile: fixed 4-column grid. With 6 single-width cards + the
   double-width Venues card (grid-column: span 2) that's exactly two full
   rows of four — row 1 holds cards 1–4, row 2 holds cards 5, 6 and the
   span-2 Venues card. auto-fit packed 6 across on wide screens and orphaned
   Venues onto a row of its own; a hard 4-col cap keeps the rows balanced.
   minmax(0, 1fr) lets the cells shrink rather than overflow as the viewport
   narrows toward the 767px mobile breakpoint. */
.biog-metrics {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px;
    margin-bottom: 10px;
}

/* Wrapper around a single .biog-metric-card when a Hotspot needs to anchor to
   that card. The <a> can't be the host (button-in-anchor swallows the click);
   the wrapper takes the grid cell, the card fills it, and the hotspot is
   positioned absolute against the wrapper. Only used on the first card today. */
.biog-metric-card-host {
    position: relative;
    display: block;
}
.biog-metric-card-host > .biog-metric-card { display: block; }

/* Drillable stat cards (Pattern D). Cards with a natural in-app destination
   render as <a class="biog-metric-card-drillable"> — they navigate on click
   and show a chevron in the subtitle. Non-drillable cards stay as plain
   divs with the same shell. */
.biog-metric-card {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 10px;
    padding: 16px 18px;
    transition: border-color 0.15s, transform 0.15s, background 0.15s;
    text-decoration: none;
    color: inherit;
    display: block;
}

a.biog-metric-card-drillable {
    cursor: pointer;
}

a.biog-metric-card-drillable:hover {
    border-color: var(--accent-border);
    background: var(--accent-bg);
    transform: translateY(-1px);
}

a.biog-metric-card-drillable:hover .biog-metric-value {
    color: var(--accent-light);
}

a.biog-metric-card-drillable .biog-metric-sub span {
    color: var(--accent);
    font-weight: 600;
}

.biog-metric-label {
    font-size: 12px;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    margin-bottom: 6px;
    font-weight: 500;
}

.biog-metric-value {
    font-size: 30px;
    font-weight: 500;
    color: var(--text-primary);
    line-height: 1.1;
}

/* Loading placeholder for the server-backed stat cards (tracks heard live, venues
   visited) — shown on first load instead of a fake "0". Reuses the activity-spin
   keyframe defined for the activity pill. Sized to sit on the value row so the card
   height doesn't jump when the real number arrives. */
.biog-metric-spinner {
    width: 22px;
    height: 22px;
    margin: 4px 0;
    border: 2px solid var(--border-med);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: activity-spin 700ms linear infinite;
}
@media (prefers-reduced-motion: reduce) {
    .biog-metric-spinner { animation-duration: 1400ms; }
}

.biog-metric-sub {
    font-size: 12px;
    color: var(--text-dim);
    margin-top: 4px;
}

/* Two-column layout: timeline + format chart. Stacks on mobile (handled
   by the responsive override below). */
.biog-columns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 10px;
}

.biog-panel {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 16px 18px;
    min-height: 180px;
}

/* All time / Last 10 years segmented toggle. Matches the .nav-btn family
   in styling so the affordance is familiar to anyone who's used the app. */
.biog-toggle {
    display: inline-flex;
    border: 1px solid var(--border-med);
    border-radius: 6px;
    overflow: hidden;
    margin-bottom: 16px;
}

.biog-toggle-btn {
    background: none;
    border: 0;
    color: var(--text-muted);
    font-size: 13px;
    padding: 8px 16px;
    cursor: pointer;
    min-height: var(--min-touch);
}

.biog-toggle-btn.active {
    background: var(--accent-bg);
    color: var(--accent-light);
}

/* Concert timeline. Pure CSS — dots are absolutely positioned along a
   horizontal line by year-percentage. multi-year dots get a slightly
   brighter shade so a busy year stands out at a glance. */
.biog-timeline-axis {
    position: relative;
    height: 24px;
    margin-top: 12px;
    margin-bottom: 4px;
}

.biog-timeline-line {
    position: absolute;
    top: 11px;
    left: 0;
    right: 0;
    height: 2px;
    background: rgba(255,255,255,0.1);
    border-radius: 1px;
}

.biog-timeline-dot {
    position: absolute;
    top: -4px;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--accent);
    transform: translateX(-50%);
    cursor: pointer;
}

.biog-timeline-dot.multi {
    background: var(--accent-light);
    width: 12px;
    height: 12px;
    top: -5px;
    box-shadow: 0 0 0 2px rgba(139,132,232,0.25);
}

/* Active state for mobile tap-to-show tooltip — gives clear feedback that the
   tap registered on a dot the user can't see clearly through their thumb. */
.biog-timeline-dot.active {
    box-shadow: 0 0 0 3px rgba(139,132,232,0.45);
}

/* Mobile tap tooltip. Viewport-anchored (position: fixed) and centred so it
   can't clip off-screen regardless of which dot was tapped or how far the
   page is scrolled. A long list of concerts in one year scrolls inside the
   tooltip body rather than overflowing the viewport. */
.biog-timeline-tooltip {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: min(360px, calc(100vw - 32px));
    max-height: min(70vh, 480px);
    display: flex;
    flex-direction: column;
    background: var(--bg-panel);
    border: 1px solid var(--border-med);
    border-radius: 10px;
    box-shadow: 0 12px 32px rgba(0, 0, 0, 0.55);
    padding: 14px 16px 12px;
    z-index: 270;
}

.biog-timeline-tooltip-year {
    font-size: 13px;
    font-weight: 600;
    color: var(--accent-light);
    text-transform: uppercase;
    letter-spacing: 0.06em;
    margin-bottom: 10px;
    padding-right: 28px;
}

.biog-timeline-tooltip-list {
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
}

.biog-timeline-tooltip-line {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 6px 0;
    border-top: 1px solid var(--border);
}

.biog-timeline-tooltip-line:first-of-type { border-top: none; padding-top: 0; }

.biog-timeline-tooltip-artist {
    font-size: 14px;
    color: var(--text-secondary);
    line-height: 1.35;
}

.biog-timeline-tooltip-meta {
    font-size: 12px;
    color: var(--text-dim);
    line-height: 1.35;
}

.biog-timeline-tooltip-close {
    position: absolute;
    top: 8px;
    right: 8px;
    background: none;
    border: 0;
    color: var(--text-dim);
    font-size: 24px;
    line-height: 1;
    padding: 8px;
    cursor: pointer;
    border-radius: 6px;
    min-width: var(--min-touch);
    min-height: var(--min-touch);
    display: inline-flex;
    align-items: center;
    justify-content: center;
}

.biog-timeline-tooltip-close:hover { color: var(--text-primary); background: var(--bg-hover); }

/* Dim backdrop — clarifies that the tooltip is modal and captures taps to
   dismiss. Sits just below the tooltip in z-stack. */
.biog-timeline-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.55);
    z-index: 265;
}

.biog-timeline-years {
    position: relative;
    height: 16px;
    margin-bottom: 10px;
}

.biog-timeline-year {
    position: absolute;
    transform: translateX(-50%);
    font-size: 12px;
    color: var(--text-dim);
}

.biog-timeline-year.active { color: var(--text-secondary); }

.biog-timeline-pills {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 8px;
}

.biog-pill {
    font-size: 13px;
    color: var(--text-muted);
    background: var(--bg-hover);
    padding: 5px 10px;
    border-radius: 5px;
}

.biog-pill strong {
    color: var(--text-primary);
    font-weight: 500;
}

/* Collection by format bar chart. ramp-1 → ramp-4 give a darker-to-lighter
   purple progression, matching the order the rows are rendered in (CD,
   Vinyl, Blu-ray, Other). */
.biog-format-row {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}

.biog-format-label {
    font-size: 13px;
    color: var(--text-secondary);
    width: 70px;
    flex-shrink: 0;
}

.biog-format-track {
    flex: 1;
    height: 10px;
    background: rgba(255,255,255,0.06);
    border-radius: 5px;
    overflow: hidden;
}

.biog-format-fill {
    height: 100%;
    /* Floor the rendered width so a 1- or 2-release format still produces a
       visible chip; without this a tiny percentage collapses to a hairline. */
    min-width: 16px;
    border-radius: 5px;
    transition: width 0.35s ease-out;
}

.biog-format-fill.ramp-1 { background: var(--accent); }
.biog-format-fill.ramp-2 { background: #7b73d4; }
.biog-format-fill.ramp-3 { background: var(--accent-light); }
.biog-format-fill.ramp-4 { background: #d6d2f5; }

.biog-format-count {
    font-size: 13px;
    color: var(--text-muted);
    width: 44px;
    text-align: right;
    flex-shrink: 0;
}

/* Artist ranking table. Plain rows with a thin separator instead of a real
   <table> — keeps the markup simpler and avoids fighting Bootstrap defaults. */
.biog-ranking { margin-bottom: 22px; }

.biog-ranking-subtext {
    font-size: 12px;
    color: var(--text-dim);
    margin-top: -4px;
    margin-bottom: 10px;
    font-style: italic;
}

.biog-ranking-legend {
    font-size: 13px;
    color: var(--text-dim);
    margin-bottom: 14px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}

.biog-ranking-row {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 0;
    border-bottom: 1px solid var(--border);
    min-height: 38px;
}

/* Anchor variant — entire row is clickable, navigates to artist page. */
a.biog-ranking-row-link {
    text-decoration: none;
    color: inherit;
    padding: 10px 8px;
    border-radius: 6px;
    transition: background 0.12s;
}

a.biog-ranking-row-link:hover {
    background: var(--bg-hover);
    color: var(--accent-light);
}

.biog-ranking-chevron {
    color: var(--text-dim);
    font-size: 18px;
    margin-left: auto;
}

a.biog-ranking-row-link:hover .biog-ranking-chevron { color: var(--accent); }

.biog-ranking-rank {
    font-size: 13px;
    color: var(--text-dim);
    width: 24px;
    flex-shrink: 0;
}

.biog-ranking-name {
    font-size: 14px;
    color: var(--text-secondary);
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Badge variants. Pattern G fix (Phase 5a): green is reserved for the
   "owned" semantic only. The "both" core-artist state is the active filter
   state, so it uses purple/accent — same as everywhere else in the app. */
.biog-badge {
    font-size: 11px;
    font-weight: 500;
    padding: 3px 9px;
    border-radius: 4px;
    flex-shrink: 0;
}

.biog-badge-both {
    background: var(--accent-bg);
    color: var(--accent-light);
    border: 1px solid var(--accent-border);
}

.biog-badge-releases {
    background: var(--accent-bg);
    color: var(--accent-light);
}

.biog-badge-concerts {
    background: var(--teal-bg);
    color: var(--teal-light);
    border: 1px solid var(--teal-border);
}

/* Mobile responsive overrides for the biography page. */
@media (max-width: 767px) {
    .biog-page { padding: 18px 14px 28px; }
    .biog-columns { grid-template-columns: 1fr; }
    .biog-metrics { grid-template-columns: repeat(2, 1fr); gap: 10px; }
    .biog-metric-value { font-size: 24px; }
    .biog-title { font-size: 22px; }
}

/* ─── Library view toggle (PR1) ──────────────────────────────────────────── */
/* Sits in the .pane-mid panel header next to the page title; switches between
   the existing AlbumRow rendering (List), the CoverWall grid (Wall), and the
   StackView drag-to-flick carousel (Stack, added in PR2). */

.panel-title-row {
    display: flex;
    /* Column layout: view-toggle pills sit on top, artist name below. Row layout
       truncated the artist name at narrow widths where the toggle pills won the
       horizontal budget; stacking gives each its natural width on every breakpoint. */
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
    margin-bottom: 6px;
}
/* The base .panel-title has its own margin-bottom for the count-badge layout
   used in the Artists sidebar. When wrapped in .panel-title-row the row owns
   the spacing, so zero out the inner margin to avoid stacking. */
.panel-title-row .panel-title {
    margin-bottom: 0;
}

/* Segmented control. The pill outline lives on the parent; the button cells
   share a single contiguous tap surface (no gap, no parent padding) so taps
   never land in dead zones between buttons. */
.view-toggle {
    display: inline-flex;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 0;
    gap: 0;
    overflow: hidden;
}

.vt-btn {
    border: 0;
    background: transparent;
    color: var(--text-muted);
    font-size: 13px;
    font-weight: 500;
    padding: 6px 16px;
    border-radius: 999px;
    cursor: pointer;
    line-height: 1.4;
    min-height: 32px;
    transition: color 0.12s, background 0.12s;
}

.vt-btn + .vt-btn {
    border-left: 1px solid var(--border);
}

.vt-btn:hover:not(.disabled):not(.on) {
    color: var(--text-primary);
    background: var(--bg-hover);
}

.vt-btn.on {
    background: var(--accent);
    color: #ffffff;
}

/* Strip the divider where the active pill butts up against a neighbour — the
   filled button doesn't need a separator. */
.vt-btn.on + .vt-btn,
.vt-btn.on { border-left: 0; }

.vt-btn.disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

/* ─── Cover Wall (PR1) ───────────────────────────────────────────────────── */
/* Fixed 3 columns at desktop in the existing 260px .pane-mid (so column 2 stays
   the same width across List/Wall/Bin — view-mode mustn't reshape the layout
   around it). 3 small tiles per row deliberately leans into "scan a lot of
   covers at once" rather than "showcase one big cover". Mobile media query
   below drops to 2 columns since the pane is full-viewport-width there. */
.cover-wall {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 10px;
    padding: 10px;
    align-content: start;
}

.cover-wall-item {
    /* Native button reset so the cover sits flush. */
    border: 0;
    background: transparent;
    padding: 0;
    margin: 0;
    color: inherit;
    text-align: inherit;
    font: inherit;
    cursor: pointer;
    display: flex;
    flex-direction: column;
    gap: 6px;
    min-width: 0;
}

.cover-wall-cover {
    position: relative;
    aspect-ratio: 1;
    border-radius: 5px;
    overflow: hidden;
    background: var(--bg-panel);
    box-shadow: 0 2px 6px rgba(0,0,0,0.3);
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.cover-wall-item:hover .cover-wall-cover {
    transform: translateY(-2px);
    box-shadow: 0 8px 18px rgba(0,0,0,0.5);
}

/* Selection: outline wrapping the entire .cover-wall-item (cover + caption),
   plus a small lift and accent-coloured glow on the cover, plus a bold
   accent-light title. Earlier rounds put the ring on .cover-wall-cover only —
   that competed visually with the hover shadow and was easy to miss. Wrapping
   the whole button reads unambiguously even when scrolled past the cover. */
.cover-wall-item {
    border-radius: 5px;
}

/* Selection: 2px accent ring on the cover + small lift + accent-light bold
   title. Ring is on .cover-wall-cover only (not the outer button) so it
   wraps just the artwork, not the caption text below. */
.cover-wall-item.selected .cover-wall-cover {
    transform: translateY(-2px);
    box-shadow:
        0 0 0 2px var(--accent),
        0 8px 22px rgba(139, 132, 232, 0.45);
}

.cover-wall-item.selected .cwc-title {
    color: var(--accent-light);
    font-weight: 700;
}

/* Suppress the browser's default focus outline on click — it leaves a ring
   wrapping the whole button that overlaps the selection treatment and
   confuses the visual hierarchy. :focus-visible (keyboard navigation) keeps
   a clear ring so the page is still keyboard-accessible. */
.cover-wall-item:focus {
    outline: none;
}

.cover-wall-item:focus-visible {
    outline: 2px solid var(--accent-light);
    outline-offset: 2px;
}

/* Small green corner dot to match the AlbumRow "in your library" indicator —
   without it the Wall loses the at-a-glance owned signal that List has. */
.cover-wall-owned-dot {
    position: absolute;
    top: 6px;
    right: 6px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--owned);
    box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
    z-index: 3;
}

/* Caption meta line: flex so the meta text + AlbumType badge + mixed-media
   badge can wrap together. Title above is single-line ellipsis; meta is
   allowed to wrap to a second row if both meta and badges are long. */
.cwc-meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 6px;
    margin-top: 1px;
    font-size: 11px;
    color: var(--text-dim);
    line-height: 1.3;
}

.cwc-meta-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

/* Overlay layout: fallback sits behind the img (both absolute, inset:0).
   On a 404 the img sets display:none and the gradient title-card is revealed.
   When Thumbnail is null/empty the <img> isn't rendered at all, so the fallback
   shows directly. This avoids the nextElementSibling fragility — there's no
   sibling-walking, just z-order. */
.cover-wall-cover img,
.cover-wall-cover .cover-wall-fallback {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.cover-wall-cover img {
    object-fit: cover;
    z-index: 2;
    display: block;
}

.cover-wall-fallback {
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px;
    text-align: center;
    font-size: 0.78rem;
    font-weight: 500;
    color: var(--accent-light);
    background: linear-gradient(135deg, var(--bg-panel), var(--accent-bg));
    /* Long titles mustn't overflow the cover square. */
    overflow: hidden;
    word-break: break-word;
    line-height: 1.3;
}

.cover-wall-caption {
    min-width: 0;
}

.cwc-title {
    font-size: 12px;
    color: var(--text-primary);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    line-height: 1.3;
}

.cover-wall-item.not-owned .cwc-title {
    color: var(--text-muted);
    font-style: italic;
}

/* Mobile (≤767px): 3 columns at full-viewport-width feels right — matches
   the user's mental model from the original v1 mobile view, and gives a
   tile-density that reads as "scan a lot at once" rather than "showcase one". */
@media (max-width: 767px) {
    .cover-wall {
        grid-template-columns: repeat(3, 1fr);
        gap: 8px;
        padding: 10px;
    }
}

/* ─── Rail-collapsed mode for Wall / Stack views (desktop/tablet only, ≥768px) ─── */
/* When the user toggles to Wall or Stack, the Artists rail compacts to
   avatars-only (64px) and .pane-mid widens so cover-driven views get
   breathing room beyond the 260px text-row default. List view keeps the
   historical 240+260+flex layout. Mobile media query overrides both pane
   widths to 100% regardless, so this only affects ≥768px.

   Layout key: when the rail is *expanded* (hover or manual chevron click),
   the visible content is positioned absolute INSIDE .pane-sidebar so it
   overlays .pane-mid rather than pushing it out of the flex flow. The
   outer .pane-sidebar keeps its 64px slot in the layout regardless of
   expanded state. The .rail-content wrapper is what grows from 64 → 240. */

/* Chevron is hidden by default (List mode has nothing to collapse). The
   rail-collapsed cascade below re-shows it. */
.rail-toggle {
    display: none;
}

@media (min-width: 768px) {
    .view-3pane.rail-collapsed .pane-sidebar {
        /* 76px = 40px avatar + 6px left margin + 8px clear gap + 22px
           letter-jump strip. 64px was too tight; the avatars and letter-jump
           buttons overlapped. */
        width: 76px;
        /* Anchor the absolute-positioned .rail-content child + allow overflow
           when the content grows past the collapsed width. */
        position: relative;
        overflow: visible;
        /* Drop the base .pane-sidebar background/border-right onto .rail-content
           instead so the floating overlay carries them when expanded. */
        background: transparent;
        border-right: none;
    }
    .view-3pane.rail-collapsed .pane-mid {
        width: 480px;
    }

    /* The .rail-content wrapper carries the visible rail surface (background,
       border, scroll, content). Default state: anchored to the collapsed slot. */
    .view-3pane.rail-collapsed .rail-content {
        position: absolute;
        left: 0;
        top: 0;
        bottom: 0;
        width: 76px;
        background: var(--bg-sidebar);
        border-right: 1px solid var(--border);
        display: flex;
        flex-direction: column;
        overflow: hidden;
        z-index: 50;
        transition: width 0.25s ease, box-shadow 0.25s ease;
    }

    /* Panel header stays visible in rail-collapsed mode — it's the row that
       carries the rail-toggle chevron (and, when expanded, the "Artists /
       N albums" title text on the same line). Becomes a flex row so the
       title and chevron sit side-by-side. */
    .view-3pane.rail-collapsed .rail-content .panel-header {
        display: flex;
        align-items: center;
        gap: 8px;
        padding: 8px 10px;
    }

    /* Chevron toggle button — sits at the right of the panel-header line.
       Touch users tap it to expand; mouse users get the same effect via
       :hover. Default state (collapsed-collapsed): just the chevron, no
       label text — there's no room at 64px. Expanded state adds a "Collapse"
       label (CSS ::before, see expanded-state cascade below). */
    .view-3pane.rail-collapsed .rail-toggle {
        display: inline-flex;
        align-items: center;
        height: 24px;
        padding: 0 6px;
        margin-left: auto;
        border-radius: 6px;
        background: var(--bg-card);
        border: 1px solid var(--border-med);
        color: var(--text-dim);
        font-size: 11px;
        font-weight: 500;
        line-height: 1;
        cursor: pointer;
        flex-shrink: 0;
        transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
    }
    .view-3pane.rail-collapsed .rail-toggle:hover {
        background: var(--bg-hover);
        border-color: var(--accent-border);
        color: var(--text-primary);
    }
    /* Chevron icon — wrapped in its own span so we can rotate just the icon
       (not the optional "Collapse" label that sits beside it via ::before).
       Default: rotated 180° so the literal ‹ glyph shows as › for "expand". */
    .view-3pane.rail-collapsed .rail-toggle-icon {
        display: inline-block;
        transform: rotate(180deg);
        transition: transform 0.25s ease;
        font-size: 14px;
        line-height: 1;
    }

    /* Title hidden in collapsed-collapsed (no room beside the chevron at 64px). */
    .view-3pane.rail-collapsed .rail-content .panel-title {
        display: none;
    }

    /* Other content not relevant in collapsed-collapsed. Section labels stay
       visible in BOTH states — without them, expanding the rail would push
       every artist row down by the section-label height, and mid-click on
       an avatar would miss its target as the avatar shifted under the cursor. */
    .view-3pane.rail-collapsed .rail-content .filter-input-row,
    .view-3pane.rail-collapsed .rail-content .artist-info,
    .view-3pane.rail-collapsed .rail-content .artist-count {
        display: none;
    }

    /* Compact section labels for the 64px collapsed rail — centred letter,
       reduced horizontal padding. The expanded-state cascade further down
       restores the default left-aligned styling. */
    .view-3pane.rail-collapsed .rail-content .section-label {
        padding: 8px 4px 2px;
        text-align: center;
    }
    .view-3pane.rail-collapsed .rail-content .artist-row {
        /* Left-anchored with a small breathing margin so the avatars don't
           kiss the rail's left edge; flex-start instead of center because
           the letter-jump strip eats ~22px on the right and centering looked
           offset left as a result. */
        justify-content: flex-start;
        padding: 6px 4px 6px 6px;
        border-left: 0;
    }
    /* Active-artist indicator switches from the .artist-row left-border highlight
       (which doesn't read at this width) to a 2px accent ring around the avatar. */
    .view-3pane.rail-collapsed .rail-content .artist-row.active {
        background: transparent;
    }
    .view-3pane.rail-collapsed .rail-content .artist-row.active .artist-avatar {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
    }

    /* ── Manual-expanded state (works on all devices including pure touch) ── */
    /* Triggered by the user clicking the chevron — the .rail-expanded class is
       set by Blazor regardless of pointer type. No intent delay; click is an
       explicit commit. */
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content {
        width: 240px;
        box-shadow: 12px 0 28px rgba(0, 0, 0, 0.55);
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .panel-title {
        display: flex;
        align-items: center;
        flex: 1;
        margin-bottom: 0;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .filter-input-row {
        display: flex;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .section-label {
        padding: 10px 14px 3px;
        text-align: left;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .artist-info {
        display: flex;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .artist-count {
        display: inline;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .artist-row {
        justify-content: flex-start;
        padding: 7px 14px;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .artist-row.active {
        background: var(--bg-active);
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content .artist-row.active .artist-avatar {
        outline: none;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-toggle::before {
        content: "Collapse";
        margin-right: 6px;
    }
    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-toggle-icon {
        transform: rotate(0deg);
    }
}

/* ── Hover-expand state (mouse-style devices only) ──────────────────────── */
/* Gated behind @media (hover: hover) so true touch devices and DevTools
   touch emulation skip the :hover cascade entirely. Without this gate, the
   :hover rule fires intermittently during a tap (synthesised mouse events on
   touch) and produces flicker / "click does nothing" symptoms — pointer
   events in C# can't fully defeat that because some event orderings put
   :hover-active right at the moment the click handler runs. Excluding the
   rule entirely on touch is more robust than wrangling state. */
@media (min-width: 768px) and (hover: hover) {
    /* 150ms intent delay before the rail starts expanding so cursors that
       just graze the rail edge don't trigger an unwanted pop-out. Collapse
       path uses the base transition (no delay) so leaving the rail feels
       instant. */
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content {
        width: 240px;
        box-shadow: 12px 0 28px rgba(0, 0, 0, 0.55);
        transition-delay: 0.15s;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .panel-title {
        display: flex;
        align-items: center;
        flex: 1;
        margin-bottom: 0;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .filter-input-row {
        display: flex;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .section-label {
        padding: 10px 14px 3px;
        text-align: left;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .artist-info {
        display: flex;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .artist-count {
        display: inline;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .artist-row {
        justify-content: flex-start;
        padding: 7px 14px;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .artist-row.active {
        background: var(--bg-active);
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-content .artist-row.active .artist-avatar {
        outline: none;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-toggle::before {
        content: "Collapse";
        margin-right: 6px;
    }
    .view-3pane.rail-collapsed .pane-sidebar:not(.rail-suppress-hover):hover .rail-toggle-icon {
        transform: rotate(0deg);
    }

    /* Smooth the layout shift when toggling between view modes. */
    .pane-sidebar,
    .pane-mid {
        transition: width 0.25s ease;
    }
}

/* ─── Record Stack (PR2) ────────────────────────────────────────────────── */
/* Three-layer DOM:
   .stack-frame      — flex-column container, fills the .list-scroll area, centres
                     the stage and the browse-info / controls beneath it
   .stack-stage      — fixed 260×260 box with perspective + 3D transform context;
                     JS populates this with up to 5 absolutely-positioned sleeve
                     <div>s on init / setAlbums
   .stack-sleeve     — individual album sleeve; JS sets transform/opacity inline
                     per stack position. Front sleeve (data-stack="0") receives
                     pointer events; the rest are visually behind */
.stack-frame {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100%;
    padding: 20px 16px;
    user-select: none;
}

.stack-stage {
    position: relative;
    width: 260px;
    height: 260px;
    perspective: 1000px;
    transform-style: preserve-3d;
    /* touch-action: pan-y allows vertical scroll of the parent .list-scroll
       while still capturing horizontal drags for the flick gesture. */
    touch-action: pan-y;
    margin-bottom: 20px;
}

.stack-sleeve {
    position: absolute;
    inset: 0;
    border-radius: 4px;
    overflow: hidden;
    background: var(--bg-panel);
    box-shadow: 0 14px 30px rgba(0, 0, 0, 0.55), 0 0 0 1px rgba(0, 0, 0, 0.4);
    cursor: grab;
    transition: transform 0.4s cubic-bezier(0.25, 0.85, 0.3, 1), opacity 0.3s ease;
    will-change: transform, opacity;
}

.stack-sleeve.grabbing {
    cursor: grabbing;
    /* Drop the transition while the user is actively dragging — the JS sets
       transform on every pointermove, and a CSS transition would lag the
       sleeve a frame behind the cursor. The transition resumes on pointerup
       when the class is removed. */
    transition: none;
}

/* Cover image and gradient fallback are both absolute inset:0; img on top
   (z-index: 2), fallback behind (z-index: 1). On 404 the JS-set onerror
   collapses the img to opacity:0 so the gradient bleeds through. Same
   overlay pattern Cover Wall uses. */
.stack-sleeve img,
.stack-sleeve .stack-sleeve-fallback {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
}

.stack-sleeve img {
    object-fit: cover;
    z-index: 2;
    display: block;
    /* User shouldn't be able to drag the cover image as a separate
       drag-image — that would interfere with the flick gesture. */
    -webkit-user-drag: none;
    pointer-events: none;
}

.stack-sleeve-fallback {
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 16px;
    text-align: center;
    font-size: 0.95rem;
    font-weight: 500;
    color: var(--accent-light);
    background: linear-gradient(135deg, var(--bg-panel), var(--accent-bg));
    word-break: break-word;
    line-height: 1.3;
}

/* Browse-info: title + meta line + position counter ("3 / 12"). JS sets
   textContent on each on every render. */
.stack-info {
    text-align: center;
    margin-bottom: 12px;
    min-height: 60px;
    max-width: 320px;
}

.stack-info-title {
    font-size: 14px;
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 4px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.stack-info-meta {
    font-size: 12px;
    color: var(--text-dim);
}

.stack-info-pos {
    font-size: 12px;
    color: var(--text-dim);
    margin-top: 5px;
    letter-spacing: 0.5px;
    opacity: 0.8;
}

.stack-controls {
    display: flex;
    gap: 14px;
    margin-bottom: 10px;
}

.stack-prev,
.stack-next {
    width: var(--min-touch);
    height: var(--min-touch);
    border-radius: 50%;
    background: var(--bg-card);
    border: 1px solid var(--border-med);
    color: var(--text-primary);
    cursor: pointer;
    font-size: 18px;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: background 0.15s ease, border-color 0.15s ease;
}

.stack-prev:hover,
.stack-next:hover {
    background: var(--bg-hover);
    border-color: var(--accent-border);
}

.stack-hint {
    font-size: 12px;
    color: var(--text-dim);
    letter-spacing: 0.4px;
    opacity: 0.7;
    text-align: center;
}

/* Phone-narrow viewports: 260px sleeves are tight in a ~360px viewport once
   the .stack-frame's 16px padding × 2 is taken out. min(260, 70vw) keeps them
   at the demo's intended size on tablet+ but scales down on small phones. */
@media (max-width: 380px) {
    .stack-stage {
        width: min(260px, 70vw);
        height: min(260px, 70vw);
    }
}


/* ─── Venue picker (Phase A of the venue feature) ──────────────────────────
   Custom Blazor dropdown anchored beneath the Venue input. The parent
   .form-field carries position:relative inline so absolute positioning
   here lands directly under the input regardless of surrounding layout.
   Z-index above standard form chrome but below modals (matches the
   admin-modal-backdrop precedent at z-index 1000+). */
.venue-picker-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 2px;
    background: var(--bg-card, #1c1a30);
    border: 1px solid var(--border, #2a2647);
    border-radius: 6px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
    z-index: 50;
    max-height: 320px;
    overflow-y: auto;
}

.venue-picker-row {
    padding: 9px 12px;
    cursor: pointer;
    border-bottom: 1px solid var(--border-soft, #211d3c);
    transition: background 0.1s;
}

.venue-picker-row:last-child {
    border-bottom: none;
}

.venue-picker-row:hover {
    background: var(--accent-soft, rgba(167, 139, 250, 0.12));
}

.venue-picker-name {
    font-size: 13px;
    color: var(--text, #e8e6f5);
    font-weight: 500;
    line-height: 1.3;
}

.venue-picker-sub {
    font-size: 11px;
    color: var(--text-dim, #8a86b0);
    margin-top: 2px;
    line-height: 1.3;
}

.venue-picker-use-as-typed {
    font-size: 12px;
    color: var(--text-dim, #8a86b0);
    background: rgba(255, 255, 255, 0.02);
    font-style: italic;
}

.venue-picker-use-as-typed strong {
    color: var(--text, #e8e6f5);
    font-style: normal;
}

/* Phase 2 local-first picker — "Add new venue" affordance. Subtly purple-
   tinted so it reads as a positive action ("escalate to Geoapify") rather
   than a passive option like Use-as-typed. */
.venue-picker-add-new {
    font-size: 12px;
    color: var(--accent, #a78bfa);
    background: rgba(167, 139, 250, 0.06);
}

.venue-picker-add-new strong {
    color: var(--text, #e8e6f5);
}

/* ─── Track picker (Phase 2 polish) ─────────────────────────────────────────
   Custom Blazor dropdown replacing the native <datalist> on the concert-edit
   track-add row. Same visual shape as the venue picker — positioned absolute
   beneath the input, padded rows, hover highlight — so the two pickers
   inside the same form feel consistent. */
.track-picker-row {
    display: flex;
    gap: 6px;
    align-items: center;
}

.track-picker-input-wrap {
    position: relative;
    flex: 1;
}

.track-picker-input {
    width: 100%;
    font-size: 12px;
    padding: 6px 10px;
}

.track-picker-dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    margin-top: 2px;
    background: var(--bg-card, #1c1a30);
    border: 1px solid var(--border, #2a2647);
    border-radius: 6px;
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.35);
    z-index: 50;
    max-height: 280px;
    overflow-y: auto;
}

.track-picker-suggestion {
    padding: 8px 12px;
    cursor: pointer;
    border-bottom: 1px solid var(--border-soft, #211d3c);
    transition: background 0.1s;
    font-size: 13px;
    color: var(--text, #e8e6f5);
    line-height: 1.3;
}

.track-picker-suggestion:last-child {
    border-bottom: none;
}

.track-picker-suggestion:hover {
    background: var(--accent-soft, rgba(167, 139, 250, 0.12));
}

.track-picker-encore {
    font-size: 11px;
    color: var(--text-dim, #8a86b0);
    display: flex;
    align-items: center;
    gap: 4px;
    white-space: nowrap;
}

/* ─── Venue picker — linked state (canonical-venue chip) ────────────────────
   Replaces the old subtle ✓-text-+-× treatment. Per requirements §4.6 the
   chip must read as a "thing" rather than a "field" — a clear card with the
   venue name + sub-line + a prominent action button. The Change button is
   text-labelled and ≥44×44 tap-target on mobile (small icons fail accessibility
   on touch). */
.venue-linked-chip {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    background: var(--accent-soft, rgba(167, 139, 250, 0.10));
    border: 1px solid var(--accent-dim, #7c6fd6);
    border-left: 3px solid var(--accent, #a78bfa);
    border-radius: 6px;
    min-height: 56px;
}

.venue-linked-info {
    flex: 1;
    min-width: 0;
}

.venue-linked-name {
    font-size: 14px;
    color: var(--text, #e8e6f5);
    font-weight: 600;
    line-height: 1.3;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.venue-linked-sub {
    font-size: 12px;
    color: var(--text-dim, #8a86b0);
    margin-top: 2px;
    line-height: 1.3;
}

.venue-unlink-btn {
    flex-shrink: 0;
    background: var(--bg-elev, #1f1c36);
    color: var(--accent, #a78bfa);
    border: 1px solid var(--accent-dim, #7c6fd6);
    border-radius: 5px;
    padding: 8px 14px;
    cursor: pointer;
    font-size: 12px;
    font-weight: 500;
    font-family: inherit;
    min-height: 36px;
    transition: background 0.15s, color 0.15s;
}

.venue-unlink-btn:hover {
    background: var(--accent, #a78bfa);
    color: var(--accent-on, #0e0d1a);
}

/* Read-only city display in linked state. Visually distinct from an
   editable input — no border, muted text, no hover affordance. */
.form-input-readonly {
    padding: 8px 12px;
    color: var(--text-dim, #8a86b0);
    font-size: 14px;
    min-height: 36px;
    display: flex;
    align-items: center;
    background: transparent;
}

/* Mobile: stack the unlink button below the info instead of beside it,
   keep tap targets large (≥44px). */
@media (max-width: 600px) {
    .venue-linked-chip {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    .venue-unlink-btn {
        min-height: 44px;
        width: 100%;
    }
}

/* Concert detail wrapper. Desktop fixes height to the parent column with
   internal flex scrolling on the .detail-content tab body. On mobile the
   parent column doesn't have a fixed height (the page scrolls vertically
   end-to-end), so height:100% collapses to 0 and overflow:hidden then
   clips the entire detail view. Auto-height + visible overflow on mobile
   matches the .concert-edit-shell pattern (existing precedent). */
.concert-detail-shell {
    position: relative;
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}
@media (max-width: 767px) {
    .concert-detail-shell {
        height: auto;
        overflow: visible;
    }
}

/* ==========================================================================
   Phase B — Venue card + Venue page + photo attribution
   ========================================================================== */

/* Visible Wikimedia Commons photo attribution caption. Required by CC BY-SA
   on every surface that shows a photo (licensing readout §3.2 / §3.3). */
.photo-attribution {
    display: block;
    font-size: 11px;
    line-height: 1.4;
    color: var(--text-dim, #9aa);
    padding: 6px 10px;
}
.photo-attribution-sep {
    margin: 0 4px;
    color: var(--text-very-dim, #677);
}
.photo-attribution-licence,
.photo-attribution-source {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: rgba(255, 255, 255, 0.25);
    text-underline-offset: 2px;
}
.photo-attribution-licence:hover,
.photo-attribution-source:hover {
    text-decoration-color: currentColor;
}

/* Two-column grid wrapping ArtistCard | VenueCard on Concert detail.
   Stacks on mobile. Sits inside the Details tab content. */
.concert-detail-cards {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin: 0 0 16px 0;
}
@media (max-width: 720px) {
    .concert-detail-cards {
        grid-template-columns: 1fr;
        gap: 12px;
    }
}

/* Shared card surface (subtle border + dark fill, no shadow — matches the
   detail-page tab content's existing flat styling). */
.artist-card,
.venue-card {
    background: rgba(255, 255, 255, 0.025);
    border: 1px solid rgba(255, 255, 255, 0.08);
    border-radius: 12px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
}

/* ── ArtistCard (hero layout to match VenueCard's photo treatment) ── */
.artist-card-link {
    display: flex;
    flex-direction: column;
    color: inherit;
    text-decoration: none;
}
.artist-card-link:hover .artist-card-name {
    color: var(--accent, #8b6dff);
}
.artist-card-hero {
    width: 100%;
    aspect-ratio: 16 / 9;
    background: rgba(255, 255, 255, 0.05);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}
/* Foreground image: shows the full photo (object-fit: contain) so band
   shots and portrait orientations don't get heads cropped off. Sits on
   top of the blurred backdrop. */
.artist-card-hero-img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}
/* Blurred + cover-scaled copy of the same image — fills the empty space
   left around the contained foreground image. Reads as a stylised
   backdrop rather than the harsh letterbox bars that would otherwise
   appear when the photo's aspect ratio doesn't match the 16:9 frame. */
.artist-card-hero-backdrop {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    filter: blur(24px) brightness(0.6);
    /* Scale up slightly so the blur's soft edge doesn't show the
       original image bounds inside the frame. */
    transform: scale(1.15);
}
.artist-card-hero-initials {
    font-size: 96px;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.08);
    letter-spacing: 4px;
}
.artist-card-body {
    padding: 14px 16px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.artist-card-name {
    font-size: 17px;
    font-weight: 600;
    color: var(--text, #fff);
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
}
.artist-card-name-chevron {
    font-size: 16px;
    opacity: 0.6;
}
.artist-card-support {
    font-size: 12px;
    font-weight: 400;
    color: var(--text-dim, #9aa);
    margin-left: 4px;
}
.artist-card-tracks {
    font-size: 13px;
    color: var(--text-dim, #9aa);
}
.artist-card-stat {
    font-size: 13px;
    color: var(--accent, #8b6dff);
    margin-top: 2px;
}
.artist-card-stat strong {
    font-weight: 600;
}

/* ── VenueCard ── */
.venue-card--linked .venue-card-photo {
    width: 100%;
    aspect-ratio: 16 / 9;
    background: rgba(255, 255, 255, 0.05);
    overflow: hidden;
    display: flex;
    align-items: center;
    justify-content: center;
}
/* When the photo slot is filled by a map (no Wikidata photo available),
   the Leaflet canvas must size itself to the 16:9 aspect-ratio container
   above. The wrapper class is the same .venue-card-photo so the slot
   height stays consistent regardless of photo / map fallback. */
.venue-card-photo--map {
    position: relative;
}
.venue-card-map-canvas {
    /* Leaflet's .leaflet-container has its own background; clear ours so
       the map tiles fill cleanly without the parent's neutral fill
       showing through edge pixels. */
    background: transparent;
}

/* ── Venue page (/venues/{id}) ──────────────────────────────────────────── */
/* The .app-body wrapper claims viewport height with overflow:hidden, so this
   page owns its own internal scroll. Breadcrumb pinned (flex-shrink:0);
   .venue-page-scroll takes the remaining height with overflow-y:auto. Same
   pattern as ArtistDetailPage's .artist-page-layout. */
.venue-page-layout {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}
.venue-page-scroll {
    flex: 1;
    overflow-y: auto;
    overflow-x: hidden;
    /* Centred-content rail; the breadcrumb above sits flush to the page
       edges because it shares the breadcrumb-bar styling, but the rest of
       the page content stays in a comfortable reading width. */
    padding: 16px 24px 40px;
    display: flex;
    flex-direction: column;
    gap: 24px;
}
.venue-page-scroll > * {
    max-width: 1100px;
    width: 100%;
    margin-left: auto;
    margin-right: auto;
    /* flex-shrink: 1 (default) inside a column flex collapses fixed-height
       children (.venue-page-hero with height: 240px) to 0. Pin to 0 so
       intrinsic / fixed heights are respected and the parent's
       overflow-y:auto provides scroll space when content exceeds the
       viewport. */
    flex-shrink: 0;
}

.venue-page-breadcrumb {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: 13px;
    color: var(--text-dim, #9aa);
    padding: 0 24px;
    height: 40px;
    flex-shrink: 0;
    background: var(--bg-panel, rgba(255,255,255,0.02));
    border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
/* Reuse the existing .breadcrumb-back-btn / .breadcrumb-sep / .breadcrumb-current
   styles from ArtistDetailPage's pattern — they're defined globally and the
   tags here pick them up without duplication. */

/* Compact hero — capped at a fixed pixel height so the concerts-list section
   below remains visible on laptop viewports without requiring a scroll. The
   previous aspect-ratio: 21/9 sized the band to ~470px on a 1100px-wide
   layout, which crowded out the concerts list. */
.venue-page-hero {
    position: relative;
    width: 100%;
    height: 240px;
    background: rgba(255, 255, 255, 0.04);
    border-radius: 16px;
    overflow: hidden;
}
@media (max-width: 600px) {
    .venue-page-hero {
        height: 180px;
        border-radius: 12px;
    }
}
.venue-page-hero-img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.venue-page-hero .photo-attribution {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    background: linear-gradient(0deg, rgba(0,0,0,0.6), rgba(0,0,0,0));
    padding: 24px 16px 10px;
    color: rgba(255,255,255,0.9);
}
.venue-page-hero .photo-attribution-sep,
.venue-page-hero .photo-attribution-licence,
.venue-page-hero .photo-attribution-source {
    color: rgba(255,255,255,0.85);
}

.venue-page-info {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 24px;
    align-items: start;
}
@media (max-width: 900px) {
    .venue-page-info {
        grid-template-columns: 1fr;
    }
}

.venue-page-info-text {
    display: flex;
    flex-direction: column;
    gap: 10px;
}
.venue-page-name {
    font-size: 28px;
    font-weight: 700;
    color: var(--text, #fff);
    margin: 0;
    letter-spacing: -0.4px;
}
.venue-page-address {
    font-size: 15px;
    color: var(--text-dim, #9aa);
}
.venue-page-aliases {
    font-size: 13px;
    color: var(--text-dim, #9aa);
}
.venue-page-aliases-label {
    font-weight: 600;
    margin-right: 4px;
}

.venue-page-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 4px;
}
.venue-page-chip {
    display: inline-block;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 4px 10px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-dim, #9aa);
    border: 1px solid rgba(255, 255, 255, 0.12);
}
.venue-page-chip--closed {
    background: rgba(255, 176, 0, 0.12);
    color: #ffb000;
    border-color: rgba(255, 176, 0, 0.3);
}

/* Concert-list inline "Closed YYYY" pill. Smaller than the venue-card
   variant — it shares the row's vertical space with the location text and
   any setlist.fm pill, so the compact treatment keeps the row from
   wrapping unnecessarily. */
.concert-row-closed-badge {
    display: inline-flex;
    align-items: center;
    margin-left: 8px;
    padding: 1px 7px;
    border-radius: 8px;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    background: rgba(255, 176, 0, 0.12);
    color: #ffb000;
    border: 1px solid rgba(255, 176, 0, 0.3);
    vertical-align: middle;
}

.venue-page-stats {
    display: flex;
    gap: 24px;
    margin-top: 16px;
    flex-wrap: wrap;
}
.venue-page-stat {
    display: flex;
    flex-direction: column;
    gap: 2px;
}
.venue-page-stat-value {
    font-size: 22px;
    font-weight: 700;
    color: var(--accent, #8b6dff);
    line-height: 1;
}
.venue-page-stat-label {
    font-size: 11px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--text-dim, #9aa);
}

.venue-page-info-map {
    width: 100%;
    aspect-ratio: 4 / 3;
    border-radius: 12px;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.04);
}
.venue-page-map-canvas {
    background: transparent;
}

.venue-page-concerts {
    display: flex;
    flex-direction: column;
    gap: 12px;
}
.venue-page-section-title {
    font-size: 18px;
    font-weight: 700;
    color: var(--text, #fff);
    margin: 0;
}
.venue-page-concerts-list {
    display: flex;
    flex-direction: column;
    border-radius: 12px;
    overflow: hidden;
    border: 1px solid rgba(255, 255, 255, 0.08);
}
/* Concert row: [thumb] [date+artist link, flex] [tracks pill + photos pill].
   The .venue-page-concert-link wraps the navigation surface; the photos
   pill sits outside that anchor so its click doesn't bubble to a /concerts
   navigation (the pill opens the lightbox inline instead). */
.venue-page-concert-row {
    display: grid;
    grid-template-columns: 56px 1fr auto;
    gap: 14px;
    align-items: center;
    padding: 10px 14px;
    background: rgba(255, 255, 255, 0.02);
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.venue-page-concert-row:last-child {
    border-bottom: none;
}
.venue-page-concert-row:hover {
    background: rgba(255, 255, 255, 0.05);
}

/* Multi-act linked-group bracket — same accent bar pattern as the main
   /concerts list. Margin-left + left padding shift the row contents
   right; the 3px border-left renders as the accent bar. First / last
   members of the group get rounded corners + a small margin gap so two
   adjacent groups don't appear merged into one. */
.venue-page-concert-row.linked-group-row {
    border-left: 3px solid var(--accent);
    margin-left: 4px;
    padding-left: 11px;
    position: relative;
}
.venue-page-concert-row.linked-group-first {
    margin-top: 4px;
    border-top-left-radius: 4px;
}
.venue-page-concert-row.linked-group-last {
    margin-bottom: 4px;
    border-bottom-left-radius: 4px;
}
/* Override the global .linked-group-tag's flex-start alignment that
   assumes a flex-column parent. Venue-page rows are grid; the pill
   anchors to the top-right corner via absolute positioning so it doesn't
   distort the row's column track sizing. */
.venue-page-linked-group-tag {
    position: absolute;
    top: -10px;
    right: 12px;
    align-self: auto;
}

.venue-page-concert-thumb {
    width: 56px;
    height: 56px;
    border-radius: 8px;
    object-fit: cover;
    background: rgba(255, 255, 255, 0.06);
}
.venue-page-concert-thumb--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-dim, #9aa);
    font-size: 22px;
}

.venue-page-concert-link {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    color: inherit;
    text-decoration: none;
}
.venue-page-concert-date {
    font-size: 12px;
    color: var(--text-dim, #9aa);
    font-variant-numeric: tabular-nums;
}
.venue-page-concert-artist {
    font-size: 15px;
    font-weight: 600;
    color: var(--text, #fff);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
.venue-page-concert-link:hover .venue-page-concert-artist {
    color: var(--accent, #8b6dff);
}

.venue-page-concert-meta {
    display: flex;
    align-items: center;
    gap: 10px;
}
.venue-page-concert-tracks {
    font-size: 12px;
    color: var(--text-dim, #9aa);
}
.venue-page-concert-photos-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 3px 9px;
    border-radius: 10px;
    font-size: 12px;
    font-weight: 600;
    background: rgba(139, 109, 255, 0.12);
    color: var(--accent, #8b6dff);
    border: 1px solid rgba(139, 109, 255, 0.3);
    cursor: pointer;
    /* Default <button> reset so the pill reads as a chip not a form
       control. */
    font-family: inherit;
    line-height: 1.2;
}
.venue-page-concert-photos-pill:hover {
    background: rgba(139, 109, 255, 0.18);
}

@media (max-width: 600px) {
    .venue-page-concert-row {
        grid-template-columns: 44px 1fr auto;
        gap: 10px;
        padding: 10px 12px;
    }
    .venue-page-concert-thumb {
        width: 44px;
        height: 44px;
    }
    .venue-page-concert-tracks {
        display: none;
    }
}
.venue-card-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}
.venue-card-visits strong {
    font-weight: 600;
}
.venue-card-body {
    padding: 14px 16px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.venue-card-name {
    font-size: 17px;
    font-weight: 600;
    color: var(--text, #fff);
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 6px;
}
.venue-card-name:hover {
    color: var(--accent, #8b6dff);
}
.venue-card-name--free-text {
    /* Free-text venues aren't clickable — render as static text. */
    color: var(--text-dim, #9aa);
}
.venue-card-name-chevron {
    font-size: 16px;
    opacity: 0.6;
}
.venue-card-address {
    font-size: 13px;
    color: var(--text-dim, #9aa);
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
}
.venue-card-address-country {
    font-size: 11px;
    padding: 1px 5px;
    border: 1px solid rgba(255, 255, 255, 0.15);
    border-radius: 3px;
    color: var(--text-very-dim, #677);
}
.venue-card-address--missing {
    font-style: italic;
}
.venue-card-closed-badge {
    display: inline-block;
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 3px 8px;
    border-radius: 10px;
    background: rgba(255, 176, 0, 0.12);
    color: #ffb000;
    border: 1px solid rgba(255, 176, 0, 0.3);
    align-self: flex-start;
    margin-top: 4px;
}
.venue-card-visits {
    font-size: 13px;
    color: var(--accent, #8b6dff);
    margin-top: 2px;
}
.venue-card-unverified-chip {
    display: inline-block;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 3px 8px;
    border-radius: 10px;
    background: rgba(255, 255, 255, 0.06);
    color: var(--text-dim, #9aa);
    border: 1px solid rgba(255, 255, 255, 0.12);
    margin: 14px 16px 0 16px;
    align-self: flex-start;
}

/* =========================================================================
   Phase C — Biography venues map (stat card C1 + full-page C2).
   ========================================================================= */

/* 7th stat card on /biog, per requirements §4.4. Spans TWO grid columns
   (i.e. twice the width of a peer stat card) — the grid behaves "as if
   there are 8 cards rather than 7" producing clean wrapping geometry.
   Internal vertical split: text on the left (label + number + subtitle),
   preview map on the right with an accent border. Roughly square each
   half so the text reads like its peers and the map gets proper
   proportions. Mobile collapses to the same shape full-width via the
   media query below. */
.biog-metric-card-venues {
    grid-column: span 2;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: stretch;
    padding: 0;
    overflow: hidden;
}
.biog-metric-venues-text {
    padding: 16px 18px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-width: 0;
}
.biog-metric-venues-map {
    /* Accent-bordered map slot per spec §4.4. The border lives on the
       inner slot rather than the card so the rest of the card keeps the
       neutral stat-card border that peer cards have. */
    border-left: 1px solid var(--accent-border, var(--accent));
    background: rgba(255, 255, 255, 0.03);
    overflow: hidden;
    min-height: 110px;
}
.biog-venues-preview-canvas {
    background: transparent;
}
/* Mobile (§4.4 "2-column grid; Venues card spans full width as a banner
   with map on the right"). span 2 on a 2-col mobile grid = full row;
   keep the side-by-side split so the map stays "on the right side". */
@media (max-width: 720px) {
    .biog-metric-card-venues {
        grid-column: 1 / -1;
    }
    .biog-metric-venues-map {
        min-height: 100px;
    }
}
/* Very narrow phones where even 2 columns is too tight — stack vertically
   so the map gets a usable width rather than slivering at < 50% of an
   already-narrow viewport. */
@media (max-width: 420px) {
    .biog-metric-card-venues {
        grid-template-columns: 1fr;
    }
    .biog-metric-venues-map {
        border-left: none;
        border-top: 1px solid var(--accent-border, var(--accent));
        aspect-ratio: 16 / 9;
    }
}

/* Full-page /biography/venues — same .app-body height shell as the Venue
   page and ArtistDetailPage. Header + filter chips pinned; map + sidebar
   own the scroll inside. */
.biography-venues-layout {
    display: flex;
    flex-direction: column;
    height: 100%;
    overflow: hidden;
}
/* Breadcrumb back to /biog. Mirrors .venue-page-breadcrumb spacing; the
   inner .breadcrumb-back-btn / .breadcrumb-sep / .breadcrumb-current
   classes are global (defined ~line 3309) and pick up the styling without
   duplication. */
.biography-venues-breadcrumb {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 24px 8px;
    flex-shrink: 0;
}
.biography-venues-header {
    flex-shrink: 0;
    padding: 4px 24px 12px;
    border-bottom: 1px solid var(--border);
}
.biography-venues-title {
    font-size: 22px;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 4px;
}
.biography-venues-count {
    font-size: 13px;
    color: var(--text-dim);
    margin-bottom: 12px;
}
.biography-venues-filters {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
}
.biography-venues-chip {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    color: var(--text-primary);
    border-radius: 999px;
    padding: 6px 14px;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.biography-venues-chip:hover {
    border-color: var(--accent-border);
}
.biography-venues-chip.active {
    background: var(--accent-bg, rgba(124, 58, 237, 0.12));
    border-color: var(--accent);
    color: var(--accent-light, #b794f4);
}
.biography-venues-country-select {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    color: var(--text-primary);
    border-radius: 999px;
    padding: 6px 12px;
    font-size: 12px;
}

.biography-venues-body {
    flex: 1;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    overflow: hidden;
}
.biography-venues-map-canvas {
    background: transparent;
}
.biography-venues-map-slot {
    overflow: hidden;
    border-right: 1px solid var(--border);
}
.biography-venues-sidebar {
    overflow-y: auto;
    padding: 12px 18px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}
.biography-venues-sidebar-title {
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin: 4px 0 8px;
}
.biography-venues-sidebar-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 8px 10px;
    border-radius: 8px;
    text-decoration: none;
    color: inherit;
    transition: background 0.15s, color 0.15s;
    gap: 12px;
}
.biography-venues-sidebar-row:hover {
    background: rgba(255, 255, 255, 0.04);
}
/* Mirror the link-affordance pattern used on the /biog stat cards:
   on hover the name shifts to the accent colour so the user can tell
   the row is a navigation target (not an expandable "show me the
   concerts at this venue" disclosure). Chevron also fades up at the
   same time. */
.biography-venues-sidebar-row:hover .biography-venues-sidebar-name {
    color: var(--accent-light, #b794f4);
}
.biography-venues-sidebar-row:hover .biography-venues-sidebar-chevron {
    color: var(--accent);
    transform: translateX(2px);
}
.biography-venues-sidebar-name {
    font-size: 13px;
    color: var(--text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1;
    min-width: 0;
}
.biography-venues-sidebar-name.closed {
    color: var(--text-dim);
}
.biography-venues-sidebar-visits {
    font-size: 11px;
    color: var(--text-dim);
    flex-shrink: 0;
}
/* Chevron disambiguates "this row is a link" from "this row is an
   expand-to-show-concerts disclosure". Static dim colour until hover. */
.biography-venues-sidebar-chevron {
    font-size: 14px;
    color: var(--text-muted, #888);
    flex-shrink: 0;
    transition: color 0.15s, transform 0.15s;
}
.biography-venues-empty {
    padding: 40px 20px;
    text-align: center;
    color: var(--text-dim);
    grid-column: 1 / -1;
}
/* Mobile: the desktop "map + sidebar each own an internal scroll" model
   breaks in a single column. .app-body clips with overflow:hidden, so if the
   map fills the body the By-visits list below it is unreachable — the page
   has no scrollable region. Instead, hand the scroll to the layout itself,
   bound the map's height so it doesn't eat the viewport, and let the
   un-hidden By-visits list flow beneath the map. The user scrolls the page
   to reach it rather than the map swallowing the whole screen. */
@media (max-width: 900px) {
    .biography-venues-layout {
        overflow-y: auto;
    }
    .biography-venues-body {
        grid-template-columns: 1fr;
        flex: 0 0 auto;
        overflow: visible;
    }
    .biography-venues-map-slot {
        border-right: none;
        height: 55vh;
        min-height: 260px;
    }
    .biography-venues-sidebar {
        /* Un-hidden on mobile (was display:none). overflow-y:visible so it
           contributes to the layout's scroll height instead of nesting a
           second scroll region inside the page scroll. */
        overflow-y: visible;
    }
}

/* Leaflet popup contents — emitted by venue-map.js buildPopupNode.
   Note: Leaflet popups have a HARDCODED white background regardless of
   the page's theme, so popup text must use light-mode-friendly colours
   directly rather than the dark-theme --text-primary token (which would
   render white-on-white). */
.venue-map-popup {
    font-family: inherit;
    color: #1f2937;
    display: flex;
    flex-direction: column;
    gap: 4px;
    min-width: 200px;
}
.venue-map-popup-photo {
    width: 100%;
    max-height: 120px;
    object-fit: cover;
    border-radius: 6px;
    margin-bottom: 4px;
}
.venue-map-popup-attribution {
    font-size: 10px;
    color: #6b7280;
    margin-bottom: 4px;
    line-height: 1.3;
}
.venue-map-popup-attribution a {
    color: inherit;
    text-decoration: underline;
}
.venue-map-popup-name {
    font-size: 14px;
    font-weight: 600;
    /* Accent purple on white reads clearly; matches the link-affordance the
       sidebar list uses on the dark side. */
    color: #6d28d9;
    text-decoration: none;
}
.venue-map-popup-name:hover {
    text-decoration: underline;
}
.venue-map-popup-city {
    font-size: 12px;
    color: #6b7280;
}
.venue-map-popup-closed {
    display: inline-block;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 2px 6px;
    border-radius: 4px;
    background: rgba(217, 119, 6, 0.15);
    color: #b45309;
    border: 1px solid rgba(217, 119, 6, 0.4);
    align-self: flex-start;
}
.venue-map-popup-visits {
    font-size: 12px;
    color: #1f2937;
    font-weight: 600;
}



/* ─── Artist Biog tab — stats band + timeline ─────────────────────────────────
   Reuses .detail-tabs (tab strip) and .biog-metric-* (stats cards). The timeline has
   two layouts driven off one model: a year-row grid on wide viewports (members in
   instrument columns with tenure bars; releases left / concerts right) and the vertical
   "constrained" timeline below the breakpoint OR whenever the grid's lanes won't fit. */
.artist-biog-tabs { margin-bottom: 16px; }

/* The Biog tab content is natural-height and page-scrolls; the Overview layout clips with
   internally-scrolling columns, so let the whole layout scroll while the Biog tab is active. */
.artist-page-layout.biog-active { overflow-y: auto; overflow-x: hidden; }

/* Page inset so the band-member / release / concert columns don't bleed to the window edge,
   matching the rest of the artist page. */
.biog-tab-root { padding: 4px 20px 36px; }

.biog-artist-metrics {
    grid-template-columns: repeat(6, minmax(0, 1fr));
    margin: 4px 0 22px;
}

.biog-tl-empty {
    color: var(--text-dim);
    font-size: 14px;
    padding: 28px 4px;
    text-align: center;
}

/* ── Desktop year-row grid ── */
.biog-tl-desktop { display: block; }
.biog-tl-grid {
    display: grid;
    column-gap: 10px;
    align-items: start;
    padding-bottom: 12px;
}
.biog-tlg-head {
    font-size: 11px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--text-dim);
    padding: 0 2px 8px;
    align-self: end;
    border-bottom: 1px solid var(--border-soft);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.biog-tlg-head-right { text-align: right; }
/* Instrument headers centre over their bucket span so a multi-member instrument (e.g. two
   bassists in side-by-side sub-lanes) reads as one group rather than leaving the extra
   sub-lane looking header-less. */
.biog-tlg-head-bucket { text-align: center; }
.biog-tlg-year {
    font-size: 11px;
    color: var(--text-dim);
    padding-top: 2px;
}
.biog-tlg-cell { display: flex; flex-direction: column; gap: 6px; padding: 4px 0; min-width: 0; }
.biog-tlg-note { color: var(--text-dim); font-size: 12px; padding: 12px 4px; align-self: start; }

/* Release card — main-view style (cover + title + year · format). */
.biog-rel-card {
    display: flex;
    gap: 8px;
    align-items: center;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 5px 8px;
    min-width: 0;
}
.biog-rel-cover {
    width: 34px; height: 34px;
    border-radius: 4px; flex-shrink: 0; overflow: hidden;
    display: flex; align-items: center; justify-content: center;
    background: var(--bg-input); color: var(--text-dim);
    font-size: 9px; font-weight: 700; text-transform: uppercase;
}
.biog-rel-cover img { width: 100%; height: 100%; object-fit: cover; }
.biog-rel-info { min-width: 0; }
.biog-rel-title { font-size: 13px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.biog-rel-meta { font-size: 11px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }

/* Member tenure bar — spans its year rows in an instrument-bucket column. Unlike the
   point-event release/concert cells (which sit at the top of their single year row via the
   grid's align-items:start), the bar must STRETCH to fill its whole tenure span vertically. */
.biog-mbar {
    align-self: stretch;
    margin: 1px 2px 2px;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-left: 3px solid var(--border-strong);
    border-radius: 5px;
    padding: 5px 7px;
    overflow: hidden;
    min-width: 0;
}
.biog-mbar-name { font-size: 12px; font-weight: 500; color: var(--text-primary); line-height: 1.2; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.biog-mbar-role { font-size: 10px; color: var(--text-secondary); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.biog-mbar-dates { font-size: 10px; color: var(--text-dim); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* Concert card — gig-row style (year badge + venue + city), right-aligned column. */
.biog-con-card {
    display: flex;
    gap: 8px;
    align-items: center;
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 8px;
    padding: 5px 8px;
    min-width: 0;
}
.biog-con-year {
    font-size: 11px; font-weight: 600; letter-spacing: 0.03em;
    color: var(--accent); background: var(--accent-bg);
    padding: 3px 7px; border-radius: 4px; flex-shrink: 0; white-space: nowrap;
}
.biog-con-info { min-width: 0; }
.biog-con-venue { font-size: 13px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.biog-con-city { font-size: 11px; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 1px; }

/* ── Desktop ↔ vertical swap ──
   Default (≥ breakpoint) = desktop year-row grid. Below the breakpoint the vertical timeline shows.
   A plain viewport media query — predictable across tablets/desktops, and the grid's fr columns
   shrink to fit so it never scrolls sideways. .biog-tl-constrained--always forces the vertical
   layout regardless of width when the lane count exceeds the hard cap (band too wide to lay out). */
.biog-tl-desktop { display: block; }
.biog-tl-constrained { display: none; }
.biog-tl-constrained--always { display: block; }

@media (max-width: 900px) {
    .biog-tl-desktop { display: none; }
    .biog-tl-constrained { display: block; }
    .biog-artist-metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

.biog-tlm-legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 11px; color: var(--text-dim); margin-bottom: 14px; }
.biog-tlm-legend span { display: inline-flex; align-items: center; gap: 5px; }
.biog-tlm-swatch { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }
.biog-tlm-swatch-release { background: var(--accent); }
.biog-tlm-swatch-concert { background: var(--teal); }
.biog-tlm-swatch-era { background: #6f8df0; }

.biog-tlm-yearmark { font-size: 11px; font-weight: 700; color: var(--text-dim); text-align: center; padding: 8px 0 2px; }
.biog-tlm-row { display: grid; grid-template-columns: 1fr 16px 1fr; align-items: stretch; min-height: 30px; }
.biog-tlm-spine { position: relative; }
.biog-tlm-seg { position: absolute; left: 6px; width: 4px; top: 0; bottom: 0; opacity: 0.55; }
.biog-tlm-dot { position: absolute; left: 2px; width: 12px; height: 12px; border-radius: 50%; top: 14px; border: 2px solid var(--bg-base); }
.biog-tlm-left { display: flex; justify-content: flex-end; padding: 5px 0; min-width: 0; }
.biog-tlm-right { padding: 5px 0; min-width: 0; }
.biog-tlm-item { border: 1px solid var(--border); border-radius: 8px; padding: 7px 9px; max-width: 100%; cursor: pointer; background: var(--bg-card); }
.biog-tlm-release { border-left: 3px solid var(--accent-dim, var(--accent)); }
.biog-tlm-concert { border-left: 3px solid var(--teal); }
.biog-tlm-head { display: flex; gap: 8px; align-items: center; }
.biog-tlm-cover { width: 30px; height: 30px; }
.biog-tlm-t { font-size: 12.5px; font-weight: 500; color: var(--text-primary); }
.biog-tlm-m { font-size: 11px; color: var(--text-dim); margin-top: 1px; }
.biog-tlm-hint { font-size: 10px; color: var(--text-very-dim); margin-top: 5px; }
.biog-tlm-reveal { margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--border); }
.biog-tlm-reveal-h { font-size: 10px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); margin-bottom: 4px; }
.biog-tlm-reveal-m { font-size: 11.5px; color: var(--text-secondary); line-height: 1.5; }
.biog-tlm-reveal-role { color: var(--text-very-dim); }
