/* Photos */

/* The Photos pane: the .library shell exactly -- the pane is itself the scroller on the
   shared card field, and the viewer is a second view swapped in for the grid the way
   .library-detail swaps in for .library-list. */
.photos[hidden] {
  display: none;
}

.photos-title:focus {
  outline: none;
}

/* This line sits directly on the wallpaper, below the filter row -- the on-wall ink pair
   every caption between cards reads through, not the plain --muted this used to read
   (tuned for a card, not the wallpaper it actually sat on). */
.photos-health {
  margin: 0;
  color: var(--ink-on-wall-muted);
  text-shadow: var(--text-shadow-on-wall);
  font-size: var(--text-detail-meta);
}

.photos-controls {
  margin: 0;
}

/* Both filters take their share of the row: unlike the Library's pair (a search that
   leads and a type filter that yields), neither of these is the primary one. Compact
   controls directly on the wallpaper, not a card of their own (the brief's own call for
   this pane) -- so each takes .app-top-button's own translucent on-wall ground rather
   than .app-field's card-tuned --field-fill, which read as a washed-out, barely-there box
   straight on the marble. The field shape (border-radius, font-size, the 40px floor) stays
   .app-field's; only the ground and ink move to the on-wall pair. */
.photos-album,
.photos-source {
  min-height: var(--touch-min);
  color: var(--ink-heading);
  background: var(--top-button-ground);
  border-color: transparent;
  box-shadow: var(--shadow-inset-highlight);
}

/* The one prominent thing on the setup dialog's Sign-in step: the address a person must
   read off this dialog and type into the Immich app. Monospace so it reads as a thing to
   copy, one-tap selectable in full (user-select: all), and wrapping anywhere rather than
   overflowing a phone screen. It reads at the scale's own top step rather than a rem of
   its own (detail-typography.test.ts owns that rule). */
.photos-setup-address {
  margin: 0;
  padding: var(--space-r2) var(--space-r3);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-detail-title);
  font-weight: var(--w-title);
  color: var(--ink);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  user-select: all;
  -webkit-user-select: all;
  overflow-wrap: anywhere;
}

.photos-setup-list {
  margin: 0;
  padding: 0;
  list-style: none;
}

.photos-setup-list > .app-row {
  padding-inline: 0;
}

.photos-setup-list .app-row-title,
.photos-setup-list .app-row-meta {
  overflow-wrap: anywhere;
  overflow: visible;
  -webkit-line-clamp: unset;
  line-clamp: unset;
  display: block;
}

/* Both store links stack down the dialog body's content edge: a placement modifier on the
   shared action group, not a second button style. */
.photos-setup-stores {
  flex-direction: column;
  align-items: flex-start;
}

/* The thumbnail grid: the artwork-grid idiom at a 104px floor, because a thumbnail is a
   smaller thing than a show and three columns have to fit a 390px phone
   (390 - 2 x 12 gutter - 2 x 8 gap = 350; 350 / 3 = 116 >= 104). auto-fill is the
   whole responsive story -- no breakpoint, no hard-coded count. */
.photos-grid {
  display: grid;
  gap: var(--card-gap);
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  padding: 0 var(--list-gutter) var(--space-r4);
}

/* The grid and the viewer are the pane's two views, toggled against each other exactly
   as Library toggles .library-list/.library-detail. Both set a display of their own,
   which outranks the browser's [hidden] rule at equal specificity -- the footgun the
   .library-list comment above exists for. The pane's other hidden element,
   #photos-more, gets no such guard on purpose: neither it nor .app-quiet-button names a
   display, so the browser's own [hidden] rule hides it unaided. */
.photos-grid[hidden],
.photos-viewer[hidden] {
  display: none;
}

/* One square tappable tile: a plain button rather than an .app-card, because a tile is a
   frame for the photograph, not a bounded entry. padding: 0 is written out because the
   bare `button` rule supplies its own, and leaving it unset would make the tile larger,
   not neutral. --panel is the one-step-off-the-field tone in both themes (darker than
   --panel-subtle in light, lighter in dark, the direction each theme's elevation runs),
   so a tile whose image has not loaded yet reads as a slot in the field. The 104px
   column floor makes the square at least 104px tall -- the touch target is the tile
   itself and never drops below the 44px the bare button rule already asks for. */
.photos-tile {
  position: relative;
  aspect-ratio: 1 / 1;
  padding: 0;
  overflow: hidden;
  color: inherit;
  font: inherit;
  background: var(--panel);
  border: 0;
  border-radius: var(--card-radius);
}

.photos-tile-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* A video's duration pill sits on an arbitrary photograph, so it carries its own ground
   and ink rather than inheriting the pane's -- a --muted on an unknown image is not a
   contrast anyone can reason about. The theme's ink at 70% over whatever is beneath,
   with the theme's own ground as the text, keeps the app's ink-on-surface relationship
   in both themes; even the worst case (the wash over a pure-white photograph, light
   theme) still clears 4.5:1. */
.photos-tile-duration {
  position: absolute;
  right: 4px;
  bottom: 4px;
  padding: 0.1rem 0.35rem;
  color: var(--panel-subtle);
  background: color-mix(in srgb, var(--ink) 70%, transparent);
  border-radius: var(--radius-control);
  font-size: var(--text-detail-meta);
}

/* The grid's initial-fetch state: the same square tile shape the real grid draws, filled
   by the shared skeleton primitive, replaced wholesale once the first page answers. A
   plain div, not a button -- there is nothing to press yet. */
.photos-skeleton-tile {
  aspect-ratio: 1 / 1;
}

/* The viewer: a centred column at 720px rather than the old Now Playing 520px, because a photograph is
   the widest thing this app shows. The stage, the nav, the metadata panel and the
   download anchor are stretched children of this column, so its width, cap and centring
   reach them without any of them restating those measures. */
.photos-viewer {
  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 100%;
  max-width: 720px;
  margin-inline: auto;
}

.photos-viewer-stage {
  display: flex;
  justify-content: center;
  max-height: 70vh;
}

.photos-viewer-stage img,
.photos-viewer-stage video {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
  display: block;
  margin-inline: auto;
}

.photos-viewer-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-r2);
  justify-content: space-between;
}

.photos-viewer-nav > * {
  min-width: var(--touch-min);
  min-height: var(--touch-min);
}

/* The viewer's facts read one step quieter than the photograph above them -- meta-sized
   lines with a muted label and an ink value -- so the stage keeps clear visual dominance
   over its own caption block rather than sharing the page with it at full size. */
.photos-meta-row {
  margin: 0;
  font-size: var(--text-detail-meta);
  line-height: var(--leading-detail-tight);
}

.photos-meta-label {
  color: var(--muted);
}

/* #photos-metadata and #photos-download carry no rule of their own: the metadata panel's
   rhythm is .app-divider-list's, and the download anchor is an .app-quiet-button whole. */

/* The photo wall is a fixed full-bleed field over every pane at z-index 40, matching
   .command-bar; its shared page token follows both light and dark themes. */
.photos-wall {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--page);
}

/* The rule above sets display: flex, which outranks the browser's [hidden] rule at equal
   specificity -- the same footgun .library-list[hidden] above exists to fix. */
.photos-wall[hidden] {
  display: none;
}

/* contain, never cover: a kiosk must not crop a photograph. The empty alt is deliberate
   -- decorative full-screen display with nobody at a keyboard, and a date read aloud
   every twenty seconds is noise, not access. */
.photos-wall-image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

/* Only ever the one quiet sentence saying the slideshow has nothing to show. */
.photos-wall-status {
  color: var(--muted);
  font-size: var(--text-detail-meta);
  text-align: center;
}

/* Podcasts */

/* The pane is itself the scroller, the .library shape: three stacked views of which
   exactly one is ever showing. The pale field belongs to this scroller, not to the grid
   inside it -- a field that stopped at the last card would leave the pane half tinted
   and half white, the same seam a list comment exists to avoid. (The mini player
   bar no longer lives in this scroll at all -- see .podcast-mini's own comment in
   styles.css for where it moved and why.) */
.podcasts {
  /* The shared scroller on the card field, with no inset of its own: three sub-views
     share this scroll, and each carries its own gutter. */
  padding: 0;
}

.podcasts[hidden] {
  display: none;
}

/* The one result line for Mark played and its Undo, shared by the followed view and a show
   page. It sticks to the top of the scroller so it is seen wherever the list is scrolled to,
   on the card ground because the pane itself is transparent over the wallpaper. */
.podcasts-action {
  position: sticky;
  top: 0;
  z-index: 6;
  display: flex;
  flex: none;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-r1) var(--space-r3);
  padding: var(--space-r1) var(--list-gutter);
  background: var(--panel-subtle);
  font-size: var(--text-detail-body);
}

.podcasts-action[hidden] {
  display: none;
}


.podcasts-action-text {
  margin: 0;
}

/* Podcasts' own violet subtitle/section-marker tint is gone: the gold-and-blue card round
   drops every per-destination wash (SURFACES: "cards are equal") in favour of artwork
   carrying the pane's identity (see the brief's "let followed-show and episode artwork
   dominate"). Continue, What's New and What's Next now sit directly on the wallpaper as
   labels for the card below them, so their placement is the only destination-specific rule. */
#podcasts .app-section-label {
  margin: 18px 0 var(--space-md);
}

/* The sections that hold a card (Up Next, What's New, a show's episode list) take the shared
   phone gutter themselves, because .podcasts zeroes the scroller's own padding: the card edge
   and its label sit at --list-gutter, the same x as Today's cards and Music's headings. The
   page header and the back bars carry their own inset already, so neither is listed here (a
   second inset would double). */
.podcasts-up-next,
.podcasts-whats-new,
#podcasts-shows-empty,
.podcasts-queue {
  padding-inline: var(--list-gutter);
}

/* A show's card sits straight in its view, so the gutter is a margin on the card -- the
   followed shows list in its own pane, like a show's episode card in its own. */
.podcasts-show > .app-card,
.podcasts-shows-view > .app-card {
  margin-inline: var(--list-gutter);
}

/* What's New sits under Up next, with the shared card-to-card rhythm. */
.podcasts-up-next + .podcasts-whats-new:not([hidden]) {
  margin-top: var(--rhythm-card-card);
}

/* The Now playing card keeps the shared card-to-card rhythm above Up next or its empty line. */
#podcasts-now-block:not([hidden]) {
  margin-bottom: var(--rhythm-card-card);
}

/* One show per row, in the list idiom: the rows are .app-row entries of the .app-card's
   own .app-divider-list, so the card pads the list and the divider gives the rhythm.
   A <ul> needs what the episode lists' <div>s had for free (list-style, margin). */
.podcast-show-list {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* "This feed failed" reads differently from "this show has nothing new" by border style
   alone, the same dashed-not-solid distinction an unavailable row makes; the muted
   failure line is an addition to that cue, never the cue itself. */
.podcast-show-row-broken {
  border: 1px dashed var(--muted);
  border-radius: var(--radius-control);
}

/* A lock and nothing else: inline, meta-sized, no surface of any kind -- it must never
   grow into anything that could carry a URL. */
.podcast-lock {
  display: inline;
  color: var(--muted);
  font-size: var(--text-detail-meta);
}

/* The Always queue star: a 44px hit target whose visible disc inside it is smaller so the
   artwork beside it stays readable. Padding is explicit because the base `button` rule
   would otherwise supply its own. The disc has its own ground so the star holds contrast
   over any cover, and the pressed star takes the accent ink. */
.podcast-always-queue {
  display: inline-flex;
  flex: none;
  width: var(--touch-min);
  height: var(--touch-min);
  align-items: center;
  justify-content: center;
  padding: 0;
  color: var(--muted);
  font-size: var(--text-detail-title);
  line-height: 1;
  background: transparent;
  border: 0;
  border-radius: var(--radius-control);
  cursor: pointer;
}

.podcast-always-queue > span {
  display: inline-flex;
  width: 1.9rem;
  height: 1.9rem;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 50%;
}

.podcast-always-queue[aria-pressed="true"] {
  color: var(--accent-ink);
}

.podcast-always-queue:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* On a show's own page the star is inline beside its label, not over a card. */
.podcast-show-policy {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-r1) 0.6rem;
  margin: 0 var(--list-gutter) var(--space-lg);
}

.podcast-show-policy-label {
  color: var(--ink-heading);
  font-size: var(--text-detail-meta);
  font-weight: var(--w-title);
  text-shadow: var(--text-shadow-on-wall);
}

.podcast-keep-policy {
  min-height: var(--touch-min);
  max-width: 100%;
  font: inherit;
}

.podcast-show-policy-hint {
  flex: 1 1 100%;
  margin: 0;
  color: var(--ink-heading);
  font-size: var(--text-detail-meta);
  line-height: var(--leading-detail-tight);
  text-shadow: var(--text-shadow-on-wall);
}

/* A download's state, in the row's own meta line: the meta ink (--muted already clears
   4.5:1 on the card) for streaming and downloading, and the accent ink for a finished one,
   which is a card-ground colour used the same way elsewhere. */
.podcast-offline-state {
  flex: none;
  font-variant-numeric: tabular-nums;
}

.podcast-offline-state[data-state="downloaded"] {
  color: var(--accent-ink);
}

.podcast-offline-state[data-state="downloading"] {
  color: var(--muted);
}

.podcast-show-policy[hidden] {
  display: none;
}

/* One show's recent episodes: .app-divider-list already gives the rhythm; every caller now
   wraps this in its own .app-card (Continue, What's New, a show's own list, the queue), so
   this needs no field gutter of its own any more -- the card's own padding is the inset. */
.podcast-episodes {
  padding: 0;
  container-type: inline-size;
}

/* An episode row: artwork, a two-line text column and a compact action area, about 64px tall.
   The text column takes the slack and truncates (show name and title each on one line), so
   the actions stay beside it and need no wrapping under the text. The card already pads the
   list, so the first and last rows take no extra vertical padding. A followed-show row is
   the same shape: artwork, title and meta lines, and the same compact action area. */
.podcast-episode,
.podcast-show-row {
  display: flex;
  align-items: center;
  column-gap: var(--list-lead-gap);
  row-gap: 0;
  padding: var(--space-md) 0;
  border-radius: 0;
}

.podcast-episodes > :first-child,
.podcast-show-list > :first-child {
  padding-top: 0;
}

.podcast-episodes > :last-child,
.podcast-show-list > :last-child {
  padding-bottom: 0;
}

/* The card is the bounded surface; a wash on the row inside it reads as a defect (the
   same call .agenda-event-toggle makes), and the main button carries the press cue. */
.podcast-episode:hover,
.podcast-episode:focus-visible,
.podcast-show-row:hover,
.podcast-show-row:focus-visible,
.podcast-group:hover,
.podcast-group:focus-visible {
  background: transparent;
}

.podcast-episode:active,
.podcast-show-row:active,
.podcast-group:active {
  transform: none;
}

/* A series header: art and title share the first line, and the parts (and the two text
   buttons while open) each take a line of their own under the title column. */
.podcast-group {
  flex-wrap: wrap;
  column-gap: var(--list-lead-gap);
  padding: var(--space-xl) 0;
  border-radius: 0;
}

.podcast-group-resume,
.podcast-group-queue-all {
  flex: 0 0 calc(100% - 48px - var(--list-lead-gap));
  margin-left: calc(48px + var(--list-lead-gap));
  padding-left: 0;
  text-align: left;
}

/* The overflow menu on a followed-show row and an episode row: the shared icon-button
   toggle with an .app-inset menu hanging below it, under podcast-* names; in both rows it
   sits in the action area. */
.podcast-overflow {
  position: relative;
  display: flex;
  flex: none;
}

.podcast-overflow-menu {
  position: absolute;
  top: calc(100% + 0.15rem);
  right: 0;
  z-index: 5;
  min-width: 14rem;
  padding: 0.2rem;
  border: 1px solid var(--line);
}

.podcast-overflow-menu[hidden] {
  display: none;
}

.podcast-overflow-menu button {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 100%;
  color: var(--ink);
  text-align: left;
}

.podcast-overflow-note {
  white-space: normal;
}

/* A quiet icon: no ring or fill at rest, a subtle ground on hover/press, and a visible
   ring for the keyboard. .icon-button supplies the 44px box, the muted ink and the 20px
   stroked glyph. */
.podcast-action {
  border: 0;
  border-radius: var(--radius-control);
  cursor: pointer;
}

.podcast-action:active {
  background: var(--panel);
}

.podcast-action:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.podcast-action:disabled {
  opacity: 0.5;
  cursor: default;
}

/* An episode/group/queue row's own artwork, at the row's left edge as the first flex
   child -- .podcast-mini-art's own square and either/or, at the same 48px .music-art
   already uses for a track row. Built fresh per row (see episodeArt in podcasts.js)
   rather than wired once, since these rows are rebuilt wholesale on every render. */
.podcast-episode-art {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 48px;
  width: 48px;
  height: 48px;
  overflow: hidden;
}

.podcast-episode-art-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.podcast-episode-art-image[hidden],
.podcast-episode-art-fallback[hidden] {
  display: none;
}

/* The initials stand-in: a show with no artwork, or whose artwork failed to load, still
   occupies the same square, so the row reads as a cover that is missing rather than a
   broken one. Decorative -- the title beside it already names the show. */
.podcast-episode-art-fallback {
  color: var(--muted);
  font-size: var(--text-detail-meta);
  font-weight: var(--w-title);
}

/* A part's series header, an episode's and a followed show's main button share one shape:
   no chrome, a left-aligned column, and the 44px touch target every control in this app carries. */
.podcast-episode-main,
.podcast-show-main,
.podcast-group-main {
  flex: 1 1 0;
  min-width: 0;
  min-height: 48px;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--space-2xs);
  padding: 0;
  color: inherit;
  background: transparent;
  border: 0;
  font: inherit;
  text-align: left;
}

/* The episode row's text block: the show line (name, then icons), the title (wraps in full),
   then a meta line (length, age), centred beside the 48px art. A show row's text block
   (title, then the meta lines) centres the same way. */
.podcast-episode-main,
.podcast-show-main {
  min-height: 0;
  justify-content: center;
  gap: 0;
}

.podcast-episode-show,
.podcast-episode-line {
  display: flex;
  align-items: baseline;
  column-gap: var(--space-sm);
  min-width: 0;
  margin: 0;
  line-height: 1.25;
}

.podcast-episode-show {
  font-size: var(--text-support);
}

.podcast-episode-show-name,
.podcast-episode-title {
  min-width: 0;
  overflow-wrap: break-word;
}

.podcast-episode-show-name {
  flex: 0 1 auto;
}

.podcast-episode-line {
  flex-wrap: wrap;
}

.podcast-episode-title {
  flex: 1 1 100%;
  line-height: 1.25;
}

:root[data-text-size="large"] .podcast-episode-show {
  flex-wrap: wrap;
}

.podcast-episode-length {
  flex: none;
  font-size: var(--text-support);
  font-variant-numeric: tabular-nums;
}

.podcast-episode-age {
  flex: none;
  font-size: var(--text-support);
}

.podcast-episode-length + .podcast-episode-age::before {
  content: "· ";
}

.podcast-pick-mark {
  flex: none;
  color: var(--accent-ink);
}

/* A series' parts read as subordinate to it: a modest indent (the 76px rail left a part's
   title a sliver of the row), with their own hairline dividers so the part list is a
   list inside the entry, not a continuation of the entry list. */
.podcast-group-parts {
  flex: 0 0 100%;
  margin: 0;
  padding: 0 0 0.2rem 16px;
  list-style: none;
}

.podcast-group-parts[hidden] {
  display: none;
}

.book-progress {
  position: absolute;
  inset: auto 0 -3px;
  height: 2px;
  overflow: hidden;
  border-radius: var(--radius-pill);
  background: var(--line);
}

.book-progress > span {
  display: block;
  width: 0;
  height: 100%;
  background: var(--accent);
}

/* Books: a row is a 48px cover and a text column, the .podcast-episode shape. The title and
   the meta line wrap in full; a started book carries the same 2px
   strip along the bottom of its text column. */
.book-row {
  display: flex;
  align-items: center;
  column-gap: var(--list-lead-gap);
  padding: var(--space-md) 0;
}

/* The browser's default <ul> padding and margins would indent every cover and pad every card;
   .podcast-episodes zeroes the same things. */
.books-list {
  padding: 0;
  margin: 0;
  list-style: none;
}

.books-list > :first-child {
  padding-top: 0;
}

.books-list > :last-child {
  padding-bottom: 0;
}

.book-cover {
  flex: 0 0 48px;
  width: 48px;
  height: 48px;
  object-fit: cover;
}

.book-cover-initials {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--muted);
  font-size: var(--text-detail-meta);
  font-weight: var(--w-title);
}

.book-main {
  position: relative;
  display: flex;
  flex: 1 1 0;
  flex-direction: column;
  justify-content: center;
  gap: var(--space-2xs);
  min-width: 0;
  min-height: 48px;
  padding: 0;
  color: inherit;
  background: transparent;
  border: 0;
  font: inherit;
  text-align: left;
  text-decoration: none;
}

/* Title, author/chapter line and the "Open in Audible" hint all wrap: a book's name and
   progress are primary text. The cover stays 48px at every text size. */
.book-title,
.book-meta,
.book-open {
  min-width: 0;
  overflow-wrap: break-word;
}

.books-search-row {
  margin-bottom: var(--space-r3);
}

/* The shared .app-field floor is --touch-min. */
.books-search {
  min-height: var(--touch-min);
}

.books-author {
  margin: 14px 0 var(--space-sm);
  color: var(--muted);
  font-size: var(--text-detail-meta);
  font-weight: var(--w-title);
}

/* The Now playing / Up next section at the top of Podcasts, Music and Books: a Now playing
   card holding one state-only row (48px cover, title and subtitle, then Playing or Paused),
   and under it each page's own Up next card when something will actually play next. The
   state word sits in the row's meta ink, which reads on the card; there is no tint here. */
/* Music uses .app-page-body and .app-section for this spacing; other audio pages keep it. */
.podcasts .audio-now,
.books .audio-now {
  margin-bottom: var(--card-gap);
}

.audio-now[hidden],
.audio-now > [hidden] {
  display: none;
}

.audio-now-row {
  align-items: center;
}

.audio-now-row .audio-now-title,
.audio-now-row .audio-now-subtitle {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Podcasts paints the Now playing row as its own play/pause tap (audio-ui.js adds .audio-now-tap
   with role=button). The row is a div, so the global button:focus-visible ring does not cover it:
   the same inset outline .podcast-scrub gets. */
.audio-now-tap {
  cursor: pointer;
}

.audio-now-tap:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.audio-now-art {
  display: flex;
  flex: 0 0 48px;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  overflow: hidden;
}

.audio-now-text {
  flex: 1 1 auto;
  min-width: 0;
}

.audio-now-state {
  flex: none;
  margin: 0;
  white-space: nowrap;
}

.audio-now-state .app-quiet-button {
  flex: none;
  padding-inline: var(--space-md);
  white-space: nowrap;
}

/* The player bar's progress line: a thin strip overlaid on the foot of the surface, absolutely
   placed so it adds no row. The input is --space-sm tall, full width. The drawn line is the
   3px at its foot, painted by background layers that are transparent elsewhere. touch-action:
   none keeps a horizontal touch drag on the line from being taken as a scroll. The thumb is not
   drawn (opacity: 0, because WebKit paints a transparent thumb's own chrome) and Chromium starts
   a touch drag only on the thumb (a press on the bare track is a tap that seeks), so the thumb
   is the touch target for a drag. Chromium paints the accent share from --fill (podcasts.js
   writes it), Firefox has its own ::-moz-range-progress; both engines' pseudo-elements are
   styled because each ignores the other's. */
.podcast-scrub {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: var(--space-sm);
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  touch-action: none;
  -webkit-appearance: none;
  appearance: none;
}

.podcast-scrub::-webkit-slider-runnable-track {
  height: var(--space-sm);
  border-radius: 0;
  background:
    linear-gradient(var(--accent), var(--accent)) 0 100% / var(--fill, 0%) 3px no-repeat,
    linear-gradient(var(--player-progress-track), var(--player-progress-track)) 0 100% / 100% 3px no-repeat;
}

.podcast-scrub::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 28px;
  height: var(--space-sm);
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  opacity: 0;
}

.podcast-scrub::-moz-range-track {
  height: var(--space-sm);
  border-radius: 0;
  background: linear-gradient(var(--player-progress-track), var(--player-progress-track)) 0 100% / 100% 3px no-repeat;
}

.podcast-scrub::-moz-range-progress {
  height: var(--space-sm);
  background: linear-gradient(var(--accent), var(--accent)) 0 100% / 100% 3px no-repeat;
}

.podcast-scrub::-moz-range-thumb {
  width: 28px;
  height: var(--space-sm);
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  opacity: 0;
}

/* A source that cannot seek shows the same line read-only. */
.podcast-scrub:disabled {
  cursor: default;
}

/* Range inputs are not covered by the global button:focus-visible ring, so this view
   gives its own, the .library-search shape. */
.podcast-scrub:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* The transport controls: quiet 44px hit areas with no outline of their own -- a transparent
   ground, a subtle hover/active ground and a visible focus ring -- and 22px inline-SVG
   glyphs. Only play/pause is accented (rule 01: one accent per region). Padding is 0
   explicitly, because the base button rule sets 0.7rem 1.2rem and an absent declaration
   here means that, not nothing. */
.podcast-control {
  display: grid;
  place-items: center;
  width: var(--touch-min);
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  padding: 0;
  color: var(--ink);
  background: transparent;
  border: 0;
  border-radius: 50%;
  font-size: var(--text-detail-body);
  line-height: 1;
}

.podcast-control svg {
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.podcast-control svg .icon-solid {
  fill: currentColor;
}

.podcast-control:hover {
  background: var(--code);
}

.podcast-control:focus-visible {
  background: var(--code);
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

.podcast-control:active {
  background: var(--line-strong);
  transform: scale(0.94);
}

@media (prefers-reduced-motion: reduce) {
  .podcast-control:active {
    transform: none;
  }
}

/* Play/pause is the one a person reaches for without looking, and the region's one
   accented control: a 46px gold disk with a filled 22px symbol. Both symbols are in the
   markup and data-state picks the one drawn, so the swap needs no DOM rebuild and the
   button keeps focus across a tap. */
.podcast-control-main {
  width: var(--touch-min);
  min-width: var(--touch-min);
  min-height: var(--touch-min);
  color: var(--on-accent);
  background: var(--accent);
}

.podcast-control-main:hover,
.podcast-control-main:focus-visible {
  background: var(--accent-hover);
}

/* The base .podcast-control:active paints --line-strong, which would turn the accent disk grey (dim play glyph) while a touch press or a stuck :active state lingers after a tap; the disk keeps its accent ground. */
.podcast-control-main:active {
  background: var(--accent-hover);
}

.podcast-control-main:focus-visible {
  outline-color: var(--ink);
  outline-offset: 2px;
}

/* A chat command's play() the browser refused: the button to tap is ringed (static, so
   nothing to tone down for prefers-reduced-motion) until the next play or a tap on it. */
.podcast-control.playback-attention {
  box-shadow: 0 0 0 2px var(--page), 0 0 0 4px var(--ink);
}

.podcast-control-main svg {
  fill: currentColor;
  stroke-width: 1;
}

.podcast-control-main .icon-pause,
.podcast-control-main[data-state="playing"] .icon-play {
  display: none;
}

.podcast-control-main[data-state="playing"] .icon-pause {
  display: inline;
}

/* What's Next: the ordered queue below Now Playing. .podcast-episodes already gives the
   list its gutters and .app-divider-list the hairline rhythm between rows -- both reused
   as-is (see index.html); this only adds what a <ul> needs that a <div> already had for
   free (list-style, margin) and the row's own three-part layout. */
.podcast-queue-list {
  list-style: none;
  margin: 0;
}

/* A leading handle before the artwork: a fixed-size control (via .icon-button, reused
   whole) that stays on the first line, so the wrapped action area drops under the title
   exactly as it does in the other rows. */
.podcast-queue-row,
.music-queue-row {
  align-items: center;
  min-height: var(--touch-min);
}

.podcast-queue-handle,
.music-queue-handle {
  align-self: center;
  /* A narrow visible column: the glyph sits in a 32px-wide, 44px-tall box whose left 8px
     hang into the card's padding (-8px margin), so the column costs the row 24px, and the
     -4px right margin makes the art follow the grip by 8px instead of the full 12px gap. */
  width: var(--touch-min);
  min-width: var(--touch-min);
  margin-left: -8px;
  margin-right: -4px;
  position: relative;
}

/* The glyph column stays 32px; the overlay widens the hit area to --control-min (48px) by
   reaching into the row's own padding on the left and the gap on the right, never as far
   as the art that follows. */
.podcast-queue-handle::after,
.music-queue-handle::after {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -6px;
  right: -6px;
}

/* touch-action: none is scoped to the handle alone, not the row: grabbing the handle must
   not fight the page's own vertical scroll, but the rest of the row (and every other list
   in the app) still scrolls exactly as before. cursor: grab signals what the control does
   before a person ever touches it; chat.js swaps no cursor while dragging (a pressed state
   would need a body-level class this feature does not need). */
.podcast-queue-handle,
.music-queue-handle {
  touch-action: none;
  cursor: grab;
}

/* The dragged row while a pointer holds it: lifted just enough to read as "this one is
   moving", never so far it looks broken. No transform/position change -- the row is
   physically reordered in the DOM as the pointer crosses each neighbour's midpoint (see
   chat.js), so this is the only visual feedback the drag needs. */
.podcast-queue-row-dragging,
.music-queue-row-dragging {
  opacity: 0.6;
  background: var(--panel-subtle);
}

/* The status slots of the followed and show views share the standard placement. */
#podcasts-status,
#podcasts-show-status {
  margin: 0.3rem var(--list-gutter) 0.6rem;
  color: var(--ink-heading);
  text-shadow: var(--text-shadow-on-wall);
}

#podcasts-status.app-status-failure,
#podcasts-show-status.app-status-failure {
  color: var(--error);
}

/* An empty status line is a live region waiting for text, not a row: its margin would trail
   the last card by ~30px and make this pane the odd one out at maximum scroll. Kept
   rendered rather than display: none so a screen reader still announces the text that
   arrives. */
#podcasts-status:empty,
#podcasts-show-status:empty {
  margin: 0;
}

/* The persistent player, its own control above the composer pill (apps/chat/public/index.html
   puts #playback-mini first inside <form id="composer">, before the thread
   zone, the status rail and .composer-row). It is a separate, fully rounded surface,
   --stack-gap above the connected surface the zone, rail and pill make, so the player and
   the message box read as two distinct controls. The outer .podcast-mini is only the layout
   shell: it takes the composer's own gutters (100% of its content box), carries that
   gap as its bottom margin, and [hidden] (below) is what makes it disappear cleanly, gap
   and reserved height with it, when nothing is playing. The surface -- the inner
   .podcast-mini-surface -- keeps
   the --panel-subtle fill with --radius-surface on all four corners and a slight card lift.
   Music and Podcasts share this geometry.

   One row at every width and every text size: the art, a one-line title taking the free
   width, and the transport at the right, with Podcasts' thin progress strip overlaid on the
   foot of the surface (.podcast-scrub), which adds no row. The bar's height is still inside the composer, which chat.js's
   ResizeObserver on #composer re-measures into --command-bar-space, so every pane's bottom
   reservation grows with it. It lives in the composer and not inside Podcasts' or Music's
   own pane scroller (it was once position: sticky there) because the bar would vanish the
   moment the person left that one pane even though playback itself survives navigation
   (#podcast-audio lives at body level, outside every pane). */
.podcast-mini {
  position: relative;
  flex: none;
  width: 100%;
  margin-bottom: var(--stack-gap);
}

.podcast-mini[hidden] {
  display: none;
}

/* Folded: the surface is gone and the chip below is the whole player. The container keeps its
   width (the chip is absolutely positioned, so it adds no height), the dropped bottom gap lets
   the composer shrink, and chat.js's ResizeObserver hands the height back to the page
   reservation. */
.podcast-mini[data-folded="true"] {
  margin-bottom: 0;
}

.podcast-mini[data-folded="true"] .podcast-mini-surface {
  display: none;
}

/* The folded player: one circle in the composer's right gutter. Its conic ring is the progress
   line's circular twin -- the same accent over the same track colour .podcast-scrub fills with,
   from 12 o'clock clockwise. Padding and border are stated explicitly: deleting either makes
   the button BIGGER or accent-inked, not smaller, because the base `button` rule underneath
   supplies its own. */
.podcast-mini-chip {
  position: absolute;
  right: 0;
  bottom: var(--stack-gap);
  width: var(--touch-min);
  height: var(--touch-min);
  padding: 0;
  border: 0;
  border-radius: 50%;
  overflow: hidden;
  background: conic-gradient(var(--accent) var(--progress, 0%), var(--player-progress-track) 0);
  box-shadow: var(--shadow-card);
}

.podcast-mini-chip[hidden] {
  display: none;
}

/* The art circle inset by the ring's 3px thickness. Both children fill the same inset circle,
   so each hands [hidden] back itself (see .podcast-mini-art-image): exactly one may show. */
.podcast-mini-chip-art-image {
  position: absolute;
  inset: 3px;
  width: calc(100% - 6px);
  height: calc(100% - 6px);
  border-radius: 50%;
  object-fit: cover;
}

.podcast-mini-chip-art-fallback {
  display: grid;
  place-items: center;
  position: absolute;
  inset: 3px;
  width: calc(100% - 6px);
  height: calc(100% - 6px);
  border-radius: 50%;
  background: var(--panel-subtle);
  color: var(--muted);
  font-size: var(--text-detail-body);
  font-weight: var(--w-title);
}

.podcast-mini-chip-art-image[hidden],
.podcast-mini-chip-art-fallback[hidden] {
  display: none;
}

/* The play state glyph, tucked inside the rim: the chip's overflow: hidden clips the circle's
   corners, so an edge-flush glyph would lose part of itself. */
.podcast-mini-chip-state {
  position: absolute;
  right: 1px;
  bottom: 1px;
  display: grid;
  place-items: center;
  width: 18px;
  height: 18px;
  color: var(--ink);
  background: var(--player-surface);
  border-radius: 50%;
}

.podcast-mini-chip-state svg {
  width: 10px;
  height: 10px;
  fill: currentColor;
}

/* Same swap as .podcast-control-main: paused draws the play triangle, playing the pause bars. */
.podcast-mini-chip-state .icon-pause,
.podcast-mini-chip-state[data-state="playing"] .icon-play {
  display: none;
}

.podcast-mini-chip-state[data-state="playing"] .icon-pause {
  display: inline;
}

/* No border: the padding plus the touch-floor transport makes the row. overflow: hidden lets
   the progress strip follow the rounded corners (isolation: isolate makes the surface its own
   stacking context, which WebKit needs to clip the range input's composited layer to the
   radius). The surface is one non-wrapping row: the opener and the transport share it at every
   width and text size, and the scrub strip is overlaid on its foot, so the bar has no empty
   band beneath the controls.
   touch-action keeps the downward fold drag a pointer gesture: without it a touch scroller
   claims the vertical pan and the surface sees pointercancel, never pointerup. */
.podcast-mini-surface {
  position: relative;
  display: flex;
  align-items: center;
  column-gap: var(--space-md);
  padding: var(--space-sm) var(--space-xs) var(--space-sm) var(--space-md);
  overflow: hidden;
  isolation: isolate;
  touch-action: pan-x;
  color: var(--ink);
  background: var(--panel-subtle);
  border: 0;
  border-radius: var(--radius-surface);
  box-shadow: var(--shadow-inset-highlight), var(--shadow-card);
}

/* The artwork and the title. In all three bars the opener button is this, so the whole of it
   opens the source's page and no transport hit area lies inside it. Padding and radius are
   set explicitly rather than omitted: deleting a padding override makes a button BIGGER, not
   smaller, because the base `button` rule underneath would supply its own. */
.podcast-mini-open {
  display: flex;
  flex: 1 1 0;
  align-items: center;
  gap: var(--space-lg);
  min-width: 0;
  min-height: var(--touch-min);
  padding: 0;
  border-radius: var(--radius-control);
}

.podcast-mini-open {
  cursor: pointer;
}

/* The playing show's artwork: a fixed 40px square rather than a share of the free width,
   so the title's ellipsis stays the only thing that gives way on a narrow screen and one
   oddly shaped cover cannot change the bar's height. It carries .app-media for the
   ground, border and corner every other piece of art in this app already draws, so this
   rule owns only the square itself and the centring of whichever child is showing. */
.podcast-mini-art {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 40px;
  width: 40px;
  height: 40px;
  overflow: hidden;
}

/* The image fills the square and crops rather than distorts. .app-media's object-fit
   landed on the outer span, where a span can do nothing with it, so the img states its
   own. */
.podcast-mini-art-image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Both children of the art slot set display, so each hands [hidden] back itself: the
   UA's own display: none is outranked by any author rule naming display, and exactly one
   of these two may ever be showing. */
.podcast-mini-art-image[hidden],
.podcast-mini-art-fallback[hidden] {
  display: none;
}

/* The initials stand-in: a show with no artwork still occupies the same square, so the
   slot reads as a cover that is missing its image rather than as a broken one. Decorative
   -- the title beside it says the show's name. */
.podcast-mini-art-fallback {
  color: var(--muted);
  font-size: var(--text-detail-body);
  font-weight: var(--w-title);
}

/* The title, taking the row's whole remaining width. The element is a <span> inside the
   Podcasts opener (a button may hold no block content) and a <p> in Music's plain block, so
   the block display is stated here rather than inherited. */
.podcast-mini-text {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
}

.podcast-mini-title {
  display: block;
  min-width: 0;
  margin: 0;
  overflow: hidden;
  color: var(--ink);
  font-size: var(--text-detail-body);
  font-weight: var(--w-title);
  line-height: 1.3;
  /* One line, then a cap: the row stays one line tall and the full title is in the opener's
     accessible name. */
  display: -webkit-box;
  -webkit-line-clamp: 1;
  -webkit-box-orient: vertical;
  overflow-wrap: anywhere;
}

.podcast-mini-subtitle {
  display: block;
  min-width: 0;
  margin: 0;
  overflow: hidden;
  color: var(--muted);
  font-size: var(--text-support);
  line-height: 1.3;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.podcast-mini-subtitle[hidden] { display: none; }

/* The transport: three touch-floor targets on the row's right, beside the title. Real
   --space-xs gaps separate them, so no hit rect ever overlaps another, and the scrub strip is
   overlaid on the surface's foot (see .podcast-scrub). Inside the bar the glyphs are 20px. */
.podcast-mini-controls {
  display: flex;
  flex: none;
  align-items: center;
  gap: var(--space-xs);
}

/* Every control in the row is exactly the touch floor square; play/pause keeps its accent
   disk at the same size as its neighbours. */
.podcast-mini-controls .podcast-control,
.podcast-mini-controls .podcast-control-main {
  width: var(--touch-floor);
  min-width: var(--touch-floor);
  height: var(--touch-floor);
  min-height: var(--touch-floor);
}

/* The control's own display: grid outranks the UA's [hidden] rule, so the permanently hidden
   back button needs this to give up its box in the row. */
.podcast-mini-controls [hidden] {
  display: none;
}

.podcast-mini-controls .podcast-control svg {
  width: 20px;
  height: 20px;
}

/* Busy dims only the play/pause glyph; the accent disk stays full, so Play still reads as
   present. Every control stays operable while the bar reports aria-busy (chat-6-playback.js). */
.podcast-mini[aria-busy="true"] .podcast-control-main svg {
  opacity: 0.6;
}

/* The Now playing surface is a full player; its inner column remains readable on wide screens. */
.now-playing-dialog {
  width: calc(100% - 2 * var(--space-md));
  max-width: none;
  height: var(--dialog-max-height);
  max-height: var(--dialog-max-height);
}

.now-playing-dialog .app-dialog-frame {
  height: var(--dialog-max-height);
  max-height: var(--dialog-max-height);
}

.now-playing-dialog .music-proposal-body {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  align-items: center;
  overflow: auto;
}

.now-playing-full-player {
  display: flex;
  width: min(100%, var(--now-playing-column, 36rem));
  flex-direction: column;
  align-items: stretch;
  justify-content: center;
  gap: var(--space-lg);
  margin: auto;
  padding: var(--space-md) 0;
}

.now-playing-full-art {
  align-self: center;
  width: min(100%, max(var(--now-playing-art-min, 10rem), min(34dvh, 24rem)));
  height: auto;
  aspect-ratio: 1;
  flex-basis: auto;
}

.now-playing-full-content {
  display: flex;
  min-width: 0;
  flex-direction: column;
  gap: var(--space-sm);
}

.now-playing-full-title {
  font-size: var(--text-detail-title);
}

.now-playing-full-meta {
  min-height: 1lh;
}

.now-playing-full-scrub {
  flex: none;
  height: var(--touch-floor);
}

.now-playing-full-scrub::-webkit-slider-runnable-track,
.now-playing-full-scrub::-moz-range-track,
.now-playing-full-scrub::-moz-range-progress,
.now-playing-full-scrub::-webkit-slider-thumb,
.now-playing-full-scrub::-moz-range-thumb {
  height: var(--touch-floor);
}

.now-playing-full-timing {
  display: flex;
  justify-content: space-between;
  color: var(--muted);
  font-size: var(--text-support);
}

.now-playing-full-controls {
  justify-content: center;
}

.now-playing-full-controls .podcast-control,
.now-playing-full-controls .podcast-control-main {
  width: calc(var(--touch-floor) * 1.25);
  min-width: calc(var(--touch-floor) * 1.25);
  height: calc(var(--touch-floor) * 1.25);
  min-height: calc(var(--touch-floor) * 1.25);
}

.now-playing-full-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-sm);
}

.now-playing-full-up-next {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
}

.now-playing-full-actions[hidden],
.now-playing-full-up-next[hidden] {
  display: none;
}

.now-playing-full-up-next-item {
  display: flex;
  width: 100%;
  min-height: var(--touch-floor);
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  padding: var(--space-sm) 0;
  color: var(--ink);
  text-align: left;
  background: transparent;
  border: 0;
}

@media (min-aspect-ratio: 4/3) {
  .now-playing-full-player {
    width: min(100%, var(--now-playing-column, 48rem));
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: center;
  }

  .now-playing-full-art {
    grid-row: 1;
    grid-column: 1;
  }

  .now-playing-full-content {
    grid-row: 1;
    grid-column: 2;
  }
}
