/* ========================================
   LAYOUT - HEADER & FOOTER
   Black background with reduced height
   ======================================== */

.rz-header {
    background-color: #000 !important;
    min-height: 1.5rem !important;
    padding: 0.25rem 0.5rem !important;
}

.rz-footer {
    background-color: #000 !important;
    min-height: 1rem !important;
    padding: 0.25rem 0.5rem !important;
}

.header-stack {
    padding: 0.25rem 0.5rem !important;
    justify-content: space-between;
}

.footer-stack {
    padding: 0.25rem 0.5rem !important;
    display: flex;
    width: 100%;
}

/* ========================================
   LAYOUT - BODY & CONTAINERS
   Remove vertical padding, keep horizontal padding
   ======================================== */

.rz-body {
    padding: 0 1rem !important;
    margin: 0 !important;
}

.rz-body-content {
    padding: 0 !important;
    margin: 0 !important;
}

.body-content {
    padding: 0 !important;
    margin: 0 !important;
}

    .rz-body .container,
    .rz-body .container-fluid,
    .body-content .container,
    .body-content .container-fluid {
        padding: 0 !important;
        margin: 0 !important;
        max-width: 100% !important;
    }

    .rz-body .row,
    .body-content .row {
        margin: 0 !important;
    }

/* ========================================
   HEADER - COMPONENTS
   Title, toggle, and text rotator
   ======================================== */

.header-toggle-container {
    display: flex;
    align-items: center;
    gap: 1rem;
}

.header-label {
    font-size: 1.2rem;
    font-weight: bold;
}

.title-container {
    display: flex;
    align-items: center;
    gap: 1rem;
    position: relative;
    flex: 1;
}

.nss-main-title {
    margin: 0;
    flex-shrink: 0;
    /* No font-size: the heading's only child is the NssWordmark SVG, which is
       sized by height below. The h1 is kept purely for the document outline;
       its accessible name comes from the SVG's aria-label. */
    line-height: 0;
}

/* These heights are all 0.84 of what they were when the mark still carried its
   drop shadow, because the shadow used to occupy the bottom 16% of the box.
   Keeping that factor holds the letters at the same optical size as before
   rather than making the wordmark suddenly jump larger.

   The old hard floor of ~40px went away with the shadow — the bare letterforms
   stay crisp far smaller, as the same artwork does at ~11px on the Bio page —
   so these values are about presence in the header, not legibility. */
.nss-wordmark {
    display: block;
    height: 40px;
    width: auto;
}

.text-rotator {
    min-height: 2em;
    display: flex;
    align-items: center;
    overflow: hidden;
    position: relative;
    flex-grow: 1;

    /* ── The two knobs ──────────────────────────────────────────────────────
       --rotator-pitch is the distance between one word and the next, so it
       sets how many are on screen at once (rotator width ÷ pitch). It has a
       hard floor. The lead parks at the stop line while its follower keeps
       coming, and the follower closes 29.4% of the pitch during that stop (the
       stop is 2.5s of an 8.5s period), so the pitch must cover the widest title
       — "Music Historian & Collector", ~283px at 19.2px — divided by the 0.706
       that survives: ~401px minimum. 425px leaves ~17px of clear air between
       the longest title and whatever is bearing down on it. That worst case
       only arrives at the very end of the fade: measured over a full cycle the
       tightest gap is 20px with the parked word down to 5% opacity, and 30px
       while it is still legible at 28%, so the two are never both readable that
       close together. A longer fade spends this margin — it is the reason the
       fade cannot grow much past 0.9s without also widening the pitch.

       --rotator-period is how often a word reaches the stop line, and with the
       pitch it fixes the speed: pitch ÷ period, ~50px/s here. Changing it is
       safe — the stop is a fixed share of the cycle, so the clearance above
       holds at any period; only the pace changes.

       Note these two cannot both be improved by tuning alone. At the pitch
       floor, speed = (widest + gap) ÷ (period − stop) while the word count goes
       with (period − stop) ÷ period, so a shorter period buys speed and costs
       words. Only a shorter stop — or a narrower widest title — moves both, and
       that is what the 1.6s hold and 0.9s fade below are paying for.

       Fewer words fit than a stop-and-go conveyor would allow, and that is the
       trade for continuous motion: the ground the follower covers during the
       stop is dead space that has to exist. ~2.9 words at desktop, ~3.7 at
       1920px, more on an ultrawide. */
    --rotator-pitch: 425px;
    --rotator-period: 8.5s;
    --rotator-lead-in: 4s;

    /* Derived. --rotator-count is the title count, handed over by the markup.
       0.7 is the share of a word's cycle spent travelling and MUST match the
       arrival keyframe in rotatorTravel below — it is what turns a pitch into
       the distance a word starts out at, and a word has to cross the whole
       train (count × pitch) before it comes round again. The result must land
       beyond the widest rotator anyone will render, or words pop into existence
       on screen instead of drifting in: 4165px here, good past a 5000px
       viewport. */
    --rotator-cycle: calc(var(--rotator-period) * var(--rotator-count));
    --rotator-enter: calc(var(--rotator-pitch) * 0.7 * var(--rotator-count));

    /* Dissolve the tail of the queue rather than letting the overflow clip cut
       a word off mid-letter. A fixed ramp rather than a percentage: this box is
       a few hundred pixels wide at the 1024 breakpoint and close to 2000 on an
       ultrawide, so a percentage would be an invisible sliver on one and would
       swallow half the strip on the other. 6rem also stays clear of the focus
       word at the narrow end. */
    -webkit-mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 6rem), transparent 100%);
            mask-image: linear-gradient(to right, #000 0, #000 calc(100% - 6rem), transparent 100%);
}

    /* The strip is only a frame: it stretches over the rotator so the words can
       be positioned against it, and carries the font and the optical nudge. It
       does not move — every word drives itself. */
    .text-rotator h3 {
        margin: 0;
        white-space: nowrap;
        position: absolute;
        inset: 0;
        font-size: 1.2rem;
        /* Dark until the host page's init pill has gone — see .is-live below. */
        opacity: 0;
        transition: opacity 0.6s ease-out;
        /* Nudge the rotating text down so it optically centres against the
           wordmark. Both are centred by align-items, but the wordmark's box IS
           its cap band (the artwork is cropped to the baseline) while this h3's
           box is a full line box — its ascender and descender leading push the
           visible glyphs into the upper part of it. Centring the two boxes
           therefore leaves the text sitting about 3px high.

           A flat px rather than an em, which is not the obvious choice. In
           theory the correction is proportional to this font's size (~0.151em),
           but the rendered baseline is rounded to a whole pixel — 12px into the
           line box at 16px text, 15px at 19.2px — and that rounding is large
           enough at these sizes to dominate. Measured against the actual
           rendering, the correction wanted is 3.09px at the 1024 breakpoint and
           3.02px at desktop, so 3px lands within 0.1px of both. An em value
           tuned to either one is visibly off at the other. Re-measure rather
           than rescale if the rotator's font-size changes.

           transform rather than top: the strip is stretched over the whole
           rotator by inset:0, so top is spoken for. The words centre themselves
           inside it and only ever animate translateX and opacity, so transform
           here is free — and it shifts them along with the frame. */
        transform: translateY(3px);
    }

/* The conveyor does not run until the markup adds .is-live, which MainLayout
   does once the host page's "Initializing Main Menu" pill has faded. Paused
   from the very first paint, so the animations sit at time zero rather than
   racing ahead during the wait — the lead-in below is measured from the moment
   the pill goes, not from page load. */
    .text-rotator.is-live h3 {
        opacity: 1;
    }

    .text-rotator.is-live .rotator-word,
    .text-rotator.is-live .rotator-step,
    .text-rotator.is-live .rotator-word-fade {
        animation-play-state: running;
    }

/* Every word is stacked at the stop line and pushed back out to its place in
   the queue by translateX, so the spacing is a product of the timing rather
   than of layout — which is what lets the lead park while the rest keep
   coming. There is no strip to shunt along and nothing to recycle in C#: the
   words share one clock, each offset by its index, so they can never drift
   apart.

   The brightness is a ladder, and it is split across nested elements because
   each rung runs on its own clock. The word at the stop line is at full
   strength, the one behind it at 60%, then 30%, then 10% — and every rung steps
   up together at the instant the leader fades out, so a word inherits the
   leader's brightness while handing its own down the queue.

   Rung k lights up k periods before the word's own death, so the rungs cannot
   be keyframes in one animation without baking the period-to-cycle ratio (and
   therefore the title count) into a percentage. Instead each .rotator-step
   layer runs the SAME two-frame animation with its clock wound one more period
   ahead, and nested opacities multiply: a layer sitting at --step-dim scales
   everything inside it. The multiplication is why the rungs are ratios rather
   than the levels themselves — see .rotator-step. Add or remove a title and
   every handoff stays exact. */
.rotator-word {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    display: flex;
    align-items: center;
    white-space: nowrap;

    /* Word i reaches the stop line at lead-in + i × period. The subtraction
       winds the animation forward to the travel phase, which is why the early
       words get a negative offset and the late ones a positive one. Inherited
       by .rotator-step and .rotator-word-fade, which share the clock. */
    --rotator-start: calc(var(--rotator-lead-in) + var(--i) * var(--rotator-period) - 0.7 * var(--rotator-cycle));

    animation-name: rotatorTravel;
    animation-duration: var(--rotator-cycle);
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    /* backwards so the words whose turn has not come yet sit on the 0% frame —
       offstage — instead of showing their unanimated base styles parked on top
       of the lead. */
    animation-fill-mode: backwards;
    animation-play-state: paused;
    animation-delay: var(--rotator-start);
}

/* One rung of the brightness ladder. The markup nests three of them inside each
   word, handing over --m (how many periods ahead this rung's clock runs) and
   --step-dim (what it scales the letters to while it is unlit). Because the
   layers multiply, --step-dim is the RATIO between neighbouring rungs, not the
   level itself:

       rung   --m   --step-dim   word at that place in the queue
       lead    –        –        1.00   ← parked at the stop line
       1st     1      0.6        0.60
       2nd     2      0.5        0.30
       3rd     3      0.333      0.10   ← and everything further back

   Three rungs is the most the cycle can carry, not a taste decision. A layer's
   animation wraps (lit → unlit) at word-phase 1 − m/count, and that jump is
   only invisible if it lands after the word has extinguished — anywhere in the
   27.9% of the cycle from 72.1% to 100%. That needs m/count ≤ 0.279; with 14
   titles it allows m ≤ 3.9. A fourth rung would flash on the parked leader
   mid-fade. Adding one is safe only at 15 titles or more, and a slower fade
   eats into the same margin: it is the fade's end frame that sets the window. */
.rotator-step {
    animation-name: rotatorStep;
    animation-duration: var(--rotator-cycle);
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    animation-fill-mode: backwards;
    animation-play-state: paused;
    animation-delay: calc(var(--rotator-start) - var(--m) * var(--rotator-period));
}

.rotator-word-fade {
    animation-name: rotatorExtinguish;
    animation-duration: var(--rotator-cycle);
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    animation-fill-mode: backwards;
    animation-play-state: paused;
    animation-delay: var(--rotator-start);
}

/* Constant-speed drift in, park through the stop, then a fast invisible throw
   back to the start. The 70% arrival is the number --rotator-enter is built
   from; move one and you must move the other. The park has to end on the same
   frame the word finishes fading (72.1%) — leave the throw where the fade used
   to end and the word flies backwards while still half legible. */
@keyframes rotatorTravel {
    0% {
        transform: translateX(var(--rotator-enter));
    }

    70% {
        transform: translateX(0);
    }

    72.1% {
        transform: translateX(0);
    }

    75.46% {
        transform: translateX(var(--rotator-enter));
    }

    100% {
        transform: translateX(var(--rotator-enter));
    }
}

/* Both halves of the brightness, on identical percentages: 71.34% is arrival
   (70%) plus a 1.6s hold, and 72.1% is that plus a 0.9s fade, as shares of an
   8.5s × 14 = 119s cycle. They scale with the period, which is what keeps the
   follower's clearance constant when the pace is changed.

   rotatorExtinguish runs on the word's own clock, so it dims the word 1.6s
   after it parks. rotatorStep runs m periods ahead, so the same frames lift a
   rung at the instant the word m places IN FRONT is dimming — every step of the
   ladder is a handoff, not an arrival cue. The fully lit word is therefore
   always the one travelling in, coming up ~315px out and staying lit through
   the whole final approach, with the queue behind it stepped 0.6 / 0.3 / 0.1.

   Each animation's own wrap lands while the word is extinguished, so no jump is
   ever visible — the ceiling that caps the ladder at three rungs. */
@keyframes rotatorStep {
    0%, 71.34% {
        opacity: var(--step-dim);
    }

    72.1%, 100% {
        opacity: 1;
    }
}

@keyframes rotatorExtinguish {
    0%, 71.34% {
        opacity: 1;
    }

    72.1%, 100% {
        opacity: 0;
    }
}

/* ========================================
   FOOTER - COMPONENTS
   Footer text alignment
   ======================================== */

.footer-text {
    flex: 1;
}

.footer-left {
    text-align: left;
}

.footer-center {
    text-align: center;
}

.footer-right {
    text-align: right;
}

/* Footer technology links — site red accent, white on hover. The :not()
   chains mirror app.css so these out-specify the global link colours. */
.footer-right a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a),
.footer-right a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a) {
    color: var(--nss-accent) !important;
    text-decoration: none;
}

    .footer-right a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a):hover,
    .footer-right a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a):hover {
        color: #fff !important;
        text-decoration: underline;
    }

/* ========================================
   HOME PAGE
   Hero section with background image
   ======================================== */

.nss-style {
    height: 800px;
    /* Cap the hero width so it stays near the image's own ~1.33:1 ratio.
       Without this, a fluid-width box on a very wide monitor forces
       background-size: cover to zoom in and crop the top/bottom heavily. */
    max-width: 1060px;
    background-image: url(/images/nssworldmainpage.jpg);
    background-position-x: center;
    background-position-y: center;
    background-repeat: no-repeat;
    background-size: cover;
    padding: 0;
}

/* Left-align the capped hero within the content area, flush with the side
   menu. The hero is a Bootstrap .row whose own margin rules fight auto-margin
   alignment, so align it from the parent with flex instead. width: 100% lets
   the row fill up to its max-width without collapsing as a flex item. */
.home-page-container {
    display: flex;
    justify-content: flex-start;
}

.home-page-container .row.nss-style {
    width: 100%;
}

/* ========================================
   FLIPPER PAGES
   Navigation row shared by paginated pages
   Used by: DataPagination, WeddingsPerformed, YouTubeVideos, SpotifyPlaylists
   ======================================== */

.flipper-navigation {
    margin-bottom: 20px;
}

/* ========================================
   SKELETON SHIMMER
   Custom shimmer placeholder used while discography images and
   Spotify iframes are loading. Fully self-contained — no dependency
   on Radzen's skeleton CSS, which fights layout on this dark theme.
   ======================================== */

@keyframes nss-skeleton-shimmer {
    0%   { background-position: 200% 0; }
    100% { background-position: -200% 0; }
}

.nss-skeleton {
    position: absolute;
    inset: 0;
    z-index: 1;
    background-color: #111;
    background-image: linear-gradient(
        90deg,
        #111  0%,
        #111  30%,
        #4a4a4a 50%,
        #111  70%,
        #111  100%
    );
    background-size: 200% 100%;
    animation: nss-skeleton-shimmer 1.4s ease-in-out infinite;
}

/* ========================================
   DISCOGRAPHY - album-cover grid + detail
   Grid of square covers (click opens a detail
   dialog). Black + red to match site identity.
   ======================================== */

.discography-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
    gap: 1.25rem;
    margin: 1rem 0;
}

.discography-card {
    display: flex;
    flex-direction: column;
    background-color: #111;
    border: 1px solid #2a2a2a;
    border-radius: 10px;
    overflow: hidden;
    transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease;
}

    .discography-card:hover {
        transform: translateY(-4px);
        border-color: var(--nss-accent);
        box-shadow: 0 10px 28px rgba(0, 0, 0, .6), var(--nss-glow);
    }

.discography-card__art {
    display: block;
    width: 100%;
    aspect-ratio: 1 / 1;
    padding: 0;
    border: 0;
    background-color: #000;
    cursor: pointer;
    overflow: hidden;
    position: relative;
}

    .discography-card__art img {
        width: 100%;
        height: 100%;
        object-fit: cover;
        opacity: 0;
        transition: opacity .3s ease, transform .35s ease;
    }

    .discography-card__art img.img-loaded {
        opacity: 1;
    }

.discography-card:hover .discography-card__art img {
    transform: scale(1.05);
}

.discography-card__noart {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    color: #777;
    font-size: 0.85rem;
}

.discography-card__body {
    display: flex;
    flex-direction: column;
    gap: 0.35rem;
    padding: 0.75rem 0.85rem 0.9rem;
    flex: 1;
    text-align: center;
}

.discography-card__title {
    padding: 0;
    border: 0;
    background: none;
    text-align: center;
    color: var(--nss-accent);
    font-weight: 700;
    font-size: 1rem;
    line-height: 1.25;
    cursor: pointer;
}

    .discography-card__title:hover {
        text-decoration: underline;
    }

.discography-card__subtitle {
    color: #cfcfcf;
    font-size: 0.85rem;
    font-style: italic;
}

.discography-card__meta {
    color: #9a9a9a;
    font-size: 0.8rem;
}

/* Streaming brand-icon links (shared by card + detail dialog) */
.discography-links {
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    margin-top: auto;
    padding-top: 0.5rem;
}

/* Centered on the grid card only; the detail dialog keeps them left-aligned. */
.discography-card .discography-links {
    justify-content: center;
}

.discography-link {
    width: 36px;
    height: 36px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid #2a2a2a;
    border-radius: 8px;
    background-color: #0a0a0a;
    transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease;
}

    .discography-link:hover {
        border-color: var(--nss-accent);
        transform: translateY(-2px);
        box-shadow: 0 4px 12px rgba(0, 0, 0, .5), var(--nss-glow);
    }

    .discography-link img {
        width: 20px;
        height: 20px;
        display: block;
    }

/* Detail dialog */
.discography-detail {
    display: grid;
    grid-template-columns: 280px 1fr;
    gap: 1.5rem;
    align-items: start;
}

    .discography-detail__art img {
        width: 100%;
        height: auto;
        border-radius: 8px;
        display: block;
    }

    /* Inline links in the detail body (Record Label, Review, Notes) use the
       site's red accent instead of the default blue. Scoped + !important to
       beat the global "external links are blue" rule in app.css. */
    .discography-detail a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a),
    .discography-detail a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a) {
        color: var(--nss-accent) !important;
        text-decoration: underline;
    }

        .discography-detail a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a):hover,
        .discography-detail a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a):hover {
            color: #fff !important;
        }

.discography-detail__facts {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 0.25rem 0.75rem;
    margin: 0 0 1rem;
}

    .discography-detail__facts dt {
        color: var(--nss-accent);
        font-weight: 700;
    }

    .discography-detail__facts dd {
        margin: 0;
        color: #eaeaea;
    }

.discography-detail__links {
    margin-bottom: 1rem;
}

.discography-detail__links-label {
    display: block;
    color: var(--nss-accent);
    font-weight: 700;
    margin-bottom: 0.35rem;
}

.discography-detail__notes {
    color: #eaeaea;
    line-height: 1.6;
}

.discography-detail__footer {
    display: flex;
    justify-content: flex-end;
    margin-top: 1rem;
}

@media (max-width: 768px) {
    .discography-detail {
        grid-template-columns: 1fr;
    }

    .discography-detail__art {
        max-width: 260px;
        margin: 0 auto;
    }
}

@media (max-width: 480px) {
    .discography-grid {
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: 0.75rem;
    }
}

/* ========================================
   VIDEO & PLAYLIST GRIDS
   Responsive grid of embedded video / playlist
   cards. Shares the discography card look
   (black + red, red hover border + glow + lift).
   Used by: YouTubeVideos, SpotifyPlaylists
   ======================================== */

.video-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
    gap: 1.25rem;
    margin: 1rem 0;
}

.video-grid--playlists {
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
}

.video-card {
    background-color: #111;
    border: 1px solid #2a2a2a;
    border-radius: 10px;
    overflow: hidden;
    transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease;
}

    .video-card:hover {
        transform: translateY(-4px);
        border-color: var(--nss-accent);
        box-shadow: 0 10px 28px rgba(0, 0, 0, .6), var(--nss-glow);
    }

.video-card__frame {
    position: relative;
    aspect-ratio: 16 / 9;
    background-color: #000;
}

    .video-card__frame iframe {
        position: absolute;
        inset: 0;
        width: 100%;
        height: 100%;
        border: 0;
    }

/* ----------------------------------------
   Click-to-load facade: a thumbnail + play button shown until the user clicks,
   at which point the page swaps in the real player (with autoplay). Keeps the
   click-to-play behaviour while avoiding loading a YouTube iframe per card up
   front. Used by: YouTubeVideos, WeddingsPerformed.
   ======================================== */

.video-card__facade {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    padding: 0;
    border: 0;
    cursor: pointer;
    background-color: #000;
    background-position: center;
    background-size: cover;
    background-repeat: no-repeat;
    display: flex;
    align-items: center;
    justify-content: center;
}

    /* Dark veil so the play button keeps contrast over bright thumbnails. */
    .video-card__facade::before {
        content: "";
        position: absolute;
        inset: 0;
        background: linear-gradient(rgba(0, 0, 0, .05), rgba(0, 0, 0, .4));
        transition: background .2s ease;
    }

    .video-card__facade:hover::before,
    .video-card__facade:focus-visible::before {
        background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, .2));
    }

/* Red rounded play badge with a CSS triangle — no image needed. */
.video-card__play {
    position: relative;
    width: 68px;
    height: 48px;
    border-radius: 12px;
    background: var(--nss-accent, #D60202);
    box-shadow: 0 4px 14px rgba(0, 0, 0, .5);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform .2s ease;
}

.video-card__facade:hover .video-card__play,
.video-card__facade:focus-visible .video-card__play {
    transform: scale(1.08);
}

.video-card__play::after {
    content: "";
    width: 0;
    height: 0;
    border-style: solid;
    border-width: 11px 0 11px 19px;
    border-color: transparent transparent transparent #fff;
    margin-left: 4px;
}

/* Title caption under the frame, clamped to two lines. */
.video-card__caption {
    padding: .6rem .8rem .75rem;
    font-size: .95rem;
    line-height: 1.3;
    color: #fff;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

@media (max-width: 480px) {
    .video-card__play {
        width: 58px;
        height: 40px;
    }

        .video-card__play::after {
            border-width: 9px 0 9px 16px;
        }
}

/* Spotify playlist embeds are tall, not 16:9 */
.video-card--playlist .video-card__frame {
    aspect-ratio: auto;
    height: 380px;
}

@media (max-width: 480px) {
    .video-grid,
    .video-grid--playlists {
        grid-template-columns: 1fr;
        gap: 0.75rem;
    }
}

/* ========================================
   DATA GRID PAGES
   Generic styles for pages with data grids and filters
   Used by: CDCollection, ConcertsAttended, ShowsAppearances
   ======================================== */

/* Override the global "external links are blue" rule from app.css for links
   that appear inside data grids (desktop) and mobile collection cards.
   Both containers need the same treatment so the colour is consistent
   across breakpoints. The :not() chain mirrors app.css to guarantee these
   rules beat it on specificity. */
.data-grid-container a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a),
.data-grid-container a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a),
.collection-card a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a),
.collection-card a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a) {
    color: var(--nss-accent) !important;
    text-decoration: none !important;
}

.data-grid-container a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a):hover,
.data-grid-container a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a):hover,
.collection-card a[href^="http"]:not(.rz-panel-menu a):not(.rz-navigation a):hover,
.collection-card a[href^="https"]:not(.rz-panel-menu a):not(.rz-navigation a):hover {
    color: #fff !important;
    text-decoration: underline !important;
}

.data-grid-filter-container {
    margin-bottom: 20px;
}

.data-grid-filter-label {
    margin-right: 10px;
}

.data-grid-filter-dropdown {
    width: 500px;
}

.data-grid-container {
    margin-bottom: 20px;
}

/* Grids never scroll on their own: the PAGE is the only scroller. Radzen makes
   the grid body an overflow:auto box, so a grid wider than the content area got
   its own horizontal scrollbar on top of the page's, two bars for one overflow.
   With the body left visible, the grid grows to full data height and width and
   the page scrolls instead. Opening the body alone isn't enough: the grid's
   outer box is overflow:hidden (it would silently clip the extra columns) and a
   tab panel is overflow:auto (it would just move the second bar out a level),
   so both have to let the table through for the overflow to reach the page.
   This is also what freezes the header row: Radzen's thead is position:sticky,
   top:0, which pins to the nearest scrolling ancestor. With nothing in between
   clipping, that is .rz-body, whose top edge sits just under the site header.
   Give any of these (or a new wrapper around a grid) an overflow again and the
   header pins to that box instead, so it scrolls away with the page. */
.rz-data-grid-data,
.rz-data-grid,
.rz-tabview-panels:has(.rz-data-grid),
.rz-tabview-panels:has(.pinned-list-controls) {
    overflow: visible !important;
}

/* Pinned list controls (Components/PinnedListControls.razor): a paged list's
   filters and top pager pin under the site header while only the data scrolls.
   Like the grid header, this pins to .rz-body, so it needs the same unclipped
   path, which is why the rule above also opens a tab panel holding a block (on
   cards there's no grid to open it). The background must stay opaque, because
   rows scroll underneath. The grid's own header row then pins just below the
   block, at the height js/pinnedControls.js keeps in --pinned-controls-height.
   Phones don't pin: the block plus a two-row pager would take a third of the
   screen. There, paging scrolls back up to the filters instead (pagerScroll.js). */
@media (min-width: 769px) {
    .pinned-list-controls {
        position: sticky;
        top: 0;
        z-index: 3;
        background-color: #131313;
        padding-bottom: 0.75rem;
    }

    .pinned-list-controls .data-grid-filter-container {
        margin-bottom: 0.75rem;
    }

    .rz-tabview-panel:has(> .pinned-list-controls) .rz-grid-table thead,
    .rz-tabview-panel:has(> .pinned-list-controls) .rz-grid-table thead th {
        top: var(--pinned-controls-height, 0px);
    }
}

.data-grid-loading-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 9999;
}

.data-grid-loading-content {
    padding: 30px;
    border-radius: 8px;
    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    text-align: center;
    min-width: 300px;
}

.data-grid-loading-progress {
    margin-bottom: 15px;
}

.data-grid-loading-text {
    margin-top: 10px;
}

.data-grid-no-data-text {
    font-size: 12px;
    opacity: 0.5;
}

/* DialogOptions.ContentCssClass for a dialog whose content should fill it: Radzen's
   content box doesn't grow (flex 0 1 auto), so without this, resizing the dialog or
   putting it full screen only adds blank space. The component inside still has to
   flex (root: flex 1 1 auto + min-height 0; the part that should grow: flex 1). */
.rz-dialog-content.dialog-fill {
    display: flex;
    flex-direction: column;
    flex: 1 1 auto;
    min-height: 0;
}

.data-grid-modal-body {
    flex: 1 1 auto;
    overflow-y: auto;
    padding: 10px;
}

.data-grid-modal-text {
    white-space: pre-wrap;
    font-family: monospace;
}

.data-grid-modal-footer {
    flex: 0 0 auto;
    padding: 10px;
    text-align: right;
    margin-top: auto;
}

.data-grid-cell-center {
    text-align: center;
    vertical-align: top;
}

.data-grid-cell-justified {
    text-align: justify;
    vertical-align: top;
    word-wrap: break-word;
    white-space: normal;
    overflow-wrap: break-word;
}

.data-grid-cancelled-indicator {
    margin-bottom: 5px;
}

    .data-grid-cancelled-indicator img {
        display: block;
        margin: 0 auto;
    }

.data-grid-cancelled-notes {
    font-style: italic;
}

/* ========================================
   COMMON UTILITIES
   Reusable classes for any component
   ======================================== */

.container-responsive {
    width: 100%;
    padding: 1rem;
}

.text-responsive-large {
    font-size: 1.5rem;
}

.text-responsive-medium {
    font-size: 1.2rem;
}

.text-responsive-small {
    font-size: 1rem;
}

.gap-responsive {
    gap: 1rem;
}

.hide-mobile {
    display: block;
}

.show-mobile {
    display: none;
}

/* ========================================
   RESPONSIVE - TABLET (1024px and below)
   ======================================== */

@media (max-width: 1024px) {
    .header-label {
        font-size: 1rem;
    }

    .nss-wordmark {
        height: 34px;
    }

    .text-rotator h3 {
        font-size: 1rem;
    }

    /* The words are 5/6 as wide at 1rem, so the pitch floor drops with them.
       Holding the desktop pitch here would leave barely one word in a ~630px
       rotator. 365px clears the widest title at this size by ~35px. */
    .text-rotator {
        --rotator-pitch: 365px;
    }

    .footer-stack {
        flex-direction: column;
        align-items: flex-start !important;
        gap: 0.5rem !important;
    }

    .footer-text {
        text-align: left !important;
    }

    .data-grid-filter-dropdown {
        width: 400px;
    }
}

/* ========================================
   RESPONSIVE - MOBILE (768px and below)
   ======================================== */

@media (max-width: 768px) {
    /* Body */
    .rz-body {
        padding: 0 0.5rem !important;
    }

    /* Header */
    .header-label {
        font-size: 0.9rem;
    }

    .nss-wordmark {
        height: 30px;
    }

    .text-rotator {
        display: none;
    }

    /* CSS-only sidebar drawer (mobile).
       The sidebar element is always in the DOM. On mobile we
       fix-position it off-canvas. The --open modifier slides it in.
       The backdrop is always in the DOM too; CSS hides it on desktop
       and reveals it on mobile only when the sidebar has --open.
       No JS isMobile check controls drawer visibility. */
    .nss-sidebar.rz-sidebar {
        position: fixed !important;
        top: 0;
        left: 0;
        z-index: 1001;
        height: 100vh;
        transform: translateX(-100%);
        transition: transform 0.25s ease;
        box-shadow: 2px 0 8px rgba(0, 0, 0, 0.4);
    }

    .nss-sidebar.nss-sidebar--open.rz-sidebar {
        transform: translateX(0);
    }

    .nss-sidebar-backdrop.is-open {
        display: block;
    }

    .sidebar-toggle {
        position: relative;
        z-index: 1002;
    }

    .mobile-menu-header {
        display: flex;
        justify-content: flex-end;
        padding: 0.5rem;
        background-color: #000;
        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
    }

    .mobile-close-button {
        color: #fff !important;
    }

        .mobile-close-button:hover {
            color: var(--nss-accent) !important;
            background-color: rgba(255, 255, 255, 0.1) !important;
        }

    /* Footer */
    .footer-text {
        font-size: 0.75rem;
    }

    /* Home page */
    .nss-style {
        height: 500px;
        background-size: cover;
        background-position: center top;
    }

    .data-grid-loading-content {
        padding: 20px;
        min-width: 250px;
    }

    /* Utilities */
    .container-responsive {
        padding: 0.5rem;
    }

    .text-responsive-large {
        font-size: 1.2rem;
    }

    .text-responsive-medium {
        font-size: 1rem;
    }

    .text-responsive-small {
        font-size: 0.9rem;
    }

    .gap-responsive {
        gap: 0.5rem;
    }

    .hide-mobile {
        display: none;
    }

    .show-mobile {
        display: block;
    }

    /* Pagination buttons collapse to icon-only on phones.
       Targets every .flipper-navigation row (DataPagination,
       Discography, YouTube, Spotify, Weddings). Button text is
       hidden from view; explicit aria-label on each button in
       DataPagination keeps the accessible name. */
    .flipper-navigation .rz-button .rz-button-text {
        display: none;
    }

    .flipper-navigation .rz-button {
        min-width: 44px;
        min-height: 44px;
        padding-inline: 0.5rem;
    }
}

/* ========================================
   RESPONSIVE - SMALL MOBILE (480px and below)
   ======================================== */

@media (max-width: 480px) {
    /* Header */
    .header-label {
        font-size: 0.8rem;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
        max-width: 200px;
    }

    .nss-wordmark {
        height: 27px;
    }

    /* Footer */
    .footer-center {
        display: none;
    }

    .footer-text {
        font-size: 0.7rem;
    }

    /* Home page */
    .nss-style {
        height: 400px;
    }

    .flipper-navigation {
        margin-bottom: 10px;
    }

    .data-grid-loading-content {
        padding: 15px;
        min-width: 200px;
    }

    .data-grid-no-data-text {
        font-size: 11px;
    }

    .data-grid-modal-body {
        padding: 5px;
    }

    .data-grid-modal-footer {
        padding: 5px;
    }

    .data-grid-modal-text {
        font-size: 0.85rem;
    }

    /* Utilities */
    .text-responsive-large {
        font-size: 1rem;
    }

    .text-responsive-medium {
        font-size: 0.9rem;
    }

    .text-responsive-small {
        font-size: 0.8rem;
    }
}

/* ========================================
   SIDEBAR BACKDROP (CSS-only drawer)
   Always rendered. Default hidden. Mobile media query above
   reveals it when paired with .is-open.
   ======================================== */

.nss-sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background-color: rgba(0, 0, 0, 0.55);
    z-index: 1000;
    cursor: pointer;
}

/* Desktop: never show the backdrop, regardless of state. */
@media (min-width: 769px) {
    .nss-sidebar-backdrop,
    .nss-sidebar-backdrop.is-open {
        display: none !important;
    }
}

/* ========================================
   MOBILE DATA CARD VIEW
   Used by CDCollection / ConcertsAttended / ShowsGrid when
   IsMobile is true (Blazor cascading parameter). Desktop
   continues to render the original RadzenDataGrid. Only one
   tree is in the DOM at any time.
   ======================================== */

.card-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
    gap: 0.75rem;
    margin: 0.5rem 0 0.75rem;
}

.collection-card {
    background-color: #111;
    border: 1px solid #2a2a2a;
    border-radius: 10px;
    padding: 0.75rem 0.9rem;
}

.collection-card-title-row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.5rem;
    margin-bottom: 0.15rem;
}

.collection-card-title {
    font-size: 1rem;
    font-weight: 700;
    line-height: 1.25;
    overflow-wrap: anywhere;
}

.collection-card-year {
    font-size: 0.85rem;
    opacity: 0.75;
    flex-shrink: 0;
}

.collection-card-subtitle {
    font-size: 0.9rem;
    line-height: 1.3;
    margin-bottom: 0.35rem;
    overflow-wrap: anywhere;
}

.collection-card-meta {
    font-size: 0.85rem;
    line-height: 1.4;
    opacity: 0.85;
    overflow-wrap: anywhere;
}

    .collection-card-meta + .collection-card-meta {
        margin-top: 0.2rem;
    }

.collection-card-badges {
    display: flex;
    flex-wrap: wrap;
    gap: 0.35rem;
    margin: 0.35rem 0;
}

.collection-card-badge {
    background-color: #2a2a2a;
    border: 1px solid #444;
    border-radius: 4px;
    padding: 0.15rem 0.5rem;
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    line-height: 1.4;
}

    .collection-card-badge.badge-autographed {
        background-color: #4a1a00;
        border-color: #8a3700;
        color: var(--nss-red-warm);
    }

    .collection-card-badge.badge-cancelled {
        background-color: #4a0000;
        border-color: #8a0000;
        color: #ff8a8a;
    }

.collection-card details {
    margin-top: 0.4rem;
}

    .collection-card details > summary {
        cursor: pointer;
        padding: 0.35rem 0;
        min-height: 32px;
        font-size: 0.9rem;
        color: var(--nss-alert);
        list-style: none;
    }

        .collection-card details > summary::-webkit-details-marker {
            display: none;
        }

        .collection-card details > summary::after {
            content: "\25BC";
            margin-left: 0.4rem;
            font-size: 0.7rem;
            transition: transform 0.15s ease;
            display: inline-block;
        }

    .collection-card details[open] > summary::after {
        transform: rotate(180deg);
    }

.collection-card-disclosure {
    padding: 0.35rem 0;
    font-size: 0.9rem;
    line-height: 1.5;
    white-space: pre-wrap;
    word-break: break-word;
    border-top: 1px solid #222;
}

/* Honour user OS preference for reduced motion. */
@media (prefers-reduced-motion: reduce) {
    .text-rotator h3,
    .rotator-word,
    .rotator-step,
    .rotator-word-fade,
    .nss-sidebar.rz-sidebar,
    .collection-card details > summary::after {
        animation: none !important;
        transition: none !important;
    }

    /* Killing the animation strands every word offstage on its base styles, so
       the rotator would just be empty. Park the first title at the stop line
       and hide the rest — one static credential instead of a conveyor. */
    .rotator-word {
        opacity: 0;
        transform: none;
    }

        .rotator-word:first-child {
            opacity: 1;
        }
}

/* ── Shared tabbed-page frame ───────────────────────────────────────────────
   Slick framing + flat accent-underlined Radzen tabs, shared by any page that
   presents its content in RadzenTabs (CD Collection, Shows & Appearances, …).
   Wrap the RadzenTabs in <div class="nss-tab-frame"> and give the RadzenTabs
   itself class="nss-tabs". */
.nss-tab-frame {
    border: 1px solid #2a2a2a;
    border-radius: 12px;
    background: linear-gradient(180deg, #161616 0%, #0d0d0d 100%);
    padding: 0.35rem 0.85rem 0.85rem;
    box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
}

/* Tab strip: flat, transparent, with an accent underline on the active /
   hovered tab instead of the default Radzen pill chrome. */
.nss-tabs .rz-tabview-nav {
    border: none !important;
    border-bottom: 1px solid #2a2a2a !important;
    background: transparent !important;
    gap: 0.35rem;
}

    .nss-tabs .rz-tabview-nav > li > button,
    .nss-tabs .rz-tabview-nav > li > a {
        background: transparent !important;
        border: none !important;
        border-bottom: 2px solid transparent !important;
        border-radius: 8px 8px 0 0 !important;
        padding: 0.6rem 1.1rem !important;
        font-weight: 600;
        letter-spacing: 0.06em;
        text-transform: uppercase;
        opacity: 0.6;
        transition: opacity 0.15s ease, color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
    }

        .nss-tabs .rz-tabview-nav > li > button:hover,
        .nss-tabs .rz-tabview-nav > li > a:hover {
            opacity: 1;
            color: var(--nss-accent) !important;
            background: rgba(255, 31, 31, 0.06) !important;
        }

    .nss-tabs .rz-tabview-nav > li.rz-tabview-selected > button,
    .nss-tabs .rz-tabview-nav > li.rz-tabview-selected > a {
        opacity: 1;
        color: var(--nss-accent) !important;
        border-bottom-color: var(--nss-accent) !important;
    }

        /* Accent the active tab's icon as well as its label. */
        .nss-tabs .rz-tabview-nav > li.rz-tabview-selected > button .rz-tabview-icon,
        .nss-tabs .rz-tabview-nav > li.rz-tabview-selected > a .rz-tabview-icon {
            color: var(--nss-accent) !important;
        }

.nss-tabs .rz-tabview-panels {
    background: transparent !important;
    border: none !important;
    padding: 0.85rem 0 0 !important;
}

@media (max-width: 768px) {
    .nss-tab-frame {
        padding: 0.25rem 0.5rem 0.6rem;
    }

    .nss-tabs .rz-tabview-nav > li > button,
    .nss-tabs .rz-tabview-nav > li > a {
        padding: 0.5rem 0.7rem !important;
        letter-spacing: 0.03em;
    }
}

/* ── Shared statistics stat-boxes ───────────────────────────────────────────
   The dark cards with an uppercase title and a big red value, shared by the
   statistics headers on the CD Collection and Shows & Appearances pages. */
.nss-stat-boxes {
    display: flex;
    gap: 0.75rem;
    flex-wrap: wrap;
}

/* Stat boxes modelled on the Contact page cards: dark square-ish cards with
   a red value, title on top and the number justified to the bottom. */
.nss-stat-box {
    flex: 1 1 0;
    min-width: 120px;
    min-height: 84px;
    display: flex;
    flex-direction: column;
    background-color: #111;
    border: 1px solid #2a2a2a;
    border-radius: 10px;
    padding: 0.55rem 0.75rem;
    text-align: center;
}

.nss-stat-title {
    font-size: 0.72rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    opacity: 0.55;
}

.nss-stat-value {
    margin-top: auto;
    font-family: "Special Elite", sans-serif;
    font-size: 1.7rem;
    font-weight: 700;
    line-height: 1.1;
    color: var(--nss-accent);
}
