/* net4V global styles.

   Radzen supplies the component theme (material-dark-base). The rules below are
   app-level only: shared custom properties and the validation/error styling the
   Identity pages depend on.

   Bootstrap is still linked in App.razor. The scaffolded Identity area is ~30
   pages of Bootstrap markup and only Login/Register are re-skinned in 01-03;
   dropping Bootstrap now would visually break the rest (Manage/*, password
   reset, 2FA). Removing it is a full Identity re-skin, tracked as deferred work.
*/

:root {
    /* Height of the persistent player bar. Everything that sits against the bar
       is derived from this one value -- the content's clearance, the equalizer
       panel, and the floating video -- so changing it here moves them all
       together rather than leaving one of them behind.

       6rem, not the 4.5rem this was, because the now-playing block is three
       stacked rows and 4.5rem could not hold them:

           title    1rem    x ~1.5 line-height  ~24px
           artist   0.8125rem x ~1.5            ~20px
           stars    min-height on .net4v-star    44px
                                                -----
                                                ~88px  against 72px of bar

       The stars are what broke it. 08-03 added them to the meta stack and
       `Compact` narrows a star from 44px to 36px WIDE while leaving its height at
       44 -- so the row costs full height however compact it looks. The overflow
       clipped the title at the top of the bar and the stars at the bottom, and it
       showed only on desktop, because the same phase hides the stars below 768px.

       Phone stays at 4rem: the stars are hidden there, so the stack is just title
       and artist. Phase 9 brings them back in a two-row layout and will have to
       raise that one too. */
    --net4v-player-height: 6rem;
}

@media (max-width: 768px) {
    :root {
        --net4v-player-height: 4rem;
    }
}

a, .btn-link {
    color: #006bb7;
}

.btn-primary {
    color: #fff;
    background-color: #1b6ec2;
    border-color: #1861ac;
}

.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
  box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}

.content {
    padding-top: 1.1rem;
}

h1:focus {
    outline: none;
}

.valid.modified:not([type=checkbox]) {
    outline: 1px solid #26b050;
}

.invalid {
    outline: 1px solid #e50000;
}

.validation-message {
    color: #e50000;
}

.blazor-error-boundary {
    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
    padding: 1rem 1rem 1rem 3.7rem;
    color: white;
}

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

.darker-border-checkbox.form-check-input {
    border-color: #929292;
}

.form-floating > .form-control-plaintext::placeholder, .form-floating > .form-control::placeholder {
    color: var(--bs-secondary-color);
    text-align: end;
}

.form-floating > .form-control-plaintext:focus::placeholder, .form-floating > .form-control:focus::placeholder {
    text-align: start;
}
/* --- Auth pages (Login / Register) --- */
.net4v-auth-card {
    max-width: 26rem;
    margin: 2rem auto;
    display: flex;
    flex-direction: column;
    gap: 1rem;
}

.net4v-auth-card .net4v-checkbox {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    cursor: pointer;
}

.net4v-auth-or,
.net4v-auth-links {
    margin-top: 0.5rem;
}

@media (max-width: 768px) {
    .net4v-auth-card {
        margin: 1rem auto;
        max-width: 100%;
    }
}

/* --- Library track cards (mobile) --- */
.net4v-track-card {
    display: flex;
    /* Wraps so the stars can take a line of their own. Without this they become a
       fourth item competing for width and squeeze the title down to an ellipsis --
       which the 44px and no-scroll assertions both happily allow. */
    flex-wrap: wrap;
    align-items: center;
    gap: 0.75rem;
    padding: 0.6rem 0.5rem;
    border-bottom: 1px solid var(--rz-base-300, #333);
    cursor: pointer;
    /* Comfortably tappable: 44px is the smallest reliable touch target. */
    min-height: 56px;
}

.net4v-track-card:active {
    background: var(--rz-base-200, #262626);
}

.net4v-track-art {
    width: 48px;
    height: 48px;
    flex: 0 0 48px;
    object-fit: cover;
    border-radius: 0.25rem;
    background: var(--rz-base-300, #2a2a2a);
}

.net4v-track-meta {
    flex: 1 1 auto;
    min-width: 0;   /* lets the ellipsis work instead of forcing overflow */
}

.net4v-track-title,
.net4v-track-sub {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.net4v-track-sub {
    font-size: 0.8125rem;
    opacity: 0.7;
}

.net4v-track-right {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 0.25rem;
    flex: 0 0 auto;
}

.net4v-track-duration {
    font-size: 0.8125rem;
    opacity: 0.7;
    font-variant-numeric: tabular-nums;
}

/* --- Playlists --- */
.net4v-page-title { margin: 0; }

.net4v-playlist-card {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.75rem 0.5rem;
    border-bottom: 1px solid var(--rz-base-300, #333);
    cursor: pointer;
    min-height: 56px;
}

.net4v-playlist-meta { flex: 1 1 auto; min-width: 0; }
.net4v-playlist-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.net4v-playlist-sub { font-size: 0.8125rem; opacity: 0.7; }

/* --- Playlist editor items --- */
.net4v-playlist-items {
    display: flex;
    flex-direction: column;

    /* pan-y, NOT pan-x. This is a vertical list, and pan-x permits only sideways
       panning -- which meant a touch anywhere on a playlist could not scroll it at
       all, and a long playlist could only be scrolled from the margins outside the
       rows. The intent was to stop the page scrolling mid-drag, but the drag handle
       already owns its own gesture with `touch-action: none` below, so the
       container never needed to give up vertical scrolling to achieve it. */
    touch-action: pan-y;
}

.net4v-playlist-item {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.4rem 0.5rem;
    border-bottom: 1px solid var(--rz-base-300, #333);
    background: var(--rz-base-background-color, transparent);
    min-height: 52px;
}

.net4v-playlist-item.net4v-dragging {
    opacity: 0.85;
    background: var(--rz-base-300, #333);
    box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.5);
}

/* Tombstones and unplayable items read as inert, which is the whole visible
   payoff of the SetNull decision in 04-01. */
.net4v-playlist-item.unavailable .net4v-item-title,
.net4v-playlist-item.unavailable .net4v-item-art {
    opacity: 0.45;
    text-decoration: line-through;
}

.net4v-drag-handle {
    cursor: grab;
    display: flex;
    align-items: center;
    /* 44px is the smallest reliable touch target. */
    min-width: 44px;
    min-height: 44px;
    justify-content: center;
    opacity: 0.6;
    touch-action: none;   /* the handle owns the gesture, not the scroller */
}

.net4v-drag-handle:active { cursor: grabbing; opacity: 1; }

.net4v-item-index {
    min-width: 1.75rem;
    text-align: right;
    font-size: 0.8125rem;
    opacity: 0.6;
    font-variant-numeric: tabular-nums;
}

.net4v-item-art {
    width: 36px; height: 36px;
    object-fit: cover;
    border-radius: 0.25rem;
    flex: 0 0 36px;
}

.net4v-item-meta { flex: 1 1 auto; min-width: 0; }
.net4v-item-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.net4v-item-sub {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    font-size: 0.8125rem;
    opacity: 0.7;
}

.net4v-item-duration {
    font-size: 0.8125rem;
    opacity: 0.7;
    font-variant-numeric: tabular-nums;
    flex: 0 0 auto;
}

@media (max-width: 768px) {
    .net4v-item-index,
    .net4v-item-duration { display: none; }

    /* The end padding is a scroll gutter, not decoration. Every row is wall-to-wall
       44px touch targets out to the remove button, so on a phone there was nowhere
       to put a thumb that was not also a control -- starting a scroll meant starting
       it on a button and hoping the gesture was read as a pan rather than a tap.
       This reserves a strip at the trailing edge that holds nothing interactive.

       Logical property, so it follows the writing direction rather than assuming the
       controls are always on the right. */
    .net4v-playlist-item {
        gap: 0.25rem;
        padding: 0.4rem 0.25rem;
        padding-inline-end: 1.75rem;
    }
}

/* Queue position line in the player. */
.net4v-queue-position {
    margin-inline-start: 0.4rem;
    opacity: 0.75;
}

/* Shuffle/repeat toggles stay compact so the transport keeps its centre of mass
   on play/pause, which is the control that matters most at 390px. */
.net4v-mode-button {
    min-width: 2rem;
}

@media (max-width: 768px) {
    .net4v-queue-position { display: block; margin-inline-start: 0; }
}

/* --- Audio diagnostics --- */
.net4v-audio-diagnostics { margin-top: 1rem; }

.net4v-diag-row {
    display: flex;
    justify-content: space-between;
    gap: 1rem;
    padding: 0.15rem 0;
    font-size: 0.875rem;
}

.net4v-diag-row span { opacity: 0.7; }
.net4v-ok { color: var(--rz-success, #4caf50); }
.net4v-warn { color: var(--rz-warning, #ff9800); }

/* Radzen pads .rz-body by 1rem. That padding put a gap above the sticky player
   bar and pushed the bar's contents out of line with the list below, and both
   were being compensated for separately -- a negative margin for the gap, a
   padding sum for the indent -- which is two corrections for one cause. Zeroing
   it removes the cause: .net4v-content supplies the page padding, and the bar
   sits flush against the header with a single padding of its own.

   It lives here rather than in MainLayout.razor.css because ::deep cannot reach
   it. CSS isolation only puts the scope attribute on elements a component writes
   itself, and .rz-body is rendered by RadzenBody, so the compiled
   "[b-xxx] .rz-body" has no ancestor to match.

   The descendant selector is deliberate: app.css is linked BEFORE the Radzen
   theme, so a bare .rz-body would lose the tie on source order. */
.rz-layout .rz-body {
    padding: 0;
    /* Transparent so the layout's washes show through. Left opaque, the body
       paints over them and the whole effect is invisible -- which is exactly what
       happened on the first attempt. */
    background: transparent;
}

/* --- Player bar position ---------------------------------------------------
   The bar sits under the header, above the library list, EXCEPT while a video is
   on screen: there it returns to the bottom, out of the picture's way and with
   the floating video anchored above it.

   Driven by a class net4v-player.js puts on <body>, not by :has() on the video
   container: the container belongs to MediaPlayer and the bar to MainLayout, two
   different CSS isolation scopes whose generated attributes cannot both appear
   in one selector. These rules are global and so carry no scope attribute, which
   is exactly what lets them outrank the scoped defaults they override. */
body.net4v-video-mode .net4v-player-region {
    /* Out of the flow and pinned to the bottom. .rz-body carries a transform, so
       it -- not the viewport -- is the containing block here, which is what keeps
       the bar inside the content area rather than across the sidebar. */
    position: fixed;
    inset-inline: 0;
    top: auto;
    bottom: 0;
    /* The bar grows by the inset rather than being pushed up, so its background
       reaches the bottom of the screen while its contents stay above the home
       indicator. */
    height: calc(var(--net4v-player-height) + env(safe-area-inset-bottom));
    padding-bottom: env(safe-area-inset-bottom);
    border-top: 1px solid var(--rz-base-300, #333);
    border-bottom: none;
}

/* Taking the bar out of the flow removes the space it was occupying, so the
   clearance has to come back as padding. */
body.net4v-video-mode .net4v-content {
    padding-bottom: calc(var(--net4v-player-height) + 1rem + env(safe-area-inset-bottom));
}

@media (max-width: 768px) {
    body.net4v-video-mode .net4v-content {
        padding-bottom: calc(var(--net4v-player-height) + 0.75rem + env(safe-area-inset-bottom));
    }
}

/* --- Equalizer panel --- */
/* Positioned against the player bar itself rather than the viewport, so it
   follows the bar between the top and bottom layouts without either of them
   needing to know where the other ended up. The bar is a positioned element in
   both states, so it is the containing block either way. */
.net4v-audio-panel {
    position: absolute;
    inset-inline: 0;
    top: 100%;
    max-height: 55vh;
    overflow-y: auto;
    padding: 0.75rem 1rem;
    background: var(--rz-base-background-color, #1a1a1a);
    border-bottom: 1px solid var(--rz-base-300, #333);
    box-shadow: 0 0.5rem 1.5rem rgba(0, 0, 0, 0.45);
    z-index: 8;
}

body.net4v-video-mode .net4v-audio-panel {
    top: auto;
    bottom: 100%;
    border-top: 1px solid var(--rz-base-300, #333);
    border-bottom: none;
    box-shadow: 0 -0.5rem 1.5rem rgba(0, 0, 0, 0.45);
}

.net4v-eq-toolbar { width: 100%; }
::deep .net4v-eq-preset { min-width: 12rem; }
.net4v-eq-hint { opacity: 0.7; }

.net4v-eq-bands {
    display: flex;
    align-items: flex-end;
    gap: 0.25rem;
    padding: 0.75rem 0 0.25rem;
    /* Ten vertical sliders cannot be made comfortable across 390px, so the strip
       scrolls rather than squeezing each one below a usable touch target. */
    overflow-x: auto;
    overscroll-behavior-x: contain;
}

.net4v-eq-bands.bypassed { opacity: 0.45; }

.net4v-eq-band {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 0.35rem;
    /* 44px keeps each slider a reliable touch target. */
    flex: 0 0 44px;
}

::deep .net4v-eq-slider {
    height: 8rem;
}

.net4v-eq-value,
.net4v-eq-label {
    font-size: 0.6875rem;
    opacity: 0.75;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.net4v-eq-value { min-height: 1rem; }

@media (max-width: 768px) {
    .net4v-audio-panel { max-height: 60vh; padding: 0.5rem 0.5rem; }
    ::deep .net4v-eq-slider { height: 6.5rem; }
    ::deep .net4v-eq-preset { min-width: 9rem; }
}

/* --- Frequency meter --- */
:root {
    --net4v-meter-bar: #4db6ac;
    --net4v-meter-peak: #e0f2f1;
}

.net4v-meter-wrap {
    position: relative;
    width: 100%;
    height: 7rem;
}

.net4v-meter {
    display: block;
    width: 100%;
    height: 100%;
    background: var(--rz-base-200, #1e1e1e);
    border-radius: 0.375rem;
}

.net4v-meter-idle {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
}

@media (max-width: 768px) {
    /* Shorter so it does not crowd the sliders on a phone. */
    .net4v-meter-wrap { height: 4.5rem; }
}

/* --- Responsive audit fixes (06-01) --- */

/* The page body must never scroll horizontally. Wide content scrolls inside its
   own container instead -- applied globally so a page added later inherits it. */
html, body {
    overflow-x: hidden;
    max-width: 100%;
}

/* Radzen grids and any wide block scroll themselves. */
::deep .rz-datatable,
.net4v-grid,
.net4v-eq-bands {
    max-width: 100%;
    overflow-x: auto;
}

/* Long words and URLs must wrap rather than force the page wide -- the Play URL
   page and playlist titles are both prone to this. */
.net4v-content {
    overflow-wrap: anywhere;
}

/* Touch targets. 44px is the smallest reliably tappable size; Radzen's small
   buttons render below that, so they are floored on touch devices only, where
   the extra size costs nothing. */
@media (hover: none) {
    ::deep .rz-button-sm,
    ::deep .rz-button-md {
        min-width: 44px;
        min-height: 44px;
    }
}

/* Users who ask for less motion get it everywhere, not just in the EQ panel. */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* --- Diagnostics --- */
.net4v-check {
    padding: 0.5rem 0.75rem;
    border-inline-start: 3px solid var(--rz-base-400, #555);
    border-radius: 0.25rem;
    background: var(--rz-base-200, #1e1e1e);
}

.net4v-check.pass { border-inline-start-color: var(--rz-success, #4caf50); }
.net4v-check.warn { border-inline-start-color: var(--rz-warning, #ff9800); }
.net4v-check.fail { border-inline-start-color: var(--rz-danger, #f44336); }

.net4v-check-head {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
}

.net4v-check-detail {
    font-size: 0.875rem;
    opacity: 0.85;
    /* Long paths must wrap rather than force the page wide at 390px. */
    overflow-wrap: anywhere;
    margin-top: 0.15rem;
}

.net4v-check-fix {
    font-size: 0.8125rem;
    opacity: 0.7;
    margin-top: 0.25rem;
    font-style: italic;
}

.net4v-diag-row strong { overflow-wrap: anywhere; text-align: end; }

/* --- Add tracks dialog --- */
.net4v-add-tracks { height: 100%; display: flex; flex-direction: column; }
::deep .net4v-add-search { flex: 1 1 16rem; min-width: 10rem; }

.net4v-add-list {
    flex: 1 1 auto;
    overflow-y: auto;
    border: 1px solid var(--rz-base-300, #333);
    border-radius: 0.375rem;
}

.net4v-add-row {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.4rem 0.6rem;
    border-bottom: 1px solid var(--rz-base-300, #333);
    cursor: pointer;
    /* 44px keeps rows tappable on a phone. */
    min-height: 48px;
}

.net4v-add-row:last-child { border-bottom: none; }
.net4v-add-row:hover { background: var(--rz-base-200, #262626); }
.net4v-add-row.selected { background: var(--rz-primary-lighter, #1e3a38); }

.net4v-add-art {
    width: 32px; height: 32px; flex: 0 0 32px;
    object-fit: cover; border-radius: 0.2rem;
    background: var(--rz-base-300, #2a2a2a);
}

.net4v-add-meta { flex: 1 1 auto; min-width: 0; }

.net4v-add-title,
.net4v-add-sub { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.net4v-add-sub { font-size: 0.8125rem; opacity: 0.7; }

.net4v-add-duration {
    font-size: 0.8125rem; opacity: 0.7;
    font-variant-numeric: tabular-nums; flex: 0 0 auto;
}

.net4v-add-actions {
    padding-top: 0.5rem;
    border-top: 1px solid var(--rz-base-300, #333);
}

@media (max-width: 768px) {
    .net4v-add-duration { display: none; }
    .net4v-add-row { gap: 0.4rem; padding: 0.4rem 0.35rem; }
}

/* --- Add-to-playlist picker ------------------------------------------------
   Global rather than component-scoped: Radzen renders dialogs into a host at the
   root of the document, so these live outside the calling page's subtree. */
.net4v-playlist-picker {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    max-height: 40vh;
    overflow-y: auto;
}

.net4v-playlist-pick {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75rem;
    width: 100%;
    /* Comfortably tappable: this is the primary control of the dialog and the
       one most likely to be used on a phone. */
    min-height: 2.75rem;
    padding: 0.5rem 0.75rem;
    border: 1px solid var(--rz-base-300, #333);
    border-radius: 0.375rem;
    background: transparent;
    color: inherit;
    font: inherit;
    text-align: start;
    cursor: pointer;
}

.net4v-playlist-pick:hover:not(:disabled) {
    background: var(--rz-base-200, #2a2a2a);
    border-color: var(--rz-primary, #8a2be2);
}

.net4v-playlist-pick:disabled { opacity: 0.5; cursor: default; }

.net4v-pick-name { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.net4v-pick-count { opacity: 0.6; font-size: 0.8125rem; flex: 0 0 auto; }
.net4v-add-target { opacity: 0.75; }

/* --- Star rating -----------------------------------------------------------
   Global, not component-scoped: this control renders inside Radzen dialogs in
   later work, and dialogs render into a host at the document root where scoped
   CSS does not reach. */
.net4v-stars {
    /* Reversed so a star's CSS sibling selector reaches the stars to its LEFT --
       see the component for why the hover preview cannot be done in C#. */
    display: inline-flex;
    flex-direction: row-reverse;
    justify-content: flex-end;
    align-items: center;
    border-radius: 0.25rem;
}

.net4v-stars:focus-visible {
    outline: 2px solid var(--rz-primary, #8a2be2);
    outline-offset: 2px;
}

.net4v-star {
    /* 44px: the whole control is five targets side by side and this is the
       tightest thing in the app on a 390px phone. */
    min-width: 44px;
    min-height: 44px;
    padding: 0;
    border: 0;
    background: none;
    cursor: pointer;
    font-size: 1.35rem;
    line-height: 1;
    color: var(--rz-base-400, #555);
    transition: color 0.12s ease, transform 0.12s ease;
}

.net4v-star-on { color: var(--rz-warning, #ffb300); }

/* Hover preview, pointer devices only. On touch there is no hover, and a preview
   that sticks after a tap reads as a bug. */
@media (hover: hover) {
    .net4v-stars:not(.net4v-stars-readonly) .net4v-star:hover,
    .net4v-stars:not(.net4v-stars-readonly) .net4v-star:hover ~ .net4v-star {
        color: var(--rz-warning, #ffb300);
        transform: scale(1.08);
    }
}

@media (prefers-reduced-motion: reduce) {
    .net4v-star { transition: none; }
    .net4v-stars .net4v-star:hover,
    .net4v-stars .net4v-star:hover ~ .net4v-star { transform: none; }
}

/* The dense DESKTOP grid row only -- that grid is hidden below 768px. The mobile
   card uses full-size stars on their own line, because five 44px targets need
   220px and will not share a row with a title and a duration at 390px. */
.net4v-stars-compact .net4v-star {
    min-width: 36px;
    min-height: 44px;
    font-size: 1.1rem;
}

/* Mobile card: stars get their own row beneath the title. flex-basis 100% is what
   forces the wrap; the card is a flex row, not a grid. */
.net4v-track-stars {
    flex: 0 0 100%;
    display: flex;
    justify-content: flex-start;
    margin-top: -0.2rem;
}

/* The player bar is 3.5rem on a phone and already carries a title, an artist and
   the transport. A 44px star row does not fit, and squeezing it turns the title
   into an ellipsis. Rating stays available on every library row, which is where
   a phone user is choosing tracks anyway. */
@media (max-width: 768px) {
    .net4v-player-region .net4v-stars { display: none; }
}

.net4v-stars-readonly .net4v-star { cursor: default; }

.net4v-random-all {
    display: flex;
    align-items: center;
    gap: 0.4rem;
    /* Its own tap target on a phone, where it sits under the media filters. */
    min-height: 44px;
}

.net4v-random-all label { cursor: pointer; white-space: nowrap; }

.net4v-random-all input[type="checkbox"] {
    width: 1.1rem;
    height: 1.1rem;
    cursor: pointer;
    /* Follows the Radzen theme's accent rather than the browser default blue. */
    accent-color: var(--rz-primary, #8a2be2);
}

.net4v-random-all input[type="checkbox"]:disabled,
.net4v-random-all input[type="checkbox"]:disabled + label { opacity: 0.5; cursor: default; }


/* --- Application header -----------------------------------------------------
   Global rather than scoped to MainLayout: these style elements that Radzen
   renders, and CSS isolation only reaches elements the component writes itself. */
.net4v-header {
    width: 100%;
    padding: 0 0.75rem;
}

/* The spacer is what pushes the account and sign-out to the right edge. */
.net4v-header-spacer {
    flex: 1 1 auto;
}

.net4v-brand {
    font-size: 1.25rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-decoration: none;
}

/* ===========================================================================
   Gradients
   ---------------------------------------------------------------------------
   Built from the theme's own two accents -- primary #bb86fc and secondary
   #01a299 -- rather than new colours, so the app still looks like one thing.

   Three tokens, used everywhere, so the look can be retuned in one place:
     accent   a diagonal purple-to-teal, for things that should draw the eye
     surface  a barely-there top light, for panels and bars
     glow     large soft washes behind the whole page

   Every selector here needs more weight than the Radzen theme's own, because
   app.css is linked BEFORE the theme -- a bare .rz-header would lose the tie on
   source order. Hence .rz-layout in front of each.
   =========================================================================== */
:root {
    /* lightseagreen leads, the theme's own teal carries the middle, darkslategray
       grounds it, and the theme purple survives only as a final highlight -- so
       the app reads as teal with a violet edge rather than the reverse. */
    --net4v-teal: #20b2aa;          /* lightseagreen */
    --net4v-teal-deep: #01a299;     /* the theme's secondary */
    --net4v-slate: #2f4f4f;         /* darkslategray */

    --net4v-accent-gradient: linear-gradient(135deg, #20b2aa 0%, #01a299 42%, #2f4f4f 88%, #bb86fc 140%);
    --net4v-accent-soft: linear-gradient(135deg, rgba(32, 178, 170, 0.28), rgba(47, 79, 79, 0.34));

    /* Surfaces are tinted with darkslategray rather than neutral grey, which is
       what gives every panel the same sea-green cast instead of looking grey with
       coloured bits stuck on. */
    --net4v-surface-gradient: linear-gradient(180deg, rgba(32, 178, 170, 0.10), rgba(47, 79, 79, 0.04) 70%);
    --net4v-surface-base: rgba(24, 38, 38, 0.78);
}

/* Retargets the THEME's own primary, which colours links, the active filter,
   text buttons, focus rings and slider handles. Without it the shell goes teal
   while every control stays violet, and the two fight each other.

   "html:root", not ":root" and not "body". The theme declares these on :root and
   is linked afterwards, so a plain :root rule ties and loses -- html:root outranks
   it. And it has to be at the root, not on body: the theme DERIVES other variables
   there, like --rz-slider-handle-background-color: var(--rz-primary), and a var()
   resolves where it is declared. Overriding lower down leaves every derived value
   still holding the old purple, which is exactly how the slider handle stayed
   violet on a fully teal bar. */
html:root {
    --rz-primary: #20b2aa;
    --rz-primary-light: #3fc7bf;
    --rz-primary-dark: #01a299;
    --rz-primary-darker: #017c76;
    --rz-primary-lighter: rgba(32, 178, 170, 0.16);
}

/* The page itself: two soft washes, one per accent, behind everything. Large and
   low-opacity so they read as depth rather than as colour. */
/* "body .rz-layout", not ".rz-layout": the theme sets a background on that exact
   selector, and a bare match TIES on specificity and then loses on source order
   because app.css is linked first. The washes were invisible until this changed --
   the same trap this file warns about, walked into two rules later. */
body .rz-layout {
    background:
        /* Centres sit ON the viewport, not above it. The first version placed them
           at -14% and -4%, so only the faint tails were ever on screen and the
           whole effect looked like nothing had been applied -- which is how it
           looked, until the computed styles said otherwise. */
        radial-gradient(1200px 780px at 4% 2%, rgba(32, 178, 170, 0.30), transparent 62%),
        radial-gradient(1050px 680px at 99% 14%, rgba(1, 162, 153, 0.26), transparent 60%),
        radial-gradient(950px 640px at 58% 104%, rgba(47, 79, 79, 0.55), transparent 64%),
        radial-gradient(700px 520px at 88% 92%, rgba(187, 134, 252, 0.10), transparent 60%),
        /* A darkslategray floor rather than flat near-black, so the washes have
           something in the same family to sit on. */
        linear-gradient(180deg, #101c1c 0%, #0d1717 100%);
}

/* Header and sidebar are translucent over that wash rather than opaque slabs,
   so the glow reads through them. */
.rz-layout .rz-header {
    background: var(--net4v-surface-gradient), rgba(22, 38, 38, 0.78);
    backdrop-filter: blur(8px);
    border-bottom: 1px solid rgba(32, 178, 170, 0.30);
    box-shadow: 0 1px 0 rgba(255, 255, 255, 0.03), 0 6px 18px rgba(0, 0, 0, 0.28);
}

.rz-layout .rz-sidebar {
    background: linear-gradient(180deg, rgba(32, 178, 170, 0.08), rgba(13, 23, 23, 0.35)), rgba(20, 34, 34, 0.80);
    backdrop-filter: blur(8px);
    border-right: 1px solid rgba(32, 178, 170, 0.16);
}

/* The selected nav item carries the accent, which is the one place in the shell
   that should be unmistakable. */
.rz-layout .rz-navigation-item-wrapper.rz-navigation-item-wrapper-active,
.rz-layout .rz-navigation-item-wrapper:hover {
    background: var(--net4v-accent-soft);
    border-radius: 0.375rem;
}

/* The player bar: lit from above, with a hairline of accent along its edge. */
.net4v-player-region {
    background: var(--net4v-surface-gradient), rgba(21, 36, 36, 0.90) !important;
    backdrop-filter: blur(10px);
    border-bottom: 1px solid rgba(32, 178, 170, 0.28);
}

body.net4v-video-mode .net4v-player-region {
    border-bottom: none;
    border-top: 1px solid rgba(32, 178, 170, 0.28);
}

/* Primary and filled buttons pick up the accent. Text and light variants stay
   flat -- a gradient on every button is noise, not emphasis. */
.rz-layout .rz-button.rz-primary.rz-variant-filled,
.rz-layout .rz-button.rz-secondary.rz-variant-filled {
    background: var(--net4v-accent-gradient);
    border: none;
    color: #12121a;
    font-weight: 600;
    box-shadow: 0 2px 12px rgba(32, 178, 170, 0.30);
}

.rz-layout .rz-button.rz-primary.rz-variant-filled:hover,
.rz-layout .rz-button.rz-secondary.rz-variant-filled:hover {
    filter: brightness(1.08);
    box-shadow: 0 4px 18px rgba(32, 178, 170, 0.42);
}

/* Grid: a lifted header row and a gradient wash on hover, so a long library
   scans as rows rather than as a wall. */
.rz-layout .rz-data-grid .rz-grid-table thead th {
    background: linear-gradient(180deg, rgba(32, 178, 170, 0.16), rgba(47, 79, 79, 0.10));
    border-bottom: 1px solid rgba(187, 134, 252, 0.20);
}

.rz-layout .rz-data-grid .rz-grid-table tbody tr:hover {
    background: linear-gradient(90deg, rgba(32, 178, 170, 0.16), rgba(47, 79, 79, 0.10) 70%, transparent);
}

/* Cards and dialogs sit on the same lit surface as the bars. */
.rz-layout .rz-card,
.rz-dialog-content,
.net4v-audio-panel {
    background: var(--net4v-surface-gradient), rgba(24, 40, 40, 0.94);
}

/* The slider fills -- seek and volume -- read as progress, so they get the
   accent rather than a flat primary. */
.rz-layout .rz-slider-range {
    background: var(--net4v-accent-gradient);
}

/* Mobile track cards: the same hover wash, and a divider that fades out rather
   than ruling a hard line across the screen. */
.net4v-track-card {
    border-bottom: 1px solid transparent;
    border-image: linear-gradient(90deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0)) 1;
}

.net4v-track-card:active,
.net4v-track-card:hover {
    background: linear-gradient(90deg, rgba(32, 178, 170, 0.16), transparent 75%);
}

/* Brand wordmark in the accent, which is the cheapest way to make a plain dark
   header look deliberate. */
.net4v-brand {
    /* Teal-only rather than the shared accent: the accent ends in the theme
       purple, and on a word this short that tail is most of what shows. */
    background: linear-gradient(120deg, #3fc7bf 0%, #20b2aa 45%, #01a299 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

/* The grid sits on its own faintly lifted panel so a long library reads as a
   surface rather than as text floating on the page background. */
.rz-layout .rz-data-grid {
    background: linear-gradient(180deg, rgba(32, 178, 170, 0.07), rgba(47, 79, 79, 0.05));
    border: 1px solid rgba(32, 178, 170, 0.18);
    border-radius: 0.5rem;
    overflow: hidden;
}

/* Radzen paints its own row backgrounds; clearing them lets the panel and the
   hover wash show instead of a flat slab per row. */
.rz-layout .rz-data-grid .rz-grid-table tbody tr,
.rz-layout .rz-data-grid .rz-grid-table tbody td {
    background: transparent;
}

/* Toolbar controls: same lit surface as the panels, so the row of filters reads
   as one strip rather than a line of separate boxes. */
.rz-layout .rz-textbox,
.rz-layout .rz-dropdown {
    background: linear-gradient(180deg, rgba(32, 178, 170, 0.10), rgba(47, 79, 79, 0.06));
    border: 1px solid rgba(32, 178, 170, 0.22);
}

.rz-layout .rz-textbox:focus,
.rz-layout .rz-dropdown:focus-within {
    border-color: rgba(32, 178, 170, 0.70);
    box-shadow: 0 0 0 3px rgba(32, 178, 170, 0.20);
}

/* --- Blazor's unhandled-error banner ---------------------------------------
   HIDDEN BY DEFAULT. This is the whole point of the rule: the markup for this
   banner is always in the document, and Blazor reveals it by setting
   display:block inline when a circuit actually fails. Without a rule hiding it,
   it is simply always on screen -- reported as "an exception when scrolling to
   the bottom", with nothing in the browser console and nothing in the server log,
   because there was never an error at all. The template ships this rule; it was
   lost when the Bootstrap-era CSS was trimmed.

   Styled to match the app as well, so that when a real failure does surface it
   looks deliberate rather than like a stray browser artefact. */
#blazor-error-ui {
    display: none;
    position: fixed;
    inset-inline: 0;
    bottom: 0;
    z-index: 2000;
    padding: 0.75rem 1.25rem;
    padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
    background: linear-gradient(180deg, rgba(47, 79, 79, 0.96), rgba(20, 34, 34, 0.98));
    border-top: 1px solid rgba(32, 178, 170, 0.45);
    box-shadow: 0 -0.5rem 1.5rem rgba(0, 0, 0, 0.5);
    color: #e8f6f4;
    font-size: 0.9375rem;
}

#blazor-error-ui .reload {
    color: var(--net4v-teal, #20b2aa);
    text-decoration: underline;
    margin-inline-start: 0.35rem;
}

#blazor-error-ui .dismiss {
    position: absolute;
    top: 0.4rem;
    right: 0.85rem;
    cursor: pointer;
    /* A real target rather than a 12px glyph, since this is the control someone
       reaches for when something has already gone wrong. */
    padding: 0.35rem 0.5rem;
    line-height: 1;
}
