/* ==========================================================================
   NOT YET REDESIGNED — what legacy.css was still holding.

   This is the remainder of the pre-redesign stylesheet, moved here verbatim
   when legacy.css was deleted. It sits at the TOP of this file, which is the
   position legacy.css held relative to everything below, so the move changed
   nothing: base.css, then this, then the redesign.

   It is deliberately a straight concatenation and not a residue merge. Moving
   these rules to the END of the file instead looks tidier and is wrong: a rule
   appended after the redesign wins every specificity tie it used to lose, and
   @media adds no specificity, so an unscoped rule here would start beating a
   phone-only rule below. The first attempt at that did exactly this — the
   unscoped .progress-wrap landed after app.css's phone `max-width: none` and
   pushed the player bar 9 pixels wider than the viewport on every phone page.

   What lives here: the sidebar's brand and artist list, the album cards, the
   track list, the player bar's interior, the toast, the modals and their form
   fields, the "⋯" menus, the consent banner, and the upload and royalty rows.
   Each one leaves as the slice that restyles it lands, and the section is empty
   when the last of them has gone. Do not add to it.
   ========================================================================== */


#sidebar-logo {
    padding: 24px 20px 20px;
    display: flex;
    align-items: center;
    gap: 10px;
    cursor: pointer;
    flex-shrink: 0;
}

#sidebar-logo img {
    width: 32px;
    height: 32px;
    object-fit: contain;
    border-radius: 4px;
}

#sidebar-logo span {
    font-family: 'DM Serif Display', serif;
    font-size: 16px;
    color: var(--gold);
    letter-spacing: 0.02em;
}

.beta-badge {
    font-family: 'DM Mono', monospace;
    font-size: 9px;
    font-weight: 500;
    background: rgba(201, 168, 76, 0.15);
    color: var(--gold);
    border-radius: 12px;
    padding: 2px 6px;
    margin-left: 8px;
    letter-spacing: 0.04em;
    white-space: nowrap;
    border: 0.5px solid rgba(201, 168, 76, 0.3);
    transition: all 0.2s ease;
}

#sidebar-logo:hover .beta-badge {
    background: rgba(201, 168, 76, 0.25);
    border-color: rgba(201, 168, 76, 0.5);
}

#sidebar-logo-fallback {
    width: 32px;
    height: 32px;
    background: var(--gold-dim);
    border-radius: 4px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 16px;
}

#sidebar-nav {
    padding: 0 12px 12px;
    flex-shrink: 0;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.nav-btn {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 10px;
    border-radius: var(--radius);
    cursor: pointer;
    color: var(--text-dim);
    transition: color var(--transition), background var(--transition);
    font-size: 13px;
    font-weight: 500;
    user-select: none;
    border: none;
    background: none;
    width: 100%;
    text-align: left;
}

.nav-btn:hover {
    color: var(--text);
    background: #ffffff08;
}

.nav-btn.active {
    color: var(--gold);
    background: #c9a84c10;
}

.nav-btn svg {
    flex-shrink: 0;
    opacity: 0.7;
}

.nav-btn.active svg {
    opacity: 1;
}

#sidebar-divider {
    height: 1px;
    background: #ffffff08;
    margin: 4px 12px 12px;
    flex-shrink: 0;
}

#sidebar-label {
    padding: 0 20px 8px;
    font-size: 10px;
    font-weight: 500;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-muted);
    flex-shrink: 0;
}

.page-header {
    padding: 40px 40px 24px;
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
}

.page-title {
    font-family: 'DM Serif Display', serif;
    font-size: 32px;
    color: var(--text);
    line-height: 1.1;
}

.page-subtitle {
    font-size: 13px;
    color: var(--text-dim);
    margin-top: 4px;
}

.artist-hero {
    position: relative;
    padding: 60px 40px 32px;
    background: linear-gradient(to bottom, #2a2820, var(--bg));
    margin-bottom: 8px;
}

.artist-hero-name {
    font-family: 'DM Serif Display', serif;
    font-size: 52px;
    line-height: 1.05;
    color: var(--text);
}

.artist-hero-meta {
    margin-top: 10px;
    font-size: 13px;
    color: var(--text-dim);
    display: flex;
    gap: 16px;
}

.artist-hero-meta span {
    display: flex;
    align-items: center;
    gap: 6px;
}

.section-title {
    font-family: 'DM Serif Display', serif;
    font-size: 22px;
    padding: 24px 40px 16px;
    color: var(--text);
}

.album-hero {
    display: flex;
    align-items: flex-end;
    gap: 28px;
    padding: 48px 40px 28px;
    background: linear-gradient(to bottom, #2a2820, var(--bg));
}

.album-hero-cover {
    width: 180px;
    height: 180px;
    object-fit: cover;
    border-radius: var(--radius);
    flex-shrink: 0;
    box-shadow: 0 20px 60px #00000060;
    background: var(--bg3);
}

.album-hero-info {
    flex: 1;
    min-width: 0;
}

.album-hero-type {
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: 8px;
}

.album-hero-title {
    font-family: 'DM Serif Display', serif;
    font-size: 42px;
    line-height: 1.05;
    color: var(--text);
    margin-bottom: 10px;
}

.album-hero-meta {
    font-size: 13px;
    color: var(--text-dim);
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
}

.album-hero-meta .dot {
    color: var(--text-muted);
}

.album-actions {
    display: flex;
    align-items: center;
    gap: 16px;
    padding: 20px 40px;
}

.btn-play-all {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 22px;
    background: var(--gold);
    color: #17181c;
    border: none;
    border-radius: 100px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, transform 0.1s;
}

.btn-play-all:hover {
    background: #dbb95e;
    transform: scale(1.02);
}

.btn-play-all:disabled,
.btn-play-all:disabled:hover {
    background: var(--gold);
    opacity: 0.6;
    cursor: default;
    transform: none;
}

.btn-play-all svg {
    fill: #17181c;
}

.btn-outline {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 9px 18px;
    border: 1px solid #ffffff18;
    border-radius: 100px;
    background: transparent;
    color: var(--text-dim);
    font-size: 13px;
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s;
    text-decoration: none;
}

.btn-outline:hover {
    border-color: #ffffff35;
    color: var(--text);
}

.tracklist {
    padding: 0 24px 40px;
}

.tracklist-header {
    display: grid;
    grid-template-columns: 40px 1fr 100px 60px 28px;
    padding: 8px 16px;
    border-bottom: 1px solid #ffffff08;
    margin-bottom: 4px;
    font-size: 11px;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
}

.track-row {
    display: grid;
    grid-template-columns: 40px 1fr 100px 60px 28px;
    padding: 6px 16px;
    border-radius: var(--radius);
    align-items: center;
    cursor: pointer;
    transition: background var(--transition);
    gap: 4px;
}

.track-row:hover {
    background: #ffffff07;
}

.track-row.playing {
    background: #c9a84c0c;
}

.track-row.playing .track-title {
    color: var(--gold);
}

.track-num {
    font-family: 'DM Mono', monospace;
    font-size: 13px;
    color: var(--text-muted);
    text-align: center;
}

.track-row.playing .track-num {
    color: var(--gold);
}

.track-info {
    min-width: 0;
}

.track-title {
    font-size: 14px;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.track-artist {
    font-size: 12px;
    color: var(--text-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-top: 1px;
}

.track-album-name {
    font-size: 12px;
    color: var(--text-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.track-duration {
    font-family: 'DM Mono', monospace;
    font-size: 12px;
    color: var(--text-muted);
    text-align: right;
}

.playing-bars {
    display: flex;
    align-items: flex-end;
    gap: 2px;
    height: 14px;
    justify-content: center;
}

.playing-bars span {
    width: 3px;
    background: var(--gold);
    border-radius: 1px;
    animation: bar 0.9s ease-in-out infinite;
}

.playing-bars.paused span {
    animation-play-state: paused;
}

/* The row's loading spinner sits beside the bars and replaces them
   while the audio can't actually play yet (see
   refreshLoadingIndicator). Swapped by class rather than by
   rewriting the markup, so the bars' animation never restarts. */
.track-num .spinner {
    display: none;
    width: 12px;
    height: 12px;
    margin: 0 auto;
}

body.player-loading .track-row.playing .playing-bars {
    display: none;
}

body.player-loading .track-row.playing .spinner {
    display: block;
}

.playing-bars span:nth-child(2) {
    animation-delay: 0.15s;
}

.playing-bars span:nth-child(3) {
    animation-delay: 0.3s;
}

@keyframes bar {

    0%,
    100% {
        height: 4px;
    }

    50% {
        height: 14px;
    }
}

#player-track {
    display: flex;
    align-items: center;
    gap: 12px;
    min-width: 0;
    width: 240px;
    flex-shrink: 0;
}

#player-cover {
    width: 48px;
    height: 48px;
    border-radius: 4px;
    object-fit: cover;
    background: var(--bg3);
    flex-shrink: 0;
}

#player-info {
    min-width: 0;
}

#player-title {
    font-size: 13px;
    font-weight: 500;
    color: var(--text);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

#player-artist {
    font-size: 11px;
    color: var(--text-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-top: 1px;
}

#player-controls {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
}

.ctrl-btns {
    display: flex;
    align-items: center;
    gap: 16px;
}

.ctrl-btn {
    background: none;
    border: none;
    cursor: pointer;
    color: var(--text-dim);
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    transition: color var(--transition), transform 0.1s;
    padding: 4px;
}

.ctrl-btn:hover {
    color: var(--text);
}

.ctrl-btn:active {
    transform: scale(0.92);
}

.ctrl-btn.primary {
    position: relative;
    width: 36px;
    height: 36px;
    background: var(--text);
    border-radius: 50%;
    color: var(--bg);
}

body.player-loading #play-pause-btn::after {
    content: '';
    position: absolute;
    inset: -4px;
    border: 2px solid transparent;
    border-top-color: var(--gold);
    border-radius: 50%;
    animation: spin 0.7s linear infinite;
    pointer-events: none;
}

.ctrl-btn.primary:hover {
    background: var(--gold);
    transform: scale(1.05);
}

.ctrl-btn.primary svg {
    fill: var(--bg);
    margin-left: 2px;
}

#shuffle-btn.shuffle-active,
#repeat-btn.repeat-active {
    color: var(--gold);
}

#shuffle-btn.shuffle-active svg,
#repeat-btn.repeat-active svg {
    stroke: var(--gold);
}

.progress-wrap {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    max-width: 540px;
}

.time-label {
    font-family: 'DM Mono', monospace;
    font-size: 10px;
    color: var(--text-muted);
    min-width: 34px;
    text-align: center;
}

#progress-bar {
    flex: 1;
    height: 3px;
    background: var(--bg4);
    border-radius: 2px;
    cursor: pointer;
    position: relative;
    transition: height 0.15s;
}

#progress-bar:hover {
    height: 5px;
}

#progress-fill {
    height: 100%;
    background: var(--gold);
    border-radius: 2px;
    width: 0%;
    pointer-events: none;
    transition: width 0.5s linear;
}

#progress-bar.dragging #progress-fill {
    transition: none;
}

#player-extras {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 200px;
    justify-content: flex-end;
    flex-shrink: 0;
}

.vol-wrap {
    display: flex;
    align-items: center;
    gap: 8px;
}

#vol-slider {
    width: 72px;
    height: 3px;
    background: var(--bg4);
    border-radius: 2px;
    cursor: pointer;
    appearance: none;
    -webkit-appearance: none;
    outline: none;
}

#vol-slider::-webkit-slider-thumb {
    appearance: none;
    width: 10px;
    height: 10px;
    background: var(--text);
    border-radius: 50%;
    cursor: pointer;
}

#vol-slider:hover::-webkit-slider-thumb {
    background: var(--gold);
}

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

.mobile-menu-btn {
    display: none;
    position: fixed;
    top: calc(16px + var(--safe-top));
    left: calc(16px + var(--safe-left));
    z-index: 1001;
    background: rgba(30, 31, 36, 0.9);
    border-radius: 50%;
    width: 40px;
    height: 40px;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    border: none;
    color: var(--text);
    backdrop-filter: blur(8px);
    transition: background 0.2s ease;
}

.mobile-menu-btn:hover {
    background: var(--bg3);
}

.sidebar-backdrop {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.6);
    z-index: 999;
    backdrop-filter: blur(4px);
}

body.mobile-sidebar-open .sidebar-backdrop {
    display: block;
}

.status-indicator {
    position: fixed;
    bottom: calc(20px + var(--safe-bottom));
    right: calc(20px + var(--safe-right));
    z-index: 1002;
    background: rgba(0, 0, 0, 0.85);
    backdrop-filter: blur(8px);
    border-radius: 32px;
    padding: 6px 12px;
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 12px;
    font-family: 'DM Mono', monospace;
    pointer-events: none;

    /* Base animation state */
    opacity: 0;
    transform: translateY(10px);
    transition: opacity 0.25s ease, transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);

    /* Default variables (Info theme) */
    --status-color: #7ab4d6;
    /* Soft blue */
    --status-border: rgba(122, 180, 214, 0.4);

    border: 1px solid var(--status-border);
    color: var(--status-color);
}

.status-indicator.visible {
    opacity: 1;
    transform: translateY(0);
}

.status-indicator svg {
    width: 14px;
    height: 14px;
    flex-shrink: 0;
}

/* --- Theme Modifiers --- */
.status-indicator[data-type="danger"] {
    --status-color: var(--red);
    --status-border: rgba(224, 90, 78, 0.4);
}

.status-indicator[data-type="warning"] {
    --status-color: var(--gold);
    --status-border: rgba(201, 168, 76, 0.4);
}

/* GDPR Cookie Consent Banner */
/* --- Accounts: sidebar block, modal, forms --- */

/* #artist-list is flex:1 and absorbs all slack, so margin-top:auto
   would be a no-op here. flex-shrink:0 is what actually matters: without
   it a long artist list squeezes this block to nothing. */
/* z-index must clear .consent-banner (1003) — a first-time visitor
   sees that banner, and the modal has to sit above it. */
/* Scoped by id, not by class: a second .modal-backdrop in the DOM would
   otherwise be opened by the auth modal's body class, and vice versa. */
/* The tag form carries up to eight fields, so it needs more room than
   the two-field sign-in panel. */
/* The per-row edit control. Sits in the track row's own grid, revealed
   on hover or focus so it never competes with the title for attention,
   but always reachable by keyboard. */
/* "⋯" menus: a row's, the album's and the artist's. */
/* Touch screens have no hover: show the row controls, dimmed. */
/* The legal dialog sits above the consent banner like every modal, and
   above the auth modal it can follow. */
/* Upload page. <select> copies .field input so the form reads as one set of controls. */
@media (max-width: 640px) { .field-row { grid-template-columns: 1fr; } }
.modal-actions .btn-play-all,
/* --- My listening --- */
@media (max-width: 640px) {
}

@media (max-width: 640px) {
    .mobile-menu-btn {
        display: flex;
    }

    .album-hero,
    .artist-hero,
    .page-header,
    .section-title,
    .album-actions,
    .tracklist {
        padding-left: 16px;
        padding-right: 16px;
    }

    .album-actions {
        flex-wrap: wrap;
        gap: 10px;
    }

    .album-hero {
        flex-direction: column;
        align-items: center;
        text-align: center;
        gap: 16px;
        padding: 32px 16px 24px;
    }

    .album-hero-cover {
        width: 140px;
        height: 140px;
    }

    .album-hero-title {
        font-size: 28px;
    }

    .artist-hero-name {
        font-size: 36px;
    }

    #player-extras {
        display: none;
    }

    #player-track {
        width: auto;
        flex: 1;
    }

    .tracklist-header,
    .track-row {
        grid-template-columns: 40px 1fr 60px 28px;
        gap: 8px;
    }

    .track-album-name {
        display: none;
    }

    .status-indicator {
        bottom: calc(16px + var(--safe-bottom));
        right: calc(16px + var(--safe-right));
        font-size: 10px;
        padding: 4px 10px;
    }

}

/* ========================= end of the pre-redesign remainder ============ */

/* The redesign. Loaded after legacy.css, which it overrides one region at a
   time; when the last region moves here, legacy.css is deleted.

   Two rules keep the two files from fighting invisibly:
     - a rule here matches the specificity of the legacy rule it replaces, so
       order alone decides and nothing needs !important;
     - new markup uses kb- class names. The prototype learned this the hard way
       with a bare .side that collided, so every class here is prefixed and
       component-scoped.

   Colours come from docs/brand/palette/tokens.css and are checked against WCAG
   2.1 by docs/brand/palette/contrast.py. Change them there first. */

:root {
    /* ink: the dark surfaces */
    --ink-950: #0e0f12;
    --ink-900: #17181c;
    --ink-850: #1e1f24;
    --ink-800: #25262d;
    --ink-700: #2e2f38;

    /* sand: text on dark */
    --sand-100: #e8e4dc;
    --sand-300: #9a968f;
    --sand-500: #7a7670;

    /* gold: the brand */
    --gold-400: #dbb95e;
    --gold-500: #c9a84c;
    --gold-700: #8a6e2f;

    /* accents and states. The -700s are the deep ends of the warm ramp, used
       by the search genre cards; they were in docs/brand/palette/tokens.css
       from the start and simply had not been needed here yet. */
    --river-400: #6fb7c9;
    --clay-400: #e8905e;
    --clay-700: #99461c;
    --red-400: #f0676e;
    --red-700: #a82d3a;
    --green-400: #6fbf8a;

    --serif: 'DM Serif Display', Georgia, serif;
    --sans: 'DM Sans', system-ui, sans-serif;
    --mono: 'DM Mono', ui-monospace, monospace;

    --ease: cubic-bezier(.2, .8, .2, 1);
    --glass: rgba(23, 24, 28, .62);
    --glass-line: rgba(232, 228, 220, .09);
}

/* ==========================================================================
   Shell: the chrome that every page sits inside — sidebar, player bar, phone
   tab bar and toast. The pages themselves are still legacy.css and move here
   one slice at a time.
   ========================================================================== */

:root {
    --kb-sidebar-w: 248px;
    --kb-bar-h: 88px;
    --kb-tabbar-h: calc(60px + var(--safe-bottom));
    --kb-panel-w: 0px;
}

/* The grid gains a row on phones for the tab bar, and a third column for the
   desktop right panel. The column is 0 wide until body.kb-panel-open, so
   nothing moves until the panel is asked for. */
#app {
    grid-template-columns: var(--kb-sidebar-w) 1fr var(--kb-panel-w);
}

body.kb-panel-open {
    --kb-panel-w: 340px;
}

/* legacy.css spans the bar "1 / 3" — a NUMBERED span, written when there were
   two columns. Left alone, adding a third leaves a 340px hole above the panel
   with #main's background showing through it. 1 / -1 is one id against one id
   in the later file, so no !important is needed. */
#player-bar {
    grid-column: 1 / -1;
}

/* ==========================================================================
   The stacking order, in one place. It has caused one shipped bug already (the
   story panel's prev/next zones painted over it in slice 3), and a control
   hidden behind an opaque layer looks identical to a control that is missing.

     999   .sidebar-backdrop
    1000   #sidebar (phone drawer)
    1001   .kb-np            <- Now Playing
    1002   .more-menu, .status-indicator
    1003   .consent-banner
    1004   .modal-backdrop
    1005   #legal-modal
    1006   .kb-story-layer

   Now Playing sits BELOW the "⋯" menu deliberately. The instinct is to put a
   full-screen view on top of everything, which would open this view's own "⋯"
   behind it — unclickable, and invisible in a screenshot. The desktop panel is
   a grid column and is not on this ladder at all.
   ========================================================================== */

/* --- sidebar ------------------------------------------------------------ */

#sidebar {
    background: var(--ink-950);
    border-right: 1px solid var(--ink-700);
    padding: 22px 0 12px;
    padding-left: var(--safe-left);
    gap: 18px;
}

#sidebar-logo {
    padding: 0 22px 2px;
    gap: 9px;
}

#sidebar-logo span {
    font-family: var(--serif);
    font-size: 17px;
    color: var(--gold-500);
}

.beta-badge {
    font-family: var(--mono);
    background: rgba(201, 168, 76, .12);
    color: var(--gold-500);
    border: 1px solid rgba(201, 168, 76, .28);
}

#sidebar-nav {
    padding: 0 10px;
    gap: 2px;
}

/* 44px is the smallest target a thumb reliably hits, and the drawer is a
   thumb target on phones even though this is the desktop sidebar. */
.nav-btn {
    height: 44px;
    padding: 0 12px;
    border-radius: 10px;
    gap: 14px;
    font-size: 15px;
    color: var(--sand-300);
    transition: background .12s, color .12s;
}

.nav-btn:hover {
    color: var(--sand-100);
    background: rgba(232, 228, 220, .05);
}

/* The active item reads as raised sand, not as a gold block: gold is the
   brand's accent and is spent on the icon alone. */
.nav-btn.active {
    color: var(--sand-100);
    background: rgba(232, 228, 220, .07);
}

.nav-btn svg {
    opacity: 1;
    stroke: var(--sand-500);
}

.nav-btn:hover svg {
    stroke: var(--sand-300);
}

.nav-btn.active svg {
    stroke: var(--gold-500);
}

#sidebar-divider {
    background: var(--ink-700);
    margin: 0 22px;
}

#sidebar-label {
    padding: 0 22px 6px;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .1em;
    color: var(--sand-300);
}

#artist-list {
    padding: 0 10px 8px;
}

#artist-list::-webkit-scrollbar-thumb {
    background: var(--ink-700);
}

/* #player-track became a <button> so the whole block opens Now Playing; these
   are the UA button styles it has to shed to keep looking like the div it was. */
#player-track {
    border: none;
    background: none;
    text-align: left;
    font: inherit;
    color: inherit;
    cursor: pointer;
}

/* --- player bar --------------------------------------------------------- */

/* Glass rather than a flat fill: the bar sits over scrolling artwork, and the
   blur is what keeps the titles readable against whatever passes under it.
   -webkit- prefix included — iOS is the platform this matters most on. */
#player-bar {
    background: rgba(14, 15, 18, .82);
    backdrop-filter: blur(28px) saturate(1.5);
    -webkit-backdrop-filter: blur(28px) saturate(1.5);
    border-top: 1px solid var(--glass-line);
    gap: 16px;
    padding: 0 calc(16px + var(--safe-right)) 0 calc(16px + var(--safe-left));
    padding-bottom: var(--safe-bottom);
}

#player-cover {
    width: 60px;
    height: 60px;
    border-radius: 6px;
    background: var(--ink-800);
}

#player-title {
    font-size: 15px;
}

#player-artist {
    font-size: 13px;
    color: var(--sand-300);
}

.ctrl-btn {
    color: var(--sand-300);
}

.ctrl-btn:hover {
    color: var(--sand-100);
}

/* Sand, not gold. The play button is the one control that is pressed without
   being read, so it wins on contrast; gold at this size reads as a warning. */
.ctrl-btn.primary {
    background: var(--sand-100);
    color: var(--ink-900);
}

.ctrl-btn.primary:hover {
    background: #fff;
    transform: scale(1.04);
}

.ctrl-btn.primary svg {
    fill: var(--ink-900);
}

#progress-fill {
    background: var(--sand-100);
}

#progress-bar:hover #progress-fill,
#progress-bar.dragging #progress-fill {
    background: var(--gold-500);
}

.time-label {
    font-family: var(--mono);
    color: var(--sand-300);
}

/* --- phone tab bar ------------------------------------------------------- */

/* Hidden by default and shown at the phone breakpoint, rather than the other
   way round: a stylesheet that fails to load should leave a usable desktop,
   and a stray tab bar across the bottom of a 27" screen is not that. */
#tabbar {
    display: none;
}

.kb-tab {
    flex: 1;
    min-width: 0;
    height: 52px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 3px;
    border: none;
    background: none;
    cursor: pointer;
    font-family: inherit;
    font-size: 11px;
    font-weight: 500;
    color: var(--sand-300);
    transition: color .12s;
    -webkit-tap-highlight-color: transparent;
}

.kb-tab svg {
    stroke: var(--sand-500);
    transition: stroke .12s;
}

.kb-tab.active {
    color: var(--gold-500);
}

.kb-tab.active svg {
    stroke: var(--gold-500);
}

/* --- toast --------------------------------------------------------------- */

/* Sits above the player bar rather than in the corner: the corner is where the
   volume slider and the fullscreen control live, and a status pill there was
   read as a control. Centred over the content column on desktop. */
.status-indicator {
    top: auto;
    bottom: calc(var(--kb-bar-h) + 18px + var(--safe-bottom));
    right: auto;
    left: calc(var(--kb-sidebar-w) + (100vw - var(--kb-sidebar-w)) / 2);
    transform: translate(-50%, 10px);
    background: rgba(37, 38, 45, .9);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border-radius: 22px;
    padding: 11px 18px;
    font-family: var(--sans);
    font-size: 14px;
    max-width: min(440px, calc(100vw - 32px));
    --status-color: var(--sand-100);
    --status-border: var(--glass-line);
}

.status-indicator.visible {
    transform: translate(-50%, 0);
}

.status-indicator svg {
    width: 16px;
    height: 16px;
    color: var(--river-400);
}

.status-indicator[data-type="danger"] {
    --status-border: rgba(240, 103, 110, .35);
}

.status-indicator[data-type="danger"] svg {
    color: var(--red-400);
}

.status-indicator[data-type="warning"] {
    --status-border: rgba(219, 185, 94, .35);
}

.status-indicator[data-type="warning"] svg {
    color: var(--gold-400);
}

/* --- focus --------------------------------------------------------------- */

/* One visible focus ring for the whole shell. :focus-visible only, so a mouse
   press does not leave a ring behind, and gold so it is never mistaken for the
   active state's sand. */
#sidebar :focus-visible,
#player-bar :focus-visible,
#tabbar :focus-visible {
    outline: 2px solid var(--gold-500);
    outline-offset: 2px;
    border-radius: 8px;
}

/* --- phones -------------------------------------------------------------- */

@media (max-width: 640px) {
    #app {
        grid-template-columns: 1fr;
        grid-template-rows: 1fr var(--kb-bar-h) var(--kb-tabbar-h);
    }

    #main {
        grid-row: 1 / 2;
        padding-top: var(--safe-top);
    }

    /* The mini player. Cover and title take the row, transport shrinks to the
       three controls a thumb actually uses, and the progress bar becomes a
       hairline along the bottom edge — which is also the only place it can go
       without stealing a line of height from the title. */
    #player-bar {
        grid-row: 2 / 3;
        grid-column: 1 / -1;
        position: relative;
        padding: 0 calc(12px + var(--safe-right)) 0 calc(12px + var(--safe-left));
        padding-bottom: 0;
        gap: 10px;
    }

    #player-track {
        width: auto;
        flex: 1;
        min-width: 0;
    }

    #player-controls {
        flex: none;
        flex-direction: row;
        gap: 0;
    }

    .ctrl-btns {
        gap: 10px;
    }

    /* Shuffle and repeat are modes, not actions: they belong on the Now
       Playing screen, where there is room to show which one is on. */
    #shuffle-btn,
    #repeat-btn {
        display: none;
    }

    .progress-wrap {
        position: absolute;
        left: 0;
        right: 0;
        bottom: 0;
        max-width: none;
        gap: 0;
    }

    /* The times are on the Now Playing screen too. Here they would cost more
       width than they are worth next to a two-line title. */
    .time-label {
        display: none;
    }

    #progress-bar {
        height: 2px;
        border-radius: 0;
    }

    #progress-bar:hover {
        height: 2px;
    }

    #tabbar {
        grid-row: 3 / 4;
        grid-column: 1 / -1;
        display: flex;
        align-items: flex-start;
        padding: 4px 6px var(--safe-bottom);
        background: rgba(14, 15, 18, .86);
        backdrop-filter: blur(26px) saturate(1.5);
        -webkit-backdrop-filter: blur(26px) saturate(1.5);
        border-top: 1px solid var(--glass-line);
    }

    /* The More tab replaced it. Kept in the markup so nothing that reaches for
       #mobile-menu-btn breaks; hidden because two ways to open one drawer is
       one too many. */
    .mobile-menu-btn {
        display: none;
    }

    #sidebar {
        width: 86%;
        max-width: 300px;
        padding-top: calc(22px + var(--safe-top));
    }

    #player-cover {
        width: 48px;
        height: 48px;
    }

    .status-indicator {
        left: 50%;
        bottom: auto;
        top: calc(16px + var(--safe-top));
        transform: translate(-50%, -10px);
        font-size: 13px;
    }

    .status-indicator.visible {
        transform: translate(-50%, 0);
    }
}

/* ==========================================================================
   Home: the story rail, the chips and the shelves. Everything here is filled
   from the catalogue, so every block has to survive being empty — home.js hides
   the section rather than leaving a heading over nothing.
   ========================================================================== */

/* Phones only. On desktop the sidebar already carries the wordmark two
   centimetres to the left, and two of them on one screen reads as a mistake. */
.kb-home-head {
    display: none;
    align-items: center;
    justify-content: space-between;
    padding: 28px 40px 10px;
}

.kb-home-brand {
    display: flex;
    align-items: center;
    gap: 9px;
    font-family: var(--serif);
    font-size: 20px;
    color: var(--sand-100);
}

/* --- story rail ---------------------------------------------------------- */

/* Scrolls sideways, with the scrollbar hidden: a horizontal bar under a row of
   circles reads as a broken layout, and the rail is short enough to flick. */
.kb-stories {
    display: flex;
    gap: 16px;
    padding: 28px 40px 14px;
    overflow-x: auto;
    scrollbar-width: none;
}

.kb-stories::-webkit-scrollbar {
    display: none;
}

.kb-story {
    flex: none;
    width: 74px;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 7px;
    border: none;
    background: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
    color: var(--sand-300);
    -webkit-tap-highlight-color: transparent;
}

/* The ring is the unread state: gold until opened, then a flat border. Two
   nested elements because the gradient is the ring and the inner element masks
   its middle out — a border-image cannot be rounded reliably across browsers. */
.kb-story-ring {
    width: 66px;
    height: 66px;
    border-radius: 50%;
    padding: 2px;
    background: linear-gradient(140deg, var(--gold-400), var(--clay-400));
    display: grid;
    place-items: center;
    transition: transform .12s var(--ease);
}

.kb-story:active .kb-story-ring {
    transform: scale(.95);
}

.kb-story-ring.seen {
    background: var(--ink-700);
}

.kb-story-img {
    width: 100%;
    height: 100%;
    border-radius: 50%;
    overflow: hidden;
    display: grid;
    place-items: center;
    background: var(--ink-850);
    border: 2px solid var(--ink-900);
}

.kb-story-img img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.kb-story-news {
    width: 100%;
    height: 100%;
    display: grid;
    place-items: center;
    background: var(--gold-500);
}

.kb-story-name {
    font-size: 11px;
    max-width: 100%;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- chips --------------------------------------------------------------- */

.kb-chips {
    display: flex;
    gap: 8px;
    padding: 4px 40px 18px;
    overflow-x: auto;
    scrollbar-width: none;
}

.kb-chips::-webkit-scrollbar {
    display: none;
}

.kb-chip {
    flex: none;
    height: 34px;
    padding: 0 15px;
    border-radius: 17px;
    border: 1px solid var(--ink-700);
    background: none;
    color: var(--sand-300);
    font: 500 13px var(--sans);
    cursor: pointer;
    transition: background .12s, color .12s, border-color .12s;
    -webkit-tap-highlight-color: transparent;
}

.kb-chip:hover {
    color: var(--sand-100);
    border-color: var(--sand-500);
}

.kb-chip.on {
    background: var(--sand-100);
    border-color: var(--sand-100);
    color: var(--ink-900);
}

/* --- shelves ------------------------------------------------------------- */

.kb-section-head {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    padding: 12px 40px 10px;
}

.kb-section-head h2 {
    font: 400 22px/1.1 var(--serif);
    color: var(--sand-100);
}

.kb-shelf {
    display: flex;
    gap: 16px;
    padding: 0 40px 24px;
    overflow-x: auto;
    scrollbar-width: none;
}

.kb-shelf::-webkit-scrollbar {
    display: none;
}

.kb-tile {
    position: relative;
    flex: none;
    width: 168px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    border: none;
    background: none;
    padding: 0;
    text-align: left;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.kb-tile-open {
    display: flex;
    flex-direction: column;
    gap: 2px;
    border: none;
    background: none;
    padding: 0;
    text-align: left;
    color: inherit;
    font: inherit;
    cursor: pointer;
}

.kb-tile-cover {
    width: 168px;
    height: 168px;
    border-radius: 8px;
    object-fit: cover;
    background: var(--ink-850);
    margin-bottom: 8px;
}

.kb-tile-cover.round {
    border-radius: 50%;
}

.kb-tile b {
    font-weight: 500;
    font-size: 14px;
    color: var(--sand-100);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kb-tile i {
    font-style: normal;
    font-size: 12px;
    color: var(--sand-300);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Appears on hover on a pointer device, and is always there on a touch screen,
   where there is no hover to reveal it with. */
.kb-tile-play {
    position: absolute;
    right: 10px;
    top: 118px;
    width: 40px;
    height: 40px;
    border-radius: 50%;
    border: none;
    background: var(--gold-500);
    color: var(--ink-900);
    display: grid;
    place-items: center;
    cursor: pointer;
    opacity: 0;
    transform: translateY(6px);
    transition: opacity .14s, transform .14s var(--ease);
    box-shadow: 0 6px 18px rgba(0, 0, 0, .45);
}

.kb-tile:hover .kb-tile-play,
.kb-tile-play:focus-visible {
    opacity: 1;
    transform: none;
}

@media (hover: none) {
    .kb-tile-play {
        opacity: 1;
        transform: none;
    }
}

/* --- story viewer -------------------------------------------------------- */

.kb-story-layer {
    position: fixed;
    inset: 0;
    z-index: 1006;
    display: none;
    place-items: center;
    background: rgba(8, 9, 11, .92);
    backdrop-filter: blur(18px);
    -webkit-backdrop-filter: blur(18px);
    padding: 24px;
}

body.story-open .kb-story-layer {
    display: grid;
}

/* Above the prev/next tap zones. Without the z-index those two half-screen
   buttons paint over the card — they come later in the markup and carry z-index
   1 — and the Play and Open album buttons cannot be clicked at all. */
.kb-story-panel {
    position: relative;
    z-index: 2;
    width: min(420px, 100%);
    max-height: 100%;
    display: flex;
    flex-direction: column;
    gap: 14px;
    animation: kbStoryIn .22s var(--ease) both;
}

@keyframes kbStoryIn {
    from { transform: scale(.96); opacity: 0; }
    to { transform: none; opacity: 1; }
}

.kb-story-cover {
    width: 100%;
    aspect-ratio: 1;
    object-fit: cover;
    border-radius: 14px;
    background: var(--ink-850);
}

.kb-story-meta,
.kb-story-newscard {
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.kb-story-newscard {
    gap: 10px;
    padding: 26px;
    border-radius: 14px;
    background: var(--ink-850);
    border: 1px solid var(--glass-line);
}

.kb-story-mark {
    width: 52px;
    height: 52px;
    border-radius: 50%;
    background: var(--gold-500);
    display: grid;
    place-items: center;
}

.kb-story-eyebrow {
    font: 500 11px var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--gold-500);
}

.kb-story-title {
    font: 400 24px/1.15 var(--serif);
    color: var(--sand-100);
}

.kb-story-sub {
    font-size: 14px;
    color: var(--sand-300);
}

.kb-story-body {
    font-size: 14px;
    line-height: 1.6;
    color: var(--sand-300);
}

.kb-story-actions {
    display: flex;
    gap: 10px;
}

.kb-story-play,
.kb-story-open {
    height: 42px;
    padding: 0 20px;
    border-radius: 21px;
    border: 1px solid var(--ink-700);
    background: none;
    color: var(--sand-100);
    font: 500 14px var(--sans);
    cursor: pointer;
}

.kb-story-play {
    background: var(--gold-500);
    border-color: var(--gold-500);
    color: var(--ink-900);
}

.kb-story-close {
    position: absolute;
    top: calc(16px + var(--safe-top));
    right: 16px;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: none;
    background: rgba(37, 38, 45, .8);
    color: var(--sand-100);
    display: grid;
    place-items: center;
    cursor: pointer;
    z-index: 2;
}

/* Half the screen each, behind the card, so a tap anywhere left or right moves
   through the rail the way a story is expected to behave. */
.kb-story-prev,
.kb-story-next {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 50%;
    border: none;
    background: none;
    cursor: pointer;
    z-index: 1;
}

.kb-story-prev { left: 0; }
.kb-story-next { right: 0; }

/* --- phones -------------------------------------------------------------- */

@media (max-width: 640px) {
    .kb-home-head { display: flex; padding: 18px 16px 8px; }
    .kb-stories { padding: 4px 16px 12px; gap: 14px; }
    .kb-chips { padding: 2px 16px 14px; }
    .kb-section-head { padding: 8px 16px 8px; }
    .kb-section-head h2 { font-size: 20px; }
    .kb-shelf { padding: 0 16px 20px; gap: 12px; }
    .kb-tile { width: 144px; }
    .kb-tile-cover { width: 144px; height: 144px; }
    .kb-tile-play { top: 96px; right: 8px; }
}

/* Trending reuses the ordinary track list, so a chart row plays and queues like
   any other row. Only the padding differs: .tracklist carries its own page
   gutter, and inside a Home section that would double up. */
#trending-list {
    padding: 0 40px 12px;
}

@media (max-width: 640px) {
    #trending-list {
        padding: 0 16px 8px;
    }
}

/* ==========================================================================
   Album, Artist and All Tracks. The pages that were the old player's, brought
   onto the brand: a blurred wash of the artwork behind each hero, serif titles
   at display size, and one gold play button that is obviously the thing to
   press.
   ========================================================================== */

/* --- the ambient wash ---------------------------------------------------- */

/* The page is what the wash is positioned against. Without this it resolves to
   the nearest positioned ancestor — which is outside #main — and the blur
   spreads across the sidebar, washing the navigation out. */
.page {
    position: relative;
}

/* Sits behind the hero and nothing else. The gradient fades it into the page
   background so there is no visible edge where the blur stops. */
.kb-ambient {
    position: absolute;
    left: 0;
    right: 0;
    top: 0;
    height: 460px;
    overflow: hidden;
    pointer-events: none;
    z-index: 0;
}

.kb-ambient img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: blur(48px) saturate(1.3) brightness(.5);
    transform: scale(1.15);
}

.kb-ambient::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(rgba(23, 24, 28, .35) 0%, rgba(23, 24, 28, 0) 28%, var(--ink-900) 94%);
}

/* The page is position:static, so anything that must paint above the wash needs
   its own stacking context. This bit the desktop prototype: an absolute layer
   paints over static content that follows it. */
#page-album > *:not(.kb-ambient),
#page-artist > *:not(.kb-ambient) {
    position: relative;
    z-index: 1;
}

.kb-eyebrow {
    font: 500 11px var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--gold-500);
}

.kb-facts {
    padding: 4px 40px 40px;
    font-size: 13px;
    color: var(--sand-300);
}

/* --- album hero ---------------------------------------------------------- */

.album-hero {
    align-items: flex-end;
    gap: 28px;
    padding: 56px 40px 20px;
}

.album-hero-cover {
    width: 232px;
    height: 232px;
    border-radius: 8px;
    box-shadow: 0 24px 50px rgba(0, 0, 0, .45);
}

.album-hero-type {
    font: 500 11px var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--gold-500);
    margin-bottom: 8px;
}

.album-hero-title {
    font: 400 52px/1.02 var(--serif);
    color: var(--sand-100);
    margin-bottom: 10px;
}

.album-hero-meta {
    font-size: 14px;
    color: var(--sand-300);
}

/* --- artist hero --------------------------------------------------------- */

.artist-hero {
    min-height: 300px;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    gap: 6px;
    padding: 40px 40px 16px;
}

.artist-hero-name {
    font: 400 72px/1 var(--serif);
    color: var(--sand-100);
}

.artist-hero-meta {
    font-size: 14px;
    color: var(--sand-300);
}

/* --- the actions row ----------------------------------------------------- */

.album-actions {
    align-items: center;
    gap: 10px;
    padding: 4px 40px 20px;
}

/* The one control on the page that is pressed without being read, so it is the
   only round gold thing on it. Order -1 puts it first on desktop, where the
   design leads with it; the exports follow as outlines. */
/* .btn-play-all is the shared PRIMARY button: Save, Sign in, Accept, Send
   report and the album's play control all carry it. Styling the class itself as
   a round icon button turns every modal's submit into a gold circle with an
   invisible label — which is what happened, and what this split prevents. The
   class gets a primary pill; only the album's actions row gets the circle. */
.btn-play-all {
    height: 40px;
    padding: 0 20px;
    border-radius: 20px;
    background: var(--gold-500);
    color: var(--ink-900);
    font: 500 14px var(--sans);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    transition: background .12s;
}

.btn-play-all:hover {
    background: var(--gold-400);
}

/* The one control on the page that is pressed without being read, so it is the
   only round gold thing on it. order:-1 leads with it on desktop. */
.album-actions .btn-play-all {
    order: -1;
    width: 56px;
    height: 56px;
    padding: 0;
    border-radius: 28px;
    box-shadow: 0 8px 24px rgba(201, 168, 76, .25);
    transition: transform .12s var(--ease), background .12s;
}

.album-actions .btn-play-all:hover {
    transform: scale(1.04);
}

.album-actions .btn-play-all svg {
    width: 22px;
    height: 22px;
    fill: var(--ink-900);
}

/* The label stays in the DOM for the accessible name but out of the layout: as
   a flex item, even at font-size 0, it shifts the triangle off centre. */
.kb-sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
}

.btn-outline {
    height: 36px;
    padding: 0 16px;
    border-radius: 18px;
    border: 1px solid var(--ink-700);
    color: var(--sand-300);
    font: 500 13px var(--sans);
    align-items: center;
    transition: border-color .12s, color .12s;
}

.btn-outline:hover {
    border-color: var(--sand-500);
    color: var(--sand-100);
}

/* --- the album grid, on Library and an artist's page --------------------- */

/* Both pages used to draw .album-card: a second album tile with its own cover
   wrapper, its own hover overlay and its own round play button, which is why
   restyling Home in slice 3 left them looking like the old player. They share
   .kb-tile now and .album-card is gone. A shelf tile is a fixed 168px and does
   not shrink; here the same markup fills its column instead, which is the only
   difference between the two uses. */

.kb-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
    gap: 24px 20px;
    padding: 0 40px 40px;
}

.kb-grid .kb-tile,
.kb-grid .kb-tile-open {
    width: 100%;
}

.kb-grid .kb-tile-cover {
    width: 100%;
    height: auto;
    aspect-ratio: 1;
}

/* The play button is pinned inside the cover's bottom-right corner. On a shelf
   the cover is a fixed 168px so the offset can be a number; here it is whatever
   the column is wide, and the title underneath may run to two lines, so neither
   end of the tile is a fixed distance from the cover.
   A percentage margin resolves against the containing block's WIDTH — which,
   with aspect-ratio: 1 on the cover, is exactly the cover's height. So this
   tracks the column at any size, and hardcoding it does not: measured at 174px
   the button hung 5px below the artwork, onto the title. */
.kb-grid .kb-tile-play {
    top: 0;
    margin-top: calc(100% - 50px);
}

.kb-tile-ghost {
    aspect-ratio: 1;
    border-radius: 8px;
    background: var(--ink-850);
}

.kb-tile-ghost:nth-child(2) { opacity: .7; }
.kb-tile-ghost:nth-child(3) { opacity: .45; }

/* Two up on a phone. minmax(168px, 1fr) would give one column at 390px minus
   the gutters, which wastes the width a cover most wants. */
@media (max-width: 640px) {
    .kb-grid {
        grid-template-columns: repeat(2, 1fr);
        gap: 18px 12px;
        padding: 0 16px 24px;
    }
}

/* --- the sidebar's artist and album list --------------------------------- */

/* The sidebar chrome was restyled in slice 2 and its list was not, so the one
   part of the sidebar a listener actually reads kept the old type and spacing.
   The active album reads as raised sand with a gold rule, the same way the nav
   items above it do — gold is the brand, not a highlighter. */

.artist-item {
    padding: 7px 10px;
    margin-top: 6px;
    border-radius: 6px;
    font-size: 13px;
    font-weight: 500;
    color: var(--sand-300);
    letter-spacing: .01em;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: pointer;
}

.artist-item:hover {
    background: rgba(232, 228, 220, .05);
    color: var(--sand-100);
}

.artist-item.active {
    background: rgba(232, 228, 220, .07);
    color: var(--sand-100);
}

.album-item {
    padding: 6px 10px 6px 20px;
    border-radius: 6px;
    font-size: 13px;
    color: var(--sand-300);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: pointer;
}

.album-item:hover {
    background: rgba(232, 228, 220, .04);
    color: var(--sand-100);
}

.album-item.active {
    color: var(--gold-400);
    box-shadow: inset 2px 0 0 var(--gold-500);
}

/* --- track rows ---------------------------------------------------------- */

.track-row {
    border-radius: 8px;
    padding: 8px 12px;
}

.track-row:hover {
    background: rgba(232, 228, 220, .045);
}

.track-num {
    font-family: var(--mono);
    color: var(--sand-300);
}

.track-title {
    font-size: 15px;
    color: var(--sand-100);
}

.track-row.playing .track-title {
    color: var(--gold-500);
}

.track-artist,
.track-album-name,
.track-duration {
    color: var(--sand-300);
}

.track-duration {
    font-family: var(--mono);
    font-size: 13px;
}

.tracklist-header {
    border-bottom: 1px solid var(--ink-700);
    color: var(--sand-300);
    font-family: var(--mono);
    letter-spacing: .08em;
}

/* --- page headers and section titles ------------------------------------- */

.page-title {
    font: 400 40px/1.05 var(--serif);
    color: var(--sand-100);
}

.page-subtitle {
    font-size: 14px;
    color: var(--sand-300);
}

.section-title {
    font: 400 22px/1.1 var(--serif);
    color: var(--sand-100);
}

/* --- phones -------------------------------------------------------------- */

@media (max-width: 640px) {
    .kb-ambient { height: 380px; }

    /* Centred and stacked, the way a phone shows a record sleeve.
       align-items has to be restated: legacy.css centres it inside its own
       media query, but this file's unscoped .album-hero rule comes later in the
       cascade and wins even on a phone — which left the whole hero aligned to
       the right-hand edge. */
    .album-hero {
        flex-direction: column;
        align-items: center;
        text-align: center;
        padding: 40px 16px 16px;
        gap: 16px;
    }

    .album-hero-cover {
        width: 210px;
        height: 210px;
    }

    .album-hero-title { font-size: 32px; }

    .artist-hero {
        min-height: 240px;
        padding: 24px 16px 12px;
    }

    .artist-hero-name { font-size: 40px; }

    .album-actions { padding: 4px 16px 16px; }

    .kb-facts { padding: 4px 16px 24px; }
    .page-title { font-size: 30px; }
}

/* ==========================================================================
   Now Playing, the desktop right panel and the queue rows they share.

   Class names are kb-np-*, kb-panel-* and kb-queue-*, with nothing bare. The
   prototype used a plain .dragging on three different elements and .side on
   two; .dragging is ALREADY taken here by #progress-bar, styled in both
   stylesheets, so a bare one would have restyled the seek bar every time a
   cover was swiped.
   ========================================================================== */

/* --- the shortcuts list and the sidebar's way in ------------------------ */

#sidebar-keys {
    display: flex;
    align-items: center;
    gap: 10px;
    width: calc(100% - 24px);
    margin: 0 12px 8px;
    padding: 9px 12px;
    border-radius: 8px;
    background: none;
    border: none;
    color: var(--sand-300);
    font: 500 13px var(--sans);
    cursor: pointer;
    transition: color .12s, background .12s;
}

#sidebar-keys:hover {
    color: var(--sand-100);
    background: var(--ink-850);
}

#sidebar-keys span { flex: 1; text-align: left; }

kbd {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 24px;
    height: 24px;
    padding: 0 7px;
    border-radius: 6px;
    background: var(--ink-800);
    border: 1px solid var(--ink-700);
    border-bottom-width: 2px;
    color: var(--sand-100);
    font: 500 12px var(--mono);
    white-space: nowrap;
}

.kb-keys { display: grid; gap: 10px; margin: 4px 0 8px; }

.kb-keys div {
    display: flex;
    align-items: center;
    gap: 8px;
    font-size: 14px;
    color: var(--sand-300);
}

.kb-keys span { flex: 1; text-align: right; }

body.keys-modal-open #keys-modal {
    opacity: 1;
    pointer-events: auto;
}

body.keys-modal-open #keys-modal .modal-panel { transform: translateY(0); }

/* --- queue rows, shared by Now Playing and the panel -------------------- */

/* Deliberately NOT .track-row: markPlayingRows sweeps that class across the
   whole document, swapping each row's .track-num for equaliser bars and
   restoring it from data-track-num, which these rows have not got. */
.kb-queue-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 52px;
    padding: 6px 0;
    background: none;
    border: none;
    text-align: left;
    color: inherit;
    font: inherit;
    cursor: pointer;
    border-radius: 8px;
}

.kb-queue-row:hover { background: rgba(232, 228, 220, .045); }

.kb-queue-art {
    width: 40px;
    height: 40px;
    flex: none;
    border-radius: 4px;
    object-fit: cover;
}

.kb-queue-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }

.kb-queue-txt b {
    font-size: 14px;
    font-weight: 500;
    color: var(--sand-100);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kb-queue-txt i {
    font-style: normal;
    font-size: 12px;
    color: var(--sand-300);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kb-queue-dur { font: 12px var(--mono); color: var(--sand-300); flex: none; }

.kb-queue-now .kb-queue-txt b { color: var(--gold-500); }

.kb-np-eyebrow {
    display: block;
    margin-top: 10px;
    font: 500 11px var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--gold-500);
}

.kb-np-empty { padding: 18px 0; font-size: 14px; color: var(--sand-300); }

.kb-credit {
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 10px 0;
    border-bottom: 1px solid var(--ink-800);
}

.kb-credit b { font-size: 14px; font-weight: 500; color: var(--sand-100); }
.kb-credit i { font-style: normal; font-size: 12px; color: var(--sand-300); }

/* --- the desktop right panel ------------------------------------------- */

#panel {
    grid-column: 3 / 4;
    grid-row: 1 / 2;
    background: var(--ink-950);
    border-left: 1px solid var(--ink-700);
    /* The column snaps between 0 and 340px while the panel slides its own
       transform. Transitioning grid-template-columns is newer than this player
       assumes, and animating a margin on #main relayouts the scrolling
       container on every frame. */
    overflow: hidden;
    display: flex;
    flex-direction: column;
    transform: translateX(100%);
    transition: transform .24s var(--ease);
}

body.kb-panel-open #panel { transform: none; }

.kb-panel-tabs {
    display: flex;
    align-items: center;
    gap: 4px;
    flex: none;
    padding: 14px 8px 10px 14px;
    border-bottom: 1px solid var(--ink-700);
}

.kb-panel-tab {
    height: 34px;
    padding: 0 12px;
    border-radius: 17px;
    border: none;
    background: none;
    color: var(--sand-300);
    font: 600 13px var(--sans);
    cursor: pointer;
}

.kb-panel-tab.on { background: rgba(232, 228, 220, .08); color: var(--sand-100); }

.kb-panel-x {
    margin-left: auto;
    width: 32px;
    height: 32px;
    display: grid;
    place-items: center;
    border: none;
    background: none;
    color: var(--sand-300);
    cursor: pointer;
}

.kb-panel-x:hover { color: var(--sand-100); }

.kb-panel-body {
    flex: 1;
    overflow-y: auto;
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 2px;
    scrollbar-width: thin;
}

.kb-panel-cover {
    width: 100%;
    aspect-ratio: 1;
    border-radius: 8px;
    object-fit: cover;
    box-shadow: 0 20px 40px rgba(0, 0, 0, .4);
}

.kb-panel-open-np {
    margin-top: 12px;
    height: 36px;
    border-radius: 18px;
    border: 1px solid var(--ink-700);
    background: none;
    color: var(--sand-100);
    font: 500 13px var(--sans);
    cursor: pointer;
}

.kb-panel-open-np:hover { border-color: var(--gold-500); color: var(--gold-400); }

.kb-panel-title { margin-top: 14px; font: 20px/1.2 var(--serif); color: var(--sand-100); }

.kb-panel-artist {
    margin-top: 2px;
    align-self: flex-start;
    padding: 0;
    border: none;
    background: none;
    color: var(--sand-300);
    font: 14px var(--sans);
    cursor: pointer;
}

.kb-panel-artist:hover { color: var(--gold-400); }

.kb-panel-card {
    margin-top: 16px;
    padding: 14px;
    border-radius: 12px;
    background: var(--ink-850);
    display: flex;
    flex-direction: column;
    gap: 6px;
}

.kb-panel-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    padding: 4px 0;
    background: none;
    border: none;
    text-align: left;
    cursor: pointer;
}

/* The drop box only exists while something is being dragged. The listeners are
   on #panel rather than here precisely because this element appears mid-drag
   and reflows the panel under the cursor. */
.kb-panel-drop {
    display: none;
    margin: 10px 0 4px;
    min-height: 56px;
    border-radius: 12px;
    border: 1.5px dashed var(--ink-700);
    align-items: center;
    justify-content: center;
    gap: 10px;
    font-size: 14px;
    color: var(--sand-300);
    transition: border-color .12s, background .12s;
}

body.kb-drag-active .kb-panel-drop { display: flex; }

.kb-panel-drop.over {
    border-color: var(--gold-500);
    background: rgba(201, 168, 76, .08);
    color: var(--sand-100);
}

#panel :focus-visible {
    outline: 2px solid var(--gold-500);
    outline-offset: 2px;
    border-radius: 8px;
}

/* Rows are draggable on a pointer device. draggable is emitted unconditionally
   and gated in the dragstart handler, so a window narrowed past the breakpoint
   cannot leave a live drag with nowhere to drop it. */
.track-row[draggable="true"] { cursor: pointer; }

/* --- Now Playing -------------------------------------------------------- */

.kb-np {
    position: fixed;
    inset: 0;
    z-index: 1001;
    background: var(--ink-950);
    transform: translateY(100%);
    /* visibility, not display: the sheet has to keep its box so the carousel
       can be measured, but a merely translated layer still takes pointer
       events across the whole viewport. The 0s delay lets the slide-down
       finish before it goes away. */
    transition: transform .3s var(--ease), visibility 0s linear .3s;
    /* The blurred backdrop is inset by -60px, so without this the layer's own
       overflow reaches outside the viewport. */
    overflow: hidden;
    visibility: hidden;
}

body.kb-np-open .kb-np {
    transform: none;
    visibility: visible;
    transition: transform .3s var(--ease), visibility 0s;
}

.kb-np.kb-np-dragging { transition: none; }

.kb-np-bg { position: absolute; inset: -60px; }

.kb-np-bg img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    filter: blur(50px) saturate(1.4) brightness(.5);
    transform: scale(1.15);
}

.kb-np-bg::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(rgba(14, 15, 18, .1), rgba(14, 15, 18, .55) 70%, rgba(14, 15, 18, .85));
}

.kb-np-inner {
    position: relative;
    height: 100%;
    display: flex;
    flex-direction: column;
    padding-bottom: var(--safe-bottom);
}

.kb-np-grab {
    flex: none;
    height: 34px;
    padding-top: calc(28px + var(--safe-top));
    display: grid;
    place-items: center;
    touch-action: none;
}

.kb-np-grab i { width: 40px; height: 5px; border-radius: 3px; background: rgba(232, 228, 220, .35); }

.kb-np-head { display: flex; align-items: center; flex: none; padding: 10px 8px 0; }

.kb-np-icon {
    width: 44px;
    height: 44px;
    display: grid;
    place-items: center;
    border: none;
    background: none;
    color: var(--sand-100);
    font-size: 22px;
    line-height: 1;
    cursor: pointer;
}

.kb-np-from {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}

.kb-np-from span {
    font: 500 10px var(--mono);
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--sand-300);
}

.kb-np-from b {
    max-width: 100%;
    font-size: 14px;
    font-weight: 500;
    color: var(--sand-100);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* The stage holds the carousel and, on a phone, the Up next and Credits lists
   that replace it. Swapping in place rather than opening a second overlay
   keeps one thing to dismiss and one way to dismiss it. */
.kb-np-stage {
    flex: 1;
    min-height: 0;
    position: relative;
    margin-top: 18px;
    /* Clips the neighbours to a peek. On a phone the viewport already does it;
       inside the desktop column nothing would, and a whole second cover at full
       size reads as two covers rather than as what comes next. */
    overflow: hidden;
}

.kb-np-carousel {
    position: absolute;
    inset: 0;
    /* pan-y so a vertical scroll still belongs to the page; without an explicit
       touch-action Chrome on Android cancels the gesture mid-swipe. */
    touch-action: pan-y;
    cursor: grab;
}

.kb-np-slides {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    display: flex;
    align-items: center;
    gap: 20px;
    transition: transform .32s var(--ease);
    will-change: transform;
}

/* Suppresses the transition while the window is re-centred after a commit. */
.kb-np-slides.kb-np-nofx { transition: none; }

.kb-np-slide {
    /* Measured at bind time, never assumed: the prototype hard-coded a stride
       for a 390px screen, which misplaces every slide on a 320px one. */
    width: min(326px, calc(100vw - 64px));
    aspect-ratio: 1;
    flex: none;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 30px 60px rgba(0, 0, 0, .45);
    transition: transform .32s var(--ease), opacity .32s;
}

.kb-np-slide img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }

.kb-np-slide-empty { box-shadow: none; }

.kb-np-side { transform: scale(.9); opacity: .45; }

.kb-np-list {
    position: absolute;
    inset: 0;
    overflow-y: auto;
    padding: 0 24px;
    display: none;
    flex-direction: column;
}

.kb-np[data-np-tab="cover"] .kb-np-list { display: none; }
.kb-np[data-np-tab="cover"] .kb-np-carousel { display: block; }
.kb-np[data-np-tab="queue"] .kb-np-list,
.kb-np[data-np-tab="credits"] .kb-np-list { display: flex; }
.kb-np[data-np-tab="queue"] .kb-np-carousel,
.kb-np[data-np-tab="credits"] .kb-np-carousel { display: none; }

.kb-np-title {
    flex: none;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding: 18px 32px 0;
}

.kb-np-title b {
    font: 30px/1.08 var(--serif);
    font-weight: 400;
    color: var(--sand-100);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kb-np-artist {
    align-self: flex-start;
    min-height: 32px;
    padding: 0;
    border: none;
    background: none;
    color: var(--sand-300);
    font: 16px var(--sans);
    cursor: pointer;
}

.kb-np-artist:hover { color: var(--gold-400); }

.kb-np-seek { flex: none; padding: 14px 32px 0; }

.kb-np-bar {
    position: relative;
    height: 24px;
    display: flex;
    align-items: center;
    cursor: pointer;
    /* The scrub owns the gesture outright. */
    touch-action: none;
}

.kb-np-rail {
    position: relative;
    width: 100%;
    height: 4px;
    border-radius: 2px;
    background: rgba(232, 228, 220, .2);
}

.kb-np-fill {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    border-radius: 2px;
    background: var(--sand-100);
    width: 0;
}

.kb-np-knob {
    position: absolute;
    top: 50%;
    left: 0;
    width: 14px;
    height: 14px;
    margin: -7px 0 0 -7px;
    border-radius: 7px;
    background: var(--sand-100);
    transform: scale(0);
    transition: transform .12s;
}

.kb-np-bar.kb-scrubbing .kb-np-knob,
.kb-np-bar:hover .kb-np-knob { transform: scale(1); }

.kb-np-bar.kb-scrubbing .kb-np-fill,
.kb-np-bar:hover .kb-np-fill { background: var(--gold-500); }

.kb-np-times {
    display: flex;
    justify-content: space-between;
    margin-top: 2px;
    font: 12px var(--mono);
    color: var(--sand-300);
}

.kb-np-controls {
    flex: none;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 10px 24px 0;
}

.kb-np-ctl,
.kb-np-step {
    display: grid;
    place-items: center;
    border: none;
    background: none;
    color: var(--sand-100);
    cursor: pointer;
}

.kb-np-ctl { width: 44px; height: 44px; color: #d4cfc6; }
.kb-np-step { width: 56px; height: 56px; }

.kb-np-ctl.shuffle-active,
.kb-np-ctl.repeat-active { color: var(--gold-500); }

.kb-np-pp {
    width: 76px;
    height: 76px;
    border-radius: 38px;
    border: none;
    background: var(--gold-500);
    color: var(--ink-900);
    display: grid;
    place-items: center;
    cursor: pointer;
}

.kb-np-pp svg { fill: currentColor; grid-area: 1 / 1; }

.kb-np-tabs {
    flex: none;
    display: flex;
    justify-content: center;
    gap: 6px;
    margin-top: auto;
    padding: 16px 0 30px;
}

.kb-np-tabs button {
    height: 44px;
    padding: 0 16px;
    border-radius: 22px;
    border: none;
    background: rgba(232, 228, 220, .08);
    color: var(--sand-100);
    font: 500 14px var(--sans);
    cursor: pointer;
}

.kb-np-tabs button[aria-pressed="true"] { background: var(--gold-500); color: var(--ink-900); }

.kb-np :focus-visible {
    outline: 2px solid var(--gold-500);
    outline-offset: 2px;
    border-radius: 8px;
}

/* Desktop: the same sheet in a 420px column, which is the whole difference. */
@media (min-width: 641px) {
    .kb-np-inner { width: 420px; margin: 0 auto; }
    .kb-np-grab { display: none; }
    .kb-np-head { padding-top: 28px; }
}

@media (prefers-reduced-motion: reduce) {
    .kb-np,
    .kb-np-slides,
    .kb-np-slide,
    #panel { transition: none; }
}

/* ==========================================================================
   Search. A filter over the catalogue already in memory — the field, the
   results, and the warm-glass genre cards that stand in for an empty query.
   ========================================================================== */

.kb-search-head {
    /* Sticky inside #main, which is the scrolling container: on a phone the
       software keyboard covers most of the viewport and the field has to stay
       reachable while the results scroll under it. */
    position: sticky;
    top: 0;
    z-index: 2;
    padding: 40px 40px 12px;
    background: var(--ink-950);
}

.kb-search-box {
    display: flex;
    align-items: center;
    gap: 10px;
    max-width: 560px;
    height: 48px;
    margin-top: 16px;
    padding: 0 8px 0 14px;
    border-radius: 12px;
    background: var(--ink-850);
    border: 1px solid var(--ink-700);
    color: var(--sand-300);
    cursor: text;
    transition: border-color .12s;
}

.kb-search-box:focus-within { border-color: var(--gold-500); }

.kb-search-box input {
    flex: 1;
    min-width: 0;
    height: 44px;
    background: none;
    border: 0;
    outline: 0;
    color: var(--sand-100);
    /* 16px, and it must stay 16px at every width: below that, focusing an
       input makes iOS Safari zoom the whole layout in and never back out —
       and #app is a fixed-row grid, so the zoomed viewport clips the tab bar. */
    font: 16px var(--sans);
}

.kb-search-box input::placeholder { color: var(--sand-500); }

/* The native affordance is replaced by .kb-search-clear, so that the input
   event is the only signal this page needs. */
.kb-search-box input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    display: none;
}

.kb-search-clear {
    width: 36px;
    height: 36px;
    flex: none;
    display: grid;
    place-items: center;
    border: none;
    background: none;
    color: var(--sand-500);
    cursor: pointer;
}

.kb-search-clear:hover { color: var(--sand-100); }

.kb-search-hint {
    max-width: 560px;
    margin-top: 10px;
    font-size: 13px;
    color: var(--sand-300);
}

#search-results { padding: 8px 40px 40px; }

.kb-search-tracks { padding: 8px 0 0; }

.kb-search-empty {
    padding: 40px 0 6px;
    font-size: 16px;
    color: var(--sand-100);
}

.kb-search-empty-sub {
    padding-bottom: 40px;
    font-size: 14px;
    color: var(--sand-300);
}

/* --- artist and album result rows -------------------------------------- */

.kb-result {
    display: flex;
    align-items: center;
    gap: 14px;
    width: 100%;
    min-height: 64px;
    padding: 8px 0;
    border: none;
    background: none;
    text-align: left;
    color: inherit;
    font: inherit;
    cursor: pointer;
    border-radius: 8px;
}

.kb-result:hover { background: rgba(232, 228, 220, .045); }

.kb-result-art {
    width: 48px;
    height: 48px;
    flex: none;
    border-radius: 4px;
    object-fit: cover;
}

.kb-result-art.round { border-radius: 24px; }

.kb-result-txt {
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.kb-result-txt b {
    font-size: 15px;
    font-weight: 500;
    color: var(--sand-100);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.kb-result-txt i {
    font-style: normal;
    font-size: 13px;
    color: var(--sand-300);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* --- genre cards: warm glass ------------------------------------------- */

.kb-genres {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: 12px;
    margin-top: 12px;
}

.kb-genre {
    position: relative;
    height: 120px;
    padding: 12px 16px;
    display: flex;
    align-items: flex-end;
    text-align: left;
    border-radius: 14px;
    overflow: hidden;
    isolation: isolate;
    cursor: pointer;
    /* The plain gradient FIRST, as the fallback: color-mix appears nowhere else
       in this stylesheet, and a browser that does not know it drops the whole
       declaration below — leaving a card with no background rather than a
       warm one. Both are warm either way; the pair comes from --g1/--g2, which
       search.js picks from a fixed palette of brand tokens, so a cool hue is
       not reachable. */
    background: linear-gradient(135deg, var(--g1), var(--g2));
    background:
        radial-gradient(90% 120% at 100% 0%, color-mix(in srgb, var(--g1) 75%, transparent), transparent 70%),
        linear-gradient(135deg,
            color-mix(in srgb, var(--g1) 58%, var(--ink-900)),
            color-mix(in srgb, var(--g2) 62%, var(--ink-900)));
    border: 1px solid rgba(255, 240, 210, .22);
    box-shadow:
        inset 0 1px 0 rgba(255, 246, 228, .30),
        inset 0 -18px 30px rgba(14, 15, 18, .22),
        0 10px 26px rgba(0, 0, 0, .28);
}

/* The shine across the top half. */
.kb-genre::before {
    content: "";
    position: absolute;
    inset: 0 0 50% 0;
    z-index: -1;
    background: linear-gradient(rgba(255, 248, 232, .16), rgba(255, 248, 232, 0));
    pointer-events: none;
}

.kb-genre-mark {
    position: absolute;
    right: -14px;
    top: -16px;
    width: 96px;
    height: 96px;
    opacity: .18;
    z-index: -1;
    transform: rotate(-12deg);
    pointer-events: none;
}

.kb-genre-name {
    font: 21px/1.05 var(--serif);
    color: #fffaf0;
    text-shadow: 0 1px 10px rgba(14, 15, 18, .35);
}

.kb-genre:hover { border-color: rgba(232, 228, 220, .45); }

@media (max-width: 640px) {
    .kb-search-head { padding: 24px 16px 10px; }
    #search-results { padding: 8px 16px 24px; }
    .kb-genres { grid-template-columns: repeat(2, minmax(0, 1fr)); }
    .kb-genre { height: 96px; }
    .kb-genre-name { font-size: 18px; }
    /* Never below 16px — see the input rule above. */
    .kb-search-box input { font-size: 16px; }
}

/* ==========================================================================
   You, Settings, Plans and deleting an account.
   ========================================================================== */

.kb-you-hero {
    max-width: 560px;
    margin: 0 40px 28px;
    padding: 24px;
    border-radius: 14px;
    background: var(--ink-850);
    border: 1px solid var(--ink-700);
}

.kb-you-hero h2 {
    margin: 0 0 10px;
    font: 400 26px/1.15 var(--serif);
    color: var(--sand-100);
}

.kb-you-hero p {
    margin: 0;
    font-size: 14px;
    line-height: 1.55;
    /* sand-300, not sand-500: the brand guide calls sand-500 "not for text you
       must read" at 3.3:1, and this is the paragraph that explains the product. */
    color: var(--sand-300);
}

.kb-you-actions {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
    margin-top: 18px;
}

/* Scoped rather than !important: .kb-you-hero p already sets these, and
   app.css's contract is that specificity and source order decide. */
.kb-you-hero .kb-you-note {
    margin-top: 16px;
    font-size: 13px;
    color: var(--sand-300);
}

.kb-rows {
    max-width: 560px;
    margin: 0 40px 24px;
    border-radius: 12px;
    overflow: hidden;
    border: 1px solid var(--ink-700);
}

.kb-row {
    display: flex;
    align-items: center;
    gap: 12px;
    width: 100%;
    min-height: 60px;
    padding: 10px 16px;
    border: none;
    border-bottom: 1px solid var(--ink-800);
    background: var(--ink-900);
    text-align: left;
    color: inherit;
    font: inherit;
    text-decoration: none;
    cursor: pointer;
}

.kb-row:last-child { border-bottom: none; }
.kb-row:hover { background: var(--ink-850); }

.kb-row-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }

.kb-row-txt b {
    font-size: 15px;
    font-weight: 500;
    color: var(--sand-100);
}

.kb-row-txt i {
    font-style: normal;
    font-size: 13px;
    color: var(--sand-300);
}

.kb-row-go { flex: none; color: var(--sand-500); }

.kb-rows-danger .kb-row-txt b { color: var(--red-400); }

.kb-you-facts {
    max-width: 560px;
    margin: 0 40px 24px;
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.kb-you-facts p {
    margin: 0;
    font-size: 14px;
    line-height: 1.55;
    color: var(--sand-300);
}

.kb-you-facts b { color: var(--sand-100); font-weight: 500; }
.kb-you-facts a, .kb-you-hero a { color: var(--gold-400); }

.kb-settings-note { margin: 0 40px 24px; }

/* --- Plans ------------------------------------------------------------- */

.kb-plan {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
    gap: 16px;
    max-width: 880px;
    margin: 0 40px 40px;
}

.kb-plan-card {
    padding: 24px;
    border-radius: 14px;
    background: var(--ink-850);
    border: 1px solid var(--ink-700);
}

.kb-plan-next { border-color: var(--gold-700); }

.kb-plan-card h3 {
    margin: 8px 0 12px;
    font: 400 22px/1.2 var(--serif);
    color: var(--sand-100);
}

.kb-plan-card p {
    margin: 0 0 12px;
    font-size: 14px;
    line-height: 1.6;
    color: var(--sand-300);
}

.kb-plan-card p:last-child { margin-bottom: 0; }
.kb-plan-card em { color: var(--sand-100); font-style: normal; font-weight: 500; }

.kb-plan-card .kb-plan-note {
    padding-top: 12px;
    border-top: 1px solid var(--ink-700);
    color: var(--sand-300);
}

/* --- deleting an account ------------------------------------------------ */

.kb-delete-facts {
    max-width: 560px;
    margin: 0 40px 20px;
}

.kb-delete-blockers {
    max-width: 560px;
    margin: 0 40px 20px;
    padding: 14px 16px;
    border-radius: 12px;
    font-size: 14px;
    line-height: 1.55;
    color: var(--sand-100);
    background: rgba(232, 144, 94, .10);
    border: 1px solid var(--clay-400);
}

.kb-delete-blockers:empty { display: none; }

.kb-delete-form { max-width: 560px; margin: 0 40px 40px; }

/* Red is for destructive actions, and this is the only one in the player. */
.kb-btn-danger {
    height: 40px;
    padding: 0 20px;
    border-radius: 20px;
    border: none;
    background: var(--red-400);
    color: var(--ink-950);
    font: 600 14px var(--sans);
    cursor: pointer;
}

.kb-btn-danger:hover { background: #f4818a; }
.kb-btn-danger:disabled { opacity: .5; cursor: default; }

.kb-link {
    border: none;
    background: none;
    padding: 0;
    color: var(--gold-400);
    font: inherit;
    text-decoration: underline;
    cursor: pointer;
}

/* --- the auth modal's two steps ---------------------------------------- */

.kb-invite-pill {
    display: inline-flex;
    align-items: center;
    height: 28px;
    margin-bottom: 14px;
    padding: 0 12px;
    border-radius: 14px;
    background: rgba(201, 168, 76, .14);
    border: 1px solid var(--gold-700);
    color: var(--gold-400);
    font: 500 12px var(--sans);
}

.kb-pw-rule {
    margin-top: 8px;
    font: 12px var(--sans);
    color: var(--sand-300);
}

.kb-pw-rule.ok { color: var(--green-400); }

.kb-auth-help {
    margin-top: 14px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--sand-300);
}

.kb-auth-help a { color: var(--gold-400); }

#auth-email[readonly] {
    color: var(--sand-300);
    cursor: default;
}

@media (max-width: 640px) {
    .kb-you-hero,
    .kb-rows,
    .kb-you-facts,
    .kb-plan,
    .kb-delete-facts,
    .kb-delete-blockers,
    .kb-delete-form,
    .kb-settings-note { margin-left: 16px; margin-right: 16px; }
}

/* ==========================================================================
   The payee screens: Upload, Your Audience, and the listener's My Listening
   which shares their rows.

   These are the last pages to come onto the brand, and they had no prototype:
   the design rounds covered the listener core, the account screens and the
   content pages, and the artist side is Stage 4. So nothing here is invented.
   The page frame, the rows, the notes and the primary button are the ones Home,
   Album and You already use.
   ========================================================================== */

/* --- the page frame, shared by every body-rendered page ------------------ */

/* #royalties-body, #audience-body and #upload-body all carried
   style="padding:0 40px 40px" in the markup, which no stylesheet could reach. */
.kb-page-body {
    padding: 0 40px 40px;
}

/* The uppercase label over a section. Three copies of this lived in uploads.js
   as an inline style on .page-subtitle, overriding the real .page-subtitle
   rule outright — and not even identically: one used a 8px bottom margin and
   the other two 4px. */
/* The section label. .kb-eyebrow itself is the Artist page's, from slice 5b —
   gold, mono, uppercase — and the Upload page reuses it rather than declaring a
   second one. The first version of this block did declare a second one, 1500
   lines below the original, and quietly turned the Artist page's label sand.
   The kb- prefix cannot catch that; TestPlayerCSS_KbClassesAreDeclaredOnce can. */
.kb-eyebrow + * { margin-top: 0; }

/* The explanatory paragraph under a list. This was .royalty-note, which the
   Upload page borrowed for four unrelated notes; naming it after one page was
   how a My Listening change would have restyled the upload form. */
.kb-note {
    max-width: 560px;
    margin-top: 24px;
    font-size: 13px;
    line-height: 1.65;
    color: var(--sand-300);
}

.kb-note strong { color: var(--sand-100); font-weight: 500; }
.kb-note + .kb-note { margin-top: 12px; }

/* --- empty and loading states -------------------------------------------- */

/* .empty-state was a 300px flex box that every caller had to override with an
   inline height, because 300px is right for a page and wrong for a spinner. */
.kb-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 56px 16px;
    text-align: center;
    color: var(--sand-300);
    font-size: 14px;
    line-height: 1.6;
}

.kb-empty .kb-note { margin-top: 0; text-align: left; }
.kb-empty.kb-empty-load { padding: 40px 16px; }

.spinner {
    width: 18px;
    height: 18px;
    border: 2px solid var(--ink-700);
    border-top-color: var(--gold-500);
    border-radius: 50%;
    animation: spin .7s linear infinite;
}

/* --- shares and plays: My Listening and Your Audience -------------------- */

/* One set of rows for both pages. They answer different questions — where your
   listening went, and who is listening to you — with the same three columns. */
/* auto, not fixed widths. A grid shares its column widths across every row, so
   auto still lines the numbers up down the page — and it sizes to the longest
   of them instead of to a guess. The guess was 92px, and "3 listeners" wrapped
   onto two lines on a phone. */
.kb-share {
    display: grid;
    grid-template-columns: 1fr auto auto;
    gap: 16px;
    align-items: baseline;
    padding: 14px 0;
    border-bottom: 1px solid var(--ink-800);
}

.kb-share:last-child { border-bottom: none; }

.kb-share-name {
    font-size: 15px;
    color: var(--sand-100);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* An artist whose catalogue row has been removed. The listen still counts, so
   the row stays and says so rather than disappearing. */
.kb-share-name.unknown {
    color: var(--sand-300);
    font-style: italic;
}

.kb-share-pct {
    font-family: var(--mono);
    font-size: 15px;
    color: var(--gold-400);
    text-align: right;
    white-space: nowrap;
}

.kb-share-plays {
    font-family: var(--mono);
    font-size: 13px;
    color: var(--sand-300);
    text-align: right;
    white-space: nowrap;
}

/* Your Audience adds a second line under the row and, sometimes, a warning. */
.kb-share-block {
    padding: 18px 0;
    border-bottom: 1px solid var(--ink-800);
}

.kb-share-block .kb-share { padding: 0; border: none; }

.kb-share-sub {
    margin-top: 6px;
    font-family: var(--mono);
    font-size: 12px;
    color: var(--sand-300);
}

.kb-share-warn {
    margin-top: 8px;
    padding: 8px 12px;
    border-radius: 8px;
    border-left: 2px solid var(--red-400);
    background: rgba(240, 103, 110, .07);
    font-size: 13px;
    line-height: 1.55;
    color: var(--sand-100);
}

.kb-share-warn.kb-share-soon {
    border-left-color: var(--gold-500);
    background: rgba(201, 168, 76, .07);
    color: var(--sand-300);
}

@media (max-width: 640px) {
    .kb-page-body { padding: 0 16px 32px; }
    .kb-share { gap: 10px; }
    .kb-share-name { font-size: 14px; }
    .kb-share-pct { font-size: 14px; }
}

/* --- the Upload page ----------------------------------------------------- */

/* The form and the list of what you have uploaded, on one page, because a
   payee's whole job here is "add a track, then watch what happened to it". */

.kb-form {
    max-width: 560px;
    margin-bottom: 36px;
}

/* Used on the Upload page, not in a modal. It was .modal-error and
   .modal-actions, borrowed from the dialogs — which meant restyling a dialog
   restyled this form, silently and in a place nobody would think to look. */
.kb-form-error {
    min-height: 18px;
    margin-bottom: 10px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--red-400);
}

.kb-form-actions {
    display: flex;
    align-items: center;
    gap: 12px;
    margin-top: 4px;
}

.kb-note-lead { margin: 0 0 14px; }
.kb-eyebrow-list { margin-top: 32px; }

/* The progress bar for the upload in flight. The fill's width is written from
   JS as a percentage, so nothing here may set it. */
.kb-upload-bar {
    height: 4px;
    margin: 4px 0 8px;
    border-radius: 2px;
    background: var(--ink-700);
    overflow: hidden;
}

.kb-upload-fill {
    height: 100%;
    background: var(--gold-500);
    transition: width .2s linear;
}

.kb-upload-pct {
    min-height: 18px;
    margin-bottom: 12px;
    font-family: var(--mono);
    font-size: 12px;
    color: var(--sand-300);
}

/* One row per upload. The status sits in its own column and keeps its width,
   so a list of rows reads down the right-hand edge as a column of states
   rather than as ragged text. */
.kb-upload {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 16px;
    align-items: start;
    padding: 16px 0;
    border-bottom: 1px solid var(--ink-800);
}

.kb-upload:last-child { border-bottom: none; }

.kb-upload-title {
    font-size: 15px;
    color: var(--sand-100);
}

.kb-upload-meta {
    margin-top: 3px;
    font-family: var(--mono);
    font-size: 12px;
    color: var(--sand-300);
}

.kb-upload-note {
    max-width: 460px;
    margin-top: 8px;
    font-size: 13px;
    line-height: 1.55;
    color: var(--sand-300);
}

.kb-upload .btn-outline {
    display: inline-flex;
    margin: 10px 8px 0 0;
    padding: 5px 12px;
    font-size: 12px;
}

/* The state, in the same mono the rest of the player uses for anything a
   machine decided. Sand by default: most of these states are "wait". */
.kb-upload-status {
    justify-self: end;
    white-space: nowrap;
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--sand-300);
}

.kb-upload-status.live { color: var(--gold-400); }
.kb-upload-status.held { color: var(--clay-400); }
.kb-upload-status.failed { color: var(--red-400); }

@media (max-width: 640px) {
    .kb-form { max-width: none; }
    .kb-upload { grid-template-columns: 1fr; gap: 6px; }
    .kb-upload-status { justify-self: start; }
    .kb-upload-note { max-width: none; }
}

/* The artist's name in an album hero: a link in everything but markup, because
   it is a <span> the router listens on. It carried its colour and weight
   inline, where the stylesheet could not reach it. */
.kb-hero-link {
    cursor: pointer;
    font-weight: 500;
    color: var(--sand-100);
}

.kb-hero-link:hover { color: var(--gold-400); }

/* ==========================================================================
   The chrome: dialogs, form fields, the "⋯" menus, the consent banner and the
   sidebar's account block.

   Everything here was the last of the pre-redesign stylesheet. It is shared by
   every screen — the auth dialog, the Terms gate, tag editing, reporting, the
   shortcuts list and both page forms all draw these — which is why it moved
   last and why it moves all at once.
   ========================================================================== */

/* --- dialogs ------------------------------------------------------------- */

/* The z-index ladder is asserted by TestPlayerCSS_StackingOrder: the drawer
   backdrop 999, the sidebar 1000, Now Playing 1001, the "⋯" menu 1002 (it
   opens from inside Now Playing), the consent banner 1003, dialogs 1004, and
   the Terms gate 1005 above them because it is the one nothing may cover. */
.modal-backdrop {
    position: fixed;
    inset: 0;
    z-index: 1004;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(14, 15, 18, .72);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
    opacity: 0;
    pointer-events: none;
    transition: opacity .18s var(--ease);
}

#legal-modal { z-index: 1005; }

.modal-panel {
    width: 100%;
    max-width: 400px;
    max-height: 90dvh;
    overflow-y: auto;
    padding: 28px;
    border: 1px solid var(--glass-line);
    border-radius: 16px;
    background: var(--ink-850);
    box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
    transform: translateY(10px) scale(.99);
    transition: transform .18s var(--ease);
}

/* The tag form carries up to eight fields and the report form four plus a
   checkbox, so neither fits the width a sign-in dialog wants. */
#tag-modal .modal-panel { max-width: 560px; }
#report-modal .modal-panel { max-width: 480px; }

.modal-title {
    margin-bottom: 6px;
    font: 400 26px/1.15 var(--serif);
    color: var(--sand-100);
}

.modal-subtitle {
    margin-bottom: 20px;
    font-size: 13px;
    line-height: 1.55;
    color: var(--sand-300);
}

.modal-error {
    min-height: 18px;
    margin-bottom: 10px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--red-400);
}

.modal-done {
    margin: 4px 0 14px;
    font-size: 14px;
    line-height: 1.6;
    color: var(--green-400);
}

.modal-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 20px;
}

/* Both buttons share the row. .btn-play-all is the shared primary pill and
   .btn-outline the secondary; neither may become the album page's gold circle
   here, which is the bug slice 5b shipped by styling the class unscoped. */
.modal-actions .btn-play-all,
.modal-actions .btn-outline {
    flex: 1;
    justify-content: center;
}

.modal-switch {
    margin-top: 18px;
    font-size: 13px;
    color: var(--sand-300);
    text-align: center;
}

.modal-switch button {
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    color: var(--gold-400);
    cursor: pointer;
}

.modal-switch button:hover { text-decoration: underline; }

/* --- form fields --------------------------------------------------------- */

/* Every form in the player: both dialogs, tag editing, reporting and the two
   page forms on Upload. tags.js reads input.previousElementSibling as the
   label, so the label stays the input's immediate previous sibling. */

.field { margin-bottom: 16px; }

.field label {
    display: block;
    margin-bottom: 6px;
    font-size: 12px;
    color: var(--sand-300);
}

.field input,
.field select,
.field textarea {
    width: 100%;
    padding: 11px 13px;
    border: 1px solid var(--ink-700);
    border-radius: 10px;
    background: var(--ink-800);
    color: var(--sand-100);
    font-family: var(--sans);
    font-size: 14px;
    transition: border-color .15s var(--ease), box-shadow .15s var(--ease);
}

.field textarea {
    min-height: 72px;
    resize: vertical;
    line-height: 1.5;
}

/* A visible ring, not `outline: none`. The pre-redesign rules removed the
   browser's outline on every field, every menu item and the "⋯" button, and
   this file's only replacement was scoped to the sidebar, the player bar and
   the tab bar — so nothing inside a dialog or inside #main had a focus
   indicator at all. Removing an affordance is only allowed with one to put in
   its place. */
.field input:focus,
.field select:focus,
.field textarea:focus {
    outline: none;
    border-color: var(--gold-500);
    box-shadow: 0 0 0 3px rgba(201, 168, 76, .18);
}

.field input[type="file"] {
    padding: 9px 12px;
    color: var(--sand-300);
}

.field input:read-only {
    color: var(--sand-300);
    background: var(--ink-850);
}

.field-row {
    display: grid;
    grid-template-columns: 1fr 110px;
    gap: 12px;
}

.field-hint {
    margin-top: 6px;
    font-size: 12px;
    color: var(--sand-300);
}

.field-check {
    display: flex;
    align-items: flex-start;
    gap: 9px;
    margin: 6px 0 14px;
    font-size: 13px;
    line-height: 1.5;
    color: var(--sand-300);
}

.field-check input {
    margin-top: 2px;
    accent-color: var(--gold-500);
}

.field-check a,
.terms-hint a { color: var(--gold-400); }

.terms-hint {
    margin: 10px 0 0;
    font-size: 12px;
    line-height: 1.55;
    color: var(--sand-300);
}

@media (max-width: 640px) {
    .modal-panel { padding: 22px; border-radius: 14px; }
    .modal-title { font-size: 23px; }
    .field-row { grid-template-columns: 1fr; }
}

/* --- the "⋯" menus and the row's edit control ---------------------------- */

/* One menu element serves every "⋯" button — a track row's, the album's and
   the artist's — and moderation.js positions it by writing left and top, and
   reads style.display === 'block' to know whether it is open. So this rule may
   neither set display nor assume a value for it. */
.more-menu {
    position: fixed;
    z-index: 1002;
    min-width: 176px;
    padding: 6px;
    border: 1px solid var(--glass-line);
    border-radius: 12px;
    background: var(--glass);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    box-shadow: 0 16px 44px rgba(0, 0, 0, .5);
}

.more-menu button {
    display: block;
    width: 100%;
    padding: 9px 12px;
    border: none;
    border-radius: 8px;
    background: none;
    color: var(--sand-100);
    font: inherit;
    font-size: 14px;
    text-align: left;
    cursor: pointer;
}

.more-menu button:hover { background: rgba(232, 228, 220, .07); }

.more-menu button:focus-visible {
    outline: 2px solid var(--gold-500);
    outline-offset: -2px;
}

.more-btn,
.track-edit {
    width: 28px;
    height: 28px;
    padding: 0;
    border: 1px solid transparent;
    border-radius: 100px;
    background: none;
    color: var(--sand-300);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
}

.track-edit {
    width: auto;
    padding: 0 9px;
    font-size: 12px;
}

.more-btn:hover,
.track-edit:hover {
    border-color: var(--ink-700);
    color: var(--sand-100);
}

.more-btn:focus-visible,
.track-edit:focus-visible {
    outline: 2px solid var(--gold-500);
    outline-offset: 1px;
}

/* --- the cover-review notice on an album ---------------------------------- */

.album-notice {
    margin: 0 40px 16px;
    padding: 11px 14px;
    border-radius: 10px;
    border-left: 2px solid var(--river-400);
    background: rgba(111, 183, 201, .07);
    font-size: 13px;
    line-height: 1.55;
    color: var(--sand-100);
}

.album-notice.warn {
    border-left-color: var(--gold-500);
    background: rgba(201, 168, 76, .07);
}

/* --- the consent banner --------------------------------------------------- */

/* Built by static/consent.js, which also serves the landing page and the legal
   pages — those two inline their own copy of this, so only the player's is
   styled here. */
.consent-banner {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 1003;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 16px calc(24px + var(--safe-right)) calc(16px + var(--safe-bottom)) calc(24px + var(--safe-left));
    border-top: 1px solid var(--glass-line);
    background: var(--glass);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    box-shadow: 0 -8px 30px rgba(0, 0, 0, .45);
    font-size: 13px;
    transform: translateY(100%);
    transition: transform .3s var(--ease);
}

.consent-banner.visible { transform: translateY(0); }

.consent-banner p {
    flex: 2;
    min-width: 260px;
    margin: 0;
    line-height: 1.6;
    color: var(--sand-300);
}

.consent-banner p a { color: var(--gold-400); }

.consent-buttons {
    display: flex;
    flex-wrap: wrap;
    gap: 10px;
}

.consent-btn {
    padding: 9px 18px;
    border: 1px solid transparent;
    border-radius: 100px;
    font: 500 13px var(--sans);
    cursor: pointer;
    transition: background .15s var(--ease), border-color .15s var(--ease);
}

.consent-accept {
    background: var(--gold-500);
    color: var(--ink-900);
}

.consent-accept:hover { background: var(--gold-400); }

.consent-decline {
    border-color: var(--ink-700);
    background: none;
    color: var(--sand-100);
}

.consent-decline:hover { border-color: var(--sand-500); }

.consent-btn:focus-visible {
    outline: 2px solid var(--gold-400);
    outline-offset: 2px;
}

@media (max-width: 640px) {
    .consent-banner { flex-direction: column; align-items: stretch; padding: 16px; }
    .consent-buttons { justify-content: stretch; }
    .consent-btn { flex: 1; }
}

/* --- the sidebar's account block and legal links -------------------------- */

#account-block {
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 10px;
    min-width: 0;
    padding: 12px 16px calc(12px + var(--safe-bottom));
    border-top: 1px solid var(--ink-800);
}

#account-email {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    font-family: var(--mono);
    font-size: 11px;
    color: var(--sand-300);
}

.account-link {
    flex-shrink: 0;
    padding: 0;
    border: none;
    background: none;
    font: 500 12px var(--sans);
    color: var(--gold-400);
    cursor: pointer;
}

.account-link:hover { text-decoration: underline; }

#legal-links {
    flex-shrink: 0;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px 10px;
    margin-top: auto;
    padding: 10px 16px 0;
    font-size: 11px;
}

/* sand-300, not sand-500. docs/brand/palette/contrast.py measures sand-500 at
   3.34 and labels it "not for text"; this row is 11px, which is the last place
   to be casual about it. (Fifteen rules from earlier slices still use it for
   text — on shipped pages, so changing those is a decision, not a fix.) */
#legal-links a,
.cookie-settings {
    padding: 0;
    border: none;
    background: none;
    font: inherit;
    color: var(--sand-300);
    text-align: left;
    text-decoration: none;
    cursor: pointer;
}

#legal-links a:hover,
.cookie-settings:hover { color: var(--sand-100); }

/* --- motion ---------------------------------------------------------------- */

/* The existing block covers Now Playing and the panel only. Everything the
   player animates on a route change, a drawer, a dialog or a spinner is here
   too now — a page transition and a sliding drawer are exactly what this
   setting is for. */
@media (prefers-reduced-motion: reduce) {
    .page { animation: none; }

    #sidebar,
    .modal-backdrop,
    .modal-panel,
    .consent-banner,
    .kb-upload-fill {
        transition: none;
    }

    .spinner,
    .playing-bars span {
        animation: none;
    }
}

/* The open state. The closed panel is offset and very slightly scaled down, so
   opening one has to clear BOTH — `transform` is a single property and a value
   replaces it whole, so `none` and `translateY(0)` are equally complete here;
   what would not be is `translateY(0) scale(.99)`, or an offset left in by
   accident. The harness asserts the computed matrix is the identity. */
body.auth-modal-open #auth-modal,
body.legal-modal-open #legal-modal,
body.tag-modal-open #tag-modal,
body.report-modal-open #report-modal {
    opacity: 1;
    pointer-events: auto;
}

body.auth-modal-open #auth-modal .modal-panel,
body.legal-modal-open #legal-modal .modal-panel,
body.tag-modal-open #tag-modal .modal-panel,
body.report-modal-open #report-modal .modal-panel {
    transform: none;
}

/* The row's controls appear on hover on a pointer device. On a touch screen
   there is no hover, so they are always there and dimmed instead — without
   this pair the Edit and "⋯" controls on every track row are unreachable on a
   phone, which is also the only place the payee actually is. */
.track-row .more-btn,
.track-row .track-edit {
    opacity: 0;
    transition: opacity .15s var(--ease);
}

.track-row:hover .more-btn,
.track-row:hover .track-edit,
.track-row .more-btn:focus-visible,
.track-row .track-edit:focus-visible,
.track-row .more-btn[aria-expanded="true"] {
    opacity: 1;
}

@media (hover: none) {
    .track-row .more-btn,
    .track-row .track-edit { opacity: .6; }
}

/* The album's and the artist's "⋯" are page-level controls, not row controls:
   bigger, always visible, and the artist's is pinned to its hero. */
.album-actions .more-btn,
.artist-hero .more-btn {
    width: 36px;
    height: 36px;
    border-color: var(--ink-700);
}

.artist-hero .more-btn {
    position: absolute;
    top: 20px;
    right: 24px;
}

@media (max-width: 640px) {
    .album-notice { margin: -8px 16px 16px; }
    .artist-hero .more-btn { top: 12px; right: 16px; }
}
