audio-player {
    --button-padding: 0.5rem;
    --icon-size: 1em;
    --transport-icon-size: 2em;
    --artwork-play-icon-size: 45%;
    --volume-icon-size: 0.675em;
    --credits-padding: 0.25rem;
    --credits-animation: topToBottom 0.5s ease 1 forwards;
    /*--credits-animation: bottomToTop 3s ease 1 forwards;*/
    /*--credits-animation: backInUp 3s ease 1 forwards;*/
}

audio-player[initializing] {
    visibility: hidden;
    pointer-events: none;
}

/* Custom elements default to display:inline — set block baseline
   for all elements that replaced former <div> containers. */
audio-player-details,
audio-player-artwork,
audio-player-play-overlay,
audio-player-title-and-artists,
audio-player-title,
audio-player-artists,
audio-player-info,
audio-player-titles,
audio-player-playback-controls,
audio-player-progress-bar-section,
audio-player-progress-bar,
audio-player-buffer-progress,
audio-player-progress,
audio-player-elapsed-time,
audio-player-remaining-time,
audio-player-total-time,
audio-player-previous,
audio-player-play-pause-group,
audio-player-play,
audio-player-pause,
audio-player-next,
audio-player-volume-icon,
audio-player-volume-bar,
audio-player-volume-level,
audio-player-playback-options,
audio-player-raw,
audio-player-level-match,
audio-player-random,
audio-player-list {
    display: block;
}

@keyframes topToBottom {
    from {
        transform: translateY(-100%) scale(0.3);
        opacity: 0;
    }

    50% {
        opacity: 0;
    }

    80% {
        transform: translateY(0%) scale(0.7);
        opacity: 0.1;
    }

    to {
        transform: scale(1);
        opacity: 1;
    }
}

@keyframes bottomToTop {
    from {
        transform: scale(1);
        opacity: 1;
    }

    20% {
        transform: translateY(0%) scale(0.7);
        opacity: 0.7;
    }

    50% {
        opacity: 0;
    }

    to {
        transform: translateY(-100%) scale(0.3);
        opacity: 0;
    }
}

/* TESTING renderFluid ... */
@keyframes backInDown {
    from {
        transform: translateY(-1200px) scale(0.7);
        opacity: 0.7;
    }

    80% {
        transform: translateY(0px) scale(0.7);
        opacity: 0.7;
    }

    to {
        transform: scale(1);
        opacity: 1;
    }

    /* to {
        transform: translateY(0%);
    } */
}

@keyframes backOutUp {
    0% {
        transform: scale(1);
        opacity: 1;
    }

    20% {
        transform: translateY(0px) scale(0.7);
        opacity: 0.7;
    }

    100% {
        transform: translateY(-700px) scale(0.7);
        opacity: 0.7;
    }
}

@keyframes backInUp {
    0% {
        transform: translateY(1200px) scale(0.7);
        opacity: 0.7;
    }

    80% {
        transform: translateY(0px) scale(0.7);
        opacity: 0.7;
    }

    100% {
        transform: scale(1);
        opacity: 1;
    }
}

@keyframes backOutDown {
    0% {
        transform: scale(1);
        opacity: 1;
    }

    20% {
        transform: translateY(0px) scale(0.7);
        opacity: 0.7;
    }

    100% {
        transform: translateY(700px) scale(0.7);
        opacity: 0.7;
    }
}

@keyframes backInLeft {
    0% {
        transform: translateX(-2000px) scale(0.7);
        opacity: 0.7;
    }

    80% {
        transform: translateX(0px) scale(0.7);
        opacity: 0.7;
    }

    100% {
        transform: scale(1);
        opacity: 1;
    }
}

@keyframes backOutRight {
    0% {
        transform: scale(1);
        opacity: 1;
    }

    20% {
        transform: translateX(0px) scale(0.7);
        opacity: 0.7;
    }

    100% {
        transform: translateX(2000px) scale(0.7);
        opacity: 0.7;
    }
}

@keyframes backInRight {
    0% {
        transform: translateX(2000px) scale(0.7);
        opacity: 0.7;
    }

    80% {
        transform: translateX(0px) scale(0.7);
        opacity: 0.7;
    }

    100% {
        transform: scale(1);
        opacity: 1;
    }
}

@keyframes backOutLeft {
    0% {
        transform: scale(1);
        opacity: 1;
    }

    20% {
        transform: translateX(0px) scale(0.7);
        opacity: 0.7;
    }

    100% {
        transform: translateX(-2000px) scale(0.7);
        opacity: 0.7;
    }
}

@keyframes bounce {

    from,
    20%,
    53%,
    80%,
    to {
        animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
        transform: translate3d(0, 0, 0);
    }

    40%,
    43% {
        animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
        transform: translate3d(0, -30px, 0);
    }

    70% {
        animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
        transform: translate3d(0, -15px, 0);
    }

    90% {
        transform: translate3d(0, -4px, 0);
    }
}

/* ... TESTING renderFluid */

audio-player {
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;

    display: flex;
    flex-direction: column;
    max-width: 18em;
    margin: 0 auto;
    padding: 1.5em 2em 0.5em 2em;
    position: sticky;
    bottom: 0;
    z-index: 2147483647;
}

/* audio-player:active,
audio-player *:active {
    cursor: ns-resize;
} */

audio-player audio-player-playback-controls>*,
audio-player audio-player-playback-options>*,
audio-player audio-player-progress-bar-section,
audio-player audio-player-info,
audio-player audio-player-progress-bar-section,
audio-player audio-player-volume-bar {
    cursor: default;
}

audio-player audio-player-playback-controls>*,
audio-player audio-player-playback-options>*,
audio-player audio-player-progress-bar-section,
audio-player audio-player-info {
    scale: 1;
    transition: scale 0.1s ease;
}

audio-player audio-player-playback-controls>*:active,
audio-player audio-player-playback-options>*:active,
audio-player audio-player-info:active {
    scale: 0.95;
}

/* audio-player audio-player-progress-bar-section,
audio-player audio-player-volume-bar {
    scale: 1;
    transition: scale 0.1s ease;
}

audio-player audio-player-progress-bar-section:active,
audio-player audio-player-volume-bar:active {
    scale: 1 1.5;
} */

audio-player-main {
    display: block;
    position: relative;
    pointer-events: none;
}

audio-player-on-playback {
    display: block;
    padding: 0.5em 0;
}

audio-player[list="on"] audio-player-on-playback {
    opacity: 0;
}

audio-player-on-playback-cover {
    /* Reserve a square layout box regardless of the source cover's
       aspect ratio — otherwise a non-square artwork (e.g. portrait
       cover) would change the container height and the whole player
       would jump on every track change.

       The cover is purely the layout box. All visual transforms
       (size + position changes for play↔pause and [info-shown]) live
       on the audio-player-on-playback-cover-art wrapper inside, so
       the img and the RAW / LEVEL MATCH banners transform together
       as one unit.

       overflow is left visible so the resting cover-art's box-shadow
       renders unclipped. Skip motion is rendered in a separate fixed
       viewport layer, so this layout box never needs to clip it. */
    display: block;
    flex-shrink: 0;
    width: 100%;
    aspect-ratio: 1 / 1;
    margin-bottom: 1rem;
    position: relative;

}

/* The wrapper that actually carries the cover's visual size, position
   and animation. Centered inside the (full-size) cover via top/left:50%
   + translate(-50%, -50%); width is driven by --cover-width, height
   follows from aspect-ratio:1/1. */
audio-player-on-playback-cover-art {
    /* Default tokens: full-size, centered. The on-pause and [info-shown]
       rules below override these. */
    --cover-width: 100%;

    display: block;
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--cover-width);
    aspect-ratio: 1 / 1;

    /* Declare as a size container so the RAW / LEVEL MATCH banner text
       inside can use cqi units — they then scale proportionally with
       the wrapper itself, including when shrinking to 3em under
       [info-shown]. */
    container-type: inline-size;

    border-radius: inherit;

    transform: translate(-50%, -50%);
    /* Permanently pin the cover-art to its own compositor layer.
       Reason: the sibling info-pane is a scroll container whose
       content can be tall enough to be promoted to a heavy raster
       layer at rest. On iOS Safari, the existence of that scrollable
       sibling layer in the same stacking context causes the cover's
       transform animations (including the info open/close FLIP) to be
       de-promoted to the main thread — producing the
       cover-art lag the user observes only when the pane is
       scrollable. Forcing the cover-art onto its own GPU layer with
       `will-change: transform` keeps its transform animations on the
       compositor regardless of what the pane layer is doing. The
       memory cost is negligible (one square layer per audio-player). */
    will-change: transform;
    transition:
        width 0.3s ease,
        top 0.3s ease,
        left 0.3s ease,
        transform 0.3s ease;
}

audio-player[playback="on-pause"] audio-player-on-playback-cover-art {
    --cover-width: var(--standard-cover-width, 80%);
}

/* Disable cover-art's own size/position transition while the player is
   compact. The cover element itself is being WAAPI-animated to 2rem at
   that point — if cover-art's width:100% (resolved against the
   shrinking cover) were transitioning at the same time from a previous
   info-shown 3em, the interpolated values could overflow the cover.
   Snapping is correct in compact: the info pane is fading out anyway,
   so the cover-art has no special positioning to preserve. */
audio-player[visual-state="compact"] audio-player-on-playback-cover-art {
    transition: none;
}

audio-player[view-transitioning] audio-player-on-playback-cover-art {
    transition: none;
}

/* In compact, always fill the tiny cover box regardless of play/pause state
   or info-shown. Overrides the on-pause 80% and info-shown 3em rules. */
audio-player[visual-state="compact"] audio-player-on-playback-cover-art {
    --cover-width: var(--standard-cover-width, 100%);
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

audio-player-on-playback-cover-art > img {
    /* Fill the wrapper. */
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;

    border-radius: inherit;
}

/* Cover skips render here instead of translating descendants of the player.
   The layer owns a viewport-sized paint boundary: shadows remain visible
   everywhere on-screen, while pixels past the physical viewport cannot add
   transformed overflow to the root document. */
audio-player-cover-swipe-layer {
    display: block;
    position: fixed;
    inset: 0;
    overflow: clip;
    contain: strict;
    pointer-events: none;
    user-select: none;
    z-index: 2147483647;
}

audio-player-cover-swipe-visual {
    display: block;
    position: absolute;
    box-sizing: border-box;
    container-type: inline-size;
    transform-origin: center;
    will-change: transform, opacity;
}

audio-player-cover-swipe-visual > img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-radius: inherit;
}

audio-player-cover-swipe-layer[audioversion="raw"] raw-audio-banner::after,
audio-player-cover-swipe-layer[levelmatch="on"] level-match-banner::after {
    transform: translateY(0);
}

audio-player-cover-swipe-layer[audioversion="raw"] raw-audio-banner,
audio-player-cover-swipe-layer[levelmatch="on"] level-match-banner {
    opacity: 1;
}

/* RAW + LEVEL MATCH badges — persistent banner elements layered on top
   of the cover-art wrapper. Each banner fills and clips to the complete
   cover. This matters for percentage radii: resolving (for example) 10%
   against a short banner strip produces a much smaller vertical radius
   than resolving it against the square artwork. The visible ::after
   strip is anchored to the appropriate edge inside that full-cover clip,
   so its corners now follow the artwork exactly. */
raw-audio-banner,
level-match-banner {
    position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    overflow: hidden;
    pointer-events: none;
    display: block;
    border-radius: inherit;

    /* Inactive by default — fade in once the matching audio-player
       attribute is present (see active-state rule below). */
    opacity: 0;

    transition: opacity 0.25s ease;
}

raw-audio-banner::after,
level-match-banner::after {
    display: block;
    position: absolute;
    left: 0;
    right: 0;
    padding: 0.35em 0;

    transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);
}

raw-audio-banner::after {
    content: "RAW";
    bottom: 0;
    /* cqi = % of cover-art's inline size, so the banner text stays
       proportional to the wrapper at every point of the shrink
       animation (including [info-shown] = 3em). */
    font-size: 9cqi;
    transform: translateY(100%);
}

level-match-banner::after {
    content: "LEVEL MATCH";
    top: 0;
    font-size: 6.5cqi;
    transform: translateY(-100%);
}

audio-player[audioversion="raw"] raw-audio-banner::after,
audio-player[levelmatch="on"] level-match-banner::after {
    transform: translateY(0);
}

/* Show the banner element itself only while its state is active. */
audio-player[audioversion="raw"] raw-audio-banner,
audio-player[levelmatch="on"] level-match-banner {
    opacity: 1;
}

audio-player-on-playback-info-container {
    display: block;
    position: relative;
    overflow-x: clip;
    overflow-y: visible;
    z-index: -1;
}

/* Info button: absolutely positioned — no layout change to the container.
   - top/bottom:0 + margin:auto = vertical centering without transform or %
     (both would be recomputed every drag frame inside the animated container)
   - px sizes, not em: the container's fontSize is animated per frame during
     drag, so em-based sizes on children force per-frame recomputation on iOS.
   - the icon has an explicit size, so no replaced-element fallback can affect
     layout while the player stylesheet is active. */
audio-player-on-playback-info-container>audio-player-info {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    margin: auto 0;
    height: 1em;
    width: 1em;
    pointer-events: auto;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Keep title/artist from sliding under the info button.
   Use px not em — avoids per-frame recomputation when the container's
   fontSize is animated during compact<->full-size drag.
   `will-change: transform` permanently pins the title element to its
   own compositor layer for the same reason cover-art does — see the
   long comment on cover-art's will-change above. The skip-to-track
   slide WAAPI-animates `transform` on this element; without the layer
   pin, iOS Safari de-promotes that animation to the main thread when
   the info-pane is open with scrollable content as a sibling layer. */
audio-player-on-playback-info {
    display: block;
    padding-right: 24px;
    will-change: transform;
}

/* Pane's sliding content wrapper (live + preview during a skip-swipe).
   The skip animation drives translateX on this wrapper via inline style
   from the progress callback; pinning it to a compositor layer keeps
   that slide on the GPU. Contains the paged-content host (credits) plus
   the links/releasedate footer — all slide as a single unit.
   flex: 1 1 auto + min-height: 0 lets it fill the pane. */
audio-player-on-playback-info-pane-content {
    will-change: transform;
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
}

/* The paged-content host fills the remaining space above the footer. */
audio-player-on-playback-info-pane-content > [holds="info-content"] {
    flex: 1 1 auto;
    min-height: 4.25em;
    --paged-content-dots-gap: 0.45em;
    --paged-content-dots-padding: 0.35em 0 0.4em;
    --paged-content-dots-min-height: 1.35em;
    --paged-content-dot-size: 0.62em;
    --paged-content-dot-button-padding: 0.18em;
}

/* Info pane lives inside audio-player-on-playback-cover (position: relative).
   It is no longer a scrollable container: its single permanent child is a
   <paged-content holds="info-content"> element that partitions tall info
   into discrete pages. Eliminating the composited scrollable layer here is
   what fixes the iOS Safari compositor de-promotion of the cover/title/
   pane-content transform animations. */
audio-player-on-playback-info-pane {
    display: none;
    position: absolute;
    top: calc(3em + 1.75em);
    left: 0;
    right: 0;
    bottom: 0;
    padding: 0 0.25em 0.2em 0.25em;
    box-sizing: border-box;
    pointer-events: none;
    font-size: 0.75em;
    margin: 0 0 0.25em 0;
    /* Hard clip the pane to its layout box so the paged-content host
       can never visually leak into the audioplayer controls below.
       `overflow: clip` purely clips paint and (unlike `scroll` or
       `hidden`) does NOT create a scrollable / composited layer, so it
       does not re-introduce the iOS Safari compositor de-promotion of
       the cover / title transform animations. */
    overflow: clip;
    z-index: -1;
    transform: scale(1);
    transform-origin: var(--info-pane-origin-x, 50%) var(--info-pane-origin-y, 0%);
}

/* When info is shown:
   - Cover-art wrapper shrinks to 3em and shifts to the cover's
     top-left corner (via top:0 / left:0 + transform:none, overriding
     the centered 50%/50% / translate(-50%,-50%) base rule).
   - The img + banners ride along inside the wrapper automatically.
   - cqi-based banner text stays proportional because the wrapper is
     the size container.
   - Pane animation is driven by [show-info] / [hide-info] attributes
     (set by Animation.render.toggle), using the same 300ms ease — so
     the close is a perfect reverse of the open. */
/* Pane is visible only when info is logically shown AND the player is
   in its full-size resting state. While compact (or transitioning to
   compact) the pane fades out via the opacity transition above and the
   cover-art rule below reverts so the cover can shrink normally. The
   logical [info-shown] attribute is preserved through size changes, so
   returning to full-size automatically fades the pane back in. */
audio-player[info-shown] audio-player-on-playback-info-pane {
    /* Keep pane in layout so the opacity transition has both endpoints
       to animate between. (Default rule above is display:none.) Also
       keeps the pane visible after [show-info] is removed at the
       button-driven open animation's animationend. */
    display: flex;
    /* Column direction so align-items: stretch stretches the
       paged-content host across the full pane width (not just height).
       Without this, the host shrinks to its intrinsic content width and
       the dot navigator appears left-aligned instead of centered in
       the pane. */
    flex-direction: column;
    justify-content: flex-start;
    align-items: stretch;
    pointer-events: auto;
    will-change: transform, opacity;
}

/* While the player is compact, fade the pane out without removing it
   from layout — the opacity transition (set on the base rule) handles
   the fade. Removing [visual-state="compact"] (i.e. returning to full)
   automatically fades it back in. */
audio-player[info-shown][visual-state="compact"] audio-player-on-playback-info-pane {
    opacity: 0;
    pointer-events: none;
}

audio-player[info-shown] audio-player-on-playback-cover-art {
    --cover-width: var(--info-cover-width, 3em);
    top: var(--info-cover-top, 0);
    left: var(--info-cover-left, 0);
    transform: var(--info-cover-transform, translate(0, 0));
    transition: none;
}

/* Attribute-driven blend animation. --info-pane-origin-x and
   --info-pane-origin-y are set inline by JS so the pane scales from the info
   button's position while the cover-art shrinks / grows in parallel. */
@keyframes infoPaneBlend {
    0% {
        opacity: 0;
        transform: scale(0);
    }

    100% {
        opacity: 1;
        transform: scale(1);
    }
}

audio-player-on-playback-info-pane[show-info],
audio-player-on-playback-info-pane[hide-info] {
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    align-items: stretch;
    animation: infoPaneBlend var(--info-pane-toggle-duration, 0.3s) var(--info-pane-toggle-easing, ease) 1 forwards;
    overflow: clip;
    will-change: transform, opacity;
}

audio-player-on-playback-info-pane[hide-info] {
    animation-direction: reverse;
}

/* Info pane content styles (mirrors list-view). */
audio-player-on-playback-info-pane [holds="credits"] {
    display: grid;
    grid-template-columns: minmax(0, min(8.75em, 45%)) minmax(0, 1fr);
    gap: 0.25em 0.5em;
}

audio-player-on-playback-info-pane [holds="credits"] > [holds="credit-row"] {
    display: contents;
}

audio-player-on-playback-info-pane [holds="credit-row"] {
    display: grid;
    grid-template-columns: minmax(0, min(8.75em, 45%)) minmax(0, 1fr);
    gap: 0.25em 0.5em;
    align-items: start;
    line-height: 1.2;
}
audio-player-titles [holds="credit-row"]:nth-of-type(2n) {
    text-align: right;
    width: 100%;
}

audio-player-on-playback-info-pane [holds="credit-row"] + [holds="credit-row"] {
    margin-top: 0.25em;
}

audio-player-on-playback-info-pane [holds="credit-key"] {
}

audio-player-on-playback-info-pane [holds="credit-value"] {
    overflow-wrap: break-word;
    word-wrap: break-word;
}

audio-player-on-playback-info-pane [holds="releasedate"] {
    display: block;
    text-align: right;
    flex-shrink: 0;
}

audio-player-on-playback-info-pane [holds="links"] {
    display: flex;
    gap: 0.5em;
    justify-content: end;

    /* flex-direction: column;
    gap: 0.25em;
    flex-shrink: 0; */
}

audio-player-on-playback-info-pane [holds="link-container"] {
    display: flex;
    align-items: center;
    gap: 0.35em;
}

audio-player-on-playback-info-pane [holds="link-icon"] {
    width: 1em;
    display: flex;
    justify-content: center;
    align-items: center;
}

audio-player-on-playback-info-pane [holds="link-icon"] img {
    width: 100%;
    height: 100%;
    pointer-events: none;
}

audio-player-on-playback-info-pane [holds="link"] {
    color: inherit;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Hide info button in compact view.
   Use visibility:hidden not display:none — the visual-state attribute is
   toggled every drag frame at the midpoint, so display:none would trigger
   a layout recalculation exactly there (the "later, shorter" iOS lag). */
audio-player[visual-state="compact"] audio-player-on-playback-info-container>audio-player-info {
    visibility: hidden;
    pointer-events: none;
}

/* Hide info button when no track is selected for playback. */
audio-player:not(:has(audio-title[on-playback])) audio-player-on-playback-info-container>audio-player-info {
    visibility: hidden;
    pointer-events: none;
}

audio-player-on-playback-title {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

audio-player-on-playback-artist {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

audio-player-on-playback-title,
audio-player-on-playback-artist {
    min-width: 0;
}

audio-player[playback="on-play"] audio-player-controls audio-player-playback-controls audio-player-play {
    opacity: 0;
    scale: 0.5;
    pointer-events: none;
}

audio-player[playback="on-pause"] audio-player-controls audio-player-playback-controls audio-player-pause {
    opacity: 0;
    scale: 0.5;
    pointer-events: none;
}

audio-player audio-player-controls audio-player-playback-controls audio-player-play,
audio-player audio-player-controls audio-player-playback-controls audio-player-pause {
    transition: opacity 0.2s ease, scale 0.2s ease;
}

audio-player audio-player-controls audio-player-playback-controls audio-player-play-pause-group {
    position: relative;
    height: 2em;
    width: 2em !important;
}

audio-player audio-player-controls audio-player-playback-controls audio-player-play-pause-group>* {
    position: absolute;
    inset: var(--button-padding);
    display: flex;
    justify-content: center;
    align-items: center;
}

audio-player [data-audioplayer-icon] {
    display: block;
    pointer-events: none;
    width: var(--icon-size);
    height: var(--icon-size);
    flex: 0 0 auto;
}

/* Transport glyphs deliberately fill the 2em content box. Their owning
   controls add --button-padding, preserving the original 3em touch target.
   Explicit width and height avoid browser-dependent sizing of replaced IMG
   elements inside the absolutely positioned play/pause stack. */
audio-player audio-player-controls audio-player-playback-controls
    :is(audio-player-previous, audio-player-play, audio-player-pause, audio-player-next)
    > [data-audioplayer-icon] {
    width: var(--transport-icon-size);
    height: var(--transport-icon-size);
}

audio-player-controls {
    display: block;
}

/* Square viewports use the wide layout too. Keep the existing DOM so
   playback, info, and swipe handlers continue to own the same elements. */
@media (min-aspect-ratio: 1/1) {
    audio-player {
        --wide-cover-size: min(18em, calc(100svh - 5em), calc((100vw - 8em) / 2));
        /* The right column starts this far below the cover's top edge
           instead of flush with it, so the title has breathing room
           above it. The player centers its two columns, so the slack
           below the (12.5em tall) control column is half its height
           difference to the cover — drop further than that and the
           RAW / LEVEL MATCH row leaves the player. Short landscape
           viewports shrink the cover until there is no slack left, so
           the drop tapers to 0 there instead of clipping.
           The drag keyframes in gestures.js repeat the custom property
           by name — keep the two in sync. */
        --wide-column-drop: clamp(0px, calc((var(--wide-cover-size) - 12.5em) / 2), 2rem);
        box-sizing: border-box;
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
        width: min(42em, calc(100vw - 2rem));
        max-width: calc(100vw - 2rem);
        position: fixed;
        left: 50%;
        transform: translateX(-50%);
        /* The player follows navigation in the DOM; share its layer so
           expanded controls stay reachable on short landscape screens. */
        z-index: 2147483647;
    }

    audio-player-cover-swipe-layer {
        z-index: 2147483647;
    }

    audio-player-main,
    audio-player-on-playback {
        display: contents;
    }

    audio-player-on-playback-cover {
        width: var(--wide-cover-size);
        margin-bottom: 0;
    }

    audio-player-on-playback-info-container {
        position: absolute;
        top: calc(1.5em + var(--wide-column-drop));
        left: calc(var(--wide-cover-size) + 4em);
        width: calc(100% - var(--wide-cover-size) - 6em);
    }

    audio-player-controls {
        flex: 0 0 auto;
        width: calc(100% - var(--wide-cover-size) - 2em);
        margin-top: 3em;
        min-width: 0;
        /* A transform, not extra margin: the player centers its flex
           children, so margin-top would only move the column by half of
           what it adds — and it would feed back into the measured
           full-size height the drag range is derived from. */
        transform: translateY(var(--wide-column-drop));
    }
}

audio-player audio-player-controls audio-player-playback-controls {
    display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    margin: 1.5em 0.75em;
}

audio-player audio-player-controls audio-player-playback-controls>* {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: var(--button-padding);
    height: 2em;
    width: auto;
}

audio-player audio-player-controls audio-player-playback-options {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin: 0.25em 1em;
}

audio-player audio-player-controls audio-player-playback-options>* {
    display: flex;
    justify-content: center;
    align-items: center;
    padding: var(--button-padding);
    height: 1em;
    width: 1em;
}

audio-player audio-player-controls audio-player-playback-options>* span {
    display: block;
    text-transform: uppercase;
    text-align: center;
    font-size: 0.35em;
    line-height: 1.5;
    overflow: hidden;
}

audio-player [holds] {
    cursor: default;
}

audio-player audio-player-controls audio-player-progress-bar-section {
    position: relative;
    touch-action: none;

    margin: 0.5em 0;

    transform: scale(1);
    transition: transform 0.3s ease;
}

audio-player audio-player-controls audio-player-progress-bar-section[seek="true"] {
    transform: scale(1.025);
    transition: transform 0.5s ease;
}

audio-player audio-player-controls audio-player-progress-bar {
    width: 100%;
    height: 0.25em;
    position: relative;
    overflow: hidden;
}

audio-player audio-player-controls audio-player-progress-bar audio-player-buffer-progress {
    width: 100%;
    height: inherit;
    position: absolute;
    top: 0;
    left: 0;
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 0.3s ease;
    will-change: transform;
}

audio-player audio-player-controls audio-player-progress-bar audio-player-progress {
    width: 100%;
    height: inherit;
    position: relative;
    transform: scaleX(0);
    transform-origin: left center;
    will-change: transform;
}

audio-player audio-player-controls audio-player-progress-bar-section[seek="false"] audio-player-progress {
    /* transition: width 0.75s ease; */
}

audio-player audio-player-controls audio-player-elapsed-time,
audio-player audio-player-controls audio-player-remaining-time,
audio-player audio-player-controls audio-player-total-time {
    position: absolute;
    font-size: 0.5em;
    bottom: -1rem;
}

audio-player audio-player-controls audio-player-elapsed-time {
    left: 0;
    /* flex so the ::after spinner centers vertically with the text
       regardless of the text's baseline / line-height. */
    display: inline-flex;
    align-items: center;
}

audio-player[waiting] audio-player-controls audio-player-elapsed-time::after {
    content: '';
    display: inline-block;
    width: 0.75em;
    height: 0.75em;
    margin-left: 0.5em;
    box-sizing: border-box;
    flex-shrink: 0;
    animation: spinner 0.7s linear infinite;
}

@keyframes spinner {
    to {
        transform: rotate(360deg);
    }
}

audio-player audio-player-controls audio-player-remaining-time,
audio-player audio-player-controls audio-player-total-time {
    right: 0;
    display: none;
}

audio-player audio-player-controls [time-display="remaining-time"] audio-player-remaining-time,
audio-player audio-player-controls [time-display="total-time"] audio-player-total-time {
    display: block;
}

audio-player audio {
    display: none;
}

audio-player audio-player-titles {
    position: absolute;
    inset: 0;
    overflow: hidden;
    z-index: 1;
    background: inherit;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s ease;
    display: flex;
    flex-direction: column;
}

audio-player[list="on"] audio-player-titles {
    opacity: 1;
    pointer-events: auto;
    overflow-y: auto;
}

@media (min-aspect-ratio: 1/1) {
    /* display: contents removes main's containing block in wide mode.
       Keep the list in the cover column, leaving the controls usable. */
    audio-player audio-player-titles {
        inset: 1.5em auto 0.5em 2em;
        width: var(--wide-cover-size);
        background: transparent;
    }

    audio-player[list="on"] audio-player-on-playback-cover {
        opacity: 0;
        pointer-events: none;
    }

    audio-player[list="on"] audio-player-on-playback-info-container > audio-player-info {
        visibility: hidden;
        pointer-events: none;
    }

    /* audio-player audio-player-titles audio-title {
        margin-block: 0.5rem;
    } */
}

audio-player audio-title[list-group-start]::before {
    content: attr(list-group-start);
    display: block;
    font-size: 0.7em;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    padding: 0.75em 0.5em 0.25em;
}

audio-player audio-title[list-role="history"] {
    opacity: 0.5;
}

audio-player audio-title {
    display: block;
    flex-shrink: 0;
    position: relative;
    border: 1px solid transparent;
    /* margin: 1.5rem 0; */
}

/* audio-player [holds="list-section"] audio-title {
    transform: scale(0.8);
}
audio-player [holds="list-section"] audio-title[on-playback] {
    transform: scale(1);
} */

audio-player audio-title audio-player-details {
    display: flex;
    align-items: center;
    /* padding: 0em 0.5em; */
    margin: 0.25em 0;
}

audio-player audio-title audio-player-details audio-player-title-and-artists {
    min-width: 0;
    overflow-wrap: anywhere;
    padding: 0.25em 0em;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    justify-content: flex-start;
    row-gap: 0.0625em;
    flex-grow: 1;
}

audio-player audio-title audio-player-details audio-player-artists {
}

audio-player audio-title audio-player-details audio-player-artwork {
    flex-shrink: 0;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-right: 0.5em;
    position: relative;
}

audio-player audio-title audio-player-details audio-player-artwork img {
    /* Fixed square box so a portrait/landscape cover doesn't change
       the title row height. object-fit: contain keeps the full cover
       visible, centered inside the square. */
    height: 3em;
    width: 3em;
    object-fit: contain;
    overflow: hidden;
}

audio-player audio-title audio-player-details audio-player-artwork audio-player-play-overlay {
    position: absolute;
    inset: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    /* cursor: pointer; */
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.15s ease;
}

audio-player audio-title audio-player-details audio-player-artwork audio-player-play-overlay > [data-audioplayer-icon] {
    width: var(--artwork-play-icon-size);
    height: var(--artwork-play-icon-size);
}

@media (hover: hover) {
    audio-player audio-title:hover audio-player-details audio-player-artwork audio-player-play-overlay {
        opacity: 1;
        pointer-events: auto;
    }
}

audio-player audio-title audio-player-info {
    height: 1em;
    padding: var(--button-padding);

    display: flex;
    align-items: center;
    justify-content: center;
}

audio-player final-audio,
audio-player raw-audio {
    display: none;
    pointer-events: none;
}

audio {
    display: none;
    pointer-events: none;
}

audio-player audio-title audio-player-details {
    margin: 0 0;
    /* margin-bottom: 2em; */
    /* margin-top: 5em; */
    /* Calculate margin-bottom in px of element above and margin-top in px of element below. As new element is added in between jump is happening of the spacing between the element as the margins don't overlap anymore. The jump will be exactly the overlap, so Math.min(margin-top, margin-bottom). */
}

audio-player-titles {
    position: relative;
}

audio-player audio-title [holds="info-content"] {
    font-size: 0.75em;
    line-height: 1rem;
    /* TESTING renderFluid ... */
    /*top: -10em;*/
    /* container-type: inline-size; */

    /* padding: 1em 0; */
    /* margin: 3em 0; */
    /* margin-bottom: 3em; */
    /* margin-top: 8em; */
    /* margin-top: 8em; */

    /* !!!!! when no margin on animated element jump when animation-direction is reverse and element is animated out.
    The issue is in case 0.
    Not changing the marginBottomValue fixed the issue. Why?
    Maybe because element is animated to -100% in reverse but also has size 0.3 => so when its removed this creates the jump! Check! => Nope!
    When "if Case 0" is removed the jump is at the end of the animation instead of the start.

    Something is off in the margin Collapse compensation! Either the computation itself or the handling logic.
    */
    /*margin-bottom: 8em;*/

    /*padding-top: 2em;
    padding-bottom: 4em;*/

    /* border: 20px solid; */

    /* margin-left: 1em;
    margin-right: 2em;
    margin-left: 2em;
    margin-right: 4em;
    margin-left: 4em;
    margin-right: 8em; */

    /* margin-bottom: 10em;
    margin-top: 15em; */

    /* position: absolute;
    top: 0;
    left: 0;
    width: 10em; */

    /* ... Testing renderFluid */
}

audio-title {
    /* display: flex; */
    /* flex-direction: column; */
    position: relative;
}

audio-player audio-title [holds="info-content"][show-credits] {
    /* Testing renderFluid ... */
    animation: backInDown 0.2s ease 1;

    /* animation: backInDown 5s ease 1; */
    animation-fill-mode: forwards;
    /* animation-direction: reverse; */
    /* animation:
        backInDown 5s ease 1,
        backInLeft 2s ease 2; */
    /* animation: backInUp 2s ease 1; */
    /* animation: backInLeft 2s ease 1; */
    /* animation: backInRight 2s ease 1; */

    /* ... Testing renderFluid */

    /* !!!!! when no margin on animated element jump when animation-direction is reverse and element is animated out.
    The issue is in case 0.
    Not changing the marginBottomValue fixed the issue. Why?
    Maybe because element is animated to -100% in reverse but also has size 0.3 => so when its removed this creates the jump! Check! => Nope!
    When "if Case 0" is removed the jump is at the end of the animation instead of the start.

    Something is off in the margin Collapse compensation! Either the computation itself or the handling logic.
    */
    animation: var(--credits-animation);
    /*animation-direction: reverse;*/
    /*animation: bottomToTop 3s ease 1 forwards;*/
    /*animation-direction: reverse;*/
}

audio-player audio-title [holds="info-content"][hide-credits] {
    /* Testing renderFluid ... */
    /*animation: backOutUp 0.2s ease 1;

    animation: backOutUp 5s ease 1;
    animation-fill-mode: forwards;

    animation: backInDown 5s ease 1;
    animation-fill-mode: forwards;
    animation-direction: reverse;*/

    /* animation: backOutDown 2s ease 1; */
    /* animation: backOutRight 2s ease 1;
    animation: backOutLeft 2s ease 1; */

    /* ... Testing renderFluid */

    /* !!!!! when no margin on animated element jump when animation-direction is reverse and element is animated out.
    The issue is in case 0.
    Not changing the marginBottomValue fixed the issue. Why?
    Maybe because element is animated to -100% in reverse but also has size 0.3 => so when its removed this creates the jump! Check! => Nope!
    When "if Case 0" is removed the jump is at the end of the animation instead of the start.

    Something is off in the margin Collapse compensation! Either the computation itself or the handling logic.
    */

    animation: var(--credits-animation);
    animation-direction: reverse;
    /*animation: bottomToTop 3s ease 1;*/
    /*animation: var(--credits-animation-out);*/
}

audio-player audio-title [holds="credits"] {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.5rem 0.5rem;
    align-items: start;
    justify-items: start;
    padding: var(--credits-padding);
}

@container (max-width: 10em) {
    audio-player audio-title [holds="credits"] {
        grid-template-columns: 1fr;
    }
}

audio-player audio-title [holds="credit"] {
    display: flex;
    flex-direction: column-reverse;
    font-size: 0.75em;
}

audio-player audio-title [holds="credit-value"] {}

audio-player audio-title [holds="credit-key"] {
    font-size: 0.75em;
}

audio-player audio-title [holds="releasedate"] {
    text-align: right;
    padding: var(--credits-padding);
    font-size: 0.5em;
}

audio-player audio-title [holds="links"] {
    display: flex;
    justify-content: flex-end;
    column-gap: 0.75em;
    padding: var(--credits-padding);
}

audio-player audio-title [holds="link-container"] {
    display: flex;
    column-gap: 0.25em;
}
audio-player audio-title [holds="link-container"] a {
    color: inherit;
}

audio-player audio-title [holds="link-icon"] {
    display: flex;
    align-items: center;
    justify-content: center;
}

audio-player audio-title [holds="link-icon"] img {
    height: 1em;
}

/* ── Volume bar ────────────────────────────────────────────────────── */
audio-player-volume {
    display: flex;
    align-items: center;
    position: relative;
    /* padding: 0 0.9em 0 0.6em; */
    padding: 0 18px 0 12px;
    touch-action: none;
    margin: 1em 0;
}

audio-player-volume audio-player-volume-icon {
    position: absolute;
    display: flex;
    align-items: center;
}

audio-player-volume audio-player-volume-icon:first-child {
    left: 0;
}

audio-player-volume audio-player-volume-icon:last-child {
    right: 0;
}

audio-player-volume audio-player-volume-icon [data-audioplayer-icon] {
    width: var(--volume-icon-size);
    height: var(--volume-icon-size);
}

audio-player-volume audio-player-volume-bar {
    flex: 1;
    height: 0.25em;
    position: relative;
    overflow: hidden;
    cursor: pointer;
}

audio-player-volume audio-player-volume-level {
    width: 100%;
    height: inherit;
    transform: scaleX(1);
    transform-origin: left center;
    will-change: transform;
}

/* ── Compact visual state ──────────────────────────────────────────── */
audio-player[visual-state="compact"] audio-player-controls {
    pointer-events: none;
}

audio-player[visual-state="compact"] audio-player-controls audio-player-playback-controls audio-player-play-pause-group,
audio-player[visual-state="compact"] audio-player-controls audio-player-playback-controls audio-player-next {
    pointer-events: auto;
}
