.application-body.application-music {
    /* Styling for the Music application. Name-spaced under */
    /* #application-body-music (compiled into the per-app block in */
    /* assets/base/applications/_registry.scss by */
    /* scripts/compile_sass.py:generate_registry_scss()). */
    /* The DB App.css field is appended on top at compile time. */

    & {
        --app-colour: var(--app-colour-music, #00C300);
    }

    /* Mid-inject, hide the stale content so the swap doesn't flash the */
    /* old listing under the new one (mirrors #app-content-documents). */
    #app-content-music {
        &.injecting {
            opacity: 0;
        }
    }

    /* --- Track tiles ------------------------------------------------ */
    /* Album art is a square, unlike the office apps' portrait page       */
    /* previews, so pin the aspect ratio on both the real cover and the   */
    /* placeholder — otherwise a coverless track collapses to a short     */
    /* icon card and the row goes ragged next to its neighbours (see the  */
    /* preview_placeholder note in partials/pat_link_list.html).          */
    .music-track-list {
        .preview.album-art {
            aspect-ratio: 1 / 1;
            /* Large art carries the SMALL radius (requested) — the tiles read
               as sleeves, not as cards. */
            border-radius: var(--border-radii-small, 4px);
            display: flex;
            align-items: center;
            justify-content: center;
            overflow: hidden;
            background: var(--quaive-colour-gray-95, #f0f0f0);
        }

        .preview.album-art img {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        /* Coverless album/track: a pale app-colour square with a bleached    */
        /* music-notes glyph CENTRED on it (requested 2026-07-31). The first  */
        /* version masked the WHOLE element into the note shape — light-green */
        /* note on the light-green page, i.e. tiles that read as empty. Now   */
        /* the square keeps a ground and only the ::after is masked. The mask */
        /* (not a background-image) is what lets a data-URI glyph take        */
        /* --app-colour; pseudo-elements don't break :empty.                 */
        .preview.album-art:empty {
            background-color: color-mix(in srgb, var(--app-colour) 14%, white);
            position: relative;
        }

        .preview.album-art:empty::after {
            content: '';
            position: absolute;
            inset: 28%;
            background-color: var(--app-colour);
            opacity: 0.45;
            -webkit-mask: var(--music-note-mask) center / contain no-repeat;
            mask: var(--music-note-mask) center / contain no-repeat;
        }
    }

    /* The note used by the placeholders, declared once on the app root. On */
    /* the & level, NOT on a ".application-music" selector: the registry     */
    /* compiler wraps every rule in this file under                          */
    /* ``.application-body.application-music``, so a class selector here     */
    /* compiles to a descendant match that hits nothing — the var never      */
    /* resolved and the glyph silently rendered mask:none. The drawing is  */
    /* shifted 1.2 units left of the viewBox centre: measured on the       */
    /* rasterized ink, that is where extent-centre and mass-centre offsets */
    /* cancel — geometrically centred, the up-right beam made it LEAN      */
    /* right (reported 2026-07-31). Keep in sync with icon/icon.svg.       */
    & {
        /* The shared coverless-tile face reads this. Same drawing as
           --music-note-mask below, which the album/table placeholders still use
           directly. */
        --kik-audio-tile-glyph: var(--music-note-mask);
        --music-note-mask: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.8" cy="17.5" r="2.75"/><circle cx="16.8" cy="15.5" r="2.75"/><path d="M8.55 17.5V6.25l11-2v11.25"/><path d="M8.55 9.75l11-2"/></svg>');
    }

    /* --- Albums grid -------------------------------------------------- */
    /* The grid itself is SHARED (``.kik-cover-grid`` in
       static/libraries/kikaron/link-list/link-list.css) — the Podcasts app's Favourites
       renders the identical gallery of square art. */

    /* THREE SLEEVES ACROSS A PHONE (Daniel, 2026-09-23: "make the album art
       slightly smaller so that three across fit on this screen size").
     *
     * The shared style-preview-cards grid steps 2 → 3 → 6 at 350 / 530 / 890px of the
     * card box's own width (patterns/components/table), so a phone — call it
     * 390px of content inside a 412px screen — sits in the 2-up band and draws a
     * 190px sleeve. A record sleeve is recognised at a glance from its artwork,
     * not read like a document thumbnail, so it can afford to be smaller than the
     * shared default gives it: three across put it at about 120px, still a
     * legible cover and a third more of the collection on screen.
     *
     * BOUNDED AT THE TOP, and that is the whole reason this is a range rather
     * than a lower step point. Above 530px the shared grid says 3-up and above
     * 890px it says 6-up; an open-ended rule here would out-rank the 6 and leave
     * a desk pane at three enormous sleeves. Inside the band this is the only
     * thing that speaks; outside it, the component is.
     *
     * 360px is where the third card stops being worth it: below that the sleeves
     * fall under ~110px and the titles beneath them have nothing to say. The
     * small phones (a 320px screen, an SE at 375) therefore keep the 2-up grid.
     *
     * The albums table only (music.views builds it as ``music-album-card-table``).
     * The track cards are a different listing and keep the shared steps. */
    /* THE BASIS SUBTRACTS THE GAPS, NOT ONE GUTTER. The card box is a flex row
       with `gap: var(--kik-card-gap)` (16px), and three cards across it leave TWO
       of those gaps: a card is (100% - 2 gaps) / 3. Written the old way —
       `100% / 3 - var(--pat-x-list-gutter)`, one 10px gutter off each card — the
       three came to 488px in a 486px box and the third wrapped, so the grid stayed
       two across with a third of the row standing empty (Daniel, 2026-09-23: "this
       was supposed to be three across"). The component itself moved to this form;
       this rule is stated in the same vocabulary so the two cannot drift again. */
    @container preview-cards (360px <= width < 530px) {
        .music-album-card-table tbody tr {
            flex-basis: calc((100% - 2 * var(--kik-card-gap)) / 3);
        }
    }

    /* NEVER ONE SLEEVE ACROSS (Daniel, 2026-09-27: "go 2 or 3 across for albums
       when the grid is as narrow as in the reduced document body"). Below 350px
       the shared grid gives each card the whole row, and that is exactly the width
       of the list standing beside an album open in the quick look (280-380px): one
       sleeve the size of the column, two albums to a screen. A sleeve still reads
       at ~130px, so the albums keep two across there — from a small phone down to
       the narrowest quick-look list — and step to three at 360px as above. */
    @container preview-cards (width < 360px) {
        .music-album-card-table tbody tr {
            flex-basis: calc((100% - 1 * var(--kik-card-gap)) / 2);
        }
    }

    /* --- Album page -------------------------------------------------- */
    /* Apple Music's album layout: the cover leads, at a size that makes it   */
    /* the subject of the page rather than a thumbnail, with the album's      */
    /* identity set beside it and the running order underneath.              */
    .music-album-header {
        /* THE HEADER IS THE CONTAINER, NOT THE WINDOW (Daniel, 2026-09-16: "use
           @container instead of media query"). The app runs in a resizable pane,
           so a vw cap measures a box the sleeve does not live in — two panes of
           different widths would cap it identically. Sizing off this header's own
           inline size is the same intent, measured against the right box. */
        container-type: inline-size;
        container-name: music-album-header;
        display: flex;
        gap: 2rem;
        align-items: flex-start;
        flex-wrap: wrap;
        margin-bottom: 2rem;
    }

    .music-album-cover {
        /* BIG ENOUGH TO CARRY A TWO-LINE TITLE BESIDE IT (Daniel, 2026-09-16).
           The identity column runs title, genre, the play button and the track
           count; at 17rem a title that wrapped left the words taller than the
           sleeve they stand next to, which reads as the art having shrunk rather
           than the title having grown. The cap keeps it in proportion on a narrow
           pane, where the header wraps and the cover is the whole width anyway. */
        width: 21rem;
        max-width: min(100%, 45cqi);
        aspect-ratio: 1 / 1;
        flex: none;
        border-radius: var(--border-radii-small, 4px);
        overflow: hidden;
        background: var(--quaive-colour-gray-95, #f0f0f0);
        box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 25%);
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .music-album-cover img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }

    /* The gallery anchor is a pure wrapper — it must not disturb the cover's
       box, and it opens a lightbox rather than navigating. */
    .music-album-cover > a {
        display: block;
        width: 100%;
        height: 100%;
        cursor: default;
    }

    .music-album-cover svg {
        width: 40%;
        height: 40%;
        opacity: 0.35;
        color: var(--app-colour);
    }

    .music-album-identity {
        min-width: 0;
        display: flex;
        flex-direction: column;
        gap: 0.35rem;
        padding-top: 0.5rem;
    }

    .music-album-title {
        font-size: 2.25rem;
        font-weight: 700;
        line-height: 1.1;
        margin: 0;
    }

    /* The artist is the one accent-coloured line on the page — it reads as    */
    /* the album's author, the way Apple Music sets it.                        */
    .music-album-artist {
        font-size: 1.5rem;
        font-weight: 600;
        line-height: 1.2;
        margin: 0;
        color: var(--app-colour);
    }

    .music-album-meta,
    .music-album-summary {
        margin: 0;
        font-size: 0.85rem;
        text-transform: uppercase;
        letter-spacing: 0.04em;
    }

    .music-album-actions {
        margin: 0.75rem 0 0.25rem;
    }

    /* Non-default buttons on the action bars (Shuffle beside Play) get a
       more opaque white so they hold up over the hero image. Buttons paint
       their own surface, so the hero's text halo is dropped here. */
    .music-album-actions.pat-button-bar {
        --pat-button-background-colour: #ffffffa1;
        --pat-button-background-colour-hover: #fffc;
        text-shadow: none;
    }
    /* The album's running order is the SHARED ``.audio-tracks`` table
       (static/libraries/patterns/table/table.css) — the number column that becomes a play
       control on hover, the name column, the tabular duration and the functions
       column. Podcasts renders the identical table for its episodes.

       What stays below is only what is Music's own: the level meter that takes
       the play glyph's place on the sounding row. */


    /* The now-playing level meter used to live here, and a copy of it lived in
       the Podcasts app's stylesheet. It is one instrument shared by both apps, so
       it now lives once with the components it sits inside:
       static/libraries/patterns/table/table.css for the table contexts (the number column's
       gutter, a songs table's cover cell) and static/libraries/kikaron/link-list/link-list.css
       for a list row's cover art. */


    /* The name and duration columns are the shared table's too. */
    /* The genre list face: stacked per-page table chunks must align, so the
       layout is fixed and every side column has an explicit width — the name
       column absorbs the rest. */
    /* The songs TABLE is not styled here any more. Its layout, its clipping, its
       leading media cell and that cell's overlays all moved into the shared
       component (static/libraries/patterns/table/table-upstream.css,
       the MEDIA TABLES block) the moment Radio rendered the identical table: three
       apps owning pieces of one table is how the column widths drifted apart and
       how the cover cell ended up padded differently in each. What is left below
       is what is genuinely Music's — the note-glyph placeholder, the sleeve's
       hairline, and the artist/album columns nothing else has. */

    /* The songs table's column widths are grid tracks now
       (libraries/patterns/table/table.css, COLUMN GEOMETRY); a cell's own width
       means nothing in a grid row. */

    /* The credited artist. Both selectors on purpose: the table's cells come from
       pat_table, whose ``links`` cell renders the name as the link's own text —
       there is no inner span to clamp. The span still exists in the hand-built
       lists elsewhere in the app, so it keeps its rule. */
    .music-track-artist-name,
    .music-track-artist-link {
        display: block;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* EVERY COLUMN SITS ON THE ROW'S MIDDLE (Daniel, 2026-09-16: "vertically
       center the album name"). The component's data cells are ``vertical-align:
       top`` — right for a table of paragraphs — while the name cell and the cover
       are pinned to the middle, so a one-line album or duration hung a couple of
       px above the title it belongs to. The album link centres its own contents
       already; what was off was the CELL it sits in. */
    .music-track-table tbody td:is(.music-track-artist, .music-track-album,
                                   .audio-track-duration, .functions) {
        vertical-align: middle;
    }

    /* …and it ELLIPSISES (Daniel, 2026-09-16: "and truncate where it doesn't
       fit"). It used to be a flex box, and text-overflow has nothing to act on
       there: a flex container's text becomes an anonymous flex item, which happily
       overflows and gets clipped mid-glyph by the cell — a long album title was
       cut through the middle of a letter with no ellipsis to say so. A block box
       truncates its own inline content the way the artist column already does; it
       carried no leading glyph that needed the flex row. */
    .music-track-album-link {
        display: block;
        min-width: 0;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        color: inherit;
        text-decoration: none;
    }

    /* The leading COVER cell — its geometry, its hover veil and the meter slot —
       is NOT here either. It moved to the shared table in
       static/libraries/patterns/table/table.css (``td.audio-track-cover``, with
       ``.music-track-cover`` kept as an alias so this app's markup is unchanged)
       the moment Radio rendered the identical column: two apps drawing the same
       cell is the component's business, and the same drift that split the column
       widths across two files would have split the veil next. */

    /* The square, its crop and its hairline are the component's. Music adds the
       sleeve's own border and the coverless note. */
    .music-track-cover-img {
        border: 1px solid rgb(0 0 0 / 0.12);
    }

    .music-track-cover-placeholder {
        background-color: color-mix(in srgb, var(--app-colour) 14%, white);
    }

    /* …but only while there is nothing over it (Daniel, 2026-09-16: "i see an
       icon over the covers"). The square is drawn ALWAYS and the artwork laid on
       top of it (partials/pat_table_cell.html), so this glyph — an ``::after``,
       and therefore painted after the img inside the same box — sat on the sleeve
       it was standing in for. ``:has()`` rather than a second class because the
       img removes ITSELF when its URL 404s (``onerror``), and the note has to come
       back at that moment without anything re-rendering the row. */
    .music-track-cover-placeholder:has(> img)::after {
        content: none;
    }

    .music-track-cover-placeholder::after {
        content: '';
        position: absolute;
        inset: 20%;
        background-color: var(--app-colour);
        opacity: 0.45;
        -webkit-mask: var(--music-note-mask) center / contain no-repeat;
        mask: var(--music-note-mask) center / contain no-repeat;
    }

    .music-track-album-name,
    .music-track-album-link {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }

    /* The album and artist links NAVIGATE (unlike the row, which plays in
       place) — an underline on hover says so. Pointer devices only.
       Set on the LINK, not on an inner span: pat_table's link cells have no
       inner span (see the artist rule above). */
    @media (hover: hover) {
        .music-track-album-link:hover,
        .music-track-artist-link:hover {
            text-decoration: underline;
        }
    }

    .music-track-artist-link {
        display: block;
        min-width: 0;
        color: inherit;
        text-decoration: none;
    }


    /* The sidebar count badges — the soft wash and the alignment that pins the
       well pills to the same trailing edge as the section-nav pills — are SHARED
       (static/libraries/kikaron/sidebar/sidebar.css). They were keyed on this app's ids, which
       is exactly why Podcasts, rendering the identical sidebar, had its well
       counters drift out of line. */

    /* The songs table's stacked-batch rules lived here: a margin reset so
       successive batch tables read as one list, and a fade-in for the rows that
       arrived by scrolling. Both went with the film-roll pattern itself — the
       table is a pat_table now, its batches land in one tbody, and the component
       fades the arriving rows (.pat-table tr.lazy-loaded, table.css). */

    /* Rows play/act in place — nothing here reads as a classic link. The
       hover variant mirrors the table component's own deep selector
       (a[href]:not(.icon):not(.current):hover { cursor: pointer }) so the
       app-scoped default outweighs it. */
    .audio-tracks a {
        cursor: default;
    }

    @media (hover: hover) {
        .audio-tracks a[href]:hover,
        .audio-tracks a[href]:not(.icon):not(.current):hover {
            cursor: default;
        }
    }

    /* The row link, its two text lines, the phone duration slot and the
       functions hover-reveal are all the shared ``.audio-tracks`` table's
       (static/libraries/patterns/table/table.css). */

    /* The now-playing highlight. The adapter puts .current on the SONG NAME
       itself (requested) and .music-now-playing on the row element, moving
       both along with playback (skip, auto-advance, shuffle) and re-asserting
       them after a list re-injects. */
    .item-title.current,
    .audio-track-title.current,
    .music-player-title.current,
    .music-now-playing > a .item-title,
    a.music-now-playing .audio-track-title,
    .audio-track.music-now-playing .audio-track-title {
        color: var(--app-colour);
        font-weight: 700;
    }

    /* On the tinted playing row (table.css tr:has(.current) — 10% accent
       ground, accent ink) the green name tint already matches; only the
       artist line dims to a shade of the accent. */
    .audio-tracks tr:has(.current) .audio-track-byline {
        color: color-mix(in srgb, var(--colour-accent, var(--app-colour)) 75%, transparent);
    }

    /* The hub-wide floating player (templates/partials/kik_media_player.html)
       replaced the app-local bar; its styling lives in static/kikaron/css/
       base.css with the shell chrome. Only the now-playing row highlight stays
       app-side, keyed by the shared adapter (kik-music-play.js). */
    /* --- Player (the no-JS fallback page) ----------------------------- */
    .music-player {
        display: flex;
        flex-direction: column;
        gap: 1.5rem;
    }

    .music-player-stage {
        container-type: inline-size;
        container-name: music-player-stage;
        display: flex;
        gap: 1.5rem;
        align-items: flex-end;
        flex-wrap: wrap;
    }

    .music-player-cover {
        width: 14rem;
        max-width: min(100%, 40cqi);
        aspect-ratio: 1 / 1;
        border-radius: var(--border-radii-small, 4px);
        overflow: hidden;
        background: var(--quaive-colour-gray-95, #f0f0f0);
        display: flex;
        align-items: center;
        justify-content: center;
        flex: none;
    }

    .music-player-cover img {
        width: 100%;
        height: 100%;
        object-fit: cover;
    }

    .music-player-cover svg {
        width: 40%;
        height: 40%;
        opacity: 0.35;
        color: var(--app-colour);
    }

    .music-player-meta {
        display: flex;
        flex-direction: column;
        gap: 0.25rem;
        min-width: 0;
    }

    .music-player-meta .music-player-title {
        font-size: 2rem;
        font-weight: 700;
        line-height: 1.15;
        margin: 0;
    }

    .music-player-meta .music-player-byline {
        opacity: 0.75;
    }

    .music-player audio {
        width: 100%;
    }

    /* Up next — a plain list under the player, Spotify-style. */
    .music-up-next {
        margin-top: 0.5rem;
    }

    /* The "Up next" heading is a house canvas-separation-header now — no
       custom face of its own. */


    /* Track rows play IN PLACE (the floating player) — they don't navigate,
       so they don't get the link cursor (house rule: pointer only on things
       that look like links). The Albums grid tiles DO navigate to the album
       page and keep their pointer. */
    .music-track-list:not(.music-album-grid) .pat-link-list li a:not(.pat-lazy-load),
    .audio-tracks a.audio-track-link {
        cursor: default;
    }

    /* No hover underline on those rows either (requested): the underline is
       the vendored list's navigation affordance, and these rows play in
       place. !important for the same reason as the shared a:not([href]) rule
       in link-list.css — the vendored underline sits behind deep native
       nesting whose specificity can't be out-ranked cleanly. */
    @media (hover: hover) {
        .music-track-list:not(.music-album-grid) .pat-link-list li a:not(.pat-lazy-load):hover,
        .music-track-list:not(.music-album-grid) .pat-link-list li a:not(.pat-lazy-load):hover .item-title {
            text-decoration: none !important;
        }
    }

    /* IN A ROW OR A CARD TABLE the tile is a MARK, not a face: the component's
       media size, cropped. Without this it was left to size itself — and since the
       tile holds a full-resolution cover, a playlist's rows came out as one huge
       painting each (Daniel, 2026-09-16: "the styling is a mess"). The preview
       cards below are the other case, where the art IS the card. */
    @container pat-table-when(shape: table | bare | cards | list) {
        .pat-table td.item-media > .kik-audio-tile {
            flex: 0 0 auto;
            box-sizing: border-box;
            width: var(--kik-table-media-size, 2.4rem);
            height: var(--kik-table-media-size, 2.4rem);
            aspect-ratio: 1 / 1;
            overflow: hidden;
        }

        .pat-table td.item-media > .kik-audio-tile > img {
            display: block;
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
    }

    /* A SLEEVE IS SQUARE, AND THE COMPONENT ASKS. The card grid sizes a
       preview from ``--ll-preview-ratio`` and falls back to 4/3 when nobody says —
       which is why every cover here came out landscape however the figure was
       restyled (Daniel, 2026-09-16, with the winning rule in hand). Answering the
       question is the fix; overriding the answer was not. Album art is square,
       everywhere in this app. */
    .pat-table {
        --ll-preview-ratio: 1 / 1;
    }

    /* …and the selection frame is cut to the same corner. One token, read by the
       sleeve above and by the card's wash/ring (libraries/kikaron/row-select), so
       the two cannot drift apart. */
    .pat-table {
        --kik-card-art-radius: var(--border-radii-small, 4px);
    }

    /* THE SLEEVES — the preview-card shape only (table-shape-preview-cards.css). */
    @container pat-table-when(shape: preview-cards) {
        .pat-table td.item-media figure.preview,
        .pat-table td.item-media > .kik-audio-tile {
            /* THE ART IS THE CARD'S FACE, so it takes the card's WIDTH and squares
               itself off that. Stating the ratio alone was not enough: with nothing
               to measure against, a figure sizes itself to the picture inside it and
               every sleeve came out a different shape (Daniel, 2026-09-16). Width
               first, ratio second, and the picture cropped into what that leaves. */
            grid-area: preview;
            display: block;
            box-sizing: border-box;
            width: 100%;
            height: auto;
            margin: 0 0 var(--pat-x-list-gutter, 10px);
            aspect-ratio: 1 / 1;
            border-radius: var(--kik-card-art-radius, var(--border-radii-small, 4px));
            overflow: hidden;
            background: var(--quaive-colour-gray-95, #f0f0f0);
        }

        .pat-table td.item-media figure.preview > img,
        .pat-table td.item-media > .kik-audio-tile > img {
            display: block;
            width: 100%;
            height: 100%;
            object-fit: cover;
        }

        /* A COVERLESS card's picture is the app's own note on a pale square — drawn
           in the same figure a cover would fill (``preview-mark``), so the two sit
           level in a row of sleeves and every rule that shapes a picture card applies
           to both. The note is bleached into the ground rather than printed on it:
           full-strength it reads as a symbol placed on the card, which is a different
           thing from a sleeve nobody has a photograph of. */
        .pat-table td.item-media figure.preview.preview-mark {
            display: grid;
            place-items: center;
            background-color: color-mix(in srgb, var(--app-colour) 8%, white);
        }

        .pat-table td.item-media figure.preview.preview-mark > svg {
            /* SMALL AND FAINT: a mark that says "no sleeve", not a logo stamped on
               the card (Daniel, 2026-09-16). The note is the app's green at a whisper,
               on a ground that is the same green fainter still — the two read as one
               quiet square rather than as a symbol on a plate. */
            width: 30%;
            height: 30%;
            opacity: 1;
            color: color-mix(in srgb, var(--app-colour) 38%, white);
        }

        /* AN ALBUM STILL ARRIVING from the catalogue (views._pending_album_items):
           on the shelf at once, dimmed, with a bar along the foot of its sleeve that
           fills as the files land (Daniel, 2026-09-27). The sleeve fills its square
           like a cover does; the bar sits inside it, on the art, not under the card.

           The ART and the WORDS are dimmed, not the row: opacity on the row would take
           the bar down with it, and nothing inside can undo an ancestor's opacity. The
           bar is the one thing on the card that is live, so it shows at full strength
           (Daniel, 2026-09-27). */
        .pat-table tbody tr.music-album-pending {
            td.item-media figure.preview.preview-mark > :not(.music-pending-bar),
            .link-title,
            .music-card-byline {
                opacity: 0.55;
            }

            td.item-media figure.preview.preview-mark {
                position: relative;
                display: block;

                > img {
                    display: block;
                    width: 100%;
                    height: 100%;
                    object-fit: cover;
                    /* Shown outright, at the dimmed strength. The grid fades a cover in when its own onload
                       marks it ``loaded``; a drawn mark has no such handler, and the
                       card is re-drawn every few seconds while it fills, so a fade
                       would hold it at nothing or flash it on every refresh. */
                    opacity: 0.55;
                    animation: none;
                }

                > svg {
                    position: absolute;
                    inset: 35%;
                }
            }
        }
    }

    .music-pending-bar {
        position: absolute;
        inset-inline: 8%;
        bottom: 8%;
        height: 5px;
        border-radius: 3px;
        background: rgb(255 255 255 / 85%);
        box-shadow: 0 0 0 1px rgb(0 0 0 / 12%);
        overflow: hidden;

        > span {
            display: block;
            height: 100%;
            background: var(--app-colour);
            transition: width 0.4s ease;
        }
    }

    /* --- Artist page (design A3) -------------------------------------- */
    /* The fanart background BLEEDS: it is absolutely positioned against
       .document-body (top/left/right 0), so it touches the page edges, sits
       behind the sticky toolbar's frost, and runs tall enough that its long
       fade only dies out behind the Albums section. The scroll box is made
       an isolated stacking context so z-index -1 tucks the image under all
       in-flow content but above the page ground. */
    .document-body:has(.music-artist-page) {
        position: relative;
        isolation: isolate;
    }

    /* Height only matters when a background needs room to breathe — without
       one the hero hugs its content so the first section follows the Play
       button immediately. */
    .music-artist-hero {
        margin-bottom: 1.5rem;
    }

    .music-artist-hero.has-background {
        min-height: 16rem;
    }

    /* The FADE lives on this element and never mirrors — mirroring it moved
       the cleared zone away from the text, which is what put the name back
       on top of a bright face. The PICTURE lives on the ::before below and
       is the only thing that flips. */
    .music-artist-hero-image {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 42rem;
        z-index: -1;
        /* One long, gradual dissolve down the page and a soft clearing on
           the left — the text column's zone stays almost CLEAR of image (a
           bright or busy photo behind the serif name killed the contrast)
           before the picture ramps in towards the right. */
        -webkit-mask-image: linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.07) 30%, black 65%),
            linear-gradient(180deg, black 25%, transparent 97%);
        -webkit-mask-composite: source-in;
        mask-image: linear-gradient(90deg, transparent 0%, rgba(0,0,0,0.07) 30%, black 65%),
            linear-gradient(180deg, black 25%, transparent 97%);
        mask-composite: intersect;
    }

    .music-artist-hero-image::before {
        content: '';
        position: absolute;
        inset: 0;
        background-image: var(--kik-hero-image);
        background-size: cover;
        background-position: center var(--kik-hero-focus, 25%);
    }

    /* Faces in the cleared zone: mirror the PICTURE into the open half. */
    .music-artist-hero-image.is-flipped::before {
        transform: scaleX(-1);
    }

    /* RTL: the text column hugs the right automatically, so the whole image
       composition mirrors — one transform flips picture AND mask, moving the
       cleared zone under the text and the face-picked side into view. */
    /* RTL: the text column hugs the right, so the FADE's clearing must move
       with it — done by reversing the gradient, not by transforming (a
       transform would take the picture's own framing with it). */
    body[dir="rtl"] .music-artist-hero-image {
        -webkit-mask-image: linear-gradient(270deg, transparent 0%, rgba(0,0,0,0.07) 30%, black 65%),
            linear-gradient(180deg, black 25%, transparent 97%);
        mask-image: linear-gradient(270deg, transparent 0%, rgba(0,0,0,0.07) 30%, black 65%),
            linear-gradient(180deg, black 25%, transparent 97%);
    }

    /* …and the picture's flip inverts with it: in RTL the open half is the
       LEFT, so a photo whose faces are already left needs no mirroring while
       one with faces on the right does. */
    body[dir="rtl"] .music-artist-hero-image::before {
        transform: scaleX(-1);
    }

    body[dir="rtl"] .music-artist-hero-image.is-flipped::before {
        transform: none;
    }

    .music-artist-hero-body {
        position: relative;
        max-width: 46%;
        padding-top: 1.5rem;
        /* A soft page-ground halo lifts the text off whatever image residue
           remains behind it. */
        text-shadow: 0 1px 2px var(--application-body-background-colour, white),
            0 0 14px var(--application-body-background-colour, white);
    }

    .music-artist-hero:not(.has-background) .music-artist-hero-body {
        max-width: 70%;
    }

    /* The name in the brand's display serif (the rich level-1 face). */
    .music-artist-name {
        font-family: var(--pat-rich-header-level-1-font-family, serif);
        font-size: 2.5rem;
        font-weight: 500;
        line-height: 1.1;
        margin: 0 0 0.5rem;
    }

    /* The facet chips and the clamped bio are SHARED
       (static/libraries/kikaron/chips/chips.css) — the Podcasts show page renders the
       identical pair under its own page head. Both read --app-colour, so they
       take Music's green here with no rules of their own. */

    .music-artist-section {
        margin-bottom: 1.5rem;
    }

    /* --- Countries page ------------------------------------------------ */
    /* The shared flag-list draws known flags; countries it can't get the
       abbreviation tile in the app tint. The count rides as a soft badge
       beside the name. */
    .music-countries {
        padding-top: 1rem;
    }

    /* Real flat SVG flags (vendored flag-icons set) for every mapped
       country — the extra selector depth outranks the component's own
       per-code gradient rules, so the US keeps its stars. */
    .music-countries ul.flag-list li.flag.svg a::before {
        background-image: var(--kik-flag);
        background-size: cover;
        background-position: center;
    }

    .music-countries .flag.unknown a::before {
        content: attr(data-abbr);
        display: flex;
        align-items: center;
        justify-content: center;
        font-size: 1.6rem;
        font-weight: 700;
        letter-spacing: 0.1em;
        color: color-mix(in srgb, var(--app-colour) 55%, black);
        background-color: color-mix(in srgb, var(--app-colour) 14%, white);
    }

    .music-countries .flag a .counter {
        margin-inline-start: 0.4rem;
        background-color: rgb(0 0 0 / 0.06);
        color: color-mix(in srgb, currentColor 75%, transparent);
        border-radius: 999px;
        padding: 0.05rem 0.5rem;
        font-size: 0.75rem;
    }

    .music-countries .flag a {
        cursor: default;
        text-decoration: none;
        color: inherit;
    }

    /* --- Playing-row progress line ------------------------------------ */
    /* A 3px accent line along the playing row's bottom edge, growing with
       the track position to the row's full width at 100%. ONE overlay
       element, positioned by the adapter from the row's real geometry — a
       tr::after is no option: a table row is not an absolute containing
       block in every engine (Safari clipped the line to the first cell). */
    #app-content-music {
        position: relative;
    }

    /* The row-sized clip box (radii copied from the row's outer cells by the
       adapter) with the actual bar inside — the bar therefore ends WITH the
       card's rounded corner instead of overlaying it. */
    .kik-music-progress-line {
        position: absolute;
        overflow: hidden;
        pointer-events: none;
        /* Above the STICKY title th (z-index 9 in the table component) — at 1
           the line painted under it and surfaced only beside it, reading as a
           short bar plus a second fragment past the title column. */
        z-index: 10;
        display: none;
    }

    /* The click-to-seek strip. At REST it draws only the old 3px accent
       line; hovering it grows the visuals to the full strip and reveals the
       whisper-grey track to the row's end. The hit area stays a constant
       10px (inline) so the thin resting line is still easy to reach. */
    .kik-music-progress-track {
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        height: 10px;
        background: transparent;
        pointer-events: auto;
        transition: background-color 0.15s ease-out;
    }

    .kik-music-progress-track > i {
        position: absolute;
        bottom: 0;
        inset-inline-start: 0;
        height: 3px;
        background: var(--colour-accent);
        border-radius: 0 2px 2px 0;
        transition: width 0.9s linear, height 0.15s ease-out;
    }

    @media (hover: hover) {
        .kik-music-progress-track:hover {
            background: rgb(0 0 0 / 0.05);
        }

        .kik-music-progress-track:hover > i {
            height: 100%;
        }
    }

    body[dir="rtl"] .kik-music-progress-track > i {
        border-radius: 2px 0 0 2px;
    }

    /* A coverless row's tile — the pale square with the bleached glyph — is now
       the SHARED ``.kik-audio-tile-empty`` face in
       static/libraries/kikaron/link-list/link-list.css, so every app rendering an audio list
       gets it (Podcasts renders the identical row). Only the glyph is Music's,
       and it is handed over as --kik-audio-tile-glyph on the app root below. */


    /* Collapsed, the songs table runs as real rows (.small-style-bare): its fixed
       layout and colgroup widths must stand down, or the hidden columns keep
       their share (176 + 256 + 80px of artist/album/duration on a 390px
       screen) and the title collapses to nothing. Written at the same
       specificity as the rules above and later in the file, because the
       registry prefixes this app's rules and they would otherwise outrank
       the component's own modifier.

       Keyed on the table's own BOX, exactly as the collapse is — asking the
       screen instead left a narrow pane on a wide monitor collapsed but still
       carrying its full-width colgroup. See the same note in table.css. */
    @container pat-table-when(shape: bare) {

        /* The title's ceiling is NOT here any more. It was
           ``max-width: calc(100cqi - 8rem)`` on the link — the container's width
           less a guess at what the artwork and the row menu spend — and a guess
           is wrong somewhere by construction: it cut every title ~50px short in
           the dashboard widget. The component says it without measuring anything
           (``max-width: 0; width: 100%`` on the name CELL, table.css), and the
           title, already ellipsising at 100% of that cell, breaks exactly where
           the row ends. Keeping this rule as well only re-imposed the guess at
           the app's weight. */

        /* Desktop's text treatment, not the card layout's heading size. */
        .music-track-table th.item-name,
        .music-track-table .audio-track-title {
            font-size: inherit;
            line-height: inherit;
        }

    /* THE GRIDS ARE pat_tableS NOW (music.views._music_card_table), and a table
       draws its picture as ``figure.preview`` inside the name cell rather than as
       the link list's ``.preview.album-art`` div. Same sleeve, keyed on the shape
       that is actually there: square, cropped to fill, with the small radius the
       tiles above ask for.

       A COVERLESS card has no figure at all — the builder hands over the empty
       tile instead — so the square is put on that, and the pale ground and the
       bleached note it already carries do the rest. Either way a card in the grid
       is the same square, which is what keeps a row of sleeves a row. */
    }

    /* ---------------------------------------------------------------------------
       THE CATALOGUE (music/catalogue.html, catalogue_album.html): royalty-free
       albums from Jamendo. The shelf is the shared kik_catalogue_grid; these are
       the album page's head and the source line under the shelf.
       --------------------------------------------------------------------------- */
    /* The album page wears the library album's header (.music-album-header,
       above) — the sleeve fills its square like any other. What is its own: the
       licence line, and the Add form sitting in the action bar as one more
       button rather than a block of its own. */
    .music-catalogue-licence {
        margin: 0;
        font-size: 0.85rem;
    }
    .music-catalogue-album .music-album-actions {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.5rem;
    }
    .music-catalogue-album .music-album-actions > .music-catalogue-add {
        display: contents;
    }
    .music-catalogue-added-note {
        flex-basis: 100%;
        margin: 0;
        opacity: 0.7;
    }
    .music-catalogue-source {
        margin: 1.5rem 0;
        text-align: center;
        font-size: 0.85em;
        opacity: 0.6;
    }

    /* ROOM UNDER THE LAST TRACK ON A PHONE (Daniel, 2026-09-30: "on mobile,
       album and playlist pages: a bottom padding, the height of the media player
       and a bit"). A running order is read down to its last track, and the bar
       the player sits in at the foot of a phone covered it. The bar's own
       clearance (--kik-dock-clearance, global-nav-side.css: its height, its
       offset and its air) and a rem more, so the last row stops clear of it. */
    @media (max-width: 680px) {
        :is(.music-album-page, .music-playlist-page) {
            padding-bottom: calc(var(--kik-dock-clearance, 85px) + 1rem);
        }
    }
}
