/* P14.2: status accents are token-driven (was raw hex). */
.entity-card.not-booked {
    border-left: 4px solid var(--status-not-booked);
}

.entity-card.booked {
    border-left: 4px solid var(--status-booked);
}

.entity-card.backlog {
    border-left: 4px solid var(--status-backlog);
}

.entity-card.in-progress {
    border-left: 4px solid var(--status-in-progress);
}

.entity-card.completed {
    border-left: 4px solid var(--status-completed);
}

.entity-card.canceled {
    border-left: 4px solid var(--status-canceled);
}

/* Published to the public portfolio — the headline state that outranks the
   work-status border above; green to match its chip. */
.entity-card.published {
    border-left: 4px solid var(--success);
}

.status-not-booked {
    background: color-mix(in srgb, var(--status-not-booked) 16%, transparent);
    color: color-mix(in srgb, var(--status-not-booked) 55%, var(--ink));
}

.status-booked {
    background: color-mix(in srgb, var(--status-booked) 16%, transparent);
    color: color-mix(in srgb, var(--status-booked) 60%, var(--ink));
}

.status-backlog {
    background: color-mix(in srgb, var(--status-backlog) 16%, transparent);
    color: color-mix(in srgb, var(--status-backlog) 60%, var(--ink));
}

.status-in-progress {
    background: color-mix(in srgb, var(--status-in-progress) 16%, transparent);
    color: color-mix(in srgb, var(--status-in-progress) 60%, var(--ink));
}

.status-completed {
    background: color-mix(in srgb, var(--status-completed) 16%, transparent);
    color: color-mix(in srgb, var(--status-completed) 60%, var(--ink));
}

.status-canceled {
    background: color-mix(in srgb, var(--status-canceled) 16%, transparent);
    color: color-mix(in srgb, var(--status-canceled) 60%, var(--ink));
}

/* Completes the global hue set so hub session badges (declined / no-show map
   to status_class "declined") render here too — this file is loaded on the
   client session pages, sessions.css's #sessions-list-scoped copy is not. */
.status-declined {
    background: color-mix(in srgb, var(--status-declined) 16%, transparent);
    color: color-mix(in srgb, var(--status-declined) 60%, var(--ink));
}

/* Published on the public /work/ portfolio — brand-coral pill. */
.status-public {
    background: var(--tint-20);
    color: var(--link);
}

.project-filter .filter-chip.active[data-value="not-booked"] {
    background: var(--status-not-booked);
    border-color: var(--status-not-booked);
    color: var(--text-on-coral);
    box-shadow: var(--shadow-sm);
}

.project-filter .filter-chip.active[data-value="booked"] {
    background: var(--status-booked);
    border-color: var(--status-booked);
    color: var(--text-on-coral);
    box-shadow: var(--shadow-sm);
}

.project-filter .filter-chip.active[data-value="backlog"] {
    background: var(--status-backlog);
    border-color: var(--status-backlog);
    color: var(--text-on-coral);
    box-shadow: var(--shadow-sm);
}

.project-filter .filter-chip.active[data-value="in-progress"] {
    background: var(--status-in-progress);
    border-color: var(--status-in-progress);
    color: var(--text-on-coral);
    box-shadow: var(--shadow-sm);
}

.project-filter .filter-chip.active[data-value="completed"] {
    background: var(--status-completed);
    border-color: var(--status-completed);
    color: var(--text-on-coral);
    box-shadow: var(--shadow-sm);
}

.project-filter .filter-chip.active[data-value="canceled"] {
    background: var(--status-canceled);
    border-color: var(--status-canceled);
    color: var(--text-on-coral);
    box-shadow: var(--shadow-sm);
}

.project-card-brief,
.project-summary-brief {
    font-size: 13px;
    line-height: 1.55;
    color: var(--text-muted);
    margin-bottom: 1.25em;
}

.project-card-inner {
    display: flex;
    align-items: flex-start;
    gap: 1.25em;
}

.project-card-content {
    flex: 1 1 auto;
    min-width: 0;
}

.project-card-artwork {
    display: block;
    width: 108px;
    aspect-ratio: 1;
    flex: 0 0 108px;
    overflow: hidden;
    border-radius: var(--radius-md);
    border: 1px solid var(--border);
    background: var(--surface-tint);
    box-shadow: var(--shadow-md);
}

.project-card-artwork img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.project-summary-panel {
    margin: -0.75em 0 1.5em;
    padding: 1.5em 1.75em;
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(255, 170, 168, 0.14) 100%);
    border: 1.5px solid rgba(255, 170, 168, 0.28);
    border-radius: 20px;
    box-shadow: 0 10px 30px rgba(255, 170, 168, 0.12), 0 4px 16px rgba(0, 0, 0, 0.04);
}

.project-summary-main {
    display: flex;
    align-items: flex-start;
}

.project-summary-body {
    display: flex;
    align-items: flex-start;
    gap: 1.25em;
    min-width: 0;
    width: 100%;
}

.project-summary-artwork {
    display: block;
    width: 132px;
    aspect-ratio: 1;
    flex: 0 0 132px;
    overflow: hidden;
    border-radius: 14px;
    border: 1px solid rgba(0, 0, 0, 0.08);
    background: rgba(0, 0, 0, 0.03);
    box-shadow: 0 10px 24px rgba(0, 0, 0, 0.08);
}

.project-summary-artwork img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
}

.project-summary-info {
    min-width: 0;
    flex: 1 1 auto;
}

/* Free-text notes under the summary header (hub session detail). */
.project-summary-notes {
    margin-top: var(--space-3);
}

.project-summary-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 1em;
}

.project-summary-heading {
    min-width: 0;
    flex: 1 1 auto;
}

.project-summary-info .entity-meta-row {
    margin-bottom: 0.85em;
}

.project-summary-info .entity-meta-row:not(.project-duration-row) .meta-item:first-child {
    font-size: 17px;
    font-weight: 600;
    color: var(--text-soft);
}

.project-summary-brief {
    white-space: pre-line;
    margin-bottom: 0;
}

.project-summary-title {
    font-family: 'Archivo Black', sans-serif;
    font-size: clamp(20px, 2.5vw, 30px);
    color: var(--text);
    margin-bottom: 0.5em;
}

.project-summary-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.55em;
    flex-wrap: wrap;
    flex: 0 0 auto;
}

.project-hub-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.25em;
}

.project-hub-section {
    background: white;
    border: 1.5px solid var(--glass-border);
    border-radius: 18px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
    padding: 1.5em;
}

.project-hub-section--scrollable {
    display: flex;
    flex-direction: column;
    height: 480px;
    min-height: 0;
}

.project-hub-section--songs {
    grid-column: 1 / -1;
}

.project-hub-section--full-width {
    grid-column: 1 / -1;
}

.project-hub-section--songs .project-hub-list {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75em;
    max-height: 460px;
    overflow-y: auto;

    scrollbar-gutter: stable both-edges;
}

/* A lone song shouldn't sit in half the section width */
.project-hub-section--songs .project-hub-row:only-child {
    grid-column: 1 / -1;
}

.project-hub-section--songs .project-hub-row-body {
    min-width: 0;
    flex: 1 1 0;
}

/* tracklist cards only (NOT the assets tab, which reuses --songs): a column
   body filling the equal-height card so the progress hint pins to the bottom */
#project-songs-section .project-hub-row {
    align-items: stretch;
}
#project-songs-section .project-hub-row-body {
    display: flex;
    flex-direction: column;
    align-self: stretch;
}

.project-hub-section--songs .project-hub-row-actions {
    flex-shrink: 0;
}

.project-song-brief {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 0.35em;
    line-height: 1.45;
}

.project-hub-section-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1em;
    margin-bottom: 1em;
    padding-bottom: 0.85em;
    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
    flex: 0 0 auto;
}

.project-hub-section-header h3 {
    font-family: 'Archivo Black', sans-serif;
    font-size: 18px;
    color: var(--text);
}

.project-hub-section-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.5em;
    flex-wrap: wrap;
    margin-left: auto;
}

.project-hub-row {
    background: rgba(0, 0, 0, 0.02);
    border: 1px solid rgba(0, 0, 0, 0.06);
    border-radius: 14px;
    padding: 1em;
}

/* The two lines a row prints, named for what they are (#253).

   Both were written as `.project-hub-row span` / `strong` — one class *and* an
   element, 0-1-1, heavier than any single component class. So every `dd-`
   component and every pill that landed in a row lost to the row: `.project-pill`
   was drawn as a muted block line instead of a pill, the song hub's
   `.status-badge` lost its colour, and three separate places in the tree had
   grown selectors or inline styles to win the fight. Same values, on a class,
   so a component in a row is styled by the component again.

   They had a twin set for the facts grid until #260, which deleted the one
   template that grid was ever rendered in. */
.project-hub-row-meta {
    display: block;
    font-size: 12px;
    color: var(--text-muted);
    margin-top: 0.2em;
}

.project-hub-row-name {
    font-size: 14px;
    color: var(--text);
}

.project-hub-list {
    display: flex;
    flex-direction: column;
    gap: 0.75em;
}

.project-hub-section--scrollable .project-hub-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding-right: 0.25em;
    scrollbar-gutter: stable;
}

/* The edge fade's host (#201) stands where the list stood in the section's
   column, and hands the height straight back down to it. */
.project-hub-section--scrollable > .dd-scroll-fade {
    flex: 1 1 auto;
    min-height: 0;
}

.project-hub-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1em;
}

/* The asset URL printed under a row — a long, wrapping, unclickable-looking
   string, which is why it is small, coral and breaks anywhere.

   It was written as `.project-hub-row a`, which is one class *and* an element:
   heavier than any single component class, so it captured every link that ever
   landed in a row. Seventeen of them are `dd-btn`s, and they were being drawn
   as coral 12px text on a coral fill — `view song.` on a track list was very
   nearly illegible. Naming the one element it is for is the whole fix; a row's
   buttons style themselves from the kit again. */
.project-hub-row-url {
    display: block;
    max-width: 420px;
    overflow-wrap: anywhere;
    font-size: 12px;
    color: var(--primary);
    margin-top: 0.25em;
}

.project-hub-row-actions {
    display: flex;
    align-items: center;
    gap: 0.5em;
    flex-wrap: wrap;
    justify-content: flex-end;
}

/* The tint/foreground pair is `.status-published`'s, which is `notice.css`'s
   `--success` notice — the same pill at the same 12px/600, so this is the third
   user of one green rather than a third green (#259). It was written as
   `rgba(16, 185, 129, 0.12)` on `#059669`: emerald-500 at 12% under
   emerald-600, neither of them a token, and emerald-600 not a colour this
   palette has anywhere. Nobody saw it because the nine pills outside a hub row
   were emerald while the five inside one were being repainted `--text-muted` by
   `.project-hub-row span`; #253 stopped the row doing that, all fourteen agreed,
   and the green they agreed on was the wrong one. */
.project-pill {
    display: inline-flex;
    align-items: center;
    padding: 4px 10px;
    border-radius: 50px;
    background: color-mix(in srgb, var(--success) 14%, transparent);
    color: var(--success);
    font-size: 12px;
    font-weight: 600;
}

.project-hub-empty {
    color: var(--text-muted);
    font-size: 13px;
}

@media (max-width: 800px) {

    .project-card-artwork {
        width: 88px;
        flex-basis: 88px;
    }

    .project-hub-grid {
        grid-template-columns: 1fr;
    }

    .project-hub-section--scrollable {
        height: 420px;
    }

    .project-hub-section--songs .project-hub-list {
        grid-template-columns: 1fr;
    }

    .project-summary-main,
    .project-hub-row,
    .project-hub-section-header {
        flex-direction: column;
        align-items: flex-start;
    }

    .project-summary-body {
        width: 100%;
    }

    .project-summary-header {
        flex-direction: column;
        align-items: flex-start;
    }

    .project-summary-actions {
        justify-content: flex-start;
    }

    .project-summary-artwork {
        width: 96px;
        flex-basis: 96px;
    }

    .project-hub-row-actions {
        justify-content: flex-start;
    }
}

@media (max-width: 520px) {
    .project-card-inner.has-artwork {
        flex-direction: column;
    }

    .project-card-artwork {
        width: 100%;
        max-width: 180px;
        flex-basis: auto;
    }

    .project-summary-body {
        flex-direction: column;
    }

    .project-summary-artwork {
        width: 100%;
        max-width: 220px;
        flex-basis: auto;
    }
}

/* ── Shared audio player (used by both song hub and project hub) ── */

.song-player-panel {
    margin-top: 1.1em;
    padding-top: 1.1em;
    border-top: 1px solid rgba(255, 170, 168, 0.3);
}

.song-player-inner {
    display: flex;
    align-items: center;
    gap: 0.55em;
}

.player-play-btn {
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(255, 170, 168, 0.25);
    border: 1.5px solid rgba(255, 170, 168, 0.45);
    border-radius: 50%;
    cursor: pointer;
    padding: 0;
    transition: background 0.15s;
    color: var(--text);
}

.player-play-btn:hover {
    background: rgba(255, 170, 168, 0.45);
}

.player-transport {
    flex: 1;
    display: flex;
    align-items: center;
    gap: 0.5em;
    min-width: 0;
}

.player-time {
    font-size: 11px;
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    flex-shrink: 0;
}

.player-seek-bar {
    flex: 1;
    min-width: 0;
    height: 4px;
    cursor: pointer;
    accent-color: rgba(220, 100, 100, 0.7);
}

@media (max-width: 520px) {
    .song-player-inner {
        flex-wrap: wrap;
    }

    /* Break the transport onto its own full-width line so the seek bar gets
       room to breathe (issue #55). flex-basis (not width) is what governs a
       flex item's main size — `flex: 1` sets basis 0, so `width: 100%` alone
       was ignored and the transport stayed cramped beside the controls. */
    .player-transport {
        flex-basis: 100%;
    }
}

/* ── Project playlist player ── */

.project-player-title-row {
    display: flex;
    align-items: center;
    gap: 0.6em;
    margin-bottom: 0.6em;
}

.project-player-song-title {
    font-weight: 600;
    font-size: 13px;
    color: var(--text);
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.project-player-kind-label {
    font-size: 11px;
    font-weight: 600;
    padding: 2px 8px;
    border-radius: 999px;
    background: rgba(255, 170, 168, 0.18);
    color: var(--text-muted);
    flex-shrink: 0;
}

.project-player-track-count {
    font-size: 11px;
    color: var(--text-muted);
    flex-shrink: 0;
    font-variant-numeric: tabular-nums;
}

.player-prev-next-btn {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1.5px solid rgba(255, 170, 168, 0.35);
    border-radius: 50%;
    cursor: pointer;
    padding: 0;
    transition: background 0.15s;
    color: var(--text);
}

.player-prev-next-btn:hover {
    background: rgba(255, 170, 168, 0.2);
}

.player-prev-next-btn:disabled {
    opacity: 0.3;
    cursor: not-allowed;
}

.player-loop-btn {
    position: relative;
    flex-shrink: 0;
    width: 30px;
    height: 30px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: transparent;
    border: 1.5px solid var(--glass-border, rgba(255, 170, 168, 0.3));
    border-radius: 50%;
    cursor: pointer;
    padding: 0;
    color: var(--text-muted);
    transition:
        background 0.2s ease,
        border-color 0.2s ease,
        color 0.2s ease,
        box-shadow 0.25s ease,
        transform 0.15s ease;
}

.player-loop-btn:hover:not(:disabled) {
    background: var(--primary-extra-light, rgba(255, 170, 168, 0.2));
    border-color: var(--primary, #FFAAA8);
    color: var(--text);
}

.player-loop-btn:active:not(:disabled) {
    transform: scale(0.94);
}

.player-loop-btn.is-active {
    background: var(--primary, #FFAAA8);
    border-color: var(--primary-dark, #FF8B88);
    color: var(--text);
    box-shadow: 0 0 0 3px rgba(255, 170, 168, 0.28), 0 4px 10px rgba(255, 139, 136, 0.25);
}

/* The little corner dot moved to `::before` (#208): the tooltip owns
   `::after`, and the loop button only clashed with it once JS had added
   `.is-active` — a state no template scan can see, so it would have been
   a tooltip that worked until you turned looping on. */
.player-loop-btn.is-active::before {
    content: '';
    position: absolute;
    bottom: -3px;
    right: -3px;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--primary-dark, #FF8B88);
    border: 1.5px solid var(--white, #FFFFFF);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
}

.player-loop-btn.is-active:hover:not(:disabled) {
    background: var(--primary-dark, #FF8B88);
    border-color: var(--primary-dark, #FF8B88);
}

.player-loop-btn:disabled {
    opacity: 0.35;
    cursor: not-allowed;
}

.project-title-edit {
    display: flex;
    align-items: baseline;
    gap: 0.4em;
    flex-wrap: wrap;
    margin-bottom: 0;
}

.project-title-edit-btn {
    display: inline-flex;
    text-decoration: none;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--text-muted, #888);
    cursor: pointer;
    flex-shrink: 0;
    transition: color 0.15s ease;
}

.project-title-edit-btn svg {
    width: 13px;
    height: 13px;
}

.project-title-edit-btn:hover {
    color: var(--primary);
}

.project-songs-actions {
    display: flex;
    align-items: center;
    gap: 8px;
}

.project-reorder-toggle-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 30px;
    height: 30px;
    padding: 0;
    border: 1px solid rgba(0, 0, 0, 0.1);
    border-radius: 50%;
    background: #fff;
    color: var(--text-muted, #666);
    cursor: pointer;
    transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
}

.project-reorder-toggle-btn svg {
    width: 14px;
    height: 14px;
}

.project-reorder-toggle-btn:hover {
    color: var(--primary);
    border-color: var(--primary);
}

.project-reorder-toggle-btn[aria-pressed="true"] {
    background: var(--primary);
    color: #fff;
    border-color: var(--primary);
}

.project-song-title-line {
    display: block;
    min-width: 0;
}

.project-song-title-display {
    white-space: pre-wrap;
}

.project-song-title-edit-btn {
    display: inline-flex;
    text-decoration: none;
    align-items: center;
    justify-content: center;
    width: 18px;
    height: 18px;
    padding: 0;
    margin-left: 0.4em;
    border: none;
    border-radius: 50%;
    background: transparent;
    color: var(--text-muted, #888);
    cursor: pointer;
    vertical-align: baseline;
    transition: color 0.15s ease;
}

.project-song-title-edit-btn svg {
    width: 12px;
    height: 12px;
}

.project-song-title-edit-btn:hover {
    color: var(--primary);
}

#project-songs-section.reorder-on .project-hub-row.is-draggable {
    cursor: grab;
    background: rgba(255, 170, 168, 0.06);
    border-radius: 12px;
    transition: background 0.15s ease;
}

#project-songs-section.reorder-on .project-hub-row.is-draggable:hover {
    background: rgba(255, 170, 168, 0.14);
}

#project-songs-section.reorder-on .project-hub-row.is-dragging {
    opacity: 0.55;
    cursor: grabbing;
}

#project-songs-section.reorder-on .project-hub-row-actions,
#project-songs-section.reorder-on .project-song-title-edit-btn {
    pointer-events: none;
    opacity: 0.45;
}

/* ============================================
   PROJECT HUB — OPEN / FLOWING VARIANT
   Mirrors the borderless feel of the client home hub.
   Scoped to .project-hub-flow so other hubs (artist/song/job)
   keep their existing boxed layout.
   ============================================ */

.project-hub-breadcrumb {
    display: inline-flex;
    margin-bottom: 1em;
}

.project-summary-panel--flow {
    margin-top: 0;
    background: linear-gradient(135deg, rgba(255, 255, 255, 0.98) 0%, rgba(255, 170, 168, 0.08) 100%);
    border: 1px solid var(--glass-border);
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.04);
}

.project-hub-flow {
    display: flex;
    flex-direction: column;
    gap: 2.25em;
}

/* Pinned sections are cards here too (owner, 2026-09-01: the project hub's flow
   sections "don't look great when pinned"). Every section in this flow is a
   pinned one — that is what the flow is — so the borderless treatment below now
   reaches nothing in it, and is kept for the shapes that predate the slot. */
.project-hub-flow > .project-hub-section,
.project-hub-flow-pair > .project-hub-section {
    background: transparent;
    border: none;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
    height: auto;
    min-height: 0;
    overflow: visible;
    display: flex;
    flex-direction: column;
    gap: 0.75em;
}

/* The flow no longer draws its own heavier coral rule under a section's title
   (owner, 2026-09-07). That rule was for a borderless section sitting straight
   on the page, where it was the only thing marking the title off from the list
   under it. Inside a slot it is a second line drawn inside a card that already
   has an edge — an ornament, next to the quiet hairline the song hub's sections
   have always had, because nothing there ever overrode the default. The default
   `.project-hub-section-header` draws is the one both hubs use now. */
.project-hub-flow-pair .project-hub-section-header {
    margin-bottom: 0;
    padding-bottom: 0.6em;
    border-bottom: 1.5px solid rgba(255, 170, 168, 0.35);
}

.project-hub-flow-pair .project-hub-section-header {
    min-height: 50px;
}

.project-hub-flow .project-hub-section--scrollable .project-hub-list,
.project-hub-flow-pair .project-hub-section--scrollable .project-hub-list {
    flex: none;
    max-height: none;
    overflow-y: visible;
    padding-right: 0;
}

.project-hub-flow .project-hub-section--songs .project-hub-list {
    max-height: none;
    overflow-y: visible;
    padding-right: 0;
    gap: 0.625em;
}

.project-hub-flow-pair {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1.5em 2em;
    align-items: start;
}

.project-hub-flow .project-hub-row,
.project-hub-flow-pair .project-hub-row {
    background: rgba(255, 255, 255, 0.85);
    border: 1.5px solid var(--glass-border);
    border-radius: 14px;
    padding: 0.75em 0.875em 0.75em 1em;
    transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
}

.project-hub-flow .project-hub-row:hover,
.project-hub-flow-pair .project-hub-row:hover {
    background: white;
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.07);
    transform: translateY(-1px);
    border-color: var(--primary-light, rgba(255, 170, 168, 0.5));
}

.project-hub-flow .project-hub-empty,
.project-hub-flow-pair .project-hub-empty {
    padding: 0.25em 0;
}

@media (max-width: 800px) {
    .project-hub-flow-pair {
        grid-template-columns: 1fr;
        gap: 2.25em;
    }
}

/* ============================================================
   P20.2 — genre chips + at-a-glance song progress hint
   (loaded by both project_detail and song_detail; token-only)
   ============================================================ */
.genre-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: 0 0 0.85em;
    padding: 0;
    list-style: none;
}
.genre-tag {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 0.2em 0.7em;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-wide);
    color: var(--coral-800);
    background: var(--coral-100);
    border: 1px solid var(--coral-200);
    border-radius: var(--radius-pill);
}
.genre-tag::before {
    content: "";
    width: 0.4em;
    height: 0.4em;
    border-radius: var(--radius-pill);
    background: var(--accent);
}

/* tracklist progress hint — its own line under the title, the glyph to the
   LEFT of the label. */
.song-progress {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: max-content;
    max-width: 100%;
    /* pin to the bottom-left of the card — tight above the label, roomier below */
    margin-top: auto;
    padding-top: var(--space-2);
    padding-bottom: var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-wide);
    line-height: 1;
}

/* hintless songs (backlog / not booked / canceled) render an invisible row
   of the same height, so tracklist cards match with or without a chip (#66) */
.song-progress--placeholder {
    visibility: hidden;
}

/* booked / upcoming — muted, with a small "stop"-style square marker */
.song-progress--booked {
    color: var(--text-muted);
}
.song-progress-square {
    flex: 0 0 auto;
    display: block;
    width: 0.5em;
    height: 0.5em;
    border-radius: 0.12em;
    background: currentColor;
}

/* in progress — accent text + a slowly pulsing dot to its left */
.song-progress--in_progress {
    color: var(--coral-700);
}
.song-progress-dot {
    flex: 0 0 auto;
    display: block;
    width: 0.5em;
    height: 0.5em;
    border-radius: var(--radius-pill);
    background: var(--accent-strong);
    /* a slow, soft "breath" — alive, not an alert */
    animation: song-progress-pulse 3.2s ease-in-out infinite;
}
@keyframes song-progress-pulse {
    0%, 100% { opacity: 0.6;  transform: scale(0.94); }
    50%      { opacity: 1;    transform: scale(1.06); }
}

/* completed — success colour + check to the left, past tense */
.song-progress--completed {
    color: var(--success);
}
.song-progress-check {
    flex: 0 0 auto;
    width: 1em;
    height: 1em;
}

/* ---- Project list headline states (shares this chip) --------------------
   The projects list reuses this chip for a project's one headline status.
   "published" is the headline state that outranks every work status — it reads
   green (same as the songs list's completed chip), with a solid dot marker so
   it stays distinct from the green ✓ of a completed project. */
.song-progress--published {
    color: var(--success);
}
.song-progress--published .song-progress-dot {
    background: currentColor;
    animation: none;
}

/* not booked / backlog / canceled — the quiet end of the scale */
.song-progress--not_booked,
.song-progress--backlog,
.song-progress--canceled {
    color: var(--text-muted);
}
.song-progress--canceled {
    opacity: 0.75;
}

/* hollow ring — nothing scheduled yet (not booked / backlog) */
.song-progress-ring {
    flex: 0 0 auto;
    display: block;
    width: 0.5em;
    height: 0.5em;
    border-radius: var(--radius-pill);
    border: 1.5px solid currentColor;
}

/* small cross — canceled */
.song-progress-cross {
    flex: 0 0 auto;
    width: 0.82em;
    height: 0.82em;
}

/* credits & world — grouped by role: a mono role heading + a list of names */
.credit-groups {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
}
/* Credit families (#288) — the same four as the public pages, so the hub and the
   page read a credit list in one order. The division is a hairline, and the
   family label sits above the role labels it gathers: one step larger in
   weight's place, so the hierarchy reads without a second colour. */
.credit-family + .credit-family {
    margin-top: var(--space-5);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

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

.credit-family .credit-group + .credit-group {
    margin-top: var(--space-4);
}

.credit-group-role {
    margin: 0 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}
/* studio roles are kept but never published (#281) — the heading says so, so
   nobody has to wonder why the name is missing from the public page */
.credit-group-tag {
    margin-left: var(--space-2);
    padding: 0.1em 0.45em;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: 0.85em;
    font-weight: 500;
    letter-spacing: normal;
    text-transform: lowercase;
    color: var(--text-muted);
}
/* Where the work was done, beside whoever did it (#288) — quieter than the name,
   and the reason the `internal` tag is gone: a room is no longer a credit of its
   own to mark as unpublished. */
.credit-room {
    color: var(--text-muted);
    font-weight: 400;
}

.credit-group-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
}
.credit-group-list li {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}
.credit-name {
    color: var(--text);
    font-weight: 500;
}
/* compact, quiet edit affordance that only asserts itself on hover/focus */
.credit-edit-btn {
    padding: 0.15em 0.6em;
    font-size: var(--fs-label);
    opacity: 0;
    transition: opacity var(--dur-base) var(--ease-soft);
}
.credit-group-list li:hover .credit-edit-btn,
.credit-edit-btn:focus-visible {
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    .song-progress-dot { animation: none; opacity: 1; }
    .credit-edit-btn { transition: none; }
}

/* touch / coarse pointers can't hover — keep the edit button visible */
@media (hover: none) {
    .credit-edit-btn { opacity: 1; }
}

/* ============================================================
   P20.2 — the three lower project sections inside dd-tabs.
   credits & world (default) · project assets · linked artists.
   Each tab's content is laid out in TWO COLUMNS.
   ============================================================ */
.project-hub-tabs {
    margin-top: var(--space-7);
}

/* inside a tab the panel IS the container — drop the section's card chrome
   and its fixed height so content flows naturally under the tab */
.project-hub-tabs .project-hub-section {
    height: auto;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    display: flex;
    flex-direction: column;
}

/* the tab label is the heading now — hide the section's own h3 */
.project-hub-tabs .project-hub-section-header h3 {
    display: none;
}
/* the action row drops to the BOTTOM-left of the tab, below the content */
.project-hub-tabs .project-hub-section-header {
    order: 2;
    justify-content: flex-start;
    border-bottom: 0;
    border-top: 1px solid var(--border);
    margin: var(--space-5) 0 0;
    padding: var(--space-4) 0 0;
}

/* ...which only works for a section that is a flex column with no height of its
   own — the shape `.project-hub-tabs .project-hub-section` gives it above. The
   lyrics panel is neither: it is a clamp with a fixed max-height, laid out in
   ordinary flow. `order` does nothing there, so the chrome meant to sit *under*
   the content landed on top of it — a divider, an empty band, and the buttons a
   block lower than they belong (#204, owner 2026-09-01). Nor could the row move
   to the bottom even if it could be ordered: the bottom of a clamp is the part
   that is clipped. So it keeps its own header here, heading hidden like every
   other tab. */
.project-hub-tabs .song-lyrics-panel > .project-hub-section-header {
    order: 0;
    justify-content: space-between;
    margin-bottom: 1em;
    margin-top: 0;
    padding: 0 0 0.85em;
    border-top: 0;
    border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.project-hub-tabs .project-hub-section-actions {
    justify-content: flex-start;
    margin-left: 0;
}
/* the "+" add is always the left-most action */
.project-hub-tabs .dd-btn--add {
    order: -1;
}
/* …except where the heading beside it is showing. The rule above is for a
   tab's bottom action row, where the section heading is hidden and the "+" is
   the first thing in the row there is. display.'s cover-art block keeps a
   visible `cover art.` subheading of its own — same case as the lyrics panel
   above — so its add button (#258) reads as belonging to that heading rather
   than sitting in front of it. `your own lines.` on release. is the same shape
   again (#200): a heading inside the panel with one control on its row. */
.project-hub-tabs .title-tryouts .dd-btn--add,
.project-hub-tabs .release-group-head .dd-btn--add {
    order: 0;
}
/* if a section has no actions (read-only), don't leave an empty header gap */
.project-hub-tabs .project-hub-section-header:not(:has(button)):not(:has(a)) {
    display: none;
}
/* …but a row holding the note mark is not that empty row. `royalty splits.`
   keeps its one action inside the calculator, because "save split." belongs
   under the thing it saves, so in a tab its header carries the (?) and nothing
   else — and `<summary>` is neither a button nor a link, which is the whole of
   why the rule above would have swallowed it (#213). */
.project-hub-tabs .splits-hub > .project-hub-section-header {
    display: flex;
}
/* in-tab instant search (artist_detail songs/projects/jobs tabs): full-width, sits
   below the status filters with equal breathing room above and below. The filter
   group hands its bottom gap to the search box so the spacing stays symmetric. */
.project-hub-tabs .client-hub-filter-group {
    margin-bottom: 0;
}
.project-hub-tabs .hub-search {
    margin: 0 0 var(--space-6);
}
/* when the search follows the status filters (projects/jobs) give it an equal top gap;
   on the songs tab it's the first element, so it stays flush to the top */
.project-hub-tabs .client-hub-filter-group + .hub-search {
    margin-top: var(--space-6);
}
.project-hub-tabs .hub-search-input {
    width: 100%;
    max-width: none;
}

/* two columns of content in every tab (assets already grids via --songs) */
.project-hub-tabs .project-hub-section--scrollable .project-hub-list,
.project-hub-tabs .credit-groups {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3) var(--space-6);
    align-content: start;
    overflow: visible;
    flex: none;
    min-height: 0;
}
/* let the natural-height panels grow rather than scroll internally */
.project-hub-tabs .project-hub-section--songs .project-hub-list {
    max-height: none;
    overflow: visible;
}

@media (max-width: 700px) {
    .project-hub-tabs .project-hub-section--scrollable .project-hub-list,
    .project-hub-tabs .project-hub-section--songs .project-hub-list,
    .project-hub-tabs .credit-groups {
        grid-template-columns: 1fr;
    }
}

/* ============================================================
   P20.2 — project settings: a gear in the hero opens a right
   slide-over drawer (basics · sharing · lifecycle).
   ============================================================ */
.project-settings-gear {
    display: inline-grid;
    place-items: center;
    width: 2.25em;
    height: 2.25em;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    transition: color var(--dur-base) var(--ease-soft),
                border-color var(--dur-base) var(--ease-soft);
}
/* The gear turns, the button does not (#208). It used to be the button's own
   `transform`, which was the same picture until the control grew a tooltip:
   a `::after` is a child of the box it hangs off, so rotating the box rotated
   the bubble with it and the label came out on its side. Rotating the glyph
   instead is what the motion was ever about. */
.project-settings-gear svg {
    width: 1.15em;
    height: 1.15em;
    transition: transform var(--dur-base) var(--ease-dreamy);
}
.project-settings-gear:hover {
    color: var(--accent-strong);
    border-color: var(--accent);
}
.project-settings-gear:hover svg { transform: rotate(35deg); }
.project-settings-gear:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 2px;
}

/* the archived state badge in the hero */
.status-archived {
    background: var(--surface-tint);
    color: var(--text-muted);
}

/* drawer body: groups stacked, lifecycle pinned to the bottom */
.settings-drawer-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-7);
    min-height: 100%;
}
.settings-drawer-title {
    font-family: var(--font-display);
    font-size: var(--fs-h3);
    color: var(--text);
    margin: 0;
}
/* each section is a soft card so the three are clearly distinct */
.settings-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
    padding: var(--space-5);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}
.settings-group-title {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--fs-small);
    font-weight: 600;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-soft);
}
.settings-group-title::before {
    content: "";
    flex: 0 0 auto;
    width: 0.4em;
    height: 0.4em;
    border-radius: var(--radius-pill);
    background: var(--accent);
}
.settings-group-hint {
    margin: 0;
    font-size: var(--fs-small);
    color: var(--text-muted);
}
.settings-actions {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}
.settings-action,
.settings-actions form {
    width: 100%;
}
.settings-action {
    justify-content: flex-start;
}
/* keep the lifecycle (destructive) card at the bottom of the drawer */
.settings-group--lifecycle {
    margin-top: auto;
}
/* destructive action — quiet until hovered */
.settings-action--danger {
    border-color: color-mix(in srgb, var(--danger) 40%, transparent);
    color: var(--danger);
    background: var(--surface);
}
.settings-action--danger:hover {
    background: color-mix(in srgb, var(--danger) 10%, var(--surface));
    border-color: var(--danger);
    transform: translateY(-2px);
}

/* hub cards — shared by the song artists / projects / jobs tabs.
   2-col grid: a lone card on a row spans full width; two share at half each. */
.hub-card-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-3);
}
.hub-card-grid .hub-card:last-child:nth-child(odd) {
    grid-column: 1 / -1;
}
.hub-card {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-4) var(--space-5);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-sm);
}
.hub-card-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    min-width: 0;
}
.hub-card-name {
    font-weight: 600;
    color: var(--text);
}
.hub-card-meta {
    font-size: var(--fs-small);
    color: var(--text-muted);
    overflow-wrap: anywhere;
}
.hub-card-meta a { color: var(--link); }
/* job type + status badge on one line */
.hub-card-titleline {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}
.hub-card-actions {
    display: flex;
    gap: var(--space-2);
    flex-wrap: wrap;
    flex: 0 0 auto;
}
/* empty-state message spans the full grid row */
.hub-card-grid > .project-hub-empty {
    grid-column: 1 / -1;
}

/* "show completed" switch on the job-card grids (#109, job_card_filter.js).
   Rides the section's action row — below the rule, sharing the line with the
   "+" add (which order:-1 keeps left) and pushed to the far right. */
.job-filter-bar {
    display: flex;
    align-items: center;
    margin-left: auto;
}
/* In a tab, the far right of a divider-topped bottom row belongs to the (?)
   explainer (#198), so whatever else lives at that end stops short of it. Two
   rows qualify — this one and `display.`'s testing bar — and both yield,
   because the mark's placement is decided by the row's existence rather than
   by what happens to be sitting in it (owner, 2026-09-01). */
.project-hub-tabs .job-filter-bar,
.project-hub-tabs .display-test-bar {
    margin-right: calc(var(--dd-note-mark) + var(--space-3));
}

/* `display` above beats the UA `[hidden]` rule, and the whole bar starts
   hidden — job_card_filter.js reveals it only once the grid holds a completed
   card. Without this the switch paints on every jobs tab, offering to hide
   work that is not there (#211; conventions.md §3). */
.job-filter-bar[hidden] {
    display: none;
}

/* the action row hides itself when it holds no button/link; a visible switch
   is reason enough to keep it (matters if these ever render read-only) */
.project-hub-tabs .project-hub-section-header:has(.job-filter-bar:not([hidden])) {
    display: flex;
}
.job-filter-toggle {
    font-size: 12px;
    color: var(--text-muted);
    white-space: nowrap;
}
.job-card-filtered-out {
    display: none;
}
/* stands in for :last-child:nth-child(odd), which counts the hidden cards */
.hub-card-grid .hub-card.job-card-span-full {
    grid-column: 1 / -1;
}

/* the song artists tab groups its cards (primary / secondary) */
.artist-groups {
    display: flex;
    flex-direction: column;
    gap: var(--space-6);
}
.artist-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

@media (max-width: 700px) {
    .hub-card-grid { grid-template-columns: 1fr; }
}

/* inline edit form inside the drawer */
.settings-form-actions {
    margin-top: var(--space-4);
}

/* inline sharing manager inside the drawer */
.settings-share-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin-bottom: var(--space-5);
}
.settings-share-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
}
.settings-share-who {
    display: flex;
    flex-direction: column;
    gap: 0.1em;
    min-width: 0;
}
.settings-share-email {
    font-size: var(--fs-small);
    color: var(--text-muted);
    overflow-wrap: anywhere;
}
.settings-share-add {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

@media (prefers-reduced-motion: reduce) {
    .project-settings-gear,
    .project-settings-gear svg,
    .settings-action--danger { transition: none; }
    .project-settings-gear:hover svg { transform: none; }
}

/* ============================================================
   P20.2 — job detail: the revision viewer + context cards.
   ============================================================ */
.dash-kicker {
    display: inline-block;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

.revision-viewer {
    margin-bottom: 1.5em;
}
.revision-viewer-bar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-5);
    padding-bottom: var(--space-4);
    border-bottom: 1px solid var(--border);
}
.revision-nav {
    display: flex;
    align-items: center;
    gap: var(--space-3);
}
.revision-nav-btn {
    display: inline-grid;
    place-items: center;
    width: 2.1em;
    height: 2.1em;
    padding: 0;
    border: 1px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--text);
    cursor: pointer;
    transition: border-color var(--dur-base) var(--ease-soft),
                color var(--dur-base) var(--ease-soft);
}
.revision-nav-btn svg { width: 1.05em; height: 1.05em; }
.revision-nav-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent-strong); }
.revision-nav-btn:disabled { opacity: 0.35; cursor: default; }
.revision-nav-btn:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.revision-counter {
    font-family: var(--font-mono);
    font-size: var(--fs-small);
    letter-spacing: var(--tracking-wide);
    color: var(--text-soft);
    text-transform: lowercase;
}
.revision-counter b { color: var(--text); }
.revision-counter-sep { color: var(--text-muted); }

.revision-card[hidden] { display: none; }
.revision-card-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    flex-wrap: wrap;
    margin-bottom: var(--space-4);
}
.revision-card-heading,
.revision-card-actions {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex-wrap: wrap;
}
.revision-version {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
}
/* #68 order: the player sits between the to-address block and the fixed-since
   block, so it needs its own top margin (collapses against block margins). */
.revision-player {
    width: 100%;
    margin-top: var(--space-5);
    margin-bottom: var(--space-5);
}
.revision-no-audio {
    margin-top: var(--space-5);
    margin-bottom: var(--space-5);
    font-size: var(--fs-small);
    color: var(--text-muted);
}
.revision-block { margin-top: var(--space-5); }
.revision-block-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}
.revision-block-title {
    margin: 0 0 var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}
.revision-block-head .revision-block-title { margin-bottom: 0; }

/* #69 instant work sessions — the end button carries a live dot that
   breathes like the song-progress dot. */
.revision-session-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.45em;
}
.revision-session-dot {
    flex: 0 0 auto;
    width: 0.5em;
    height: 0.5em;
    border-radius: var(--radius-pill);
    background: currentColor;
    animation: song-progress-pulse 3.2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
    .revision-session-dot { animation: none; }
}

/* The two ways of leaving a note, kept together at the end of the row (#226).
   Without this the head is a three-child flex and "+ note" strands in the
   middle of the panel, reading as a third, unrelated thing. */
.revision-block-actions {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
    justify-content: flex-end;
}

/* On a phone the heading and two controls do not fit on one line, and wrapping
   them inside a space-between row strands the buttons at three different
   indents. Give the actions the whole width instead, under the heading. */
@media (max-width: 560px) {
    .revision-block-head { flex-wrap: wrap; }
    .revision-block-actions { width: 100%; justify-content: flex-start; }
}

/* ── Voice notes on a revision (#226) ───────────────────────────────────────
   One control with three states, swapped by `hidden`. Every button below
   declares a `display`, which outranks the UA's `[hidden] { display: none }`,
   so the host stands guard for everything under it — the second accepted shape
   in conventions §3, and what `test_frontend_integrity` looks for. */
.voice-note-recorder {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    flex-wrap: wrap;
}

.voice-note-recorder [hidden] { display: none; }

.voice-note-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.45em;
}

/* The one moving thing on the page while it records, and the reason you know
   it is still listening. Same pulse the running-session dot uses, so "something
   is live" reads the same way twice on one card. */
.voice-note-dot {
    flex: 0 0 auto;
    width: 0.5em;
    height: 0.5em;
    border-radius: var(--radius-pill);
    background: currentColor;
    animation: song-progress-pulse 3.2s ease-in-out infinite;
}

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

/* The file picker's input (#226). The control is the button beside it; this is
   clipped rather than `display: none` so the browser still counts it as a live
   file input, and taken out of the tab order so the one affordance is announced
   once. Absolute, so it needs the recorder as its containing block. */
.voice-note-file-input {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: 0;
    opacity: 0;
    clip-path: inset(50%);
    overflow: hidden;
}

.voice-note-elapsed {
    font-family: var(--font-mono);
    font-size: 11px;
    letter-spacing: 0.04em;
    font-variant-numeric: tabular-nums;
}

.voice-note-saving {
    display: inline-flex;
    align-items: center;
    gap: 0.45em;
    font-size: var(--fs-label);
    color: var(--text-muted);
}

/* Refused microphone, failed save, empty recording. Always the last word in
   the row, never in place of the control — the button you press again has to
   still be there. */
.voice-note-status {
    display: inline-block;
    max-width: 32ch;
    font-size: var(--fs-label);
    line-height: 1.35;
    color: var(--danger);
}

/* A recording inside a note card. Capped rather than full-bleed: the round's
   own player is the mix and spans the card, and a note should not arrive
   looking like the thing it is a note about. */
.feedback-item-voice {
    display: block;
    width: 100%;
    max-width: 420px;
    margin-top: var(--space-2);
}

/* On a phone the card's actions and a player cannot share a row — the player
   ends up ~100px wide and unusable. Only the cards that carry one stack; a
   typed note reads as it always has. */
@media (max-width: 560px) {
    .feedback-item--voice .feedback-item-main {
        flex-direction: column;
        align-items: stretch;
        gap: var(--space-2);
    }
    .feedback-item--voice .feedback-item-right { justify-content: flex-end; }
    .feedback-item--voice .feedback-item-voice { max-width: none; }
}

/* The heading of a note that has no words to head it with. */
.feedback-item-track--voice {
    color: var(--text-soft);
}

/* the song + project context cards below the viewer */
.job-context-grid {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.25em;
    margin-top: 1.5em;
}
.job-context-card {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-5);
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-sm);
}
.job-context-title {
    font-family: var(--font-display);
    font-size: var(--fs-h3);
    color: var(--text);
}
.job-context-meta { color: var(--text-muted); }
.job-context-link { margin-top: var(--space-2); }

@media (max-width: 700px) {
    .job-context-grid { grid-template-columns: 1fr; }
}

/* ============================================
   SONG TASKS (#61)
   Song hub: the whole-song to-do panel (quick-add + done / star / delete).
   Project hub: the read-only "starred tasks." section. Lives here because
   both pages load projects.css.
   ============================================ */

.song-tasks-panel-title {
    display: flex;
    align-items: center;
    gap: 0.6em;
}

.song-tasks-list {
    display: flex;
    flex-direction: column;
    gap: 0.5em;
}

.song-task-row {
    display: flex;
    align-items: center;
    gap: 0.75em;
    padding: 0.6em 0.9em;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    transition: border-color 0.2s ease, opacity 0.2s ease;
}

/* Done tasks recede — the open ones stay visually forward. */
.song-task-row.is-done { opacity: 0.65; }

.song-task-row.is-done .song-task-title {
    text-decoration: line-through;
    color: var(--text-muted);
}

.song-task-body {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6em;
}

.song-task-title {
    color: var(--text);
    font-size: 14px;
    overflow-wrap: anywhere;
}

.song-task-chip {
    display: inline-flex;
    align-items: center;
    padding: 3px 9px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--accent) 16%, transparent);
    color: color-mix(in srgb, var(--accent) 60%, var(--ink));
    font-size: 11px;
    font-weight: 600;
    white-space: nowrap;
}

.song-task-actions {
    display: flex;
    align-items: center;
    gap: 0.35em;
}

/* Done toggle — mirrors the revision surface's quick-resolve ✓. */
.song-task-check.is-checked {
    background: color-mix(in srgb, var(--status-resolved) 16%, transparent);
    border-color: var(--status-resolved);
    color: color-mix(in srgb, var(--status-resolved) 60%, var(--ink));
}
.song-task-check.is-checked:hover {
    background: color-mix(in srgb, var(--status-resolved) 26%, transparent);
    border-color: var(--status-resolved);
    color: color-mix(in srgb, var(--status-resolved) 60%, var(--ink));
}

/* Star toggle — filled brand pink once the task is pinned to the project hub. */
.song-task-star svg,
.song-task-edit svg,
.song-task-archive svg { width: 16px; height: 16px; }

.song-task-star.is-starred {
    background: color-mix(in srgb, var(--accent) 16%, transparent);
    border-color: var(--accent);
    color: var(--accent-strong);
}
.song-task-star.is-starred:hover {
    background: color-mix(in srgb, var(--accent) 26%, transparent);
    border-color: var(--accent);
    color: var(--accent-strong);
}

/* Archive (the ✕) is a soft drop, not a delete — muted, darkening on hover. */
.song-task-archive { color: var(--text-muted); }
.song-task-archive:hover { color: var(--text); }

.song-task-add-form {
    display: flex;
    align-items: center;
    gap: 0.5em;
    margin-top: 0.9em;
}

.song-task-add-input {
    flex: 1 1 0;
    min-width: 0;
    padding: 9px 12px;
    font: inherit;
    font-size: 14px;
    color: var(--text);
    background: var(--surface);
    border: 1.5px solid var(--border);
    border-radius: var(--radius-sm);
}

.song-task-add-select {
    padding: 9px 10px;
    font: inherit;
    font-size: 13px;
    color: var(--text-muted);
    background: var(--surface);
    border: 1.5px solid var(--border);
    border-radius: var(--radius-sm);
}

.song-task-add-input:focus,
.song-task-add-select:focus {
    outline: none;
    border-color: var(--accent);
}

/* --- song hub: stages over those tasks (#230). ---------------------------
   A stage is a user-made group of tasks that is blocked until the one above
   it is finished. Blocked wears --attention (the warm "awaiting action" cue),
   never a red: nothing is wrong, the work is simply not reachable yet. */

.song-task-grip,
.song-stage-grip {
    display: inline-flex;
    flex: 0 0 auto;
    color: var(--text-muted);
    cursor: grab;
    opacity: 0.5;
}
.song-task-grip svg,
.song-stage-grip svg { width: 14px; height: 14px; }
.song-task-row:hover .song-task-grip,
.song-stage-header:hover .song-stage-grip { opacity: 1; }

.song-task-row.is-dragging,
.song-stage.is-dragging { opacity: 0.45; }

/* A locked row is legible, not hidden: it is the road ahead, and the reason
   it cannot be ticked sits right on it. */
.song-task-row.is-locked { border-style: dashed; }
.song-task-row.is-locked .song-task-title { color: var(--text-soft); }

.song-task-check.is-locked,
.song-task-check[disabled] {
    color: var(--text-muted);
    cursor: not-allowed;
}

.song-task-waiting {
    flex: 1 1 100%;
    font-size: 11px;
    color: var(--attention);
}

.song-stages {
    display: flex;
    flex-direction: column;
    gap: 0.7em;
    margin-top: 1.1em;
}

.song-stage {
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    padding: 0.7em 0.8em 0.8em;
}

.song-stage.is-blocked {
    border-color: color-mix(in srgb, var(--attention) 35%, var(--border));
    background: color-mix(in srgb, var(--attention) 4%, var(--surface));
}

.song-stage.is-complete {
    border-color: color-mix(in srgb, var(--status-resolved) 35%, var(--border));
}

.song-stage-header {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.5em;
    margin-bottom: 0.6em;
}

.song-stage-rename { flex: 1 1 8em; min-width: 0; }

/* Reads as the stage's heading until it is focused, at which point it admits
   to being the rename field it always was. */
.song-stage-name {
    width: 100%;
    padding: 4px 6px;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    color: var(--text);
    background: transparent;
    border: 1.5px solid transparent;
    border-radius: var(--radius-sm);
    transition: border-color var(--dur-base) var(--ease-soft),
                background-color var(--dur-base) var(--ease-soft);
}
.song-stage-name:hover { border-color: var(--border); }
.song-stage-name:focus {
    outline: none;
    background: var(--surface-tint);
    border-color: var(--accent);
}
.song-stage-name::placeholder { color: var(--text-muted); font-weight: 600; }

.song-stage-count {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
    white-space: nowrap;
}

.song-stage-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    padding: 3px 9px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--attention) 14%, transparent);
    color: var(--attention);
    font-size: 11px;
    font-weight: 600;
}
.song-stage-badge svg { width: 12px; height: 12px; }
.song-stage-badge.is-done {
    background: color-mix(in srgb, var(--status-resolved) 16%, transparent);
    color: color-mix(in srgb, var(--status-resolved) 60%, var(--ink));
}

.song-stage-delete { color: var(--text-muted); }
.song-stage-delete:hover { color: var(--text); }
.song-stage-delete svg { width: 16px; height: 16px; }

.song-stage-tasks {
    display: flex;
    flex-direction: column;
    gap: 0.4em;
    min-height: 2.2em;
    border-radius: var(--radius-sm);
    transition: background-color var(--dur-base) var(--ease-soft);
}

/* The drop target says where the task will land, on both lists. */
.song-tasks-list.is-drop-target,
.song-stage-tasks.is-drop-target {
    background: var(--surface-tint);
    box-shadow: inset 0 0 0 1.5px var(--accent);
}

.song-stage-hint {
    margin: 0;
    padding: 0.4em 0.2em;
    font-size: 12px;
    color: var(--text-muted);
}

.song-stage-add { margin-top: 0.8em; }

.song-stage-add-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.45em;
}
.song-stage-add-btn svg { width: 14px; height: 14px; }

/* A ticked task with no stage is on its way out (#252). The row starts
   leaving on the click and the panel swap removes it a moment later — the
   same optimism as the tick itself, and the same correction. */
.song-task-row.is-leaving {
    opacity: 0;
    transform: translateX(0.5em);
    transition: opacity var(--dur-base) var(--ease-soft),
                transform var(--dur-base) var(--ease-soft);
}

/* Offered only on a finished pass that holds something, so it reads as the
   end of the work rather than another control on every stage. */
.song-stage-archive { white-space: nowrap; }

/* --- the completed drawer (#252) --- */

.song-tasks-done { margin-top: 1em; }

.song-tasks-done-summary {
    display: flex;
    align-items: center;
    gap: 0.5em;
    padding: 0.5em 0.2em;
    cursor: pointer;
    font-size: 12px;
    font-weight: 600;
    color: var(--text-muted);
    list-style: none;
}
.song-tasks-done-summary::-webkit-details-marker { display: none; }
/* The disclosure triangle is drawn rather than inherited, so it can turn with
   the drawer and match the other carets on this hub. */
.song-tasks-done-summary::before {
    content: "";
    width: 0;
    height: 0;
    border-left: 5px solid currentColor;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
    transition: transform var(--dur-base) var(--ease-soft);
}
.song-tasks-done[open] .song-tasks-done-summary::before { transform: rotate(90deg); }
.song-tasks-done-summary:hover { color: var(--text); }

.song-tasks-done-count {
    font-family: var(--font-mono);
    font-size: 11px;
}

.song-tasks-done-list {
    display: flex;
    flex-direction: column;
    gap: 0.4em;
    padding-top: 0.3em;
}

.song-done-row {
    display: flex;
    align-items: center;
    gap: 0.75em;
    padding: 0.5em 0.9em;
    background: var(--surface-tint);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
}

.song-done-name {
    flex: 1;
    min-width: 0;
    font-size: 13px;
    color: var(--text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.song-done-meta {
    font-family: var(--font-mono);
    font-size: 11px;
    color: var(--text-muted);
    white-space: nowrap;
}

.song-done-restore { flex-shrink: 0; }

@media (max-width: 480px) {
    .song-stage-rename { flex-basis: 100%; order: -1; }
    .song-task-add-form { flex-wrap: wrap; }
    .song-task-add-input { flex-basis: 100%; }
    .song-stage-archive { flex-basis: 100%; order: 1; }
}

@media (prefers-reduced-motion: reduce) {
    .song-stage-name,
    .song-stage-tasks { transition: none; }
    .song-task-row.is-leaving { transition: none; }
    .song-tasks-done-summary::before { transition: none; }
}

/* --- project hub: starred tasks. --- */

.starred-tasks-header-star {
    width: 16px;
    height: 16px;
    color: var(--accent-strong);
}

.starred-tasks-groups {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0.75em;
}

/* A group left alone on its row (odd card count — including a lone group)
   shouldn't sit in half the section width; mirrors the tracklist's
   :only-child rule. */
.starred-tasks-group:last-child:nth-child(odd) {
    grid-column: 1 / -1;
}

.starred-tasks-group {
    padding: 0.9em 1.1em;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}

.starred-tasks-song {
    font-weight: 600;
    color: var(--text);
    text-decoration: none;
}
.starred-tasks-song:hover { color: var(--accent-strong); }

.starred-tasks-list {
    list-style: none;
    margin: 0.5em 0 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4em;
}

.starred-task-item {
    display: flex;
    align-items: center;
    gap: 0.6em;
}

.starred-task-item::before {
    content: "";
    flex: none;
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--accent);
}

.starred-task-title {
    color: var(--text);
    font-size: 14px;
    overflow-wrap: anywhere;
}

@media (max-width: 700px) {
    .song-task-add-form { flex-wrap: wrap; }
    .song-task-add-input { flex-basis: 100%; }
    .starred-tasks-groups { grid-template-columns: 1fr; }
}


/* --- links. rows -----------------------------------------------------------
   One row per streaming service (#258), wearing that service's own colour.

   The pair comes from `components/streaming.css` by inheritance — the row
   carries `data-stream-platform`, the badge just paints with what it finds —
   so the seven hexes are written once and a service with no entry there falls
   back to the panel's own muted chip rather than to nothing.

   The colour is the whole point of the row: the name is scanned by shape long
   before it is read, which is what makes "is this release on Deezer yet?" a
   glance instead of a list-read.
*/
.project-link-list {
    list-style: none;
    margin: 0;
    padding: 0;
}

.project-link-row {
    border-left: 3px solid var(--brand, var(--border));
}

.project-link-badge {
    display: inline-flex;
    align-items: center;
    padding: 3px 10px;
    border-radius: var(--radius-pill);
    background: var(--brand, var(--surface-tint));
    color: var(--on-brand, var(--text));
    font-size: var(--fs-label);
    font-weight: 600;
}


/* --- Asset panel -----------------------------------------------------------
   Shared by the project hub, the song hub and the staff job page — all three
   load this stylesheet, which is why these live here rather than in songs.css
   (they moved out of it when project assets became versioned too, #156). The
   client hub renders the same templates, so it inherits the panel unchanged.
*/

.asset-group-title {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
    margin: 0 0 var(--space-2);
}

.asset-group-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: 0.5em;
}

.asset-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 0.75em;
    padding: 0.6em 0.85em;
    background: rgba(0, 0, 0, 0.02);
    border: 1px solid rgba(0, 0, 0, 0.06);
    border-radius: 12px;
}

.asset-row-version {
    min-width: 2.5em;
    text-align: center;
}

/* Optional per-asset title (#49) — sits between the version pill and the
   action icons, truncating so it never pushes the icons off the row. */
.asset-row-title {
    flex: 1;
    min-width: 0;
    font-size: var(--fs-label);
    color: var(--text-muted);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.asset-row-actions {
    display: flex;
    gap: 0.4em;
    align-items: center;
}

/* Version browser for an image asset group (#156). Cover art is chosen by
   looking, not by reading URLs, so versions show as thumbnails and the live
   one is ringed in the brand accent rather than merely labelled. */
.asset-version-strip {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-wrap: wrap;
    gap: 0.75em;
}

.asset-version {
    display: flex;
    flex-direction: column;
    gap: 0.4em;
    /* Bigger since #187: in display. these are options you are choosing
       between by eye, not rows in a version history. */
    width: 208px;
    padding: 0.5em;
    background: rgba(0, 0, 0, 0.02);
    border: 1px solid rgba(0, 0, 0, 0.06);
    border-radius: 12px;
}

.asset-version.is-live {
    border-color: var(--accent);
    background: var(--surface-tint);
}

.asset-version-thumb {
    display: block;
    aspect-ratio: 1;
    border-radius: 8px;
    overflow: hidden;
    background: var(--surface-tint);
}

.asset-version-thumb img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.asset-version-meta {
    display: flex;
    align-items: center;
    gap: 0.4em;
    flex-wrap: wrap;
}

/* Mono micro-label, per the design language's sparing use of them. */
.asset-version-live-tag {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: var(--tracking-wide);
    color: var(--link);
}

.asset-version-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 0.4em;
}

@media (max-width: 480px) {

    .asset-version {
        width: 100%;
    }
}

/* ---- the "not ready yet" notice at the top of a gated tab (#199) ----
   `_hub_tab_gate.html`. The warm --attention cue rather than --danger, for the
   reason the token itself gives: awaiting-action is neither success nor
   failure, and nothing here is wrong or lost. It sits above the panel's own
   content and never in place of it. */
/* What a tab is for, and where its changes land (#198).

   A (?) in the panel's bottom-right corner, opposite the "+" that the tab
   layout drops to the bottom-left, opening a card above it. Closed by default:
   the explanation earns its place the first time somebody meets a tab and is
   noise every time after, so it waits to be asked for.

   The panel is the positioning context — `position: relative` on the panel
   itself rather than on a wrapper, so the mark lands in the corner of the tab
   and not in the corner of whatever section happens to be last. */
.dd-tabs__panel {
    position: relative;
}

/* The mark's box. It is `--hub-pin-size` and not a number of its own: #204
   settled that every control in a section's button row is one size — the pin,
   the pills, and the round "+" that was resized to match — and the mark is one
   more control in that row. Reading their token is also what keeps it correct
   when it moves again; it was 42px against the old "+" and went 4px out of
   line the moment that button changed underneath it.

   Three rules have to agree on this value: the trigger's box, and the
   clearance anything sharing its row leaves for it. */
.hub-sections {
    --dd-note-mark: var(--hub-pin-size);
}

/* The row the mark sits in is what the card hangs off, so the row has to be
   the positioning context. Without this the card finds the `<section>` instead
   and `top: 100%` puts it at the *bottom of the whole section* — which for a
   pinned section means it opens underneath the thing it is describing rather
   than over it (owner, 2026-09-02: if you are reading the hint you are not
   reading the content, so covering it is right). */
.project-hub-section-header,
.display-test-bar {
    position: relative;
}

/* `static`, so the card below hangs off the **row** rather than off the mark:
   the mark can sit anywhere along a button row, and a card anchored to it
   started wherever that happened to be — off the right of a wide section, and
   off a phone entirely. That is the card a page without the script shows; with
   `components.js` running the card is placed against the mark and held inside
   the viewport, so neither edge can take it (#265). */
.project-hub-note {
    position: static;
    z-index: var(--z-dropdown);
}

/* Where the mark sits, in one rule each way (owner, 2026-09-01).

   It lives in the section's own button row now, not in a corner of the panel,
   because both placements the owner asked for are relative to the buttons and
   nothing positioned from a wrapper can find the end of a row of them.

   **Unpinned**, the row is at the bottom of the tab and reads left to right:
   the "+" first, then whatever else the section offers, then the mark. With no
   "+" and no buttons at all the same rule puts it at the far left, which is
   where `display.` and the settings tabs want it.

   **Pinned**, the row is at the top and its buttons are right-aligned, so the
   mark leads them instead of trailing them — `margin-left: auto` makes it the
   first thing in that cluster. Trailing them there would push it under the pin
   in the gutter; leading them lines every section's mark up at the same edge,
   which is what "all over the place" was about. */
/* Two headers are not the bottom action row the rule below assumes, and each
   needs the mark to behave the way it does when pinned.

   `lyrics.` keeps its own title bar — `justify-content: space-between` rather
   than the tab's `flex-start` — so ordering the mark last does not send it to
   the end of a left-aligned row: it becomes a third thing for space-between to
   spread, and the buttons get pushed to the middle of the header. It leads
   them here exactly as it does pinned.

   `visualization.` has no buttons at all, only a "saving…" status that is
   invisible almost always and 59px wide regardless. Ordered before the mark it
   left a gap to the left of it with nothing in it. A status trails everything;
   it is not a control the mark should be deferring to. */
.song-lyrics-panel > .project-hub-section-header > .project-hub-note {
    order: 0;
    margin-left: auto;
    margin-right: calc(var(--space-2) - var(--space-4));
}

.song-lyrics-panel > .project-hub-section-header > .project-hub-note + * {
    margin-left: 0;
}

.song-visualization-status {
    order: 100;
}

/* …and with the status behind it the mark leads the row, so it has nothing to
   pull against: the negative margin that tightens it to a neighbour would just
   outdent it past the content edge. */
.project-hub-section-header:has(> .song-visualization-status) > .project-hub-note {
    margin-left: 0;
}

/* **The mark's auto margin has to be the only one in the row.** A section's
   action group pushes itself right with `margin-left: auto` of its own, and two
   auto margins split the free space between them rather than pooling it — which
   parked the mark in the middle of the header with the buttons still at the far
   right. Cancelling the neighbour's lets the mark's own margin take all of it,
   so the two end up adjacent. */
.project-hub-section-header > .project-hub-note--titled + * {
    margin-left: 0;
}

/* `royalty splits.` used to head itself with a stacked kicker/title/intro
   rather than a button row, so there was no line for the mark to join and it
   was positioned into the head's top-right corner by hand. #213 gave the
   section the same header every sibling has, which is a row — so the four
   rules that used to place its mark are gone, and the ones above place it. */

/* The mark carries the **buttons'** rhythm, not the header's. A section's
   controls sit `--space-2` apart inside their own group; the header spaces its
   children `--space-4`, and the mark is a header child, so left alone it
   stands twice as far from the row as the buttons stand from each other —
   which reads as belonging to something else. The negative margin gives back
   the difference, on whichever side the mark's neighbours are. */
.project-hub-note {
    order: 99;
    margin-left: calc(var(--space-2) - var(--space-4));
}

.project-hub-note--titled {
    order: 0;
    margin-left: auto;
    margin-right: calc(var(--space-2) - var(--space-4));
}

/* A header that holds nothing but a heading hides itself in a tab; one holding
   the mark has something to show. */
.project-hub-tabs .project-hub-section-header:has(.project-hub-note) {
    display: flex;
}

/* `display.`'s testing bar puts its own text and button at opposite ends, so
   the mark takes the left with the text rather than crowding the button. */
.display-test-bar .project-hub-note {
    order: -1;
    margin-right: var(--space-4);
}

/* Room for the card to land in. The card is absolutely positioned, so it adds
   nothing to the page's height and lands on whatever is below — which on a hub
   whose last element is the tab strip is the footer. Only the tab case needs
   it: a pinned section's card opens *into* the section, over content there is
   no point protecting, so there is nothing below it to make room for. `:has()` rather than a
   permanent gap or a line of JS: the space appears the moment a card opens and
   is not there the rest of the time. Sized to the tallest card measured across
   every section, per breakpoint — a narrower card is a taller one.

   Only for a page the script never ran on. With `components.js` running the
   card is placed in the top layer instead (`components/hint.css`), where it
   lands on nothing and needs no room kept for it. */
html:not(.dd-tip-js) .project-hub-tabs:has(.project-hub-note[open]) {
    padding-bottom: 15rem;
}

@media (max-width: 768px) {
    html:not(.dd-tip-js) .project-hub-tabs:has(.project-hub-note[open]) {
        padding-bottom: 22rem;
    }
}

/* The trigger is the kit's `.dd-hint`, in its note form (#212) — the box, the
   glyph, the states, the killed `<summary>` triangle and the pointer all live
   in `components/hint.css`. What is left here is the one fact only this row
   knows: how big the mark is.

   The box, not the glyph — the icon inside stays the kit's size. It is
   `--hub-pin-size` and not a number of its own because the mark is one more
   control in a row of them; at 32px it sat 5px low against the "+" it is meant
   to be opposite. */
.project-hub-note-trigger {
    --dd-hint-size: var(--dd-note-mark);
}

/* The card drops down and to the left of the trigger. Down, not up, and that
   is the whole of it: the (?) sits at the *bottom* of the panel, so upward
   covers the content somebody is reading — and on a short panel it covered the
   tab strip itself, which is the one thing on the page that must stay
   clickable. Below the panel is usually free page, and an absolutely
   positioned card extends the scroll area if it is not.

   This is the card a page without the script shows. With `components.js`
   running the open card is placed in the top layer instead
   (`components/hint.css`, `.is-placed`): still down first, up only where the
   viewport has no room below the (?), 8px inside the viewport. */
.project-hub-note-card {
    position: absolute;
    left: 0;
    top: calc(100% + var(--space-2));
    width: max-content;
    max-width: min(34rem, 78vw);
    padding: var(--space-4) var(--space-5);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: var(--surface);
    box-shadow: var(--shadow-md);
}

@media (prefers-reduced-motion: no-preference) {
    .project-hub-note[open] .project-hub-note-card {
        animation: project-hub-note-in var(--dur-fast) var(--ease-soft);
    }

    @keyframes project-hub-note-in {
        from {
            opacity: 0;
            transform: translateY(-4px);
        }
    }
}

.project-hub-note-text {
    margin: 0;
    font-size: var(--fs-small);
    line-height: var(--lh-body);
    color: var(--text-soft);
}

/* Each label takes its own line and its content starts the next one (owner,
   2026-09-01). Inline, the label was competing with the sentence for the first
   line and — in a card this narrow — eating a third of every line the
   destinations wrapped onto. Stacked, both halves read as a heading and an
   answer, which is what they are. */
.project-hub-note-label {
    display: block;
    margin-bottom: 0.15em;
    font-weight: 600;
    color: var(--text-muted);
}

/* The second half. */
.project-hub-note-line {
    margin: var(--space-3) 0 0;
    font-size: var(--fs-small);
    line-height: var(--lh-body);
    color: var(--text-muted);
}

/* The destinations run inline and wrap, separated by the brand dot. A vertical
   list of three four-word phrases reads as a checklist of chores, which is the
   gate's job and not this one.

   Plain inline flow rather than a wrapping flex row, which is what this was
   first: as flex items the phrases could not break internally, so on a phone a
   long one took the whole line and its separator was pushed to the far right —
   a dot floating in space next to the phrase it does not belong to. Inline
   text wraps mid-phrase and keeps the dot against the word it follows. */
.project-hub-note-shows {
    list-style: none;
    display: inline;
    margin: 0;
    padding: 0;
}

.project-hub-note-item {
    display: inline;
}

/* Decoration, so it is generated rather than written: the `<ul>`/`<li>` is
   what carries the separation to a screen reader. */
.project-hub-note-item:not(:last-child)::after {
    content: "\00b7";
    margin: 0 0.45em;
    color: var(--text-muted);
}

/* Two destinations, one look — the same bargain the gate's actions strike: an
   `<a>` off this page and a `<button>` that opens a sibling tab in place read
   identically, because to a reader they are the same offer. */
.project-hub-note-action {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

.project-hub-note-action:hover {
    color: var(--link-hover);
}

.project-hub-note-action:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 3px;
    border-radius: var(--radius-sm);
}

.project-hub-gate {
    margin: 0 0 var(--space-6);
    padding: var(--space-4) var(--space-5);
    border: 1px solid color-mix(in srgb, var(--attention) 28%, transparent);
    border-radius: var(--radius-md);
    background: color-mix(in srgb, var(--attention) 7%, transparent);
}

/* The consequence, said once, in the cue's own colour. */
.project-hub-gate-head {
    display: flex;
    align-items: flex-start;
    gap: 0.5em;
    margin: 0 0 var(--space-3);
    font-size: var(--fs-small);
    font-weight: 600;
    color: var(--attention);
}

.project-hub-gate-icon {
    flex: none;
    width: 1.1em;
    height: 1.1em;
    margin-top: 0.1em;
}

.project-hub-gate-list {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}

/* Wraps rather than squeezes: on a phone the sentence takes the line and its
   way out takes the next one. */
.project-hub-gate-item {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.15em 0.5em;
    font-size: var(--fs-small);
    line-height: var(--lh-body);
    color: var(--text-soft);
}

/* Two destinations, one look: an `<a>` off this page and a `<button>` that
   opens another tab in place read identically, because to a reader they are
   the same offer. */
.project-hub-gate-action {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-weight: 600;
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

.project-hub-gate-action:hover {
    color: var(--link-hover);
}

.project-hub-gate-action:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 3px;
    border-radius: var(--radius-sm);
}

/* ============================================
   TESTING MODE — titles & cover art (#187)
   The titles panel on the song hub and the project hub, and the "in the mix"
   marker on the cover-art version strip. Lives here beside the tasks panel for
   the same reason: both pages load projects.css.
   ============================================ */

.title-tryouts-heading {
    display: flex;
    align-items: center;
    gap: 0.6em;
}

.title-tryouts-live {
    background: color-mix(in srgb, var(--attention) 14%, transparent);
    color: var(--attention);
}

/* The name itself (#205). Renaming moved off the hub's heading and into this
   panel, so the field holding the record's actual name sits above the
   shortlist competing for the job — and is set heavier than the "try another
   name…" input at the foot of the panel, because one of the two holds what
   this song is called and the other holds a suggestion. */
.title-tryouts-name {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: 0.45em 0.5em;
    margin: 0 0 1.1em;
}

.title-tryouts-name-label {
    grid-column: 1 / -1;
    margin: 0;
    color: var(--text-muted);
    /* `--fs-small`, not the `--fs-sm` its neighbours reach for: that one is
       not defined anywhere, so those rules quietly inherit. Not fixed here —
       it is thirteen call sites and none of them is this one. */
    font-size: var(--fs-small);
}

.title-tryouts-name-input {
    min-width: 0;
    padding: 0.55em 0.8em;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font-family: inherit;
    font-size: 15px;
    font-weight: 600;
}

.title-tryouts-name-input:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 1px;
}

.title-tryouts-intro {
    margin: 0 0 1em;
    color: var(--text-muted);
    font-size: var(--fs-sm);
    max-width: 62ch;
}

.title-tryouts-list {
    display: flex;
    flex-direction: column;
    gap: 0.5em;
}

/* A grid, not a flex row, so the switch and the two actions line up down the
   column — the official row has no "mark as official title" button, and with
   flex its switch slid right and the list read as ragged. */
.title-tryout-row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto auto auto;
    align-items: center;
    gap: 0.75em;
    padding: 0.6em 0.9em;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    transition: border-color 0.2s ease, background-color 0.2s ease;
}

/* A name in the rotation is the one thing on this panel worth spotting from
   across the room — it is what the app is currently wearing. */
.title-tryout-row.is-testing {
    border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
    background: var(--surface-tint);
}

.title-tryout-body {
    flex: 1 1 0;
    min-width: 0;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 0.6em;
}

.title-tryout-text {
    color: var(--text);
    font-size: 14px;
    overflow-wrap: anywhere;
}

.title-tryout-chip {
    display: inline-flex;
    align-items: center;
    padding: 3px 9px;
    border-radius: var(--radius-pill);
    background: color-mix(in srgb, var(--accent) 16%, transparent);
    color: color-mix(in srgb, var(--accent) 60%, var(--ink));
    font-size: 11px;
    font-weight: 600;
}

.title-tryout-toggle { grid-column: 2; }
.title-tryout-promote { grid-column: 3; }
.title-tryout-remove { grid-column: 4; }

/* Holds the column open on the official row — see the template. */
.title-tryout-promote.is-placeholder {
    visibility: hidden;
    pointer-events: none;
}

.title-tryouts-add-form {
    display: flex;
    gap: 0.5em;
    margin-top: 0.9em;
}

.title-tryouts-add-input {
    flex: 1 1 0;
    min-width: 0;
    padding: 0.55em 0.8em;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--text);
    font-family: inherit;
    font-size: 14px;
}

.title-tryouts-add-input:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 1px;
}

/* Narrow: the name takes its own line and the controls sit under it. */
@media (max-width: 560px) {
    .title-tryout-row {
        grid-template-columns: minmax(0, 1fr) auto auto;
    }

    .title-tryout-body {
        grid-column: 1 / -1;
    }

    .title-tryout-toggle { grid-column: 1; }
    .title-tryout-promote { grid-column: 2; }
    .title-tryout-remove { grid-column: 3; }
}

/* The cover-art twin: a version in the rotation, on the version strip. */
.asset-version.is-testing {
    border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}

/* Both switches are the kit's `dd-choice--toggle dd-choice--sm` — the small
   variant exists precisely for this (a control beside a name, not a form row),
   and nothing here restyles the component. The only local rule is that the
   label does not wrap. */
.asset-version-toggle,
.title-tryout-toggle {
    flex: 0 0 auto;
    white-space: nowrap;
}

/* `.project-hub-tabs` hides a section's own h3 and drops its action row to the
   bottom, on the sound assumption that a tab holds one section and the tab
   label names it. display. holds two — the names and the artwork — so the tab
   label cannot be either one's heading. Put both headings back, at the top of
   their own column, where they say which half you are looking at. */
/* `display.` has a title of its own since #198, so the exception that used to
   force its headings visible in a tab is spent: the main `h3` hides like every
   other section's (the tab label is the heading there) and the two halves are
   `h4` subtitles, which were never covered by that rule. */
.project-hub-tabs .display-stack-head {
    display: none;
}

.display-stack-head {
    grid-column: 1 / -1;
}

.project-hub-tabs .display-stack .project-hub-section-header,
.project-hub-tabs .display-stack .project-hub-section-header:not(:has(button)):not(:has(a)) {
    display: flex;
    order: 0;
    justify-content: space-between;
    border-top: 0;
    border-bottom: 1px solid var(--border);
    margin: 0 0 var(--space-4);
    padding: 0 0 var(--space-3);
}

/* The one switch for the tab: a full-width line under both columns, so it
   reads as covering them rather than belonging to either. */
.display-test-bar {
    grid-column: 1 / -1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-5);
    flex-wrap: wrap;
    margin-top: var(--space-2);
    padding-top: var(--space-5);
    border-top: 1px solid var(--border);
}

.display-test-state {
    /* No max-width: this line belongs to the whole tab, and capping it made it
       wrap at the titles column's edge as though it were part of it. */
    margin: 0;
    color: var(--text-muted);
    font-size: var(--fs-small);
    flex: 1 1 auto;
}

/* display. runs the names and the artwork side by side — two halves of one
   decision, read together (owner, 2026-08-31). */
.display-stack {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-6);
    align-items: start;
}

@media (max-width: 1024px) {
    .display-stack {
        grid-template-columns: minmax(0, 1fr);
    }
}

/* ============================================================
   #204 — pinning a section between the page flow and the strip

   A section renders in one of two places and the CSS for both already existed
   (`.project-hub-flow` above, `.project-hub-tabs` below); what is new here is
   the control that moves it, and the slot it is positioned against. The slot
   wraps the section in *both* places on purpose — one wrapper, one position to
   decide, and the section keeps its own id for every swap that targets it.
   ============================================================ */
.hub-sections {
    /* One number for the control and the gutter it sits in: it matches the
       height of an `.action-btn` pill, which is what the section's own controls
       are (owner, 2026-09-01 — the round "+" was resized to match too). */
    --hub-pin-size: 34px;
}

.hub-section-slot {
    position: relative;
    /* `--navbar-h` says 60px and the bar renders 97 (conventions §5 — the token
       describes one row, not the whole thing), so `+ space-8` left three pixels
       of clearance and a section scrolled to read as cropped. This is the
       rendered height plus a gap you can see. */
    scroll-margin-top: calc(var(--navbar-h) + var(--space-10));
}

.hub-pin {
    position: absolute;
    /* Positioned per context below; the size is the section's own controls'
       size, so a header reads as one row of buttons rather than a pin floating
       over them (owner, 2026-09-01). */
    top: 0;
    right: 0;
    z-index: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--hub-pin-size);
    height: var(--hub-pin-size);
    padding: 0;
    /* The same outline the pills beside it wear (`.action-btn`), because it is
       one of the buttons in that row and reading as one is the whole point
       (owner, 2026-09-01). It was drawn only on hover before, which left a
       ghost sitting where a control should be. */
    border: 1.5px solid var(--border);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--text-muted);
    cursor: pointer;
    transition: color var(--dur-fast) var(--ease-soft),
        background var(--dur-fast) var(--ease-soft),
        border-color var(--dur-fast) var(--ease-soft);
}

.hub-pin:hover,
.hub-pin:focus-visible {
    color: var(--accent-strong);
    background: var(--tint-faint);
    border-color: var(--primary);
}

.hub-pin:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
}

/* Pinned reads as "held": the pin is upright and coloured. Unpinned tilts, the
   way a pin does when it is out of the board. */
.hub-pin--on {
    color: var(--accent-strong);
}

.hub-pin:not(.hub-pin--on) .dd-icon {
    transform: rotate(35deg);
}

/* Inside a tab the section's own control is not drawn. There is nowhere in a
   panel it can go: an audit of all thirty tab panels (#204) found the top-right
   corner landing on a `clear all` button, a section header, a list row, an
   asset-tab nav or a gate notice depending on the tab, and the bottom is where
   the tab skin already puts the action row. It stays in the markup, carrying
   the URL and the label for the section it belongs to, and the strip's one
   presses it. */
.dd-tabs__panel .hub-section-slot > .hub-pin {
    display: none;
}

/* The strip's pin: last in the row, on the labels' line, and the only thing on
   that side of it. */
.hub-pin--strip {
    /* `relative`, not `static`, and the difference is invisible here: with no
       offsets it sits exactly where it did, in flow at the end of the row. It
       matters because a tooltip is `position: absolute` against its trigger,
       and a static trigger is not a containing block — the bubble was hanging
       off the whole section instead, ~900px away and off the side of the page
       (#208). `.hub-pin`'s own `absolute` is what this was undoing. */
    position: relative;
    margin-left: auto;
    align-self: center;
    margin-bottom: var(--space-3);
}

/* In a page flow the slot is a card, so the control comes in past its padding —
   level with the top of the header's buttons and flush with their right edge,
   which is what makes the header read as one row rather than a button with
   something floating over it. In a tab there is no padding and no card, and the
   header starts at the corner, so the base rule above already puts it there. */
.hub-sections__flow > .hub-section-slot > .hub-pin,
.hub-sections__tail > .hub-section-slot > .hub-pin {
    top: var(--space-6);
    right: var(--space-6);
}

/* ...and the header gives up exactly the room the control needs, so the two sit
   side by side rather than on top of each other.

   Whatever the section's root element is, not just `.project-hub-section`: the
   lyrics panel is a `.song-lyrics-panel` and its header is where `sync timings`
   and `edit lyrics` live, which is exactly where the control landed. The second
   pair reaches one level deeper for `display.`, whose root is a two-column
   stack and whose first column carries the header the control sits beside. */
.hub-section-slot > * > .project-hub-section-header,
.hub-section-slot > * > *:first-child > .project-hub-section-header {
    padding-right: calc(var(--hub-pin-size) + var(--space-3));
}

/* **Pinned is a card; unpinned flows into the page** (owner, 2026-09-01).

   The card belongs to the SLOT rather than to the section, because being
   pinned is a fact about *where a section is* and nothing about what it is.
   Drawn on the slot it is one rule; drawn on each section it was one rule per
   shape and they disagreed — the lyrics panel brought its own card into the
   tabs, the splits tool brought none into the flow, and `display.` brought two
   nested inside whatever it landed in. */
.hub-sections__flow > .hub-section-slot,
.hub-sections__tail > .hub-section-slot {
    background: var(--surface);
    border: 1.5px solid var(--glass-border);
    border-radius: 18px;
    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
    padding: 1.5em;
}

/* The room between them. These are normal-flow containers, so neighbouring
   margins collapse into one gap rather than stacking — the same 1.25em the
   song hub's panels have carried since long before pinning existed. The project
   hub is excluded: its flow is a flex column with a gap of its own, where a
   margin would add to it instead. */
.hub-sections__flow:not(.project-hub-flow) > .hub-section-slot,
.hub-sections__tail > .hub-section-slot {
    margin: 1.25em 0;
}

/* ...and nothing inside a slot draws card chrome of its own, in either place.
   In a flow the slot has already drawn it; in a tab the panel *is* the
   container and a section that keeps its own box reads as a card sitting on a
   page rather than a page. The fixed height goes with it: `--scrollable` is
   480px of scrolling window, which is right for a list in a box and is an empty
   box anywhere else.

   The lyric sheet is excluded while it is staged. Fullscreen takes the panel
   out of the slot's layout entirely — `position: fixed` over the whole
   viewport — so it is no longer a section drawing card chrome inside a
   container, and this rule has nothing to say about it. Reaching it anyway is
   what broke it: this selector is one class more specific than the panel's own
   rules, so a sheet meant to be opaque and padded rendered transparent and
   edge-to-edge over the page behind it. */
.hub-section-slot .project-hub-section,
.hub-section-slot > .song-lyrics-panel:not(.song-lyrics-panel--stage) {
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: 0;
    margin: 0;
    height: auto;
    min-height: 0;
}

/* One thing the padding above cannot be zero for. `--has-lyrics` is a clamp —
   `max-height` and `overflow: hidden` — and the section's own controls live
   inside it, sitting exactly on its top edge once the slot has taken the
   panel's padding away. A clipped box cuts whatever paints outside itself, so
   the hover lift and the focus ring on `sync timings` and `edit lyrics` came
   off sliced flat along the top (owner, 2026-09-07, on #243's sweep — the lift
   is the kit's, and `.action-btn` had a smaller one that clipped less
   visibly). Give back what a button paints outside its box, and hand the same
   amount to the clamp so the preview still shows the same words. */
.hub-section-slot > .song-lyrics-panel--has-lyrics:not(.song-lyrics-panel--stage) {
    /* The negative margin is the point: it grows the clip box *upwards*, into
       padding the slot already has, instead of pushing the header down. The
       pin is absolutely positioned against the slot at a fixed `top`, so a
       header that moves stops lining up with it — which is what the first
       version of this fix did, by 4px (owner, 2026-09-07). Content stays where
       it was, the box around it is 4px taller at the top, and the section's
       bottom edge does not move either. */
    margin-top: calc(-1 * var(--dd-btn-overhang));
    padding-top: var(--dd-btn-overhang);
    max-height: calc(var(--song-lyrics-clamp) + var(--dd-btn-overhang));
}

.hub-section-slot .project-hub-section--scrollable .project-hub-list {
    max-height: none;
    overflow-y: visible;
    padding-right: 0;
}

/* The offer to keep an arrangement (#204 phase 3). Quiet, right-aligned, under
   everything it is talking about — it is an aside about the page, not an action
   on the record. */
.hub-sections__offer {
    display: flex;
    justify-content: flex-end;
    margin-top: var(--space-7);
}

/* The component sets its own `display`, so it has to say what `hidden` means or
   the UA rule loses to it and the element paints while the markup says it does
   not (`conventions.md` §3). This one is hidden until there is an arrangement
   to keep, and the browser toggles it as you pin. */
.hub-sections__offer[hidden] {
    display: none;
}

/* The kit's buttons never wrap, which is right for a verb and wrong for this
   sentence. At 390 the Spanish — "usa estas chinchetas en todos tus proyectos."
   — is wider than the screen, and a row aligned to its end pushes what does not
   fit off the *left* edge: 85px of it, found by the design check once the
   note card stopped masking it (#284). Text-only and borderless, the tertiary
   button can take a second line without looking broken; the max-width is what
   makes it. Two classes so it outranks `.dd-btn` whatever loads first. */
.hub-sections__offer .hub-sections__offer-btn {
    max-width: 100%;
    white-space: normal;
    text-align: right;
    line-height: 1.3;
}

.hub-sections__offer--done {
    color: var(--text-muted);
    font-size: var(--fs-small);
}

/* Where the eye should land after a move. One pulse of the section's own tint,
   then gone — it answers "where did it go", it does not decorate. */
@keyframes hub-section-arrived {
    from {
        box-shadow: 0 0 0 3px var(--tint-20);
    }

    to {
        box-shadow: 0 0 0 3px transparent;
    }
}

.hub-section-slot--arrived {
    border-radius: var(--radius-lg);
    animation: hub-section-arrived var(--dur-slow) var(--ease-soft) 2;
}

@media (prefers-reduced-motion: reduce) {
    .hub-pin {
        transition: none;
    }

    .hub-section-slot--arrived {
        animation: none;
        box-shadow: 0 0 0 3px var(--tint-20);
    }
}

/* ── Job deliverables (#240) ──────────────────────────────────────────────
   What a job owes, above the assets that satisfy it. A row is a promise, so
   the delivered ones recede rather than celebrate: the tick and the muted
   label are the whole difference. The outstanding ones are the reason the
   list is on the page, and they read at full strength. */
/* The same 1.5em the revision viewer above and the context grid below use —
   without it the section sat flush against the assets section under it. */
#job-deliverables-section {
    margin-bottom: 1.5em;
}
.job-deliverables {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
}
.job-deliverable {
    display: flex;
    align-items: center;
    /* Wraps rather than overflows: at ≤480 the label, its hint and the status
       do not fit on one line, and a row that scrolls sideways is worse than a
       row that takes two. */
    flex-wrap: wrap;
    gap: var(--space-3);
    padding: var(--space-3) 0;
    border-bottom: 1px solid var(--border);
}
.job-deliverable:last-child { border-bottom: 0; }
.job-deliverable-state {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.25rem;
    height: 1.25rem;
    flex: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--success);
}
.job-deliverable.is-delivered .job-deliverable-state {
    border-color: color-mix(in srgb, var(--success) 45%, transparent);
    background: color-mix(in srgb, var(--success) 12%, transparent);
}
.job-deliverable-label {
    font-size: var(--fs-small);
}
.job-deliverable.is-delivered .job-deliverable-label { color: var(--text-muted); }
/* Pushed to the end, so the eye runs down one column of answers rather than
   along each row to find them. */
.job-deliverable-status {
    margin-left: auto;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
}
.job-deliverable.is-delivered .job-deliverable-status { color: var(--success); }

/* The one spec every audio deliverable on the job is bounced at (#240) — mono
   micro-label, because it is a technical qualifier on the list under it rather
   than a heading of its own (design-language §3). */
.job-deliverable-spec {
    margin: 0 0 var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
}
.job-deliverable-notes {
    margin: 0 0 var(--space-4);
    font-size: var(--fs-small);
    color: var(--text);
}

/* ── The sign-off strip (#240) ────────────────────────────────────────────
   At the head of the deliverables section, because approving is what opens
   that stage. Unapproved it is quiet and asks a question; approved it fills
   with the success tint, so the state of the job reads from across the room —
   this is the one line on the page somebody scans for. */
.job-approval {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin: 0 0 var(--space-4);
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
}
.job-approval.is-approved {
    border-color: color-mix(in srgb, var(--success) 40%, transparent);
    background: color-mix(in srgb, var(--success) 8%, transparent);
}
.job-approval-said {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-right: auto;
}
.job-approval-said strong {
    font-size: var(--fs-small);
}
.job-approval-who {
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
}
.job-approval-note {
    font-size: var(--fs-label);
    color: var(--text-muted);
}
/* The #207 rule made visible: the file changed under the approval, so it came
   off. Says why, because "it was approved a minute ago" is the reaction. */
.job-approval-stale {
    margin: 0 0 var(--space-4);
    font-size: var(--fs-small);
    color: var(--text-muted);
}

/* ── Revisions, closed (#240) ─────────────────────────────────────────────
   The viewer is replaced rather than disabled: a surface that still looks like
   a form is one people keep trying to use, and every endpoint behind it
   answers 409. Quiet by design — this is a finished thing, not an alert. */
.revisions-locked-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: var(--space-4);
}
.revisions-locked-title {
    margin: 0 0 var(--space-2);
}
.revisions-locked-body {
    margin: 0;
    max-width: 60ch;
    font-size: var(--fs-small);
    color: var(--text-muted);
}
/* Closed by default: the record matters on the day somebody asks for it, and
   every other day it is a wall of resolved notes on finished work. */
.revisions-locked-history {
    margin-top: var(--space-5);
    border-top: 1px solid var(--border);
    padding-top: var(--space-4);
}
.revisions-locked-history > summary {
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    letter-spacing: var(--tracking-wide);
    color: var(--text-muted);
}
.revisions-locked-history > summary:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 3px;
}
.revisions-locked-round {
    margin-top: var(--space-5);
}
.revisions-locked-round-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

/* ============================================
   RELEASE CHECKLIST (#200)
   The `release.` section on the project hub — what the record is still missing
   at the moment you put it out. Two groups of the same row: what keeps it off
   the site, and what a finished release carries.
   ============================================ */

/* The answer, before the detail. Green is the list being done and nothing more
   — the record being *ready* is a judgement this surface never makes. */
.release-headline {
    display: flex;
    align-items: center;
    gap: 0.45em;
    margin: 0 0 var(--space-4);
    font-size: var(--fs-body);
    font-weight: 600;
    color: var(--text);
}

.release-headline.is-done {
    color: var(--success);
}

.release-headline-icon {
    flex: none;
    width: 1.15em;
    height: 1.15em;
}

/* No scroller: see the note in `_project_release_section.html`. The groups are
   a column with air between them and nothing else — a bounded list has no
   business clamping its own height, and clamping it here put a horizontal
   scrollbar under the panel by way of the row buttons' tooltips. */
.release-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-5);
}

/* Mono micro-label, the same one the credit groups wear — these are two lists
   on one panel and they should read as siblings. */
.release-group-title {
    margin: 0 0 var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--fs-label);
    font-weight: 600;
    letter-spacing: var(--tracking-wide);
    text-transform: uppercase;
    color: var(--text-muted);
}

.release-lines {
    list-style: none;
    margin: 0;
    padding: 0;
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
}

/* The mark keeps its column whether or not it is drawing anything, so the
   sentences line up down the list rather than stepping in and out with it. */
.release-line {
    display: grid;
    grid-template-columns: 1.15em 1fr;
    gap: 0.15em 0.55em;
    font-size: var(--fs-small);
    line-height: var(--lh-body);
    color: var(--text);
}

.release-line-mark {
    display: flex;
    align-items: center;
    justify-content: center;
    height: calc(var(--fs-small) * var(--lh-body));
}

/* An empty ring for a line that is not done: the dot's outline, so an unticked
   line is a shape rather than an absence, and the two states sit on one axis. */
.release-line-mark::before {
    content: "";
    width: 0.62em;
    height: 0.62em;
    border: 1.5px solid var(--border);
    border-radius: var(--radius-pill);
}

.release-line.is-done .release-line-mark::before {
    display: none;
}

.release-line-tick {
    width: 1.05em;
    height: 1.05em;
    color: var(--success);
}

/* Done recedes: what is left is what the panel is for. */
.release-line.is-done {
    color: var(--text-muted);
}

.release-line-body {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0.15em 0.5em;
}

.release-line-note {
    color: var(--text-muted);
}

/* The way out, in the gate's own look — a link off this page, a button that
   opens a section in place and a button that opens the settings drawer are the
   same offer to a reader, so they are one style. */
.release-line-action {
    padding: 0;
    border: 0;
    background: none;
    font: inherit;
    font-weight: 600;
    color: var(--link);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
}

.release-line-action:hover {
    color: var(--link-hover);
}

.release-line-action:focus-visible {
    outline: 2px solid var(--focus-ring);
    outline-offset: 3px;
    border-radius: var(--radius-sm);
}

/* A per-song check names the songs that fail it, each into its own hub. They
   are a sentence's worth of links, so they wrap and are separated the way a
   list in prose is. */
.release-line-songs {
    display: flex;
    flex-wrap: wrap;
    gap: 0.15em 0.4em;
}

.release-line-songs .release-line-action:not(:last-child)::after {
    content: ",";
    color: var(--text-muted);
    text-decoration: none;
}

/* The switch that takes a line off this record, and the group it lands in. The
   button keeps its column on every row, done or not, so the sentences stay
   aligned and the control does not appear to move as the list changes. */
.release-line {
    grid-template-columns: 1.15em 1fr auto;
}

.release-line-switch {
    --dd-btn-pad-x: 0.35em;
    --dd-btn-pad-y: 0.35em;
    align-self: start;
    opacity: 0;
    transition: opacity var(--dur-fast) var(--ease-soft);
}

/* Present on hover, on keyboard focus, and always on a touch screen — where
   there is no hover and an invisible control is simply a missing one.

   Always on a line that is already off, too: putting it back is the only thing
   left to do with that row, and hiding the one control on it until the pointer
   finds it is hiding the way out. */
.release-line:hover .release-line-switch,
.release-line-switch:focus-visible,
.release-line.is-off .release-line-switch {
    opacity: 1;
}

@media (hover: none) {
    .release-line-switch {
        opacity: 1;
    }
}

@media (prefers-reduced-motion: reduce) {
    .release-line-switch {
        transition: none;
    }
}

/* Taken off: the sentence stays readable — you have to be able to find the
   line to put it back — but it carries no state of its own any more. */
.release-line.is-off {
    color: var(--text-muted);
}

.release-group--off .release-group-title {
    color: var(--text-muted);
}

/* A line you wrote: a real checkbox where the read-only mark goes, and the
   words in a field, because both are yours to change. The checkbox keeps the
   mark column's width so your lines and the app's line up down the panel. */
.release-line-tickbox {
    --dd-choice-size: 1.05em;
    align-self: start;
    margin-top: 0.1em;
}

.release-own-input {
    width: 100%;
    padding: 0.15em 0.35em;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: none;
    font: inherit;
    color: inherit;
}

.release-own-input:hover {
    border-color: var(--border);
}

.release-own-input:focus {
    border-color: var(--accent);
    background: var(--surface);
    outline: none;
}

.release-line.is-done .release-own-input {
    text-decoration: line-through;
    text-decoration-color: color-mix(in srgb, var(--text-muted) 60%, transparent);
}

/* The `+` that writes one sits on the group's own heading row, the way every
   other add on these hubs does — the heading keeps the mono micro-label look,
   so the row reads as a label with a control on it rather than a second
   section header inside the panel.

   It carries none of `project-hub-section-header`'s styling on purpose. That
   class is a section's *single* header, and in a tab it becomes the action row
   at the foot of the panel: heading hidden, rule above it, all of its margin on
   top. Borrowed for a heading inside the panel it reads as designed and spaces
   as designed for somewhere else — the gap under this row went missing in the
   tab and turned up above it. So the row is this rule and nothing else, and it
   is the same in both skins. */
.release-group-head {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    margin-bottom: var(--space-3);
}

.release-group-head .release-group-title {
    margin: 0;
}

/* The offer to keep this list, and the way back — under the scrolling body
   rather than inside it, because they are about the whole list and should not
   scroll away from it. */
.release-offer {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-3);
    margin-top: var(--space-4);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border);
}
