/* /Components/Layout/LoginDisplay.razor.rz.scp.css */
/* The account menu owns its avatar, panel and items. */

/* <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[b-pcihh61wg8] {
    position: relative;
    display: inline-block;
    margin-right: 6px;
    flex-shrink: 0;
}

.user-menu-toggle[b-pcihh61wg8]::-webkit-details-marker { display: none; }

.user-menu-toggle[b-pcihh61wg8]::marker { content: ""; }

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

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

.user-menu-icon[b-pcihh61wg8] {
    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[b-pcihh61wg8] {
    background: var(--accent);
    border-color: transparent;
    /* Dark-on-accent like .add-hub-btn: white initials at 11px on the accent measure ~3.2:1 against
       the 4.5:1 floor. The pairing is composed at runtime, so no token-level guard sees it. */
    color: var(--bg-base);
    letter-spacing: 0.5px;
    /* Sized like a control, not a caption: this disc IS the account button, sitting beside the
       ~40px Help pill — at the thumb-xs 24px it read as an icon. The size rides the thumb
       primitive's own variable so the disc and its initials stay centred by the primitive. */
    --thumb-size: 32px;
    font-size: var(--text-md);
}

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

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

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

/* Chrome, placement and padding come from .menu + .menu-below-end. What is left
   is this panel's own: it is wider than a command menu because it carries the
   signed-in identity header, and it sits above the drawer layer because the
   navbar it hangs off stays fixed over everything below a dialog. */
.user-menu-panel[b-pcihh61wg8] {
    min-width: 240px;
    z-index: 1000;
}

.user-menu-panel-name[b-pcihh61wg8] {
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text-primary);
    line-height: 1.25;
}

.user-menu-item-icon[b-pcihh61wg8] {
    flex-shrink: 0;
    opacity: 0.75;
}


/* The mobile header is this component's ONLY mobile host: .topnav is display:none below this
   width, and the bottom bar (.mobile-footer) has five slots of its own and never renders the
   account menu. Anything here that is not scoped to .mobile-header therefore styles nothing.
   Must live in THIS sheet rather than app.css: `.mobile-header` is not in this component's markup,
   but Blazor scopes the last element in the selector, so this becomes
   `.mobile-header .user-menu-panel[b-…]` — one class more specific than the global version, which
   only tied and lost on load order. */
@media (max-width: 849px) {
    .mobile-header .user-menu[b-pcihh61wg8] {
        flex: 0 0 auto;
        margin-right: 0;
    }

    /* No padding, and no min-height: here the toggle shows nothing but the avatar, so the button's
       own box is pure chrome around a disc that is already the full target. With the disc at 32px
       the leftover padding and the 32px floor made the hover/open background a visible slab. */
    .mobile-header .user-menu-toggle[b-pcihh61wg8] {
        flex: 0 0 auto;
        padding: 0;
        min-height: 0;
        border: 0;
        background: none;
    }

    .mobile-header .user-menu-panel[b-pcihh61wg8] {
        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[b-pcihh61wg8],
    .mobile-header .user-menu-caret[b-pcihh61wg8] { display: none; }
}

/* 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[b-pcihh61wg8] {
    list-style: none;
}
/* /Components/Layout/MainLayout.razor.rz.scp.css */
.skip-link[b-mffbec9klb] {
    position: absolute;
    top: -100%;
    left: var(--space-3);
    z-index: var(--z-toast);
    padding: var(--space-2) var(--space-4);
    border-radius: 0 0 var(--radius-md) var(--radius-md);
    background: var(--accent);
    color: var(--bg-base);
    font-weight: 600;
    text-decoration: none;
}

.skip-link:focus[b-mffbec9klb] {
    top: 0;
}

.menu-item-body[b-mffbec9klb] {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}

.menu-item-title[b-mffbec9klb] { color: var(--text-primary); }

.menu-item-sub[b-mffbec9klb] {
    color: var(--text-dim);
    font-size: var(--text-sm);
    line-height: 1.35;
}

/* Keeps the menu group heading inset from the popover edge; the type comes from .section-label. */
.menu-header[b-mffbec9klb] {
    padding: var(--space-2) var(--space-3) var(--space-1);
}

/* ─── Top Nav ────────────────────────────────────────────────────────────── */
.topnav[b-mffbec9klb] {
    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[b-mffbec9klb] {
    display: inline-flex;
    align-items: center;
    margin-right: 14px;
    text-decoration: none;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0;
}

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

.nav-spacer[b-mffbec9klb] { flex: 1; }

.add-hub-plus[b-mffbec9klb] {
    font-size: 1.125rem;
    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. */
/* The pair sets --z-popover and the backdrop primitive derives from it. Declared on both: they
   are siblings with no owned parent, and a parent would hand the value to every .menu beneath. */
.add-hub-popover[b-mffbec9klb],
.add-hub-backdrop[b-mffbec9klb] { --z-popover: var(--z-popover-raised); }

.add-hub-popover[b-mffbec9klb] {
    position: fixed;
    top: calc(var(--topnav-height) + 8px);
    left: 80px;
    z-index: var(--z-popover);
    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-item-icon[b-mffbec9klb] {
    --thumb-size: 32px;
    border-radius: var(--radius-lg);
    background: var(--accent-bg);
    border-color: var(--accent-border);
    color: var(--accent-light);
}

.add-hub-centre-plus[b-mffbec9klb] {
    font-size: var(--display-md);
    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-handle[b-mffbec9klb] {
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border-med);
    margin: 6px auto 10px;
}

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

/* Now a <button> rather than the scaffolded <a> (see MainLayout), so it needs the
   button chrome stripped back to what the anchor looked like. */
#blazor-error-ui .dismiss[b-mffbec9klb] {
    cursor: pointer;
    position: absolute;
    right: 0.75rem;
    top: 0.5rem;
    background: none;
    border: 0;
    padding: 0;
    color: inherit;
    font: inherit;
    line-height: 1;
}

/* Maintenance page shown to non-admins while the lock is held. Replaces the app body
   entirely — their library never loaded, so a normal shell would be empty and throw. */
.maintenance-page[b-mffbec9klb] {
    max-width: 560px; margin: 15vh auto; padding: 0 24px; text-align: center;
}

.maintenance-page h1[b-mffbec9klb] { font-size: var(--display-sm); margin: 0 0 12px; color: var(--text-primary); }

.maintenance-page p[b-mffbec9klb] { font-size: var(--text-xl); color: var(--text-secondary); margin: 0 0 8px; }

.maintenance-page-sub[b-mffbec9klb] { font-size: var(--text-md); opacity: 0.75; }

/* Hidden below the mobile-chrome breakpoint. Lives HERE, not in app.css: .topnav's display is set
   in this scoped sheet, so a global hide carries less specificity and never wins. A hide and the
   rule it overrides must share a stylesheet. */
@media (max-width: 849px) {
    .topnav[b-mffbec9klb] { display: none; }
}

/* The Insights chip mirrors Help's shape — same disc, same pill — so the two read as a pair of
   entry points rather than two kinds of control. Its mark is a drawing, not a glyph, so the disc
   holds an image where Help's holds a "?". */
.insights-btn[b-mffbec9klb] {
    position: relative;
}

/* The bar's 4px gap is for borderless links; two bordered pills at 4px read as touching. 8px
   is what the mobile bar already gives the pair (owner, 05/09/2026: "a little close to Help"). */
.topnav .insights-btn[b-mffbec9klb] { margin-right: 4px; }

.insights-btn:hover[b-mffbec9klb] {
    background: var(--bg-hover);
    border-color: var(--accent-border);
}

/* The mark is a complete drawing, not a glyph, so it needs no tinted disc to read as an icon —
   and without one it can be the size the chip actually has room for. Help keeps its disc
   because a bare "?" would not read as a control (owner, 04/09/2026: "we could clearly have a
   much larger icon"). */
.insights-mark[b-mffbec9klb] {
    width: 24px;
    height: 24px;
}

/* Gold, the same as the lit day on the mark beneath it: one idea about what is new, drawn twice. */
.insights-badge[b-mffbec9klb] {
    position: absolute;
    top: -3px;
    right: -3px;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: var(--wanted);
    border: 2px solid var(--bg-nav);
}

.insights-btn.mobile-insights-btn[b-mffbec9klb] {
    height: 36px;
    padding: 0 10px;
    border-radius: 18px;
}

@media (max-width: 849px) {
    .add-hub-popover[b-mffbec9klb],
    .add-hub-backdrop[b-mffbec9klb] { display: none; }
}

/* Hidden by default and revealed inside the ≤849px block below. Both halves MUST live in the same
   stylesheet: a scoped rule carries its component's attribute selector and so out-ranks a global
   media query, which silently pins these hidden at every width. */
.mobile-header[b-mffbec9klb],
.mobile-footer[b-mffbec9klb] { display: none; }

@media (max-width: 849px) {
    .mobile-header[b-mffbec9klb] {
        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[b-mffbec9klb] {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-left: auto;
    }

    .mobile-header-logo[b-mffbec9klb] {
        background: none;
        border: 0;
        padding: 0;
        display: flex;
        align-items: center;
        cursor: pointer;
    }

    /* 34px, where the desktop bar's .nav-logo img is 30px. Scoped, because global it LOST: the
       sibling `.nav-logo img[b-…]` in this same sheet is (0,2,1) against its (0,1,1), so the
       mobile height never applied at all. */
    .mobile-header-logo img[b-mffbec9klb] { height: 34px; width: auto; }

    .mobile-footer[b-mffbec9klb] {
        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[b-mffbec9klb] {
        flex: 1;
        text-align: center;
        font-size: var(--text-sm);
        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;
    }

    .mobile-footer .nav-btn .nav-icon[b-mffbec9klb] {
        font-size: var(--text-2xl);
        line-height: 1;
    }

    .app-body[b-mffbec9klb] {
        flex: 1;
        min-height: 0;
    }
}

.add-hub-btn[b-mffbec9klb] {
    margin-right: var(--space-2);
    background: var(--accent);
    border-color: var(--accent);
    color: var(--bg-base);
    /* Nav size, not the .btn-sm size the class list otherwise imposes: this button sits IN the top
       nav beside Library/Artists/Concerts, which are --text-lg. At --text-sm it read as a different
       class of control from the items it lines up with. */
    font-size: var(--text-lg);
}

/* Matches .btn-primary:hover:not(:disabled) — that selector scores a class plus two pseudos, so a
   bare :hover here loses and the tint wins the fill back on hover. */
.add-hub-btn:hover:not(:disabled)[b-mffbec9klb] {
    background: var(--accent-light);
    border-color: var(--accent-light);
    /* Held explicitly: the generic button hover lightens text for a dark fill, which on this pale
       fill leaves white-on-lavender. */
    color: var(--bg-base);
}

.add-hub-btn:active[b-mffbec9klb] { transform: scale(0.98); }

/* 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[b-mffbec9klb] {
    width: 56px;
    height: 56px;
    border-radius: 50%;
    background: var(--accent);
    color: var(--bg-base);
    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[b-mffbec9klb] { background: var(--accent-light); }

.add-hub-centre-btn:active[b-mffbec9klb] { transform: scale(0.95); }

/* Help button — always visible in chrome. Re-triggers Tier 2 banners and
   Tier 3 hotspots for the current page. It is a .chip; what follows is only the difference. */
.help-btn:hover[b-mffbec9klb] {
    background: var(--bg-hover);
    color: var(--accent-light);
    border-color: var(--accent-border);
}

.help-btn .help-icon[b-mffbec9klb] {
    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: var(--text-sm);
    font-weight: 700;
    line-height: 1;
    flex-shrink: 0;
}

.help-btn .help-label[b-mffbec9klb] { line-height: 1; }

.help-btn.mobile-help-btn .help-label[b-mffbec9klb] {
    display: inline;
    font-size: var(--text-md);
    line-height: 1;
}

.help-btn.mobile-help-btn .help-icon[b-mffbec9klb] {
    width: 22px;
    height: 22px;
    font-size: var(--text-md);
}

/* 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[b-mffbec9klb] {
    height: 36px;
    padding: 0 12px 0 10px;
    border-radius: 18px;
    margin-right: 0;
    align-items: center;
    gap: 6px;
}

.nav-btn.active[b-mffbec9klb] { color: var(--accent-light); background: var(--accent-bg); }

.nav-btn[b-mffbec9klb] {
    position: relative;
    padding: 6px 14px;
    border-radius: 6px;
    font-size: var(--text-lg);
    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[b-mffbec9klb] { color: var(--text-primary); background: var(--bg-hover); }

/* Nav items are <a>, so they announce as links and support open-in-new-tab. The UA underline
   has to come off explicitly; .nav-btn alone would leave it. */
a.nav-btn[b-mffbec9klb] { text-decoration: none; }

/* The visual chip is shorter than --min-touch, so the hit area is supplied here rather than by
   padding. Grow the padding instead and the chip grows with it — that is why this exists. */
.nav-btn[b-mffbec9klb]::after {
    content: '';
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    height: var(--min-touch);
    transform: translateY(-50%);
}

body:has(.landing) .topnav[b-mffbec9klb] {
    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);
}
/* /Components/Pages/AccountSettings.razor.rz.scp.css */
/* The account screen's own: the plan panel, the data rows and the danger zone. */

.account-header h1[b-11hh96pf0a] {
    font-size: var(--display-sm);
    font-weight: 600;
    margin: 0 0 4px;
}

.account-section-header h2[b-11hh96pf0a] {
    font-size: var(--text-lg);
    font-weight: 600;
    margin: 0 0 4px;
}

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

.identity-list[b-11hh96pf0a] {
    list-style: none;
    margin: 0;
    padding: 0;
}

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

.identity-row:first-child[b-11hh96pf0a] {
    border-top: 0;
}

.identity-provider[b-11hh96pf0a] {
    font-size: var(--text-md);
    font-weight: 600;
    margin-bottom: 2px;
}

.identity-detail[b-11hh96pf0a] {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}

.identity-email-missing[b-11hh96pf0a] {
    font-style: italic;
    /* A quiet token, not opacity: 0.7 over already-secondary text multiplied below the contrast
       floor, and opacity dims any future child content with it. */
    color: var(--text-dim);
}

.data-export-error[b-11hh96pf0a] {
    /* 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[b-11hh96pf0a] {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

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

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

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

/* The banner tints its text (success green / error red); the dismiss button follows that tint
   instead of the ghost button's neutral grey, border included. */
.account-banner-dismiss[b-11hh96pf0a] {
    border-color: currentColor;
    color: inherit;
}

.account-legal-list li[b-11hh96pf0a] {
    border-bottom: 1px solid var(--border);
}

.account-legal-list li:last-child[b-11hh96pf0a] {
    border-bottom: none;
}

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

.account-legal-list a[b-11hh96pf0a]::after {
    content: '›';
    color: var(--text-dim);
    font-size: 1.125rem;
    font-weight: 400;
}

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

.account-legal-list a:hover[b-11hh96pf0a]::after,
.account-legal-list a:focus-visible[b-11hh96pf0a]::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[b-11hh96pf0a] {
    border: none;
    margin: 0 0 16px 0;
    padding: 0;
}

.music-service-toggles:last-child[b-11hh96pf0a] {
    margin-bottom: 0;
}

.music-service-group-label[b-11hh96pf0a] {
    font-size: var(--text-md);
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 4px 0;
    padding: 0;
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.music-service-group-hint[b-11hh96pf0a] {
    font-size: var(--text-md);
    color: var(--text-dim);
    margin: 0 0 10px 0;
    line-height: 1.4;
}

.music-service-toggle:last-child[b-11hh96pf0a] {
    border-bottom: none;
}

.music-service-toggle:hover[b-11hh96pf0a] {
    color: var(--accent);
}

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

.storage-quota[b-11hh96pf0a] {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.storage-quota-summary[b-11hh96pf0a] {
    margin: 0;
    font-size: var(--text-md);
    color: var(--text-primary);
}

.storage-quota-percent[b-11hh96pf0a] {
    color: var(--text-dim);
    margin-left: 4px;
}

.storage-quota-meta[b-11hh96pf0a] {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-muted);
}

.storage-quota-upgrade[b-11hh96pf0a] {
    margin-top: 4px;
    align-self: flex-start;
    font-size: var(--text-sm);
    color: var(--accent);
    text-decoration: none;
}

.storage-quota-upgrade:hover[b-11hh96pf0a] {
    text-decoration: underline;
}

/* The panels are plain block siblings, so nothing separates them. A gap on the column keeps
   them apart without every panel having to carry a margin it would also apply as the last one. */
.account-settings[b-11hh96pf0a] {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
}

.meter-fill.is-full[b-11hh96pf0a] { background: var(--danger); }

/* Shape and states are .btn now. The one thing kept: a disabled export is WAITING,
   not forbidden, so the cursor stays `progress` rather than .btn:disabled's
   `not-allowed` — the export can take a while and the pointer is the only feedback. */
.data-export-btn:disabled[b-11hh96pf0a] { cursor: progress; pointer-events: auto; }
/* /Components/Pages/Admin/AdminTabs.razor.rz.scp.css */
/* The admin tab strip. One page carries these, so they live with it rather than in the shared
   sheet — a class with a single consumer is that page's, not vocabulary.

   The whole family is here, base through active through responsive, so their relative weights are
   unchanged — scoping adds an attribute selector to every rule equally. What breaks a split family
   is DIRECTION, not the split: the half that must WIN has to end up later and higher. Moving an
   OVERRIDE out to a scoped sheet is safe, because the bundle loads after app.css; moving a BASE
   out, or putting a modifier in @layer while its base stays unlayered, is not. */

.admin-tabs[b-4wa1539bsz] {
    display: flex;
    gap: 4px;
}

.admin-tab[b-4wa1539bsz],
.admin-tab:visited[b-4wa1539bsz] {
    padding: 6px 12px;
    font-size: var(--text-md);
    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[b-4wa1539bsz],
.admin-tab:visited:hover[b-4wa1539bsz] {
    color: var(--text-primary);
    background: var(--bg-input);
}

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

@media (max-width: 849px) {
    .admin-tabs[b-4wa1539bsz] {
        justify-content: stretch;
    }

    .admin-tab[b-4wa1539bsz] {
        flex: 1;
        text-align: center;
    }
}
/* /Components/Pages/Admin/Backfills.razor.rz.scp.css */
/* The backfills screen owns the job rows and their progress readouts. */

/* Generous bottom gap, deliberately: adjacent backfill cards each own a Run button,
   and visual ambiguity there is a safety problem. */
.admin-card[b-8nnta7uc83] {
    margin: 0 0 20px;
}

.admin-card h2[b-8nnta7uc83] {
    margin: 0 0 8px;
    font-size: var(--text-xl);
    font-weight: 600;
    color: var(--text-primary);
}

/* Flex-grows because the maintenance-lock message is long-form; the fixed-width controls
   beside it in .admin-actions do not. */
.admin-input[b-8nnta7uc83] {
    flex: 1 1 260px;
    min-width: 0;
}
/* /Components/Pages/Admin/Stats.razor.rz.scp.css */
/* The admin stats screen owns the figure grid and its captions. */

.admin-stat-grid[b-edjt6t0owm] {
    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[b-edjt6t0owm] {
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

@media (max-width: 849px) {
    /* Scoped to this grid, not .stat-value globally: the admin cards are six-across and the
       figure has to give way first. Other stat surfaces are not that dense. */
    .admin-stat-grid .stat-value[b-edjt6t0owm] {
        font-size: var(--display-sm);
    }
}
/* /Components/Pages/Admin/UiStyleguide.razor.rz.scp.css */
/* Breakpoint / input readout. Ordered narrowest-LAST so the most specific match wins on cascade
   order rather than on specificity. These bands must equal the @media widths in app.css — G11
   asserts it, because a readout that disagrees with the app reports confidence it hasn't earned. */

.ui-sg-readout[b-uy3nlanwpd] {
    display: flex;
    flex-wrap: wrap;
    gap: 8px 20px;
    margin-bottom: 16px;
    padding: 10px 14px;
    border: 1px dashed var(--border-med);
    border-radius: 6px;
    color: var(--text-muted);
    font-size: var(--text-sm);
}

.ui-sg-readout strong[b-uy3nlanwpd] {
    color: var(--accent-light);
    font-weight: 600;
}

.ui-sg-bp[b-uy3nlanwpd]::after { content: "\2265 1201px \2014 desktop, wide"; }
@media (max-width: 1200px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "901\2013 1200px \2014 desktop, medium"; } }
@media (max-width: 900px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "850\2013 900px \2014 desktop, narrow"; } }
@media (max-width: 849px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "\2264 849px \2014 MOBILE (3 queries)"; } }
@media (max-width: 720px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "\2264 720px"; } }
@media (max-width: 600px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "\2264 600px"; } }
@media (max-width: 480px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "\2264 480px"; } }
@media (max-width: 420px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "\2264 420px"; } }
@media (max-width: 400px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "\2264 400px"; } }
@media (max-width: 380px) { .ui-sg-bp[b-uy3nlanwpd]::after { content: "\2264 380px"; } }

/* Separate from the band label: three queries describe the 849/850 state and move together, so
   "are we mobile" must read as one fact rather than be inferred from a number. */

.ui-sg-mobile[b-uy3nlanwpd]::after { content: "no \2014 multi-pane"; }
@media (max-width: 849px) { .ui-sg-mobile[b-uy3nlanwpd]::after { content: "YES \2014 panes stacked"; } }

/* ─── Pane harness ───────────────────────────────────────────────────────────
   Specimen widths are the floor and ceiling of the real panes in app.css, not numbers
   chosen to look right here — a control shown only at a comfortable width has not been
   tested where it actually has to survive. Move these when the pane clamps move. */

.ui-sg-panes[b-uy3nlanwpd] {
    --sg-pane-narrow: 190px;   /* .pane-sidebar floor */
    --sg-pane-mid:    260px;   /* .pane-mid ceiling */
    --sg-pane-wide:   480px;   /* .rail-collapsed .pane-mid ceiling */
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
}

.ui-sg-pane[b-uy3nlanwpd] {
    flex: 0 0 auto;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--bg-panel);
    overflow: hidden;
}

.ui-sg-pane[b-uy3nlanwpd]::before {
    display: block;
    padding: 4px 8px;
    border-bottom: 1px dashed var(--border-med);
    color: var(--text-dim);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
}

.ui-sg-pane--narrow[b-uy3nlanwpd] { width: var(--sg-pane-narrow); }
.ui-sg-pane--narrow[b-uy3nlanwpd]::before { content: "190px \2014 sidebar floor"; }
.ui-sg-pane--mid[b-uy3nlanwpd] { width: var(--sg-pane-mid); }
.ui-sg-pane--mid[b-uy3nlanwpd]::before { content: "260px \2014 mid pane"; }
.ui-sg-pane--wide[b-uy3nlanwpd] { width: var(--sg-pane-wide); }
.ui-sg-pane--wide[b-uy3nlanwpd]::before { content: "480px \2014 wide pane"; }

/* ─── Focus-ring specimens ───────────────────────────────────────────────────
   Rings forced on so they can be compared side by side. A clipped or missing ring is
   invisible when each control is tabbed to in isolation. */

.ui-sg-rings[b-uy3nlanwpd] {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
}

.ui-sg-rings > *[b-uy3nlanwpd] { outline: 2px solid var(--accent); outline-offset: 2px; }
.ui-sg-rings > .ui-sg-ring-inset[b-uy3nlanwpd] { outline-offset: -3px; }

.ui-sg-ptr[b-uy3nlanwpd]::after { content: "fine (mouse)"; }
@media (pointer: coarse) { .ui-sg-ptr[b-uy3nlanwpd]::after { content: "coarse (touch)"; } }
@media (pointer: none) { .ui-sg-ptr[b-uy3nlanwpd]::after { content: "none"; } }

.ui-sg-hover[b-uy3nlanwpd]::after { content: "yes"; }
@media (hover: none) { .ui-sg-hover[b-uy3nlanwpd]::after { content: "no"; } }

/* A panel to sit the demo rows in, so their dividers and full-bleed hover have an edge to work
   against — a bare row on the page background reads as floating text and tells you nothing about
   how it behaves in a real sidebar. */

.ui-sg-listbox[b-uy3nlanwpd] {
    margin-bottom: 14px;
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--bg-panel);
    overflow: hidden;
}

.ui-sg-h3[b-uy3nlanwpd] {
    margin: 28px 0 4px;
    font-size: var(--text-md);
    font-weight: 600;
    color: var(--text-primary);
}

.ui-sg-fields-demo[b-uy3nlanwpd] {
    max-width: 460px;
    margin-bottom: 16px;
    padding: var(--space-2) var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    background: var(--bg-panel);
}

.ui-sg-why[b-uy3nlanwpd] {
    margin: 8px 0 16px;
    border-left: 2px solid var(--border-med);
    padding-left: 12px;
}

.ui-sg-why > summary[b-uy3nlanwpd] {
    cursor: pointer;
    color: var(--text-dim);
    font-size: var(--text-sm);
    list-style: none;
}

.ui-sg-why > summary[b-uy3nlanwpd]::-webkit-details-marker { display: none; }

.ui-sg-why > summary[b-uy3nlanwpd]::before {
    content: "▸ ";
    color: var(--text-dim);
}

.ui-sg-why[open] > summary[b-uy3nlanwpd]::before { content: "▾ "; }

.ui-sg-why > summary:hover[b-uy3nlanwpd] { color: var(--text-primary); }

.ui-sg-why > summary:focus-visible[b-uy3nlanwpd] {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

/* The one line that must NOT be collapsed: what this section is, and what to do with it. */

.ui-sg-test[b-uy3nlanwpd] {
    margin: 4px 0 12px;
    color: var(--text-secondary);
    font-size: var(--text-lg);
}

.ui-sg-test[b-uy3nlanwpd]::before {
    content: "Test: ";
    color: var(--accent-light);
    font-weight: 600;
}

.ui-sg-tabs[b-uy3nlanwpd] {
    position: sticky;
    top: 0;
    z-index: 5;
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-bottom: 16px;
    padding: 8px 0;
    background: var(--bg-base);
    border-bottom: 1px solid var(--border-med);
}

/* Same neutral backdrop as the tile demos, for the same reason: chip and tag edges are judged
   against the surface behind them, which in the app is --bg-base. */

.ui-sg-demo[b-uy3nlanwpd] {
    max-width: 640px;
    margin-bottom: 16px;
    padding: 12px;
    border-radius: 8px;
    background: var(--bg-base);
}

.ui-sg-blurb kbd[b-uy3nlanwpd] {
    padding: 1px 5px;
    border: 1px solid var(--border-med);
    border-radius: var(--radius-sm);
    background: var(--bg-input);
    color: var(--text-secondary);
    font-family: inherit;
    font-size: var(--text-sm);
}

.ui-sg-note[b-uy3nlanwpd] {
    margin: 0 0 24px;
    padding: 10px 14px;
    border-left: 3px solid var(--accent-border);
    background: var(--bg-hover);
    color: var(--text-muted);
    font-size: var(--text-md);
    line-height: 1.5;
}

/* The proposal is the one thing on this page that is an argument rather than a record, so it is
   marked as such — everything below it is the app as it stands today. */

.ui-sg-proposal[b-uy3nlanwpd] {
    padding: 16px 18px 24px;
    border: 1px solid var(--accent-border);
    border-radius: 8px;
    background: var(--accent-bg);
}

.ui-sg-proposal .detail-tabs .btn-row[b-uy3nlanwpd] {
    margin-block: var(--space-1);
}

.ui-sg-section[b-uy3nlanwpd] {
    margin-bottom: 36px;
    padding-bottom: 28px;
    border-bottom: 1px solid var(--border);
}

.ui-sg-section:last-child[b-uy3nlanwpd] {
    border-bottom: none;
}

.ui-sg-section h2[b-uy3nlanwpd] {
    margin: 0 0 4px;
    font-size: var(--text-xl);
    font-weight: 600;
    color: var(--text-primary);
}

.ui-sg-blurb[b-uy3nlanwpd] {
    margin: 0 0 14px;
    max-width: 68ch;
    color: var(--text-muted);
    font-size: var(--text-md);
    line-height: 1.5;
}
/* /Components/Pages/Admin/Users.razor.rz.scp.css */
/* The admin user list's own: the table, its filters and the status pills. */

/* Outer wrapper provides horizontal scroll on mobile so the table can keep
   its min-width without breaking the page layout. */
.admin-users-table-wrap[b-a02x0e5nad] {
    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;
}

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



.admin-users-table[b-a02x0e5nad] {
    width: 100%;
    min-width: 720px;
    border-collapse: collapse;
    font-size: var(--text-md);
}

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

.admin-users-table td[b-a02x0e5nad] {
    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[b-a02x0e5nad] {
    text-align: right;
    white-space: nowrap;
    font-variant-numeric: tabular-nums;
}

.admin-users-table tbody tr[b-a02x0e5nad] {
    transition: background 0.1s;
}

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

.admin-emails[b-a02x0e5nad] {
    color: var(--text-muted);
    font-size: var(--text-sm);
    word-break: break-all;
    max-width: 280px;
}

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

/* Pending-cancel marker — sits next to the status badge for an Active user
   who has scheduled a cancel at period end. Amber (warning) rather than red:
   the user still has access until expiry, it's not a lapsed state. */
.admin-status-cancelling[b-a02x0e5nad] {
    /* Drop onto its own line below the status pill, hugging its content and
       left-aligned with it — avoids the cramped overlap when the column is
       narrow. */
    display: block;
    width: fit-content;
    margin: 4px 0 0 0;
    text-transform: none;
    background: rgba(184, 120, 32, 0.15);
    color: var(--warn);
    border: 1px solid rgba(184, 120, 32, 0.3);
}

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

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

.admin-filter-bar[b-a02x0e5nad] {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    align-items: end;
    margin: 0 0 16px;
}

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

.admin-filter-label[b-a02x0e5nad] {
    font-size: var(--text-sm);
    color: var(--text-muted);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}

.admin-filter-input[b-a02x0e5nad] {
    min-width: 200px;
}

.admin-filter-clear[b-a02x0e5nad] {
    --btn-height: 32px;
    align-self: end;
}

.admin-stripe-customer code[b-a02x0e5nad] {
    font-size: var(--text-xs);
    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[b-a02x0e5nad] {
    /* 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[b-a02x0e5nad] {
    padding: 16px 14px 14px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    max-width: 540px;
}

/* 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[b-a02x0e5nad] {
    display: grid;
    grid-template-columns: 160px 1fr auto;
    align-items: center;
    gap: 12px;
}

.admin-edit-field label[b-a02x0e5nad] {
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-weight: 500;
}

/* ~180px fits dd/mm/yyyy and the picker glyph; without a cap a date field
   stretches across the whole form. */
.admin-edit-input[b-a02x0e5nad] {
    max-width: 220px;
}

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

/* 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. */



.admin-edit-clear input[type="checkbox"][b-a02x0e5nad] {
    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[b-a02x0e5nad] {
    max-width: 220px;
}

/* Renewal select — wider than Status because "Cancelling at period end" is a
   longer option. No third grid column (help text sits on its own line below),
   so the select fills the 1fr track up to this cap instead of being squeezed. */
.admin-edit-field-cancel .admin-edit-input[b-a02x0e5nad] {
    max-width: 260px;
}

.admin-edit-help[b-a02x0e5nad] {
    margin: 4px 0 0;
    color: var(--text-dim);
    font-size: var(--text-xs);
    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[b-a02x0e5nad] {
    display: flex;
    gap: 8px;
    justify-content: flex-end;
    margin-top: 6px;
}

/* "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[b-a02x0e5nad] {
    white-space: nowrap;
}

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

/* Held in this sheet: these classes are defined here, so the scoped rule carries the component
   attribute selector and outranks a global override regardless of media query. */
@media (max-width: 849px) {
    .admin-emails[b-a02x0e5nad] {
        max-width: 200px;
    }
}

/* 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[b-a02x0e5nad] {
        grid-template-columns: 1fr;
        gap: 4px;
    }

    .admin-edit-field label[b-a02x0e5nad] {
        font-size: var(--text-xs);
        text-transform: uppercase;
        letter-spacing: 0.5px;
        color: var(--text-dim);
    }

    .admin-edit-help[b-a02x0e5nad] {
        padding-left: 0;
    }

    .admin-edit-meta[b-a02x0e5nad] {
        white-space: normal;
    }
}

/* 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[b-a02x0e5nad] {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    justify-content: flex-end;
    align-items: center;
}

@media (max-width: 849px) {
    /* 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[b-a02x0e5nad],
    .admin-users-table td[b-a02x0e5nad] {
        padding: 10px 10px;
    }
}

/* 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-trial[b-a02x0e5nad] {
    background: var(--accent-bg);
    color: var(--accent);
    border: 1px solid var(--accent-border);
}

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

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

.admin-status-cancelled[b-a02x0e5nad],
.admin-status-expired[b-a02x0e5nad] {
    background: var(--danger-bg);
    color: var(--danger);
    border: 1px solid var(--danger-border);
}
/* /Components/Pages/AlbumDetailsComponent.razor.rz.scp.css */
/* This release page's own: the two cover images, the hero's artist/badges/catalogue lines,
   the disc pills, the release notes block, and the collection fields unique to a purchase. */

/* Covers and their barcode travel as one column. The barcode sits under the front cover at the
   front cover's width: spanning both sleeves made it nearly three times wider than a printed
   symbol, and it read as a smear rather than a barcode. */
.hero-covers-block[b-uq0dv8qm9f] { display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; align-items: flex-start; }

.hero-covers-block[b-uq0dv8qm9f]  .hero-barcode { width: 140px; }

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

/* 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[b-uq0dv8qm9f],
.cover-back.cover-broken[b-uq0dv8qm9f] {
    /* 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[b-uq0dv8qm9f] { font-size: 0; }

.cover-back.cover-broken[b-uq0dv8qm9f] { font-size: 0; }

.cover-back[b-uq0dv8qm9f] {
    --thumb-size: 92px;
    margin-top: 24px;
}

/* 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[b-uq0dv8qm9f] { cursor: pointer; transition: opacity 0.15s; }

.art-clickable:hover[b-uq0dv8qm9f] { opacity: 0.9; }

.hero-artist[b-uq0dv8qm9f] { margin-bottom: 4px; }

.hero-artist-link[b-uq0dv8qm9f] {
    color: inherit;
    text-decoration: none;
    transition: color 0.12s;
}

.hero-artist-link:hover[b-uq0dv8qm9f] {
    color: var(--accent-light);
    text-decoration: underline;
    text-underline-offset: 3px;
}

.hero-artist-link span[b-uq0dv8qm9f] { color: var(--accent); }

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

/* ─── Disc Pills ─────────────────────────────────────────────────────────── */
.disc-pills[b-uq0dv8qm9f] { margin-bottom: 14px; }

/* A number is a much shorter label than "Disc 1", so without a floor the chips collapse to
   inconsistent widths — 1 narrower than 10 — and stop reading as one set of controls. */
.disc-pill[b-uq0dv8qm9f] {
    justify-content: center;
    min-width: 2.5rem;
}

.disc-pills-row[b-uq0dv8qm9f] {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
    padding: 4px 0 6px;
}

/* 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[b-uq0dv8qm9f] { margin-bottom: 0; }

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

.notes-section > summary[b-uq0dv8qm9f] {
    list-style: none;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 0 10px;
    font-size: var(--text-sm);
    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[b-uq0dv8qm9f]::-webkit-details-marker { display: none; }


.notes-body[b-uq0dv8qm9f] { font-size: var(--text-lg); color: var(--text-secondary); line-height: 1.6; padding-bottom: 14px; }

.notes-recording-heading[b-uq0dv8qm9f] { font-weight: 600; color: var(--text-primary); padding-bottom: 4px; margin-top: 12px; }

/* Credit lines are a dense list — one per contributor — so they drop the paragraph
   margin the notes body carries. */
.notes-credit-line[b-uq0dv8qm9f] { padding-bottom: 2px; margin: 0; }


/* All-edit mode: a single-column form so every input gets the full panel width.
   Expressed as the grid's own minimum rather than a different display mode —
   "one column" is a width answer, not a different kind of layout. */
.coll-fields-stacked[b-uq0dv8qm9f] { --field-min: 100%; }

.coll-amount-input[b-uq0dv8qm9f] {
    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[b-uq0dv8qm9f] {
    width: 70px;
    max-width: 70px;
    flex: 0 0 70px;
}

.coll-amount-currency[b-uq0dv8qm9f] {
    flex: 0 0 auto;
    width: auto;
    max-width: none;
}

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

/* One vertical rhythm for the hero column. Children are conditional - the artist link, the
   badges, the catalogue line and the actions each render only sometimes - so per-child margins
   collapse to nothing when the ones carrying them are absent. A gap on the column cannot. */
.hero-info[b-uq0dv8qm9f] {
    display: flex;
    flex-direction: column;
    /* Without this the column stretches every child to full width, which turns the Discogs
       pill and the images button into bars. They are content-width controls. */
    align-items: flex-start;
    gap: var(--space-2);
}

/* Their spacing now comes from the column. */
.hero-badges[b-uq0dv8qm9f] {
    margin-bottom: 0;
}
/* /Components/Pages/AlbumRow.razor.rz.scp.css */
/* This component's own rules. */

.album-row[b-xkj0olh5pa] {
    background: linear-gradient(90deg,
        color-mix(in srgb, var(--row-color, transparent) 25%, transparent) 0%,
        transparent 70%);
}

.album-meta[b-xkj0olh5pa] { margin-top: 3px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }

.album-meta-chip[b-xkj0olh5pa] {
    display: inline-flex;
    align-items: center;
    padding: 1px 6px;
    height: 16px;
    border-radius: 3px;
    font-size: var(--text-xs);
    background: transparent;
    color: var(--text-dim);
    border: 1px solid var(--border);
    flex-shrink: 0;
    line-height: 1;
    white-space: nowrap;
}
/* /Components/Pages/AlbumVideoComponent.razor.rz.scp.css */
/* ─── Video carousel (AlbumVideoComponent) ───────────────────────────────── */
/* Custom video carousel. State-driven (one iframe in the DOM at a time,
   chosen by index in the component) so we never spin up N YouTube players
   at once. Prev/next + dot indicators wrap around. */
.video-carousel[b-9rbdqjnvsl] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
    width: 100%;
}

.video-carousel-stage[b-9rbdqjnvsl] {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    width: 100%;
}

.video-carousel-frame[b-9rbdqjnvsl] {
    flex: 1 1 auto;
    min-width: 0;
    /* 16:9 stage that scales with the available width. */
    aspect-ratio: 16 / 9;
    max-height: 70vh;
    background: #000;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}

.video-carousel-iframe[b-9rbdqjnvsl] {
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
}

.video-carousel-nav[b-9rbdqjnvsl] {
    border-radius: 50%;
}

.video-carousel-title[b-9rbdqjnvsl] {
    font-size: var(--text-md);
    color: var(--text-secondary);
    text-align: center;
    max-width: 100%;
    line-height: 1.4;
}

.video-carousel-dots[b-9rbdqjnvsl] {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 8px;
}

.video-carousel-dot[b-9rbdqjnvsl] {
    width: 9px;
    height: 9px;
    padding: 0;
    border-radius: 50%;
    border: 0;
    background: var(--border-med);
    cursor: pointer;
    transition: background 0.12s, transform 0.12s;
    position: relative;
}

/* WCAG 2.5.8 Target Size (AA): 9px dots with an 8px gap are 17px centre to centre,
   so neither the size rule nor the spacing exception was met. Transparent 24x24
   pseudo, no visual change — same trick as .biog-timeline-dot::after. */
.video-carousel-dot[b-9rbdqjnvsl]::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 24px;
    height: 24px;
    transform: translate(-50%, -50%);
}

.video-carousel-dot:hover[b-9rbdqjnvsl] {
    background: var(--text-dim);
}

.video-carousel-dot.active[b-9rbdqjnvsl] {
    background: var(--accent);
    transform: scale(1.2);
}

.video-carousel-dot:focus-visible[b-9rbdqjnvsl] {
    outline: 2px solid var(--accent-light);
    outline-offset: 2px;
}

.video-carousel-empty[b-9rbdqjnvsl] {
    color: var(--text-dim);
    font-size: var(--text-md);
    text-align: center;
    padding: 24px 12px;
}

/* Shown in place of the iframe when a stored video URI isn't a recognised
   YouTube link (so we never feed an arbitrary URI into the frame). */
.video-carousel-unavailable[b-9rbdqjnvsl] {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-dim);
    font-size: var(--text-md);
}
@media (max-width: 849px) {
    .video-carousel-stage[b-9rbdqjnvsl] { gap: 4px; }
}
/* /Components/Pages/ArtistDetailPage.razor.rz.scp.css */
/* The artist page's own: the hero, the discography and gig expanders, and the hero links menu. */

/* 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[b-90rgqyc8g8] {
    margin-left: 2px;
    margin-right: 2px;
    vertical-align: baseline;
}

/* Ownership is carried by the TITLE, so this rides .row-title rather than any one page's
   title class ? the library list and the discography both state it the same way. */
.row-title.not-owned[b-90rgqyc8g8] { color: var(--text-muted); font-style: italic; }

/* ─── Artist Detail Layout ───────────────────────────────────────────────── */

.discog-item[b-90rgqyc8g8] {
    gap: var(--space-2);
    padding-left: 0;
    padding-right: 0;
    border-radius: var(--radius-sm);
}

/* left = container-padding(14) + year-width(38) + gap(8) + dot-half(4) = 64px */

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

/* 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[b-90rgqyc8g8] { 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[b-90rgqyc8g8] {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    gap: 1px;
    padding-right: 4px;
    transition: color 0.12s;
}

.discog-item-unowned:hover .discog-search-affordance[b-90rgqyc8g8] { color: var(--accent-light); }

.discog-search-chevron[b-90rgqyc8g8] { font-size: var(--text-xl); line-height: 1; }

/* .list-row is a flex column with align-items: stretch, so the tag would span the
   the row width without this. */

/* ─── Artist Detail Page ─────────────────────────────────────────────────── */

.artist-page-breadcrumb[b-90rgqyc8g8] {
    height: 32px;
    padding: 0 var(--space-5);
}

/* ─── Artist Hero ────────────────────────────────────────────────────────── */
.artist-page-hero[b-90rgqyc8g8] {
    display: flex;
    align-items: flex-start;
    gap: 14px;
    padding: 10px 16px;
    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;
}

/* The photo is a fixed 96px while the hero's height comes from the text beside it, so the space
   under the photo was dead. The photo count belongs with the photo rather than as one more chip in
   the text column, and pinning it to the bottom of this column is what fills that space.

   Centred, not stretched or start-aligned: stretch pulls the chip to the column's full width and
   leaves it a different shape from every other chip, and flex-start pins the photo to the left of
   a column the wider chip is sizing. Centred, each keeps its own width and they share an axis. */
.artist-page-hero-figure[b-90rgqyc8g8] {
    display: flex;
    flex-direction: column;
    align-self: stretch;
    align-items: center;
    gap: var(--space-2);
}

.artist-page-photo-count[b-90rgqyc8g8] { margin-top: auto; }

.artist-page-photo[b-90rgqyc8g8] {
    --thumb-size: 96px;
    border-radius: 10px;
}

.artist-page-photo-initial[b-90rgqyc8g8] {
    font-size: 2.375rem;
    font-weight: 800;
    color: var(--text-muted);
}

.artist-page-hero-content[b-90rgqyc8g8] { flex: 1; min-width: 0; }

.artist-page-hero-name[b-90rgqyc8g8] {
    font-size: var(--display-md);
    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[b-90rgqyc8g8] {
    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;
}

/* Discogs styles and genres arrive lowercase. */
.artist-hero-tag[b-90rgqyc8g8] { border-radius: var(--radius-pill); text-transform: capitalize; }

/* The clamp mechanics and the toggle are .clamp / .clamp-toggle now; ClampedText
   sets the line count (1 here, so the hero stays compact at 1280×800 and the
   1366×768 recording target). This keeps only the bio's own type.
   ::deep because the class is handed to ClampedText, which renders the element: it carries
   ClampedText's scope attribute, not this page's, and without ::deep this rule matches nothing. */
.artist-page-hero-content[b-90rgqyc8g8]  .artist-page-bio {
    font-size: var(--text-lg);
    color: var(--text-secondary);
    line-height: 1.6;
    margin-bottom: 8px;
}

.artist-page-hero-links[b-90rgqyc8g8] {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}

/* 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[b-90rgqyc8g8] {
    position: relative;
    display: inline-flex;
}

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

.hero-links-caret[b-90rgqyc8g8] {
    font-size: var(--text-xs);
    line-height: 1;
    color: var(--text-dim);
}

/* The panel and its rows are .menu / .menu-item now. This keeps the one thing the
   primitive does not say: external link labels ("Official site", a long Bandcamp
   subdomain) run away without a ceiling, and a menu of commands does not need one. */
.hero-links-panel[b-90rgqyc8g8] { max-width: 280px; }

.hero-links-item-icon[b-90rgqyc8g8] {
    width: 16px;
    text-align: center;
}

/* 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-hero-actions[b-90rgqyc8g8] {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
    flex-shrink: 0;
    padding-top: 4px;
}

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

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

.artist-page-col:last-child[b-90rgqyc8g8] { border-right: none; }

/* The shared .section-label supplies the lettering; this only lets it take the row. */
.artist-col-title[b-90rgqyc8g8] { flex: 1; }

.artist-col-scroll[b-90rgqyc8g8] { flex: 1; overflow-y: auto; }

/* ─── Discog list (artist page) ──────────────────────────────────────────── */

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

.discog-expand.is-open[b-90rgqyc8g8] { display: block; }

/* Layout only — .meta-list in app.css owns how a run of values reads, so this cannot drift from
   the cover wall's run or the venue card's. */
.discog-expand-tags[b-90rgqyc8g8] { display: flex; flex-wrap: wrap; gap: 0 var(--space-2); }

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

.discog-expand-track[b-90rgqyc8g8] {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding: 4px 0;
}

.discog-expand-track-dur[b-90rgqyc8g8] { flex-shrink: 0; }

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

/* .list-row supplies the shape, the border, the hover and the selected state.
   Only the extra left indent is this list's own. */

.gig-track-count[b-90rgqyc8g8] { flex-shrink: 0; }

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

/* ─── Art gallery (in drawer) ────────────────────────────────────────────── */

.gig-expand.is-open[b-90rgqyc8g8] { display: block; }

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

.gig-expand-date[b-90rgqyc8g8] { font-size: var(--text-2xl); font-weight: 700; color: var(--text-primary); }

.gig-expand-actions[b-90rgqyc8g8] { display: flex; gap: 8px; margin-left: auto; }

.setlist-track[b-90rgqyc8g8] {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 6px 0;
    border-bottom: 1px solid var(--border);
}

.setlist-track:last-child[b-90rgqyc8g8] { border-bottom: none; }

/* 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[b-90rgqyc8g8] {
    flex-shrink: 0;
    font-size: var(--text-md);
    color: #f5b50a;
    letter-spacing: 1px;
    margin-left: 8px;
}

/* Search results include releases the user does not own; they read as quieter
   so the owned ones lead. */
.card.not-owned .card-title[b-90rgqyc8g8] {
    color: var(--text-muted);
    font-style: italic;
}

@media (max-width: 900px) {
    .discog-item-rating[b-90rgqyc8g8] { display: none; }
}

/* The Biog tab scrolls the layout itself; Overview scrolls its own panes. Below 849px the block
   under this one already scrolls the layout on EVERY tab, so these two declarations are only doing
   work at >=850px — change one and change the other. */
.artist-page-layout.biog-active[b-90rgqyc8g8] { overflow-y: auto; overflow-x: hidden; }

@media (max-width: 849px) {
    .artist-page-layout[b-90rgqyc8g8] { overflow-y: auto; overflow-x: hidden; }
    /* Compact hero: photo inline with name, hide art strip */
    .artist-page-hero[b-90rgqyc8g8] { flex-direction: row; flex-wrap: wrap; gap: 14px; padding: 14px 16px; }
    .artist-page-photo[b-90rgqyc8g8] { width: 64px; height: 64px; border-radius: 10px; }
    .artist-page-hero-content[b-90rgqyc8g8] { flex: 1; min-width: 0; }
    .artist-page-hero-name[b-90rgqyc8g8] { font-size: var(--display-sm); margin-bottom: 6px; }
    .artist-hero-tag[b-90rgqyc8g8] { font-size: var(--text-xs); padding: 3px 9px; }
    .artist-page-hero-content[b-90rgqyc8g8]  .artist-page-bio { -webkit-line-clamp: 1; font-size: var(--text-md); margin-bottom: 6px; line-height: 1.45; }
    .artist-page-hero-links[b-90rgqyc8g8] {
        flex-wrap: wrap;
        gap: 6px;
    }
    .artist-page-hero-links .chip[b-90rgqyc8g8],
    /* ::deep: the wrapper and its chip are DiscogsPill's elements, so they carry ITS attribute. */
    .artist-page-hero-links[b-90rgqyc8g8]  .hero-link-chip-wrap .chip {
        font-size: var(--text-sm);
        padding: 4px 10px;
        min-height: 28px;
    }
    /* This column holds the outbound links as well as refresh, so it flows as a wrapped row
       rather than being pinned into a reserved corner: at this width a 64px corner cannot hold
       them, and a row that carries the links is no longer the strip of dead space that pinning
       the lone refresh button was there to avoid. */
    .artist-page-hero-actions[b-90rgqyc8g8] {
        /* Its own row. Sharing a line with the text column leaves that column whatever these
           chips do not take, which at this width squeezes the tags to one per line and clips the
           biography to a single word. */
        flex-basis: 100%;
        align-items: center;
        padding-top: 0;
        flex-direction: row;
        flex-wrap: wrap;
        gap: 8px;
    }
    .artist-page-cols[b-90rgqyc8g8] { flex-direction: column; overflow: visible; flex: none; }
    .artist-page-col[b-90rgqyc8g8] { overflow: visible; flex: none; border-right: none; border-bottom: 1px solid var(--border); }
    .artist-col-scroll[b-90rgqyc8g8] { flex: none; overflow-y: visible; }
}

@media (max-width: 400px) {
    .artist-page-hero-content[b-90rgqyc8g8] {
        flex-basis: 100%;
    }
}

.deep-link-flash[b-90rgqyc8g8] {
    border-radius: 3px;
    animation: deep-link-flash 3s ease-out forwards;
}
/* /Components/Pages/ArtistList.razor.rz.scp.css */
/* This component's own rules. */

.active-filter-chip[b-glmbpzue28] {
    margin: 6px 14px;
}

.artist-list-body[b-glmbpzue28] {
    display: flex;
    flex: 1;
    min-height: 0;
}
/* /Components/Pages/Artists/ArtistBiogTab.razor.rz.scp.css */
/* The biography tab's own: the timeline, its grid and marker rails, and the release/concert cards. */

/* 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. */

/* Page inset so the band-member / release / concert columns don't bleed to the window edge,
   matching the rest of the artist page. */

.biog-artist-metrics[b-te8mglbgny] {
    --stat-cols: 6;
    margin: 4px 0 22px;
}

.biog-tl-empty[b-te8mglbgny] {
    color: var(--text-dim);
    font-size: var(--text-lg);
    padding: 28px 4px;
    text-align: center;
}

/* 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. */
/* The accent edge is the era colour, set inline per member — the only part the card primitive
   does not already give this. */
.biog-mbar[b-te8mglbgny] {
    align-self: stretch;
    margin: 1px 2px 2px;
    border-left: 3px solid var(--border-strong);
    padding: 5px 7px;
    min-width: 0;
}

/* No recorded end and no death: the bar reaches the foot of the axis, so a hard bottom edge would
   say the member left in the year of the latest release owned — a fact about the collection, not
   about them. Bars run down the years here, so the fade is vertical. */
.biog-mbar-open[b-te8mglbgny] {
    mask-image: linear-gradient(to bottom, #000 72%, transparent);
}

.biog-mbar-name[b-te8mglbgny] { font-size: var(--text-sm); 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[b-te8mglbgny] { font-size: var(--text-xs); color: var(--text-secondary); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }


.biog-legend[b-te8mglbgny] { display: flex; flex-wrap: wrap; gap: 14px; margin-bottom: 14px; }

.biog-legend span[b-te8mglbgny] { display: inline-flex; align-items: center; gap: 5px; }

.biog-swatch[b-te8mglbgny] { width: 9px; height: 9px; border-radius: 2px; display: inline-block; }

.biog-swatch-release[b-te8mglbgny] { background: var(--accent); }

.biog-swatch-concert[b-te8mglbgny] { background: var(--concert); }

.biog-swatch-era[b-te8mglbgny] { background: #6f8df0; }


/* A disclosure, so it is a <button> — it was a clickable <div> with no role, no
   Tab stop and no aria-expanded. font/color/text-align are the UA reset a button
   needs; no width because the card is meant to shrink to its content rather than
   fill its column. */
.biog-ev-card[b-te8mglbgny] { padding: 7px 9px; max-width: 100%; text-align: left; }

.biog-ev-release[b-te8mglbgny] { border-left: 3px solid var(--accent); }

.biog-ev-concert[b-te8mglbgny] { border-left: 3px solid var(--concert); }

.biog-ev-head[b-te8mglbgny] { display: flex; gap: 8px; align-items: center; }

/* .thumb sizes from this token; the initials fallback needs a smaller face than its default. */
.biog-ev-cover[b-te8mglbgny] { --thumb-size: 30px; font-size: var(--text-xs); }

.biog-ev-title[b-te8mglbgny] { font-size: var(--text-md); font-weight: 500; color: var(--text-primary); }

/* ── One timeline, every width ────────────────────────────────────────────────────────────────
   Columns: [left member lanes] [owned releases] [spine] [concerts] [right member lanes].
   The four templates are computed in the code-behind and handed in as custom properties; the
   media queries below pick one and hide the lanes it has collapsed to zero. Every lane keeps a
   single inline grid-column at every width, so a lane can never be placed in a track the current
   template has not reserved. Change a template and its matching hide rule together. */
.biog-spine-grid[b-te8mglbgny] {
    display: grid;
    grid-template-columns: var(--cols-members);
    column-gap: 10px;
    align-items: start;
    padding-bottom: 12px;
}

.biog-head[b-te8mglbgny] {
    font-size: var(--text-xs);
    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(--divider);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.biog-head-release[b-te8mglbgny] { text-align: right; }

.biog-head-right-members[b-te8mglbgny] { text-align: right; }

/* Centred over its bucket span so a multi-member instrument reads as one group rather than
   leaving the extra sub-lane looking header-less. */
.biog-head-bucket[b-te8mglbgny] { text-align: center; }


.biog-head-spine[b-te8mglbgny] { border-bottom-color: transparent; }
/* The era colouring IS the spine: a segment per year, washed with that year's line-up era, so
   succession reads as colour bands down the centre. align-self stretch keeps the bands contiguous
   — the grid has no row gap, so year boundaries meet with no hairline. */
.biog-spine-seg[b-te8mglbgny] {
    justify-self: center;
    align-self: stretch;
    width: 4px;
    opacity: 0.55;
    border-radius: 0;
}

/* An event marker on the spine, not inside a card — z-index lifts it over the segment. */
.biog-year-dot[b-te8mglbgny] {
    justify-self: center;
    align-self: start;
    margin-top: 14px;
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid var(--bg-base);
    z-index: 1;
}

.biog-year[b-te8mglbgny] {
    justify-self: center;
    align-self: start;
    z-index: 1;
    padding-top: 2px;
}

/* The knockout background is load-bearing: the label sits ON the spine segment, and without an
   opaque plate the year reads through the colour band. */
.biog-year-label[b-te8mglbgny] {
    font-size: var(--text-xs);
    font-weight: 700;
    color: var(--text-dim);
    background: var(--bg-base);
    padding: 0 4px;
    border-radius: var(--radius-sm);
}

.biog-cell[b-te8mglbgny] { display: flex; flex-direction: column; gap: 6px; padding: 4px 0; min-width: 0; }

/* Releases hang off the left of the spine, so they align to it rather than to the page. */
.biog-cell-release[b-te8mglbgny] { align-items: flex-end; }

.biog-cell-concert[b-te8mglbgny] { align-items: flex-start; }


/* Shown only while the instrument columns are collapsed — above that every bar is visible and a
   tab would hide members for no reason.

   subgrid is what lets the collapsed column keep the timeline: its rows ARE the parent grid's
   rows, so a bar still spans exactly the years its member was in the band. Without it this column
   would need its own row heights and would drift out of step with the releases beside it. */
.biog-roster[b-te8mglbgny] {
    display: none;
    grid-template-rows: subgrid;
    column-gap: 4px;
    min-width: 0;
}

.biog-roster-tabs[b-te8mglbgny] { min-width: 0; }
/* The intermediate width: tenure bars out, one roster column each side in. The template keeps
   ONE lane track per side sized and zeroes the rest, and this hides the bars in the same breath —
   a sized track whose content is hidden leaves a gap, the reverse overflows. */
@media (max-width: 1200px) {
    /* Collapsed lane tracks are 0px but a column-gap still applies BETWEEN them, so eight dead
       tracks kept eighty pixels of gutter and the grid never reached the page edges. Gap goes to
       zero and the visible cells carry the spacing themselves — a zero-width track with no content
       then costs nothing. */
    .biog-spine-grid[b-te8mglbgny] { column-gap: 0; }
    .biog-cell[b-te8mglbgny],
    .biog-roster[b-te8mglbgny],
    .biog-head[b-te8mglbgny] { margin-inline: 5px; }
    .biog-lane[b-te8mglbgny] { display: none; }
    .biog-roster[b-te8mglbgny] { display: grid; }
}

/* Spine only below the app's 849px convention: the member columns go, and the template zeroes
   their tracks in the same breath — a hidden column whose track kept its width would leave an
   unexplained gap, and a sized track with hidden content overflows the page. */
@media (max-width: 849px) {
    .biog-spine-grid[b-te8mglbgny] { grid-template-columns: var(--cols-spine); column-gap: 0; }
    .biog-lane[b-te8mglbgny],
    .biog-roster[b-te8mglbgny] { display: none; }
    .biog-artist-metrics[b-te8mglbgny] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The two text lines beside a release cover. min-width lets the title ellipsis inside a grid
   column rather than forcing the column wider than its track. */
.biog-ev-lines[b-te8mglbgny] { min-width: 0; display: flex; flex-direction: column; }

/* The reveal inside an event card. The hint is the card's quietest line — at body size it was the
   LOUDEST, which is what made every card read as clutter. */
.biog-ev-hint[b-te8mglbgny] { font-size: var(--text-xs); color: var(--text-dim); margin-top: 4px; }

/* A button may only contain phrasing content, so these are spans — without display:block the
   line-up runs together as one paragraph. */
.biog-ev-reveal[b-te8mglbgny],
.biog-ev-reveal-h[b-te8mglbgny],
.biog-ev-reveal-m[b-te8mglbgny],
.biog-ev-hint[b-te8mglbgny] { display: block; }

.biog-ev-reveal[b-te8mglbgny] { margin-top: 7px; padding-top: 7px; border-top: 1px solid var(--border); }

.biog-ev-reveal-h[b-te8mglbgny] { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); margin-bottom: 4px; }

.biog-ev-reveal-m[b-te8mglbgny] { font-size: var(--text-sm); color: var(--text-secondary); line-height: 1.5; }

.biog-ev-reveal-role[b-te8mglbgny] { color: var(--text-dim); }

/* Wide enough for hover and focus affordances to say "this opens" — the textual hint only earns
   its row where touch is the only affordance. */
@media (min-width: 850px) {
    .biog-ev-hint[b-te8mglbgny] { display: none; }
}

/* Chip tabs: the strip inside a column, where the full-width underlined bar wraps every tab onto
   its own line and carries a page inset the column has already paid for. Chips wrap as chips. */
[b-te8mglbgny] .detail-tabs.tabs-chips {
    flex-wrap: wrap;
    gap: var(--space-1);
    padding: 0 0 var(--space-2);
    border-bottom: 0;
}

[b-te8mglbgny] .detail-tabs.tabs-chips .detail-tab {
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    padding: 2px var(--space-2);
    font-size: var(--text-sm);
    min-height: 0;
    white-space: nowrap;
}

[b-te8mglbgny] .detail-tabs.tabs-chips .detail-tab.is-active {
    border-color: var(--accent-border);
    background: var(--accent-bg);
    color: var(--accent-light);
}

/* /Components/Pages/Artists/PersonPanel.razor.rz.scp.css */
/* Every element here is authored in PersonPanel.razor, so it carries this sheet's scope attribute
   even though it renders inside the Dialog — plain selectors reach it. The dialog PANEL itself
   (.person-panel) is the Dialog's element and is sized from app.css, next to .help-panel. */
.person-head[b-bz5x4453sm] {
    display: flex;
    gap: var(--space-4);
    align-items: flex-start;
    margin-bottom: var(--space-5);
}

.person-portrait[b-bz5x4453sm] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-1);
    flex: 0 0 auto;
    max-width: 140px;
    text-align: center;
}

/* A size modifier on .thumb — same primitive as the strip's faces, one step larger. */
.person-photo[b-bz5x4453sm] {
    --thumb-size: 96px;
}

.person-photo > img[b-bz5x4453sm] {
    object-position: center 20%;
}

.person-summary[b-bz5x4453sm] {
    min-width: 0;
}

/* The head is three descending statements, not three equal muted ones. Read together they were
   all dim and widely spaced, which made the block look like something had failed to load. */
.person-lifespan[b-bz5x4453sm] {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: 600;
    color: var(--text-primary);
    line-height: 1.2;
    font-variant-numeric: tabular-nums;
}

.person-description[b-bz5x4453sm] {
    margin: 2px 0 0;
    font-size: var(--text-lg);
    color: var(--text-secondary);
    line-height: 1.35;
}

.person-summary > .meta-xs[b-bz5x4453sm] {
    display: block;
    margin-top: var(--space-2);
}

.person-tenures[b-bz5x4453sm] { gap: var(--space-3); }

/* Label beside track, so every bar shares one axis and the eye can compare spans down the column. */
.person-tenure[b-bz5x4453sm] {
    display: grid;
    grid-template-columns: minmax(0, 13rem) 1fr;
    gap: var(--space-3);
    align-items: center;
}

.person-tenure-label[b-bz5x4453sm] {
    min-width: 0;
}

.person-tenure-band[b-bz5x4453sm] {
    display: block;
    font-weight: 600;
    color: var(--text-primary);
    text-decoration: none;
}

.person-tenure-band:hover[b-bz5x4453sm],
.person-tenure-band:focus-visible[b-bz5x4453sm] {
    color: var(--accent-light);
    text-decoration: underline;
}

.person-tenure-meta[b-bz5x4453sm] {
    display: block;
    font-variant-numeric: tabular-nums;
}

/* .meter is the pill track; this only sizes it and makes it the positioning context, because a
   tenure bar starts partway along the axis rather than at zero like a progress fill. */
.person-tenure-track[b-bz5x4453sm] {
    position: relative;
    --meter-height: 10px;
}

.person-tenure-bar[b-bz5x4453sm] {
    position: absolute;
    top: 0;
}

/* An unclosed tenure fades out instead of stopping: MusicBrainz not recording a departure is not
   evidence of one, and a hard right edge would claim a leaving year we do not have. */
.person-tenure-bar-open[b-bz5x4453sm] {
    border-radius: var(--radius-pill) 0 0 var(--radius-pill);
    background: linear-gradient(to right, var(--accent) 55%, rgba(139, 132, 232, 0));
}

.person-timeline-axis[b-bz5x4453sm] {
    display: flex;
    justify-content: space-between;
    margin-top: var(--space-2);
    /* Line the axis labels up with the track column, not the whole drawer. */
    margin-left: calc(13rem + var(--space-3));
    font-size: var(--text-xs);
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
}

/* Narrow: the track drops under its label rather than squeezing to a stub. Same 850px breakpoint
   the rest of the app changes shape at. */
@media (max-width: 849px) {
    .person-tenure[b-bz5x4453sm] {
        grid-template-columns: 1fr;
        gap: var(--space-1);
    }

    .person-timeline-axis[b-bz5x4453sm] {
        margin-left: 0;
    }
}

/* ── "Also on your shelf" ─────────────────────────────────────────────────────
   Rows are .list-row; these rules only place the title against the roles and
   set the band heading apart from the tenure bars above it. */
.person-credits-label[b-bz5x4453sm] {
    margin-top: var(--space-5);
    margin-bottom: var(--space-2);
}

.person-credit-band[b-bz5x4453sm] {
    display: block;
    margin-top: var(--space-3);
    font-weight: 600;
}

.person-credits[b-bz5x4453sm] {
    list-style: none;
    margin: 0;
    padding: 0;
}

/* A row that happens to be a link, not a link that happens to be a row: without this it takes
   the global anchor colour and reads as a sentence of hyperlinks down the drawer. */
.person-credit[b-bz5x4453sm] {
    color: inherit;
    text-decoration: none;
}

/* Never shrinks, so the roles sit hard right and read as one column down the
   list, rather than trailing each title at a different place. */
.person-credit-roles[b-bz5x4453sm] {
    flex-shrink: 0;
    text-align: right;
}

/* Narrow: the roles drop under the title. Same 850px breakpoint as the tenure lanes. */
@media (max-width: 849px) {
    .person-credit[b-bz5x4453sm] {
        flex-wrap: wrap;
    }

    .person-credit-roles[b-bz5x4453sm] {
        flex-basis: 100%;
        text-align: left;
        margin-left: calc(24px + var(--space-3));
    }
}
/* /Components/Pages/Artists/SharedMembersStrip.razor.rz.scp.css */
/* One strip, three visible states, chosen by COUNT alone — never by width. Above 850px every
   size here is a clamp() that scales continuously with the pane, which is why this sheet adds no
   media query of its own and uses only the app's single 850px breakpoint at the foot. */
/* The band itself is `.bar bar-bottom bar-panel` — a padded panel strip with an under-edge is
   exactly what that primitive is, so this only widens the gap it sets. */
.shared-strip[b-0eloqa697k] {
    gap: var(--space-4);
}

/* ── State B: label + a row of tiles ──────────────────────────────────────── */

.shared-strip-label[b-0eloqa697k] {
    display: flex;
    flex-direction: column;
    flex: 0 0 clamp(96px, 12%, 160px);
    line-height: 1.1;
}

.shared-strip-tiles[b-0eloqa697k] {
    display: flex;
    flex: 1;
    gap: var(--space-3);
    min-width: 0;
}

/* ::deep — the Card component renders this element, so it carries no scope attribute. The frame,
   surface and focus ring come from .card; this only turns its body into a row and sizes it. */
/* Tiles divide the row between them, but only up to a point: `flex: 1 1 0` alone hands ALL the
   remaining width to whatever is there, so a single shared member became a 1,700px card wrapped
   around a 56px portrait. The cap is what a tile actually needs — portrait, name, one line of
   bands — and the row simply ends early when there are fewer of them. */
.shared-strip-tiles[b-0eloqa697k]  .shared-tile {
    flex: 1 1 0;
    max-width: 360px;
    min-width: 0;
}

.shared-strip-tiles[b-0eloqa697k]  .shared-tile .card-body {
    flex-direction: row;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    min-width: 0;
}

/* The portrait is the one thing that scales with the pane: 56px at 1920, ~44 at 1366, 40 at 1024.
   A size modifier on .thumb, nothing more — the round crop and the initials centring are the
   primitive's, and .thumb already treats people as round. */
.shared-strip-tiles[b-0eloqa697k]  .shared-tile-photo {
    --thumb-size: clamp(40px, 3.2vw, 56px);
    flex: 0 0 auto;
}

/* Faces sit high in a portrait; the primitive's centred crop cuts foreheads. */
.shared-strip-tiles[b-0eloqa697k]  .shared-tile-photo > img {
    object-position: center 20%;
}

.shared-strip-tiles[b-0eloqa697k]  .shared-tile-text {
    display: flex;
    flex-direction: column;
    min-width: 0;
}

.shared-strip-tiles[b-0eloqa697k]  .shared-tile-name {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-primary);
    line-height: 1.25;
}

/* The designed sacrifice as the pane narrows: the band list gives up characters before the name
   does, so a tile always says WHO before it says where else. */
.shared-strip-tiles[b-0eloqa697k]  .shared-tile-bands {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── State C: the collapsed pile ──────────────────────────────────────────── */

/* The row IS the control, so it needs a button's resets over .bar's div styling. */
.shared-strip-many[b-0eloqa697k] {
    gap: var(--space-3);
    width: 100%;
    border-left: 0;
    border-right: 0;
    border-top: 0;
    font: inherit;
    text-align: start;
    cursor: pointer;
}

.shared-strip-many:hover[b-0eloqa697k] {
    background: var(--bg-raised);
}

/* Inset, like a card's full-bleed child. The row spans the page edge to edge, so the app's usual
   2px OUTSET ring falls outside the viewport left and right and under the tab bar above — all that
   survived was a hairline along the top, which read as a rendering artefact rather than focus. */
.shared-strip-many:focus-visible[b-0eloqa697k] {
    outline: 2px solid var(--accent);
    outline-offset: -3px;
}

/* Sits beside the caption rather than pinned to the far edge — a chevron a screen's width from
   what it opens is a chevron nobody finds. */
.shared-strip-caption[b-0eloqa697k] {
    flex: 0 1 auto;
    min-width: 0;
}

.shared-pile[b-0eloqa697k] {
    display: flex;
    align-items: center;
    flex: 0 0 auto;
    /* Same size as a tile's portrait: dropping to a pile is a change of arrangement, not a demotion
       of the people in it. */
    --pile-face: clamp(40px, 3.2vw, 56px);
    /* What the rest of the row needs — count, caption, chevron, padding — before the pile may use
       what is left. */
    --pile-reserve: 520px;
}

/* A monogram in the app's accent rather than .thumb's neutral card surface. Among nineteen
   photographs a grey disc reads as an image that failed to load; tinted, it reads as a person the
   portrait fetch found nothing for — which is what it is. Applies wherever initials stand in for a
   face, so the treatment is one thing rather than a pile-only special case. */
.shared-face-monogram[b-0eloqa697k] {
    background: var(--accent-bg);
    color: var(--accent-light);
}

/* The pile is ONE object, not a row of targets. The faces look like the tiles' portraits, so
   without this they promise a click-through to that person — and the row expands instead. They
   cannot each be a control: a button may not contain buttons, and in a pile most faces are partly
   covered anyway, so a per-face target would mostly hit the wrong person. */
.shared-pile[b-0eloqa697k] {
    pointer-events: none;
}

/* Share the row between the faces there ARE. The first version divided by viewport width alone,
   tuned for the nineteen-face case — so five faces on a wide row were crushed together for space
   nobody needed. Dividing by --pile-count instead means the pile only overlaps once the width
   genuinely runs out: five faces stay spaced at any desktop width, nineteen space out at 1366 and
   above and tighten below it. Clamped both ways so it never spreads into a gappy row and never
   compresses to slivers, and it never truncates — nobody is dropped at any width. */
.shared-pile > * + *[b-0eloqa697k] {
    margin-left: clamp(
        -30px,
        calc((100vw - var(--pile-reserve)) / var(--pile-count) - var(--pile-face)),
        var(--space-1));
}

/* Faces need separating from each other where they overlap, and the first must read as the front
   of the pile rather than the back — hence the descending z-index the markup sets.
   No per-face border: the 1px outline .thumb carries makes each read as its own tile, which is
   exactly the promise the pile cannot keep. The ring in the panel colour separates them instead. */
.shared-pile-face[b-0eloqa697k] {
    --thumb-size: var(--pile-face);
    position: relative;
    border-color: transparent;
    box-shadow: 0 0 0 2px var(--bg-panel);
    opacity: 0.88;
    transition: opacity 0.15s ease;
}

.shared-pile-face > img[b-0eloqa697k] {
    object-position: center 20%;
}

/* The whole pile lifts together on hover, which is the group behaving as the single control it is
   — where a face lighting up on its own would say "click me specifically". */
.shared-strip-many:hover .shared-pile-face[b-0eloqa697k],
.shared-strip-many:focus-visible .shared-pile-face[b-0eloqa697k] {
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    .shared-pile-face[b-0eloqa697k] { transition: none; }
}

/* Caption and cue travel together, close enough to read as one thing — the chevron on its own,
   spaced like a separate item, looked like an unrelated control. */
.shared-strip-tail[b-0eloqa697k] {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    min-width: 0;
}

/* The app's dropdown cue, same caret the account menu uses — a rotated navigation chevron read as
   a third idiom on a page that already has two.
   ::deep because the <svg> is rendered by the Icon COMPONENT and carries its scope attribute, not
   this sheet's. Without it these rules match nothing: the caret kept its resting angle however
   many times the row was opened, and the colour came from inheritance rather than from here. */
.shared-strip-tail[b-0eloqa697k]  .shared-expand {
    flex: 0 0 auto;
    color: var(--text-dim);
    transition: transform 0.15s ease;
}

.shared-strip-tail[b-0eloqa697k]  .shared-expand-open {
    transform: rotate(180deg);
}

@media (prefers-reduced-motion: reduce) {
    .shared-strip-tail[b-0eloqa697k]  .shared-expand { transition: none; }
}

/* ── State D: everyone, as chips ──────────────────────────────────────────── */

/* Same primitive as the strip above it, plus `bar-wrap` for the wrapping. */
.shared-chips[b-0eloqa697k] {
    gap: var(--space-2);
}

.shared-chips[b-0eloqa697k]  .thumb,
.shared-chip .thumb[b-0eloqa697k] {
    flex: 0 0 auto;
}

.shared-chip-name[b-0eloqa697k] {
    font-size: var(--text-sm);
}

/* ── The app's single breakpoint ──────────────────────────────────────────── */

@media (max-width: 849px) {
    /* The two-column scrolling frame does not survive here, so the strip scrolls with the page
       rather than eating a fixed list area — and tiles stack into rows the full width of it. */
    .shared-strip[b-0eloqa697k] {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
    }

    .shared-strip-label[b-0eloqa697k] {
        flex: none;
        flex-direction: row;
        align-items: baseline;
        gap: var(--space-2);
    }

    /* The axis flips here, and `flex: 1 1 0` is a MAIN-AXIS rule: as a column it means a
       flex-basis of zero HEIGHT, and an auto-height container has no free space to grow it back
       from — so every tile collapsed to a line. Both the container and the tiles have to give up
       their row-direction sizing, not just change direction. */
    .shared-strip-tiles[b-0eloqa697k] {
        flex: 0 0 auto;
        flex-direction: column;
    }

    .shared-strip-tiles[b-0eloqa697k]  .shared-tile {
        flex: 0 0 auto;
        max-width: none;
    }

    /* The count and the pile keep the first line; the caption and its caret drop to a second.
       The span belongs on the TAIL, not the caption inside it — the caption stopped being a grid
       item the moment it was wrapped, so this rule quietly did nothing and the caption wrapped
       four lines deep inside a narrow column instead. */
    .shared-strip-many[b-0eloqa697k] {
        display: grid;
        grid-template-columns: auto 1fr;
        align-items: center;
    }

    .shared-strip-tail[b-0eloqa697k] {
        grid-column: 1 / -1;
    }

}
/* /Components/Pages/ArtistsPage.razor.rz.scp.css */
/* This component's own rules. */

/* ─── Artists content wrapper ───────────────────────────────────────────────
   .app-body (the routed-page parent) is a plain block with a fixed height,
   not flex — so .view-1pane's own height:100% resolved against that FULL
   height regardless of the preceding breadcrumb bar (shown when Filter is
   set), pushing the pane's bottom edge past .app-body's overflow:hidden
   boundary and clipping content at the foot of the pane permanently out of
   reach. Same fix as LibraryView.razor.css's .library-content: wrap the
   breadcrumb + pane in a local flex column so the breadcrumb keeps its
   natural height and the pane takes exactly what's left. */
.artists-content[b-ex93bx1qwq] {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* 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[b-ex93bx1qwq] {
    margin: 0;
    padding: 12px 16px 14px;
    font-size: var(--text-md);
    color: var(--text-dim);
    border-bottom: 1px solid var(--border);
    line-height: 1.45;
}

.view-1pane[b-ex93bx1qwq] {
    background: var(--bg-base);
}

.pane-sidebar-full[b-ex93bx1qwq] {
    width: 100%;
    max-width: 480px;
}
/* /Components/Pages/BiogPage.razor.rz.scp.css */
/* The biography page's own: the stat cards, the year rail and the map panel. */

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

.biog-title[b-fhd2jfnyjh] {
    font-size: var(--display-sm);
    font-weight: 500;
    color: var(--text-primary);
    margin: 0 0 4px;
}

.biog-divider[b-fhd2jfnyjh] {
    border: none;
    border-top: 1px solid var(--border);
    margin: 10px 0;
}

.biog-section-title[b-fhd2jfnyjh] { margin-bottom: 10px; }

/* 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 849px mobile breakpoint. */

.biog-metric-card-host[b-fhd2jfnyjh] {
    position: relative;
    display: block;
}

.biog-metric-spinner[b-fhd2jfnyjh] {
    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;
}

/* Two-column layout: timeline + format chart. Stacks on mobile (handled
   by the responsive override below). */
.biog-columns[b-fhd2jfnyjh] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    margin-bottom: 10px;
}

.biog-panel[b-fhd2jfnyjh] {
    min-height: 180px;
}

/* 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[b-fhd2jfnyjh] {
    position: relative;
    height: 24px;
    margin-top: 12px;
    margin-bottom: 4px;
}

/* Connecting line through the dot column. */
.biog-timeline-line[b-fhd2jfnyjh] {
    position: absolute;
    top: 11px;
    left: 0;
    right: 0;
    height: 2px;
    background: rgba(255,255,255,0.1);
    border-radius: 1px;
}

/* The year panel's scrolling body. Viewport anchoring and centring now come from
   Dialog; all this has to do is let a long list of concerts scroll inside the dialog
   body rather than overflow it. */
.biog-timeline-tooltip-list[b-fhd2jfnyjh] {
    overflow-y: auto;
    flex: 1 1 auto;
    min-height: 0;
}

.biog-timeline-tooltip-line[b-fhd2jfnyjh] {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 6px 0;
    border-top: 1px solid var(--border);
}

.biog-timeline-tooltip-line:first-of-type[b-fhd2jfnyjh] { border-top: none; padding-top: 0; }

.biog-timeline-tooltip-artist[b-fhd2jfnyjh] {
    font-size: var(--text-lg);
    color: var(--text-secondary);
    line-height: 1.35;
}

.biog-timeline-tooltip-meta[b-fhd2jfnyjh] {
    font-size: var(--text-sm);
    color: var(--text-dim);
    line-height: 1.35;
}

.biog-timeline-years[b-fhd2jfnyjh] {
    position: relative;
    height: 16px;
    margin-bottom: 10px;
}

.biog-timeline-year[b-fhd2jfnyjh] {
    position: absolute;
    transform: translateX(-50%);
}

.biog-timeline-year.active[b-fhd2jfnyjh] { color: var(--text-secondary); }

.biog-timeline-dot[b-fhd2jfnyjh] {
    position: absolute;
    top: -4px;
    width: 10px;
    height: 10px;
    /* UA reset — this is a <button> now. */
    padding: 0;
    border: 0;
    border-radius: 50%;
    background: var(--accent);
    transform: translateX(-50%);
    cursor: pointer;
}

/* THE TARGET IS BIGGER THAN THE DOT. A 10px dot is a 10px tap target, well under
   the 24x24 floor (WCAG 2.5.8), and on a dense timeline the dots sit close enough
   that a miss selects the wrong year. Growing the dot itself would wreck the
   graphic — the whole point is that a year is a small mark on an axis — so an
   invisible pseudo-element carries the target instead. Appearance unchanged.
   Not on the ::before/::after used elsewhere in this family; check before adding. */
.biog-timeline-dot[b-fhd2jfnyjh]::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 24px;
    height: 24px;
    transform: translate(-50%, -50%);
}

.biog-timeline-dot:focus-visible[b-fhd2jfnyjh] {
    outline: 2px solid var(--accent-light);
    outline-offset: 3px;
}

.biog-timeline-dot.active[b-fhd2jfnyjh] {
    box-shadow: 0 0 0 3px rgba(139,132,232,0.45);
}

/* AFTER .active, and both are (0,2,0): a multi-year dot that is also selected takes this
   box-shadow, which is the wider mark. Swap the order and the selection ring wins instead. */
.biog-timeline-dot.multi[b-fhd2jfnyjh] {
    background: var(--accent-light);
    width: 12px;
    height: 12px;
    top: -5px;
    box-shadow: 0 0 0 2px rgba(139,132,232,0.25);
}

.biog-timeline-pills[b-fhd2jfnyjh] {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 8px;
}

.biog-pill strong[b-fhd2jfnyjh] {
    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[b-fhd2jfnyjh] {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-bottom: 8px;
}

.biog-format-label[b-fhd2jfnyjh] {
    font-size: var(--text-md);
    color: var(--text-secondary);
    width: 70px;
    flex-shrink: 0;
}

.biog-format-track[b-fhd2jfnyjh] {
    --meter-height: 10px;
    flex: 1;
}

/* 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. */
.biog-format-fill[b-fhd2jfnyjh] {
    min-width: 16px;
}

.biog-format-count[b-fhd2jfnyjh] {
    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-legend[b-fhd2jfnyjh] {
    margin-bottom: 14px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
}

/* Anchor variant — entire row is clickable, navigates to artist page. */

.biog-ranking-chevron[b-fhd2jfnyjh] {
    color: var(--text-dim);
    font-size: 1.125rem;
    margin-left: auto;
}

.biog-ranking-rank[b-fhd2jfnyjh] {
    width: 24px;
    flex-shrink: 0;
}

.biog-metric-card-venues[b-fhd2jfnyjh] {
    grid-column: span 2;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: stretch;
    padding: 0;
    overflow: hidden;
}

/* This card zeroes its own padding so the map can run to the edge, so the text half has to put
   it back — with the SAME values .stat uses, or its lines sit lower and further in than the
   plain cards beside it. Two hand-picked numbers were 4px and 2px out. */
.biog-metric-venues-text[b-fhd2jfnyjh] { padding: var(--space-3) var(--space-4); }

/* Top-aligned, not centred: this card sits in a row of .stat cards whose label starts at the
   top, and centring drops its first line below theirs. */

.biog-metric-venues-map[b-fhd2jfnyjh] {
    /* 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);
    background: rgba(255, 255, 255, 0.03);
    overflow: hidden;
    min-height: 110px;
}

/* =========================================================================
   Phase C — Biography venues map (stat card C1 + full-page C2).
   ========================================================================= */

.biog-venues-preview-canvas[b-fhd2jfnyjh] {
    background: transparent;
}

/* A real heading for the section, not the small-caps caption used for panel labels: this one
   opens a section of the page rather than labelling a control. */
.biog-section-title[b-fhd2jfnyjh] {
    margin: 0 0 var(--space-1);
    color: var(--text-primary);
    font-size: var(--display-sm);
    font-weight: 600;
}

/* The filter tags sat directly under the ranking note with nothing between them. */
.biog-ranking-legend[b-fhd2jfnyjh] {
    margin-top: var(--space-3);
}

/* These three blocks live here rather than in app.css because the classes are defined in THIS
   sheet: a scoped rule carries the component's attribute selector, a global one does not, and a
   media query adds no specificity to make up the difference. Held globally they never applied. */
@media (max-width: 849px) {
    .biog-page[b-fhd2jfnyjh] { padding: 18px 14px 28px; }
    .biog-columns[b-fhd2jfnyjh] { grid-template-columns: 1fr; }
    .biog-title[b-fhd2jfnyjh] { font-size: var(--display-sm); }
}

/* 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[b-fhd2jfnyjh] {
        grid-column: 1 / -1;
    }

    .biog-metric-venues-map[b-fhd2jfnyjh] {
        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[b-fhd2jfnyjh] {
        grid-template-columns: 1fr;
    }

    .biog-metric-venues-map[b-fhd2jfnyjh] {
        border-left: none;
        border-top: 1px solid var(--accent-border);
        aspect-ratio: 16 / 9;
    }
}

a.list-row:hover .biog-ranking-chevron[b-fhd2jfnyjh] { color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
    .biog-metric-spinner[b-fhd2jfnyjh] { animation-duration: 1400ms; }
}
/* /Components/Pages/BiographyVenues.razor.rz.scp.css */
/* The venues map page's own: the map panel, its filters and the venue list. */

.biography-venues-breadcrumb[b-zcbfa9g8cd] {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 24px 8px;
    flex-shrink: 0;
}

.biography-venues-header[b-zcbfa9g8cd] {
    flex-shrink: 0;
    padding: 4px 24px 12px;
    border-bottom: 1px solid var(--border);
}

.biography-venues-title[b-zcbfa9g8cd] {
    font-size: var(--display-sm);
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 4px;
}

.biography-venues-filters[b-zcbfa9g8cd] {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
    align-items: center;
    margin-top: var(--space-3);
}

.biography-venues-country-select[b-zcbfa9g8cd] {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    color: var(--text-primary);
    border-radius: 999px;
    padding: 6px 12px;
    font-size: var(--text-sm);
}

.biography-venues-body[b-zcbfa9g8cd] {
    flex: 1;
    display: grid;
    grid-template-columns: minmax(0, 1fr) 320px;
    overflow: hidden;
}

.biography-venues-map-canvas[b-zcbfa9g8cd] {
    background: transparent;
}

.biography-venues-map-slot[b-zcbfa9g8cd] {
    overflow: hidden;
    border-right: 1px solid var(--border);
}

.biography-venues-sidebar[b-zcbfa9g8cd] {
    overflow-y: auto;
    padding: 12px 18px;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.biography-venues-sidebar-title[b-zcbfa9g8cd] { margin: 4px 0 8px; }

.biography-venues-sidebar-row[b-zcbfa9g8cd] {
    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[b-zcbfa9g8cd] {
    background: rgba(255, 255, 255, 0.04);
}

.biography-venues-sidebar-row:hover .biography-venues-sidebar-name[b-zcbfa9g8cd] {
    color: var(--accent-light);
}

.biography-venues-sidebar-row:hover .biography-venues-sidebar-chevron[b-zcbfa9g8cd] {
    color: var(--accent);
    transform: translateX(2px);
}

.biography-venues-sidebar-name[b-zcbfa9g8cd] {
    font-size: var(--text-md);
    color: var(--text-primary);
    flex: 1;
    min-width: 0;
}

.biography-venues-sidebar-visits[b-zcbfa9g8cd] {
    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[b-zcbfa9g8cd] {
    font-size: var(--text-lg);
    color: var(--text-muted);
    flex-shrink: 0;
    transition: color 0.15s, transform 0.15s;
}

.biography-venues-empty[b-zcbfa9g8cd] {
    padding: 40px 20px;
    text-align: center;
    color: var(--text-dim);
    grid-column: 1 / -1;
}

/* Lives here, not in app.css: these classes are defined in THIS sheet, so a scoped rule carries
   the component's attribute selector while a global override does not — and a media query adds no
   specificity to close the gap. The same block in app.css never applied, so the page stayed
   two-column with a fixed 320px sidebar on a phone. */
@media (max-width: 900px) {
    .biography-venues-layout[b-zcbfa9g8cd] {
        overflow-y: auto;
    }

    .biography-venues-body[b-zcbfa9g8cd] {
        grid-template-columns: 1fr;
        flex: 0 0 auto;
        overflow: visible;
    }

    .biography-venues-map-slot[b-zcbfa9g8cd] {
        border-right: none;
        height: 55vh;
        min-height: 260px;
    }

    .biography-venues-sidebar[b-zcbfa9g8cd] {
        overflow-y: visible;
    }
}

/* A closed venue reads dim. This MUST sit in this sheet: in app.css it lost to the scoped
   .biography-venues-sidebar-name above — same (0,2,0) specificity, and the scoped bundle loads
   second — so a closed venue rendered at full --text-primary from the day the base rule moved. */
.biography-venues-sidebar-name.closed[b-zcbfa9g8cd] {
    color: var(--text-dim);
}
/* /Components/Pages/ConcertDetailComponent.razor.rz.scp.css */
/* Positioning context for the tap-dot tooltips inside the hero. */
.concert-detail-shell[b-1oumx73tup] {
    position: relative;
}

/* ::deep because the tab strip is rendered by a child component and carries no scope
   attribute of its own. The album tracklist sits flush by design, so this is not shared. */
.concert-detail-shell[b-1oumx73tup]  .detail-tabs {
    margin-bottom: 10px;
}

@media (max-width: 849px) {
    .concert-detail-shell[b-1oumx73tup] {
        height: auto;
        overflow: visible;
    }
}

.concert-hero-venue[b-1oumx73tup] {
    font-size: var(--text-xl);
    color: var(--text-muted);
    margin-bottom: 14px;
}

.concert-detail-cards[b-1oumx73tup] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 16px;
    /* The insight panel directly above carries no spacing of its own, so without this the two
       surfaces touch. */
    margin: 16px 0;
}

@media (max-width: 720px) {
    .concert-detail-cards[b-1oumx73tup] {
        grid-template-columns: 1fr;
        gap: 12px;
    }
}

/* A bare wrapper button around the image — NOT a chip-remove, whose fixed 20px circle would clip
   the photo. line-height: 0 stops the inline image reserving a text baseline below itself. */
.concert-image-thumb-btn[b-1oumx73tup] {
    display: block;
    padding: 0;
    border-radius: 5px;
    line-height: 0;
}

.concert-image-thumb-btn:focus-visible[b-1oumx73tup] {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.concert-image-thumb--clickable[b-1oumx73tup] { cursor: pointer; transition: opacity 0.15s; }

.concert-image-thumb--clickable:hover[b-1oumx73tup] { opacity: 0.8; }

.setlist-section-break td[b-1oumx73tup] {
    padding-top: 16px;
    padding-bottom: 6px;
}

/* The shared .hero-title supplies the type. This adds only the row that lets the setlist.fm chip
   share the line, aligned on the artist name's baseline so it reads as attached to it. */
.concert-hero-title[b-1oumx73tup] {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: 10px;
    margin-bottom: 6px;
}

/* Trailing mark on a chip that navigates. */
.chip-chevron[b-1oumx73tup] {
    color: var(--accent);
    font-weight: 600;
    margin-left: 2px;
}

/* The chip label. Without this the line box makes the pill taller than its min-height. */
.chip-name[b-1oumx73tup] { line-height: 1; }
/* /Components/Pages/ConcertEditComponent.razor.rz.scp.css */
/* The venue picker, the image uploader and the photo tiles: this screen's own features. */

.concert-edit-image-wrap[b-bjyixz6spz] { position: relative; display: inline-block; }

.concert-image-remove[b-bjyixz6spz] {
    position: absolute;
    top: -6px;
    right: -6px;
    width: 20px;
    height: 20px;
    border-radius: 50%;
    background: var(--danger);
    border: none;
    color: white;
    font-size: var(--text-xs);
    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[b-bjyixz6spz], .concert-image-remove:focus[b-bjyixz6spz] { opacity: 0.85; outline: none; }

.venue-picker-name[b-bjyixz6spz] {
    font-size: var(--text-md);
    color: var(--text-primary);
    font-weight: 500;
    line-height: 1.3;
}

.venue-picker-sub[b-bjyixz6spz] {
    font-size: var(--text-xs);
    color: var(--text-dim);
    margin-top: 2px;
    line-height: 1.3;
}

.venue-picker-use-as-typed[b-bjyixz6spz] {
    background: rgba(255, 255, 255, 0.02);
}

.venue-picker-use-as-typed strong[b-bjyixz6spz] {
    color: var(--text-primary);
    font-style: normal;
}

.venue-picker-add-new[b-bjyixz6spz] {
    font-size: var(--text-sm);
    color: var(--accent);
    background: rgba(167, 139, 250, 0.06);
}

.venue-picker-add-new strong[b-bjyixz6spz] {
    color: var(--text-primary);
}

.track-picker-row[b-bjyixz6spz] {
    display: flex;
    gap: 6px;
    align-items: center;
}

.venue-linked-chip[b-bjyixz6spz] {
    display: flex;
    align-items: center;
    gap: 12px;
    padding: 10px 12px;
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
    border-left: 3px solid var(--accent);
    border-radius: 6px;
    min-height: 56px;
}

.venue-linked-name[b-bjyixz6spz] {
    font-size: var(--text-lg);
    color: var(--text-primary);
    font-weight: 600;
    line-height: 1.3;
}

.venue-linked-sub[b-bjyixz6spz] {
    font-size: var(--text-sm);
    color: var(--text-dim);
    margin-top: 2px;
    line-height: 1.3;
}

/* ─── Concert image uploader (ConcertEditComponent) ──────────────────────── */
/* Custom concert-image uploader. A styled label wraps a hidden file input
   (the picker); concert-upload.js does the sequential POSTs and reports
   per-file progress into .concert-upload-list. */
.concert-uploader[b-bjyixz6spz] {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.concert-upload-btn[b-bjyixz6spz] {
    position: relative;
}

/* The <input type="file"> is the focus target (deliberately — it covers the label so
   the whole control is the hit area), and it is opacity: 0. Opacity applies to the
   outline, so tabbing to it produced NO visible indicator anywhere. The ring has to
   go on the visible label instead. WCAG 2.4.7 Focus Visible. */
.concert-upload-btn:focus-within[b-bjyixz6spz] {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.concert-upload-btn input[type="file"][b-bjyixz6spz] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    padding: 0;
    border: 0;
    opacity: 0;
    cursor: pointer;
}

.concert-upload-list[b-bjyixz6spz] { gap: 6px; }

.concert-upload-item[b-bjyixz6spz] {
    display: flex;
    align-items: center;
    gap: 10px;
    font-size: var(--text-sm);
    color: var(--text-secondary);
    background: var(--bg-card);
    border: 1px solid var(--border);
    border-radius: 6px;
    padding: 6px 10px;
}

.cu-name[b-bjyixz6spz] {
    flex: 1 1 auto;
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.cu-status[b-bjyixz6spz] {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 8px;
    white-space: nowrap;
}

.cu-bar[b-bjyixz6spz] {
    --meter-height: 6px;
    width: 90px;
}

.cu-pct[b-bjyixz6spz] {
    font-variant-numeric: tabular-nums;
    color: var(--text-dim);
    min-width: 32px;
    text-align: right;
}

.cu-queued[b-bjyixz6spz] {
    color: var(--text-dim);
    font-style: italic;
}

.cu-processing[b-bjyixz6spz] {
    color: var(--accent-light);
}

.cu-mini-spin[b-bjyixz6spz] {
    width: 12px;
    height: 12px;
    border-radius: 50%;
    border: 2px solid var(--border-med);
    border-top-color: var(--accent);
    animation: activity-spin 0.8s linear infinite;
}

.cu-done[b-bjyixz6spz] {
    color: var(--owned);
    font-weight: 600;
}

.cu-failed[b-bjyixz6spz] {
    color: var(--danger);
    /* Allow a long server message to wrap rather than blow out the row. */
    white-space: normal;
    text-align: right;
}

@media (prefers-reduced-motion: reduce) {
    .cu-mini-spin[b-bjyixz6spz] { animation-duration: 1600ms; }
}

/* Mobile: stack the unlink button below the info instead of beside it,
   keep tap targets large (≥44px). */
@media (max-width: 600px) {
    .venue-linked-chip[b-bjyixz6spz] {
        flex-direction: column;
        align-items: stretch;
        gap: 8px;
    }
    /* The button is .btn .btn-sm now and already meets 44px; it still needs to go
       full-width when the chip stacks. */
    .venue-linked-chip .btn[b-bjyixz6spz] {
        width: 100%;
    }
}

/* On mobile let the failed-message and progress wrap under the filename so the
   row never overflows the edit sheet. */
@media (max-width: 849px) {
    .concert-upload-item[b-bjyixz6spz] {
        flex-wrap: wrap;
        align-items: flex-start;
    }
    .cu-failed[b-bjyixz6spz] { text-align: left; }
}

/* The form is a plain stack; the fields inside it are the shared .field. */
.concert-form[b-bjyixz6spz] {
    display: flex;
    flex-direction: column;
    gap: 16px;
    /* The container the setlist-table block below queries. The form renders in TWO hosts — the
       concerts pane and the artist page's edit drawer — and only the form's own width says which
       collapse state is right: a 480px drawer on a 1300px screen crushed the title column to two
       characters because the old viewport query never fired. Width comes from the host, so
       inline-size containment costs nothing. */
    container-type: inline-size;
}

/* A value you cannot edit. It keeps the input's box so it does not read as a third kind of
   control beside the real inputs, and dims the text to say it is not editable. */
.form-input-readonly[b-bjyixz6spz] {
    /* A div does not centre its text the way an input does, so it needs saying explicitly. */
    display: flex;
    align-items: center;
    color: var(--text-dim);
}

@media (max-width: 849px) {
    .concert-edit-shell[b-bjyixz6spz] {
                height: auto;
                overflow: visible;
            }
}

/* Hidden by default and revealed inside the 600px block below. Both halves MUST sit in this order:
   they score the same, so a default placed AFTER the media query wins at every width — which blanked
   the header entirely, the abbreviation hidden and the full word screen-reader-only. */
.setlist-enc-abbr[b-bjyixz6spz] { display: none; }

/* When the FORM is narrow the browser sized these columns from their content: the three order
   buttons and the encore box took what they wanted and the title input - the only thing anyone
   types into - was left about 100px wide. table-layout: fixed makes the widths declared rather
   than negotiated, so the title gets everything the controls do not need. The row stays a row:
   the headers keep their meaning and every control keeps its column.
   A CONTAINER query on .concert-form, not a viewport query: the form also renders in the artist
   page's edit drawer, which is ~480px wide on any desktop - a viewport threshold can never
   describe that host. Same 600px figure as before, measured where the table actually lives. */
@container (max-width: 600px) {
    .concert-setlist-table[b-bjyixz6spz] {
        table-layout: fixed;
        width: 100%;
    }

    .concert-setlist-table th[b-bjyixz6spz],
    .concert-setlist-table td[b-bjyixz6spz] {
        padding-left: var(--space-1);
        padding-right: var(--space-1);
    }

    .concert-setlist-table .col-num[b-bjyixz6spz] { width: 24px; }

    /* Right-aligned so the checkbox sits with the order controls rather than stranded in the
       middle, which hands the width back to the title. */
    .concert-setlist-table th:nth-child(3)[b-bjyixz6spz],
    .concert-setlist-table td:nth-child(3)[b-bjyixz6spz] {
        width: 40px;
        text-align: right;
    }

    .concert-setlist-table th[b-bjyixz6spz] {
        font-size: var(--text-xs);
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* "Encore" needs ~48px at this size and the column is 40px, so it ellipsised to "ENC…" — a
       header that cannot be read defeats the point of keeping the row a row. The full word stays
       in the accessibility tree; only the visible text shortens. */
    .setlist-enc-full[b-bjyixz6spz] {
        position: absolute;
        width: 1px;
        height: 1px;
        margin: -1px;
        overflow: hidden;
        clip-path: inset(50%);
        white-space: nowrap;
    }

    .setlist-enc-abbr[b-bjyixz6spz] { display: inline; }

    .concert-setlist-table .col-actions[b-bjyixz6spz] { width: 104px; }

    /* Three 32px targets in 104px only fit with the gap closed; they stay 32px, which clears
       the 24px WCAG 2.5.8 floor. */
    .concert-setlist-table .col-actions .btn-row[b-bjyixz6spz] { gap: 0; }
}

.choice[b-bjyixz6spz] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--min-touch);
    cursor: pointer;
}

.choice input[type="checkbox"][b-bjyixz6spz],
.choice input[type="radio"][b-bjyixz6spz] {
    width: 20px;
    height: 20px;
    margin: 0;
    flex: 0 0 auto;
    accent-color: var(--accent);
    cursor: pointer;
}

.choice input:focus-visible[b-bjyixz6spz] {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}

.choice input:disabled[b-bjyixz6spz],
.choice:has(input:disabled)[b-bjyixz6spz] {
    opacity: 0.5;
    cursor: not-allowed;
}

/* Numeric columns align right on their decimal and use tabular figures, so
   digits sit in columns down the table. Proportional figures make a column of
   numbers ragged even when it is right-aligned. */
.data-table .col-num[b-bjyixz6spz] {
    text-align: right;
    font-variant-numeric: tabular-nums;
}

/* The identity column stays put while the row scrolls sideways — without it, a
   wide table scrolled to the right is a grid of values belonging to nobody. */
.data-table .col-pin-end[b-bjyixz6spz] {
    position: sticky;
    right: 0;
    z-index: 1;
}

.data-table tbody td.col-pin-end[b-bjyixz6spz] { background: inherit; }

.data-table th.col-pin-end[b-bjyixz6spz] { z-index: 3; }
/* /Components/Pages/ConcertsPage.razor.rz.scp.css */
/* This page's own features: the "+ New" menu, the setlist.fm browse results and the
   bulk-import confirmation panel. */

/* ─── Concerts content wrapper ──────────────────────────────────────────────
   .app-body (the routed-page parent) is a plain block with a fixed height,
   not flex — so .view-2pane's own height:100% resolved against that FULL
   height regardless of a preceding OnboardingBanner, pushing the pane's
   bottom edge past .app-body's overflow:hidden boundary and clipping content
   at the foot of the detail pane permanently out of reach. Same fix as
   LibraryView.razor.css's .library-content: wrap banner + pane in a local
   flex column so the banner (flex-shrink:0, its own rule) keeps its natural
   height and the pane takes exactly what's left. */
.concerts-content[b-rv1fa88ubr] {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* Positioning context for .menu — .menu is absolute and needs an anchor. */
.new-concert-dropdown[b-rv1fa88ubr] { position: relative; }

/* 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[b-rv1fa88ubr] {
    padding: 24px;
    display: flex;
    flex-direction: column;
    gap: 14px;
    height: 100%;
    overflow: auto;
}

.linked-import-title[b-rv1fa88ubr] {
    font-size: var(--text-2xl);
    font-weight: 700;
    color: var(--text-primary);
    margin-bottom: 4px;
}

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

.linked-import-row[b-rv1fa88ubr] {
    border-radius: 4px;
    background: var(--bg-card);
    border: 1px solid var(--border);
}

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

.linked-import-row.is-selected[b-rv1fa88ubr] {
    background: var(--accent-bg);
    border-color: var(--accent-border);
}

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

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

/* Artist picker inline dropdown */

.concert-artist-pick-list[b-rv1fa88ubr] {
    overflow-y: auto;
}

/* The artist-search row sits flush against the top of .detail-content, which scrolls and so clips
   at its padding box. The focus outline is drawn 3px outside the input (2px ring + 1px offset), so
   the top of the ring was cut off. Room is taken here rather than as padding on .detail-content:
   that padding is deliberately 0 because Chromium anchors sticky headers to the content box, and
   a top band there lets scrolled rows show through above a sticky column header. */
.detail-content > .chip-row:first-child[b-rv1fa88ubr] {
    margin-top: 3px;
}

/* /Components/Pages/CoverWall.razor.rz.scp.css */
/* This component's own rules. */

/* Rows of a fixed column count in BOTH modes, never auto-fill: the caller measures its pane and
   passes the count, the seek maths and the arrow keys assume it, and a grid that chose its own
   would put the tile a seek or an ArrowDown aimed at on another row. Small tiles deliberately
   lean into "scan a lot of covers at once" rather than "showcase one big cover". */

.cover-wall-grid[b-9atqfbirhv] {
    padding: 10px;
}

/* Album title shown INSIDE the tile when there is no cover art — not a
   placeholder glyph, because a title is more use than a musical note when you
   are scanning for one release. */
.cover-wall-fallback-text[b-9atqfbirhv] {
    padding: 8px;
    color: var(--text-dim);
    font-size: var(--text-sm);
    line-height: 1.3;
    text-align: center;
    overflow: hidden;
}

/* Placement only — the mark's size and its shadow belong to WantedDot, which owns them for every
   surface it appears on. Hangs from the top edge the way a bookmark sits in a book, and takes the
   same corner as the owned tick because a tile never shows both at once: an owned tile marked
   wanted means you hold one pressing and want another, and the want is the part worth marking. */
.thumb-wanted[b-9atqfbirhv] {
    position: absolute;
    top: 0;
    right: var(--space-2);
}

/* The wall's column count. The wrapper overrides it inline from the Columns parameter; this is
   the token's home so a stylesheet-only reader sees it exists. */
.cover-wall-rows[b-9atqfbirhv] {
    --wall-columns: 6;
}

/* One row of the wall. The gap beneath is part of the row so the measured height is the
   distance to the next row, not just the tile. */
.cover-wall-row[b-9atqfbirhv] {
    display: grid;
    grid-template-columns: repeat(var(--wall-columns), minmax(0, 1fr));
    gap: var(--space-3);
    padding-bottom: var(--space-3);
}
/* /Components/Pages/DiscographyCheckPage.razor.rz.scp.css */
/* This component's own rules. */

/* A SUGGESTION ROW WITH A THUMBNAIL, as its own class rather than as a change to
   .popover-item. The obvious fix — making .popover-item a flex row — breaks the
   venue picker, whose template is a name STACKED over an address: as a row those
   two would sit side by side. So the block default stays, and a template that
   needs a row asks for one. */
.popover-media[b-62tsv4nscu] {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    min-width: 0;
}

.rg-info[b-62tsv4nscu] { flex: 1; min-width: 0; }

.discog-artist-picker[b-62tsv4nscu] { position: relative; flex: 1; }

/* .bar is a row, so the artist picker sat beside the heading and got whatever width was left.
   Stacked, it spans the pane like the library filter. */
.discog-header[b-62tsv4nscu] {
    flex-direction: column;
    align-items: stretch;
    gap: var(--space-2);
}

.discog-header .discog-artist-picker[b-62tsv4nscu] {
    width: 100%;
}

.result-meta[b-62tsv4nscu] {
    margin-left: auto;
    flex-shrink: 0;
}
/* /Components/Pages/EuRepresentativeLocations.razor.rz.scp.css */
/* This component's own rules. */

/* DataRep's EU/EEA postal addresses (/privacy/eu-representative-locations).
   A definition list, not a table: country → address is genuinely term/definition,
   and it stacks on a phone without the horizontal-scroll a 2-column table needs. */
.rep-locations[b-hv6phkcac5] {
    margin: 0 0 14px;
}

.rep-locations dt[b-hv6phkcac5] {
    font-weight: 600;
    color: var(--text-primary);
    margin: 16px 0 2px;
}

.rep-locations dd[b-hv6phkcac5] {
    margin: 0;
}
/* /Components/Pages/ImageGalleryComponent.razor.rz.scp.css */
/* This component's own rules. */

.concert-gallery-img[b-et9h00pt69] {
    max-width: 100%; max-height: calc(90vh - 56px);
    border-radius: 6px; object-fit: contain; display: block;
}

.concert-gallery-nav[b-et9h00pt69] {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 16px;
    margin-top: 12px;
    width: fit-content;
    margin-inline: auto;
    padding: 6px 12px;
    border-radius: var(--radius-pill);
    background: rgba(0, 0, 0, 0.55);
    color: white;
}

.concert-gallery-counter[b-et9h00pt69] { font-size: var(--text-md); color: rgba(255,255,255,0.7); min-width: 50px; text-align: center; }
/* /Components/Pages/ImportCsvPage.razor.rz.scp.css */
/* A BLOCK deliberately, on top of .page-column: as a flex column the cards become flex items, which
   default to shrinking to fit the fixed height rather than overflowing it, and .card hides the
   overflow they would otherwise have spilled. */
.import-page[b-nilqqvmoeo] { --page-column-max: 760px; }

/* ::deep because the cards are a child component's markup: this sheet's scope attribute is never
   written onto it, so a plain `.import-page > *` matches the page's own elements and silently
   misses every Card. */
.import-page[b-nilqqvmoeo] >  * {
    margin-bottom: var(--space-4);
}

.import-page[b-nilqqvmoeo] >  *:last-child {
    margin-bottom: 0;
}

.import-title[b-nilqqvmoeo] {
    margin: 0 0 var(--space-4);
    color: var(--text-primary);
    font-size: var(--display-sm);
    font-weight: 600;
}

/* .card-title reserves two lines so titles align across a grid of cards. These cards are stacked
   full width, one per row, so there is nothing to align to and the reservation reads as a gap
   under every heading. */
.import-page .card-title[b-nilqqvmoeo] {
    min-height: 0;
}

/* Numbers read as the first word of their sentence, so they sit inline rather than in a column
   the eye has to cross. */
.import-found[b-nilqqvmoeo] {
    margin: 0;
    padding: 0;
    list-style: none;
    color: var(--text-secondary);
    font-size: var(--text-xl);
    line-height: 1.9;
}

.import-found strong[b-nilqqvmoeo] {
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
}

/* The column names and the sample header line are single unbroken tokens — a browser has nowhere
   to break `release_id,Collection Media Condition,...`, so at any width narrower than the string
   it pushes the table wider than the page. No ::deep: these elements are authored in this
   component, so they carry its scope attribute even though a Card renders them. */
.import-page code[b-nilqqvmoeo] {
    overflow-wrap: anywhere;
}

.import-steps[b-nilqqvmoeo] {
    margin: 0;
    padding-left: 20px;
    color: var(--text-secondary);
    font-size: var(--text-lg);
    line-height: 1.6;
}

@media (max-width: 849px) {
    .import-page[b-nilqqvmoeo] { --page-column-padding: var(--space-4); }
}
/* /Components/Pages/LandingPage.razor.rz.scp.css */
/* The landing page sits outside the design system: it is marketing, not the app, so its
   sizes and spacing are its own rather than vocabulary the rest of the app should follow. */

.landing-cta[b-jwz6h25an3] {
    padding: 15px 32px;
    font-size: var(--text-xl);
}

.landing-hero-headline:focus[b-jwz6h25an3] { outline: none; }

.landing[b-jwz6h25an3] {
    
    height: 100%;
    overflow-y: auto;
    background: var(--bg-base);
}

.landing > section[b-jwz6h25an3] {
    padding-left: 48px;
    padding-right: 48px;
}

.landing-hero[b-jwz6h25an3] {
    text-align: center;
    padding-top: 120px;
    padding-bottom: 100px;
    border-bottom: 1px solid var(--border);
    position: relative;
    overflow: hidden;
}

.landing-hero[b-jwz6h25an3]::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-wordmark[b-jwz6h25an3] {
    
    font-size: var(--text-xl);
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--text-secondary);
    margin: 0 0 12px;
    position: relative;
}

.landing-wordmark-tm[b-jwz6h25an3] {
    font-size: 0.6em;
    vertical-align: super;
    margin-left: 1px;
}

.landing-tagline[b-jwz6h25an3] {
    
    font-size: var(--text-sm);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent-light);
    margin: 0 0 24px;
    position: relative;
}

.landing-hero-headline[b-jwz6h25an3] {
    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[b-jwz6h25an3] {
    
    font-style: italic;
    color: var(--accent-light);
}

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

.landing-cta-group[b-jwz6h25an3] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 14px;
    position: relative;
}

.landing-cta-trust[b-jwz6h25an3] {
    font-size: var(--text-sm);
    color: var(--text-dim);
    line-height: 1.55;
    margin: 0;
    max-width: 460px;
    text-align: center;
    
    text-wrap: balance;
}

/* Read BEFORE the click. .landing-cta-trust on its own reassures someone who has already decided;
   this answers the question that stops them deciding, so it takes a readable weight rather than the
   dim one. Layered on that class rather than restating it, and qualified so source order cannot
   decide which of the two wins. */
.landing-cta-trust.landing-cta-lead[b-jwz6h25an3] {
    color: var(--text-secondary);
    font-size: var(--text-md);
}

.landing-cta-lead strong[b-jwz6h25an3] {
    color: var(--text-primary);
    font-weight: 500;
}

/* Two doors rather than an action and its footnote: without the rules either side, the second CTA
   reads as a caption of the first. */
.landing-cta-or[b-jwz6h25an3] {
    display: flex;
    align-items: center;
    align-self: stretch;
    gap: var(--space-3);
    color: var(--text-dim);
    font-size: var(--text-xs);
}

.landing-cta-or[b-jwz6h25an3]::before,
.landing-cta-or[b-jwz6h25an3]::after {
    content: '';
    flex: 1;
    height: 1px;
    background: var(--divider);
}

.landing-stats[b-jwz6h25an3] {
    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-lead[b-jwz6h25an3] {
    
    margin: 0 auto 24px;
    max-width: 720px;
    text-wrap: balance;
    font-size: var(--text-2xl);
    line-height: 1.6;
    color: var(--text-secondary);
    text-align: center;
}

.landing-stats-cells[b-jwz6h25an3] {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 24px;
    align-items: stretch;
}

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

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

.landing-stats-text[b-jwz6h25an3] {
    margin: 0;
    font-size: var(--text-xl);
    line-height: 1.5;
    color: var(--text-primary);
}

.landing-feature[b-jwz6h25an3] {
    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;
}

.landing-feature-reverse .landing-feature-demo[b-jwz6h25an3] {
    order: -1;
}

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

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

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

.landing-feature-demo[b-jwz6h25an3] {
    min-width: 0;
}

.landing-demo-composite[b-jwz6h25an3] {
    position: relative;
}

.landing-phone-inset[b-jwz6h25an3] {
    position: absolute;
    bottom: -2px;
    right: -2px;
    width: 126px;
    background: #000;
    border: 5px solid #2c2c2c;
    border-radius: 22px;
    padding: 13px 7px 9px;
    box-shadow: 0 16px 38px rgba(0, 0, 0, 0.62);
    z-index: 3;
}

.landing-phone-inset[b-jwz6h25an3]::before {
    
    content: '';
    position: absolute;
    top: 7px;
    left: 50%;
    transform: translateX(-50%);
    width: 32px;
    height: 3px;
    border-radius: 3px;
    background: #3a3a3a;
}

.landing-phone-vid[b-jwz6h25an3] {
    width: 100%;
    display: block;
    border-radius: 11px;
}

.landing-pricing[b-jwz6h25an3] {
    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[b-jwz6h25an3] {
    font-family: 'DM Serif Display', Georgia, serif;
    font-weight: 400;
    font-size: 2.25rem;
    color: var(--text-primary);
    margin: 0 0 10px;
}

.landing-pricing-sub[b-jwz6h25an3] {
    font-size: var(--text-xl);
    color: var(--text-secondary);
    margin: 0 0 48px;
}

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

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

.landing-pricing-card-featured[b-jwz6h25an3] {
    
    border-color: rgba(124, 92, 245, 0.55);
    box-shadow: 0 0 0 1px rgba(124, 92, 245, 0.2);
}

.landing-pricing-badge[b-jwz6h25an3] {
    
    display: inline-block;
    position: absolute;
    top: -12px;
    left: 50%;
    transform: translateX(-50%);
    padding: 4px 12px;
    background: #a78bfa;
    color: #161324;
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    border-radius: 4px;
    white-space: nowrap;
}

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

.landing-pricing-savings[b-jwz6h25an3] {
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--accent-light);
    margin: 12px 0 0;
}

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

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

@media (max-width: 720px) {
    .landing > section[b-jwz6h25an3] { padding-left: 24px;
        padding-right: 24px;
        }
    .landing-hero[b-jwz6h25an3] { padding-top: 72px;
        padding-bottom: 64px;
        }
    .landing-hero-sub[b-jwz6h25an3] { font-size: var(--text-xl);
        }
    .landing-stats[b-jwz6h25an3] { padding: 24px 24px;
        }
    .landing-stats-cells[b-jwz6h25an3] { grid-template-columns: 1fr;
        gap: 14px;
        }
    .landing-stats-lead[b-jwz6h25an3] { font-size: var(--text-xl);
        margin-bottom: 18px;
        }
    .landing-stats-cell[b-jwz6h25an3] { flex-direction: row;
        align-items: center;
        text-align: left;
        gap: 16px;
        padding: 6px 4px;
        }
    .landing-feature[b-jwz6h25an3] { grid-template-columns: 1fr;
        gap: 32px;
        padding-top: 56px;
        padding-bottom: 56px;
        }
    .landing-phone-inset[b-jwz6h25an3] { width: 108px;
        }
    .landing-feature .landing-feature-demo[b-jwz6h25an3], .landing-feature-reverse .landing-feature-demo[b-jwz6h25an3] { order: 0;
        }
    .landing-pricing[b-jwz6h25an3] { padding-top: 64px;
        padding-bottom: 64px;
        }
    .landing-pricing-cards[b-jwz6h25an3] { grid-template-columns: 1fr;
        max-width: 380px;
        }
}

/* One box for both demo slots: a still keeps its natural aspect ratio
   (launch-demo media plan: gigs + biography slots are real-screen stills). */
.demo-still[b-jwz6h25an3],
.demo-video[b-jwz6h25an3] {
    width: 100%;
    display: block;
    border-radius: 12px;
    border: 1px solid var(--border);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
}

/* Demo video — class name preserved because demo-playback.js queries on it. */
.demo-video[b-jwz6h25an3] {
    aspect-ratio: 16 / 9;
    background: var(--bg-panel);
}
/* /Components/Pages/LibraryView.razor.rz.scp.css */
/* The library's own: the view switcher, the filter bar and the empty states. */

/* ─── 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[b-wdwjw47sqs] {
    display: flex;
    flex-direction: column;
    flex: 1;
    min-height: 0;
}

/* ─── Library content wrapper ───────────────────────────────────────────────
   .app-body (the routed-page parent) is a plain block with a fixed height,
   not flex — so .view-3pane's own height:100% resolved against that FULL
   height regardless of a preceding OnboardingBanner, pushing the pane's
   bottom edge past .app-body's overflow:hidden boundary and clipping content
   near the foot of the detail pane (e.g. Notes) permanently out of reach —
   not just requiring a scroll, genuinely unreachable. Wrapping the banner and
   the pane in a local flex column lets the banner (flex-shrink:0, its own
   rule) keep its natural height and the pane take exactly what's left. */
.library-content[b-wdwjw47sqs] {
    display: flex;
    flex-direction: column;
    height: 100%;
}

/* ─── Welcome Landing (authenticated, empty library) ────────────────────── */
.welcome-landing[b-wdwjw47sqs] {
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    padding: 32px 24px;
}

.welcome-content[b-wdwjw47sqs] {
    max-width: 480px;
    text-align: center;
}

.welcome-icon[b-wdwjw47sqs] {
    font-size: 3rem;
    margin-bottom: 12px;
    color: var(--accent);
}

.welcome-title[b-wdwjw47sqs] {
    font-size: var(--display-sm);
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 12px;
}

.welcome-body[b-wdwjw47sqs] {
    font-size: var(--text-lg);
    color: var(--text-muted);
    line-height: 1.6;
    margin: 0 0 20px;
}

.welcome-actions[b-wdwjw47sqs] {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    justify-content: center;
    margin-bottom: 20px;
}

.welcome-hint[b-wdwjw47sqs] {
    font-size: var(--text-sm);
    color: var(--text-dim);
    line-height: 1.5;
    margin: 0;
}

/* 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[b-wdwjw47sqs] {
    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;
}

/* Chevron is hidden by default (List mode has nothing to collapse). The
   rail-collapsed cascade below re-shows it. */
.rail-toggle[b-wdwjw47sqs] {
    display: none;
}

/* This header carries a two-line title row (view toggle above the artist name) AND the filter.
   .bar lays its children out in a row and centres them, so the filter floats against the middle
   of the title row with dead space beside it. Stacked, it spans the pane. */
.library-header[b-wdwjw47sqs] {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
}

.library-header .input-group[b-wdwjw47sqs] {
    width: 100%;
}

/* The header is a column at gap: 0, so this row sits flush against the filter above it and the
   list below. .btn-row only gaps the buttons WITHIN itself and carries no margin. */
.library-header .btn-row[b-wdwjw47sqs] {
    margin: var(--space-2) 0;
}

/* The scope switch spans the page above the panes, because it selects WHICH pane layout you are
   looking at. Inside the mid pane's header it read as a control over the release list.
   ::deep because TabBar renders the strip and it carries that component's scope, not this one —
   same reason ConcertDetailComponent reaches its tab strip that way. flex:none because
   .library-content is a flex column and the default 0 1 auto lets the bar be squashed by the
   full-height panes below it. */
.library-content[b-wdwjw47sqs]  .library-scope-bar {
    flex: none;
}

/* Wanted drops the artist rail but keeps the detail pane, so it is a TWO-panel view, not a page.
   .pane-mid is clamp(200px, 22%, 260px) for the three-pane library, where it sits between a rail
   and a detail pane; with the rail gone it has to grow or the list renders in a 22% strip. Wider
   than pane-mid and narrower than half, so the list can carry artist + title + year on one line
   while the detail pane still gets the majority — the same balance the artist-scoped view has,
   with one pane removed rather than two. Keeps its right border: it is a divider between panes
   now, not the edge of a page.

   INSIDE the ≥850px query, and that is the whole point rather than tidiness. Below it .view-3pane
   becomes flex-direction: column, where flex-basis stops being a width and becomes a HEIGHT — so
   unscoped, this capped the wanted list at 460px tall with dead space under it and the rest of the
   list unreachable. It also out-specified the mobile .pane-mid { height: 100% } it was breaking,
   which is the pairing G22 exists to catch. */
@media (min-width: 850px) {
    .view-wanted .pane-mid[b-wdwjw47sqs] {
        width: auto;
        flex: 0 0 clamp(300px, 34%, 460px);
    }
}

/* The control that opens and closes the Artists rail.

   HIDDEN BY DEFAULT, and shown only by the .rail-collapsed rules in the ≥850px query below.
   That is the only situation where it does anything: every .rail-expanded selector in this file
   is prefixed .view-3pane.rail-collapsed, so in a wide List view — where the rail has a fixed
   width and never compacts — clicking it flips a class no rule matches. Do not "fix" its
   absence there by styling it: the layout is fixed in that view, so the control has nothing to
   toggle (owner, 23/08/2026). The same default hides it below 850px, where the rail is a
   full-width pane reached by the mobile pane switcher rather than a rail to collapse.

   It takes the letter strip's accent, on the button rather than the icon so the label matches:
   dim grey on a dark rail reads as a disabled box, border or no border, which is what made it
   easy to miss. */
.rail-toggle[b-wdwjw47sqs] {
    display: none;
    color: var(--accent);
}

[b-wdwjw47sqs] .rail-toggle-icon {
    width: 24px;
    height: 24px;
    transition: transform 0.25s ease;
}

/* 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 ≥850px.

   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. */
@media (min-width: 850px) {
    .view-3pane.rail-collapsed .pane-sidebar[b-wdwjw47sqs] {
        /* 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[b-wdwjw47sqs] {
        width: clamp(260px, 38%, 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[b-wdwjw47sqs] {
        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[b-wdwjw47sqs]  .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. The label STACKS under the icon here: the rail is 76px and the
        header keeps 10px of padding each side, so 56px is all there is — an
        icon and a word side by side needs about 68px and would clip. The
        expanded state has 240px and lays the same pair out in a row. */
        .view-3pane.rail-collapsed .rail-toggle[b-wdwjw47sqs] {
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        gap: 2px;
        padding: 4px;
        margin-left: auto;
        border-radius: 6px;
        background: var(--bg-card);
        border: 1px solid var(--accent-border);
        font-size: var(--text-xs);
        font-weight: 500;
        line-height: 1;
        flex-shrink: 0;
        transition: background 0.15s ease, border-color 0.15s ease;
        }

    /* Names the action, because a glyph alone was missed even by the person who
        built it — and this is the ONLY way back to the artist list in Wall and
        Stack view. Decorative: the button's aria-label already says it. */
        .view-3pane.rail-collapsed .rail-toggle[b-wdwjw47sqs]::after {
        content: "Expand";
        }

    .view-3pane.rail-collapsed .rail-toggle:hover[b-wdwjw47sqs] {
        background: var(--bg-hover);
        }

    /* The icon carries the rotation on its own so the label beside it stays upright. */
    .view-3pane.rail-collapsed[b-wdwjw47sqs]  .rail-toggle-icon {
        transform: rotate(180deg);
        }

    /* Title hidden in collapsed-collapsed (no room beside the chevron at 64px). */ .view-3pane.rail-collapsed .rail-content[b-wdwjw47sqs]  .panel-title {
        display: none;
        }

    .view-3pane.rail-collapsed .rail-content[b-wdwjw47sqs]  .input-group,
    .view-3pane.rail-collapsed .rail-content[b-wdwjw47sqs]  .artist-info,
    .view-3pane.rail-collapsed .rail-content[b-wdwjw47sqs]  .list-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[b-wdwjw47sqs]  .list-header {
        padding: 8px 4px 2px;
        text-align: center;
        }

    .view-3pane.rail-collapsed .rail-content[b-wdwjw47sqs]  .list-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;
        }

    .view-3pane.rail-collapsed .rail-content[b-wdwjw47sqs]  .list-row.is-active {
        background: transparent;
        }

    /* The selection ring is the avatar's OWN border thickened, not an outline round it.
        An outline is painted outside the element's box, and .list-scroll is
        overflow-x: hidden with only a couple of pixels either side of a 40px avatar in a
        76px rail — so the ring lost its left and right edges and read as a broken circle.
        A border is inside the box (border-box, see the reset), so it cannot be clipped
        however narrow the rail or wide the scrollbar. */ .view-3pane.rail-collapsed .rail-content[b-wdwjw47sqs]  .list-row.is-active .artist-avatar {
        border-color: var(--accent);
        border-width: 2px;
        }

    /* 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[b-wdwjw47sqs] {
        width: 240px;
        box-shadow: 12px 0 28px rgba(0, 0, 0, 0.55);
    }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .panel-title {
        display: flex;
        align-items: center;
        flex: 1;
        margin-bottom: 0;
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .input-group {
        display: flex;
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .list-header {
        padding: 10px 14px 3px;
        text-align: left;
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .artist-info {
        display: flex;
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .list-count {
        display: inline;
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .list-row {
        justify-content: flex-start;
        padding: 7px 14px;
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .list-row.is-active {
        background: var(--bg-active);
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-content[b-wdwjw47sqs]  .list-row.is-active .artist-avatar {
        border-color: var(--outline);
        border-width: 1px;
        }

    /* 240px of overlay, so the pair goes back to a row and the word changes to the
        action it now performs. */
        .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-toggle[b-wdwjw47sqs] {
        flex-direction: row-reverse;
        gap: 6px;
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded .rail-toggle[b-wdwjw47sqs]::after {
        content: "Collapse";
        }

    .view-3pane.rail-collapsed .pane-sidebar.rail-expanded[b-wdwjw47sqs]  .rail-toggle-icon {
        transform: rotate(0deg);
        }
}
/* /Components/Pages/Pricing.razor.rz.scp.css */
/* This component's own rules. */

.pricing-page[b-ap3eovt8m1] {
    max-width: 820px;
    margin: 0 auto;
    padding: 32px 24px 40px;
    height: 100%;
    overflow-y: auto;
    color: var(--text-primary);
}

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

.pricing-page-header h1[b-ap3eovt8m1] {
    font-size: var(--display-md);
    font-weight: 700;
    margin: 0 0 12px;
}

.pricing-page-subhead[b-ap3eovt8m1] {
    font-size: var(--text-xl);
    color: var(--text-secondary);
    margin: 0;
}

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

.pricing-section h2[b-ap3eovt8m1] {
    font-size: 1.125rem;
    font-weight: 600;
    margin: 0 0 12px;
    color: var(--text-primary);
}

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

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

.pricing-fineprint[b-ap3eovt8m1] {
    margin: 8px 0 0;
}

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

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

.pricing-page-trust[b-ap3eovt8m1] {
    /* Trial/cancellation reassurance line under the pricing header — quieter
       than the subhead, same centring. */
    margin: 8px auto 0;
    max-width: 560px;
    font-size: var(--text-lg);
    color: var(--text-muted);
    text-align: center;
}

/* Held in this sheet: a scoped rule carries the component attribute selector and outranks any
   global override, media query or not. */
@media (max-width: 480px) {
    .pricing-page[b-ap3eovt8m1] {
        padding: 24px 16px 32px;
    }

    .pricing-page-header h1[b-ap3eovt8m1] { font-size: 1.5rem; }
    .pricing-page-subhead[b-ap3eovt8m1] { font-size: var(--text-lg); }
    .pricing-section h2[b-ap3eovt8m1] { font-size: var(--text-2xl); }
}
/* /Components/Pages/Privacy.razor.rz.scp.css */
/* This component's own rules. */

.rep-block[b-dcoktrpfd9] {
    display: flex;
    gap: 16px;
    align-items: flex-start;
}

.rep-badge[b-dcoktrpfd9] {
    flex: 0 0 auto;
    width: 88px;
    height: 88px;
}

/* Held in this sheet: a scoped rule outranks any global override, media query or not. */
/* Stacks rather than squeezing: at 375px the badge + gap would leave the
   paragraph ~220px of measure, which reads badly. */
@media (max-width: 600px) {
    .rep-block[b-dcoktrpfd9] {
        flex-direction: column;
        gap: 12px;
    }
}
/* /Components/Pages/ReleaseGroupTree.razor.rz.scp.css */
/* Release-group tree — parents are releases, children the pressings of them. The parent row
   carries .list-row and adds nothing to it: same flex, same gap, same target height, and the
   button reset already supplies width and alignment. */

/* No box of its own: the rows are the structure, and a wrapper with a layout would break the
   list scroll container they sit in. */
.release-tree[b-uvbgklxlni] { display: contents; }

/* Only the pair gap: .chip-row supplies the flex, and 7px keeps two 7-9px marks reading as a
   deliberate pair rather than one smudge. */
.release-tree-marks[b-uvbgklxlni] { flex: none; gap: 7px; }

/* .meta supplies the size and colour; only the digit alignment and the no-wrap are specific. */
.release-tree-counts[b-uvbgklxlni] {
    flex: none;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

/* A child has to READ as a child. Indent and a lifted ground alone left it looking like the next
   sibling in a flat list — the hierarchy was in the data and nowhere on the screen. The rail is
   what draws it: a vertical line down the indent, turning into the row. */
.release-tree-child[b-uvbgklxlni] {
    position: relative;
    padding-left: 56px;
    background: var(--bg-panel);
}

.release-tree-child[b-uvbgklxlni]::before {
    content: '';
    position: absolute;
    left: 30px;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--border-strong);
}

/* The last child stops the rail halfway and turns it into the row, so a group visibly closes
   rather than the line running on into whatever follows.

   Keyed off a class the component sets, NOT :last-of-type. Parents and children are flat siblings
   of .release-tree, so :last-of-type matched the last div in the entire tree: with everything
   expanded, exactly one group closed its rail and all the others ran straight on into the next
   release. Nothing but a render could show that. */
.release-tree-child.is-last[b-uvbgklxlni]::before {
    bottom: 50%;
}

.release-tree-child[b-uvbgklxlni]::after {
    content: '';
    position: absolute;
    left: 30px;
    top: 50%;
    width: 12px;
    height: 1px;
    background: var(--border-strong);
}

/* Right-aligned counts and marks read as detached at desktop widths — the eye loses the row
   between the title and them. Capping the row keeps them near the content they describe. */
.release-tree-parent .fill[b-uvbgklxlni],
.release-tree-child .fill[b-uvbgklxlni] {
    max-width: 720px;
}
/* /Components/Pages/ReleaseList.razor.rz.scp.css */
/* This component's own rules. */

/* The list and the strip side by side, filling the pane under the controls bar. min-height: 0 is
   load-bearing: without it the flex child grows to its content and the list never scrolls. */
.release-list-body[b-k1umo719d9] {
    display: flex;
    flex: 1;
    min-height: 0;
}
/* /Components/Pages/ScanPage.razor.rz.scp.css */


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

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

.scanner-controls[b-6wz3hqhpt6] {
    display: flex;
    gap: 8px;
    margin-bottom: 8px;
}

.scanner-status[b-6wz3hqhpt6] {
    font-size: 0.8rem;
    color: var(--text-dim);
    margin-bottom: 8px;
    min-height: 1.2em;
}

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

.scanner-barcode-input[b-6wz3hqhpt6] {
    flex: 1;
}

/* The typed or scanned code drawn back as bars: a valid EAN/UPC shows as a symbol, anything
   else as plain digits — instant feedback before the lookup fires. */
.scanner-status[b-6wz3hqhpt6]  .scanner-barcode-preview {
    margin-top: 8px;
    max-width: 160px;
}

/* Targeting reticle — guides user to centre the barcode */
.scanner-reticle[b-6wz3hqhpt6] {
    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);
}

/* Custom spinner overlay. Shown while a barcode search is in flight, centred
   over the results list (parent is position:relative). Reuses the
   activity-spin keyframe defined above. */
.scan-spin-overlay[b-6wz3hqhpt6] {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(15, 14, 18, 0.45);
    z-index: 5;
    pointer-events: none;
}

/* ─── Scan results spinner overlay (ScanPage) ────────────────────────────── */

.scan-spinner[b-6wz3hqhpt6] {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: 3px solid var(--border-med);
    border-top-color: var(--accent);
    animation: activity-spin 0.8s linear infinite;
}

.scanner-error[b-6wz3hqhpt6] { color: var(--danger-text); }

@media (prefers-reduced-motion: reduce) {
    .scan-spinner[b-6wz3hqhpt6] { animation-duration: 1600ms; }
}
/* /Components/Pages/SearchPage.razor.rz.scp.css */
/* The search page owns its result rows and empty states. */

.search-inputs[b-bddcupktvt] {
    padding: 14px 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    border-bottom: 1px solid var(--border);
    flex-shrink: 0;
}

.field-label[b-bddcupktvt] {
    color: var(--text-secondary);
    font-size: var(--text-lg);
    font-weight: 500;
}
/* /Components/Pages/StackView.razor.rz.scp.css */
/* The stack view owns the flick deck and its card faces. */

/* 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[b-wa1zsa4bca] {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    min-height: 100%;
    padding: 20px 16px;
    user-select: none;
}

.stack-stage[b-wa1zsa4bca] {
    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;
}

/* The STAGE is the focusable control (role=button, Enter/Space opens the front album),
   so the ring goes here. The offset keeps it clear of the sleeves, which are absolutely
   positioned to fill the stage and would otherwise sit on top of the outline. */
.stack-stage:focus-visible[b-wa1zsa4bca] {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}

/* Browse-info: title + meta line + position counter ("3 / 12"). JS sets
   textContent on each on every render. */
.stack-info[b-wa1zsa4bca] {
    text-align: center;
    margin-bottom: 12px;
    min-height: 60px;
    max-width: 320px;
}

.stack-info-title[b-wa1zsa4bca] {
    font-size: var(--text-lg);
    font-weight: 600;
    color: var(--text-primary);
    margin-bottom: 4px;
}

/* Built by stack-flick.js, so it never carries this component's scope attribute — ::deep is how a
   scoped sheet reaches a descendant it did not itself render. Inline rather than a flex child:
   .stack-info-title is a centred ellipsis box, and rebuilding it as a flex row just to seat the
   mark would cost an extra element for nothing visible. Paint comes from .wanted-mark in app.css. */
/* On the sleeve, not beside the title. Inline it had two problems that pulled against each other:
   it displaced a centred title, so it had to stay small enough not to — and small is exactly what
   made it easy to miss. Anchored to the stage it is free to be the size the mark deserves, the
   title recentres, and it sits on the thing it is about.

   Inset from the corner, not pinned to it: the stage is 260px square and the front sleeve is
   both smaller than that and tilted inside it, so a mark at the wrapper corner hangs off the top
   edge of the artwork instead of resting on it. The offset puts it on the sleeve at rest; it is
   approximate by nature, because the sleeve rotates as you flick. */
.stack-stage-wrap[b-wa1zsa4bca] {
    position: relative;
}

.stack-wanted-anchor[b-wa1zsa4bca] {
    position: absolute;
    top: 14px;
    right: 12px;
    z-index: 2;
    pointer-events: none;
    filter: drop-shadow(0 2px 5px rgba(0, 0, 0, 0.55));
}

[b-wa1zsa4bca] .stack-wanted-mark {
    display: block;
    width: 22px;
    height: 31px;
}

.stack-info-pos[b-wa1zsa4bca] {
    font-size: var(--text-sm);
    color: var(--text-dim);
    margin-top: 5px;
    letter-spacing: 0.5px;
    opacity: 0.8;
}

.stack-controls[b-wa1zsa4bca] {
    display: flex;
    gap: 14px;
    margin-bottom: 10px;
}

.stack-prev[b-wa1zsa4bca],
.stack-next[b-wa1zsa4bca] {
    border-radius: 50%;
}

.stack-hint[b-wa1zsa4bca] {
    font-size: var(--text-sm);
    color: var(--text-dim);
    letter-spacing: 0.4px;
    opacity: 0.7;
    text-align: center;
}

/* Held in this sheet: a scoped rule carries the component attribute selector and outranks any
   global override, media query or not. */
@media (max-width: 380px) {
    .stack-stage[b-wa1zsa4bca] {
        width: min(260px, 70vw);
        height: min(260px, 70vw);
    }
}

/* Built by stack-flick.js, like .stack-wanted-mark above: ::deep. */
[b-wa1zsa4bca] .stack-sleeve {
    position: absolute;
    inset: 0;
    border-radius: var(--radius-sm);
    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;
}

[b-wa1zsa4bca] .stack-sleeve.grabbing {
    cursor: grabbing;
    transition: none;
}

/* Cover image (.media-cover) 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. */
[b-wa1zsa4bca] .stack-sleeve img,
[b-wa1zsa4bca] .stack-sleeve-fallback {
    position: absolute;
    inset: 0;
}

[b-wa1zsa4bca] .stack-sleeve img {
    z-index: 2;
    /* 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;
}

[b-wa1zsa4bca] .stack-sleeve-fallback {
    width: 100%;
    height: 100%;
    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;
}
/* /Components/Pages/TrackDetailsComponent.razor.rz.scp.css */
/* The track pane's own: the header, the credits list and the appearances table. */

.track-dur[b-vhh81c0pzp] { text-align: right; }

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

.track-listen-cluster[b-vhh81c0pzp] {
    display: flex;
    align-items: center;
    gap: 8px;
    justify-content: flex-end;
}

.preview-btn[b-vhh81c0pzp] {
    background: transparent;
    padding: 0;
    cursor: pointer;
    color: var(--text-secondary);
}

.preview-btn:hover[b-vhh81c0pzp] {
    color: var(--accent-light);
}

.preview-btn .preview-glyph[b-vhh81c0pzp] {
    display: block;
    width: 0;
    height: 0;
    border-left: 7px solid currentColor;
    border-top: 4.5px solid transparent;
    border-bottom: 4.5px solid transparent;
    /* Optical centring: a triangle's visual mass sits left of its box centre. */
    margin-left: 2px;
}

/* 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[b-vhh81c0pzp] {
    display: flex;
    align-items: center;
    gap: 4px;
}

.listen-icon[b-vhh81c0pzp] {
    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;
    position: relative;
}

/* WCAG 2.5.8 Target Size (AA): 18px icons in a strip with `gap: 4px` are 22px centre
   to centre, just under both the size rule and the spacing exception. Transparent
   24x24 pseudo — no visual change to a deliberately small inline control. */
.listen-icon[b-vhh81c0pzp]::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: 24px;
    height: 24px;
    transform: translate(-50%, -50%);
}

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

/* Per-track "heard live" marker in the release tracklist — the evidence for the
   insight panel above it, so it shares the panel's accent (not the green Owned
   badge on the setlist, which asserts the opposite direction: you own this song
   / you heard this song).

   The slot is a layout-only wrapper: transparent on desktop so the pill sits
   inline after the title, a full-width flex line below 900px so the pill drops
   onto its own row under the title (credits beneath). Wrapping the pill rather
   than pseudo-breaking the container means pill-less rows gain no phantom line. */
.track-heard-slot[b-vhh81c0pzp] {
    display: contents;
}

.track-heard[b-vhh81c0pzp] {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-left: 8px;
    padding: 1px 7px;
    border-radius: 5px;
    font-size: var(--text-xs);
    white-space: nowrap;
    vertical-align: 1px;
    /* Accent, matching the insight band above — the pills are that claim's
       evidence, and the spotlight mark they carry is drawn in the accent. */
    color: var(--accent-light);
    background: var(--accent-bg);
    border: 1px solid var(--accent-border);
}

/* Held in this sheet: a scoped rule carries the component attribute selector and outranks any
   global override, media query or not. */
@media (max-width: 900px) {
    .track-title-row[b-vhh81c0pzp] {
        flex-wrap: wrap;
        row-gap: 3px;
    }

    .track-heard-slot[b-vhh81c0pzp] {
        display: block;
        flex-basis: 100%;
    }

    .track-heard[b-vhh81c0pzp] {
        margin-left: 0;
        /* The longest tier ("The only song played at all 12 of your gigs — always
           the encore") is wider than a phone. Inline on desktop nowrap is right;
           on a line of its own the pill must wrap rather than push the table
           sideways. */
        white-space: normal;
    }
}

.preview-btn.playing .preview-glyph[b-vhh81c0pzp] {
    border: none;
    width: 6px;
    height: 8px;
    margin-left: 0;
    background: linear-gradient(
        to right,
        currentColor 0 2px, transparent 2px 4px, currentColor 4px 6px);
}

.preview-btn.playing[b-vhh81c0pzp] {
    color: var(--accent-light);
    border-color: var(--accent-border);
}
/* /Components/Pages/Upgrade.razor.rz.scp.css */
/* The upgrade screen owns the plan summary and the entitlement list. */

.upgrade-page .upgrade-loading[b-mo2x59227x] {
    color: var(--text-dim);
    font-size: var(--text-md);
}

.upgrade-page .upgrade-title[b-mo2x59227x] {
    font-size: 1.5rem;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 8px;
}

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

.upgrade-page .upgrade-details[b-mo2x59227x] {
    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[b-mo2x59227x] {
    font-size: var(--text-sm);
    color: var(--text-muted);
    font-weight: 500;
}

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

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

.upgrade-page .upgrade-data-notice[b-mo2x59227x] {
    margin: 24px 0 0;
    font-size: var(--text-sm);
    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[b-mo2x59227x] {
    margin: 32px 0 0;
}

.upgrade-page .upgrade-entitlements h2[b-mo2x59227x] {
    font-size: var(--text-2xl);
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 8px;
}

.upgrade-page .upgrade-entitlements p[b-mo2x59227x] {
    margin: 0 0 8px;
    font-size: var(--text-md);
    color: var(--text-secondary);
}

@media (max-width: 600px) {
    .upgrade-page .upgrade-details[b-mo2x59227x] {
                grid-template-columns: 1fr;
                gap: 2px 0;
            }

    .upgrade-page .upgrade-details dt[b-mo2x59227x] {
                margin-top: 8px;
            }

    .upgrade-page .upgrade-details dt:first-child[b-mo2x59227x] {
                margin-top: 0;
            }
}

/* 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 are the focal point. ::deep because PricingCards
   renders the element; .upgrade-page is this page's own, so the attribute lands there. */
.upgrade-page[b-mo2x59227x]  .pricing-cards {
    margin-top: 12px;
    margin-bottom: 0;
}
/* /Components/Pages/VenuePage.razor.rz.scp.css */
/* The venue page's own: the hero photo, the map panel and the visit list. */

.venue-page-scroll[b-vqdb7i9rt3] {
    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 > *[b-vqdb7i9rt3] {
    max-width: 1100px;
    width: 100%;
    margin-left: auto;
    margin-right: auto;
    flex-shrink: 0;
}

.venue-page-breadcrumb[b-vqdb7i9rt3] {
    height: 40px;
    padding: 0 24px;
}

.venue-page-hero[b-vqdb7i9rt3] {
    position: relative;
    width: 100%;
    height: 240px;
    background: rgba(255, 255, 255, 0.04);
    border-radius: 16px;
    overflow: hidden;
}

.venue-page-info[b-vqdb7i9rt3] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 24px;
    align-items: start;
}

.venue-page-info-text[b-vqdb7i9rt3] {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.venue-page-name[b-vqdb7i9rt3] {
    font-size: var(--display-md);
    font-weight: 700;
    color: var(--text-primary);
    margin: 0;
    letter-spacing: -0.4px;
}

.venue-page-address[b-vqdb7i9rt3] {
    font-size: var(--text-xl);
    color: var(--text-dim);
}

.venue-page-aliases-label[b-vqdb7i9rt3] {
    font-weight: 600;
    margin-right: 4px;
}

.venue-page-chips[b-vqdb7i9rt3] {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    margin-top: 4px;
}

.venue-page-stats[b-vqdb7i9rt3] {
    display: flex;
    gap: 24px;
    margin-top: 16px;
    flex-wrap: wrap;
}

.venue-page-stat[b-vqdb7i9rt3] {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.venue-page-stat-value[b-vqdb7i9rt3] {
    font-size: var(--display-sm);
    font-weight: 700;
    color: var(--accent);
    line-height: 1;
}

.venue-page-info-map[b-vqdb7i9rt3] {
    width: 100%;
    aspect-ratio: 4 / 3;
    border-radius: 12px;
    overflow: hidden;
    background: rgba(255, 255, 255, 0.04);
}

/* Concert page: full width directly beneath the tab bar, above the tab
   content, so the claim is visible from both tabs — the Setlist tab's Owned
   badges are its evidence, same band-above-markers shape as the release page. */

.venue-page-map-canvas[b-vqdb7i9rt3] {
    background: transparent;
}

.venue-page-concerts[b-vqdb7i9rt3] {
    display: flex;
    flex-direction: column;
    gap: 12px;
}

.venue-page-section-title[b-vqdb7i9rt3] {
    font-size: var(--display-sm);
    font-weight: 700;
    color: var(--text-primary);
    margin: 0;
}

.venue-page-concerts-list[b-vqdb7i9rt3] {
    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[b-vqdb7i9rt3] {
    display: grid;
    grid-template-columns: 56px 1fr auto;
    gap: 14px;
}

/* Absolute so the pill cannot distort the row's grid column tracks. */
.venue-page-linked-group-tag[b-vqdb7i9rt3] {
    position: absolute;
    top: -10px;
    right: 12px;
}

.venue-page-concert-thumb[b-vqdb7i9rt3] {
    --thumb-size: 56px;
    border-radius: var(--radius-lg);
}

.venue-page-concert-thumb--empty[b-vqdb7i9rt3] {
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--text-dim);
    font-size: var(--display-sm);
}

.venue-page-concert-link[b-vqdb7i9rt3] {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    color: inherit;
    text-decoration: none;
}

.venue-page-concert-date[b-vqdb7i9rt3] {
    font-variant-numeric: tabular-nums;
}

.venue-page-concert-meta[b-vqdb7i9rt3] {
    display: flex;
    align-items: center;
    gap: 10px;
}

.venue-page-concert-photos-pill[b-vqdb7i9rt3] {
    background: var(--accent-bg);
    border-color: var(--accent-border);
    color: var(--accent);
}

.venue-page-concert-photos-pill:hover[b-vqdb7i9rt3] {
    background: rgba(139, 109, 255, 0.18);
}

/* These blocks live here rather than app.css: the classes are defined in this sheet, so the
   scoped rule carries the component's attribute selector and a global override — media query or
   not — can never outrank it. */
@media (max-width: 900px) {
    .venue-page-info[b-vqdb7i9rt3] {
        grid-template-columns: 1fr;
    }
}

@media (max-width: 600px) {
    .venue-page-hero[b-vqdb7i9rt3] {
        height: 180px;
        border-radius: 12px;
    }

    .venue-page-concert-row[b-vqdb7i9rt3] {
        grid-template-columns: 44px 1fr auto;
        gap: 10px;
        padding: 10px 12px;
    }

    .venue-page-concert-thumb[b-vqdb7i9rt3] {
        width: 44px;
        height: 44px;
    }

    .venue-page-concert-tracks[b-vqdb7i9rt3] {
        display: none;
    }
}

.venue-page-concert-link:hover .row-title[b-vqdb7i9rt3] {
    color: var(--accent);
}

/* ::deep because PhotoAttribution renders these elements: they carry ITS scope attribute, not this
   page's, so a plain selector here matches nothing. The hero photo is the one place the strip sits
   on an image, which is why the scrim and the light text live with the page, not the component. */
.venue-page-hero[b-vqdb7i9rt3]  .photo-attribution {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    background: var(--scrim);
    padding: 24px 16px 10px;
    color: rgba(255,255,255,0.9);
}

.venue-page-hero[b-vqdb7i9rt3]  .photo-attribution-sep,
.venue-page-hero[b-vqdb7i9rt3]  .photo-attribution-licence,
.venue-page-hero[b-vqdb7i9rt3]  .photo-attribution-source {
    color: rgba(255,255,255,0.85);
}
/* /Components/Shared/ActivityPill.razor.rz.scp.css */

.activity-pill[b-ditpsbq0fz] {
    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: var(--text-md);
    color: var(--text-primary);
    transform: translateY(8px);
    opacity: 0;
    animation: activity-pill-in-b-ditpsbq0fz 180ms ease-out forwards;
    transition: opacity 240ms ease, transform 240ms ease, border-color 240ms ease;
}

/* @keyframes are NOT scoped by Blazor's rewriter — the names stay global, so they keep
   their prefixes to avoid colliding with another component's animation. */
@keyframes activity-pill-in-b-ditpsbq0fz {
    to { transform: translateY(0); opacity: 1; }
}

.activity-pill.state-success[b-ditpsbq0fz] { border-color: var(--owned-border); }
.activity-pill.state-error[b-ditpsbq0fz]   { border-color: var(--danger-border); }

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

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

.activity-pill .activity-icon svg[b-ditpsbq0fz] {
    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[b-ditpsbq0fz] { opacity: 1; transform: scale(1); color: var(--owned); }
.activity-pill.state-error   .activity-icon svg.cross[b-ditpsbq0fz] { opacity: 1; transform: scale(1); color: var(--danger); }

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

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

.activity-pill .activity-close[b-ditpsbq0fz] {
    color: var(--text-muted); padding: 4px;
    border-radius: 4px; display: flex; align-items: center; justify-content: center;
    transition: opacity 180ms ease, background 120ms ease, color 120ms ease;
}

/* Hidden until hover ONLY where hovering exists. A touch device never hovers and cannot Tab, so
   the reveals below were all unreachable: the button to dismiss a pill was invisible unless the
   job had errored, leaving no way to clear it but guessing where to tap. */
@media (hover: hover) {
    .activity-pill .activity-close[b-ditpsbq0fz] { opacity: 0; }
}

/* :focus-within is not optional: the control renders at opacity: 0 until hover, and
   opacity applies to the focus ring too. WCAG 2.4.7. */
.activity-pill:hover .activity-close[b-ditpsbq0fz],
.activity-pill:focus-within .activity-close[b-ditpsbq0fz],
.activity-pill.state-error .activity-close[b-ditpsbq0fz] { opacity: 1; }
.activity-pill .activity-close:hover[b-ditpsbq0fz] { background: var(--bg-hover); color: var(--text-primary); }

@media (max-width: 600px) {
    .activity-pill[b-ditpsbq0fz] { max-width: none; min-width: 0; }
}

@media (prefers-reduced-motion: reduce) {
    .activity-pill[b-ditpsbq0fz] { animation: none; transform: none; opacity: 1; }
    .activity-pill .activity-icon .spinner[b-ditpsbq0fz] { animation-duration: 1400ms; }
}
/* /Components/Shared/ActivityStack.razor.rz.scp.css */
/* The stack ignores the pointer so the page under it stays usable. */
.activity-stack[b-1k0vnmypau] {
    position: fixed;
    right: 20px;
    bottom: 20px;
    display: flex;
    flex-direction: column-reverse;
    gap: 8px;
    z-index: 1000;
    pointer-events: none;
}

@media (max-width: 600px) {
    .activity-stack[b-1k0vnmypau] { left: 12px; right: 12px; bottom: 12px; }
}
/* /Components/Shared/ArtistCard.razor.rz.scp.css */
/* The photo, letterboxed rather than cropped, over a blurred copy of itself so the frame is
   filled by the photo's own colours instead of bars. The backdrop is scaled up so the blur's
   soft edge never reveals the original image bounds. */
.artist-card-hero-img[b-aiictfvhdy] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

.artist-card-hero-backdrop[b-aiictfvhdy] {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: blur(24px) brightness(0.6);
    transform: scale(1.15);
}

/* Stands in for a missing photo: initials as a watermark, not as text to read. */
.artist-card-hero-initials[b-aiictfvhdy] {
    font-size: 6rem;
    font-weight: 700;
    color: rgba(255, 255, 255, 0.08);
    letter-spacing: 4px;
}
/* /Components/Shared/AutocompleteInput.razor.rz.scp.css */
/* Defaults, so they sit in the layer: every unlayered rule outranks them, whatever its specificity
   or order — a slot rule or a caller's class decides, this only proposes. Never !important in here:
   layers invert for it. */
@layer component {
    /* The positioning context for .popover-list. AutocompleteInput renders this
       wrapper, and it had NO rule anywhere — so `position: absolute; top: 100%` below
       resolved against whatever ancestor happened to be positioned instead of against
       the input, putting the suggestion list somewhere unrelated to the field. Every
       picker in the app depends on this one line. */
    .ac-wrap[b-96xngdwzoz] {
        position: relative;
    }

    .popover-list[b-96xngdwzoz] {
        position: absolute;
        top: 100%;
        left: 0;
        right: 0;
        margin-top: var(--space-1);
        max-height: 280px;
        overflow-y: auto;
        background: var(--bg-card);
        border: 1px solid var(--border);
        border-radius: var(--radius-md);
        box-shadow: var(--shadow-popover);
        z-index: var(--z-popover);
    }

    .popover-item[b-96xngdwzoz] {
        padding: var(--space-2) var(--space-3);
        border-bottom: 1px solid var(--divider);
        color: var(--text-secondary);
        font-size: var(--btn-font);
        line-height: 1.3;
        cursor: pointer;
        transition: background 0.1s;
    }

    .popover-item:last-child[b-96xngdwzoz] { border-bottom: none; }

    .popover-item:hover[b-96xngdwzoz] { background: var(--bg-hover); }

    /* Deliberately stronger than :hover, and carrying the same inset accent bar as
       an active list row so "this is the current item" reads identically wherever
       it appears. */
    .popover-item.is-highlighted[b-96xngdwzoz] {
        background: var(--bg-active);
        box-shadow: inset 2px 0 0 var(--accent);
        color: var(--text-primary);
    }
}

/* Action rows below the suggestions — "Add new venue", "Use as typed". Buttons,
   not options: they are separated by a rule because they do something OTHER than
   choose from the list, and they sit outside the arrow-key cycle so Enter cannot
   fire one while the user thinks they are picking a result. */
.popover-actions[b-96xngdwzoz] {
    border-top: 1px solid var(--divider);
}
/* /Components/Shared/Barcode.razor.rz.scp.css */
/* A barcode as printed on a sleeve: bars in the text colour, digits beneath. The host decides
   the width and the bars fill it; height is fixed so a narrow column gets a narrower symbol, not a
   shorter one. contain: inline-size is load-bearing — a 95:1 viewBox at 36px tall otherwise reports
   a ~3400px max-content width, and a flex host that sizes to content (the hero's cover column)
   grows to that and shoves everything beside it off-screen. */
.barcode[b-ymitup7lj7] {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    margin: 0;
    contain: inline-size;
}

.barcode svg[b-ymitup7lj7] {
    display: block;
    width: 100%;
    height: auto;
}

.barcode-digits[b-ymitup7lj7],
.barcode-text[b-ymitup7lj7] {
    font-family: ui-monospace, Menlo, Consolas, monospace;
    font-size: var(--text-xs);
    letter-spacing: 0.12em;
    color: var(--text-dim);
}

/* Plain digits for a code we would not draw: same voice, no paper, no false symbol. */
.barcode-text[b-ymitup7lj7] {
    display: inline-block;
    padding: 4px 0;
}
/* /Components/Shared/Card.razor.rz.scp.css */
.card-media[b-rk65rs7mtc] {
    display: grid;
    place-items: center;
    /* Nothing the media slot paints may leave its box — a scaled or blurred backdrop otherwise
       bleeds down past the media and into the body, and .card's own overflow clips it at the
       CARD edge, which is far too late. The image stops where the media stops, always. */
    overflow: hidden;
    position: relative;
    /* Default in the fallback, not as a declaration: declared here it would beat anything an
       ancestor sets, and a compact tile has to be able to ask for square. */
    aspect-ratio: var(--card-media-ratio, 16 / 9);
    background: var(--bg-hover);
}

/* Sits over the foot of the media slot, which supplies the positioning context. */
.card-overlay[b-rk65rs7mtc] {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--scrim);
}

/* Top-aligned, explicitly. flex: 1 makes the body absorb the grid's stretch surplus, and without
   this the surplus lands ABOVE the content — so a card with two body lines starts them lower than
   its neighbour with three, and the titles miss each other. */
.card-body[b-rk65rs7mtc] {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    gap: var(--space-2);
    flex: 1;
    padding: var(--space-3) var(--space-4);
}

/* Caption sits directly under the cover, with no side padding — the cover's own
   edges are the tile's edges. */
.card-compact .card-body[b-rk65rs7mtc] { padding: var(--space-2) 0 0; gap: 2px; }
/* /Components/Shared/ClampedText.razor.rz.scp.css */
/* Defaults, so they sit in the layer: every unlayered rule outranks them, whatever its specificity
   or order — a slot rule or a caller's class decides, this only proposes. Never !important in here:
   layers invert for it. */
@layer component {
    .clamp[b-rth76wfbfg] {
        display: -webkit-box;
        -webkit-line-clamp: var(--clamp-lines, 3);
        -webkit-box-orient: vertical;
        overflow: hidden;
    }

    .clamp.is-expanded[b-rth76wfbfg] {
        display: block;
        -webkit-line-clamp: unset;
        overflow: visible;
    }

    .clamp-toggle[b-rth76wfbfg] {
        padding: var(--space-1) 0;
        /* Separation from whatever follows. It had vertical padding and NO margin at
           all, so on the artist page it butted straight into the genre chips beneath
           it. A disclosure toggle sits between the text it reveals and the next
           thing on the page — never touching either is part of what it is, not
           something each caller should re-solve. */
        margin-bottom: var(--space-2);
        color: var(--accent-light);
        font-size: var(--text-md);
        transition: color 0.12s;
    }

    /* Hover styling only where hovering exists. On touch, :hover matches from tap until the next tap
       elsewhere, so an ungated hover colour STICKS on this toggle — it is tapped repeatedly and
       nothing else re-renders it to clear the state. */
    @media (hover: hover) {
        .clamp-toggle:hover[b-rth76wfbfg] { color: var(--text-primary); }
    }

    .clamp-toggle:focus-visible[b-rth76wfbfg] {
        outline: 2px solid var(--accent);
        outline-offset: 2px;
        border-radius: var(--radius-sm);
    }
}
/* /Components/Shared/Dialog.razor.rz.scp.css */
.dialog-lightbox .dialog-header[b-itglakewjy] {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 1;
    padding: var(--space-2);
    border-bottom: 0;
}

.dialog-lightbox .dialog-body[b-itglakewjy] {
    padding: 0;
    overflow: visible;
}

.dialog-header[b-itglakewjy] {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-shrink: 0;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--divider);
}

.dialog-title[b-itglakewjy] {
    flex: 1;
    min-width: 0;
    margin: 0;
    /* A title that can carry a mark before its text. Flex rather than an inline image, so the
       mark centres on the words instead of sitting on their baseline. */
    display: flex;
    align-items: center;
    gap: var(--space-2);
    color: var(--text-primary);
    font-size: var(--text-2xl);
    font-weight: 600;
}

/* The only scrolling region. The header and footer stay put, so a long dialog
   never hides its own actions below the fold — which is how a confirm dialog
   ends up with an unreachable "Confirm". */
.dialog-body[b-itglakewjy] {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: var(--space-4);
}

.dialog-footer[b-itglakewjy] {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    flex-shrink: 0;
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--divider);
}
/* /Components/Shared/DiscogsPill.razor.rz.scp.css */
/* This component's own rules. */

.discogs-chip[b-3b7f3vp3mp] {
    /* 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;
}

/* The pair sets --z-popover and the backdrop primitive derives from it. Declared on both: the
   backdrop is rendered OUTSIDE the wrap, a sibling of it, so nothing they share could carry it. */
.discogs-tooltip[b-3b7f3vp3mp],
.tooltip-backdrop[b-3b7f3vp3mp] { --z-popover: var(--z-popover-raised); }

.discogs-tooltip[b-3b7f3vp3mp] {
    position: absolute;
    bottom: calc(100% + 8px);
    left: 0;
    z-index: var(--z-popover);
    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);
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.discogs-tooltip-text[b-3b7f3vp3mp] {
    color: var(--text-muted);
}

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

.discogs-tooltip-link:hover[b-3b7f3vp3mp] {
    text-decoration: underline;
}

/* Held in this sheet: a scoped rule outranks any global override, media query or not. */
@media (max-width: 849px) {
    .discogs-tooltip[b-3b7f3vp3mp] {
        bottom: auto;
        top: calc(100% + 8px);
        max-width: calc(100vw - 32px);
    }
}

/* The positioning context for the tooltip this component shows beside its chip. */
.hero-link-chip-wrap[b-3b7f3vp3mp] {
    position: relative;
    display: inline-flex;
}
/* /Components/Shared/HelpPanel.razor.rz.scp.css */
/* The help panel owns the topic list, its images and the drill-in. */

.help-panel-body[b-hjnvy4n3lf] {
    display: flex;
    flex: 1;
    min-height: 0;
}

.help-panel-topic-list[b-hjnvy4n3lf] {
    flex: none;
    width: 230px;
    border-right: 1px solid var(--border);
    overflow-y: auto;
    padding: 10px;
}

.help-panel-topic-item[b-hjnvy4n3lf] {
    width: 100%;
    justify-content: flex-start;
}

.help-panel-topic-item:hover[b-hjnvy4n3lf] {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.help-panel-topic-icon[b-hjnvy4n3lf] {
    flex: none;
    width: 17px;
    height: 17px;
    color: var(--text-dim);
}

.help-panel-topic-title[b-hjnvy4n3lf] {
    flex: 1;
}

.help-panel-topic-arrow[b-hjnvy4n3lf] {
    flex: none;
    color: var(--text-dim);
    transition: opacity 0.12s;
}

/* Hidden until hover ONLY where hovering exists. A touch device never hovers, so hiding it there
   removed the sole cue that a row opens something — every row but the active one looked inert. */
@media (hover: hover) {
    .help-panel-topic-arrow[b-hjnvy4n3lf] { opacity: 0; }
}

.help-panel-topic-detail[b-hjnvy4n3lf] {
    flex: 1;
    min-width: 0;
    overflow-y: auto;
    padding: 20px 24px 24px;
}

.help-panel-topic-detail h2[b-hjnvy4n3lf] {
    font-size: var(--text-2xl);
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 14px;
}

.help-panel-topic-detail p[b-hjnvy4n3lf],
.help-panel-topic-detail dd[b-hjnvy4n3lf] {
    font-size: var(--text-lg);
    line-height: 1.7;
    color: var(--text-muted);
}

.help-panel-topic-image-wrap[b-hjnvy4n3lf] {
    position: relative;
    display: block;
    width: 100%;
    padding: 0;
    margin: 0 0 16px;
    cursor: zoom-in;
}

.help-panel-topic-image[b-hjnvy4n3lf] {
    width: 100%;
    display: block;
    border-radius: 8px;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}

/* Always-visible hint (not hover-gated) — the panel opens the same way on
   touch and pointer devices, so the affordance has to be discoverable on
   both, not just on desktop hover. */
.help-panel-topic-image-hint[b-hjnvy4n3lf] {
    position: absolute;
    right: 8px;
    bottom: 8px;
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 5px 10px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.6);
    color: rgba(255, 255, 255, 0.92);
    font-size: var(--text-sm);
    line-height: 1;
}

.help-panel-topic-image-hint svg[b-hjnvy4n3lf] {
    width: 13px;
    height: 13px;
    flex: none;
}

/* Back-link only shows in the mobile drill-in detail view. Real <button>
   (keyboard-operable inside the aria-modal dialog) with its chrome reset. */
/* Hidden at desktop width; the narrow-layout media block in app.css reveals it. */
.help-panel-back-link[b-hjnvy4n3lf] {
    display: none;
}

/* Held in this sheet: a scoped rule carries the component attribute selector and outranks any
   global override, media query or not. */
@media (max-width: 849px) {
    /* List and detail are both full-width; .show-detail toggles which one is
       visible (drill-in), rather than the desktop side-by-side split. */
    .help-panel-topic-list[b-hjnvy4n3lf] {
        width: 100%;
        border-right: none;
    }

    .help-panel-topic-detail[b-hjnvy4n3lf] {
        display: none;
    }

    .help-panel-back-link[b-hjnvy4n3lf] {
        display: flex;
        align-items: center;
        gap: 6px;
        font-size: var(--text-md);
        color: var(--accent-light);
        margin-bottom: 16px;
        cursor: pointer;
    }

    .help-panel-body.show-detail .help-panel-topic-list[b-hjnvy4n3lf] {
        display: none;
    }

    .help-panel-body.show-detail .help-panel-topic-detail[b-hjnvy4n3lf] {
        display: block;
    }
}

/* Two columns so the marks line up under each other and read as a set rather than a sentence each.
   auto/1fr rather than a fixed first column: a pill and a 7px dot share it. */
.help-panel-legend[b-hjnvy4n3lf] {
    display: grid;
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: var(--space-2) var(--space-3);
    margin-top: var(--space-4);
}

/* .owned-dot sizes a box but sets no display, so it needs a formatting context that blockifies it
   or the span collapses to nothing. The bookmark is an inline <svg> and would otherwise sit on the
   text baseline. Flex gives both a box and centres them. */
.help-panel-legend dt[b-hjnvy4n3lf] {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

/* Only the browser's default indent to undo; the colour is the panel body's. */
.help-panel-legend dd[b-hjnvy4n3lf] {
    margin: 0;
}

.help-panel-topic-item.active[b-hjnvy4n3lf] {
    background: var(--accent-bg);
    color: var(--text-primary);
}

.help-panel-topic-item.active .help-panel-topic-icon[b-hjnvy4n3lf] {
    color: var(--accent-light);
}

.help-panel-topic-item.active .help-panel-topic-arrow[b-hjnvy4n3lf],
.help-panel-topic-item:hover .help-panel-topic-arrow[b-hjnvy4n3lf] {
    opacity: 1;
}
/* /Components/Shared/Hotspot.razor.rz.scp.css */
/* Defaults, so they sit in the layer: every unlayered rule outranks them, whatever its specificity
   or order — a slot rule or a caller's class decides, this only proposes. Never !important in here:
   layers invert for it. */
@layer component {
    /* This component's own rules. */

    .hotspot-dot[b-e1udgudbsv] {
        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);
        height: var(--hotspot-size);
        background: transparent;
        border: 0;
        padding: 0;
        margin: 0;
        cursor: pointer;
        display: inline-flex;
        align-items: center;
        justify-content: center;
        z-index: 5;
    }

    .hotspot-dot[b-e1udgudbsv]::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-pulse[b-e1udgudbsv] {
        /* 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;
    }

    /* 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[b-e1udgudbsv] {
        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: 0;
        z-index: var(--z-popover);
        min-width: 180px;
        max-width: 260px;
        padding: 10px 12px;
        background: var(--bg-card);
        color: var(--text-primary);
        border: 1px solid var(--accent-border);
        border-radius: 8px;
        box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
        font-size: var(--text-md);
        line-height: 1.4;
        animation: hotspot-tooltip-in 0.15s ease-out;
    }

    .hotspot-tooltip-arrow[b-e1udgudbsv] {
        /* 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: 20px;
        width: 12px;
        height: 7px;
        overflow: hidden;
    }

    .hotspot-tooltip-arrow[b-e1udgudbsv]::before,
    .hotspot-tooltip-arrow[b-e1udgudbsv]::after {
        content: '';
        position: absolute;
        top: 0;
        left: 1px;
        width: 10px;
        height: 10px;
        transform: rotate(45deg);
        transform-origin: 0 0;
    }

    .hotspot-tooltip-arrow[b-e1udgudbsv]::before {
        background: var(--accent-border);
    }

    .hotspot-tooltip-arrow[b-e1udgudbsv]::after {
        top: 1px;
        background: var(--bg-card);
    }

    .hotspot-tooltip-text[b-e1udgudbsv] {
        margin-bottom: 8px;
        color: var(--text-primary);
    }

    /* Declared on both: the backdrop is rendered beside the tooltip, not inside it, so it cannot
       inherit the tooltip's value, and the primitive places it one below. */
    .hotspot-tooltip[b-e1udgudbsv],
    .hotspot-tooltip-backdrop[b-e1udgudbsv] { --z-popover: 1001; }

    /* Overrides the default corner inset, which assumes a host large enough that a 44px
       hit box clears the content. .hotspot-anchor hugs its content, so the dot landed on
       top of it. */
    .hotspot-anchor > .hotspot-dot[b-e1udgudbsv] {
        top: 50%;
        right: 0;
        transform: translate(calc(50% + var(--hotspot-dot-size, 14px) / 2 + 2px), -50%);
    }

    .hotspot-dot:hover[b-e1udgudbsv]::before,
    .hotspot-dot.is-active[b-e1udgudbsv]::before {
        transform: scale(1.15);
    }

    /* The size ladder. Md is the component default and every call site currently overrides it
       with Sm, so nothing renders this rung today; it sets the same 14px the six `var(…, 14px)`
       readers fall back to, which is why moving it here could not change a computed value. */
    .hotspot-dot-md[b-e1udgudbsv] { --hotspot-dot-size: 14px; }
    .hotspot-dot-sm[b-e1udgudbsv] { --hotspot-dot-size: 10px; }

    .hotspot-dot.is-active .hotspot-pulse[b-e1udgudbsv] {
        /* 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;
    }

    /* 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[b-e1udgudbsv] {
            max-width: calc(100vw - 32px);
        }
    }

    @media (prefers-reduced-motion: reduce) {
        .hotspot-pulse[b-e1udgudbsv] { animation: none; }

        .hotspot-tooltip[b-e1udgudbsv] { animation: none; }
    }
}
/* /Components/Shared/Icon.razor.rz.scp.css */
/* In a layer so that ANY unlayered rule outranks it, whatever its specificity or order: the slot
   an icon sits in (.btn > svg, a caller's Class) decides its size, and this is only the default.
   Out of the layer this selector carries Blazor's scope attribute and loads after app.css, and
   it starts beating the very rules that are meant to override it. */
@layer component {
    /* 1em-based so it takes the slot's font-size; currentColor stroke takes the slot's colour. */
    .ui-icon[b-l2iisf0j30] { width: 1.1em; height: 1.1em; display: inline-block; vertical-align: middle; flex-shrink: 0; }
}
/* /Components/Shared/ImportProgressCard.razor.rz.scp.css */
/* .bar bar-bottom bar-wrap carries the shape; only the tint and the wider lane spacing are this
   strip's own. One row tall on desktop, and nothing here may add height: whatever this takes comes
   off the view under it. */
.import-strip[b-8fjemqpksu] {
    column-gap: var(--space-5);
    border-bottom-color: var(--accent-border);
    background: var(--accent-bg);
}

.import-strip-title[b-8fjemqpksu] {
    color: var(--text-primary);
    font-weight: 600;
}

.import-strip-lane[b-8fjemqpksu] {
    display: flex;
    align-items: center;
    gap: var(--space-2);
}

.import-strip-lane .meter[b-8fjemqpksu] {
    width: 120px;
}

.import-strip-count[b-8fjemqpksu] {
    font-variant-numeric: tabular-nums;
}

.import-strip-dismiss[b-8fjemqpksu] {
    margin-left: auto;
}

/* The meters are the first thing to go: at this width the counts alone say the same. */
@media (max-width: 849px) {
    .import-strip[b-8fjemqpksu] {
        column-gap: var(--space-3);
    }

    .import-strip-lane .meter[b-8fjemqpksu] {
        display: none;
    }
}

/* The count is the way into the list, so it has to be pressable — .tag is a label costume, and a
   button needs its own chrome stripped back to match one. */
.import-strip-gone[b-8fjemqpksu] {
    border-width: 1px;
    border-style: solid;
    cursor: pointer;
    font: inherit;
    font-size: var(--text-sm);
}
/* /Components/Shared/InsightMark.razor.rz.scp.css */
/* Defaults, so they sit in the layer: every unlayered rule outranks them, whatever its specificity
   or order — a slot rule or a caller's class decides, this only proposes. Never !important in here:
   layers invert for it. */
@layer component {
    /* The spotlight mark, shared by every joined-data statement (release band, track
       pill, concert artist card, venue cross-reference) via the InsightMark component.
       Sizes are fixed rather than em-based so the mark can't scale with whatever text
       it sits beside, and `flex: 0 0 auto` keeps a claim that wraps to three lines
       squeezing the text rather than the mark. */
    .insight-mark[b-kc91hjdw1r] {
        flex: 0 0 auto;
        display: inline-block;
    }

    .insight-mark-panel[b-kc91hjdw1r] {
        width: 26px;
        height: 26px;
        margin-top: 1px;
    }

    .insight-mark-row[b-kc91hjdw1r] {
        width: 12px;
        height: 12px;
        vertical-align: -1px;
    }

    .insight-tight .insight-mark-panel[b-kc91hjdw1r] {
        margin-top: 0;
    }
}
/* /Components/Shared/InsightPanel.razor.rz.scp.css */
.insight[b-5jwk5v3vaa] {
    flex-direction: row;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3);
    border-color: var(--accent-border);
    background: var(--accent-bg);
}

/* Quieter variant for a panel that reports rather than celebrates. */
.insight-neutral[b-5jwk5v3vaa] {
    border-color: var(--border-med);
    background: var(--bg-hover);
}

/* Absence is reported, not announced. The colour alone was not enough separation:
   at the same --text-xl as a positive claim, "Nothing from this release has been
   played at a gig you've been to" is a long sentence set at headline size with no
   mark beside it, so it filled the box and read LOUDER than the achievement it is
   the absence of (owner, 11/08: "negative insight is bigger font and more
   prominent — not right"). One step down, matching the support line's scale. */
.insight-neutral .insight-claim[b-5jwk5v3vaa] {
    color: var(--text-secondary);
    font-size: var(--text-md);
}

/* Centres the mark when there is no supporting line to align against. */
.insight-tight[b-5jwk5v3vaa] { align-items: center; }

.insight-body[b-5jwk5v3vaa] { min-width: 0; }

.insight-claim[b-5jwk5v3vaa] {
    color: var(--accent-light);
    font-size: var(--text-xl);
    font-weight: 600;
    line-height: 1.3;
}

/* A second, lesser claim under a rule. Same shape one level down, which is why
   it reuses the claim/support pair rather than inventing two more classes. */
.insight-more[b-5jwk5v3vaa] {
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid var(--divider);
    font-size: var(--text-md);
    line-height: 1.35;
}

.insight-more .insight-claim[b-5jwk5v3vaa] { font-size: var(--text-md); }

/* Release page: last child of .hero-info, above the collection fields.
   Top only — the images pill above brings its own bottom margin, so this adds
   to it rather than replacing it; enough to read as a separate statement from
   the metadata rows, not enough to float free of them.
   No bottom margin: on desktop it would do nothing, and on mobile — where
   .detail-hero wraps and the collection panel drops beneath — the container's
   own `gap: 20px` already separates them. Anything here simply adds to that. */
.hero-info .insight[b-5jwk5v3vaa] {
    margin-top: 8px;
}
/* /Components/Shared/InsightsPanel.razor.rz.scp.css */
/* This component's own rules. */

.insights-strip[b-92jp918kkj] {
    display: grid;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
}

.insights-strip-line[b-92jp918kkj] { margin: 0; }

.insights-card[b-92jp918kkj] {
    display: flex;
    gap: var(--space-3);
    align-items: center;
    padding: var(--space-3) 0;
    border-top: 1px solid var(--divider);
}

.whats-new[b-92jp918kkj] {
    margin-top: var(--space-5);
    border-top: 1px solid var(--divider);
    padding-top: var(--space-3);
}

/* Flush with the cards above it, which have no side padding: an indented row reads as a child
   of something. */
.whats-new-toggle[b-92jp918kkj] { padding-inline: 0; }

.whats-new-list[b-92jp918kkj] {
    list-style: none;
    margin: var(--space-3) 0 0;
    padding: 0;
    display: grid;
    gap: var(--space-3);
}

.whats-new-item[b-92jp918kkj] {
    display: grid;
    gap: 2px;
}

.whats-new-date[b-92jp918kkj] {
    color: var(--text-dim);
    font-size: var(--text-xs);
}

/* The chip's mark again, at the size a heading can carry: the panel and the control that opens
   it are the same idea, and the title is where that gets said. */
.insights-title-mark[b-92jp918kkj] {
    width: 28px;
    height: 28px;
}
/* /Components/Shared/LetterJump.razor.rz.scp.css */
/* This component's own rules. */

.letter-jump[b-f9cgwj6qo8] {
    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[b-f9cgwj6qo8]::-webkit-scrollbar { display: none; }

/* On a phone the strip is flush with the screen edge, and a key 2px from the bezel is hard to
   hit — owner, on a real Android phone in prod, 10/09/2026: "a little hard to hit the letters …
   as they are hard up to the screen edge". The keys stay the size they are (the problem was
   proximity, not size — owner's call, same day); the strip moves in. Padding rather than margin,
   so the inset is still the strip's own touch surface and a scrub that starts there is a scrub. */
@media (max-width: 849px) {
    .letter-jump[b-f9cgwj6qo8] { padding: var(--space-1) var(--space-2); }
}

/* A key is as tall as its share of the list's height, capped at the touch target. A strip of
   six decades gets full 44px keys; the alphabet in a phone-height list gets about 30px; and a
   list too short for that shrinks the keys back to the pitch of their glyph and scrolls, rather
   than dropping keys. Not a fixed height, because the same strip serves both counts (owner,
   04/09/2026: "the year jumps and the alpha look pretty small targets"). A button centres its
   label vertically on its own, so a taller key needs no flex of its own. */
.letter-jump-btn[b-f9cgwj6qo8] {
    flex: auto;
    max-height: var(--min-touch);
}

/* Few keys means room for a bigger label; the alphabet's 27 stay at the smaller size. */
.letter-jump-sparse .letter-jump-btn[b-f9cgwj6qo8] {
    font-size: var(--text-sm);
}

/* The key under the finger, said again where the finger is not covering it. It carries the
   whole answer on its own, because the list behind it has nothing to read yet — owner,
   05/09/2026: "while the cover images are loading it's hard to see what's happening".
   Fixed, so neither the strip's own scroller nor the pane clips it; the script sets the top and
   the side with room, and the transform centres it on the finger. It takes the .menu surface;
   the size is its own, and no token goes this large — it is read past a thumb. */
.letter-jump-bubble[b-f9cgwj6qo8] {
    position: fixed;
    min-width: 2.25em;
    padding: var(--space-2) var(--space-3);
    transform: translateY(-50%);
    color: var(--accent-light);
    font-size: 2rem;
    font-weight: 600;
    line-height: 1;
    text-align: center;
    pointer-events: none;
}

.letter-jump-disabled[b-f9cgwj6qo8] {
    color: var(--text-dim);
    opacity: 0.4;
    cursor: default;
}
/* /Components/Shared/ListPlaceholder.razor.rz.scp.css */
/* This component's own rules. */

.list-placeholder[b-eksvy0asn1] {
    cursor: default;
}

.list-placeholder-lines[b-eksvy0asn1] {
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: var(--space-2);
}

/* The tile stacks its square over its lines, the way a compact card does. */
.list-placeholder-tile[b-eksvy0asn1] {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* A bone is a shape with no content: a fill and a breath. Motion is opt-in below, so a viewer
   who asked for reduced motion sees a still block at rest. */
.list-placeholder .list-placeholder-bone[b-eksvy0asn1] {
    background: var(--bg-raised);
    opacity: 0.7;
    animation: none;
}

@media (prefers-reduced-motion: no-preference) {
    .list-placeholder .list-placeholder-bone[b-eksvy0asn1] {
        animation: list-placeholder-pulse-b-eksvy0asn1 1.1s ease-in-out infinite;
    }
}

/* Sized by the column: a line is a flex item of the lines stack, not a box of its own. */
.list-placeholder-lines > .list-placeholder-bone[b-eksvy0asn1] {
    flex: 0 0 10px;
    width: 55%;
    border-radius: 4px;
}

.list-placeholder-lines > .list-placeholder-bone + .list-placeholder-bone[b-eksvy0asn1] {
    width: 30%;
}

@keyframes list-placeholder-pulse-b-eksvy0asn1 {
    0%, 100% { opacity: 0.55; }
    50% { opacity: 1; }
}
/* /Components/Shared/ListSelector.razor.rz.scp.css */
/* This component's own rules. */

/* Group header inside a selector list — the year rows in the concerts sidebar, the letter breaks
   in the artists list. Sticky so the current group stays labelled while its rows scroll; the
   opaque background is load-bearing, without it rows show through the header. */
.list-header[b-xs6ridrrqz] {
    position: sticky;
    top: 0;
    z-index: 1;
    padding: var(--space-2) var(--row-pad-x);
    background: var(--bg-panel);
    border-bottom: 1px solid var(--divider);
    color: var(--text-dim);
    font-size: var(--text-sm);
    font-weight: 600;
    letter-spacing: 0.5px;
    text-transform: uppercase;
}

/* Windowed lists seek by index × row height, so a header is the same height as a row, labelled
   at its bottom edge where the group begins. Not sticky: a header that pins outlives the window
   that rendered it and mislabels the rows below. */
.list-header-fixed[b-xs6ridrrqz] {
    position: static;
    display: flex;
    align-items: flex-end;
    /* A header that is a whole row tall labels a whole row's worth of space: the letter is sized
       to the space it has, not to the sticky strip's small caps (owner, 04/09/2026). */
    font-size: var(--display-sm);
}

/* Every windowed entry is exactly this tall. The seek lands by index × ItemSize, and the scroller
   sets --list-item-size inline from that same ItemSize, so the two cannot disagree; a header
   shorter than a row would put every jump further out with each group it passes. */
.list-windowed .list-row[b-xs6ridrrqz],
.list-windowed .list-header-fixed[b-xs6ridrrqz] {
    height: var(--list-item-size);
    min-height: 0;
}
/* /Components/Shared/MaintenanceBanner.razor.rz.scp.css */
/* This component's own rules. */

/* Admin-only, so deliberately louder than the other two — an unmissable reminder that
   other users are locked out. Not dismissible: a banner you can close is a lock you forget. */
.app-banner-warn[b-52k9zuh9uv] {
    background: var(--warn-bg);
    color: #fff;
    border-color: transparent;
}
/* /Components/Shared/OnboardingBanner.razor.rz.scp.css */
/* This component's own rules. */

/* Tier 2 onboarding banner — inline prompt that demonstrates a hidden
   interaction. Sits in the page flow above the affected content. */
.onboarding-banner[b-u8uon269pe] {
    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;
}

.onboarding-banner-icon[b-u8uon269pe] {
    font-size: 1.25rem;
    flex-shrink: 0;
}

.onboarding-banner-text[b-u8uon269pe] {
    flex: 1;
    min-width: 0;
    font-size: var(--text-lg);
    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;
}

/* Add-mode placeholder for concert image upload (P0-5). Add mode has no
   concertId yet, so the uploader can't be wired — point users at the next
   step rather than leaving an empty space where they expect a control. */

.onboarding-banner-close[b-u8uon269pe] {
    border-radius: 50%;
    font-size: var(--display-sm);
}

.onboarding-banner-close:hover[b-u8uon269pe] {
    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[b-u8uon269pe] {
    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;
}

/* Held here, not in app.css: these classes are defined in this sheet, so the scoped rule carries
   the component's attribute selector and outranks any global override regardless of media query. */

/* 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. */
@media (max-width: 600px) {
    .onboarding-banner[b-u8uon269pe] {
        margin: 0 10px 12px;
        padding: 12px 14px;
        gap: 10px;
    }

    .onboarding-banner-text[b-u8uon269pe] {
        font-size: var(--text-md);
    }

    .onboarding-banner-close[b-u8uon269pe] {
        width: 36px;
        height: 36px;
        font-size: 1.25rem;
    }
}

@media (prefers-reduced-motion: reduce) {
    .onboarding-banner[b-u8uon269pe] { animation: none; }
    .onboarding-banner.dismissing[b-u8uon269pe] { animation: none; opacity: 0; max-height: 0; }
}
/* /Components/Shared/PhotoAttribution.razor.rz.scp.css */
/* This component's own rules. */

/* 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[b-ffb4edhn2k] {
    display: block;
    font-size: var(--text-xs);
    line-height: 1.4;
    color: var(--text-dim);
    padding: 6px 10px;
}

/* ==========================================================================
   Phase B — Venue card + Venue page + photo attribution
   ========================================================================== */



.photo-attribution-sep[b-ffb4edhn2k] {
    margin: 0 4px;
    color: var(--text-dim);
}

/* Read-only city display in linked state. Visually distinct from an
   editable input — no border, muted text, no hover affordance. */



.photo-attribution-licence[b-ffb4edhn2k],
.photo-attribution-source[b-ffb4edhn2k] {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: rgba(255, 255, 255, 0.25);
    text-underline-offset: 2px;
}

.photo-attribution-licence:hover[b-ffb4edhn2k],
.photo-attribution-source:hover[b-ffb4edhn2k] {
    text-decoration-color: currentColor;
}
/* /Components/Shared/PricingCards.razor.rz.scp.css */
/* The pricing cards own the tier card, its price line and the feature list. */

.pricing-teaser[b-0fud5eu2rj] {
    margin: 36px auto 0;
    padding: 24px 24px 4px;
    border-top: 1px solid var(--border);
    text-align: center;
}

.pricing-teaser-heading[b-0fud5eu2rj] {
    font-size: 1.125rem;
    font-weight: 600;
    color: var(--text-primary);
    margin: 0 0 14px;
}

.pricing-teaser-detail-link[b-0fud5eu2rj] {
    margin: 16px 0 0;
    font-size: var(--text-sm);
}

.pricing-teaser-detail-link a[b-0fud5eu2rj] {
    color: var(--text-dim);
    text-decoration: underline;
}

.pricing-teaser-detail-link a:hover[b-0fud5eu2rj] {
    color: var(--text-muted);
}

.pricing-cards[b-0fud5eu2rj] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 20px;
    align-items: stretch;
    max-width: 760px;
    margin: 0 auto;
}

.pricing-card[b-0fud5eu2rj] {
    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[b-0fud5eu2rj] {
    border-color: var(--accent);
    box-shadow: 0 0 0 1px var(--accent) inset;
}


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

.pricing-card-amount[b-0fud5eu2rj] {
    font-size: var(--display-lg);
    font-weight: 700;
    color: var(--text-primary);
    line-height: 1.1;
}

.pricing-card-period[b-0fud5eu2rj] {
    font-size: var(--text-lg);
    color: var(--text-muted);
}

.pricing-card-equivalent[b-0fud5eu2rj] {
    font-size: var(--text-md);
    color: var(--text-secondary);
    margin: 0 0 6px;
}

.pricing-card-saving[b-0fud5eu2rj] {
    font-size: var(--text-md);
    color: var(--accent-light);
    font-weight: 500;
    margin: 0 0 14px;
}

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

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

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

/* Held in this sheet: these classes are defined here, so the scoped rule carries the component
   attribute selector and outranks a global override regardless of media query. */
@media (max-width: 849px) {
    .pricing-cards[b-0fud5eu2rj] {
        grid-template-columns: 1fr;
        gap: 28px; /* extra gap because the RECOMMENDED tag overlaps the top edge */
    }
}

@media (max-width: 480px) {
    .pricing-teaser[b-0fud5eu2rj] {
        padding: 20px 16px 4px;
    }

    .pricing-card[b-0fud5eu2rj] {
        padding: 24px 20px 20px;
    }

    .pricing-card-amount[b-0fud5eu2rj] {
        font-size: var(--display-md);
    }
}

/* flex, not block: .btn centres its label with align-items and justify-content, and both are
   inert the moment this turns the box back into a block — the label then sits top-left inside
   .btn's min-height. Full width is the only thing this rule is for. */
.pricing-cta[b-0fud5eu2rj] {
    display: flex;
    width: 100%;
}

.pricing-cta.is-disabled[b-0fud5eu2rj] {
    color: var(--text-muted);
    cursor: not-allowed;
    font-style: italic;
}

.card-badge[b-0fud5eu2rj] {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translate(-50%, -50%);
    padding: 3px var(--space-3);
    border-radius: var(--radius-sm);
    background: var(--accent);
    color: var(--bg-base);
    font-size: var(--text-xs);
    font-weight: 700;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    white-space: nowrap;
}
/* /Components/Shared/QuotaComparisonTable.razor.rz.scp.css */
/* Defaults, so they sit in the layer: a page that places the table is unlayered and outranks
   them. Never !important in here — layers invert for it. */
@layer component {
    .pricing-entitlements[b-wx5sdzvqo8] {
        width: 100%;
        border-collapse: collapse;
        margin: 14px 0 18px;
        font-size: var(--text-lg);
    }

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

    .pricing-entitlements thead th[b-wx5sdzvqo8] {
        font-size: var(--text-sm);
        text-transform: uppercase;
        letter-spacing: 0.05em;
        color: var(--text-muted);
    }

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

    .pricing-entitlements tbody td[b-wx5sdzvqo8] {
        color: var(--text-primary);
    }
}
/* /Components/Shared/SearchIcon.razor.rz.scp.css */
/* A default, so it sits in the layer: the slot the icon is dropped into (.btn > svg, a caller's
   Class) is unlayered and always outranks it. Never !important in here — layers invert for it. */
@layer component {
    /* 1em so it takes the slot's font-size; the stroke is currentColor, so it takes its colour. */
    .search-icon[b-9ruk9xpviw] { width: 1em; height: 1em; display: inline-block; vertical-align: middle; flex-shrink: 0; }
}
/* /Components/Shared/ShareButton.razor.rz.scp.css */
/* The absolutely-positioned .menu's containing block — without position: relative it escapes. */
.share-control[b-caimlgr3d8] {
    position: relative;
    display: inline-flex;
}

/* Shape, size, colour and states all come from the global .btn now — this rule keeps only what is
   specific to this trigger. Scoped CSS OUTRANKS a global class (it adds an attribute to the
   selector), so anything left here would silently beat .btn: what is absent matters as much as
   what is present. A filled accent pill beside rectangular secondary buttons read as the page's
   primary action, which Share is not. */
.share-trigger[b-caimlgr3d8] {
    gap: 8px;
}

.share-trigger-label[b-caimlgr3d8] {
    line-height: 1;
}

.share-menu-backdrop[b-caimlgr3d8] {
    position: fixed;
    inset: 0;
    z-index: 40;
}

@media (max-width: 849px) {
    .share-trigger-label[b-caimlgr3d8] {
        display: none;
    }
}

/* PLACEMENT. .menu is position: absolute, which on its own means "wherever the
   last positioned ancestor happens to be" — so every dropdown in this app had
   grown its own top/left/right trio. These are the three placements actually in
   use; the anchor supplies the positioning context (position: relative).
   -start / -end rather than -left / -right: the menu aligns to the anchor's edge,
   and which physical side that is depends on writing direction. */
.menu-below-start[b-caimlgr3d8] { top: calc(100% + var(--space-2)); left: 0; }
/* /Components/Shared/SiteFooter.razor.rz.scp.css */
/* This component's own rules. */

.site-footer[b-w20bunqmrb] {
    margin: 40px auto 0;
    padding: 16px 24px 24px;
    font-size: var(--text-sm);
    color: var(--text-dim);
    font-size: var(--text-xl);
    line-height: 1.5;
    text-align: center;
}

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

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

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

.site-footer-legal[b-w20bunqmrb] {
    /* ™/© line sits under the link row; inherits the footer's dim colour. */
    margin: 8px 0 0;
}

/* Held in this sheet: a scoped rule outranks any global override, media query or not. */
@media (max-width: 480px) {
    .site-footer[b-w20bunqmrb] {
        line-height: 1.9;
    }
}
/* /Components/Shared/SourceDot.razor.rz.scp.css */
/* The service marker beside a source link, rendered by the SourceDot component.

   ALWAYS the brand colour, not only on hover. Hover is a desktop-only reveal, so on a phone the
   dot said nothing at all — and what it distinguishes, which of five destinations a link opens,
   is needed at rest. One shared "external" tint could not say that; five dots can.

   The colours were spread across app.css and two scoped sheets before; a sixth source is now one
   value in MediaSource plus one line here. */
.source-dot[b-3wfie8spaj] {
    display: inline-block;
    flex: none;
    width: 6px;
    height: 6px;
    margin-right: 5px;
    border-radius: 50%;
    background: var(--dot-color, var(--text-dim));
    vertical-align: middle;
}

/* The dot is the whole control, so it carries no gap to a label that is not there. */
.source-dot.is-bare[b-3wfie8spaj] { margin-right: 0; }

.source-dot.is-discogs[b-3wfie8spaj] { --dot-color: #ff6600; }

.source-dot.is-apple[b-3wfie8spaj] { --dot-color: #fa2d48; }

.source-dot.is-spotify[b-3wfie8spaj] { --dot-color: #1db954; }

.source-dot.is-youtube[b-3wfie8spaj] { --dot-color: #ff0000; }

/* The one that is NOT a brand colour. Inside this app a setlist.fm link goes to concert data, so
   it takes the hue the concert list already uses — and the marker in that list and the link on the
   detail page then say the same thing. */
.source-dot.is-setlistfm[b-3wfie8spaj] { --dot-color: var(--concert); }
/* /Components/Shared/StarRatingDisplay.razor.rz.scp.css */
/* Defaults, so they sit in the layer: every unlayered rule outranks them, whatever its specificity
   or order — a slot rule or a caller's class decides, this only proposes. Never !important in here:
   layers invert for it. */
@layer component {
    /* Inline in a row of text rather than in a form. */
    .rating-sm[b-eoi8iieurs] { --rating-size: 14px; }
}
/* /Components/Shared/StarRatingInput.razor.rz.scp.css */
.rating[b-adwpqy0d4m] {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
}

/* Ten hit zones at 10% each — half a star apiece, by construction rather than
   by measurement. */
.rating-hit[b-adwpqy0d4m] {
    position: absolute;
    top: 0;
    height: 100%;
    width: 10%;
    cursor: pointer;
}
/* /Components/Shared/TabBar.razor.rz.scp.css */
/* The wrapper exists to scope role="tablist" to the tabs alone — the bar around it also hosts the
   spacer and Trailing actions, which a tablist may not own. display: contents keeps the tabs as
   flex items of the BAR, so wrapping them changes no layout: the bar's gap, wrapping and the
   spacer's stretch all behave exactly as when the tabs were its direct children. Do not give this
   element box styling — it generates no box — and do not "simplify" it away: the role must not
   move back onto the bar. */
.tab-strip[b-op8obpb97m] {
    display: contents;
}

.detail-tab[b-op8obpb97m] {
    padding: 14px 18px 13px;
    font-size: var(--text-md);
    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[b-op8obpb97m] { color: var(--text-primary); }

.detail-tab.is-active[b-op8obpb97m] { color: var(--accent-light); border-bottom-color: var(--accent); }

.detail-tab:disabled[b-op8obpb97m] { color: var(--text-dim); cursor: not-allowed; }
/* /Components/Shared/TrashIcon.razor.rz.scp.css */
/* A default, so it sits in the layer: the slot the icon is dropped into (.btn > svg, a caller's
   Class) is unlayered and always outranks it. Never !important in here — layers invert for it. */
@layer component {
    /* 1.15em rather than the magnifier's 1em: a bin is mostly outline where a magnifier is mostly
       enclosed shape, so at an identical box it reads smaller. */
    .trash-icon[b-zu4ddyecox] { width: 1.15em; height: 1.15em; display: inline-block; vertical-align: middle; flex-shrink: 0; }
}
/* /Components/Shared/VenueMap.razor.rz.scp.css */
/* The embedded form sits in a card's media slot, which paints its own surface behind the tiles. */
.map-embed[b-7w62n8k0iq] {
    background: transparent;
}
/* /Components/Shared/VenuesMap.razor.rz.scp.css */
/* The map's own: the popup a pin opens. venue-map.js builds it and Google Maps mounts it in its
   overlay pane inside the container this component renders, so nothing in it carries this sheet's
   scope attribute; ::deep from the container is how the rules reach it. */

/* An info window has a WHITE background whatever the page theme, so its text takes light-mode
   colours directly rather than the dark-theme --text-primary token (white on white). The two
   greys are the popup's own tokens, declared here where the white begins. */
[b-piz5l83rg4] .venue-map-popup {
    --popup-ink: #1f2937;
    --popup-muted: #6b7280;
    font-family: inherit;
    color: var(--popup-ink);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 200px;
}

[b-piz5l83rg4] .venue-map-popup-photo {
    width: 100%;
    max-height: 120px;
    object-fit: cover;
    border-radius: var(--radius-md);
    margin-bottom: var(--space-1);
}

[b-piz5l83rg4] .venue-map-popup-attribution {
    font-size: var(--text-xs);
    color: var(--popup-muted);
    margin-bottom: var(--space-1);
    line-height: 1.3;
}

[b-piz5l83rg4] .venue-map-popup-attribution a {
    color: inherit;
    text-decoration: underline;
}

[b-piz5l83rg4] .venue-map-popup-name {
    font-size: var(--text-lg);
    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;
}

[b-piz5l83rg4] .venue-map-popup-name:hover {
    text-decoration: underline;
}

[b-piz5l83rg4] .venue-map-popup-city {
    font-size: var(--text-sm);
    color: var(--popup-muted);
}

[b-piz5l83rg4] .venue-map-popup-closed {
    display: inline-block;
    font-size: var(--text-xs);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    padding: 2px 6px;
    border-radius: var(--radius-sm);
    background: rgba(217, 119, 6, 0.15);
    color: #b45309;
    border: 1px solid rgba(217, 119, 6, 0.4);
}

[b-piz5l83rg4] .venue-map-popup-visits {
    font-size: var(--text-sm);
    font-weight: 600;
}
/* /Components/Shared/WantedDot.razor.rz.scp.css */
/* A bookmark, not a second coloured disc. Owned is a 7px filled disc; two discs separated only by
   hue would carry the distinction in colour alone (WCAG 1.4.1), and red-green is the pairing most
   colour blind users lose. A silhouette carries it in shape, so the hue is free to be whatever
   reads best — which is why --wanted no longer aliases --accent.

   The stroke is a CONTRASTING edge, not the fill colour. Without it the mark has nothing but its
   own hue between it and whatever it lands on, and over a light sleeve or a selected row's tinted
   ground that is not enough. The edge gives it a constant outline on any surface. */
.wanted-dot[b-aqczoij8gd] {
    display: block;
    width: 9px;
    height: 13px;
    flex-shrink: 0;
}

/* Over artwork rather than beside text, so it takes the box the owned tick already occupies. */
.wanted-dot.is-wall[b-aqczoij8gd] {
    width: 17px;
    height: 24px;
    filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 0.75));
}

/* /Components/Subscription/SuspendedBanner.razor.rz.scp.css */
/* This component's own rules. */

.app-banner-danger[b-bw2mmahntr] {
    background: var(--danger-bg);
    color: var(--danger-text);
    border-color: var(--danger-border);
}
/* /Components/Subscription/TrialBanner.razor.rz.scp.css */
/* This component's own rules. */

.app-banner-accent[b-m7mxooqd9a] {
    background: var(--accent-bg);
    color: var(--accent-light);
    border-color: var(--accent-border);
}
