/* 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;
  inset: 0;
  /* 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);
  padding-bottom: calc(22px + env(safe-area-inset-bottom, 0px));
}
.ps-inner > * {
  flex: none;
}

/* ── 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;
}
.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;
  /* Was pan-y (horizontal drags belong to the stage; vertical scrolls the
   * player natively). Downward drags now close the shell (_wireStageSwipe,
   * player-shell.js) — under pan-y the browser is explicitly told it may
   * start scrolling on a vertical touch without waiting for JS, so a
   * downward drag could win that race before the close gesture ever saw
   * it. none hands the whole gesture to JS, which replicates the upward
   * scroll it used to get for free — see the pointermove handler. */
  touch-action: none;
  padding: 8px 0 0;
  /* Cover Flow: the 3D perspective origin for the stage row's children.
     1200px gives a subtle but readable recede — enough rotateY on the
     neighbours to read as "turned away" without distorting the artwork. */
  perspective: 1200px;
  perspective-origin: center center;
}
.ps-stage-row {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  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. */
}
/* Room for the panel comes out of the artwork, and only when there is a panel.
 *
 * Measured rather than guessed. At 390x844 the fixed part of the player packs
 * the window exactly — 60 + 317 + 47 + 26 + 72 + 52 plus margins — so the
 * extras panel was landing at 114px, about two transcript rows. Cover art is
 * the largest single item and the only one whose job is decorative, so it is
 * what gives. An episode with nothing to show in the strip keeps the full
 * 280px, because there is nothing to trade it for.
 *
 * min-height on .ps-stage-row does not do this: the art's size is set on
 * .ps-stage-cur in px per stage kind, so the row was 317px tall no matter what
 * floor the row itself had. Learned by changing the floor and measuring no
 * difference at all. */
.ps:not(.ps-no-extras) .ps-stage-row {
  min-height: 0;
}
.ps-stage-row {
  /* Preserve 3D so children's rotateY/translateZ render in perspective. */
  transform-style: preserve-3d;
}
.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: 280px;
  height: 280px;
}
.ps[data-stage='cover'] .ps-stage-cur {
  width: 206px;
  height: 309px; /* 2:3 */
}
/* vh, not a fixed smaller number: the amount that has to be given up depends
 * on the window, and a phone in landscape or a small laptop needs far more of
 * it than a tall phone does. Capped at the full size so a big window never
 * grows the art past its designed dimensions. */
.ps:not(.ps-no-extras)[data-stage='square'] .ps-stage-cur {
  width: min(280px, var(--ps-art-cap, 24vh));
  height: min(280px, var(--ps-art-cap, 24vh));
}
.ps:not(.ps-no-extras)[data-stage='cover'] .ps-stage-cur {
  height: min(309px, var(--ps-art-cap-tall, 26vh));
  width: auto;
  aspect-ratio: 2 / 3;
}
/* Expanded: the panel takes the screen and the art becomes a thumbnail that
 * still says what is playing. A cap rather than `display: none` on purpose —
 * losing the artwork entirely makes the transport above it read as a strip of
 * unlabelled buttons, and there is nowhere else on this screen that shows
 * what you are listening to. */
.ps.ps-extras-max {
  --ps-art-cap: 68px;
  --ps-art-cap-tall: 92px;
}
.ps.ps-extras-max[data-stage='video'] .ps-stage-cur {
  width: 46%;
}
/* Neighbouring artwork is a browsing affordance, and browsing is not what the
 * expanded state is for. Hidden rather than shrunk so nothing competes with
 * the panel for the eye. */
.ps.ps-extras-max .ps-stage-prev,
.ps.ps-extras-max .ps-stage-next {
  visibility: hidden;
}
/* Same reasoning, and these two are worth another 26px: the dots and their
 * arrows are the queue-browsing affordance, and the Queue tab is right there
 * in the panel that just took the screen. */
.ps.ps-extras-max .ps-track-nav-row {
  display: none;
}
.ps.ps-extras-max .ps-extras {
  margin-top: 10px;
}
.ps.ps-extras-max .ps-stage-cur {
  transition:
    width 0.18s ease,
    height 0.18s ease;
}
@media (prefers-reduced-motion: reduce) {
  .ps.ps-extras-max .ps-stage-cur,
  .ps-extras-grow {
    transition: none;
  }
}
.ps[data-stage='video'] .ps-stage-cur {
  width: 100%;
  aspect-ratio: 16 / 9;
  height: auto;
  border-radius: 0;
}

/* 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. Only the extras strip scrolls; the six zones above it are
 * already sized to fit. */
.ps[data-stage='video'] .ps-inner {
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.ps[data-stage='video'] .ps-extras {
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.ps[data-stage='video'] .ps-extras-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

.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 and the tilt during a
     drag (the JS updates --ps-drag-tilt in real time). */
  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: 10px;
  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: 10px;
  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;
}
/* Expanded, the artwork is a 68px thumbnail and the badge is wider than it —
 * it hung six pixels off the right edge, floating over the backdrop. There is
 * nothing useful a medium name can do at that size, and the title block
 * directly below still says what is playing. */
.ps-extras-max .ps-kind-badge {
  display: none;
}

.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;
  font-weight: 500;
  letter-spacing: -0.015em;
}
/* Video's stand-in for the art's swipe surface (see _wireDragSurface) —
 * needs the same touch-action: none the art has, for the same reason: a
 * browser free to start its own vertical pan on this element can win the
 * race against JS before pointerdown even fires.
 *
 * Scoped to the video stage, because that is the only stage where anything
 * takes the gesture over (#504). _wireDragSurface binds .ps-title with
 * allowFor: (isVideo) => isVideo, so on a podcast, a book or an article its
 * pointerdown returns immediately without capturing — while an unconditional
 * touch-action: none had already told the browser not to scroll. The result
 * was a dead strip across the title on every non-video player: drag it and
 * nothing moves, neither the shell nor the page. */
.ps[data-stage='video'] .ps-title {
  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;
}
.ps-source-link:hover,
.ps-author-link:hover {
  text-decoration-color: currentColor;
}
.ps-source-date {
  color: #8a91a0;
}
.ps-pos-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-top: 2px;
}
.ps-pos {
  font-size: 11.5px;
  color: #8a91a0;
}
/* 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: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: #8a91a0;
  cursor: pointer;
}
.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: 22px;
}
.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;
}
.ps-sq:hover:not(:disabled),
.ps-sq:focus-visible: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;
}
.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: 18px;
  padding: 0 14px;
}
.ps-key {
  flex: 1;
  max-width: 62px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  padding: 9px 0;
  border-radius: 7px;
  border: 1px solid #ffffff17;
  background: none;
  color: #c8cdd8;
  cursor: pointer;
  transition:
    border-color 0.15s,
    color 0.15s,
    background 0.15s;
}
.ps-key-label {
  font-size: 10px;
  font-weight: 500;
  line-height: 1;
}
/* Hover must not impersonate the on-state: it paints the same amber border and
 * text that `.is-on` does, so an off-but-just-touched key looked identical to a
 * key that was on. `:not(.is-on)` keeps the two distinguishable, and the
 * hover-capable media query stops a touch device latching the hover style after
 * a tap (which is what made the difference invisible on a phone specifically). */
@media (hover: hover) {
  .ps-key:not(.is-on):hover {
    border-color: var(--ps-kind, #f2a113);
    color: var(--ps-kind, #f2a113);
  }
}
.ps-key.is-on {
  background: #f2a11317;
  border-color: #f2a11340;
  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;
}
.ps-chip:hover {
  border-color: var(--ps-kind, #f2a113);
  color: var(--ps-kind, #f2a113);
}

/* ── 6 · Extras strip ─────────────────────────────────────────────────────── */

/* Takes the height the fixed part above it does not want, and gives it to the
 * body — so the panel ends where the screen does instead of at an arbitrary
 * 46vh, and the player above it never has to move. */
.ps-extras {
  margin: 22px 14px 0;
  border-top: 1px solid #ffffff14;
  padding-top: 12px;
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}
.ps-no-extras .ps-extras {
  display: none;
}
/* Tabs, and the control that trades the artwork for the panel. 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;
}
.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;
}
.ps-extras-grow:hover {
  color: #edeff4;
}
.ps-extras-max .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);
}
/* Its own scroll region, which the code has always assumed it was.
 *
 * _paintExtrasBody's comment describes the strip as "a scrollable list" and
 * takes care not to rewrite it on every poll tick because that "would reset
 * the scroll position". It never had one: with no overflow of its own, the
 * nearest scrollable ancestor is .ps-inner — the whole player. So the
 * transcript's own scrollIntoView, following the spoken cue, scrolled the
 * artwork, the title and the transport clean off the top of the screen, with
 * no gesture from the user at all. Read along followed the same pattern and
 * had the same fault.
 *
 * Bounded so the transport stays put, and overscroll contained so reaching
 * the end of the transcript does not hand the scroll back to the player and
 * start the same thing by hand. */
.ps-extras-body {
  margin-top: 4px;
  /* No max-height: the flex parent above supplies the bound now, and a fixed
   * one would either waste space or reintroduce the overflow that scrolled the
   * player. min-height: 0 is what actually lets a flex child shrink enough to
   * scroll at all. */
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
.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);
}
/* 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-skinned .ps-inner {
  display: none;
}
.ps-skin {
  position: absolute;
  inset: 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;
  padding: 12px;
  /* The notch eats a real bite out of the width in landscape, which is the
   * orientation this runs in on a phone. */
  padding-left: max(12px, env(safe-area-inset-left, 0px));
  padding-right: max(12px, env(safe-area-inset-right, 0px));
}
/* 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;
  }
}
