/* ============================================================================
   live.css — "Play somewhere live" as the real page
   ============================================================================
   Paired with live.html (generated by .claude/tools/build-live.py). It loads
   AFTER styles.css, which is kept on purpose: the auth/account modals, the search
   dropdown and sheet, the tooltips and the level-reward pop-out are all its
   classes, and re-drawing them here would be a second copy to maintain.

   The page body uses `lv-*` class names, so styles.css's `.rs-*` layout rules
   cannot reach it. What this file has to override is only the shared furniture
   the app's renderers emit: table rows, station units, song pills, the rank board
   and the article cards.

   Nothing here is loaded by index.html. Production is untouched.
   ============================================================================ */

:root {
    --p:  #f2efe6;   /* paper — the page */
    --w:  #fffdf7;   /* raised — rows, cards */
    --s:  #e7e2d6;   /* sunken — active nav, hover */
    --i:  #171714;   /* ink */
    --m:  #5f5c54;   /* metadata grey */
    --r:  #b8b2a5;   /* hairline */
    --pk: #e91e8c;
    --b:  #1747d1;
    --y:  #f2d522;
    --red:#e13b2d;
    --mono: 'JetBrains Mono', ui-monospace, monospace;

    /* The player bar's height, in ONE place: the shell's bottom inset has to
       track it exactly or the bar covers the end of the page. */
    --lv-playerh: 88px;
    --lv-tabbarh: 52px;         /* the phone's tab strip, BELOW the player bar */
    --lv-sidew:   160px;        /* the left column; the collapse toggle drives it */
    --lv-railw:   300px;
    --lv-panelh:  244px;        /* the station panel on a phone (258 on desktop) */

    /* --m is the right grey for 12-14px text. The tables set metadata in 10-11px
       uppercase mono, where the same grey stops being readable — small, tracked,
       all-caps type needs more contrast, not the same amount. */
    --m2: #45423b;
}

/* ---------------------------------------------------------------- shell ---- */

html, body.lv-body {
    height: 100%;
    background: var(--p);
    color: var(--i);
    font-family: Inter, system-ui, sans-serif;
    overflow: hidden;              /* the columns scroll, the page does not */
}

.lv-seo {
    position: absolute; left: -10000px;
    width: 1px; height: 1px; overflow: hidden;
}

/* The app shell is a full-height grid: three columns above, the player bar below. */
.lv {
    position: fixed;
    inset: 0 0 var(--lv-playerh) 0;
    display: grid;
    grid-template-columns: var(--lv-sidew) minmax(0, 1fr) var(--lv-railw);
    background: var(--p);
}

/* ---------------------------------------------------------------- left ----- */

.lv-side {
    border-right: 1px solid var(--i);
    padding: 24px 0;
    display: flex;
    flex-direction: column;
    min-height: 0;
    overflow-y: auto;
}

.lv-brand {
    padding: 0 24px 25px;
    /* A 160px column cannot hold "Radio Shuffle" on one line beside the
       toggle, at any size that still reads as a wordmark. So it stacks on
       purpose, with the leading closed up — the same tight-set treatment the
       station name and the section headings use — rather than being shrunk
       until it fits and looking like a wrap that got away. */
    font-size: 19px; line-height: 19px; font-weight: 800; letter-spacing: -.05em;
    color: var(--i); text-decoration: none;
}
.lv-brand b { color: var(--pk); }

.lv-nav { display: flex; flex-direction: column; }

.lv-navitem {
    /* 16px gutters, not 24: "My Stations" was landing within a few pixels of the
       column's edge once it came in to 160. */
    height: 40px; padding: 0 16px;
    display: flex; align-items: center; gap: 12px;
    border: 0; background: none; cursor: pointer;
    color: var(--m); font: 650 14px Inter, sans-serif; text-align: left;
    border-right: 4px solid transparent;
}
.lv-navitem i { width: 17px; font-size: 14px; }
.lv-navitem:hover { color: var(--i); }
.lv-navitem.active {
    color: var(--i);
    background: var(--s);
    border-right-color: var(--pk);
}

/* .lv-person and .lv-levelcard are gone (v0.0.56) — identity moved to the rail.
   .lv-avatar stays: the rail's profile button still uses it. */
.lv-avatar {
    width: 34px; height: 34px; flex: 0 0 34px; padding: 0;
    border: 1px solid var(--i); border-radius: 50%;
    background: var(--y); cursor: pointer; overflow: hidden;
}
.lv-avatar img { width: 100%; height: 100%; object-fit: contain; display: block; }

.lv-login {
    margin: 14px 24px 0; padding: 9px;
    border: 1px solid var(--i); background: var(--w);
    font: 700 12px Inter, sans-serif; cursor: pointer;
}


/* ---------------------------------------------------------------- centre --- */

.lv-main {
    min-width: 0; min-height: 0;
    padding: 22px 32px 44px;
    overflow-y: auto;
}

.lv-topbar {
    display: flex; justify-content: space-between; align-items: center;
    gap: 20px;
    padding-bottom: 11px; margin-bottom: 20px;
    border-bottom: 1px solid var(--i);
}
.lv-mono { font: 600 11px var(--mono); color: var(--m); letter-spacing: .05em; text-transform: uppercase; }
.lv-mono b { color: var(--i); }

/* The search keeps the app's own markup (search.js binds to it), restyled flat. */
.lv-search { position: relative; width: 420px; max-width: 55%; }
.lv-search .articles-search__field {
    height: 34px; padding: 0 0 0 11px;
    border: 1px solid var(--i); border-radius: 2px;
    background: var(--w);
    display: flex; align-items: center; gap: 8px;
}
.lv-search .articles-search__input {
    background: none; border: 0; color: var(--i);
    font: 400 13px Inter, sans-serif; padding: 0;
}
.lv-search .articles-search__submit {
    width: 34px; height: 100%; border: 0; border-radius: 0;
    background: var(--i); color: #fff; cursor: pointer;
}

/* ---- the station on air ---- */

.lv-now { background: none; border: 0; box-shadow: none; height: auto; padding: 0; }

.lv-hero {
    display: grid;
    /* One column since v0.0.62 — the facts aside that filled the second one is
       gone, its country and tag now above the station name. */
    grid-template-columns: minmax(0, 1fr);
    gap: 24px;
    padding: 0 0 22px;
    border-bottom: 3px solid var(--i);
    background: none;
}
.lv-hero .rs-hero__glow { display: none; }

.lv-nowtext { min-width: 0; display: block; }

/* v0.0.52 — the "● LIVE / country · tag" line was deleted from the markup; the
   facts column beside it already prints Country and Tag. Its rules are kept only
   so a stray .lv-live emitted by another renderer still looks right. */
.lv-live {
    font: 700 11px var(--mono); letter-spacing: .06em;
    color: var(--red); text-transform: uppercase;
    display: flex; align-items: center; gap: 7px;
}
.lv-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--red); }
.lv-live .player-meta { color: var(--red); font: inherit; }

/* Save / Share, in the corner the LIVE line used to occupy. Absolute so the
   station name keeps the full width for its two lines and never wraps early
   around them. */
.lv-nowtext { position: relative; }
.lv-heroacts {
    position: absolute; top: 2px; right: 0;
    display: flex; gap: 16px; z-index: 2;
}
.lv .lv-heroacts button {
    width: auto; height: auto; min-width: 0; min-height: 0;
    padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none;
    color: var(--m); font-size: 16px; line-height: 1; cursor: pointer;
}
.lv .lv-heroacts button:hover { color: var(--i); background: none; }
.lv .lv-heroacts button.is-on { color: var(--pk); }

/* The name is capped short of the column's edge on purpose: left to run the
   full width, a long station name filled the row edge to edge, crowding Save /
   Share and leaving nowhere for the waves to sit. */
.lv-name {
    margin: 0;
    max-width: 84%;
    font-size: 44px; line-height: 46px; font-weight: 800; letter-spacing: -.035em;
}

/* ---- the equalizer (v0.0.55) ----
   Square bars, no radius, ink with two pink beats — the page's own vocabulary
   rather than the old rounded orange/pink gradient set. INLINE inside the h2
   (live.js keeps it there), so it trails the last word and wraps with it
   instead of parking at the right edge of the name's box. */
.lv-eq {
    display: none;
    align-items: flex-end; gap: max(2px, .065em);
    height: max(11px, .55em); margin-left: .3em;
    vertical-align: baseline;
}
.rs-hero.is-playing .lv-eq { display: inline-flex; }
/* Sized in em so it tracks the name when live.js shrinks a long one. */
.lv-eq i {
    width: max(2px, .068em); height: 100%;
    background: var(--i);
    transform-origin: bottom;
    animation: lv-eq 1.05s ease-in-out infinite;
}
.lv-eq i:nth-child(2), .lv-eq i:nth-child(4) { background: var(--pk); }
.lv-eq i:nth-child(1) { animation-delay: 0s;    animation-duration: 1.05s; }
.lv-eq i:nth-child(2) { animation-delay: .17s;  animation-duration: 1.25s; }
.lv-eq i:nth-child(3) { animation-delay: .34s;  animation-duration: .92s;  }
.lv-eq i:nth-child(4) { animation-delay: .11s;  animation-duration: 1.35s; }
.lv-eq i:nth-child(5) { animation-delay: .28s;  animation-duration: 1.1s;  }
@keyframes lv-eq { 0%, 100% { transform: scaleY(.22); } 50% { transform: scaleY(1); } }
@media (prefers-reduced-motion: reduce) {
    .lv-eq i { animation: none; transform: scaleY(.45); }
}

.lv-descwrap { max-height: 96px; overflow: hidden; margin-top: 11px; }
.lv-desc { margin: 0; font-size: 14px; line-height: 21px; color: #3a382f; max-width: 640px; }

/* The rotating tuning line is now the ONLY loading message on the panel (the
   name slot used to print "Loading..." over it), so it is sized to be read. */
.lv-tuning { display: none; }
.rs-hero.is-tuning .lv-tuning {
    display: block !important;
    margin-top: 11px;
    font: 700 17px/24px var(--mono); color: var(--i);
}
.rs-hero.is-tuning .lv-tuning p { margin: 0; }
.rs-hero.is-tuning .lv-name,
.rs-hero.is-tuning .lv-descwrap { opacity: .25; }

/* ---- the three plates (v0.0.53) -------------------------------------------
   [ ▶ Play ]  [ ⤨ Shuffle ]  [ ✦♪ Identify ]

   Smaller than the mockup's pair, because there are three of them and the panel
   should not turn into a toolbar. Play is the black plate, Identify the yellow
   one — the mockup's two colours — and Shuffle sits between them on paper, so
   the eye still lands on the two ends first.

   Play/Stop carries both faces in the markup; `.rs-hero.is-playing` (main.js)
   picks one. `.lv-cta__on--idle` shows while silent, `--live` while playing. */

.lv-cta {
    display: flex; align-items: stretch; flex-wrap: wrap; gap: 9px;
    margin-top: 18px;
    border: 0; background: none; padding: 0;
}

.lv .lv-cta__btn {
    height: 38px; min-height: 0; min-width: 0;
    padding: 0 15px;
    display: inline-flex; align-items: center; justify-content: center; gap: 7px;
    border: 2px solid var(--i); border-radius: 2px;
    background: var(--w); color: var(--i);
    font: 750 13px/1 Inter, system-ui, sans-serif;
    letter-spacing: -.005em;
    cursor: pointer; box-shadow: none;
    transition: transform .08s ease;
}
.lv .lv-cta__btn i { font-size: 13px; }
.lv .lv-cta__btn:active { transform: translateY(1px); }
.lv .lv-cta__btn:hover { background: var(--s); color: var(--i); }
.lv .lv-cta__btn:disabled { opacity: .45; cursor: not-allowed; }

/* Play / Stop — the black plate. */
.lv .lv-cta__play { background: var(--i); color: #fff; min-width: 104px; }
.lv .lv-cta__play:hover { background: #000; color: #fff; }
.lv-cta__on--live { display: none; }
.rs-hero.is-playing .lv-cta__on--idle { display: none; }
.rs-hero.is-playing .lv-cta__on--live { display: inline-block; }

/* Identify — the yellow plate, the mockup's colour. */
.lv .lv-cta__id { background: var(--y); }
.lv .lv-cta__id:hover { background: #ffe83b; }
.lv .lv-cta__id .lv-note,
.lv .lv-cta__id .rs-identifybtn__note {
    font-size: 8px; margin-left: -11px; vertical-align: super;
}

/* ---- each plate spins for its own action (v0.0.54) ----
   The wheel replaces the plate's glyph and the label stays put, so the button
   does not change width mid-press and the row never reflows. `is-busy` comes
   from live.js: Play and Shuffle from the hero's tuning state, Identify from
   songidentifier's own flag — the plate that was pressed is the one that spins. */

.lv .lv-cta__btn.is-busy i,
.lv .lv-cta__btn .rs-shuffle-spinner,
.lv .lv-cta__btn .song-identifier-spinner { display: none; }

.lv .lv-cta__btn.is-busy::before {
    content: ""; flex: 0 0 13px; width: 13px; height: 13px;
    border: 2px solid rgba(23, 23, 20, .25); border-top-color: var(--i);
    border-radius: 50%;
    animation: lv-spin .7s linear infinite;
}
/* On the black plate the ring has to be light, or it disappears into it. */
.lv .lv-cta__play.is-busy::before {
    border-color: rgba(255, 255, 255, .3); border-top-color: #fff;
}
/* Identify disables its real button while it listens; the plate mirrors that,
   and the dimming would take the wheel down with it. */
.lv .lv-cta__btn.is-busy:disabled { opacity: 1; }

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

/* ---- country code · tag, above the name (v0.0.62) ----
   What the facts column used to say, on one line where the eye already is. */
.lv-meta {
    display: flex; align-items: center; gap: 9px;
    min-height: 16px; margin-bottom: 7px;
    font: 700 11px var(--mono); letter-spacing: .06em;
}
.lv-meta__cc { color: var(--i); }
.lv-meta__tag { color: var(--m2); text-transform: lowercase; letter-spacing: .02em; }
.lv-meta__cc + .lv-meta__tag::before {
    content: "\00b7"; margin-right: 9px; color: var(--r);
}

/* ---- the lists ---- */

.lv-lower { margin-top: 26px; }
.lv-two { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; align-items: start; }
.lv-block { min-width: 0; }

.lv-h {
    margin: 0 0 13px;
    padding-top: 9px;
    border-top: 3px solid var(--i);
    font-size: 25px; font-weight: 800; letter-spacing: -.02em;
}

.tab-content { display: none; }
.tab-content.active { display: block; }

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

.lv-rail {
    border-left: 1px solid var(--i);
    padding: 22px 22px 44px;
    background: var(--s);            /* #e7e2d6 — the rail is its own surface */
    min-height: 0; overflow-y: auto;
}
.lv-railblock { border-top: 3px solid var(--i); padding-top: 9px; margin-bottom: 26px; }
/* The first block carries no rule: nothing sits above it to be divided from,
   and a 3px black bar across the top of the column read as a lid. */
.lv-railblock--top { border-top: 0; padding-top: 0; }

/* ---- you, at the head of the rail (v0.0.56) ----
   Was two things in the left panel — an identity row and a level card. The card
   said "#N of M · top X%" and carried a meter; the standings underneath say the
   same thing with more detail, so only the identity survived the move. */
.lv-you {
    display: flex; align-items: center; gap: 10px;
    position: relative;
    margin-bottom: 20px;
}
.lv-you__name {
    flex: 1; min-width: 0;
    font-size: 14px; font-weight: 700; color: var(--i);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lv-you__more {
    flex: 0 0 auto; padding: 4px;
    border: 0; background: none; color: var(--m);
    font-size: 11px; cursor: pointer;
}
.lv-you__more:hover { color: var(--i); }
.lv-h3 { margin: 0; font-size: 18px; font-weight: 800; letter-spacing: -.02em; }
.lv-hint { margin: 7px 0 12px; font-size: 12px; line-height: 17px; color: var(--m); }

.lv-all {
    display: inline-block; margin-top: 4px;
    font: 700 11px var(--mono); color: var(--i); text-decoration: none;
    border-bottom: 1px solid var(--i);
}

/* ============================================================================
   SHARED FURNITURE — markup the app's own renderers emit
   ============================================================================ */

/* ---- station rows (trending / saved / history / search) ---- */

.discovery-table { background: none; border: 0; }

.lv .table-row,
.lv .table-body > [role="listitem"] {
    background: var(--w);
    border: 0; border-bottom: 1px solid var(--r);
    border-radius: 0; box-shadow: none;
    padding: 7px 12px; min-height: 66px;
    display: flex; align-items: center; gap: 10px;
    cursor: pointer;
}
.lv .table-row:first-child { border-top: 1px solid var(--i); }
.lv .table-row:hover { background: var(--s); }
.lv .table-row.active { background: var(--s); box-shadow: inset 3px 0 0 var(--pk); }

.lv .station-unit { display: flex; align-items: center; gap: 11px; min-width: 0; flex: 1; }

/* The ident tile: a hard square, initials + country code in mono, flat brand
   colour. live.js writes its contents — see there for why JS is needed. */
.lv .station-square {
    width: 46px; height: 46px; flex: 0 0 46px;
    border-radius: 2px !important;
    background-image: none !important;
    border: 1px solid transparent;
    display: grid;
    grid-template-columns: 1fr auto;
    grid-template-rows: 1fr auto;
    padding: 5px;
    font: 700 16px/1 var(--mono);
    overflow: hidden;
}
.lv .station-square .ident-l { grid-area: 1/1; align-self: start; letter-spacing: -.02em; }
.lv .station-square .ident-cc { grid-area: 2/2; align-self: end; font-size: 8px; opacity: .82; letter-spacing: .06em; }

.lv .station-unit__meta { min-width: 0; flex: 1; }
.lv .station-unit__name { display: flex; align-items: center; gap: 7px; min-width: 0; }
.lv .station-unit__name-text {
    font-size: 15px; font-weight: 700; color: var(--i);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lv .su-article { color: var(--m); font-size: 12px; }
.lv .su-article:hover { color: var(--pk); }
.lv .station-unit__tags,
.lv .station-unit__sub {
    color: var(--m); font: 600 11px var(--mono); letter-spacing: .02em;
    display: flex; align-items: center; gap: 6px; margin-top: 2px;
}
.lv .su-tags { text-transform: lowercase; }

/* Heat keeps its score and loses its flame. */
.lv .row-heat {
    background: none; border: 1px solid var(--r); border-radius: 2px;
    color: var(--m); font: 700 12px var(--mono); padding: 3px 9px;
}
.lv .row-heat.heat-hot { border-color: var(--pk); color: var(--pk); }
.lv .fa-fire, .lv .bi-fire, .lv .fas.fa-fire { display: none; }

.lv .leaderboard-heart, .lv .saved-heart, .lv .history-heart {
    background: none; border: 0; color: var(--r); font-size: 15px; cursor: pointer;
}
.lv .leaderboard-heart.saved, .lv .saved-heart.saved, .lv .history-heart.saved { color: var(--pk); }

.lv .table-footer {
    background: none; border: 0; border-top: 1px solid var(--i);
    padding: 9px 2px; display: flex; justify-content: space-between; align-items: center;
    color: var(--m); font: 600 10px var(--mono); letter-spacing: .05em; text-transform: uppercase;
}
.lv .refresh-footer-btn { background: none; border: 0; color: var(--m); cursor: pointer; }

.lv .loading-placeholder,
.lv .empty-state {
    background: none; border: 1px solid var(--r); border-radius: 0;
    padding: 26px 18px; text-align: center; color: var(--m); font-size: 13px;
}
.lv .empty-state__icon { display: none; }
.lv .empty-state__title { margin: 0 0 6px; color: var(--i); font-size: 15px; }
.lv .empty-state__cta {
    margin-top: 12px; padding: 9px 15px;
    border: 0; border-radius: 2px; background: var(--i); color: #fff;
    font: 700 12px Inter, sans-serif; cursor: pointer;
}

/* ---- song rows ---- */

.lv .song-pill {
    background: var(--p); border: 1px solid var(--r); border-radius: 2px;
    padding: 5px 8px; margin-top: 4px;
    display: flex; align-items: center; gap: 9px; min-width: 0;
}
.lv .song-pill__title { font-size: 13px; font-weight: 700; color: var(--i); }
.lv .song-pill__artist { font-size: 12px; color: var(--m); }
.lv .song-from { color: var(--m); font: 600 10px var(--mono); margin-top: 3px; }
.lv .song-user { color: var(--pk); }
.lv .track-links a {
    border: 1px solid var(--r); border-radius: 2px; color: var(--m);
    background: var(--w);
}

/* ---- the rank board (levels.js) ---- */

.lv .rank-board { list-style: none; margin: 10px 0 0; padding: 0; }
/* styles.css draws your own row as a pink plate — tinted background, accent
   border, 8px radius. Only the top border was being overridden here, so the
   other three sides kept the accent colour and the row read as a pink box
   around the name. The mockup's board is plainer than that: the row you are on
   is the same hairline-separated line as the rest, set in bold. Border and
   background are reset outright rather than patched side by side. */
.lv .rankrow {
    display: grid; grid-template-columns: 26px 19px 1fr auto;
    gap: 0 7px; align-items: center;
    height: 30px; font-size: 12px;
    padding: 0; background: none;
    border: 0; border-top: 1px solid var(--r); border-radius: 0;
}
.lv .rankrow__pos { font: 600 10px var(--mono); color: var(--m); }
.lv .rankrow__badge { width: 19px; height: 19px; object-fit: contain; }
.lv .rankrow__name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--i); }
.lv .rankrow__level { font: 600 10px var(--mono); color: var(--m); }
.lv .rankrow--you { font-weight: 800; background: none; border-color: transparent; border-top-color: var(--r); }
.lv .rankrow--you .rankrow__pos,
.lv .rankrow--you .rankrow__name,
.lv .rankrow--you .rankrow__level { color: var(--i); }

/* ---- article cards, narrowed into the rail ---- */

.lv-articles { display: block; }
.lv .rs-article-card {
    display: block; margin-bottom: 14px;
    /* The rail is its own surface now (v0.0.56); a white plate on top of it read
       as a second panel. `none` rather than var(--s) so it follows the rail if
       that colour ever moves. */
    background: none; border: 1px solid var(--i); border-radius: 0;
    box-shadow: none; color: var(--i); text-decoration: none;
    overflow: hidden;
}
.lv .rs-article-card__media { height: 96px; }
.lv .rs-article-card__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
.lv .rs-article-card__body { padding: 9px 10px 11px; }
.lv .rs-article-card__title {
    font-size: 13px; line-height: 17px; font-weight: 700; color: var(--i);
    display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.lv .rs-article-card__station { color: var(--pk); font: 600 10px var(--mono); }
.lv .rs-article-card__meta,
.lv .rs-article-card__date,
.lv .rs-article-card__tag { color: var(--m); font: 600 10px var(--mono); }
.lv .rs-article-card__listen {
    margin-top: 7px; padding: 6px 10px;
    border: 1px solid var(--i); background: var(--w); color: var(--i);
    font: 700 10px var(--mono); cursor: pointer;
}
/* Only the first three earn rail space; the rest are one click away at /radio/. */
.lv .rs-article-card:nth-child(n+4) { display: none; }

/* ============================================================================
   THE PLAYER BAR
   ============================================================================ */

.lv-player {
    position: fixed; left: 0; right: 0; bottom: 0; height: var(--lv-playerh);
    background: var(--w);
    border-top: 4px solid var(--pk);
    display: grid; grid-template-columns: 340px minmax(0, 1fr) 340px;
    align-items: center; gap: 16px;
    padding: 0 22px;
    z-index: 40;
}
.lv-pstation { display: flex; align-items: center; gap: 11px; min-width: 0; }
.lv-pident {
    width: 46px; height: 46px; flex: 0 0 46px;
    border: 1px solid var(--r); background: var(--s);
    display: grid; grid-template-columns: 1fr auto; grid-template-rows: 1fr auto;
    padding: 5px; font: 700 16px/1 var(--mono); overflow: hidden;
}
.lv-pident .ident-l { grid-area: 1/1; align-self: start; }
.lv-pident .ident-cc { grid-area: 2/2; align-self: end; font-size: 8px; opacity: .82; }
.lv-plabel { min-width: 0; background: var(--p); border: 1px solid var(--r); padding: 5px 9px; flex: 1; }
.lv-pname { font-size: 13px; font-weight: 700; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.lv-pmeta { color: var(--m); font: 600 10px var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.lv-ptransport { display: flex; align-items: center; justify-content: center; gap: 14px; }
.lv-pshuffle {
    height: 42px; flex: none; padding: 0 22px;
    border: 2px solid var(--i); border-radius: 2px;
    background: var(--pk); box-shadow: 3px 3px var(--i);
    color: var(--i); font: 700 12px var(--mono); letter-spacing: .1em; text-transform: uppercase;
    display: inline-flex; align-items: center; gap: 9px; cursor: pointer;
}
.lv-picon { background: none; border: 0; color: var(--m); font-size: 15px; cursor: pointer; }

.lv-pvol { display: flex; align-items: center; gap: 9px; justify-content: flex-end; }
.lv-pvol .volume-mute-btn { background: none; border: 0; color: var(--m); font-size: 14px; cursor: pointer; }
.lv-pvol .volume-slider { accent-color: var(--i); width: 110px; }

/* ============================================================================
   OUT-SPECIFYING styles.css
   ============================================================================
   These all fix the same class of problem: a shared class this page reuses (so
   the app's JS keeps working) drags its index.html layout along with it. Each
   rule below is scoped under `.lv` purely to outrank the original.
   ============================================================================ */

/* .player-card is a fixed-height column in index.html (680px, pinned controls —
   see v0.0.33). Here it is just a block of text, and its height was pushing the
   two lists a full screen below the fold. */
.lv .lv-now,
.lv .lv-hero {
    display: block;
    height: auto;
    min-height: 0;
    max-height: none;
    padding: 0;
    background: none;
    border: 0;
    box-shadow: none;
    overflow: visible;
}
.lv .lv-hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 24px;
    padding-bottom: 22px;
    border-bottom: 3px solid var(--i);
}
.lv .lv-nowtext { flex: none; display: block; min-height: 0; }
.lv .lv-descwrap { flex: none; max-height: 96px; }

/* The nav items are .tab-toggle-btn, so they arrive dressed as pill tabs. */
.lv .lv-navitem,
.lv .lv-navitem.active {
    border-radius: 0;
    box-shadow: none;
    text-transform: none;
    letter-spacing: 0;
    font-family: Inter, system-ui, sans-serif;
    font-size: 14px;
    min-width: 0;
    margin: 0;
}
.lv .lv-navitem { background: none; }
.lv .lv-navitem.active { background: var(--s); border-right: 4px solid var(--pk); }
.lv .lv-navitem i { color: inherit; opacity: 1; }
.lv .lv-navitem.active i { color: var(--pk); }

/* .user-profile carries the app's dark header treatment; the rail's row is flat. */
.lv .lv-you { background: none; border: 0; border-radius: 0; padding: 0; width: auto; text-align: left; }
.lv .rs-user__name { color: var(--i); }

/* The rail is 300px wide; the articles grid is a multi-column card grid. */
.lv .articles-grid { display: block; grid-template-columns: none; gap: 0; }
.lv .articles-section, .lv .rs-articles { background: none; border: 0; padding: 0; margin: 0; }


/* Tooltips and the search dropdown paint above everything, including the bar. */
.lv .rs-dd, .rs-tooltip { z-index: 60; }

/* A description cut mid-sentence with no visible scrollbar reads as a bug, not
   as an affordance. Tall enough for the long ones, still bounded so the lists
   below never get pushed off screen. */
.lv .lv-descwrap { max-height: 140px; }

/* levels.js empties #rankBoard for a visitor who is not on the board yet, which
   left the rail's standings block blank. The invitation belongs here — it is the
   one state where we can say exactly what earns a place. */
.lv-board:empty::after {
    content: "Listen once and you enter the standings.";
    display: block;
    padding: 12px 0 2px;
    font: 600 11px/16px var(--mono);
    color: var(--m);
}
.lv-board:empty + .lv-hint { display: none; }

/* main.js puts .bounce-animation on Shuffle to draw the eye, and its @keyframes
   carry a pink box-shadow. A running animation beats a normal declaration at any
   specificity, so the glow cannot be turned off by declaring it away — the class
   has to point at a keyframe that only scales. */
@keyframes lv-pulse { 0%, 100% { transform: scale(1); } 70% { transform: scale(1.09); } }
.lv .bounce-animation,
.lv-player .bounce-animation { animation: lv-pulse 2s infinite; box-shadow: none; }
.lv-player .lv-pshuffle.bounce-animation { box-shadow: 3px 3px var(--i); }



/* ============================================================================
   CLOSING THE GAP WITH THE MOCKUP
   ============================================================================ */

/* ---- the station row: tile | name / country · genre | count | round play --- */

.lv .lv-rowaud {
    flex: 0 0 auto; margin-left: auto; padding-left: 10px;
    text-align: right;
}
.lv .lv-rowaud .station-unit__sub {
    margin: 0; justify-content: flex-end;
    font: 600 12px var(--mono); color: var(--m); white-space: nowrap;
}
.lv .lv-rowaud .su-when { color: var(--m); }

.lv .lv-rowplay {
    flex: 0 0 34px; width: 34px; height: 34px; margin-left: 12px;
    border: 1px solid var(--i); border-radius: 50%;
    display: grid; place-items: center;
    font-size: 11px; color: var(--i);
    transition: background .12s, color .12s;
}
.lv .table-row:hover .lv-rowplay { background: var(--i); color: var(--w); }

/* The mockup's row carries neither a heat pill nor a heart: it is a name, a
   count and a way to play. Saving lives on the station you are listening to. */
.lv .row-heat, .lv .leaderboard-heart, .lv .saved-heart, .lv .history-heart { display: none; }

/* ---- song cards ---- */
/* The mockup's card is a bordered box with a notched top-right corner. The app
   emits a station unit with a track pill inside it, so the CARD is the row and
   the pill is restyled to sit in it. */

.lv #communityTableBody .table-row,
.lv #mySongsTableBody .table-row {
    display: block;
    background: var(--w);
    border: 1px solid var(--i);
    border-bottom: 1px solid var(--i);
    margin-bottom: 13px;
    padding: 13px 15px;
    min-height: 0;
    clip-path: polygon(0 0, calc(100% - 17px) 0, 100% 17px, 100% 100%, 0 100%);
}
.lv #communityTableBody .table-row:first-child,
.lv #mySongsTableBody .table-row:first-child { border-top: 1px solid var(--i); }
.lv #communityTableBody .table-row:hover,
.lv #mySongsTableBody .table-row:hover { background: var(--s); }

/* the station tile is not part of the mockup's song card — the station is named
   in the "HEARD ON" line instead */
.lv #communityTableBody .station-square,
.lv #mySongsTableBody .station-square { display: none; }

.lv #communityTableBody .station-unit,
.lv #mySongsTableBody .station-unit { display: block; }

/* the track becomes the headline */
.lv #communityTableBody .song-pill,
.lv #mySongsTableBody .song-pill {
    display: block; background: none; border: 0; padding: 0; margin: 0 0 9px;
}
.lv #communityTableBody .song-pill__title,
.lv #mySongsTableBody .song-pill__title {
    display: block; font-size: 17px; line-height: 21px; font-weight: 800;
    letter-spacing: -.01em; white-space: normal;
}
.lv #communityTableBody .song-pill__artist,
.lv #mySongsTableBody .song-pill__artist {
    display: block; font-size: 13px; color: var(--m); margin-top: 1px;
}

/* "HEARD ON <station>" */
.lv #communityTableBody .station-unit__name,
.lv #mySongsTableBody .station-unit__name { display: flex; align-items: baseline; gap: 8px; }
.lv #communityTableBody .station-unit__name::before,
.lv #mySongsTableBody .station-unit__name::before {
    content: "Heard on";
    font: 700 9px var(--mono); letter-spacing: .1em; text-transform: uppercase;
    color: var(--m); flex: 0 0 auto;
}
.lv #communityTableBody .station-unit__name-text,
.lv #mySongsTableBody .station-unit__name-text { font-size: 14px; font-weight: 700; }

/* the footer rule, and the streaming pills the mockup draws as bordered chips */
.lv #communityTableBody .song-from,
.lv #mySongsTableBody .song-from {
    border-top: 1px solid var(--r); margin-top: 10px; padding-top: 8px;
    font: 600 10px var(--mono); color: var(--m);
}
.lv .track-links { display: flex; gap: 7px; margin-top: 8px; }
.lv .track-links a {
    width: auto; height: auto; padding: 3px 7px;
    border: 1px solid var(--b); border-radius: 2px; background: var(--w);
    color: var(--b); font: 700 9px var(--mono); letter-spacing: .06em;
    display: inline-flex; align-items: center; gap: 4px;
}
.lv .track-links a i { font-size: 10px; }

/* ---- the rail's Read block: one card, then plain links ---- */

.lv .rs-article-card:nth-child(n+2) { border: 0; border-top: 1px solid var(--r); margin: 0; }
.lv .rs-article-card:nth-child(n+2) .rs-article-card__media,
.lv .rs-article-card:nth-child(n+2) .rs-article-card__listen,
.lv .rs-article-card:nth-child(n+2) .rs-article-card__date { display: none; }
.lv .rs-article-card:nth-child(n+2) .rs-article-card__body { padding: 10px 0; }
.lv .rs-article-card:nth-child(n+4) { display: none; }
.lv .rs-article-card__listen { display: none; }

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

.lv-player { grid-template-columns: 330px minmax(0, 1fr) 330px; }
.lv-pmid { display: flex; flex-direction: column; align-items: center; gap: 3px; }
.lv-ptransport { display: flex; align-items: center; gap: 22px; }

.lv-pplay {
    width: 40px; height: 40px; flex: 0 0 40px; padding: 0;
    border: 0; border-radius: 50%;
    background: var(--i); color: var(--w);
    font-size: 14px; cursor: pointer;
    display: grid; place-items: center;
}
.lv-player .lv-picon {
    width: auto; height: auto; min-width: 0; padding: 0;
    border: 0; border-radius: 0; background: none; box-shadow: none;
    color: var(--m); font-size: 15px; cursor: pointer;
}
.lv-player .lv-picon:hover { color: var(--i); }
.lv-player .lv-picon .rs-btn-primary__label { display: none; }
.lv-note { font-size: 8px; margin-left: -2px; vertical-align: super; }
/* .lv-pcue removed in v0.0.60 — "Top tier" under the transport. */

.lv-pactions { display: flex; align-items: center; justify-content: flex-end; gap: 17px; }
.lv-player .lv-ptext {
    border: 0; background: none; padding: 0;
    color: var(--i); font: 700 12px Inter, sans-serif; cursor: pointer;
    display: inline-flex; align-items: center; gap: 6px;
}
.lv-player .lv-ptext[aria-pressed="true"] { color: var(--pk); }

/* ---- the panel's proxy glyphs ----
   v0.0.52: the five-glyph row is gone. The only bare glyphs left in the panel are
   Save and Share in the top-right corner, and they are styled with `.lv-heroacts`
   further up. These rules therefore have to keep their hands OFF `.lv-cta__btn`,
   or they would strip the two plates back down to grey text — they sit later in
   the file and would otherwise win on order. */

.lv .lv-proxy:not(.lv-cta__btn) {
    border: 0; background: none; padding: 0; line-height: 1;
    color: var(--m); font-size: 15px; cursor: pointer;
}
.lv .lv-proxy:not(.lv-cta__btn):hover { color: var(--i); }
.lv .lv-proxy:not(.lv-cta__btn).is-on { color: var(--pk); }
.lv .lv-proxy:not(.lv-cta__btn) .lv-note { font-size: 8px; margin-left: -2px; vertical-align: super; }

/* ---- the facts column: a rule under each fact, as in the mockup ---- */


/* ---- song cards, part two: the mockup's reading order ---------------------
   The renderer emits station → track → credit. The mockup reads track → station
   → credit, with the streaming links pinned bottom-right. The DOM is left alone
   and re-ordered with flex `order`; the links are the one part that cannot be
   (they sit inside the pill), so they are positioned against the card instead. */

.lv #communityTableBody .table-row,
.lv #mySongsTableBody .table-row { position: relative; padding-bottom: 40px; }

.lv #communityTableBody .station-unit__meta,
.lv #mySongsTableBody .station-unit__meta { display: flex; flex-direction: column; }

.lv #communityTableBody .song-pill,          .lv #mySongsTableBody .song-pill        { order: 1; }
.lv #communityTableBody .station-unit__name, .lv #mySongsTableBody .station-unit__name { order: 2; margin-top: 4px; }
.lv #communityTableBody .station-unit__sub,  .lv #mySongsTableBody .station-unit__sub  { order: 3; }

/* the pill's leading note glyph is not in the mockup's card */
.lv #communityTableBody .song-pill > i.fa-music,
.lv #mySongsTableBody .song-pill > i.fa-music { display: none; }
.lv .song-pill__text { display: block; min-width: 0; }

.lv #communityTableBody .track-links,
.lv #mySongsTableBody .track-links {
    position: absolute; right: 15px; bottom: 11px; margin: 0;
}

/* "from X" → "Found by X", the mockup's wording. Presentational only: the
   username itself is still the element the renderer wrote. */
.lv .song-from__lead { font-size: 0; }
.lv .song-from__lead::after {
    content: "Found by"; font: 600 10px var(--mono); letter-spacing: .04em;
}

/* ---- the bar's Save keeps its state, loses its pill ---- */
.lv-player .lv-ptext.rs-savebtn,
.lv-player .lv-ptext.rs-savebtn.saved {
    border: 0; background: none; box-shadow: none; border-radius: 0;
    padding: 0; width: auto; height: auto; min-width: 0;
    font: 700 12px Inter, sans-serif;
}
.lv-player .lv-ptext.rs-savebtn.saved,
.lv-player .lv-ptext.rs-savebtn[aria-pressed="true"] { color: var(--pk); }

/* ---- the rail's secondary article links ---- */
.lv .rs-article-card:nth-child(n+2) .rs-article-card__tag,
.lv .rs-article-card:nth-child(n+2) .rs-article-card__station { display: none; }
.lv .rs-article-card:nth-child(n+2) .rs-article-card__title { font-size: 12px; line-height: 16px; }
.lv .rs-article-card__meta { margin-top: 4px; }


/* The credit rule spans the card, not just the words: the border belongs on the
   sub-line container, not on the inline <span> the renderer puts inside it. */
.lv #communityTableBody .song-from,
.lv #mySongsTableBody .song-from { border-top: 0; margin-top: 0; padding-top: 0; }
.lv #communityTableBody .station-unit__sub,
.lv #mySongsTableBody .station-unit__sub {
    display: flex; align-items: center; gap: 7px;
    border-top: 1px solid var(--r); margin-top: 10px; padding-top: 8px;
    font: 600 10px var(--mono); color: var(--m);
}

/* The renderer emits icon-only service links; the mockup labels them. The label
   is added in CSS because the anchor's href and target are the renderer's and
   must not be touched. */
.lv .track-links .link-btn::after {
    font: 700 9px var(--mono); letter-spacing: .06em; margin-left: 4px;
}
.lv .track-links .link-btn.apple::after   { content: "APPLE"; }
.lv .track-links .link-btn.spotify::after { content: "SPOTIFY"; }
.lv .track-links .link-btn.deezer::after  { content: "DEEZER"; }
.lv .track-links .link-btn.apple   { border-color: var(--i); color: var(--i); }
.lv .track-links .link-btn.spotify { border-color: #1a7f3c; color: #1a7f3c; }
.lv .track-links .link-btn.deezer  { border-color: var(--b); color: var(--b); }

/* ============================================================================
   NARROW — must come LAST, and match the overrides above selector for selector
   ============================================================================
   These rules were originally written before the override section and were
   silently dead: `.lv .lv-hero` (a class more) beats `.lv-hero` inside a media
   query no matter which is declared later. Media queries add no specificity.
   Every selector here therefore carries the same `.lv` prefix as the rules it
   has to undo.
   ============================================================================ */





/* The player bar sits OUTSIDE .lv, so it needs its own reset — the buttons were
   falling through to the browser default (2px outset, #efefef). */
.lv-player button {
    border: 0; background: none; border-radius: 0; padding: 0;
    font-family: Inter, system-ui, sans-serif; color: inherit; cursor: pointer;
}
.lv-player .lv-pplay { background: var(--i); color: var(--w); border-radius: 50%; }
.lv-player .lv-ptext { white-space: nowrap; }

/* The action buttons were shrinking to ~50px and their nowrap labels painted
   over the next one. They size to their content; the volume slider is the only
   thing in the group that may give way. */
.lv-player .lv-pactions > * { flex: 0 0 auto; }
.lv-player .lv-pvol { flex: 0 1 auto; min-width: 0; }
.lv-player .lv-pvol .volume-slider { width: 96px; }

/* ============================================================================
   MOBILE, part two — the bar and the tab labels
   ============================================================================ */


/* ============================================================================
   SIDE-BY-SIDE WITH THE MOCKUP — the remaining differences
   ============================================================================ */

/* The nav items are .tab-toggle-btn, which styles.css centres. The mockup's nav
   is a left-aligned list with the icons in one column. */
.lv .lv-navitem {
    justify-content: flex-start;
    text-align: left;
}

/* .user-profile is the app's stacked, centred header block; the rail's is a row. */
.lv .lv-you { flex-direction: row; align-items: center; gap: 10px; }

/* The mockup's submit is a black button that says what it does. */
.lv .lv-searchgo {
    width: auto; padding: 0 18px; height: 100%;
    background: var(--i); color: var(--w); border: 0; border-radius: 0;
    font: 700 13px Inter, system-ui, sans-serif; cursor: pointer;
}

/* Facts labels are sentence case in the mockup, not mono capitals. */

/* The station line reads "LIVE / <where>". */
.lv .lv-live::after { content: "LIVE /"; order: -1; }
.lv .lv-live .player-meta { order: 1; }

/* (v0.0.52 — the panel's play glyph became a plate; nothing to tint here now.) */

/* ---- rows: bigger tile, bigger name, count then article link then play ---- */
.lv .station-square { width: 54px; height: 54px; flex: 0 0 54px; font-size: 19px; }
.lv .table-row { min-height: 82px; }
.lv .station-unit__name-text { font-size: 16px; }
.lv .lv-rowaud .station-unit__sub { font-size: 13px; }
.lv .table-row > .su-article { flex: 0 0 auto; margin-left: 12px; font-size: 13px; }

/* ---- rail ---- */
/* The rank line is a sentence now, so it reads as body text rather than as a
   metadata chip. Direction is not colour-coded: a red "Down 2 places" on a page
   whose job is to keep you listening is a punishment, not information. */
.lv-railrank {
    margin: 0 0 13px;         /* first thing in the block now — no h3 above it */
    font: 400 12.5px/18px Inter, system-ui, sans-serif;
    color: #3a382f;
}
.lv-railrank--up { font-weight: 600; }
.lv-railrank--none { color: var(--m); }
.lv .rankrow__name { color: var(--i); }
/* the mockup marks "you" with weight, not a pink plate */
.lv .rankrow--you { background: none; border-radius: 0; padding: 0; }

/* ---- Read: title, then "country · genre" — no chip, no station line, no date */
.lv .rs-article-card__tag {
    background: none; border: 0; padding: 0; border-radius: 0;
    color: var(--m); font: 600 10px var(--mono); text-transform: lowercase;
}
.lv .rs-article-card__tag::before { content: "· "; }
.lv .rs-article-card__station,
.lv .rs-article-card__date,
.lv .rs-article-card__sep { display: none; }
.lv .rs-article-card__body {
    display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 5px;
}
.lv .rs-article-card__title { flex: 0 0 100%; order: 1; }
.lv .rs-article-card__meta  { order: 2; margin-top: 5px; }
.lv .rs-article-card__tag   { order: 3; margin-top: 5px; }

/* The red dot leads the line: "● LIVE / <where>", not "LIVE / ● <where>". */
.lv .lv-dot { order: -2; }

/* The rail already states the unranked case in its own line; the placeholder
   inside the empty board repeated it word for word. */
.lv-board:empty::after { content: none; }

/* A leading "·" with nothing before it — the article's country is often empty,
   which left the separator dangling. The gap does the separating instead. */
.lv .rs-article-card__tag::before { content: none; }
.lv .rs-article-card__meta:not(:empty) { margin-right: 5px; }

/* The mockup's small play glyph after the "heard on" station is gone (v0.0.58):
   it was decoration that could not be clicked, and `.song-play` — a real button,
   in the same place — replaced it. Keeping both put two play triangles in a row.
   It only ever showed on SHORT names, too: on a truncated one it fell inside the
   name-text's own `overflow: hidden` and vanished. */



/* ============================================================================
   THE DOUBLE RULE, AND THE SONG CARD FOOTER
   ============================================================================ */

/* Two black rules were stacking under the station: the panel drew a bottom
   border AND each section heading draws its own top rule. In the mockup only
   the headings carry one. */
.lv .lv-hero { border-bottom: 0; padding-bottom: 6px; }

/* live.js moves .track-links into the credit line, so the absolute positioning
   that used to pin them to the card's corner is gone — and with it the reserved
   space underneath. "Found by …" and the services now share one row, wrapping
   as the mockup's do. */
.lv #communityTableBody .table-row,
.lv #mySongsTableBody .table-row { padding-bottom: 13px; }
.lv #communityTableBody .track-links,
.lv #mySongsTableBody .track-links { position: static; margin-left: auto; }
.lv #communityTableBody .station-unit__sub,
.lv #mySongsTableBody .station-unit__sub { flex-wrap: wrap; row-gap: 7px; }
.lv #communityTableBody .song-from,
.lv #mySongsTableBody .song-from { flex: 0 0 auto; }

/* The credit sits left, the services right, on the same line — the row was
   inheriting a right alignment and pushing both to the edge. */
.lv #communityTableBody .station-unit__sub,
.lv #mySongsTableBody .station-unit__sub {
    justify-content: flex-start;
    text-align: left;
}
.lv #communityTableBody .track-links,
.lv #mySongsTableBody .track-links { margin-left: auto; flex: 0 0 auto; }
.lv .track-links a { padding: 3px 6px; }
.lv .track-links .link-btn::after { margin-left: 3px; }

/* ============================================================================
   THE SONG CARD'S CUT CORNERS
   ============================================================================
   The mockup cuts TWO corners — top-right and bottom-left, 14px each — and it
   does NOT use a border, because a border cannot follow a clip-path: the clip
   slices the border off exactly at the corners you want it to trace.

   So the frame is a FILL, not a stroke, exactly as the mockup does it: the outer
   box is ink with 2px of padding, the inner box is paper, and both are clipped
   with the same polygon. `clip-path: inherit` re-resolves the calc() against the
   inner box's own size, which is what makes the two outlines concentric.

   Here the row is the outer box and .station-unit — its only child — is the
   inner one, so this needs no extra markup.
   ============================================================================ */

.lv #communityTableBody .table-row,
.lv #mySongsTableBody .table-row {
    background: var(--i);
    border: 0;
    padding: 2px;
    margin-bottom: 12px;
    clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px,
                       100% 100%, 14px 100%, 0 calc(100% - 14px));
}
.lv #communityTableBody .table-row:first-child,
.lv #mySongsTableBody .table-row:first-child { border-top: 0; }

.lv #communityTableBody .station-unit,
.lv #mySongsTableBody .station-unit {
    background: var(--w);
    padding: 14px;
    clip-path: inherit;
}
.lv #communityTableBody .table-row:hover,
.lv #mySongsTableBody .table-row:hover { background: var(--i); }
.lv #communityTableBody .table-row:hover .station-unit,
.lv #mySongsTableBody .table-row:hover .station-unit { background: var(--s); }

/* the services are one blue family in the mockup, not per-brand colours */
.lv .track-links .link-btn,
.lv .track-links .link-btn.apple,
.lv .track-links .link-btn.spotify,
.lv .track-links .link-btn.deezer {
    border: 1px solid var(--b); color: var(--b); background: var(--w);
    font: 800 9px var(--mono);
}

/* the service glyphs inherit the chip's blue in the mockup, they do not keep
   their brand colours */
.lv .track-links .link-btn i { color: inherit; }

/* ============================================================================
   A PLAYER PANEL THAT DOES NOT MOVE
   ============================================================================
   The panel collapsed while a station loaded — the name and description empty
   out during TUNING and refill at whatever height the next station needs, so the
   two lists below jumped every time. This is the same class of bug the app fixed
   for its own player in v0.0.33, and the same fix: make the block's height
   independent of its content.

   Two variable parts: the description (1 to 6 lines) and the station name (1 or
   2 lines). The description gets a FIXED height and scrolls inside it; the name
   is allowed two lines and reserves them. The panel then has one height, always,
   and the controls never move.
   ============================================================================ */

.lv .lv-nowtext {
    min-height: 258px;
    display: flex; flex-direction: column;
}
/* Keep the name clear of Save / Share in the corner. (Its height is fixed on
   .lv-nowtext further down, not here — see the 258px block.) */
.lv .lv-name { padding-right: 72px; }
.lv .lv-descwrap {
    height: 96px; max-height: 96px; min-height: 96px;
    overflow: hidden; flex: none;
}
.lv .lv-cta { margin-top: auto; }     /* pinned to the bottom of the block */

/* TUNING dims the text rather than removing it, so nothing reflows mid-load. */
.lv .rs-hero.is-tuning .lv-meta,
.lv .rs-hero.is-tuning .lv-name,
.lv .rs-hero.is-tuning .lv-descwrap { opacity: .25; }
.lv .rs-hero.is-tuning .lv-tuning { position: absolute; }
.lv .lv-nowtext { position: relative; }

/* ---- "● LIVE / <where>" on one baseline ---- */
.lv .lv-live {
    align-items: center; line-height: 1; min-height: 16px;
}
/* `*` here would also hit the <i> inside the article link and override Font
   Awesome's font-family, which turned the glyph into a tofu square. Icons are
   excluded. */
.lv .lv-live .player-meta,
.lv .lv-live .player-meta > *:not(a) {
    font: 700 11px/1 var(--mono); letter-spacing: .06em; color: var(--red);
    background: none; border: 0; padding: 0; margin: 0;
    display: inline; vertical-align: middle;
}
.lv .lv-live .player-meta a { color: var(--red); margin-left: 5px; }

/* .pm-sep is an EMPTY span — its dot is drawn in CSS, and flattening the chips
   erased it. Given back explicitly rather than adding a second one. */
.lv .lv-live .pm-sep::before { content: "·"; margin: 0 5px; }

/* ---- the trend flag, now beside the station name ---- */
.lv .station-unit__name .lv-trend {
    flex: 0 0 auto; margin-left: 2px;
    font: 800 9px var(--mono); letter-spacing: .06em;
    padding: 2px 5px; border: 1px solid currentColor; border-radius: 2px;
    color: var(--m); line-height: 1;
}
.lv .lv-trend--up   { color: var(--pk); }
.lv .lv-trend--down { color: var(--m); }
.lv .lv-trend--new  { color: var(--b); }

/* ---- the article link sits with the tags ---- */
.lv .station-unit__tags .su-article { margin-left: 4px; font-size: 12px; }
.lv .table-row > .su-article { display: none; }   /* the old right-hand slot */

/* Reserving two lines for the name left a hole under short ones. The height is
   fixed on the BLOCK instead, and the description absorbs whatever the name does
   not use — same trick the app uses for its own player (v0.0.42). One height,
   no gap, and the controls still never move. */
.lv .lv-nowtext { height: 258px; min-height: 258px; }
.lv .lv-name { min-height: 0; }
.lv .lv-descwrap {
    flex: 1 1 auto; height: auto; min-height: 0; max-height: none;
    /* hidden, never `auto`: a scrollbar inside the panel is a second scrolling
       surface next to the column's own, and the whole text has a window of its
       own now (`.lv-readmodal`). The fade below is the only "there is more". */
    overflow: hidden;
    /* a cut mid-sentence reads as a bug; the fade says "there is more" */
    mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 22px), transparent);
}

/* No separator is added here: buildPlayerMeta already emits its own "·" between
   the chips, and adding one produced "SWITZERLAND · · CHILLOUT". Only the
   spacing needed restoring after the chips were flattened to inline text. */
.lv .lv-live .player-meta > * { margin: 0 2px; }

/* ============================================================================
   MEASURED AGAINST THE MOCKUP
   ============================================================================
   Not adjusted by eye — the same elements were measured in the mockup frame and
   on dev, and these are the values that differed. Eyeballing had missed every
   one of them.

     station row     72px   (was 80)
     ident tile      56px   (was 54)
     section heading 26px/700 (was 25px/800)
     player bar      88px   (was 82)
     station name    40px/700 (was 44px/800)
   ============================================================================ */

.lv .table-row { min-height: 72px; }
.lv .station-square { width: 56px; height: 56px; flex: 0 0 56px; font-size: 19px; }
.lv .lv-h { font-size: 26px; font-weight: 700; }
.lv .lv-h3 { font-weight: 700; }
.lv .lv-name { font-size: 40px; line-height: 42px; font-weight: 700; }

/* The bar's height and the shell's bottom inset are one number: change one and
   the last row of the lists disappears behind the player. */


/* styles.css reaches these rows through `.rs-app .discovery-table .table-row`,
   three classes deep — my `.lv .table-row` was one class short and lost every
   time, which is why the row stayed 82px on a 56px tile. Same depth, declared
   later, so it wins. The mockup's row is 72px with 7px/14px of padding. */
.lv .discovery-table .table-row,
.lv .table-body .table-row {
    min-height: 72px;
    padding: 7px 14px;
}
.lv #communityTableBody .table-row,
.lv #mySongsTableBody .table-row { min-height: 0; padding: 2px; }

/* The rule that actually held the padding is `#leaderboard-tab .table-row` — an
   ID selector. No class-based rule can outrank one, however many classes it
   chains, which is why three earlier attempts here changed nothing. Matched with
   an id of its own rather than reached for !important.
   (Found by asking the CSSOM which rules matched, not by guessing again.) */
.lv #leaderboard-tab .table-row,
.lv #saved-tab .table-row,
.lv #history-tab .table-row {
    padding: 7px 14px;
    min-height: 72px;
}
.lv #leaderboard-tab #communityTableBody .table-row,
.lv #mysongs-tab .table-row {
    padding: 2px;
    min-height: 0;
}

/* ============================================================================
   FROM THE CREW AUDIT
   ============================================================================ */

/* The mockup prints the volume level beside the slider. */
.lv-player .lv-volnum { font: 600 11px var(--mono); color: var(--m); min-width: 22px; }

/* ⚠️ On a phone the rail is hidden, and the rail is where the standings live —
   so the ranking, the whole retention mechanic, was invisible on mobile. The
   mockup's phone keeps it as a "YOU" card in its Community block. The rail's
   standings section is therefore moved into the main column at that width
   instead of being hidden with the rest of the rail. */

/* ============================================================================
   THE TWO LOWER LISTS
   ============================================================================ */

/* The mockup draws the station list as one hard-bordered box —
   `.stationlist{border:1px solid;background:var(--w)}` — not as rows floating on
   the paper. The rows already carry the hairline between them, so the box adds
   only its own edge, and the first row gives up the top border it used to draw
   for itself. Matched with ids because that is what the rows themselves are
   matched with further down this file. */
.lv #leaderboardStations,
.lv #savedStations,
.lv #historyStations {
    background: var(--w);
    border: 1px solid var(--i);
}
.lv #leaderboardStations > .table-row:first-child,
.lv #savedStations > .table-row:first-child,
.lv #historyStations > .table-row:first-child { border-top: 0; }
/* the box draws the bottom edge; the last row must not draw a second one */
.lv #leaderboardStations > .table-row:last-child,
.lv #savedStations > .table-row:last-child,
.lv #historyStations > .table-row:last-child { border-bottom: 0; }

/* The footer's own top rule would land flush against the box's bottom edge and
   read as one 2px line. The box already closes the list, so the caption keeps
   only its air. */
.lv #leaderboardStations ~ .table-footer,
.lv #savedStations ~ .table-footer,
.lv #historyStations ~ .table-footer { border-top: 0; padding-top: 10px; }

/* nothing inside the box draws a second border of its own */
.lv #leaderboardStations > .loading-placeholder,
.lv #savedStations > .loading-placeholder,
.lv #historyStations > .loading-placeholder,
.lv #leaderboardStations > .empty-state,
.lv #savedStations > .empty-state,
.lv #historyStations > .empty-state { border: 0; }

/* Two columns, one baseline. Song cards are variable-height and station rows are
   not, so the two lists can never be made to end together by counting rows: the
   blocks stretch to a shared height instead, and each list pushes its "Load
   more" to the bottom of it. Both buttons — and both footers — then sit on the
   same line whatever the data does. */
.lv .lv-two { align-items: stretch; }
.lv .lv-two > .lv-block { display: flex; flex-direction: column; }
.lv .lv-block > .discovery-table { flex: 1; display: flex; flex-direction: column; }
/* the list keeps its own height — the slack goes between it and the button, so
   neither the bordered box nor the card stack is stretched out of shape */
.lv .lv-block > .discovery-table > * { flex: 0 0 auto; }
.lv .lv-block > .discovery-table > .load-more-container { margin-top: auto; }

/* The mockup's `.loadmore` is a line of text under the list, not a full-width
   pill: `border:0;background:none;color:var(--m);font:600 11px Inter`. */
.lv .load-more-container { padding: 13px 14px 14px; text-align: center; }
.lv .load-more-btn {
    display: inline-block; width: auto; padding: 0;
    background: none; border: 0; border-radius: 0;
    color: var(--m); font: 600 11px Inter, sans-serif; letter-spacing: .05em;
    text-transform: none; cursor: pointer;
}
.lv .load-more-btn:hover:not(:disabled) { background: none; color: var(--i); }
.lv .load-more-btn i { display: none; }

/* ============================================================================
   EVERY MEDIA QUERY, COLLECTED AND LAST
   ============================================================================
   These were scattered through the file and kept being disabled by rules added
   afterwards: a media query adds NO specificity, so a later `.lv .lv-name`
   outranks the same selector inside `@media (max-width: 900px)` regardless of
   the breakpoint. That silently broke the phone layout twice.
   Nothing may be appended after this section. New rules go above it.
   ============================================================================ */

@media (prefers-reduced-motion: reduce) { .lv .bounce-animation,
.lv-player .bounce-animation { animation: none; } }

/* ⚠️ These rules used to sit in two blocks that disagreed: the first dropped
   the grid to TWO columns and hid the rail, the second put the rail back. Three
   displayed children in a two-column grid means the rail is placed in an
   IMPLICIT SECOND ROW — below the shell's fixed height, i.e. underneath the
   player bar, and it drags the bottom of the middle column down with it. That
   is what was hiding "Load more". One block, three columns, rail kept. */
@media (max-width: 1240px) {
    :root { --lv-railw: 260px; }   /* the side is already 160 — nothing to claw back */
    .lv .lv-two { grid-template-columns: minmax(0, 1fr); gap: 22px; }
    .lv-rail { display: block; }
    /* ⚠️ NOT :nth-child — the profile row was added ahead of these sections in
       v0.0.56 and a positional selector silently started hiding the standings
       as well. Named, so adding anything to the rail cannot repeat it. */
    .lv-rail .lv-railblock--read { display: none; }   /* Read stays desktop-only */
}

@media (max-width: 900px) {
    .lv { inset: 0 0 calc(var(--lv-playerh) + var(--lv-tabbarh)) 0;
         grid-template-columns: minmax(0, 1fr); }

    /* The sidebar becomes a tab bar, and it sits ON THE FLOOR with the player
       stacked above it — where a phone's navigation belongs (and where the
       thumb already is). The two are pinned to each other's heights, not to
       one shared inset: `.lv`'s bottom inset is their SUM either way, so the
       order can be swapped here without touching the shell. */
    .lv .lv-side {
        position: fixed; left: 0; right: 0; bottom: 0; top: auto;
        height: var(--lv-tabbarh); padding: 0;
        border-right: 0; border-top: 1px solid var(--i);
        background: var(--w); z-index: 41;
        flex-direction: row; overflow-x: auto; overflow-y: hidden;
    }
    /* The bar is no longer the bottom-most thing on the page — it rides on top
       of the tab strip. (`.lv-player` is `bottom: 0` at every other width.) */
    .lv-player { bottom: var(--lv-tabbarh); }
    /* The side is a horizontal tab strip here, not a column: the brand row and
       its collapse toggle have no place in it. (`.lv-sidetop` would otherwise
       still take a slot with the brand hidden inside it.) */
    .lv .lv-sidetop,
    .lv .lv-brand,
    .lv .lv-login { display: none; }

    .lv .lv-nav { flex-direction: row; flex: 1; }
    .lv .lv-navitem,
    .lv .lv-navitem.active {
        flex: 1; height: 52px; padding: 0 4px;
        flex-direction: column; gap: 3px; justify-content: center;
        border-right: 0; border-bottom: 3px solid transparent;
        font: 700 9px var(--mono); letter-spacing: .02em;
    }
    .lv .lv-navitem.active { background: none; border-bottom-color: var(--pk); }
    .lv .lv-navitem i { width: auto; font-size: 15px; }
    /* styles.css hides a tab button's label on phones (v0.0.19, icon-only strip).
       Here the label IS the nav, so it comes back. */
    .lv .lv-navitem span { display: block; }

    /* Bounded scrolling inside a 92px box cut the description mid-sentence with
       no scrollbar to explain it. The centre column already scrolls, so the text
       is simply allowed to finish. */
    /* styles.css pins a fixed HEIGHT on .station-desc-viewport at this breakpoint
       (v0.0.42), not just a max — so lifting max-height alone let the text spill
       over the icon row instead of growing the box. */
    .lv .lv-descwrap {
        max-height: none; height: auto; min-height: 0;
        flex: none; overflow: visible;
    }

    .lv .lv-main { padding: 16px 14px 24px; }
    .lv .lv-topbar { display: none; }        /* the sheet owns search on a phone */

    .lv .lv-hero {
        grid-template-columns: minmax(0, 1fr);
        gap: 14px; padding-bottom: 16px;
    }
    .lv .lv-name { font-size: 27px; line-height: 30px; }
    .lv .lv-descwrap { max-height: 92px; }
    .lv .lv-facts {
        border-left: 0; border-top: 1px solid var(--r);
        padding: 12px 0 0; display: flex; gap: 24px;
    }
    .lv .lv-facts p { margin: 0; }

    .lv .lv-h { font-size: 20px; }

    .lv-player { grid-template-columns: minmax(0, 1fr) auto; gap: 10px; padding: 0 12px; }
    .lv-player .lv-pvol { display: none; }          /* the OS owns volume on a phone */
    .lv-player { grid-template-columns: minmax(0, 1fr) auto; gap: 10px; padding: 0 12px; }
    .lv-pvol { display: none; }
    .lv-pshuffle { padding: 0 15px; }
}

@media (max-width: 900px) {
    /* Three groups do not fit 390px, and the third was wrapping onto a second
       row and doubling the bar's height. The mockup's phone bar is: tile, name,
       save, play — plus shuffle here, because our tab bar (unlike the mockup's)
       has no Shuffle item and it must stay reachable. */
    .lv-player {
        display: flex; align-items: center; gap: 10px;
        grid-template-columns: none; padding: 0 12px;
    }
    .lv-player .lv-pstation { flex: 1; min-width: 0; }
    .lv-player .lv-pmid { flex: 0 0 auto; flex-direction: row; gap: 14px; }
    .lv-player .lv-pcue,
    .lv-player #whatsPlayingButton,
    .lv-player #shareStationBtn,
    .lv-player #mobileSearchBtn,
    .lv-player .lv-pvol { display: none; }
    /* [tile · name] [star] [shuffle · play] — the mockup's phone bar order. */
    .lv-player .lv-pstation { order: 1; }
    .lv-player .lv-pactions { order: 2; flex: 0 0 auto; gap: 12px; }
    .lv-player .lv-pmid     { order: 3; }
    /* .lv-player .lv-ptext.rs-savebtn sets a font shorthand at (0,3,0), so this
       needs the element selector to outrank it rather than tie. */
    .lv-player .lv-pactions button.lv-ptext { font-size: 0; }
    .lv-player .lv-pactions button.lv-ptext i { font-size: 17px; }

    /* The phone's short forms are GONE, and nothing replaces them: the nav now
       reads Home / Search / Saved / History / Songs in the markup itself, which
       is short enough for a 390px strip. Those five words used to exist twice —
       once as markup for the desktop rail, once as `content:` here for the phone
       — so the two surfaces could (and did) drift apart. One set of labels now,
       at every width. */
}

@media (max-width: 900px) {
    /* The clipping was one level up: styles.css gives .rs-hero__top a fixed
       height with overflow:hidden on phones, so freeing the description alone
       changed nothing — its parent was the box doing the cutting. */
    /* v0.0.62 — the panel is a FIXED box on a phone too, the way it is on a
       desktop: the user asked for a player that does not change size as the
       station does. It was freed here originally so a long description could
       finish, but a panel that grows moves the buttons under the thumb every
       shuffle, which is the worse of the two. The description scrolls inside its
       own box instead, with the same fade that says there is more.
       (This block replaces the earlier `height: auto` freeing — same selectors,
       later in the file.) */
    .lv .lv-nowtext {
        height: var(--lv-panelh); min-height: var(--lv-panelh); max-height: var(--lv-panelh);
        display: flex; flex-direction: column; overflow: hidden;
    }
    .lv .lv-descwrap {
        flex: 1 1 auto; height: auto; min-height: 0; max-height: none;
        overflow: hidden;
        mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
        -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent);
    }
    .lv .lv-cta { margin-top: auto; }
}


@media (max-width: 900px) {
    /* A second grid row put the standings underneath the fixed tab bar. On a
       phone the column and the rail simply scroll together instead. */
    .lv { display: block; overflow-y: auto; }
    .lv-main { overflow: visible; }
    .lv-rail {
        display: block;
        border-left: 0; border-top: 3px solid var(--i);
        /* last block in the phone's flow, so it carries the clearance for the
           tab strip and the bar sitting under it */
        padding: 14px 14px 46px; overflow: visible;
    }
    .lv-rail .lv-railblock { border-top: 0; margin: 0; }
    /* The yellow mono pill here was the mockup's "YOU · name / Master · #384"
       badge. The line is a sentence now and does not fit that shape, so the
       phone reads it the same way the desktop does. */
    .lv-railrank { margin: 9px 0 13px; }
    /* The rail is where the account button lives now. On a phone it was the only
       one on the page — .lv-person used to be hidden here, which left no way to
       reach the account menu at all. */
    .lv .lv-you { margin-bottom: 16px; }
}

/* ---------------------------------------------------------------------------
   v0.0.53 — the three plates on a phone. Equal thirds across the width; at
   390px that is ~113px each, which holds "Identify" beside its glyph. The
   labels are what make the row readable at a glance, so they are the last
   thing that would go.
   --------------------------------------------------------------------------- */
@media (max-width: 900px) {
    .lv .lv-cta {
        display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
        gap: 7px; margin-top: 16px;
    }
    .lv .lv-cta__btn { height: 40px; padding: 0 6px; gap: 5px; font-size: 12px; }
    .lv .lv-cta__btn i { font-size: 12px; }
    .lv .lv-cta__play { min-width: 0; }
    .lv .lv-cta__id .lv-note { margin-left: -9px; }
    .lv .lv-heroacts { top: 0; gap: 18px; }
    .lv .lv-name { padding-right: 58px; max-width: 82%; }
    .lv .rs-hero.is-tuning .lv-tuning { font-size: 15px; line-height: 21px; }
}


/* ============================================================================
   v0.0.57 — legibility in the tables, and a play affordance on the song cards
   ============================================================================ */

/* ---- the row hover was BLACK ----
   `#leaderboard-tab .table-row:hover` in styles.css is an ID selector, so the
   live theme's `.lv .table-row:hover` (three classes) never won it: the row took
   the dark app's `--bg-tertiary` (#383838) and put ink text on a near-black
   plate. Matched with ids of our own — the same fix the row-padding rules above
   already needed, for exactly the same reason. */
.lv #leaderboard-tab .table-row:hover,
.lv #saved-tab .table-row:hover,
.lv #history-tab .table-row:hover,
.lv #mysongs-tab .table-row:hover { background: var(--s); }

/* The song cards are the exception: their frame IS ink, and the inner unit is
   what lifts on hover. Two ids, so this lands after the rule above. */
.lv #leaderboard-tab #communityTableBody .table-row:hover,
.lv #mysongs-tab #mySongsTableBody .table-row:hover { background: var(--i); }

/* styles.css floats a pink play glyph into the row's left edge on hover
   (`.table-row::before`, content \f04b). It belongs to the old design, it lands
   on top of the station tile here, and the circle below is the real affordance. */
.lv .table-row::before { display: none; }

/* ---- metadata that can actually be read ----
   Tags, "Found by", the timestamp and its clock were all 10-11px tracked mono in
   --m or, for the clock, the dark app's --text-muted (#9a9a9a) — which no rule
   in this file had ever overridden. */
.lv .station-unit__tags,
.lv .su-tags { color: var(--m2); font-size: 11.5px; }

.lv .su-when,
.lv .su-when i,
.lv .lv-rowaud .su-when { color: var(--m2); }
.lv .su-when i { opacity: 1; }

.lv #communityTableBody .station-unit__sub,
.lv #mySongsTableBody .station-unit__sub,
.lv #communityTableBody .song-from,
.lv #mySongsTableBody .song-from { color: var(--m2); font-size: 11px; }
.lv .song-from__lead::after { font-size: 11px; color: var(--m2); }
.lv .song-user { color: var(--i); }

/* ---- the song card's one control ----
   Small, circled, sitting right after the station name it plays. The CARD is not
   clickable: it also carries three streaming links and a username, so a card-wide
   handler made every click on it a guess. This button is the whole target, which
   is why it is a real <button> from the renderer rather than a decorative glyph
   dropped in by live.js the way the station rows' circle is. */
/* Scoped to the songs tables: the station rows' name also carries the ▲/▼ trend
   chip, and turning that line into a flex row would re-space it. */
.lv #communityTableBody .station-unit__name,
.lv #mySongsTableBody .station-unit__name {
    display: flex; align-items: center; gap: 8px; min-width: 0;
}
.lv #communityTableBody .station-unit__name-text,
.lv #mySongsTableBody .station-unit__name-text { min-width: 0; flex: 0 1 auto; }
/* ⚠️ `__meta` is a COLUMN flex container carrying `align-items: baseline`, which
   in a column controls the cross axis — the WIDTH. So its children sized to
   their content and overflowed the card instead of filling it: a long station
   name ran past the border and pushed the button out of sight, with the name's
   own ellipsis never triggering because its line had no upper bound to hit.
   (Measured, not guessed: __meta was 364px wide with a 422px scrollWidth.) */
.lv #communityTableBody .station-unit__meta,
.lv #mySongsTableBody .station-unit__meta { min-width: 0; align-items: stretch; }

.lv .song-play {
    flex: 0 0 24px; width: 24px; height: 24px; min-width: 0; min-height: 0;
    padding: 0; margin: 0;
    border: 1px solid var(--i); border-radius: 50%;
    background: none; color: var(--i);
    display: grid; place-items: center;
    font-size: 8px; line-height: 1; cursor: pointer;
    transition: background .12s, color .12s;
}
.lv .song-play:hover { background: var(--i); color: var(--w); }
.lv .song-play i { margin-left: 1px; }   /* optical centring of the triangle */

/* The card stops advertising itself as a tap target — no pointer, and no lift on
   hover either: a whole card that lights up but only responds on one 24px circle
   is a worse promise than a card that stays still. The button carries the hover. */
.lv #communityTableBody .table-row,
.lv #mySongsTableBody .table-row { cursor: default; }
.lv #communityTableBody .table-row:hover .station-unit,
.lv #mySongsTableBody .table-row:hover .station-unit { background: var(--w); }


/* ============================================================================
   v0.0.59 — the tier under your name, and a left panel that folds away
   ============================================================================ */

/* Name over tier, the way the left panel used to carry it. #levelName is
   levels.js's own target; it lost its element when the level card went. */
.lv .lv-you__id { flex: 1; min-width: 0; }
.lv .lv-you__name {
    display: block; font-size: 14px; font-weight: 700; color: var(--i);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lv .lv-you__tier {
    display: block; margin-top: 1px;
    font: 700 10px var(--mono); letter-spacing: .08em; text-transform: uppercase;
    color: var(--m2);
}

/* ---- the left panel folds to an icon rail ----
   Everything here is desktop-only ON PURPOSE: below 900px .lv-side is not a
   column at all, it is the fixed tab strip above the player bar, and a width
   collapse there would be meaningless. Wrapped in min-width rather than left to
   the cascade — `.lv.is-sidenarrow` (0,2,0) would otherwise outrank the phone
   block's `.lv` (0,1,0) whatever the order. */
@media (min-width: 901px) {
    .lv-sidetop {
        display: flex; align-items: flex-start; gap: 4px;
        padding: 0 8px 24px 16px;
    }
    .lv-sidetop .lv-brand { flex: 1; min-width: 0; padding: 0; }
    .lv-sidetoggle {
        flex: 0 0 22px; width: 22px; height: 22px; padding: 0;
        margin-top: -1px;                     /* on the wordmark's first line */
        border: 1px solid var(--r); border-radius: 50%;
        background: none; color: var(--m);
        display: grid; place-items: center;
        font-size: 11px; cursor: pointer;
        transition: background .12s, color .12s, border-color .12s;
    }
    .lv-sidetoggle:hover { border-color: var(--i); color: var(--i); background: var(--s); }

    /* Only the left column changes — the rail keeps whatever width its own
       breakpoint gave it, which restating the whole shorthand would have
       silently undone between 901 and 1240px. */
    .lv.is-sidenarrow { --lv-sidew: 62px; }

    /* "Radio Shuffle" becomes "RS", keeping the pink half. */
    .lv.is-sidenarrow .lv-sidetop {
        flex-direction: column; align-items: center; gap: 12px;
        padding: 0 8px 22px;
    }
    .lv.is-sidenarrow .lv-brand { flex: none; font-size: 0; }
    .lv.is-sidenarrow .lv-brand::before {
        content: "R"; font-size: 19px; font-weight: 800; letter-spacing: -.05em;
        color: var(--i);
    }
    .lv.is-sidenarrow .lv-brand b { font-size: 0; }
    .lv.is-sidenarrow .lv-brand b::after {
        content: "S"; font-size: 19px; font-weight: 800; letter-spacing: -.05em;
        color: var(--pk);
    }
    .lv.is-sidenarrow .lv-sidetoggle { margin: 0; }
    .lv.is-sidenarrow .lv-sidetoggle i { transform: scaleX(-1); }

    /* Icons only, centred, with the active marker kept on the right edge. */
    .lv.is-sidenarrow .lv-navitem {
        padding: 0; justify-content: center; gap: 0;
        border-right-width: 3px;
    }
    .lv.is-sidenarrow .lv-navitem span { display: none; }
    .lv.is-sidenarrow .lv-navitem i { width: auto; font-size: 16px; }

    /* ---- 3. Search leaves the menu ----
       The field at the top of the page is the search on a desktop, and two
       entries for it was one too many. The item STAYS below 900px: there the
       top bar is hidden and this is the only way into the search sheet. */
    #lvSearchNav { display: none; }
}


/* ============================================================================
   v0.0.60 — THE OVERLAYS
   ============================================================================
   Every surface that floats above the page still wore the previous dark app:
   #262626 plates, 16px radii, soft 32px shadows, pink gradient buttons. Against
   a flat paper page they read as a different product opening on top of this one.

   They are restyled here rather than in styles.css because styles.css is shared
   with /radio/ and the FAQ, which have not been redrawn. Scoped to
   `body.lv-body`, which is this page and only this page.

   The grammar, in one place so it stays one grammar:
     · paper (--w) plate, 2px ink border, no radius, hard 8px offset shadow
     · ink scrim, no blur
     · headings Inter 800, tight; metadata JetBrains Mono, uppercase
     · inputs flat with a 1px ink rule; focus turns it pink
     · primary action = the black plate, danger = --red, cancel = plain text
   ============================================================================ */

body.lv-body {
    --ov-shadow: 8px 8px 0 var(--i);
}

/* ---- scrim + plate, shared by both auth modals ---- */
body.lv-body .auth-modal {
    background: rgba(23, 23, 20, .74);
    backdrop-filter: none;
}
body.lv-body .auth-modal-content {
    background: var(--w);
    border: 2px solid var(--i); border-radius: 0;
    box-shadow: var(--ov-shadow);
    padding: 30px 28px 28px;
    color: var(--i);
    max-width: 440px;
}
body.lv-body .auth-modal-content h2 {
    margin: 0 0 6px;
    font: 800 26px/30px Inter, system-ui, sans-serif; letter-spacing: -.03em;
    color: var(--i);
}
body.lv-body .auth-modal-content h3 {
    margin: 0 0 10px;
    font: 800 15px Inter, system-ui, sans-serif; letter-spacing: -.01em;
    color: var(--i);
}
body.lv-body .auth-subtitle,
body.lv-body .form-description { color: var(--m2); font-size: 13px; }

body.lv-body .close-modal {
    top: 12px; right: 12px;
    width: 28px; height: 28px; padding: 0;
    border: 1px solid var(--r); border-radius: 0;
    background: none; color: var(--m);
    font-size: 20px; line-height: 1;
    display: grid; place-items: center;
}
body.lv-body .close-modal:hover { background: var(--i); border-color: var(--i); color: var(--w); }

/* ---- fields ----
   ⚠️ `#emailLoginForm input` and `#accountModal input` in styles.css are ID
   selectors — the class-only version of this rule lost to both and the login
   fields stayed dark-app grey. Matched id for id. */
body.lv-body .auth-modal-content input,
body.lv-body #emailLoginForm input,
body.lv-body #accountModal input {
    height: 42px; padding: 0 12px;
    border: 1px solid var(--i); border-radius: 2px;
    background: var(--w); color: var(--i);
    font: 400 14px Inter, system-ui, sans-serif;
    box-shadow: none;
}
body.lv-body .auth-modal-content input::placeholder,
body.lv-body #emailLoginForm input::placeholder,
body.lv-body #accountModal input::placeholder { color: var(--r); }
body.lv-body .auth-modal-content input:focus,
body.lv-body #emailLoginForm input:focus,
body.lv-body #accountModal input:focus {
    border-color: var(--pk); outline: none; box-shadow: none;
}

/* ---- buttons ---- */
body.lv-body .auth-submit-btn,
body.lv-body .account-action-btn,
body.lv-body .notification-redirect-btn,
body.lv-body .resend-verification-btn,
body.lv-body .social-login-btn {
    height: 44px; padding: 0 18px;
    border: 2px solid var(--i); border-radius: 2px;
    background: var(--i); color: #fff;
    font: 750 14px/1 Inter, system-ui, sans-serif;
    letter-spacing: -.005em;
    display: inline-flex; align-items: center; justify-content: center; gap: 9px;
    box-shadow: none; text-transform: none; cursor: pointer;
}
body.lv-body .auth-submit-btn { width: 100%; }
body.lv-body .auth-submit-btn:hover,
body.lv-body .account-action-btn:hover,
body.lv-body .notification-redirect-btn:hover,
body.lv-body .social-login-btn:hover {
    background: #000; color: #fff; transform: none; box-shadow: none;
}
/* Sign-in with Google is a second choice, not the loud one. */
body.lv-body .social-login-btn,
body.lv-body .account-action-btn,
body.lv-body .resend-verification-btn {
    background: var(--w); color: var(--i);
}
body.lv-body .account-action-btn { width: 100%; }
body.lv-body .auth-submit-btn:disabled,
body.lv-body .account-action-btn:disabled { opacity: .4; }

body.lv-body .account-action-btn.danger,
body.lv-body .danger-btn {
    border-color: var(--red); color: var(--red); background: var(--w);
}
body.lv-body .account-action-btn.danger:hover,
body.lv-body .danger-btn:hover { background: var(--red); color: #fff; }

body.lv-body .back-btn,
body.lv-body .account-edit-btn,
body.lv-body .forgot-password-link,
body.lv-body .auth-switch a {
    background: none; border: 0; padding: 0;
    color: var(--m2); font: 600 12px Inter, system-ui, sans-serif;
    text-decoration: underline; cursor: pointer;
}
body.lv-body .back-btn:hover,
body.lv-body .account-edit-btn:hover,
body.lv-body .forgot-password-link:hover,
body.lv-body .auth-switch a:hover { color: var(--pk); }
body.lv-body .auth-switch { color: var(--m2); font-size: 12px; }

body.lv-body .auth-divider span { background: var(--w); color: var(--m); font: 600 10px var(--mono); letter-spacing: .1em; }
body.lv-body .auth-divider::before,
body.lv-body .auth-divider::after { background: var(--r); }

/* ---- account sections ---- */
body.lv-body .account-section {
    border: 0; border-top: 1px solid var(--r); border-radius: 0;
    background: none; padding: 16px 0 0; margin-top: 16px;
}
body.lv-body .account-row { border-bottom: 1px solid var(--r); padding: 9px 0; }
/* The danger zone's divider was a red rule. The button under it is already red;
   a red hairline as well made the section shout before it had said anything. */
body.lv-body .account-section.danger-zone { border-top-color: var(--r); }
body.lv-body .account-row:last-child { border-bottom: 0; }
body.lv-body .account-label {
    color: var(--m); font: 600 10px var(--mono);
    letter-spacing: .08em; text-transform: uppercase;
}
body.lv-body .account-value { color: var(--i); font-size: 14px; }

body.lv-body .account-stats-grid { gap: 10px; }
body.lv-body .stat-box {
    background: var(--w); border: 1px solid var(--i); border-radius: 0;
    padding: 12px 10px;
}
body.lv-body .stat-value {
    color: var(--i); font: 800 22px Inter, system-ui, sans-serif; letter-spacing: -.02em;
}
body.lv-body .stat-label {
    color: var(--m2); font: 600 9px/13px var(--mono);
    letter-spacing: .06em; text-transform: uppercase;
}

body.lv-body .verification-notice,
body.lv-body .delete-warning,
body.lv-body .account-login-prompt {
    background: var(--s); border: 1px solid var(--i); border-radius: 0;
    color: var(--i);
}
body.lv-body .delete-warning { border-color: var(--red); }
body.lv-body .delete-warning i { color: var(--red); }
body.lv-body .form-feedback { font: 600 12px Inter, system-ui, sans-serif; }
body.lv-body .form-hint { color: var(--m); font: 500 12px/1.4 Inter, system-ui, sans-serif; }

/* ⚠️ The guest prompt's own text was the dark app's #d0d0d0 / #9a9a9a, set by
   `.account-login-prompt p` and `... i` in styles.css. Those are class-level
   rules INSIDE the box, so the block's `color: var(--i)` above never reached
   them: near-white type on a paper plate, which is exactly how it read on
   screen. Restated here in ink. */
body.lv-body .account-login-prompt p { color: var(--i); }
body.lv-body .account-login-prompt i { color: var(--m); }
body.lv-body .account-login-prompt__alt { color: var(--m2); font-size: 12px; }
body.lv-body .account-login-prompt__alt a { color: var(--i); text-decoration: underline; }
body.lv-body .account-login-prompt__alt a:hover { color: var(--pk); }
body.lv-body .auth-error {
    background: var(--w); border: 1px solid var(--red); border-radius: 0;
    color: var(--red); font: 600 12px Inter, system-ui, sans-serif; padding: 9px 11px;
}

/* ---- the identify result ---- */
/* Same plate, and it borrows the song card's clipped corner so the surface that
   announces a track matches the rows that list them. */
body.lv-body .song-notification-overlay {
    background: rgba(23, 23, 20, .74); backdrop-filter: none;
}
body.lv-body .song-notification {
    background: var(--i); border: 0; border-radius: 0;
    padding: 2px; box-shadow: var(--ov-shadow);
    clip-path: polygon(0 0, calc(100% - 18px) 0, 100% 18px,
                       100% 100%, 18px 100%, 0 calc(100% - 18px));
    color: var(--i);
}
/* The white inner face is an absolutely-positioned ::before, so the real content
   has to be lifted above it — but blanket `position: relative` also took the
   close button out of its top-right corner and dropped it into the centred flex
   column. It keeps its own positioning. */
body.lv-body .song-notification > * { position: relative; z-index: 1; }
body.lv-body .song-notification > .notification-close { position: absolute; }
body.lv-body .song-notification::before {
    content: ""; position: absolute; inset: 2px;
    background: var(--w); clip-path: inherit;
}
body.lv-body .notification-icon { color: var(--pk); }
body.lv-body .song-notification-title {
    color: var(--i); font: 800 21px/25px Inter, system-ui, sans-serif; letter-spacing: -.02em;
}
body.lv-body .song-notification-artist { color: var(--m2); font-size: 14px; }
body.lv-body .notification-station { color: var(--m); font: 600 10px var(--mono); }
body.lv-body .notification-links a {
    border: 1px solid var(--b); border-radius: 2px;
    background: var(--w); color: var(--b);
    font: 800 9px var(--mono);
}
body.lv-body .notification-links a:hover { background: var(--b); color: var(--w); }
body.lv-body .notification-links a i { color: inherit; }
body.lv-body .notification-close {
    border: 1px solid var(--r); border-radius: 0; background: none; color: var(--m);
}
body.lv-body .notification-close:hover { background: var(--i); border-color: var(--i); color: var(--w); }

/* ---- the level-up pop-out ---- */
body.lv-body .level-reward {
    background: var(--w); border: 2px solid var(--i); border-radius: 0;
    box-shadow: var(--ov-shadow); color: var(--i);
}
body.lv-body .level-reward__eyebrow {
    color: var(--m); font: 700 9px var(--mono); letter-spacing: .12em; text-transform: uppercase;
}
body.lv-body .level-reward__title {
    color: var(--i); font: 800 17px Inter, system-ui, sans-serif; letter-spacing: -.02em;
}
body.lv-body .level-reward__sub { color: var(--m2); font-size: 12px; }
body.lv-body .level-reward__cta {
    border: 2px solid var(--i); border-radius: 2px;
    background: var(--y); color: var(--i);
    font: 750 13px Inter, system-ui, sans-serif; box-shadow: none;
}

/* ---- the avatar menu ---- */
body.lv-body .rs-usermenu {
    background: var(--w); border: 2px solid var(--i); border-radius: 0;
    box-shadow: var(--ov-shadow); padding: 4px;
}
body.lv-body .rs-usermenu__item {
    border-radius: 0; color: var(--i);
    font: 650 13px Inter, system-ui, sans-serif;
}
body.lv-body .rs-usermenu__item:hover { background: var(--s); color: var(--i); }
body.lv-body .rs-usermenu__item i { color: var(--m); }

/* ---- search: the dropdown and the phone's sheet ---- */
body.lv-body .rs-dd {
    background: var(--w); border: 1px solid var(--i); border-radius: 0;
    box-shadow: var(--ov-shadow);
}
body.lv-body .rs-dd__row { border-radius: 0; color: var(--i); }
body.lv-body .rs-dd__row:hover,
body.lv-body .rs-dd__row.is-active { background: var(--s); }
/* The row's pieces carry their own dark-app colours — near-white names on a
   near-white plate, which is invisible rather than merely off-theme. */
body.lv-body .rs-dd__name { color: var(--i); }
body.lv-body .rs-dd__sub,
body.lv-body .rs-dd__hint,
body.lv-body .rs-dd__hint b,
body.lv-body .rs-dd__combo { color: var(--m2); }
body.lv-body .rs-dd__sub--station,
body.lv-body .rs-dd__hint i,
body.lv-body .rs-dd__name mark,
body.lv-body .rs-dd__sub mark { color: var(--pk); background: none; }
body.lv-body .rs-dd__group {
    color: var(--m); font: 700 9px var(--mono);
    letter-spacing: .1em; text-transform: uppercase;
}
body.lv-body .rs-dd__group span:last-child { color: var(--m); }
body.lv-body .rs-dd__play,
body.lv-body .rs-dd__art {
    background: var(--w); border: 1px solid var(--i); border-radius: 50%; color: var(--i);
}
body.lv-body .rs-dd__row:hover .rs-dd__play,
body.lv-body .rs-dd__row.is-active .rs-dd__play { background: var(--i); color: var(--w); }
body.lv-body .rs-dd__art { border-radius: 0; }
body.lv-body .rs-dd__chip {
    border: 1px solid var(--r); border-radius: 2px;
    background: var(--w); color: var(--m2);
}
body.lv-body .rs-dd__chip:hover { border-color: var(--i); color: var(--i); }
body.lv-body .rs-dd__foot { background: var(--s); border-top: 1px solid var(--r); }
body.lv-body .rs-dd__combo-term { color: var(--i); }
body.lv-body .rs-sheet { background: var(--p); }
body.lv-body .rs-sheet__field {
    background: var(--w); border: 1px solid var(--i); border-radius: 2px;
}
body.lv-body .rs-sheet__input { color: var(--i); }
body.lv-body .rs-sheet__cancel { color: var(--m2); font: 600 13px Inter, system-ui, sans-serif; }

/* ---- tooltips ---- */
body.lv-body .rs-tip {
    background: var(--i); color: var(--w); border-radius: 2px;
    font: 600 11px Inter, system-ui, sans-serif; box-shadow: none;
}


/* ============================================================================
   v0.0.63 — every transport control shows the right state
   ============================================================================
   The plates already had `.is-busy` (see v0.0.54). These are the rest: the row
   circles, the song cards' button, and the bar's own two controls. Same wheel,
   same idea — the glyph is replaced, nothing changes width.
   ============================================================================ */

/* ---- a row's circle, and a song card's button ---- */
.lv .lv-rowplay.is-on,
.lv .song-play.is-on { background: var(--i); color: var(--w); }

.lv .lv-rowplay.is-busy i,
.lv .song-play.is-busy i { display: none; }
.lv .lv-rowplay.is-busy::after,
.lv .song-play.is-busy::after {
    content: ""; width: 11px; height: 11px;
    border: 2px solid rgba(23, 23, 20, .25); border-top-color: var(--i);
    border-radius: 50%;
    animation: lv-spin .7s linear infinite;
}
/* The circle is a grid, so the wheel lands centred without extra positioning. */
.lv .lv-rowplay.is-busy,
.lv .song-play.is-busy { display: grid; place-items: center; }

/* ⚠️ That wheel is INK, and the control it spins on is INK-FILLED at exactly the
   moment it spins: the pointer is still resting on the row (or the card) it just
   clicked, and `.table-row:hover .lv-rowplay` / `.song-play:hover` paint the disc
   black. So a station tuned from a table showed a black circle with an invisible
   wheel inside it — the button read as broken rather than busy.
   The same rule the plates and the player's own disc already follow
   (`.lv-cta__play`, `.lv-pplay`): on a filled disc the ring has to be light.
   The fill is DECLARED here rather than the hover merely neutralised, so the busy
   state looks the same on a phone, where nothing ever hovers, as it does under a
   cursor. `.table-row` rides in the row selector only to outweigh `:hover`. */
.lv .table-row .lv-rowplay.is-busy,
.lv .song-play.is-busy { background: var(--i); border-color: var(--i); color: var(--w); }
.lv .table-row .lv-rowplay.is-busy::after,
.lv .song-play.is-busy::after {
    border-color: rgba(255, 255, 255, .32); border-top-color: #fff;
}

/* ---- the bar ----
   `#shuffleButton` carries `.rs-primary-action`, not `.rs-btn-primary`, so the
   spinner styles.css ships for the latter never applied to it — the bar's
   Shuffle has never shown that it was working. Its own wheel, here.
   The glyph inside #playPauseButton is rewritten by main.js on every play/pause,
   so the busy state is a CLASS on the button and a pseudo-element, never markup:
   anything written inside would be overwritten by the next transition. */
/* The wheel is the button's ONLY child while it spins, in a grid that centres
   whatever it holds. It was absolutely positioned at 50%/50% first — which is
   correct arithmetic, but an absolutely-positioned child of a GRID container is
   laid out against its grid area rather than the padding box, so where it lands
   depends on placement rules that have no business deciding this. One centred
   in-flow item cannot be off-centre. */
.lv-player .lv-picon.is-busy i,
.lv-player .lv-pplay.is-busy i,
.lv-player .lv-picon.is-busy .rs-shuffle-spinner { display: none; }
.lv-player .lv-picon.is-busy,
.lv-player .lv-pplay.is-busy { display: grid; place-items: center; }
.lv-player .lv-picon.is-busy::after,
.lv-player .lv-pplay.is-busy::after {
    content: "";
    width: 14px; height: 14px;
    border: 2px solid rgba(23, 23, 20, .28); border-top-color: var(--i);
    border-radius: 50%;
    animation: lv-spin .7s linear infinite;
}
/* The play button is a filled ink disc — its wheel has to be light. */
.lv-player .lv-pplay.is-busy::after {
    border-color: rgba(255, 255, 255, .32); border-top-color: #fff;
}


/* ---- v0.0.64 ---------------------------------------------------------------
   Country and tag are hidden, not emptied, while the next station resolves: the
   panel is a fixed box and a line vanishing out of it would move everything
   below by 23px on every shuffle. */
.lv .lv-meta.is-hidden { visibility: hidden; }

/* Save is a heart that fills when the station is kept. The outline/solid swap is
   main.js's (it rewrites the bar button's glyph); these are the colours, for the
   bar and for the panel's proxy — which now mirrors that glyph. */
.lv .lv-heroacts button.is-on,
.lv-player .lv-ptext.rs-savebtn.saved,
.lv-player .lv-ptext.rs-savebtn[aria-pressed="true"] { color: var(--pk); }


/* ============================================================================
   v0.0.66 — ONE line for the name, and a window for the whole text
   ============================================================================
   The panel is a fixed-height box with the transport pinned to its floor, so
   every line the name takes is a line the description loses. It now takes
   exactly one, at every width, and ends in an ellipsis; the description fills
   what is left and fades out where it is cut.

   NOTHING inside the panel scrolls and nothing labels itself: the "Tap to read"
   chip and the description's own scrollbar are both gone. A click on either
   text opens `.lv-readmodal` — the one surface that holds the whole name and
   the whole description, and the only one that scrolls.

   Only text that is actually cut is clickable (`is-clamped`, measured from real
   layout by live.js). The affordance is the pointer and the pink hover; on a
   phone there is none, which is the trade the plain panel is worth.
   ============================================================================ */

/* ⚠️ A flex row, NOT `-webkit-line-clamp: 1`. The equalizer lives INSIDE this
   h2 (live.js keeps it there so it trails the last word), and a one-line clamp
   would cut the name AND the waves together — the bars would vanish on exactly
   the long names this change is about. So the text gets its own element to
   ellipsise and the equalizer sits beside it, always visible.
   `.lv-nametext` is created by live.js's keepEq(), because main.js writes the
   name with textContent and wipes whatever is in here on every load. */
.lv .lv-name {
    display: flex; align-items: baseline; min-width: 0;
    /* Not shrinkable: it carries `min-height: 0`, so as a shrinkable item in a
       fixed-height column it was squashed below its own content. One line
       decides the height; the description takes what is left. */
    flex: 0 0 auto;
}
.lv .lv-nametext {
    min-width: 0; flex: 0 1 auto;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The waves keep their place at the end of the name, whether it was cut or not. */
.lv .lv-name .lv-eq { flex: 0 0 auto; }

/* Only what is actually cut off invites a click. `is-clamped` is set by live.js
   from real layout, not from a character count. */
.lv .lv-name.is-clamped,
.lv .lv-descwrap.is-clamped { cursor: pointer; }
.lv .lv-descwrap { position: relative; }
@media (hover: hover) {
    .lv .lv-name.is-clamped:hover,
    .lv .lv-descwrap.is-clamped:hover { color: var(--pk); }
}


/* ---- the reading window ----
   The house overlay: ink scrim, paper plate, 2px ink border, hard shadow, no
   radius — the same one the auth modals wear (`--ov-shadow`). The plate is
   capped at the viewport so the BODY is what scrolls, never the page. */
.lv-readmodal {
    position: fixed; inset: 0; z-index: 95;
    display: grid; place-items: center;
    padding: 24px;
    background: rgba(23, 23, 20, .74);
}
/* ⚠️ REQUIRED, not belt-and-braces. An author `display` beats the UA's
   `[hidden] { display: none }` — origin wins over specificity — so the grid
   above kept this overlay on screen for ever: empty, from the first paint,
   and `closeReading()`'s `hidden = true` did nothing. styles.css already
   carries this exact line three times (`.rs-usermenu`, `.rs-dd`, `.rs-sheet`)
   for the same reason. Any hidden-by-attribute box that sets `display` needs it. */
.lv-readmodal[hidden] { display: none; }
.lv-readmodal__plate {
    position: relative;
    display: flex; flex-direction: column;
    width: min(560px, 100%); max-height: min(78vh, 640px);
    padding: 26px 28px 24px;
    background: var(--w);
    border: 2px solid var(--i);
    box-shadow: var(--ov-shadow);
}
.lv-readmodal__close {
    position: absolute; top: 12px; right: 12px;
    width: 28px; height: 28px; padding: 0;
    border: 1px solid var(--r); background: none; color: var(--m);
    font-size: 20px; line-height: 1; cursor: pointer;
    display: grid; place-items: center;
}
.lv-readmodal__close:hover { background: var(--i); border-color: var(--i); color: var(--w); }
/* Full name — it WRAPS here. That is the whole point of the window. */
.lv-readmodal__name {
    flex: 0 0 auto;
    margin: 0 34px 12px 0;
    font: 800 30px/33px Inter, system-ui, sans-serif; letter-spacing: -.03em;
    color: var(--i);
    overflow-wrap: anywhere;
}
/* The ONE scrolling surface in this feature. */
.lv-readmodal__body { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
.lv-readmodal__body p {
    margin: 0;
    font-size: 15px; line-height: 24px; color: #3a382f;
    overflow-wrap: anywhere;
}
@media (max-width: 900px) {
    .lv-readmodal { padding: 16px; }
    .lv-readmodal__plate { padding: 22px 18px 18px; max-height: 82vh; }
    .lv-readmodal__name { font-size: 24px; line-height: 27px; }
}

/* ============================================================================
   iOS: the shell follows the VISIBLE viewport, not the layout one     (v59)
   ----------------------------------------------------------------------------
   Reported from an iPhone, opening the site from a link in Gmail: the whole app
   sat too high, with a band of bare paper between the tab strip and Safari's
   own toolbar. Intermittent, and always on load.

   The cause is that every piece of the shell is `position: fixed` measured UP
   FROM THE BOTTOM — `.lv` by `inset: 0 0 <bars> 0`, the player and the tab
   strip by `bottom`. A fixed element's containing block is the LAYOUT viewport,
   and iOS Safari does not keep that in step with what you can actually see: it
   captures a height while its chrome is one size and does not re-lay-out when
   the chrome becomes another. Arriving from another app is exactly when the two
   disagree, because Safari opens carrying the "◀ Gmail" bar and then drops it.
   `html, body { height: 100% }` resolved against the same stale box, so the
   ground stopped where the shell did and the rest of the screen showed through.

   The fix is to stop trusting that bottom edge. Everything below is measured
   DOWN FROM THE TOP against `dvh`, the dynamic viewport height, which is
   defined as what is visible right now and updates when the chrome moves. Top
   edges iOS reports correctly; it is only the bottom that drifts.

   Kept in a @supports block, so a browser without `dvh` (Safari < 15.4) keeps
   the old bottom-anchored rules rather than losing its layout to an unresolved
   calc(). The page never scrolls (`overflow: hidden`), so the chrome has
   nothing to collapse in response to and `dvh` stays put in practice.

   The safe-area inset is the second half of the same bug: `index.html` has
   declared `viewport-fit=cover` since v0.0.20 — the page draws under the home
   indicator — and nothing in this file had ever reserved room for it. Whichever
   bar is the floor takes the strip and pads its own content clear of it, so the
   indicator sits on brand surface instead of over a control.
   ============================================================================ */

:root { --lv-safeb: env(safe-area-inset-bottom, 0px); }

/* ⚠️ The shell is <div class="rs-app lv">, and styles.css's phone block still
   carries `.rs-app { height: auto; min-height: 100dvh; overflow: visible }` from
   the pre-redesign layout, where the page itself scrolled. `min-height` beats
   `height`, so on EVERY phone the shell was forced to the full viewport and its
   columns ran on underneath the player and the tab strip. Measured in Chromium
   at 393x852 before this line: shell 0->852 with the player already correctly at
   712. Reset here rather than edited there, because styles.css is shared with
   /radio/ and the FAQ, which still use that rule as intended. */
.lv { min-height: 0; overflow: hidden; }

/* ---- the floor of the layout carries the home-indicator strip ---- */
.lv-player {
    height: calc(var(--lv-playerh) + var(--lv-safeb));
    padding-bottom: var(--lv-safeb);
}
.lv { bottom: calc(var(--lv-playerh) + var(--lv-safeb)); }

@media (max-width: 900px) {
    /* On a phone the tab strip is the floor, so it takes the inset and the
       player goes back to its plain height, riding on top of it. */
    .lv-player {
        height: var(--lv-playerh);
        padding-bottom: 0;
        bottom: calc(var(--lv-tabbarh) + var(--lv-safeb));
    }
    .lv .lv-side {
        height: calc(var(--lv-tabbarh) + var(--lv-safeb));
        padding-bottom: var(--lv-safeb);
    }
    .lv { bottom: calc(var(--lv-playerh) + var(--lv-tabbarh) + var(--lv-safeb)); }
}

/* ---- and the whole thing measured against the viewport that is on screen ---- */
@supports (height: 100dvh) {
    html, body.lv-body { height: 100dvh; }

    .lv {
        top: 0; bottom: auto;
        height: calc(100dvh - var(--lv-playerh) - var(--lv-safeb));
    }
    .lv-player {
        top: calc(100dvh - var(--lv-playerh) - var(--lv-safeb));
        bottom: auto;
    }

    @media (max-width: 900px) {
        .lv { height: calc(100dvh - var(--lv-playerh) - var(--lv-tabbarh) - var(--lv-safeb)); }
        .lv-player { top: calc(100dvh - var(--lv-playerh) - var(--lv-tabbarh) - var(--lv-safeb)); }
        .lv .lv-side {
            top: calc(100dvh - var(--lv-tabbarh) - var(--lv-safeb));
            bottom: auto;
        }
    }
}

/* ---- the account screen's notifications toggle, in paper (v60) ----
   The base rules are the dark app's: a grey track on a dark ground, which on a
   paper plate is a light-grey pill that reads as disabled. Restated in ink, and
   the accent stays pink because that is the one colour this design spends. */
body.lv-body .rs-switch__track { background: var(--s); border-color: var(--r); }
body.lv-body .rs-switch__knob { background: var(--m); }
body.lv-body .rs-switch input:checked + .rs-switch__track { background: var(--pk); border-color: var(--pk); }
body.lv-body .rs-switch input:checked + .rs-switch__track .rs-switch__knob { background: var(--w); }
body.lv-body .rs-switch input:focus-visible + .rs-switch__track { outline-color: var(--pk); }
body.lv-body .account-note { color: var(--m2); font: 400 12px/1.55 Inter, system-ui, sans-serif; }
/* No rule under this one. `.account-row` draws a hairline under every row and
   only clears it on :last-child — but this row is not last, its note is, so the
   line landed between the toggle and the sentence explaining the toggle and cut
   the two apart. They are one thing. */
body.lv-body .account-row--toggle { align-items: center; border-bottom: 0; }
body.lv-body .account-status { color: var(--m); font: 400 11px/1.5 Inter, system-ui, sans-serif; }
body.lv-body .account-status.success { color: var(--pk); }
body.lv-body .account-note { font-size: 11px; line-height: 1.5; }

/* ---- the account actions row, in paper (v63) ----
   `.account-action-btn` (the old full-width plate) is styled further up this
   file; these are its small replacements and need their own grammar. */
body.lv-body .account-btn {
    height: 30px; padding: 0 12px;
    border: 1px solid var(--r); border-radius: 2px;
    background: var(--w); color: var(--m2);
    font: 650 12px Inter, system-ui, sans-serif;
}
body.lv-body .account-btn:hover { background: var(--i); border-color: var(--i); color: var(--w); }
body.lv-body .account-btn--danger { background: var(--w); color: var(--red); border-color: var(--red); }
body.lv-body .account-btn--danger:hover { background: var(--red); border-color: var(--red); color: #fff; }
