body.music-view {
    overflow-y: auto;
    min-height: 100vh;
}

/* Matches the other wide listing pages (blog index, search, threads,
   collected) — same container width so this reads as one of that family,
   not a narrower one-off. Tried vertically centering this (like index.php/
   about.php) but on a tall viewport it pushed the actual track list well
   below the fold — same problem search.php ran into. --page-top-anchor instead — same
   idea as about.php/blog.css: scales with viewport height like centering
   does, without depending on content height (so it can't jump). */
.mu-page {
    width: 100%;
    max-width: 1200px;
    margin: 0 auto;
    padding: var(--page-top-anchor) 60px 100px;
    box-sizing: border-box;
}
@media (max-width: 780px) {
    /* 20px — matches about.css's mobile gutter (and the fixed hamburger
       button's own left:20px in nav.php), not the 24px this used to sit at.
       Top reverts to the plain token too — the +30px bump above is a
       desktop-only nudge, mobile was never part of that discussion. */
    .mu-page { padding-left: 20px; padding-right: 20px; padding-top: var(--page-top-offset-single); }
    /* Back to a plain in-flow element — fixed positioning kept it
       pinned in place while scrolling, which read as it "following" the
       page instead of just being the first thing on it. */
}

.mu-kicker {
    display: block;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    text-transform: uppercase;
    letter-spacing: 0.15em;
    color: var(--color-text-muted);
    margin-bottom: 24px;
}

.mu-page-title {
    position: relative;
    display: block;
    font-family: var(--font-mono);
    font-size: clamp(1.8rem, 3.5vw, 2.8rem);
    font-weight: normal;
    line-height: 1;
    margin: 0 0 20px;
    color: var(--color-text-primary);
    text-transform: uppercase;
    letter-spacing: -0.03em;
    padding-left: 18px;
}
.mu-page-title::before {
    content: '';
    position: absolute;
    left: 0; top: 0;
    width: 2px;
    height: 1.2em;
    background: rgba(255,255,255,0.4);
}

/* Short framing copy for the collection as a whole — same body typography
   as a track's own story text, not a separate voice. */
.mu-page-intro {
    max-width: 560px;
    font-family: var(--font-mono);
    font-size: 0.7rem;
    line-height: 1.7;
    letter-spacing: 0.01em;
    color: var(--color-text-muted);
    padding-left: 18px;
    padding-bottom: 24px;
    margin: 0 0 40px;
    border-bottom: 1px solid rgba(255,255,255,0.12);
}

.mu-empty {
    font-family: var(--font-mono);
    color: var(--color-text-muted);
    font-size: 0.85rem;
}

/* ── Index — light, player-free list. Nothing audio-related loads just
   from browsing; a track only becomes a full listening experience once
   you've clicked into its own page. ────────────────────────────────── */
.mu-track-list {
    list-style: none;
    padding: 0 0 30px;
    margin: 0;
    counter-reset: track-index;
    border-bottom: 1px solid rgba(255,255,255,0.12);
}

/* Same entrance as blog.php's .post-list-index li — plain fadeIn (no
   slide), staggered 0.05s per row via an inline animation-delay (see
   music.php), not a CSS nth-child cap. */
.mu-track-row {
    position: relative;
    margin: 3px 0;
    background: rgba(255,255,255,0.015);
    transition: background 0.2s, box-shadow 0.2s, transform 0.2s;
    counter-increment: track-index;
    overflow: hidden;
    opacity: 0;
    animation: fadeIn 0.5s ease forwards;
}
.mu-track-row:hover {
    background: rgba(255,255,255,0.05);
    box-shadow: 0 0 24px rgba(255,255,255,0.06);
    transform: translateX(2px);
}

/* Large faint catalog number watermarked into each row — a quiet
   editorial/technical flourish, still fully monochrome (low-opacity
   white, no color), sitting behind the real content in paint order. */
.mu-track-row::before {
    content: counter(track-index, decimal-leading-zero);
    position: absolute;
    right: 16px;
    bottom: 8px;
    font-family: var(--font-mono);
    font-size: 2.6rem;
    line-height: 1;
    color: rgba(255,255,255,0.025);
    pointer-events: none;
}

.mu-track-row-link {
    position: relative;
    /* Same row shape as search.php's .search-recent-item (growing title
       column + a right-hand stacked "type badge above date" column) —
       just without a thumbnail, since tracks don't have one here. */
    display: flex;
    align-items: center;
    gap: 16px;
    /* Left was 16px, on top of .mu-page's own side padding — every row's
       date/title sat a further 16px right of the "Music" kicker/intro
       text above the list, reading as the whole list being shifted right.
       Kept just enough (8px) for the title's own tick below to poke into,
       not a full click-padding reservation. */
    padding: 20px 18px 20px 8px;
    text-decoration: none;
}

.mu-track-info {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 6px;
}

/* Right-hand column — mirrors .search-recent-meta exactly. */
.mu-track-meta {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 2px;
    flex-shrink: 0;
}
.mu-track-badge {
    font-family: var(--font-mono);
    font-size: 0.5rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-text-muted);
}
.mu-track-date {
    font-family: var(--font-mono);
    font-size: 0.6rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    white-space: nowrap;
}

/* Duration/plays/format collapsed into one muted line under the title —
   mirrors .search-recent-sub — instead of a separate row competing with
   the date for the same "top of the card" position. */
.mu-track-sub {
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}

.mu-track-row-title {
    position: relative;
    display: block;
    font-family: var(--font-mono);
    font-size: clamp(1rem, 2vw, 1.15rem);
    color: var(--color-text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.03em;
    line-height: 1.25;
    padding-right: 28px;
    transition: color 0.15s;
    /* Constrained to one line (like .search-recent-title) instead of
       wrapping — a long title used to stretch across the whole row and
       push everything else down, dwarfing the badge/date column next to it. */
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    box-sizing: border-box;
}
/* Tick pokes into .mu-track-row-link's own 16px left padding rather than
   reserving its own — title used to sit 16px further right than
   .mu-track-date directly above it (32px vs 16px total inset) since its
   own padding-left stacked on top of the link's. */
.mu-track-row-title::before {
    content: '';
    position: absolute;
    left: -6px; top: 50%;
    transform: translateY(-50%);
    width: 2px;
    height: 0.9em;
    background: var(--type-color, rgba(255,255,255,0.4));
}
.mu-track-row-title::after {
    content: '→';
    position: absolute; right: 0; top: 50%;
    transform: translateY(-50%) translateX(6px);
    font-family: var(--font-mono); font-size: 1rem;
    color: var(--color-text-muted);
    opacity: 0;
    transition: opacity 0.18s, transform 0.25s cubic-bezier(0.2,0.8,0.2,1);
}
.mu-track-row:hover .mu-track-row-title { color: var(--color-text-primary); }
.mu-track-row:hover .mu-track-row-title::after { opacity: 1; transform: translateY(-50%) translateX(0); }

/* ── Single track page ───────────────────────────────────────────────── */
/* As wide as blog's single-post layout (sidebar + content combined,
   1400px) — the title/player go full width for a cinematic feel, while
   the story text keeps its own readable measure so it doesn't stretch
   into unreadably long lines just because the page grew. */
.mu-page--single { max-width: 1400px; }

/* Standard single-item entrance sitewide — a plain fade, not a staggered/
   sliding one like a list's items get. Was a per-child staggered
   fadeInUp; .mu-player-block's own glow-while-playing effect lives on a
   separate ::after layer specifically so toggling playback never touches
   this element's own `animation` property (see that rule's comment) —
   simplifying this to a plain fade doesn't change that separation. */
.mu-page--single > * {
    opacity: 0;
    animation: fadeIn 0.5s ease forwards;
}


/* Track identity — kicker/title/date/tags — lives inside the card now,
   top-left, quiet: a label/value pair same restrained mono readout
   language as the player footer below it, not a big decorative page
   heading. Every piece of text in the player reads at the same size
   now (0.65rem, matching .mu-player-footer-meta) — one uniform quiet
   voice throughout; color is what distinguishes label from content. */
.mu-player-header {
    margin-bottom: 14px;
}
/* Same colored tick collected.php's own .ci-title uses (a --type-color
   custom property + a small bar before the text) — the site's existing
   convention for "this belongs to type X", just applied here for
   consistency rather than reinvented. */
.mu-player-kicker {
    position: relative;
    font-family: var(--font-mono);
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-text-muted);
}
/* Tick pokes into the card's own left padding rather than reserving its
   own — the old padding-left:10px on this element pushed "Track:" 10px
   further right than mu-time-elapsed ("0:00") directly beneath it, since
   that element has no matching offset. */
.mu-player-kicker::before {
    content: '';
    position: absolute;
    left: -8px;
    top: 0.05em;
    width: 3px;
    height: 0.9em;
    background: var(--type-color, rgba(255,255,255,0.4));
    pointer-events: none;
}
.mu-player-title {
    font-family: var(--font-mono);
    font-size: 0.65rem;
    font-weight: normal;
    line-height: 1.3;
    color: var(--color-text-secondary);
    text-transform: uppercase;
    letter-spacing: 0.02em;
    margin: 0;
    overflow-wrap: break-word;
}
/* Same shared markup renders both music.php's own single-track page (where
   this links back to itself — harmless, just a no-op click) and the
   [recording player="1"] embed (where it's the only way back to the
   track's real page — the embed otherwise has no way out at all). Plain
   inherited color/no underline so it doesn't read as a link until hovered,
   matching every other quiet-text-that-happens-to-be-a-link on this site. */
.mu-player-title a {
    color: inherit;
    text-decoration: none;
    transition: color 0.15s;
}
.mu-player-title a:hover { color: var(--color-text-primary); }
.mu-player-header-meta {
    display: flex;
    align-items: baseline; /* mixed text sizes (title/date/like count) line
       up on their actual baseline instead of by box height */
    flex-wrap: wrap;
    gap: 14px;
}
.mu-player-header-meta .mu-track-date { font-size: 0.65rem; }

/* Divider spans the full card width (matching the player/title above it —
   .mu-page--single's own cinematic-width choice), while the text inside
   stays at its own readable 60ch measure via .mu-single-story below —
   the two elements are allowed to have different widths on purpose,
   rather than forcing the divider to match the narrower text. */
.mu-single-story-block {
    margin: 40px 0 48px;
    padding-top: 40px;
    border-top: 1px solid rgba(255,255,255,0.12);
}

/* Same small uppercase label Collected uses above its description/note
   blocks (.ci-related-label) — wasn't previously defined outside
   collected.php's own inline <style>, so it silently did nothing here
   until added. */
.ci-related-label {
    font-family: var(--font-mono);
    font-size: 0.55rem;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    color: var(--color-text-muted);
    margin-bottom: 16px;
    display: block;
}

/* Matches Collected's own single-item description block (.ci-note)
   exactly — same font-size/line-height/border treatment — rather than
   the general .blog-body article typography. Block-level Markdown
   (headings, lists) still renders correctly since .blog-body's rules for
   those remain in effect; only the base text size/leading/border here is
   overridden to match Collected specifically, per the ask. */
/* This page treats every piece of text as a plain readout, description
   included, not styled prose. No border-left grouping device either;
   monochrome and flat, nothing but spacing separating one block from
   the next. Overrides .blog-body h2's own border-left (base.css) too,
   in case the Markdown itself uses headings — same "no left border on
   text" rule applies. */
.mu-single-story {
    max-width: 60ch;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    line-height: 1.7;
    color: var(--color-text-secondary);
}
.mu-single-story.blog-body h2,
.mu-single-story.blog-body h3 {
    border-left: none;
    padding-left: 0;
}

/* ── Player ────────────────────────────────────────────────────────────
   One bordered card holding two sections — the waveform, and a quiet
   footer (Play/Mute/Volume/Loop, technical readout) — set apart by a
   single inset hairline rather than floating loose on the page
   background. Sharp corners (no radius), matching the rest of the
   site's mostly-sharp language. */
.mu-player-block {
    position: relative;
    margin-bottom: 40px;
    border: 1px solid rgba(255,255,255,0.14);
    background: #0a0a0a;
    overflow: hidden;
}

/* Ambient backdrop — a slow-turning color wash generated from the
   artwork's own dominant colors (extracted client-side in
   music-player.js, handed over as --cover-c1/2/3), not the raw image
   blurred behind a scrim. Sits below everything (z-index 0); a plain
   dark card until/unless a palette actually lands, so there's no flash
   of a broken gradient while the cover is still decoding — and no
   change at all for tracks with no cover art. */
.mu-player-block::before {
    content: '';
    position: absolute;
    inset: -40%;
    background: conic-gradient(from 0deg,
        var(--cover-c1, transparent),
        var(--cover-c2, transparent),
        var(--cover-c3, transparent),
        var(--cover-c1, transparent));
    filter: blur(60px) saturate(1.8);
    opacity: 0;
    transition: opacity 1s ease;
    animation: mu-aurora-spin 24s linear infinite;
    pointer-events: none;
    z-index: 0;
}
.mu-player-block.mu-has-palette::before { opacity: 0.75; }
@keyframes mu-aurora-spin {
    to { transform: rotate(360deg); }
}

/* Dark scrim above the color wash so the monochrome content on top still
   reads as the only real content — plus the same breathing glow around
   the whole card while a track is actually playing as before, the one
   place on this page that's alive rather than static. :has() keys off
   .mu-player.mu-playing, a descendant, since the class itself lives on
   the audio wrapper.

   Lives on its own ::after layer rather than .mu-player-block's own
   `animation` property — .mu-player-block already carries the page's
   fadeInUp entrance animation (base.css, via .mu-page--single > *), and
   swapping that out for the pulse (then back) every time playback
   starts/stops made the browser re-run fadeInUp from scratch on every
   pause, which looked like the whole card reloading. A separate pseudo-
   element has its own independent `animation` property, so starting/
   stopping the pulse here never touches the card's own. */
@keyframes mu-card-pulse {
    0%, 100% { box-shadow: 0 0 0 rgba(255,255,255,0); }
    50% { box-shadow: 0 0 22px rgba(255,255,255,0.16); }
}
.mu-player-block::after {
    content: '';
    position: absolute;
    inset: 0;
    background: rgba(0,0,0,0.62);
    pointer-events: none;
    z-index: 1;
}
.mu-player-block:has(.mu-player.mu-playing)::after {
    animation: mu-card-pulse 2.6s ease-in-out infinite;
}

/* Real content sits above both backdrop layers. */
.mu-player-layout {
    position: relative;
    z-index: 2;
    display: flex;
}

.mu-track audio, .mu-player audio { display: none; }

.mu-player--single {
    padding: 18px 24px;
}

/* Cover art fills the whole left column edge-to-edge — spanning the
   card's full height beside .mu-player-content (header + waveform +
   footer stacked together), not just the top section, so the waveform
   above and the playback controls below line up under the same left
   edge instead of the footer alone reaching further left than the
   waveform does. */
.mu-player-cover {
    position: relative;
    flex-shrink: 0;
    width: 210px;
    border-right: 1px solid rgba(255,255,255,0.14);
    overflow: hidden;
}
/* position:absolute + inset:0, not width/height:100% — a percentage-
   height replaced element inside a height:auto flex item under
   align-items:stretch (the default, and what .mu-player-cover relies on
   to match .mu-player-content's height) is a known flexbox pitfall: it
   feeds back into the flex sizing algorithm's hypothetical-size pass and
   can inflate the *container's* own computed height well past what its
   actual content needs — confirmed by forcing an explicit px height on
   the cover (resolves cleanly) vs. leaving the image's height:100% in
   place (didn't). Taking the image out of flow entirely sidesteps this:
   .mu-player-cover is already position:relative, so inset:0 fills it
   edge-to-edge exactly the same as before, but an absolutely positioned
   element can never influence its container's own intrinsic size. */
/* margin/filter/cursor reset — .blog-body img/.project-body img (base.css)
   sets margin:60px 0 plus a grayscale filter and zoom-in cursor for real
   prose images, and matches this img too whenever the player is embedded
   via shortcode inside a post/project body. The margin is the one that
   actually breaks layout: on a position:absolute element it still applies,
   pushing the image 60px down inside this overflow:hidden container — the
   top 60px of the frame renders empty and the art appears to "start
   halfway down". Cover art is UI chrome, not prose content, so all three
   are neutralized here unconditionally rather than only in the embed's own
   .sc-recording-player overrides — the main music.php page just happens
   not to trigger the base.css rule at all, so this is a no-op there. */
.mu-player-cover img {
    display: block;
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    margin: 0;
    object-fit: cover;
    filter: none;
    cursor: default;
}
/* Centered vertically against the cover art's own full-bleed height —
   the header/waveform/footer group is shorter than a wide card's tall
   cover column, and left at the default flex-start it sat pinned to the
   top with all the slack collecting as dead space below the footer. */
.mu-player-content {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 18px;
}
/* No mobile-specific cover treatment beyond width — it used to switch to
   a small centered square with its own aspect-ratio at this width, a
   deliberately *different* look from desktop's edge-to-edge fill. Cover
   art on mobile still uses the exact same rule as everywhere else (the
   base .mu-player-cover / .mu-player-cover img above): same crop style,
   same behavior — just a narrower fixed column (140px vs desktop's
   210px) since on a ~350px-wide card the full 210px left almost 2/3 of
   the card to the art and squeezed the title/waveform/footer into the
   remaining third. Still edge-to-edge fill, still object-fit:cover, only
   the column's own width changed. */
@media (max-width: 620px) {
    .mu-player-cover { width: 140px; }
}

/* Big, obvious circular Play overlay on the art itself — the footer's
   .mu-play-btn is a small plain icon, easy to miss (and a small target
   to hit precisely) on mobile before you've actually started listening.
   Purely a convenience affordance: JS forwards its click straight to the
   real Play button rather than duplicating play/pause state handling, so
   there's exactly one source of truth for what "playing" means. */
.mu-cover-play-btn {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 56px;
    height: 56px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: rgba(0,0,0,0.55);
    border: 1px solid rgba(255,255,255,0.35);
    color: #fff;
    cursor: pointer;
    opacity: 1;
    visibility: visible;
    transition: opacity 0.2s ease, background 0.15s, transform 0.15s;
}
.mu-cover-play-btn:hover { background: rgba(0,0,0,0.75); }
.mu-cover-play-btn:active { transform: translate(-50%, -50%) scale(0.92); }
.mu-cover-play-btn .mu-icon {
    width: 24px;
    height: 24px;
    transform: translateX(2px);
}
/* Hidden once a track is actually playing — from then on the footer's
   own Play/Pause is the control. visibility flips only after the
   opacity fade finishes going out (so it doesn't just vanish), but
   immediately when coming back so the fade-in isn't delayed. */
.mu-player-block:has(.mu-player.mu-playing) .mu-cover-play-btn {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
    transition: opacity 0.2s ease, visibility 0s linear 0.2s;
}
@media (max-width: 620px) {
    /* Bigger, not smaller, on mobile — this is specifically the "easier
       to hit on a phone" control, so it doesn't get the usual mobile
       size-down every other icon in this file gets. */
    .mu-cover-play-btn { width: 60px; height: 60px; }
    .mu-cover-play-btn .mu-icon { width: 26px; height: 26px; }
}

/* Play — a plain icon control now, same treatment as Mute/Loop, since
   the cover art moved to the card's own background instead of a
   thumbnail for the button to sit on. */
.mu-play-btn {
    position: relative;
    display: flex;
    align-items: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: color 0.15s, transform 0.1s;
}
.mu-play-btn::before { content: ''; position: absolute; inset: -11px; }
.mu-play-btn:hover { color: var(--color-text-primary); }
.mu-play-btn:active { transform: scale(0.88); }
.mu-icon { width: 22px; height: 22px; fill: currentColor; display: block; }
/* transform, not margin — margin changes the icon's layout box width,
   so swapping play/pause (one nudged, one not) shifted everything
   after it in the footer row by that amount every time playback
   toggled. transform is paint-only and doesn't affect layout at all. */
.mu-play-btn .mu-icon-play { transform: translateX(1px); }
.mu-play-btn .mu-icon-pause { display: none; }
/* .mu-play-btn sits in the footer now, a sibling of .mu-player rather
   than a descendant — ".mu-player.mu-playing .mu-play-btn" can never
   match that, so the icon silently never swapped and the button just
   sat on "Play" forever even while actually playing. :has() reaches
   across to the sibling via their shared .mu-player-block ancestor,
   same pattern as the card-wide glow below. */
.mu-player-block:has(.mu-player.mu-playing) .mu-play-btn { color: #fff; }
.mu-player-block:has(.mu-player.mu-playing) .mu-play-btn .mu-icon-play { display: none; }
.mu-player-block:has(.mu-player.mu-playing) .mu-play-btn .mu-icon-pause { display: block; }

/* Scrubber row: elapsed time flanks the left of the waveform, total on
   the right — the conventional layout, rather than bunching both times
   in one corner away from the bar they describe. */
.mu-player-scrubber {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
}

.mu-time {
    flex-shrink: 0;
    min-width: 30px;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    color: var(--color-text-muted);
    font-variant-numeric: tabular-nums;
}
.mu-time-elapsed { text-align: right; }

/* One quiet footer strip closes out the card — playback settings on the
   left, the technical readout as plain inline text on the right. The
   divider above it is inset from the card's own edges and fades out at
   both ends (a gradient, not a flat rule) rather than a hard line
   bleeding all the way to the border — floats inside the card instead
   of turning it into a rigid grid. */
.mu-player-footer {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 16px;
    padding: 14px 24px;
}
.mu-player-footer::before {
    content: '';
    position: absolute;
    top: 0;
    left: 24px;
    right: 24px;
    height: 1px;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2) 50%, transparent);
}
.mu-player-footer-controls {
    display: flex;
    align-items: center;
    gap: 16px;
}

.mu-waveform {
    position: relative;
    flex: 1;
    cursor: pointer;
    outline: none;
}
.mu-waveform-bars {
    display: flex;
    align-items: center;
    width: 100%;
    height: 44px;
    gap: 2px;
}
/* Bigger, more of a hero element — the waveform carries the page now,
   same scale SoundCloud gives its own player instead of a thin strip. */
.mu-player--single .mu-waveform-bars { height: 64px; gap: 3px; }
.mu-wf-bar {
    flex: 1;
    min-width: 1px;
    height: 15%;
    background: rgba(255,255,255,0.1);
    border-radius: 2px 2px 0 0;
    transition: height 0.1s, background 0.2s, box-shadow 0.2s;
}
.mu-wf-bar.mu-wf-played { background: rgba(255,255,255,0.8); }
/* Still monochrome — the "richness" is a soft white glow on the active
   portion, not color, so the played bars actually radiate a bit instead
   of just being a flat brighter fill. Dialed back from a harder glow —
   the whole mass of bars was reading as too bright/glary overall. */
.mu-player--single .mu-wf-bar.mu-wf-played { box-shadow: 0 0 6px rgba(255,255,255,0.35); }
.mu-waveform:hover .mu-wf-bar:not(.mu-wf-played) { background: rgba(255,255,255,0.18); }
.mu-waveform:focus-visible { outline: 1px solid rgba(255,255,255,0.4); outline-offset: 3px; }

/* Scrub hover-preview — a distinct, dimmer shade from the actual
   "played" portion, marking how far you'd land if you clicked here
   right now. JS toggles this per-bar alongside the tooltip's time text. */
.mu-wf-bar.mu-wf-hover:not(.mu-wf-played) { background: rgba(255,255,255,0.32); }

/* Idle equalizer shimmer while a track's waveform is still being
   fetched/decoded — reads as "loading" without a spinner, and doubles
   as a little ambient life on tracks that are already in view waiting
   on the network. Cleared the instant real peaks land (mu-waveform-loaded)
   or a fetch/decode failure is confirmed (mu-waveform-error). */
@keyframes mu-wf-idle {
    0%, 100% { opacity: 0.35; transform: scaleY(0.75); }
    50% { opacity: 0.7; transform: scaleY(1.15); }
}
.mu-player:not(.mu-waveform-loaded):not(.mu-waveform-error) .mu-wf-bar {
    animation: mu-wf-idle 1.1s ease-in-out infinite;
    animation-delay: calc(var(--bar-i, 0) * 40ms);
}

/* Error state — waveform fetch/decode failed. Playback itself still
   works (it never depended on this), so this just quiets the bars down
   and surfaces a short explanation instead of leaving them looking
   broken with no explanation. */
.mu-player.mu-waveform-error .mu-wf-bar {
    background: rgba(255,255,255,0.06);
}
.mu-waveform-status {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--color-text-muted);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s;
}
.mu-player.mu-waveform-error .mu-waveform-status { opacity: 1; }

/* Playhead — the exact current position, distinct from "how much of the
   bar is colored in" (mu-wf-bar.mu-wf-played), same way a DAW timeline
   marks a precise cursor rather than just a filled region. */
.mu-waveform-playhead {
    position: absolute;
    top: -3px; bottom: -3px;
    left: 0%;
    width: 1px;
    background: var(--color-text-primary);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s;
}
.mu-player--single .mu-waveform-playhead { box-shadow: 0 0 8px rgba(255,255,255,0.8); }
.mu-player.mu-playing .mu-waveform-playhead,
.mu-waveform:hover .mu-waveform-playhead { opacity: 0.9; }

/* Comment markers — a comment pinned to a moment shows as a small tick
   sitting on the waveform, same SoundCloud idea. Brightens on hover so
   it reads as clickable; the comment's name/time shows via the native
   title tooltip rather than a second custom tooltip element. */
.mu-waveform-marks {
    position: absolute;
    inset: 0;
    pointer-events: none;
}
.mu-wf-mark {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 2px;
    margin-left: -1px;
    background: rgba(255,255,255,0.5);
    cursor: pointer;
    pointer-events: auto;
    transition: background 0.15s, box-shadow 0.15s;
}
.mu-wf-mark:hover {
    background: #fff;
    box-shadow: 0 0 8px rgba(255,255,255,0.7);
}

/* SoundCloud-style hover preview — the time you'd land on if you
   clicked here, following the cursor along the bar. JS positions it
   via left/--mu-tooltip-x; opacity is purely a hover/drag toggle. */
.mu-waveform-tooltip {
    position: absolute;
    top: -24px;
    left: 0%;
    transform: translateX(-50%);
    padding: 2px 6px;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    color: var(--color-text-primary);
    background: rgba(0,0,0,0.8);
    border: 1px solid rgba(255,255,255,0.15);
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.12s;
}
.mu-waveform:hover .mu-waveform-tooltip { opacity: 1; }

/* An absolutely-positioned ::before with a negative inset — a bigger
   invisible tap target (~44px, the mobile minimum) around a visually
   small icon, without touching the button's own box model. Unlike
   padding+negative-margin, this can't perturb the flex row's own
   gap/space-between math, since the expanded hit area is taken out of
   layout flow entirely. */
.mu-mute-btn {
    position: relative;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: color 0.15s, transform 0.1s;
}
.mu-mute-btn::before { content: ''; position: absolute; inset: -11px; }
.mu-mute-btn:hover { color: var(--color-text-primary); }
.mu-mute-btn:active { transform: scale(0.88); }
.mu-mute-btn .mu-icon-muted { display: none; }
/* Same fix as .mu-play-btn above — .mu-mute-btn is in the footer, a
   sibling of .mu-player, not a descendant, so ".mu-player.mu-muted
   .mu-mute-btn" could never match. */
.mu-player-block:has(.mu-player.mu-muted) .mu-mute-btn .mu-icon-unmuted { display: none; }
.mu-player-block:has(.mu-player.mu-muted) .mu-mute-btn .mu-icon-muted { display: block; }

/* Volume — a thin flat track with a small square-ish thumb, same
   monochrome/no-chrome language as everything else here rather than the
   browser's default slider skin. Range inputs don't reliably support a
   ::before hit-area trick, so this one just stays modestly sized. */
.mu-volume-slider {
    width: 70px;
    height: 3px;
    accent-color: #fff;
    cursor: pointer;
}
.mu-volume-slider,
.mu-volume-slider::-webkit-slider-runnable-track {
    background: rgba(255,255,255,0.15);
    border-radius: 2px;
}
.mu-volume-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: #fff;
    cursor: pointer;
    margin-top: -3.5px;
    transition: transform 0.15s;
}
.mu-volume-slider:hover::-webkit-slider-thumb,
.mu-volume-slider:focus-visible::-webkit-slider-thumb {
    transform: scale(1.3);
}
.mu-volume-slider::-moz-range-track {
    background: rgba(255,255,255,0.15);
    height: 3px;
    border-radius: 2px;
}
.mu-volume-slider::-moz-range-thumb {
    width: 10px;
    height: 10px;
    border: 0;
    border-radius: 50%;
    background: #fff;
    cursor: pointer;
    transition: transform 0.15s;
}
.mu-volume-slider:hover::-moz-range-thumb,
.mu-volume-slider:focus-visible::-moz-range-thumb {
    transform: scale(1.3);
}

/* Loop — same flat icon-button treatment as Mute, a playback setting
   living in the player's own controls row. Active state communicated
   with brightness/glow, never color. */
.mu-loop-btn {
    position: relative;
    display: flex;
    align-items: center;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--color-text-muted);
    cursor: pointer;
    transition: color 0.15s, transform 0.1s;
}
.mu-loop-btn::before { content: ''; position: absolute; inset: -11px; }
.mu-loop-btn:hover { color: var(--color-text-primary); }
.mu-loop-btn:active { transform: scale(0.88); }
.mu-loop-btn.is-active { color: #fff; }
.mu-loop-btn.is-active .mu-icon { filter: drop-shadow(0 0 4px rgba(255,255,255,0.6)); }

/* Like — same line as the track/date, pushed to that row's far right
   (margin-left:auto), not floating in the card's general top-right
   corner unrelated to any specific line of text. Not in the shared
   .share-buttons row below (blog.css's Share/Discuss styling) since it's
   a per-track toggle, not a share/discuss action. */
.mu-like-btn {
    position: relative;
    margin-left: auto;
    align-self: center;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0;
    border: 0;
    background: transparent;
    color: var(--color-text-muted);
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: 0.65rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    transition: color 0.15s;
}
.mu-like-btn::before { content: ''; position: absolute; inset: -14px; }
.mu-like-btn .mu-icon { width: 16px; height: 16px; transition: transform 0.15s; }
.mu-share-btn .mu-icon { width: 16px; height: 16px; }
.mu-like-btn:hover { color: var(--color-text-primary); }
.mu-like-btn:active .mu-icon { transform: scale(0.8); }
/* A quick pop on the heart itself right when it becomes liked — separate
   keyframe (not the :active press above) so it plays once on state
   change rather than on every mousedown. */
@keyframes mu-like-pop {
    0% { transform: scale(1); }
    40% { transform: scale(1.35); }
    100% { transform: scale(1); }
}
.mu-like-btn.is-liked .mu-icon-heart-filled { animation: mu-like-pop 0.35s ease; }

.mu-like-btn .mu-icon-heart-filled { display: none; }
.mu-like-btn.is-liked .mu-icon-heart-outline { display: none; }
.mu-like-btn.is-liked .mu-icon-heart-filled { display: block; }
.mu-like-btn.is-liked { color: #fff; }
.mu-like-count { font-variant-numeric: tabular-nums; }

/* The cover column reserves real width on narrow phones (110px, see
   .mu-player-cover's own max-width:620px rule) — width it didn't take up
   when cover art was just an ambient full-card background — leaving a
   lot less room for the title row. .mu-like-btn's margin-left:auto is
   fine while it stays on the title's own line, but once it wraps
   (.mu-player-header-meta is flex-wrap:wrap) the auto-margin flings it
   to the far right of a *new* line by itself, reading as a stray
   floating heart with a big gap above it rather than a second,
   intentional row. Dropping the auto-margin on mobile just lets it sit
   immediately after the date/tags when it wraps.
   Placed after the base .mu-like-btn rule above (not just gated by a
   media query) — with equal specificity, an unconditional rule appearing
   *later* in the file always wins a tie over a conditional one appearing
   *earlier*, active media query or not. This exact override used to sit
   above .mu-like-btn's own definition and silently never applied. */
@media (max-width: 620px) {
    .mu-like-btn { margin-left: 0; }
}

/* Technical readout — the right half of the footer, plain inline text
   (no per-item label/box/glow) since this is a quiet footnote, not a
   second thing on the page competing with the waveform for attention.
   A faint middot between entries does the separating. */
.mu-player-footer-meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    font-family: var(--font-mono);
    font-size: 0.65rem;
    text-transform: uppercase;
    color: var(--color-text-muted);
    letter-spacing: 0.02em;
}
.mu-player-footer-meta > span:not(:first-child)::before {
    content: '·';
    margin: 0 8px;
    color: rgba(255,255,255,0.25);
}
@media (max-width: 560px) {
    /* The cinematic waveform height only makes sense at desktop widths —
       collapse back to more compact proportions on phones. Same ~40%
       height trim as the desktop version got, not left at the old taller
       proportions. */
    .mu-player--single { padding: 14px; }
    .mu-player--single .mu-waveform-bars { height: 36px; gap: 2px; }

    /* Card was still reading as tall on a phone even after the waveform
       trim above — the header/scrubber/footer stack's own 18px gaps and
       14px header margin (sized for the desktop cover's much taller
       210px-wide column) were carrying over unchanged. Tightened here
       rather than in the base rules since desktop's card has the height
       budget to spare and didn't need this. */
    .mu-player-content { gap: 10px; }
    .mu-player-header { margin-bottom: 8px; }

    /* One line, always — no wrap/stack into a second row. Everything
       below is sized specifically so the full controls + full meta
       readout actually fit in that one line rather than just hoping. */
    .mu-player-footer {
        flex-wrap: nowrap;
        gap: 6px;
        padding: 10px 14px;
    }
    .mu-player-footer::before { left: 14px; right: 14px; }
    .mu-player-footer-controls { flex-shrink: 0; gap: 5px; }
    .mu-icon { width: 15px; height: 15px; }
    .mu-volume-slider { width: 20px; }
    .mu-volume-slider::-webkit-slider-thumb { width: 7px; height: 7px; margin-top: -2px; }
    .mu-volume-slider::-moz-range-thumb { width: 7px; height: 7px; }
    .mu-player-footer-meta {
        flex-wrap: nowrap;
        flex-shrink: 1;
        gap: 0;
        font-size: 0.52rem;
        overflow: hidden;
        white-space: nowrap;
    }
    .mu-player-footer-meta > span:not(:first-child)::before { margin: 0 3px; }
}

/* ── [recording player="1"] embed — compact, unconditionally ────────────
   music.php's own page gives the player a cinematic hero scale (64px
   waveform bars, 210px edge-to-edge cover) because it's the only thing on
   the page. Embedded inline in a blog post, that same sizing next to a
   few lines of prose reads as way too tall — so the shortcode's wrapper
   forces the same compact treatment the @media rules above establish for
   narrow phones (small centered-square cover, shorter waveform, tighter
   padding), unconditionally, since a blog prose column is a narrower
   context either way regardless of the actual viewport width. */
.sc-recording-player .mu-player--single { padding: 10px 12px; }
.sc-recording-player .mu-player--single .mu-waveform-bars { height: 26px; gap: 2px; }
/* No cover width override here — the base 210px edge-to-edge treatment
   (same as music.php's own page) is unaffected by the height fix above
   (that was a genuine layout bug, not something that needed a narrower
   cover to work around); at this card's now-short natural height a 210px
   cover just reads as a wide crop of the art, which is fine. */
/* The big circular Play overlay (56px, sized for a 210px/full-height
   desktop cover against a ~250px+ tall card) was covering most of the
   visible art now that the card is genuinely short — same icon,
   proportionally smaller circle instead. */
.sc-recording-player .mu-cover-play-btn { width: 34px; height: 34px; }
.sc-recording-player .mu-cover-play-btn .mu-icon { width: 16px; height: 16px; }
.sc-recording-player .mu-player-footer { padding: 6px 12px; }
.sc-recording-player .mu-icon { width: 13px; height: 13px; }
.sc-recording-player .mu-play-btn .mu-icon-play { transform: translateX(1px); }
.sc-recording-player .mu-player-content { gap: 6px; }
.sc-recording-player .mu-player-title,
.sc-recording-player .mu-player-kicker,
.sc-recording-player .mu-track-date,
.sc-recording-player .mu-like-btn,
.sc-recording-player .mu-player-footer-meta { font-size: 0.6rem; }

/* ── Comments — pin-to-time additions ─────────────────────────────────
   blog.css supplies .comment-item/.comment-meta/.comment-form as-is;
   these two are music-only extras layered on top: the button that pins
   the comment form to the current playhead, and the small readout each
   pinned comment gets showing where it points. */
.comment-pin-btn {
    display: inline-block;
    margin: 4px 0 14px;
    padding: 4px 10px;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    text-transform: uppercase;
    letter-spacing: 0.06em;
    color: var(--color-text-muted);
    background: transparent;
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 2px;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
}
.comment-pin-btn:hover { color: var(--color-text-primary); border-color: rgba(255,255,255,0.4); }
.comment-pin-btn.is-pinned { color: #fff; border-color: rgba(255,255,255,0.5); }

.comment-seek-btn {
    margin-left: 8px;
    padding: 1px 6px;
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.02em;
    color: var(--color-text-muted);
    background: transparent;
    border: 1px solid rgba(255,255,255,0.15);
    border-radius: 2px;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
}
.comment-seek-btn:hover { color: var(--color-text-primary); border-color: rgba(255,255,255,0.4); }

/* ── Track nav — settles at the bottom of the viewport, doesn't follow it ─
   Prev/next used to sit inline right after the description/comments, so
   its on-screen position jumped around depending on how long a given
   track's note was. Not position:fixed, though — that would keep it
   glued to the screen while scrolling, which reads as it "following"
   you rather than just being part of the page (same call already made
   for .mu-page itself, see that rule's own comment above). Instead
   .mu-page--single is a flex column with a viewport-height floor
   (100vh minus the same --page-top-anchor it already pads itself with),
   and this nav — its last child — gets margin-top:auto. A short track
   still lands right at the bottom of the viewport on first render; a
   long one just pushes the nav below the fold like any other content.

   Compound selector (nav.blog-nav.mu-track-nav), not just .mu-track-nav
   alone — blog.css loads *after* music.css in <head> (intentional, so
   .share-buttons/.comments-section inherit blog.css's styling), so a
   single-class selector here would lose to blog.css's own .blog-nav
   rule on load order alone. Extra specificity wins regardless of order. */
.mu-page--single {
    display: flex;
    flex-direction: column;
    min-height: calc(100vh - var(--page-top-anchor));
}
nav.blog-nav.mu-track-nav {
    margin-top: auto;
    flex-shrink: 0;
}
/* When the "Connected" block is present, it's the earlier of the two
   footer siblings — give it the auto margin instead so it rides down
   to the bottom together with the nav rather than stranding near the
   top with a huge dead gap before the nav (which still soaks up that
   leftover space via its own margin-top:auto above). The immediately-
   following nav's margin resets to a normal gap so the two don't split
   the leftover space into two separate gaps. */
.mu-page--single .content-footer-meta {
    margin-top: auto;
}
.mu-page--single .content-footer-meta + nav.blog-nav.mu-track-nav {
    margin-top: 60px;
}
/* min-width:0 — flex items default to their content's intrinsic width as
   a shrink floor, which silently defeats text-overflow:ellipsis below. */
nav.blog-nav.mu-track-nav .blog-nav-link { min-width: 0; }
/* Track titles are free text with no length limit — clamp to one line
   with an ellipsis at every width, not just narrow ones, so a long
   title can't wrap and grow the bar's height or crowd its sibling.
   Three things had to be true at once for this to actually clip instead
   of silently no-opping — confirmed by inspecting live computed styles,
   not just guessing from a screenshot:
   - display:block — blog.css's .blog-nav-title is a bare <span>; ellipsis
     does nothing on an inline box.
   - min-width:0 — .blog-nav-title is itself a flex item (.blog-nav-link
     is display:flex;flex-direction:column), so it has its own default
     min-width:auto floor based on intrinsic (unwrapped) content width.
   - width:100% — the actual culprit. .blog-nav-prev/.blog-nav-next
     (blog.css) set align-items:flex-start/flex-end on the flex column,
     overriding the default stretch — so .blog-nav-title was never being
     sized to fill its parent at all, it was shrink-to-fit on its own
     text every time (confirmed via getBoundingClientRect: a 169px-wide
     .blog-nav-link held a 513px-wide title). overflow:hidden had
     nothing to actually clip against without an explicit width. This is
     what let two untruncated titles visibly overlap on mobile (blog.css's
     own max-width:768px rule puts prev/next side-by-side at 50% width
     each) — display:block + min-width:0 alone looked right on paper but
     didn't fix anything, since the box was never constrained in the
     first place. */
nav.blog-nav.mu-track-nav .blog-nav-title {
    display: block;
    width: 100%;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* width:100% above fixes the box to its parent's full width, which as a
   side effect defeats .blog-nav-next's own align-items:flex-end — that
   only pushes the *box* to the right when the box is narrower than its
   parent, and now it never is. Text-align picks up where that left off:
   right for Next (so the now-truncated title still reads as flush with
   the → arrow above it), explicit left for Previous so it doesn't
   silently inherit some other ancestor's text-align down the line. */
nav.blog-nav.mu-track-nav .blog-nav-next .blog-nav-title { text-align: right; }
nav.blog-nav.mu-track-nav .blog-nav-prev .blog-nav-title { text-align: left; }
@media (max-width: 780px) {
    .mu-page--single { min-height: calc(100vh - var(--page-top-offset-single)); }
}
