/* Public portfolio (work/artist) + blog pages */

/* The navbar is fixed (~104px: 80px logo + 2x12px padding) and these pages
   have no full-height hero to slide under it, so clear it explicitly. */
.section.portfolio-page {
    padding-top: var(--space-14, 160px);
}

.portfolio-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--space-7, 32px);
}

.portfolio-card {
    position: relative;
    display: block;
    text-decoration: none;
    color: var(--text);
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    overflow: hidden;
    box-shadow: var(--shadow-sm);
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy, ease),
                box-shadow var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.portfolio-card:hover {
    transform: translateY(-6px);
    box-shadow: var(--shadow-lg);
}

.portfolio-card-cover,
.portfolio-card-cover-placeholder {
    aspect-ratio: 1 / 1;
    width: 100%;
    display: block;
    background: var(--primary-extra-light);
    transition: transform var(--dur-slow, 420ms) var(--ease-dreamy, ease);
}

/* Cover bloom: a gentle zoom on hover, contained by the squircle. */
.portfolio-card:hover .portfolio-card-cover,
.portfolio-card:hover .portfolio-card-cover-placeholder {
    transform: scale(1.04);
}

.portfolio-card-cover {
    object-fit: cover;
}

.portfolio-card-cover-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(40px, 6vw, 56px);
    color: var(--accent);
}

.portfolio-card-body {
    padding: var(--space-5, 20px) var(--space-6, 24px) var(--space-6, 24px);
}

.portfolio-card-title {
    font-family: 'Archivo Black', sans-serif;
    font-size: var(--fs-body, 18px);
    margin: 0 0 4px;
}

/* The dot — the brand's coral marker, before each card title. */
.portfolio-card-title::before {
    content: "";
    display: inline-block;
    width: 0.4em;
    height: 0.4em;
    margin-right: 0.5em;
    border-radius: 50%;
    background: var(--accent);
    vertical-align: middle;
}

.portfolio-card-meta {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 13px;
}

/* Project / artist detail */

.portfolio-detail {
    /* Positioned for the "edit this ..." button (#193), which hangs off the
       top right corner of this box and out of the flow. */
    position: relative;
    display: grid;
    grid-template-columns: minmax(260px, 420px) 1fr;
    gap: clamp(32px, 5vw, 56px);
    align-items: start;
}

@media (max-width: 820px) {
    .portfolio-detail {
        grid-template-columns: 1fr;
    }
}

.portfolio-detail-cover {
    width: 100%;
    border-radius: var(--radius-lg, 28px);
    border: 1px solid var(--glass-border);
    box-shadow: var(--shadow-md);
}

.portfolio-detail-title {
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(32px, 4vw, 48px);
    line-height: 1.1;
    margin: 0 0 8px;
}

.portfolio-detail-subtitle {
    color: var(--text-soft);
    font-size: 18px;
    margin: 0 0 24px;
}

.portfolio-detail-subtitle a {
    color: var(--text-soft);
    text-decoration: underline;
    text-decoration-color: var(--accent);
    text-underline-offset: 2px;
}

.portfolio-detail-subtitle a:hover {
    color: var(--link, var(--accent-strong));
}

/* A song's key and tempo, under who made it (#304): quieter than the names and
   close to them, a fact about the same song rather than a new section. */
.portfolio-detail-subtitle:has(+ .portfolio-detail-meta) {
    margin-bottom: 6px;
}

.portfolio-detail-meta {
    color: var(--text-muted);
    font-size: 15px;
    margin: 0 0 24px;
}

.portfolio-links {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    margin: 0 0 32px;
}

.portfolio-link-btn {
    display: inline-block;
    padding: 10px 22px;
    border-radius: 999px;
    border: 1px solid var(--glass-border);
    background: var(--glass-bg);
    color: var(--text);
    text-decoration: none;
    font-size: var(--fs-small, 15px);
    box-shadow: var(--shadow-sm);
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy, ease),
                background var(--dur-base, 260ms) var(--ease-dreamy, ease),
                color var(--dur-base, 260ms) var(--ease-dreamy, ease),
                border-color var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.portfolio-link-btn:hover {
    background: var(--accent);
    color: var(--white);
    border-color: var(--accent);
    transform: translateY(-2px);
}

/* ============================================================
   THE PANEL'S PLATFORMS (#182) — one stacked column, each
   button in its own service's colour, because at the moment
   someone is deciding where to go, recognising the service
   should not need reading (owner, 2026-08-30). There was a
   version of this on the old projects carousel (`e7e81d2`,
   April) and it went out with the carousel; these are its
   colours.

   **This is the preview card's end panel and nowhere else.**
   The same links appear in a plain row under the player and on
   the release page, and they stay a row: the column is for the
   moment playback stops and the panel is the only thing on the
   card, not for a page that is mostly other things (owner,
   2026-08-30 — "not on the page itself").

   **These are not palette colours and are not an exception to
   the palette being fixed** (design-language §1). They are
   other companies' identities, reproduced so a listener can
   recognise them — the same reason a logo is not restyled.
   Nothing else on the site may use them.

   The platform comes off `data-stream-platform`, which is
   already on the anchor for GA4 (`docs/seo/measurement.md`
   §3), so the colour and the analytics name cannot drift
   apart, and a platform added to `STREAMING_SERVICES` without
   a colour here still renders as a readable glass button
   rather than an unstyled one.
   ============================================================ */
.streaming-links {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    /* A stack of buttons, not seven full-bleed bars. */
    width: min(100%, 22rem);
    margin: 0;
}

.streaming-link-btn {
    display: block;
    padding: 12px 22px;
    border-radius: 999px;
    border: 1px solid transparent;
    background: var(--brand, var(--glass-bg));
    color: var(--on-brand, var(--text));
    text-align: center;
    text-decoration: none;
    font-size: var(--fs-small, 15px);
    font-weight: 600;
    box-shadow: var(--shadow-sm);
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy, ease),
                filter var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.streaming-link-btn:hover {
    transform: translateY(-2px);
    filter: brightness(1.06);
}

.streaming-link-btn:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
    .streaming-link-btn {
        transition: none;
    }

    .streaming-link-btn:hover {
        transform: none;
    }
}

/* The seven brand pairs live in `components/streaming.css` — one table, read
   here and by the project hub's links. rows (#258). `--brand` / `--on-brand`
   arrive by inheritance from the `data-stream-platform` on this very anchor;
   the fallbacks above are what a platform with no entry there is drawn in. */

/* Audio player: markup/CSS/JS shared with the dashboards (projects.css),
   lightly reskinned as a glass card to sit in the public layout. */
.portfolio-player {
    margin: 0 0 var(--space-7, 32px);
    padding: var(--space-4, 16px) var(--space-6, 24px) var(--space-5, 20px);
    border-top: none;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    box-shadow: var(--shadow-sm);
}

/* Single-track player (song page): prev/next and the 1/1 counter are noise */
.portfolio-player-single .player-prev-next-btn,
.portfolio-player-single .project-player-track-count {
    display: none;
}

.portfolio-block {
    margin-bottom: 32px;
}

.portfolio-block h2,
.song-preview-heading {
    font-family: 'Archivo Black', sans-serif;
    font-size: 20px;
    margin: 0 0 var(--space-3, 12px);
}

/* The dot before each block heading (tracklist./credits./lyrics./releases.). */
.portfolio-block h2::before,
.song-preview-heading::before {
    content: "";
    display: inline-block;
    width: 0.4em;
    height: 0.4em;
    margin-right: 0.5em;
    border-radius: 50%;
    background: var(--accent);
    vertical-align: middle;
}

.portfolio-brief {
    color: var(--text-soft);
    line-height: 1.7;
    margin: 0 0 var(--space-6, 24px);
    white-space: pre-line;
}

.portfolio-tracklist {
    list-style: none;
    padding: 0;
    margin: 0;
    counter-reset: track;
}

.portfolio-tracklist li {
    counter-increment: track;
    display: flex;
    align-items: baseline;
    padding: 8px 0;
    border-bottom: 1px solid var(--primary-extra-light);
}

.portfolio-tracklist li::before {
    content: counter(track, decimal-leading-zero) " ";
    color: var(--accent);
    font-family: 'Archivo Black', sans-serif;
    margin-right: 12px;
}

.portfolio-tracklist li > details,
.portfolio-tracklist li > .track-title {
    flex: 1;
    min-width: 0;
}

/* Tracks with a brief or own page expand in place; the closed state looks
   identical to a plain track except for the + affordance. */
.track-details summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 12px;
}

.track-details summary::-webkit-details-marker {
    display: none;
}

.track-details summary::after {
    content: "+";
    color: var(--accent);
    font-family: 'Archivo Black', sans-serif;
    flex-shrink: 0;
}

.track-details[open] summary::after {
    content: "\2212";
}

.track-details summary:hover {
    color: var(--accent-strong, var(--primary-dark));
}

.track-expand {
    padding: 10px 0 8px;
    font-size: 15px;
    color: var(--text-soft);
}

.track-expand p {
    margin: 0 0 8px;
    white-space: pre-line;
}

.track-expand a {
    color: var(--link, var(--primary-dark));
    text-decoration: none;
    font-weight: 600;
}

.track-expand a:hover {
    text-decoration: underline;
}

/* Credits as "liner notes": a mono, sleeve-style panel — a natural fit for the
   brand's lowercase, typographic voice. */
.portfolio-credits {
    display: grid;
    grid-template-columns: max-content 1fr;
    gap: 8px 28px;
    margin: 0;
    padding: var(--space-5, 20px) var(--space-6, 24px);
    background: var(--surface-tint, var(--coral-50));
    border-radius: var(--radius-md, 18px);
    font-family: var(--font-mono);
    font-size: var(--fs-small, 15px);
    line-height: 1.5;
}

.portfolio-credits dt {
    color: var(--text-muted);
    letter-spacing: 0.03em;
}

.portfolio-credits dd {
    margin: 0;
    color: var(--text);
}

/* A credited person with a page of their own links to it (#288). Underlined in
   the accent, like the artist names under a release title, so a name that goes
   somewhere reads differently from one that does not. */
.portfolio-credit-person {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: var(--accent);
    text-underline-offset: 2px;
}

.portfolio-credit-person:hover {
    color: var(--link, var(--accent-strong));
}

/* Credit families (#288). One card with hairlines in it, not four cards: a
   visitor reads a credit list down the page, and boxing each family would make
   it four things to choose between. The tint, padding and radius move here from
   `.portfolio-credits` for exactly that reason — the `dl` inside is now plain.

   The first family has no rule above it; the `credits.` heading already is one.
   The label is the same small-caps mono as every other label in the system, one
   step quieter than the names it introduces. */
.portfolio-credit-families {
    padding: var(--space-5, 20px) var(--space-6, 24px);
    background: var(--surface-tint, var(--coral-50));
    border-radius: var(--radius-md, 18px);
}

.portfolio-credit-families .portfolio-credits {
    padding: 0;
    background: none;
    border-radius: 0;
}

.portfolio-credit-family + .portfolio-credit-family {
    margin-top: var(--space-5, 20px);
    padding-top: var(--space-5, 20px);
    border-top: 1px solid var(--glass-border);
}

/* The room the work was done in, beside the person who did it (#288). One step
   quieter than the name: it is context, and the name is the credit. */
.portfolio-credit-room {
    color: var(--text-muted);
}

/* On a release, how many of its tracks a credit is on (#312). A small pink pill
   rather than more words on the line, so it weighs the credit without reading
   as part of the name. Sized off the credit (`em`) so it stays a step below it
   wherever the list is set. The text stays `--text-soft`: `--text-muted` drops
   under AA on any pink fill at this size. `nowrap` keeps `4 tracks` in one piece
   at phone width. */
.portfolio-credit-tracks {
    display: inline-block;
    padding: 0 6px;
    font-size: 0.75em;
    line-height: 1.5;
    vertical-align: middle;
    white-space: nowrap;
    color: var(--text-soft);
    background: var(--tint-soft);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-pill);
}

/* **The two-column credit grid cannot hold at phone width, and never could.**
   `grid-template-columns: max-content 1fr` means the role column does not
   shrink, so the longest label sets a floor: in Spanish, `ingeniero de
   masterización` put the card 52px past a 360px content column before #288 and
   70px past it after (the family headings and their wrapper add the rest).
   `body` carries `overflow-x: clip`, so none of that scrolled — it was silently
   cut off, which is why it survived this long.

   Stacked, the role reads as a label above the name, which is how a credit is
   printed on a sleeve anyway, and nothing has a minimum width left to enforce. */
@media (max-width: 480px) {
    .portfolio-credits {
        grid-template-columns: 1fr;
        gap: 0;
    }

    .portfolio-credits dt {
        margin-top: var(--space-3, 12px);
    }

    .portfolio-credits dt:first-child {
        margin-top: 0;
    }

    .portfolio-credit-families {
        padding: var(--space-4, 16px) var(--space-4, 16px);
    }
}

.portfolio-credit-family-title {
    margin: 0 0 var(--space-3, 12px);
    font-family: var(--font-mono);
    font-size: var(--fs-label, 11px);
    font-weight: 700;
    letter-spacing: var(--tracking-label);
    text-transform: uppercase;
    color: var(--text-muted);
}

/* Artist page */

.portfolio-artist-pic {
    width: 100%;
    aspect-ratio: 1 / 1;
    object-fit: cover;
    border-radius: var(--radius-lg, 28px);
    border: 1px solid var(--glass-border);
    box-shadow: var(--shadow-md);
}

.portfolio-artist-bio {
    color: var(--text-soft);
    line-height: 1.7;
    white-space: pre-line;
    margin: 0 0 var(--space-6, 24px);
}

/* "about the artist" card — project page (#115).
   A small, quiet block: the photo at a fixed size beside the bio, so a row of
   them reads as one list rather than as competing hero images. */

.artist-bio-card {
    display: flex;
    gap: var(--space-4, 16px);
    align-items: flex-start;
    padding: var(--space-4, 16px);
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    box-shadow: var(--shadow-sm);
}

.artist-bio-card + .artist-bio-card {
    margin-top: var(--space-3, 12px);
}

.artist-bio-card-pic {
    flex: 0 0 auto;
    width: 88px;
    height: 88px;
    object-fit: cover;
    border-radius: 50%;
    border: 1px solid var(--glass-border);
}

/* No photo yet: the initial on the same pink wash the cover placeholders use,
   so a card without one still has a shape instead of a hole. */
.artist-bio-card-pic--empty {
    display: flex;
    align-items: center;
    justify-content: center;
    background: var(--tint-20);
    font-family: 'Archivo Black', sans-serif;
    font-size: 32px;
    color: var(--accent);
    text-transform: lowercase;
}

/* `flex: 1` is load-bearing, not tidiness: without it the body sizes to its
   own content, and on a card with a short bio the head row — and with it the
   "view this artist." link — stops well short of the card's right edge and
   lands somewhere in the middle. `min-width: 0` lets long words wrap instead
   of pushing the card wide. */
.artist-bio-card-body {
    flex: 1 1 auto;
    min-width: 0;
}

/* Name on the left, the way through on the right. `baseline` rather than
   `center` so the link sits on the name's line however long either runs. */
.artist-bio-card-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3, 12px);
    margin-bottom: var(--space-2, 8px);
}

.artist-bio-card-name {
    font-family: 'Archivo Black', sans-serif;
    font-size: 17px;
    margin: 0;
}

.artist-bio-card-through {
    flex: 0 0 auto;
    display: inline-flex;
    align-items: center;
    gap: 0.25em;
    font-size: 14px;
    color: var(--link);
    text-decoration: none;
    white-space: nowrap;
}

/* The arrow leaves with the words rather than after them — one movement, and
   it reads as "away from here" instead of as a loading tick. */
.artist-bio-card-through-arrow {
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.artist-bio-card-through:hover .artist-bio-card-through-arrow,
.artist-bio-card-through:focus-visible .artist-bio-card-through-arrow {
    transform: translate(2px, -2px);
}

.artist-bio-card-through:hover,
.artist-bio-card-through:focus-visible {
    text-decoration: underline;
    text-underline-offset: 3px;
}

@media (prefers-reduced-motion: reduce) {
    .artist-bio-card-through-arrow {
        transition: none;
    }

    .artist-bio-card-through:hover .artist-bio-card-through-arrow,
    .artist-bio-card-through:focus-visible .artist-bio-card-through-arrow {
        transform: none;
    }
}

.artist-bio-card-bio {
    color: var(--text-soft);
    line-height: 1.7;
    white-space: pre-line;
    margin: 0;
}

.artist-bio-card-links {
    margin-top: var(--space-3, 12px);
}

@media (max-width: 480px) {
    .artist-bio-card {
        flex-direction: column;
    }

    .artist-bio-card-pic {
        width: 72px;
        height: 72px;
    }

    /* Narrow enough that a long name and the link would fight over one line. */
    .artist-bio-card-head {
        flex-wrap: wrap;
        gap: var(--space-2, 8px);
    }
}

/* Blog */

/* Topics: a filter, styled as a row of chips. */
.blog-topics {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
    justify-content: center;
    max-width: 880px;
    margin: 0 auto var(--space-7, 32px);
}

.blog-topic {
    padding: 6px 14px;
    border-radius: 999px;
    border: 1px solid var(--glass-border);
    color: var(--text-soft);
    text-decoration: none;
    font-family: var(--font-mono);
    font-size: 13px;
    transition: color var(--dur-fast, 160ms) var(--ease-dreamy, ease),
                border-color var(--dur-fast, 160ms) var(--ease-dreamy, ease);
}

.blog-topic:hover,
.blog-topic:focus-visible {
    color: var(--text);
    border-color: var(--accent);
}

.blog-topic.is-active {
    color: var(--text);
    border-color: var(--accent);
    background: var(--surface-tint);
}

.blog-list-parts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
    margin-top: var(--space-3, 12px);
}

.blog-list-part {
    font-family: var(--font-mono);
    font-size: 12px;
    color: var(--text-muted);
    border: 1px solid var(--border);
    border-radius: 999px;
    padding: 2px 10px;
}

/* "also written in spanish." — the one line that admits the other language
   exists (docs/blog/README.md §5). It shipped as a bare <p>: unstyled, at the
   container's left edge while the list it belongs to is a centred 880px column,
   and reading as body text rather than as somewhere to go. It sits on the
   list's own measure now, under a hairline that says it is a footer to the list
   and not another entry in it, and it wears the topic chips' vocabulary —
   because it is the same kind of move, somewhere else to read. */
.blog-other-language {
    max-width: 880px;
    margin: var(--space-9, 48px) auto 0;
    padding-top: var(--space-7, 32px);
    border-top: 1px solid var(--border);
    text-align: center;
}

.blog-other-language a {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2, 8px);
    padding: 8px 18px;
    border-radius: 999px;
    border: 1px solid var(--glass-border);
    color: var(--link);
    text-decoration: none;
    font-family: var(--font-mono);
    font-size: 13px;
    transition: color var(--dur-fast, 160ms) var(--ease-dreamy, ease),
                border-color var(--dur-fast, 160ms) var(--ease-dreamy, ease),
                background var(--dur-fast, 160ms) var(--ease-dreamy, ease);
}

.blog-other-language a:hover,
.blog-other-language a:focus-visible {
    color: var(--link-hover);
    border-color: var(--accent);
    background: var(--surface-tint);
}

.blog-list {
    max-width: 880px;
    margin: 0 auto;
    display: grid;
    gap: var(--space-6, 24px);
}

/* The card is image + blurb, side by side. The image bleeds to the card's
   edge, so the padding lives on the body rather than the card. */
.blog-list-item {
    display: grid;
    grid-template-columns: 260px 1fr;
    align-items: stretch;
    overflow: hidden;
    text-decoration: none;
    color: var(--text);
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    box-shadow: var(--shadow-sm);
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy, ease),
                box-shadow var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.blog-list-item:hover {
    transform: translateY(-4px);
    box-shadow: var(--shadow-lg);
}

.blog-list-cover {
    display: block;
    width: 100%;
    height: 100%;
    min-height: 180px;
    object-fit: cover;
    background: var(--primary-extra-light);
    transition: transform var(--dur-slow, 420ms) var(--ease-dreamy, ease);
}

/* No image yet: the title's first letter, the same fallback the portfolio
   grid uses, so the row keeps its shape. */
.blog-list-cover--empty {
    display: grid;
    place-items: center;
    font-family: 'Archivo Black', sans-serif;
    font-size: 56px;
    line-height: 1;
    color: var(--primary-dark);
    text-transform: uppercase;
}

.blog-list-item:hover .blog-list-cover {
    transform: scale(1.04);
}

.blog-list-body {
    display: block;
    padding: var(--space-7, 32px);
}

.blog-list-item h2 {
    font-family: 'Archivo Black', sans-serif;
    font-size: 22px;
    margin: 0 0 6px;
}

.blog-list-item p {
    margin: 0;
    color: var(--text-soft);
    line-height: 1.6;
}

/* Narrow: the image goes on top, in a shape that survives a phone. */
@media (max-width: 768px) {
    .blog-list-item {
        grid-template-columns: 1fr;
    }

    .blog-list-cover {
        aspect-ratio: 16 / 9;
        height: auto;
        min-height: 0;
    }

    .blog-list-body {
        padding: var(--space-6, 24px);
    }
}

.blog-meta {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 13px;
    letter-spacing: 0.03em;
    margin-bottom: 8px;
    display: block;
}

.blog-draft-badge {
    display: inline-block;
    padding: 2px 10px;
    border-radius: 999px;
    background: var(--primary-extra-light);
    color: var(--accent-strong, var(--primary-dark));
    font-family: var(--font-mono);
    font-size: 12px;
    margin-left: 8px;
    vertical-align: middle;
}

/* Article: a centred, readable typographic column, with the contents list
   parked beside it once there is room for one. */
.blog-layout {
    display: grid;
    gap: var(--space-8, 40px);
    justify-content: center;
    /* An explicit track rather than the implicit `auto` one, which is sized by
       what is inside it. An article can now contain a live component (#112,
       B5), and a component's controls have real minimum widths that a text
       column does not — left to size itself, the column grows to fit the
       widest thing in the article and carries the prose out past the container
       with it. `minmax(0, …)` makes the column the page's decision, and the
       component wraps inside it, which it does perfectly well. */
    grid-template-columns: minmax(0, 720px);
}

@media (min-width: 1024px) {
    .blog-layout {
        grid-template-columns: minmax(0, 700px) 220px;
        align-items: start;
    }

    .blog-layout__aside {
        grid-column: 2;
        grid-row: 1;
        position: sticky;
        /* The rendered navbar is taller than --navbar-h claims
           (conventions §5), so this clears it by measure rather than by the
           token alone. */
        top: calc(var(--navbar-h, 72px) + var(--space-8, 40px));
    }

    .blog-article {
        grid-column: 1;
        grid-row: 1;
    }
}

.blog-article {
    max-width: 720px;
    margin: 0 auto;
    /* Take the column's width rather than be given it by the widest thing
       inside. The auto margins that centre this also switch off the grid's
       stretch, which leaves the article shrink-to-fit — and shrink-to-fit
       floors at min-content, so the column above would be the only thing
       standing between an embed and the prose. Both, or neither works. */
    width: 100%;
}

/* Long-form measure and rhythm: an article is 1,500 words, not a marketing
   section, so the body sets its own scale rather than inheriting the page's.

   The measure is set on the blocks rather than on the body, because a measure
   is a property of *text*: an interactive block is not read line by line and
   takes the width the article column gives it (#112, B5). Every block template
   renders one root element carrying `.blog-block`, so this is the same column
   the body used to draw. */
.blog-article-body {
    font-size: 17px;
    line-height: 1.75;
}

.blog-sources {
    margin-top: var(--space-8, 40px);
    padding-top: var(--space-6, 24px);
    border-top: 1px solid var(--border);
}

.blog-sources__title {
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.06em;
    color: var(--text-muted);
    margin: 0 0 var(--space-3, 12px);
}

.blog-sources__list {
    margin: 0;
    padding-left: var(--space-5, 20px);
    font-size: 14px;
    line-height: 1.6;
    color: var(--text-soft);
    display: grid;
    gap: var(--space-2, 8px);
}

.blog-sources__checked {
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 12px;
    margin-left: var(--space-2, 8px);
}

/* Keep reading. */
.blog-related {
    max-width: 940px;
    margin: var(--space-9, 56px) auto 0;
    padding-top: var(--space-7, 32px);
    border-top: 1px solid var(--border);
}

.blog-related__title {
    font-family: 'Archivo Black', sans-serif;
    font-size: 20px;
    margin: 0 0 var(--space-5, 20px);
}

.blog-related__grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--space-5, 20px);
}

.blog-related__card {
    display: block;
    overflow: hidden;
    text-decoration: none;
    color: var(--text);
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.blog-related__card:hover {
    transform: translateY(-4px);
}

.blog-related__cover {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    background: var(--surface-tint);
}

.blog-related__body {
    display: block;
    padding: var(--space-5, 20px);
}

.blog-related__name {
    display: block;
    font-family: 'Archivo Black', sans-serif;
    font-size: 16px;
    margin-bottom: var(--space-2, 8px);
}

.blog-related__teaser {
    display: block;
    color: var(--text-soft);
    font-size: 14px;
    line-height: 1.5;
}

/* Blocks. Plain until B3 designs the reading experience — enough rhythm that
   an article reads as an article, and no new colours. */
.blog-block {
    margin: 0 0 var(--space-5, 20px);
    max-width: 68ch;
}

/* A card deck takes the reading column and no more (owner, 2026-09-08): one
   card at a time, at 100% of the width the prose already occupies. It used to
   bleed past the measure as a strip of narrow cards, which is the shape that
   was rejected — so there is nothing for this sheet to say about it any more,
   and `--dd-cards-bleed` went with the strip. The deck inherits
   `.blog-block`'s measure like every other block. */

/* An embedded component gets the whole column and a rule above and below it,
   because it is a thing you use rather than a paragraph you read. It never
   grows past the article, so it cannot introduce a horizontal scroll the prose
   does not already have. */
.blog-block--embed {
    max-width: none;
    margin: var(--space-8, 40px) 0;
}

.blog-block--heading {
    font-family: 'Archivo Black', sans-serif;
    margin: var(--space-7, 32px) 0 var(--space-3, 12px);
    scroll-margin-top: calc(var(--navbar-h, 72px) + var(--space-6, 24px));
}

h2.blog-block--heading { font-size: 26px; }
h3.blog-block--heading { font-size: 20px; }

.blog-block--quote {
    margin: var(--space-7, 32px) 0;
    padding-left: var(--space-5, 20px);
    border-left: 3px solid var(--primary);
    font-size: 20px;
    line-height: 1.5;
}

.blog-block--quote cite {
    display: block;
    margin-top: var(--space-2, 8px);
    font-size: 14px;
    font-style: normal;
    color: var(--text-muted);
}

.blog-block--list {
    padding-left: var(--space-6, 24px);
    line-height: 1.7;
}

.blog-block--list li { margin-bottom: var(--space-2, 8px); }

.blog-block--cta {
    margin: var(--space-7, 32px) 0;
}

/* The series list sits in the sidebar, in the contents list's shape. It is a
   second .dd-toc, so it only needs the gap between them. */
.blog-series-nav {
    margin-top: var(--space-7, 32px);
}

.blog-series-position {
    margin: var(--space-3, 12px) 0 0;
    font-family: var(--font-mono);
    font-size: 12px;
    letter-spacing: 0.03em;
    color: var(--text-muted);
}

/* What stays at the end of the article: the part before and the one after. */
.blog-series-steps {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    gap: var(--space-4, 16px);
    margin: var(--space-8, 40px) 0 var(--space-6, 24px);
    padding-top: var(--space-6, 24px);
    border-top: 1px solid var(--border);
    font-size: 15px;
}

.blog-counterpart {
    margin: 0 0 var(--space-6, 24px);
}

.blog-article-cover {
    display: block;
    width: 100%;
    aspect-ratio: 16 / 9;
    object-fit: cover;
    border-radius: var(--radius-lg, 28px);
    background: var(--primary-extra-light);
    margin-bottom: var(--space-6, 24px);
}

.blog-article-title {
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(32px, 4vw, 44px);
    line-height: 1.15;
    margin: var(--space-2, 8px) 0 var(--space-6, 24px);
}

.blog-article-body {
    color: var(--text-soft);
    font-size: var(--fs-body, 17px);
    line-height: 1.8;
}

.blog-article-body p {
    margin: 0 0 var(--space-5, 20px);
}

.blog-article-body a {
    color: var(--link, var(--accent-strong));
    text-underline-offset: 2px;
}

.blog-article > .portfolio-link-btn {
    margin-top: var(--space-7, 32px);
}

.portfolio-empty {
    text-align: center;
    color: var(--text-muted);
    padding: var(--space-8, 40px) 0;
}

/* Song page */

.song-article {
    /* Positioned for the "edit this song." button (#193) — see
       .portfolio-edit-link. Every other absolute here resolves against
       .song-preview, which carries its own `position: relative`. */
    position: relative;
    max-width: 760px;
    margin: 0 auto;
}

.song-article-backlink {
    display: inline-block;
    color: var(--text-muted);
    text-decoration: none;
    font-size: 14px;
    margin-bottom: 16px;
}

.song-article-backlink:hover {
    color: var(--link, var(--primary-dark));
}

.song-lyrics {
    white-space: pre-line;
    color: var(--text-soft);
    line-height: 1.9;
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    box-shadow: var(--shadow-sm);
    padding: var(--space-7, 32px);
}

/* Lyrics that follow the audio (#72 L3). The card is the one the sheet has
   always been printed in; while the timings apply it also becomes the window
   that scrolls with the song — tall enough to read from, and short enough that
   the page itself never moves under the reader. The lines themselves are a
   global component (css/components/lyric-lines.css), shared with the song hub.

   After `.song-lyrics` on purpose: same specificity, and the sheet's
   `pre-line` would otherwise win here — which renders the partial's own
   newlines as a blank line between every word. */
.song-lyrics--lines {
    white-space: normal;
}

.song-lyrics--lines .song-lyrics-content--lines[data-synced="1"] {
    max-height: min(62vh, 560px);
}


/* ============================================================
   FEATURED RAIL — auto-rotating showcase above the work grid.
   One release at a time; the dots HTMX-swap #work-featured
   (work_featured view + _work_featured.html), and work_featured.js
   drives the active state + a calm idle auto-advance.
   ============================================================ */
.work-featured-region {
    margin-bottom: var(--space-10, 40px);
}

.work-featured {
    border-radius: var(--radius-lg, 28px);
    background:
        var(--glow-coral) no-repeat -20% -40% / 70% 120%,
        var(--grad-dreamy);
    border: 1px solid var(--glass-border);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
}

/* The cross-fade is driven by work_featured.js via inline opacity/transition
   (snappy for manual nav, slower for auto-advance), so no transition is
   declared here — it would otherwise fight the JS-set timing. */

.work-featured-panel {
    display: grid;
    grid-template-columns: minmax(0, 360px) 1fr;
    gap: clamp(20px, 4vw, 48px);
    align-items: center;
    padding: clamp(20px, 3vw, 36px);
}

.work-featured-cover {
    display: block;
    aspect-ratio: 1 / 1;
    border-radius: var(--radius-md, 18px);
    overflow: hidden;
    background: var(--primary-extra-light);
    box-shadow: var(--shadow-md);
    text-decoration: none;
}

.work-featured-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
    transition: transform var(--dur-slow, 420ms) var(--ease-dreamy);
}

.work-featured-cover:hover img {
    transform: scale(1.04);
}

.work-featured-cover-placeholder {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: 100%;
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(48px, 8vw, 88px);
    color: var(--accent);
}

.work-featured-info {
    min-width: 0;
}

.work-featured-kicker {
    display: flex;
    align-items: center;
    gap: 0.5em;
    font-family: var(--font-mono);
    font-size: var(--fs-label, 13px);
    letter-spacing: 0.08em;
    color: var(--text-muted);
    margin: 0 0 var(--space-4, 16px);
}

.work-featured-kicker::before {
    content: "";
    width: 0.5em;
    height: 0.5em;
    border-radius: 50%;
    background: var(--accent);
}

.work-featured-title {
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(26px, 3.4vw, 40px);
    line-height: 1.08;
    margin: 0 0 6px;
}

.work-featured-meta {
    color: var(--text-muted);
    margin: 0 0 var(--space-4, 16px);
}

.work-featured-brief-wrap {
    margin: 0 0 var(--space-6, 24px);
}

.work-featured-brief {
    color: var(--text-soft);
    line-height: 1.7;
    margin: 0;
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}

/* JS-revealed only when the brief actually overflows its clamp. */
.work-featured-readmore {
    display: none;
    margin-top: 4px;
    font-size: var(--fs-small, 15px);
    font-weight: 600;
    color: var(--link, var(--primary-dark));
    text-decoration: none;
}

.work-featured-brief-wrap.is-clamped .work-featured-readmore {
    display: inline-block;
}

.work-featured-readmore:hover {
    text-decoration: underline;
}

.work-featured-tracklist {
    font-family: var(--font-mono);
    font-size: var(--fs-small, 15px);
    color: var(--text-muted);
    margin: var(--space-4, 16px) 0 0;
}

.work-featured-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-4, 16px) var(--space-6, 24px);
    margin-top: var(--space-6, 24px);
}

.work-featured-open {
    font-weight: 600;
    color: var(--link, var(--primary-dark));
    text-decoration: none;
}

.work-featured-open:hover {
    text-decoration: underline;
}

.work-featured-streaming {
    font-size: 14px;
    color: var(--text-muted);
}

.work-featured-streaming a {
    color: var(--text-muted);
    text-decoration: none;
}

.work-featured-streaming a:hover {
    color: var(--accent);
}

.work-featured-nav {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-4, 16px);
    margin-top: var(--space-6, 24px);
}

.work-featured-dots {
    display: flex;
    align-items: center;
    gap: 12px;
}

/* Prev/next arrows are JS-only controls — hidden until work_featured.js wires
   them and adds .has-controls (the dots stay as the no-JS fallback). */
.work-featured-arrow {
    display: none;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    padding: 0;
    border: 1px solid var(--glass-border);
    border-radius: 50%;
    background: var(--surface);
    color: var(--text);
    box-shadow: var(--shadow-sm);
    cursor: pointer;
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy),
                color var(--dur-base, 260ms) var(--ease-dreamy),
                box-shadow var(--dur-base, 260ms) var(--ease-dreamy);
}

.work-featured-region.has-controls .work-featured-arrow {
    display: inline-flex;
}

.work-featured-arrow:hover {
    color: var(--accent);
    transform: scale(1.08);
    box-shadow: var(--shadow-md);
}

.work-featured-arrow:active {
    transform: scale(0.96);
}

.work-featured-arrow:focus-visible {
    outline: 2px solid var(--focus-ring, var(--accent));
    outline-offset: 3px;
}

.work-featured-dot {
    width: 12px;
    height: 12px;
    padding: 0;
    border-radius: 50%;
    background: var(--coral-200, #FFC9C7);
    text-decoration: none;
    transition: transform var(--dur-base, 260ms) var(--ease-dreamy),
                background var(--dur-base, 260ms) var(--ease-dreamy);
}

.work-featured-dot:hover {
    transform: scale(1.2);
}

.work-featured-dot.is-active {
    background: var(--accent);
    transform: scale(1.3);
    box-shadow: var(--shadow-glow);
}

.work-featured-dot:focus-visible {
    outline: 2px solid var(--focus-ring, var(--accent));
    outline-offset: 3px;
}

@media (max-width: 720px) {
    .work-featured-panel {
        grid-template-columns: 1fr;
        text-align: center;
    }

    .work-featured-cover {
        max-width: 320px;
        margin: 0 auto;
    }

    .work-featured-kicker,
    .work-featured-actions {
        justify-content: center;
    }

    .work-featured-brief {
        text-align: left;
    }
}


/* ── Preview chrome (issue #43) ──────────────────────────────────────────────
   Unpublished /work/ pages viewed via ?preview. A warm, unmistakable strip so
   staff + share-link holders always know the page isn't live yet. */
.preview-banner {
    background: var(--tint-20);
    border-bottom: 1px solid var(--coral-200);
    font-family: var(--font-mono);
    /* The strip is the first thing on the page, and the navbar is fixed and
       renders ~97px against --navbar-h's 60 (conventions §5, "navbar height
       lies"). Without this the whole band sits *under* the bar: invisible, and
       the publish button inside it unclickable. Found while building the
       article review path (#112, B4) — it was true of /work/ previews too. */
    padding-top: calc(var(--navbar-h) + var(--space-8));
}

/* …and the page below stops paying for that clearance twice. The section
   clears the chrome itself when it is the first thing on the page; with the
   strip above it, the strip has already done it. */
.preview-banner + .section.portfolio-page {
    padding-top: var(--space-9);
}

.preview-banner-inner {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4, 16px);
    flex-wrap: wrap;
    padding-top: var(--space-3, 12px);
    padding-bottom: var(--space-3, 12px);
}

.preview-banner-label {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2, 8px);
    color: var(--coral-700);
    font-size: 0.85rem;
    letter-spacing: 0.02em;
}

.preview-banner-dot {
    width: 8px;
    height: 8px;
    border-radius: var(--radius-pill);
    background: var(--coral-500);
    animation: preview-pulse 2s var(--ease-dreamy, ease) infinite;
}

@keyframes preview-pulse {
    0%   { box-shadow: 0 0 0 0 rgba(255, 139, 136, 0.5); }
    70%  { box-shadow: 0 0 0 8px rgba(255, 139, 136, 0); }
    100% { box-shadow: 0 0 0 0 rgba(255, 139, 136, 0); }
}

.preview-banner-actions {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2, 8px);
    flex-wrap: wrap;
}

.preview-banner-publish {
    margin: 0;
}

.preview-banner-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 14px;
    font-family: var(--font-mono);
    font-size: 0.8rem;
    color: var(--coral-700);
    background: #fff;
    border: 1px solid var(--coral-200);
    border-radius: var(--radius-pill);
    cursor: pointer;
    text-decoration: none;
    transition: background var(--dur-base, 260ms) var(--ease-dreamy, ease),
                border-color var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.preview-banner-btn:hover {
    background: var(--tint-20);
    border-color: var(--coral-300);
}

.preview-banner-btn--primary {
    color: #fff;
    background: var(--coral-500);
    border-color: var(--coral-500);
}

.preview-banner-btn--primary:hover {
    background: var(--coral-700);
    border-color: var(--coral-700);
}

/* "edit this project/song/artist" — a circular icon button pinned to the top
   right of the page (#193). Absolute rather than in flow: it used to sit above
   the detail title, so a control only staff and the owning client can see
   pushed the page everyone else reads down by its own height. Its words live
   on in the tooltip + aria-label (`_edit_this_button.html`); the anchor for
   "top right" is the page's own content box — the detail grid, the song
   article — so it lands on the content edge rather than out in the gutter. */
.portfolio-edit-link {
    position: absolute;
    top: 0;
    right: 0;
    z-index: 2;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 42px;
    height: 42px;
    font-size: 16px;          /* .dd-icon sizes in em — 20px of pencil */
    /* The brand pink rather than --link: this is an icon, which is exactly
       what the contrast rule reserves the pink for (design-language §1), and
       --link's #A83C39 read as a dark red sitting on its own in the corner. */
    color: var(--accent-strong);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    box-shadow: var(--shadow-sm);
    text-decoration: none;
    transition: background var(--dur-base, 260ms) var(--ease-dreamy, ease),
                border-color var(--dur-base, 260ms) var(--ease-dreamy, ease),
                color var(--dur-base, 260ms) var(--ease-dreamy, ease);
}

.portfolio-edit-link:hover {
    color: var(--link-hover);
    background: var(--surface-tint);
    border-color: var(--accent);
}

.portfolio-edit-link:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

/* The button sits on the page's right edge, so its tooltip hangs from that
   edge instead of being centred on the button: centred, the label runs off a
   phone screen and takes the page's horizontal scroll with it. The arrow stays
   over the button. */
.portfolio-edit-link::after {
    left: auto;
    right: 0;
    transform: translateX(0) translateY(-4px);
}

.portfolio-edit-link:hover::after,
.portfolio-edit-link:focus-visible::after,
.portfolio-edit-link:focus-within::after {
    transform: translateX(0);
}

.portfolio-edit-link::before {
    left: auto;
    right: 15px;   /* half the 42px button, less half the 12px arrow */
    transform: none;
}

/* One column: the cover art is the top of the page, so the button tucks into
   its corner instead of sitting on the edge of it. */
@media (max-width: 820px) {
    .portfolio-detail > .portfolio-edit-link {
        top: 12px;
        right: 12px;
    }
}

@media (prefers-reduced-motion: reduce) {
    .preview-banner-dot {
        animation: none;
    }
}


/* ============================================================
   PREVIEW CARD (#169) — what a *timed* song gets in place of the
   player strip and the lyric sheet: the record playing from its
   hook, the words following it, an aura scene reading the audio
   behind them, and the streaming links out of it.

   The scene is content (aura's `paint` on the brand palette —
   white field, coral waves), so the card stays a light surface
   with the words in `--text`: no dark theme is introduced here.
   Legibility is bought with solid veils — one over the whole
   scene, one under each panel that carries text — and never with
   `backdrop-filter`, which re-rasterizes the glass layers under
   it (design-language §8, measured twice).
   ============================================================ */
.song-preview {
    position: relative;
    /* The veil the scene is seen through. Transparency, not a colour, which is
       why it is a local property rather than a token: paint at full tilt is a
       strong coral field, and the brand mood is a sunlit room, not a lava
       lamp. It also keeps the floor under the panels bright enough that the
       words hold their contrast through the loudest bar of the song. */
    --song-preview-veil: rgba(255, 255, 255, 0.42);
    /* The canvas is painted behind the panels and must not escape the
       rounded corners, nor sit above the navbar's stacking context. */
    isolation: isolate;
    overflow: hidden;
    margin: 0 0 var(--space-7, 32px);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    box-shadow: var(--shadow-md);
    /* What a browser with no WebGL2 keeps — and what shows for the split
       second before the first frame is drawn. */
    background: var(--grad-dreamy);
}

.song-preview-scene {
    position: absolute;
    inset: 0;
    z-index: 0;
    display: block;
    width: 100%;
    height: 100%;
}

.song-preview::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: var(--song-preview-veil);
}

/* The brand dot, deepened for this one surface: `--accent` *is* the scene's
   own colour, so the usual dot disappears whenever a coral wave passes under
   it. `--coral-700` keeps it a mark (design-language §1 — the deeper stops are
   what the palette provides for exactly this). */
.song-preview-heading::before {
    background: var(--coral-700);
}

/* The gate (#169) — the choice a visitor makes before anything plays, and the
   panel that comes back with the platforms when a preview ends. It sits over
   the scene on the card's own veil rather than a scrim of its own: the words
   behind it stay readable, because it is a choice, not a modal. */
.song-preview-gate {
    position: absolute;
    inset: 0;
    z-index: 3;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-4, 16px);
    padding: clamp(16px, 3vw, 28px);
}

/* While the choice is up, the card is the scene and the choice. The words and
   the transport leave the layout rather than sit behind the panel: two buttons
   over a lyric sheet and a player is four things offered and one asked for.
   The card keeps a height of its own, since the body was providing it. */
.song-preview--gated {
    min-height: min(58vh, 460px);
}

.song-preview--gated .song-preview-body {
    display: none;
}

/* Playing it again after a preview: the quiet option, so it reads as a link at
   the bottom of the card rather than a button competing with the platforms. */
.song-preview-again {
    margin-top: auto;
    padding: 4px 8px;
    border: 0;
    background: none;
    font: inherit;
    font-size: var(--fs-small, 15px);
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

.song-preview-again:hover {
    color: var(--link-hover);
}

.song-preview-again:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
    border-radius: var(--radius-sm, 8px);
}

.song-preview-again[hidden] {
    display: none;
}

.song-preview-gate-actions[hidden] {
    display: none;
}

.song-preview-gate[hidden] {
    display: none;
}

.song-preview-gate-panel {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3, 12px);
    max-width: 34rem;
    padding: clamp(18px, 3vw, 28px);
    text-align: center;
    background: var(--glass-bg-strong);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    box-shadow: var(--shadow-md);
}

.song-preview-gate-title,
.song-preview-gate-done {
    margin: 0;
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(18px, 2.4vw, 22px);
}

.song-preview-gate-done[hidden],
.song-preview-gate-links[hidden] {
    display: none;
}

.song-preview-gate-actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--space-2, 8px);
}

/* The platforms are a `streaming-links` column (#182); all this adds is that
   the column is centred in the panel rather than left in it. */
.song-preview-gate-links {
    align-self: center;
}

.song-preview-body {
    position: relative;
    z-index: 2;
    display: flex;
    flex-direction: column;
    gap: var(--space-4, 16px);
    padding: clamp(16px, 3vw, 28px);
}

.song-preview-lyrics {
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-md, 18px);
    padding: clamp(16px, 2.5vw, 24px);
    color: var(--text-soft);
    line-height: 1.9;
}

/* Shorter than the standalone sheet's window: here the transport and the
   links sit under it, and all three have to be on screen together for the
   card to read as one surface. */
.song-preview-lyrics .song-lyrics-content--lines[data-synced="1"] {
    max-height: min(46vh, 380px);
}

/* The transport is the shared player, already a glass card — inside this one
   it only loses the margin it uses when it stands alone. */
.song-preview .portfolio-player {
    margin: 0;
}

.song-preview-out {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3, 12px);
    padding: 0 var(--space-2, 8px);
}

.song-preview-out-label {
    margin: 0;
    font-weight: 600;
    color: var(--text);
}

.song-preview .portfolio-links {
    margin: 0;
}

/* The links sit on the scene rather than on the page, so they carry their own
   ground instead of the page's white. */
.song-preview .portfolio-link-btn {
    background: var(--glass-bg);
}

@media (max-width: 720px) {
    .song-preview-lyrics .song-lyrics-content--lines[data-synced="1"] {
        max-height: min(52vh, 300px);
    }

    .song-preview-out {
        gap: var(--space-2, 8px);
    }
}

/* ============================================================
   THE SONG'S WORLD (#169) — the project under the preview card,
   and the artists under that. The artist cards are the release
   page's own component; only the release card is new here, and
   it is the song hub's card with every staff-only signal cut.
   Its own class names: the hub's `.song-project-card` lives in
   projects.css, which this page loads for the player.
   ============================================================ */
.song-project-panel {
    display: flex;
    gap: clamp(16px, 3vw, 28px);
    align-items: flex-start;
    padding: clamp(16px, 2.5vw, 24px);
    background: var(--glass-bg);
    border: 1px solid var(--glass-border);
    border-radius: var(--radius-lg, 28px);
    box-shadow: var(--shadow-sm);
}

.song-project-panel-cover {
    flex: 0 0 auto;
    display: block;
    width: 132px;
    aspect-ratio: 1 / 1;
    border-radius: var(--radius-md, 18px);
    overflow: hidden;
    background: var(--tint-20);
}

.song-project-panel-cover img {
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.song-project-panel-body {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 6px;
    min-width: 0;
}

.song-project-panel-title {
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(18px, 2.2vw, 22px);
    color: var(--text);
    text-decoration: none;
}

.song-project-panel-title:hover {
    color: var(--link);
}

.song-project-panel-artists,
.song-project-panel-meta {
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-small, 15px);
}

.song-project-panel-artists a {
    color: var(--link);
}

.song-project-panel-brief {
    margin: 4px 0 8px;
    color: var(--text-soft);
    line-height: 1.7;
    white-space: pre-line;
}

.song-project-panel .portfolio-link-btn {
    margin-top: 4px;
}

@media (max-width: 560px) {
    .song-project-panel {
        flex-direction: column;
    }

    .song-project-panel-cover {
        width: 96px;
    }
}

/* The songs either side of this one on its release (#298), under the project
   card. The label is the card's own kicker without its dot, so "track 2" reads
   beside the card's "track 3". Next keeps to the right edge even on the first
   song, where nothing sits beside it, so each arrow stays on the side it
   points to. */
.song-steps {
    display: flex;
    justify-content: space-between;
    gap: var(--space-4, 16px);
    margin-top: var(--space-4, 16px);
}

.song-step {
    flex: 0 1 50%;
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    min-width: 0;
    padding-block: var(--space-2, 8px);
    color: var(--text);
    text-decoration: none;
}

.song-step--next {
    margin-left: auto;
    align-items: flex-end;
    text-align: right;
}

.song-step-label {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-label);
    text-transform: lowercase;
    color: var(--text-muted);
}

.song-step-title {
    font-weight: 600;
    overflow-wrap: anywhere;
}

.song-step:hover .song-step-title,
.song-step:focus-visible .song-step-title {
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 3px;
}

/* ---- the walkthrough's stage bar (#112, B6) ----------------------
   A long article that walks one thing through several stages needs a way back
   and forward that is always within reach. Above the reading breakpoint the
   contents list is already beside the article marking the section being read,
   so the bar would be a second answer to the same question — it only exists
   below 1024px, where that column is gone and the reader's thumb is at the
   bottom of the screen anyway.

   It ships `hidden` and components.js clears that once it knows which stage is
   on screen (blog/_stage_bar.html). */
.blog-stagebar {
    display: none;
}

@media (max-width: 1023px) {
    .blog-stagebar {
        display: flex;
        position: fixed;
        z-index: var(--z-sticky);
        left: 50%;
        transform: translateX(-50%);
        /* Two things already live at the bottom of a phone screen, and the bar
           gives way to both rather than competing.

           The WhatsApp float is a 60px circle at `--space-7` from the bottom
           right (whatsapp_fb.css), and a centred bar this wide runs straight
           under it — so the bar sits *above* it rather than beside it, which is
           the only arrangement that holds at every screen width. The
           safe-area inset keeps it clear of a phone's home indicator. */
        bottom: calc(
            var(--space-7, 32px) + 60px + var(--space-3, 12px) +
            env(safe-area-inset-bottom, 0px)
        );
        width: min(calc(100vw - 2 * var(--space-4, 16px)), 26rem);
        align-items: center;
        gap: var(--space-3, 12px);
        padding: var(--space-2, 8px) var(--space-3, 12px);
        border: 1px solid var(--glass-border);
        border-radius: var(--radius-pill);
        background: var(--glass-bg-strong);
        backdrop-filter: blur(12px);
        box-shadow: var(--shadow-md);
    }

    /* A `display` set here would otherwise beat `[hidden]` and paint a bar
       that knows nothing yet (conventions.md §3). */
    .blog-stagebar[hidden] {
        display: none;
    }

    /* The cookie banner is a decision the reader has to make once, and it
       takes the same corner of the screen. The bar is navigation for a page
       they are already on — it can wait, and floating a second control over a
       consent choice is the kind of thing that gets one of them tapped by
       accident. */
    body:has(#dd-consent:not([hidden])) .blog-stagebar {
        display: none;
    }
}

.blog-stagebar__step {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: 0 0 auto;
    width: 2rem;
    height: 2rem;
    border-radius: var(--radius-pill);
    color: var(--text);
}

.blog-stagebar__step[hidden] {
    display: none;
}

.blog-stagebar__at {
    display: flex;
    align-items: baseline;
    gap: var(--space-2, 8px);
    flex: 1 1 auto;
    min-width: 0;
    margin: 0;
    justify-content: center;
}

.blog-stagebar__count {
    font-family: var(--font-mono);
    font-size: var(--fs-label, 13px);
    color: var(--text-muted);
    flex: 0 0 auto;
}

.blog-stagebar__title {
    font-size: var(--fs-small, 15px);
    /* A stage name is short, but it is author text and this box is a phone's
       width — one line, clipped, rather than a bar that changes height. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
