/* Unified player shell.
 *
 * Deliberately single-palette dark in both app themes. Two reasons, and they
 * agree: the redesign spec gives Day values for every other screen but states
 * Now Playing's colours as dark literals only, and the player this replaces
 * (.hp-full) already committed to its own dark glass "the way native players
 * do" — cover art has to read correctly, and it can be any colour at all.
 *
 * Values here are the spec's, verbatim, rather than app.css theme tokens. That
 * is the exception this file is allowed; every other surface in the redesign
 * goes through tokens.
 */

.ps {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Stop short of the host's own bottom nav rather than covering it —
   * reported: opening the full player hid the tab bar entirely, with no way
   * back to another screen short of closing the player first. --pk-dock-bottom
   * is the same hook the docked mini-player already docks above (see
   * player.css's --pk-dock-bottom note); the full shell reserves the
   * identical strip instead of introducing a second one. Default 0px keeps a
   * host with no bottom nav (or the desktop sidebar layout, which sets this
   * to 0) covering the full screen exactly as before. z-index alone doesn't
   * uncover a bar underneath — see .tts-text-panel's identical note in
   * player.css — so the box itself has to stop there, not just paint above it. */
  bottom: calc(var(--pk-dock-bottom, 0px) + env(safe-area-inset-bottom, 0px));
  /* Above every docked surface it has to cover — the player bar and the
   * ambience bar both sit at the tab-bar layer (~70-90), and the sheets that
   * open FROM this shell (queue, chapters) deliberately sit higher still at
   * 1100+. A hardcoded literal because the kit can't reach app.css's z-index
   * tokens; the value is documented in app.css's own token block so the two
   * stay legible together. (This used to read "above .hp-full (1000) and
   * .tts-full (1001), which still exist while the migration is mid-flight" —
   * that migration is now finished and both surfaces are gone.) */
  z-index: 1005;
  display: flex;
  flex-direction: column;
  animation: ps-rise 0.22s ease-out;
}
.ps[hidden] {
  display: none;
}

/* The mini-player is the same item in miniature; while the full shell is up
 * it would just be the surface duplicating itself under its own backdrop.
 * `.tts-mini` carries `.hp-mini` too, so this covers both docked bars. */
body.ps-open .hp-mini {
  display: none;
}

@keyframes ps-rise {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  .ps {
    animation: none;
  }
  /* Cover Flow 3D transforms are motion — flatten to the old 2D look. */
  .ps-stage-clip {
    perspective: none;
  }
  .ps-stage-row {
    transform-style: flat;
  }
  .ps-stage-prev {
    transform: translateY(-50%) scale(0.9);
  }
  .ps-stage-next {
    transform: translateY(-50%) scale(0.9);
  }
}

/* The gradient must sit on an opaque base — a gradient alone leaves the top of
 * the overlay translucent and the screen beneath shows through. This is the
 * only place kind colour touches a background, and the reason a video and a
 * chapter feel different without anything moving. */
.ps-backdrop {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(
      120% 70% at 50% 0%,
      color-mix(in srgb, var(--ps-kind, #f2a113) 12%, transparent),
      transparent 62%
    ),
    #08090d;
}

/* The player stays; the extras panel is what scrolls.
 *
 * This was one scrolling block: artwork, title, transport and the chapters /
 * transcript / queue panel all inside `.ps-inner`, with the panel capped at
 * 46vh. Reading a transcript therefore scrolled the artwork, the title and
 * the transport up and off the top of the screen — reported twice, the second
 * time as "the tabs are supposed to scroll on top of the player, but instead
 * it scrolls the player up off the screen."
 *
 * A column instead: everything above the panel keeps its natural height
 * (`flex: none`, or a flex parent would compress it), and `.ps-extras` takes
 * whatever is left, with `.ps-extras-body` scrolling inside that. The panel
 * is now as tall as the screen allows rather than a fixed 46vh, which is also
 * more transcript on screen than before.
 *
 * `overflow-y: auto` stays on this element as a fallback and nothing more: on
 * a screen too short to fit the fixed part at all, scrolling it is better than
 * clipping it. In the normal case there is nothing to scroll here, so a drag
 * over the transcript cannot move the player even if it reaches the end of
 * the list. */
.ps-inner {
  position: relative;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  color: #edeff4;
  font-family: var(--font-body, inherit);
  /* #659: padding-bottom clears the closed extras tab strip (~56px) so
   * transport control labels don't disappear behind it. */
  padding-bottom: 52px;
}
.ps-inner > * {
  flex: none;
}
.ps-inner > .ps-stage-clip {
  flex: 1;
}

/* ── 1 · Context line ─────────────────────────────────────────────────────── */

.ps-context {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: calc(20px + env(safe-area-inset-top, 0px)) 14px 8px;
  /* Swipe-down-to-close (player-shell.js) owns vertical drags here; without
   * this the browser's own pan/scroll gesture recognition can beat our
   * pointer handlers to a touch, so the JS-driven transform never starts
   * smoothly — the same reason VideoSurface's .vs-head sets this. */
  touch-action: none;
}
.ps-round {
  flex: none;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 999px;
  background: #ffffff12;
  color: #edeff4;
  display: grid;
  place-items: center;
  cursor: pointer;
}
@media (hover: hover) {
  .ps-round:hover {
    background: #ffffff1f;
  }
}
.ps-context-label {
  flex: 1;
  min-width: 0;
  text-align: center;
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #8a91a0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── 2 · Stage ────────────────────────────────────────────────────────────── */

.ps-stage-clip {
  overflow: hidden;
  display: flex;
  flex-direction: column;

  /* touch-action is auto here; non-video stages override to none below.
   * The stage clip owns horizontal (Cover Flow) and downward (close) drags
   * for audio/book/podcast — none stops the browser racing JS with its
   * own pan. Video doesn't use the stage clip for gestures (the titleblock
   * below handles those; nothing of the app's is layered over the embed),
   * and the inner is overflow:hidden for video, so auto avoids dead zones. */
  touch-action: auto;
  padding: 8px 0 0;
  perspective: 1200px;
  perspective-origin: center center;
}
/* Non-video stages need touch-action: none on the stage clip so the
 * browser doesn't race JS with its own pan/scroll — downward drags close
 * the shell and horizontal drags do Cover Flow, both JS-driven. Video
 * doesn't use the stage clip for gestures — the titleblock below handles
 * those, and the embed owns every pixel of its own rect — and the inner is
 * overflow:hidden for video anyway, so auto just avoids dead zones where
 * touch-action:none swallowed taps on empty space around the embed. */
.ps:not([data-stage='video']) .ps-stage-clip {
  touch-action: none;
}
.ps-stage-row {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  flex: 1;
  min-height: 280px;
  /* On a short screen the artwork is what has to give.
   *
   * The fixed part of the player is roughly 550px tall, of which this is more
   * than half. Now that the extras panel takes the remainder rather than
   * having its own 46vh, a 280px floor on a 600px-tall window would leave the
   * transcript nothing at all — and the player would go back to scrolling to
   * reach it, which is the bug. Cover art earns its size on a tall phone; on a
   * short one, being able to read the transcript matters more. */
}
/* The artwork is always full-size. The extras drawer overlays it rather than
 * trading pixels against it, so there is no need to shrink the art to make
 * room for the panel. An episode with nothing in the strip keeps the full
 * stage; so does one with tabs, until the drawer is opened. */
.ps-stage-row {
  /* Preserve 3D so children's rotateY/translateZ render in perspective. */
  transform-style: preserve-3d;
  /* A size container, so the video stage below can size itself against this
   * row's BOTH dimensions (cqw/cqh) instead of guessing from one. Size
   * containment is safe here because the row never takes its size from its
   * children anyway — `flex: 1` and the 280px floor above decide it. */
  container-type: size;
}
.ps-stage {
  border-radius: 6px;
  overflow: hidden;
  box-shadow: 0 30px 60px -30px #000;
  background: #14171e;
  display: grid;
  place-items: center;
  color: #4e5563;
  /* Cover Flow: GPU-accelerated 3D transforms. */
  will-change: transform;
}
.ps-stage-cur {
  position: relative;
  flex: none;
  /* Cover Flow: smooth transition for the per-album rotation snap-back. */
  transition: transform 0.2s cubic-bezier(0.2, 0.7, 0.3, 1);
}
.ps-stage-art {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* Exactly one of art/fallback is visible at a time; the stage is a grid, so a
 * stray second child gives it two rows and neither lands where it should.
 * Both get an explicit `[hidden]` rule: `display: block` above already
 * outranks the UA sheet's `[hidden] { display: none }` for the art, and the
 * fallback only survives on the UA rule today — one later `display:` on it
 * would silently reintroduce the same bug. */
.ps-stage-art[hidden],
.ps-stage-fallback[hidden] {
  display: none;
}

/* Stage shapes — the one zone whose geometry varies by kind. */
.ps[data-stage='square'] .ps-stage-cur {
  width: 305px;
  height: 305px;
}
.ps[data-stage='cover'] .ps-stage-cur {
  width: 220px;
  height: 330px; /* 2:3 */
}
.ps[data-stage='video'] .ps-stage-cur {
  /* BASE: the pre-container-query sizing, and it must stay first.
   *
   * A video stage has no intrinsic content — both .ps-stage-art and
   * .ps-stage-fallback are hidden, the picture is a parked iframe — so a rule
   * that gives it neither a width nor a height computes to 0×0, and
   * _parkVideo's `if (!box.width || !box.height) return;` then bails on every
   * repaint tick. The video would never appear at all, on any engine without
   * container queries. This app's floor is iOS 15 (IOS-BUILD-PRD.md); cqw/cqh
   * need Safari 16. `aspect-ratio` shipped in Safari 15, which is why THIS
   * rule is safe there.
   *
   * Height-bound, so on a tall narrow row it is squarer than 16:9 rather than
   * a band — imperfect, and the reason for the override below — but never
   * zero. */
  width: auto;
  height: 100%;
  max-width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: 0;
}
/* ENHANCEMENT: an actual 16:9 band, bound by whichever dimension runs out
 * first.
 *
 * `width: auto; height: 100%` above cannot do that: an explicit height makes
 * `aspect-ratio` inert, so a tall row grew the box tall and `max-width`
 * clamped the width — 390×389 on a phone, 834×657 on a tablet. It only looked
 * right on a laptop, where the row happens to be short and wide enough for
 * height to be the binding dimension.
 *
 * min() against the row's own two dimensions picks the binding one explicitly.
 * Gated on @supports rather than applied unconditionally so the base rule above
 * survives on engines that would drop these declarations at parse time.
 * `container-type: size` on .ps-stage-row needs no guard — it is simply ignored
 * where unsupported. */
@supports (container-type: size) {
  .ps[data-stage='video'] .ps-stage-cur {
    width: min(100cqw, calc(100cqh * 16 / 9));
    height: auto;
  }
}

/* A video stage is the live embed, parked over this rect by the surface that
 * owns it (it cannot be moved into this DOM — re-parenting an iframe reloads
 * it on iOS). Parked means position:fixed against the viewport, so the panel
 * must not scroll underneath it or the video would sit still while everything
 * around it moved. The extras drawer overlays the inner area when open, so
 * the video stays parked and the drawer covers it. */
.ps[data-stage='video'] .ps-inner {
  overflow: hidden;
}

.ps-stage-prev,
.ps-stage-next {
  position: absolute;
  top: 50%;
  width: 200px;
  height: 200px;
  opacity: 0.5;
  transform: translateY(-50%) scale(0.9);
  pointer-events: none;
  /* Cover Flow: neighbours are turned 55° away from the viewer and pushed
     back in Z so they sit behind the current art in depth. The transition
     smooths the snap-back when a drag doesn't commit — during an active
     drag, the JS writes rotateY/translateZ straight onto this element's own
     inline transform every pointermove, no CSS variable involved. */
  transition:
    transform 0.2s cubic-bezier(0.2, 0.7, 0.3, 1),
    opacity 0.2s ease;
}
.ps-stage-prev {
  right: calc(50% + var(--ps-half-stage, 140px));
  margin-right: 0;
  transform-origin: right center;
  /* Facing left: rotated so its right edge comes toward the viewer. */
  transform: translateY(-50%) scale(0.9) rotateY(55deg) translateZ(-80px);
}
.ps-stage-next {
  left: calc(50% + var(--ps-half-stage, 140px));
  margin-left: 0;
  transform-origin: left center;
  /* Facing right: rotated so its left edge comes toward the viewer. */
  transform: translateY(-50%) scale(0.9) rotateY(-55deg) translateZ(-80px);
}
.ps-stage-prev[hidden],
.ps-stage-next[hidden] {
  display: none;
}

.ps-kind-badge {
  position: absolute;
  top: 10px;
  left: 10px;
  z-index: 1;
  padding: 3px 7px;
  border-radius: 999px;
  background: #08090dcc;
  color: var(--ps-kind, #f2a113);
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.ps-track-nav-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: 14px;
  min-height: 22px;
}
/* _paintDots sets `hidden` on this row for a queue with nothing to move to,
 * and `display: flex` above outranks the UA sheet's `[hidden] {display:none}`
 * — so a single-item queue (a radio station, an archive track) showed a ‹ ›
 * pair with no dots between them and nothing on either side of it. Same trap
 * .ps-stage-art[hidden] already carries a rule for; the JS was right and the
 * stylesheet was quietly ignoring it. */
.ps-track-nav-row[hidden] {
  display: none;
}
.ps-dots {
  display: flex;
  justify-content: center;
  gap: 4px;
  min-height: 5px;
}
.ps-dot {
  width: 5px;
  height: 5px;
  border-radius: 999px;
  background: #ffffff26;
  transition:
    width 0.18s,
    background 0.18s;
}
.ps-dot.is-on {
  width: 14px;
  background: var(--ps-kind, #f2a113);
}

/* ── 3 · Title block ──────────────────────────────────────────────────────── */

.ps-titleblock {
  text-align: center;
  margin-top: 18px;
  padding: 0 14px;
}
.ps-title {
  font-family: var(--font-display, inherit);
  font-size: 21px;
  line-height: 1.25;
  letter-spacing: -0.015em;
  /* #813: Title is tappable — opens the More sheet with description. */
  cursor: pointer;
}
/* The title block is a swipe-down-to-close surface for ALL track kinds
 * (_wireStageSwipe wires it with allowFor: () => true). Needs touch-action:
 * none for the same reason the art does: a browser free to start its own
 * vertical pan can win the race against JS before pointerdown ever fires.
 * Taps still pass through to source/author links because setPointerCapture
 * is deferred until after the slop threshold. */
.ps-titleblock {
  touch-action: none;
}
.ps-source {
  margin-top: 4px;
  font-size: 12.5px;
  color: var(--ps-kind, #f2a113);
}
/* A link at rest, not only on hover: the date sits beside it in the same
 * line and the two must never look alike (§8). */
.ps-source-link,
.ps-author-link {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  text-decoration: underline;
  text-decoration-color: #f2a11359;
  text-underline-offset: 3px;
}
@media (hover: hover) {
  .ps-source-link:hover,
  .ps-author-link:hover {
    text-decoration-color: currentColor;
  }
}
.ps-source-date {
  color: #9ca3af;
}
.ps-pos-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: 2px;
}
.ps-pos {
  font-size: 11.5px;
  color: #9ca3af;
}
/* Jump to the previous/next chapter boundary — a different action from
 * rew/ff's fixed-duration skip, so it lives beside the chapter name rather
 * than in the transport row with them. Only chapters (podcast/video) get
 * this; a book's sections switch via the Reader screen's own Previous/Next
 * instead of a seek within the same track (see _paintTitle, player-shell.js). */
.ps-chapter-nav {
  flex: none;
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: #9ca3af;
  cursor: pointer;
}
@media (hover: hover) {
  .ps-chapter-nav:hover:not(:disabled) {
    color: #edeff4;
    background: #ffffff12;
  }
}
.ps-source[hidden],
.ps-pos-row[hidden],
.ps-chapter-nav[hidden] {
  display: none;
}

/* ── 4 · Scrubber ─────────────────────────────────────────────────────────── */

.ps-scrub {
  padding: 0 14px;
  margin-top: 18px;
}
.ps-scrub-track {
  position: relative;
}
.ps-seek {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 4px;
  /* Without this, the app-wide `input { padding: 0.6em 0.75em }` reset (see
   * app.css) applies here too — border-box then has nowhere to put that
   * padding but into the used height, since padding can't go negative, so
   * the rail rendered at ~21px instead of the 4px above. A plain <input> has
   * no visible box to pad in the first place once -webkit-appearance is
   * none; nothing here wants that padding. */
  padding: 0;
  margin: 0;
  border-radius: 999px;
  background: linear-gradient(
    to right,
    var(--ps-kind, #f2a113) 0 var(--pct, 0%),
    #ffffff1a var(--pct, 0%) 100%
  );
  outline: none;
  cursor: pointer;
}
.ps-seek::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 2px;
  border: 0;
  background: var(--ps-kind, #f2a113);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--ps-kind, #f2a113) 15%, transparent);
  cursor: pointer;
}
.ps-seek::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border-radius: 2px;
  border: 0;
  background: var(--ps-kind, #f2a113);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--ps-kind, #f2a113) 15%, transparent);
  cursor: pointer;
}
.ps-seek:disabled {
  cursor: default;
}
.ps-seek:disabled::-webkit-slider-thumb {
  display: none;
}
.ps-seek:disabled::-moz-range-thumb {
  visibility: hidden;
}
.ps-chapter-ticks {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.ps-chapter-ticks i {
  position: absolute;
  top: 0;
  width: 2px;
  height: 4px;
  background: #08090d99;
}
.ps-times {
  display: flex;
  justify-content: space-between;
  margin-top: 8px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: #8a91a0;
}

/* ── 5 · Transport ────────────────────────────────────────────────────────── */

/* Every control here is square. It is the app's signature, and the reason the
 * transport reads as a machine rather than a media widget. Geometry is
 * identical for every kind — a control that can't work for the current source
 * is disabled in place, never removed, so nothing shifts between items. */
.ps-transport {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 0 14px;
  margin-top: 17px;
}
.ps-sq {
  flex: none;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  background: none;
  border: 1px solid #ffffff1f;
  color: #edeff4;
  cursor: pointer;
  transition:
    border-color 0.15s,
    color 0.15s;
}
.ps-sq-sm {
  width: 46px;
  height: 46px;
  border-radius: 5px;
  font-size: 12.5px;
  font-weight: 500;
}
.ps-sq-md {
  width: 54px;
  height: 54px;
  border-radius: 6px;
}
/* #813: Speed and Sleep Timer lose their box outline — only <<, >> and
 * Play/Pause keep the bordered shape in the transport row. */
.ps-sq-bare {
  border: 0;
}
.ps-sq-bare:focus-visible:not(:disabled) {
  border-color: transparent;
}
@media (hover: hover) {
  .ps-sq-bare:hover:not(:disabled) {
    border-color: transparent;
  }
}
.ps-sq:focus-visible:not(:disabled) {
  border-color: var(--ps-kind, #f2a113);
  color: var(--ps-kind, #f2a113);
}
@media (hover: hover) {
  .ps-sq:hover:not(:disabled) {
    border-color: var(--ps-kind, #f2a113);
    color: var(--ps-kind, #f2a113);
  }
}
.ps-sq:disabled {
  opacity: 0.32;
  cursor: default;
}
.ps-sq.is-on {
  border-color: var(--ps-kind, #f2a113);
  color: var(--ps-kind, #f2a113);
}
.ps-sq-num {
  font-size: 9.5px;
  font-weight: 600;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
}
.ps-sleep-count {
  font-size: 9.5px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.ps-play {
  flex: none;
  width: 72px;
  height: 72px;
  border: 0;
  border-radius: 8px;
  background: var(--ps-kind, #f2a113);
  color: #08090d;
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 12px 30px -12px color-mix(in srgb, var(--ps-kind, #f2a113) 50%, transparent);
}
.ps-play:active {
  transform: scale(0.96);
}

/* When the source genuinely can't scrub (a bare Rumble iframe has no API at
 * all), the track stays for layout but stops pretending to be interactive. */
.ps-no-seek .ps-seek {
  opacity: 0.4;
}

/* ── Popovers ─────────────────────────────────────────────────────────────── */

.ps-pop {
  margin: 12px 14px 0;
  padding: 12px;
  border: 1px solid #ffffff17;
  border-radius: 8px;
  background: #14171e;
}
.ps-pop[hidden] {
  display: none;
}
.ps-pop-label {
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #8a91a0;
  margin-bottom: 8px;
}
.ps-pop-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
}
.ps-pop-chip {
  padding: 6px 11px;
  border-radius: 999px;
  border: 1px solid #ffffff17;
  background: none;
  color: #c8cdd8;
  font-size: 11.5px;
  cursor: pointer;
}
@media (hover: hover) {
  .ps-pop-chip:hover {
    border-color: var(--ps-kind, #f2a113);
    color: var(--ps-kind, #f2a113);
  }
}

/* ── The five action keys (§10) ────────────────────────────────────────────
 *
 * One row under the transport, five equal children capped at 62px: Star,
 * Played, Clip, List, More. Star and Played are stateful and show it — the
 * label changes as well as the fill, so nothing depends on reading a colour.
 */

.ps-chips {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-top: 19px;
  padding: 0 14px;
}
.ps-key {
  flex: 1;
  max-width: 62px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: 8px 0;
  border-radius: 7px;
  border: 0;
  background: none;
  color: #c8cdd8;
  cursor: pointer;
  transition:
    color 0.15s,
    background 0.15s;
}
/* #835: Labels are icon-only now — kept in aria-label for screen readers. */
.ps-key-label {
  display: none;
}
/* #813: No border on action keys — hover just changes color, and the on-state
 * uses a subtle background tint instead of a border. */
@media (hover: hover) {
  .ps-key:not(.is-on):hover {
    color: var(--ps-kind, #f2a113);
  }
}
.ps-key.is-on {
  background: #f2a11317;
  color: #f2a113;
}
/* Still a pill wherever one is genuinely a chip rather than a key — the
 * about panel's own row keeps its shape. */
.ps-chip {
  padding: 7px 11px;
  border-radius: 999px;
  border: 1px solid #ffffff17;
  background: none;
  color: #c8cdd8;
  font-size: 11.5px;
  cursor: pointer;
}
@media (hover: hover) {
  .ps-chip:hover {
    border-color: var(--ps-kind, #f2a113);
    color: var(--ps-kind, #f2a113);
  }
}

/* ── 6 · Extras drawer ─────────────────────────────────────────────────────── */

/* The extras drawer has two states:
 *
 * Closed (default): a thin tab strip docked at the bottom of the player.
 * The artwork, title, scrubber and transport above it are all full-size.
 * The body is hidden — only the tab row and expand chevron show.
 *
 * Open (.ps-extras-open): the body overlays everything above it, covering
 * the art and title with a scrollable panel. The tab strip stays visible
 * at the top of the overlay as the handle. The art never shrinks — it is
 * simply covered, which is what "expand over the artwork" always meant.
 *
 * The drawer is position:absolute over .ps-inner, which is position:relative.
 * When closed, only the head (tab strip) shows as a strip at the bottom;
 * when open, the full drawer fills the inner area. */
.ps-extras {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 10;
  margin: 0 14px;
  border-top: 1px solid #ffffff14;
  padding-top: 12px;
  display: flex;
  flex-direction: column;
  background: #0d0f15;
  border-radius: 10px 10px 0 0;
  /* Closed: only the head strip shows. The body is hidden so the artwork
   * above is fully visible and never shrinks. */
  max-height: 56px;
  overflow: hidden;
  transition: max-height 0.22s ease;
}
.ps-no-extras .ps-extras {
  display: none;
}
/* Open: the drawer overlays the full inner area. max-height lifts so the
 * body can scroll, and a backdrop dims what's behind it. */
.ps.ps-extras-open .ps-extras {
  max-height: 100%;
  /* Fill from the top of .ps-inner, not just the bottom — the overlay
   * covers the art and title, not just the strip. */
  top: 0;
  bottom: 0;
  margin: 0;
  border-radius: 0;
  border-top: 0;
  background: #0d0f15;
}
/* Tabs, and the control that opens and closes the drawer. The tabs scroll
 * horizontally rather than squeezing, so the grow button keeps its width
 * whatever the tab set is. */
.ps-extras-head {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  flex: none;
  padding: 0 4px 8px;
}
.ps-extras-tabs {
  display: flex;
  gap: 16px;
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.ps-extras-tabs::-webkit-scrollbar {
  display: none;
}
.ps-extras-grow {
  flex: none;
  width: 32px;
  height: 32px;
  margin-top: -6px;
  display: grid;
  place-items: center;
  background: none;
  border: 0;
  border-radius: 8px;
  color: #8b93a3;
  cursor: pointer;
  transition:
    color 0.15s,
    transform 0.2s;
}
@media (hover: hover) {
  .ps-extras-grow:hover {
    color: #edeff4;
  }
}
.ps-extras-open .ps-extras-grow {
  transform: rotate(180deg);
  color: #edeff4;
}
.ps-tab {
  background: none;
  border: 0;
  padding: 0 0 8px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 500;
  color: #7a8193;
  border-bottom: 2px solid transparent;
  cursor: pointer;
}
.ps-tab.is-on {
  color: #edeff4;
  border-bottom-color: var(--ps-kind, #f2a113);
}
/* The body is hidden when the drawer is closed and scrolls when open. */
.ps-extras-body {
  margin-top: 4px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* No env(safe-area-inset-bottom) term here: .ps's own bottom inset already
   * stops above that safe area (see .ps's --pk-dock-bottom note) rather than
   * reaching into it, so double-counting it here would just pad the last row
   * away from the tab bar for no reason. */
  padding: 0 4px 22px;
}
/* Closed: the body is hidden so the strip is just the tab row. */
.ps:not(.ps-extras-open) .ps-extras-body {
  display: none;
}
.ps-row {
  display: flex;
  align-items: baseline;
  gap: 11px;
  width: 100%;
  padding: 10px 0;
  border: 0;
  border-top: 1px solid #ffffff0f;
  background: none;
  color: #edeff4;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.ps-row-lead {
  flex: none;
  min-width: 42px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: #8a91a0;
}
.ps-row-text {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  line-height: 1.5;
}
.ps-row.is-on .ps-row-lead,
.ps-row.is-on .ps-row-text {
  color: var(--ps-kind, #f2a113);
}
.ps-row-art {
  flex: none;
  width: 36px;
  height: 36px;
  border-radius: 4px;
  object-fit: cover;
}
.ps-row-wrap {
  display: flex;
  align-items: center;
  gap: 4px;
  border-top: 1px solid #ffffff0f;
}
.ps-row-wrap .ps-row {
  border-top: 0;
  flex: 1;
  min-width: 0;
}
.ps-row-wrap.is-on .ps-row-lead,
.ps-row-wrap.is-on .ps-row-text {
  color: var(--ps-kind, #f2a113);
}
.ps-row-act {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: #8a91a0;
  cursor: pointer;
  transition:
    color 0.15s,
    background 0.15s;
}
@media (hover: hover) {
  .ps-row-act:hover {
    color: #edeff4;
    background: #ffffff0f;
  }
}
.ps-row-act:disabled {
  opacity: 0.35;
  cursor: default;
}

/* Read along. Prose rather than rows: no lead column, no rules between, and a
 * measure that stays readable — this is the one tab you look at for minutes
 * rather than glance at. Dimmed by default so the paragraph being spoken is
 * findable at a glance without the rest becoming unreadable if you want to
 * read ahead. */
.ps-read-para {
  margin: 0 0 12px;
  /* The gutter the active paragraph's bar sits in, reserved on EVERY
   * paragraph. The active one used to claim it with `margin-left: -12px`,
   * which put the bar outside the extras panel's overflow clip — so the bar
   * never appeared at all, and the two pixels of text that had moved past the
   * clip edge were shaved off the first glyph of every line. Reserving it
   * here costs 12px of measure and means nothing shifts as the highlight
   * moves down the column. */
  padding-left: 12px;
  font-size: 14px;
  line-height: 1.65;
  color: #a8afbd;
  transition: color 0.2s;
}
.ps-read-para.is-on {
  color: #edeff4;
  /* The bar carries the highlight rather than a background fill: a block of
   * colour behind running text is what makes long-form unreadable, and the
   * kind colour is already this player's way of saying "here". */
  box-shadow: inset 2px 0 var(--ps-kind, #f2a113);
}
@media (prefers-reduced-motion: reduce) {
  .ps-read-para {
    transition: none;
  }
}

/* About's meta lines are facts, not controls — same two-column rhythm as the
 * rest of the strip, but nothing about them is tappable. */
.ps-row.is-static {
  cursor: default;
}
/* A transcript the publisher shipped as prose rather than timed cues: still
 * worth reading, but there is no timestamp to send you to. */
.ps-row:disabled {
  cursor: default;
}

/* Splits a list that has real groups in it — the two narration providers,
 * which are a genuine choice (server-synthesized vs. downloaded to the
 * device) rather than one long roster. */
.ps-group-label {
  padding: 14px 0 2px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: #7a8193;
}
.ps-group-label:first-child {
  padding-top: 4px;
}

.ps-about-desc {
  margin: 12px 0 0;
  font-size: 13px;
  line-height: 1.55;
  color: #c8cdd8;
  white-space: pre-wrap;
}
.ps-about-acts {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  margin-top: 14px;
}
.ps-about-acts .ps-chip.is-on {
  border-color: var(--ps-kind, #f2a113);
  color: var(--ps-kind, #f2a113);
}

/* ── The turntable skin ───────────────────────────────────────────────────
 *
 * The machine sits over the standard chrome rather than replacing the shell:
 * open(), close(), the swipe-to-close and the 250ms poll all keep working, and
 * none of them knows the skin exists. Hiding .ps-inner rather than not
 * rendering it means switching back is a class change, not a rebuild.
 *
 * The 1144x560 drawing is never re-laid-out, only scaled (TURNTABLE.md 0), so
 * it is centred and left to its own dimensions here. */
/* `.ps.ps-skinned`, not `.ps-skinned` — one extra class, kept deliberately.
 *
 * The video block up the file used to carry
 * `.ps[data-stage='video'] .ps-inner { display: flex }`, which outweighs a
 * bare `.ps-skinned .ps-inner` — three compound selectors to two. So the
 * standard chrome went on rendering underneath the machine for exactly one
 * stage: video. That is the stage where the embed then parked itself over the
 * shell's full-width 16:9 stage box and buried the turntable.
 *
 * The extras-drawer rework has since dropped that `display: flex`, so the
 * weights no longer collide today. The extra class stays anyway: this rule
 * has to beat whatever any stage decides `.ps-inner`'s display should be, and
 * the last time that was left to chance it cost a buried machine and a
 * check-run to find. */
.ps.ps-skinned .ps-inner {
  display: none;
}
/* Skinned, the shell takes the bottom of the screen too.
 *
 * `.ps` normally stops short of it — `bottom: calc(var(--pk-dock-bottom) +
 * env(safe-area-inset-bottom))` — reserving whatever docks along the bottom
 * of the app (the tab bar, a mini player) plus the home indicator. That is
 * right for the standard player, which is a panel with the app still around
 * it. The machine is not a panel: it is the whole presentation, and the
 * reserve was the last of the dead space around it.
 *
 * Measured at the reporting device's 932×430 with its 20pt home-indicator
 * inset. Reclaiming it takes the machine from 838×410 to 878×430 — scale
 * 0.732 to 0.768 — and, because the fit is height-bound, the side bands fall
 * from 47pt to 27pt at the same time. The remaining 27pt each side is
 * irreducible: the drawing is 2.043:1 against a 2.167:1 screen, so the
 * leftover width has nowhere to go that does not stretch it.
 *
 * The cost, stated plainly: the bottom row of the machine — the transport
 * keys and RECORD — now sits inside the home indicator's gesture strip, and
 * whatever was docked under the shell is covered for as long as the machine
 * is up. Rotating to portrait takes the machine down and brings both back. */
.ps.ps-skinned {
  bottom: 0;
}
.ps-skin {
  position: absolute;
  /* The whole viewport, deliberately — including whatever the notch covers.
   *
   * This used to reserve the safe area: `left/right: max(12px,
   * env(safe-area-inset-left/right))`, to keep the machine clear of the notch
   * in landscape. It worked, and the cost was worse than the problem. iOS
   * reports 59pt on BOTH sides in landscape (measured on the reporting
   * device, via the placement diagnostic), so the machine lost 118 of 932
   * points — 13% of the width — and scaled down to suit, leaving a visible
   * band of dead space down each side.
   *
   * The owner's call, made looking at it: expand to fill, and let the notch
   * cover part of the chassis. That is the right trade for this particular
   * drawing. The outer edge of the machine is bezel — the deck's controls,
   * the monitor and the receiver all sit well inboard of it — so what the
   * notch actually eats is a corner of the casing, not a control.
   *
   * INSET, not padding, and that distinction stays load-bearing whatever the
   * numbers are. The machine is centred by `left: 50%` on an absolutely
   * positioned child, and percentages there resolve against the PADDING box —
   * so padding here would be inside the box being halved and would be
   * silently ignored, which is exactly how the original notch reserve failed
   * before it was rewritten as an inset. _fitSkin measures this same box, so
   * the scale and the centring agree about what "available" means.
   *
   * Kept as four explicit longhands rather than `inset: 0` so that anything
   * reserved here later is written in the one place that already carries the
   * reasoning. */
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  display: grid;
  place-items: center;
  /* hidden, not auto. The machine is scaled to fit now, so there is nothing
   * to scroll to — and a scroll container here let a phone pan around a
   * machine that was too big instead of showing all of it. */
  overflow: hidden;
}
/* The extras drawer, opened over the machine by its MORE key.
 *
 * `.ps.ps-skinned .ps-inner { display: none }` above is what takes the
 * standard chrome away; this is the one documented exception to it. Only the
 * drawer comes back — every other child of .ps-inner is a thing the machine
 * has already replaced, and showing the transport twice is worse than not
 * showing it at all. */
.ps.ps-skinned.ps-skin-extras .ps-inner {
  display: flex;
  padding-bottom: 0;
  /* Above .ps-skin, which is appended AFTER .ps-inner and so paints over it
   * by document order. Without this the scrim below went under the machine
   * and dimmed nothing — the panel still showed, because .ps-extras carries
   * its own z-index, which is exactly the kind of half-working that hides the
   * problem. */
  z-index: 2;
  /* A scrim rather than nothing. The panel below is opaque and sits over a
   * machine with two animating needles and a turning platter, and without
   * this the eye keeps being pulled back to the movement around it. It is
   * also the dismiss target — tapping off the panel closes it, which is the
   * gesture every sheet in this app already answers to. */
  background: #05060ac2;
}
.ps.ps-skinned.ps-skin-extras .ps-inner > *:not(.ps-extras) {
  display: none;
}
/* A panel over the machine, not a screen instead of it. Full-bleed here read
 * as having navigated somewhere else; leaving the chassis showing round the
 * edges is what makes MORE feel like a drawer in the thing you are looking
 * at. Outranks .ps.ps-extras-open's own full-bleed geometry on class count,
 * which is deliberate — that rule is right everywhere the machine is not. */
.ps.ps-skinned.ps-skin-extras .ps-extras {
  pointer-events: auto;
  top: 6%;
  bottom: 6%;
  left: 50%;
  right: auto;
  width: min(720px, 86%);
  margin: 0;
  transform: translateX(-50%);
  border: 1px solid #ffffff1f;
  border-radius: 12px;
  box-shadow: 0 26px 64px -22px #000;
}
/* The machine is a fixed 1144×560 drawing and scales with transform only —
 * so one transform on the stage carries the whole thing, and every gesture,
 * hit target and animation inside keeps working in the machine's own
 * coordinate space. --bp-scale is set by _fitSkin from the holder's measured
 * box; the fallback of 1 is what a window big enough gets anyway. */
.ps-skin .bp-stage {
  /* Centred by absolute positioning, not by the grid.
   *
   * place-items: center looks like it should do this, and does not: the
   * machine is laid out at its full 1144×560 before the transform, so it is
   * an item bigger than its container, and a scroll container (which
   * overflow: hidden makes this) clamps overflow alignment to start. The
   * result was a machine correctly scaled and then anchored to the top-left,
   * hanging 114px off the right of a phone. */
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%) scale(var(--bp-scale, 1));
  transform-origin: center center;
}
@media (prefers-reduced-motion: no-preference) {
  .ps-skin .bp-stage {
    transition: transform 0.18s ease;
  }
}
