/* ============================================================
   EON player.

   Reads --cta / --cta-ink from css/tokens.css and the library's
   surface tokens from app.css. No hex is written here except the
   player's own black ground and the scrim's translucency, which are
   not brand colours — a video frame sits on black in both themes.

   The accent marks INTERACTION and STATE: the played portion of the
   scrub bar, the knob, focus rings, the spinner, the pressed state
   of a control. Everything else stays neutral, so the accent keeps
   meaning "this is actionable" rather than becoming decoration.

   The audience skews older, so the legibility floor from the rest of
   the repo holds: real 44px targets (--target-min), always-visible
   focus, nothing under 13px in the controls.
   ============================================================ */

.eonp {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  background: #000;
  overflow: hidden;
  border-radius: var(--radius, 12px);
  outline: none;
}
.eonp:focus-visible {
  outline: 3px solid var(--cta);
  outline-offset: 2px;
}
.eonp.is-full {
  border-radius: 0;
  width: 100vw;
  height: 100vh;
  aspect-ratio: auto;
}

.eonp-frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* Transparent, above the frame, swallows every pointer event before it
   can reach YouTube's own UI. Without this, a click lands on the embed
   and the viewer gets the title bar, the channel link and the share
   sheet — all the things controls=0 does not remove. */
.eonp-shield {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: pointer;
  background: transparent;
}

/* ── the unstarted state ──────────────────────────────────────────
   The only moment YouTube's own UI is allowed through, and the reason
   is sound. A shield over the whole frame stops the viewer's click ever
   reaching the player, and that click is the gesture browsers demand
   before they will permit audio — proved on production, where a bare
   iframe played with sound and the covered one would not, at any number
   of clicks.

   So before the video has ever started, the centre is left clickable and
   this cap covers the top band instead: the title, the channel avatar,
   share, watch-later. What stays visible is YouTube's plain play button,
   which carries no title, no channel, no link and no video id. One click
   on it starts the video WITH SOUND, the state leaves idle, and the full
   cloak closes for every state after this one.

   TWO bands, not one. The first version masked only the top, and the
   deployed result showed YouTube parks a "Watch on YouTube" button in the
   BOTTOM-right of the same overlay — the exact link this whole change
   exists to delete, and a live one: clicking it opens the video on
   youtube.com with the id in the URL. Both bands are opaque AND take
   pointer events, so it cannot be seen or reached. What is left is a
   horizontal strip through the middle, which is where the play button
   is. */
.eonp-cap {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: none;
  pointer-events: none;          /* only the two bands below catch clicks */
  /* Toggled by display, so the bands arrived and left in one frame — two
     black bars snapping across a quarter of the picture on every pause
     and every play, which reads as part of the same flash. They fade
     now, on the scrim's own timing. Fading IN is safe: YouTube's chrome
     takes longer than this to appear than the bands take to cover it. */
  opacity: 0;
  transition: opacity var(--dur-panel, 200ms) var(--ease-move, ease);
}
.eonp-cap::before,
.eonp-cap::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  /* Enough to cover YouTube's gradient header and its bottom row, and no
     more — every pixel here is picture the viewer does not get to see.
     22% was sized against a box that turned out to be far taller than the
     video inside it; against a true 16/9 frame this lands just under the
     title. */
  height: max(48px, 14%);
  background: #000;
  pointer-events: auto;
}
.eonp-cap::before { top: 0; }
.eonp-cap::after { bottom: 0; }
/* Paused shows the bands too — that is when YouTube paints its title and
   its "Watch on YouTube" button — but the middle stays clear. */
.eonp-idle .eonp-cap,
.eonp-gate .eonp-cap,
.eonp-paused .eonp-cap { display: block; opacity: 1; }
/* Let the click through to the player underneath. Idle only: once the
   video is running, every pointer event is ours again. */
.eonp-idle .eonp-shield { pointer-events: none; }
.eonp-scrim.is-idle { background: transparent; }

/* ── the centre badge ─────────────────────────────────────────────
   Pausing used to black the whole frame out, which hid the thing the
   viewer paused to look at. This disc sits where YouTube draws its own
   play overlay, so it covers that and nothing else: the picture stays
   up, and the state is still unmistakable. */
.eonp-badge {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 76px;
  height: 76px;
  margin: -38px 0 0 -38px;
  z-index: 3;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.72);
  display: none;
  place-items: center;
  pointer-events: none;
}
.eonp-badge::before {
  content: "";
  width: 34px;
  height: 34px;
  background: #fff;
  -webkit-mask: var(--ic-badge) center / contain no-repeat;
  mask: var(--ic-badge) center / contain no-repeat;
}
.eonp-paused .eonp-badge {
  display: grid;
  --ic-badge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 5h3.5v14H7zm6.5 0H17v14h-3.5z'/%3E%3C/svg%3E");
}
/* At the cutoff the frame stays up, so the badge is what says why it
   stopped. A play glyph would invite a press that is refused; a padlock
   states the actual situation, and sits under the page's own gate. */
.eonp-gate .eonp-badge {
  display: grid;
  --ic-badge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2a5 5 0 0 0-5 5v3H5.5A1.5 1.5 0 0 0 4 11.5v9A1.5 1.5 0 0 0 5.5 22h13a1.5 1.5 0 0 0 1.5-1.5v-9a1.5 1.5 0 0 0-1.5-1.5H17V7a5 5 0 0 0-5-5zm0 2a3 3 0 0 1 3 3v3H9V7a3 3 0 0 1 3-3z'/%3E%3C/svg%3E");
}
.eonp-ended .eonp-badge {
  display: grid;
  --ic-badge: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5V1L7 6l5 5V7a6 6 0 1 1-6 6H4a8 8 0 1 0 8-8z'/%3E%3C/svg%3E");
}

/* Hold-to-boost readout. Only while the gesture is active. */
.eonp-rate {
  position: absolute;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 3;
  display: none;
  padding: 6px 12px;
  border-radius: 999px;
  background: var(--cta);
  color: var(--cta-ink);
  font: 700 13px/1 var(--font, Roboto, Arial, sans-serif);
  pointer-events: none;
}
.eonp-boost .eonp-rate { display: block; }
/* Our own controls would be a second, non-working play button next to
   YouTube's working one. One obvious action instead. */
.eonp-idle .eonp-bar { display: none; }

/* Opaque, above the shield. Covers the frame whenever the video is not
   actively painting — idle, buffering, paused, ended, errored — which is
   precisely when YouTube paints its own chrome and its end-screen grid
   of related videos over the top. */
.eonp-scrim {
  position: absolute;
  inset: 0;
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 24px;
  text-align: center;
  background: #000;
  color: var(--text, #f1f1f1);
  font: 15px/1.5 var(--font, Roboto, Arial, sans-serif);
  transition: opacity var(--dur-panel, 200ms) var(--ease-move, ease);
  /* Purely visual. It sits above the shield, so without this a click on
     a paused player would land here and the video would never resume —
     the shield underneath owns every pointer event. */
  pointer-events: none;
}
/* Transparent as well as invisible, and the background matters here
   precisely BECAUSE the opacity is animated.

   While paused the scrim is see-through — that comes from
   .eonp-showvideo, which _setScrim removes in the same breath as it sets
   .is-hidden. Removing it snapped the background back to solid #000
   instantly while the opacity was still on its way down over 200ms, so
   pressing play turned the picture black and THEN faded the black away.
   That is the flash.

   Being explicit here means the fade-out has nothing to fade FROM: it is
   already transparent whichever state it is leaving. */
.eonp-scrim.is-hidden { opacity: 0; background: transparent; }
/* The cold start wears the video's own artwork under a dark wash, with
   the mark and its status line over it — the wait shows what is coming
   rather than a black box. --eonp-poster is set inline by player.js from
   the md rendition the cards already loaded. Only while loading: the
   showvideo rule below keeps a mid-video re-buffer on the real picture. */
.eonp-scrim.is-loading {
  background:
    linear-gradient(rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.55)),
    var(--eonp-poster, none) center / cover #000;
}
/* Paused and idle keep the picture: the cap bands and the centre badge
   do the hiding, so the scrim itself gets out of the way. */
.eonp-showvideo .eonp-scrim { background: transparent; }
/* Every state is opaque, including paused and ended.
   These were rgba(0,0,0,.82) so the viewer kept their place in the frame,
   and on production that is exactly what YouTube's own play overlay used:
   the big play button sat plainly visible through the paused scrim. The
   whole reason the scrim exists is that paused and ended are when YouTube
   draws its chrome, so a scrim you can see through at those moments is no
   scrim at all. Keeping your place is not worth showing their button. */

.eonp-msg {
  max-width: 34ch;
  font-size: 15px;
}
.eonp-scrim.is-loading .eonp-msg,
.eonp-scrim.is-paused .eonp-msg,
.eonp-scrim.is-ended .eonp-msg,
.eonp-scrim.is-gate .eonp-msg { display: none; }

/* …except when the gate lands with NO frame behind it. After a preview
   the retained frame is the argument and the padlock is enough, but a
   403 at 0:00 has nothing to retain: that case rendered as a solid black
   rectangle with a padlock and not one word on it. Only that case gets
   its caption back — the normal cutoff is untouched. */
.eonp:not(.eonp-showvideo) .eonp-scrim.is-gate .eonp-msg { display: block; }

/* The Eon mark and its status line (styled by the inline CSS in
   index.html) show only while loading — every other state, the gate
   included, hides them. */
.eonp-scrim:not(.is-loading) .eon-load,
.eonp-scrim:not(.is-loading) .eon-status { display: none; }
/* …and never over a picture. A loading scrim that shows the frame through
   (a re-buffer after the first start, showvideo) draws NOTHING of its
   own: on iOS the embed parks in exactly this state after Apple's player
   is dismissed and the mark ran over YouTube's play overlay indefinitely.
   The cold start — opaque scrim, poster under the wash — keeps the mark
   and its status line; that is the only wait the mark is for. */
.eonp-showvideo .eonp-scrim .eon-load,
.eonp-showvideo .eonp-scrim .eon-status { display: none; }

/* ── controls ─────────────────────────────────────────────── */

.eonp-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.88), rgba(0, 0, 0, 0));
  opacity: 0;
  transition: opacity var(--dur-panel, 200ms) var(--ease-move, ease);
}
/* Shown on hover, on keyboard focus anywhere inside, and whenever the
   video is not playing — so a paused player is never a dead rectangle.
   The state classes come from the player rather than :has(), which would
   quietly drop the controls entirely on Safari below 15.4. */
.eonp:hover .eonp-bar,
.eonp:focus-within .eonp-bar,
.eonp-paused .eonp-bar,
.eonp-idle .eonp-bar,
/* The gate too. `_setScrim` stopped adding `is-hidden` at the cutoff so
   the bar could stay up and show the hatched zone — but nothing here ever
   raised its opacity, so it was merely displayed and invisible. Two halves
   of one change, and only one of them had landed. */
.eonp-gate .eonp-bar,
.eonp-ended .eonp-bar { opacity: 1; }
.eonp-bar.is-hidden { display: none; }

.eonp-btn {
  flex: 0 0 auto;
  width: var(--target-min, 44px);
  height: var(--target-min, 44px);
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: #fff;
  cursor: pointer;
  padding: 0;
}
.eonp-btn:hover { background: rgba(255, 255, 255, 0.14); }
.eonp-btn:focus-visible {
  outline: 3px solid var(--cta);
  outline-offset: -2px;
}
.eonp-btn::before {
  content: "";
  width: 22px;
  height: 22px;
  background: currentColor;
  -webkit-mask: var(--ic) center / contain no-repeat;
  mask: var(--ic) center / contain no-repeat;
}

.eonp-play { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 5.5v13l11-6.5z'/%3E%3C/svg%3E"); }
.eonp-play.is-playing { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 5h3.5v14H7zm6.5 0H17v14h-3.5z'/%3E%3C/svg%3E"); }
.eonp-mute { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 9v6h4l5 4V5L8 9H4zm12.5 3a4.5 4.5 0 0 0-2.5-4v8a4.5 4.5 0 0 0 2.5-4z'/%3E%3C/svg%3E"); }
.eonp-mute.is-muted { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M4 9v6h4l5 4V5L8 9H4zm15 .4L17.6 8 15 10.6 12.4 8 11 9.4l2.6 2.6L11 14.6 12.4 16 15 13.4 17.6 16 19 14.6 16.4 12 19 9.4z'/%3E%3C/svg%3E"); }
.eonp-full { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 5h5v2H7v3H5V5zm9 0h5v5h-2V7h-3V5zM5 14h2v3h3v2H5v-5zm12 0h2v5h-5v-2h3v-3z'/%3E%3C/svg%3E"); }
/* Skip ±10s. The same arrows the keyboard uses and the same amount, so
   the two ways of doing it never disagree.

   The shared glyph is a counter-clockwise replay arrow, which already
   means "back". It was the BACK button that carried scaleX(-1), so the
   two were drawn exactly the wrong way round: the button that rewound
   showed a forward arrow and the one that skipped ahead showed a back
   arrow. The mirror belongs on forward. */
.eonp-back { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5V2L7 7l5 5V8a5.5 5.5 0 1 1-5.5 5.5H4A8 8 0 1 0 12 5z'/%3E%3C/svg%3E"); }
.eonp-fwd  { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5V2L7 7l5 5V8a5.5 5.5 0 1 1-5.5 5.5H4A8 8 0 1 0 12 5z'/%3E%3C/svg%3E"); transform: scaleX(-1); }

/* Playback rate. Native select so it works with a keyboard and with a
   screen reader without any of it being rebuilt. */
.eonp-speed {
  flex: 0 0 auto;
  height: 34px;
  padding: 0 6px;
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.4);
  color: #fff;
  font: 13px/1 var(--font, Roboto, Arial, sans-serif);
  cursor: pointer;
}
.eonp-speed:focus-visible { outline: 3px solid var(--cta); outline-offset: 2px; }
.eonp-speed option { color: #000; }

/* Shown only when the browser refused unmuted playback and the player
   started the video silently instead. It is a real message about a real
   state, so it gets the accent — this IS actionable: the mute button
   beside it turns the sound on. Removed for good once they do. */
.eonp-mute::after {
  content: "Sound off";
  display: none;
  position: absolute;
  bottom: calc(100% + 6px);
  right: 0;
  padding: 5px 9px;
  border-radius: 7px;
  background: var(--cta);
  color: var(--cta-ink);
  font: 600 13px/1 var(--font, Roboto, Arial, sans-serif);
  white-space: nowrap;
  pointer-events: none;
}
.eonp-silent .eonp-mute { position: relative; }
.eonp-silent .eonp-mute::after { display: block; }
/* The bar is normally hidden until hover; an unheard video has to say so
   without waiting to be discovered. */
.eonp-silent .eonp-bar { opacity: 1; }

.eonp-time {
  flex: 0 0 auto;
  font: 13px/1 var(--font-mono, var(--font, Roboto, Arial, sans-serif));
  color: #fff;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The bar itself is short, but the hit area is not: the padding gives it
   a full-height target so it can be grabbed without precision. */
.eonp-scrub {
  position: relative;
  flex: 1 1 auto;
  height: var(--target-min, 44px);
  display: flex;
  align-items: center;
  cursor: pointer;
  touch-action: none;
}
.eonp-scrub::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 4px;
  border-radius: 2px;
  background: rgba(255, 255, 255, 0.3);
}
.eonp-scrub:focus-visible {
  outline: 3px solid var(--cta);
  outline-offset: 4px;
  border-radius: 4px;
}
.eonp-fill {
  position: absolute;
  left: 0;
  height: 4px;
  width: 0;
  border-radius: 2px;
  background: var(--cta);
}
/* The no-go zone: everything past the preview cutoff. Diagonal hatching
   is the one pattern that reads as "closed off" without a word of copy,
   and it survives being 4px tall, which a label or an icon would not.

   The bar always sits on video, so the ground is dark in BOTH themes and
   the stripes are white-based for contrast either way. The accent still
   swaps with the theme, so the cutoff line is tinted with --cta rather
   than hard-coded — that line is where the accent belongs, since it
   marks the boundary the CTA is asking them to cross. */
.eonp-lock {
  position: absolute;
  left: 100%;
  width: 0;
  height: 4px;
  border-radius: 0 2px 2px 0;
  overflow: hidden;
  background-color: rgba(255, 255, 255, 0.13);
  background-image: repeating-linear-gradient(
    -45deg,
    rgba(255, 255, 255, 0.42) 0,
    rgba(255, 255, 255, 0.42) 2px,
    transparent 2px,
    transparent 5px
  );
  /* Left edge only: the right edge is the end of the video, which needs
     no marking. Inset so it cannot escape the rounded track. */
  box-shadow: inset 2px 0 0 var(--cta);
  pointer-events: none;
}
/* The track grows while a preview is capped. 4px is enough for a played
   position; it is not enough to read hatching at a glance. */
.eonp-haslock .eonp-scrub::before,
.eonp-haslock .eonp-fill,
.eonp-haslock .eonp-lock { height: 6px; border-radius: 3px; }
.eonp-haslock .eonp-lock { border-radius: 0 3px 3px 0; }

/* Windows high-contrast strips background images entirely, taking the
   hatching with it. A border keeps the boundary visible there. */
@media (forced-colors: active) {
  .eonp-lock { border-left: 2px solid CanvasText; background: Canvas; }
}

.eonp-knob {
  position: absolute;
  left: 0;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: var(--cta);
  transform: translateX(-50%);
  opacity: 0;
  transition: opacity var(--dur-hover, 120ms) var(--ease-move, ease);
}
.eonp:hover .eonp-knob,
.eonp-scrub:focus-visible .eonp-knob { opacity: 1; }

/* A touchscreen has no hover, so on a phone the handle was invisible for
   the whole session: the bar could be dragged but there was nothing
   showing where it would be grabbed or where the playhead sat. Reveal it
   whenever the bar itself is up — which on touch is a deliberate act,
   not an accident of where the cursor happens to be. */
.eonp-touch .eonp-knob { opacity: 1; }

/* Under the thumb. The handle grows so it is not hidden by the finger
   holding it, and the track thickens so the position stays readable
   around it — the standard answer to a 4px target being scrubbed by a
   40px fingertip. */
.eonp-scrubbing .eonp-knob { transform: translateX(-50%) scale(1.45); }
.eonp-scrubbing .eonp-scrub::before,
.eonp-scrubbing .eonp-fill,
.eonp-scrubbing .eonp-lock { height: 6px; border-radius: 3px; }
@media (prefers-reduced-motion: reduce) {
  .eonp-scrubbing .eonp-knob { transform: translateX(-50%); }
}

/* ── bare mode (Rumble) ────────────────────────────────────────────────
   Rumble's embed brings its own controls and carries no link back to an
   unlisted video, so there is nothing here to suppress — and every piece
   of the suppression is driven by YouTube's postMessage protocol, which
   Rumble does not speak. Left in place it does not degrade, it inverts:
   the shield blocks Rumble's own controls and ours command nothing.

   display:none rather than opacity or pointer-events, so the shield is
   not merely invisible but genuinely out of the pointer's way. */
.eonp-bare .eonp-shield,
.eonp-bare .eonp-cap,
.eonp-bare .eonp-badge,
.eonp-bare .eonp-rate,
.eonp-bare .eonp-seek,
.eonp-bare .eonp-mid,
.eonp-bare .eonp-bar { display: none; }

/* The frame is the whole player here. */
.eonp-bare .eonp-frame { position: absolute; inset: 0; width: 100%; height: 100%; }

/* ── viewport fullscreen ───────────────────────────────────────────────
   iPhone Safari exposes no fullscreen API for a <div> — only for a real
   <video>, which this is not — so the button used to do nothing there at
   all. This is the fallback: fill the viewport ourselves. Not identical
   to the native mode (the browser chrome stays) but the same gesture
   with the same result, which is what makes the control trustworthy
   across devices. */
/* This does NOT lift the player out of the page with position:fixed.
   That was the first attempt and it is what produced a black video on an
   iPhone with the sound still playing:

     .watch is itself position:fixed with overflow:hidden, and a fixed
     descendant inside a clipped fixed ancestor is precisely the case
     WebKit gets wrong. It did not reposition — the player stayed exactly
     where it was in the page — and the cross-origin iframe lost its
     compositing layer and painted black.

   So the overlay chain that already covers the screen is opened up
   around the player instead (see app.css), and the player just fills its
   parent. No element changes its containing block, so no element loses
   its layer.

   aspect-ratio has to be released explicitly: the 16/9 base rule would
   otherwise cap the height and letterbox the picture into a band. */
.eonp-vfull {
  /* svh MEASURED HERE, not inherited as a percentage of a chain.
     `height: 100%` only resolves to the screen if every ancestor above
     it also resolved to the screen — .player, .watch-main, .watch-inner,
     .watch — and on iOS Safari any one of them measuring the LARGE
     viewport (toolbars retracted) hands this element more height than
     the screen actually has. The overflow all lands at the bottom,
     which is where the control bar lives: reported from a handset as
     the picture cut off and only a sliver of the bar reachable above
     the browser chrome.
     Asking for the small viewport directly makes the chain irrelevant —
     whatever the ancestors think they are, this is the screen. */
  height: 100svh;
  max-height: 100svh;
  aspect-ratio: auto;
  border-radius: 0;
}
html.eonp-vfull-open,
html.eonp-vfull-open body { overflow: hidden; overscroll-behavior: none; }

/* ── double-tap seek feedback ──────────────────────────────────────────
   The only sign a phone gets that the tap registered, since the buttons
   it replaced are gone. Shaped like the gesture: a rounded lobe hugging
   the edge that was tapped, not a box in the middle.

   `display: none` by default — this exists only on phones, and on a
   desktop the two lobes would be invisible sheets over the picture. */
.eonp-seek {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 38%;
  display: none;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  z-index: 2;
  /* Never takes the tap. The shield underneath owns the gesture, and a
     lobe that swallowed the second tap would cancel the very chain it is
     drawn to report. */
  pointer-events: none;
  opacity: 0;
  color: #fff;
  background: rgba(0, 0, 0, 0.34);
}
.eonp-seek-l { left: 0;  border-radius: 0 50% 50% 0 / 0 50% 50% 0; }
.eonp-seek-r { right: 0; border-radius: 50% 0 0 50% / 50% 0 0 50%; }
.eonp-seek-ico {
  width: 42px;
  height: 30px;
  background: #fff;
  --ic-seek: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 24'%3E%3Cpath d='M9 5L2 12l7 7V5zm9 0l-7 7 7 7V5zm9 0l-7 7 7 7V5z'/%3E%3C/svg%3E");
  -webkit-mask: var(--ic-seek) center / contain no-repeat;
  mask: var(--ic-seek) center / contain no-repeat;
}
.eonp-seek-r .eonp-seek-ico { transform: scaleX(-1); }
.eonp-seek-txt { font: 600 13px/1 var(--font, Roboto, Arial, sans-serif); }

/* Rises fast, leaves slowly — the arrival has to beat the thumb, the
   departure has to not be the thing you are looking at. */
.eonp-seek.is-on { animation: eonp-seekflash 620ms var(--ease-move, ease-out); }
@keyframes eonp-seekflash {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  55%  { opacity: 1; }
  100% { opacity: 0; }
}
/* The chevrons drift the way the seek went. Opacity alone still reports
   the seek when motion is unwelcome. */
.eonp-seek.is-on .eonp-seek-ico { animation: eonp-seekslide 620ms var(--ease-move, ease-out); }
@keyframes eonp-seekslide {
  0%   { transform: translateX(0); }
  100% { transform: translateX(-10px); }
}
.eonp-seek-r.is-on .eonp-seek-ico {
  animation-name: eonp-seekslide-r;
}
@keyframes eonp-seekslide-r {
  0%   { transform: scaleX(-1) translateX(0); }
  100% { transform: scaleX(-1) translateX(-10px); }
}
@media (prefers-reduced-motion: reduce) {
  .eonp-seek.is-on .eonp-seek-ico { animation: none; }
  .eonp-seek-r.is-on .eonp-seek-ico { animation: none; transform: scaleX(-1); }
}

/* ── the transport, and where it lives ────────────────────────────── */

.eonp-transport { display: flex; align-items: center; gap: 8px; }

/* The phone slot: the middle of the picture. Inert as a surface — only
   the buttons inside take events, so a tap on the surrounding picture
   still reaches the shield and toggles the controls. */
.eonp-mid {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 3;
  pointer-events: none;
}
.eonp-mid .eonp-transport { pointer-events: auto; }

/* ── phone ────────────────────────────────────────────────────────────
   Four controls, which is as few as this can be and still be a player:
   back, play, forward, fullscreen. Everything else is either a hardware
   button or a setting nobody changes mid-episode.

   Volume goes because a phone has volume keys on its side and no video
   app on a phone draws a slider for them. Speed goes because it is a
   once-a-session decision competing for width with the one control that
   must never be cramped — the scrub bar, which is what collapsed to
   nothing when eight controls shared one row on a 390px screen.

   The bar is then exactly what a phone needs it to be: elapsed time, the
   scrub bar, fullscreen. */
@media (max-width: 700px), (pointer: coarse) {
  .eonp-mid { display: flex; }
  .eonp-phone .eonp-speed,
  .eonp-phone .eonp-mute { display: none; }

  .eonp-phone .eonp-bar { gap: 10px; padding: 8px 10px 10px; }
  /* min-width:0 or the time text's intrinsic width wins the fight and the
     scrub bar is handed whatever is left, which was nothing. */
  .eonp-phone .eonp-scrub { min-width: 0; flex: 1 1 0; }
  .eonp-phone .eonp-time { flex: 0 0 auto; font-variant-numeric: tabular-nums; }

  /* No skip buttons on a phone. The gesture replaced them — a double tap
     on either side — which is why YouTube can leave nothing in the middle
     of the picture but play, and why the picture stays uncluttered while
     the controls are up. */
  .eonp-phone .eonp-back,
  .eonp-phone .eonp-fwd { display: none; }
  .eonp-phone .eonp-seek { display: flex; }

  .eonp-phone .eonp-transport { gap: 26px; }
  /* Play sits on top of YouTube's own play overlay, so it wears the disc
     the badge used to and does that covering job itself. */
  .eonp-phone .eonp-play {
    width: 68px;
    height: 68px;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.68);
  }
  .eonp-phone .eonp-play::before { width: 30px; height: 30px; }
  .eonp-phone .eonp-back,
  .eonp-phone .eonp-fwd { width: 56px; height: 56px; }
  .eonp-phone .eonp-back::before,
  .eonp-phone .eonp-fwd::before { width: 27px; height: 27px; }
  /* Back on the left of play, forward on the right — the order every
     phone player uses, and the reverse of the desktop bar's. */
  .eonp-phone .eonp-back { order: 1; }
  .eonp-phone .eonp-play { order: 2; }
  .eonp-phone .eonp-fwd  { order: 3; }

  /* The play disc now covers what the badge covered, in the same spot. */
  .eonp-phone .eonp-badge { display: none; }
  /* Idle hands the first tap to YouTube; ours must not be in the way. */
  .eonp-idle .eonp-mid { display: none; }

  /* Touch has no hover, so the bar — which appears on :hover — was simply
     never there while a phone was playing. A tap summons it instead
     (.eonp-show) and it retires itself.

     Written as "hide while playing and unsummoned" rather than the
     obvious "hide, then re-show in five states". The states that pin the
     bar open are already doing so from `.eonp-paused .eonp-bar`, and
     `.eonp:hover .eonp-bar` sits at three class-weights — a plain
     `.eonp-touch .eonp-bar { opacity: 0 }` is two, loses to hover, and
     ties with paused only to beat it by source order, which would then
     hide the bar in the one state that most needs it. Naming the single
     state that should hide sidesteps the whole argument. */
  .eonp-touch .eonp-bar,
  .eonp-touch .eonp-mid {
    transition: opacity var(--dur-panel, 200ms) var(--ease-move, ease);
  }
  /* Out from under the iOS chrome. The rule this file already states for
     the mini-player and the install offer — "a control under the iOS
     home indicator is a control nobody can press" — was never applied to
     the player's OWN bar, which is the one that matters most: in
     viewport-fullscreen it sits flush against the bottom of the screen,
     which on an iPhone is exactly where the home indicator and its
     swipe-up gesture area live.
     Landscape is the worse case and the reason the left/right insets are
     here too: the bar spans the full width, and on a notched handset one
     end of it is under the sensor housing while the other is under the
     rounded corner — so the two controls at the ends, which is where a
     play button and a fullscreen button go, are the two that get eaten.
     env() is 0 everywhere it does not apply, so this costs nothing on a
     device without insets. */
  .eonp-bar {
    padding-left: calc(12px + env(safe-area-inset-left, 0px));
    padding-right: calc(12px + env(safe-area-inset-right, 0px));
    padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
  }
  /* A backstop, and deliberately a blunt one: on a phone the player may
     never be taller than the phone. Every other rule here computes a
     height and trusts the arithmetic; this one just refuses the result
     if it came out bigger than the screen. The bar is anchored to the
     BOTTOM of this box, so a box that overflows does not merely look
     wrong — it puts the only controls out of reach, which is the one
     failure this player cannot have. */
  .eonp { max-height: 100svh; }
}

/* ── the touch reveal, OUTSIDE any media query ────────────────────────
   These used to live inside `@media (max-width: 700px), (pointer: coarse)`
   above, and that is precisely how an iPhone ended up with no controls
   at all: Safari in desktop-site mode reports a ~980px viewport AND a
   fine pointer, so both arms of that query miss on a phone and every
   rule in it — including the only thing that can raise the bar without a
   mouse — simply does not exist.

   `.eonp-touch` is the signal, and player.js now sets it off
   maxTouchPoints rather than off a query that can be overridden from a
   browser menu. A class is a fact about the device; a media query here
   was a guess about the window. Keyed on the class alone, the reveal
   follows the finger wherever the viewport claims to be.

   Placed after the media query so source order still beats `:hover` at
   equal weight, which the comment inside that block spells out. */
.eonp-touch.eonp-show .eonp-bar { opacity: 1; }
.eonp-touch:not(.eonp-show):not(.eonp-paused):not(.eonp-idle):not(.eonp-ended):not(.eonp-gate) .eonp-bar,
.eonp-touch:not(.eonp-show):not(.eonp-paused):not(.eonp-idle):not(.eonp-ended):not(.eonp-gate) .eonp-mid,
/* Untappable, not merely invisible — otherwise the middle of the
   picture stays an invisible play button. */
.eonp-touch:not(.eonp-show):not(.eonp-paused):not(.eonp-idle):not(.eonp-ended):not(.eonp-gate) .eonp-transport {
  opacity: 0;
  pointer-events: none;
}
/* The safe-area insets and the height backstop follow the finger too. */
.eonp-touch .eonp-bar {
  padding-left: calc(12px + env(safe-area-inset-left, 0px));
  padding-right: calc(12px + env(safe-area-inset-right, 0px));
  padding-bottom: calc(10px + env(safe-area-inset-bottom, 0px));
}
.eonp-touch { max-height: 100svh; }

/* prefers-reduced-motion: the mark holds still (its own rule, inline in
   index.html), and every fade resolves instantly. The player still
   reports its state — it just stops animating to say so. */
@media (prefers-reduced-motion: reduce) {
  .eonp-scrim,
  .eonp-bar,
  .eonp-knob { transition: none; }
}
.eonp-still .eonp-scrim,
.eonp-still .eonp-bar,
.eonp-still .eonp-knob { transition: none; }

/* ── the docked mini-player ───────────────────────────────────────────
   A view onto the one audio element, so it can simply persist while the
   member browses. Fixed to the bottom because that is where a thumb is,
   and above the safe-area inset because a control under the iOS home
   indicator is a control nobody can press. */
.eonm {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 90;             /* under the watch overlay (100), over the library */
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
  background: var(--bg-elev, #14161a);
  border-top: 1px solid var(--hairline, rgba(255, 255, 255, 0.12));
  color: var(--fg, #f2f4f5);
}
.eonm[hidden] { display: none; }

/* The progress strip rides the top edge of the bar. */
.eonm-bar {
  position: absolute;
  top: -6px;
  left: 0;
  right: 0;
  height: 14px;            /* a thumb target, not a hairline */
  display: flex;
  align-items: center;
  cursor: pointer;
}
.eonm-bar::before {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 3px;
  background: rgba(255, 255, 255, 0.18);
}
.eonm-fill {
  position: absolute;
  left: 0;
  height: 3px;
  width: 0;
  background: var(--cta);
}

.eonm-art {
  flex: 0 0 auto;
  width: 44px; height: 44px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  overflow: hidden;
  background: #000;
  cursor: pointer;
}
.eonm-art img { width: 100%; height: 100%; object-fit: cover; display: block; }

.eonm-meta {
  flex: 1 1 auto;
  min-width: 0;            /* or the title's intrinsic width crushes the buttons */
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.eonm-title {
  font: 600 14px/1.2 var(--font, Roboto, Arial, sans-serif);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.eonm-time {
  font: 400 12px/1 var(--font, Roboto, Arial, sans-serif);
  opacity: 0.7;
  font-variant-numeric: tabular-nums;
}

.eonm-btn {
  flex: 0 0 auto;
  width: var(--target-min, 44px);
  height: var(--target-min, 44px);
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: pointer;
}
.eonm-btn::before {
  content: "";
  width: 22px; height: 22px;
  background: var(--fg, #fff);
  -webkit-mask: var(--ic) center / contain no-repeat;
  mask: var(--ic) center / contain no-repeat;
}
.eonm-btn:focus-visible { outline: 3px solid var(--cta); outline-offset: 2px; }

.eonm-back { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5V2L7 7l5 5V8a5.5 5.5 0 1 1-5.5 5.5H4A8 8 0 1 0 12 5z'/%3E%3C/svg%3E"); }
.eonm-fwd  { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 5V2L7 7l5 5V8a5.5 5.5 0 1 1-5.5 5.5H4A8 8 0 1 0 12 5z'/%3E%3C/svg%3E"); transform: scaleX(-1); }
.eonm-play { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M8 5v14l11-7z'/%3E%3C/svg%3E"); }
.eonm-play.is-playing { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M7 5h3.5v14H7zm6.5 0H17v14h-3.5z'/%3E%3C/svg%3E"); }
.eonm-close { --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M18.3 5.7 12 12l6.3 6.3-1.4 1.4L10.6 13.4 4.3 19.7 2.9 18.3 9.2 12 2.9 5.7l1.4-1.4 6.3 6.3 6.3-6.3z'/%3E%3C/svg%3E"); }

/* Buffering shows on the strip rather than as a spinner over the art —
   a spinner on a 44px thumbnail is illegible and reads as an error. */
.eonm.is-buffering .eonm-fill { opacity: 0.55; }

/* On a phone the skip pair is the first thing to go: play, and the tap
   target that takes them back to the episode, are what matter at 360px. */
@media (max-width: 420px) {
  .eonm-back, .eonm-fwd { display: none; }
}

/* The library must not end underneath the dock. */
body.eonm-open { padding-bottom: 68px; }

/* ── the install offer ────────────────────────────────────────────────
   Shown once, only after somebody has actually been listening, and only
   where the browser said an install is possible. It sits above the
   mini-player because it is a response to what that player is doing. */
.eonpwa {
  position: fixed;
  left: 12px;
  right: 12px;
  bottom: calc(76px + env(safe-area-inset-bottom, 0px));
  z-index: 95;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 14px;
  border-radius: 12px;
  background: var(--bg-elev, #14161a);
  border: 1px solid var(--hairline, rgba(255, 255, 255, 0.14));
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
  color: var(--fg, #f2f4f5);
}
.eonpwa-txt { flex: 1 1 auto; font: 400 14px/1.35 var(--font, Roboto, Arial, sans-serif); }
.eonpwa-yes { flex: 0 0 auto; background: var(--cta); color: var(--cta-ink); border: 0; }
.eonpwa-no {
  flex: 0 0 auto;
  min-height: var(--target-min, 44px);
  padding: 0 10px;
  border: 0;
  background: none;
  color: inherit;
  opacity: 0.7;
  font: 400 13px/1 var(--font, Roboto, Arial, sans-serif);
  cursor: pointer;
}
.eonpwa-no:focus-visible,
.eonpwa-yes:focus-visible { outline: 3px solid var(--cta); outline-offset: 2px; }
@media (max-width: 420px) {
  .eonpwa { flex-wrap: wrap; }
  .eonpwa-txt { flex: 1 0 100%; }
}



/* ── Watch / Listen ───────────────────────────────────────────────────
   Which mode is live is said in WORDS, not only in colour. A segmented
   control that distinguishes its halves by tint alone is unreadable to
   anyone who cannot separate those tints, and looks like decoration to
   everyone else — so the active half carries a filled background, a
   heavier weight, an icon, and aria-pressed for anything listening. */
/* Phones and tablets only. Audio mode answers "screen off, phone in a
   pocket" — a case a desktop does not have, where the honest answer to
   "why would I listen instead of watch" is that you would not. Offering
   it there is a control that costs a decision and buys nothing.

   Same condition the player uses for its phone layout, so the two agree
   rather than disagreeing at some width in between. */
.eon-mode { display: none; }

@media (max-width: 700px), (pointer: coarse) {
.eon-mode {
  display: inline-flex;
  align-items: stretch;
  gap: 2px;
  padding: 2px;
  border-radius: 999px;
  background: rgba(128, 128, 128, 0.16);
}
.eon-mode-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Not a fixed height: at large text sizes a rigid pill clips its own
     label. This grows instead. */
  min-height: var(--target-min, 44px);
  padding: 0 14px;
  border: 0;
  border-radius: 999px;
  background: none;
  color: inherit;
  font: 600 13px/1.2 var(--font, Roboto, Arial, sans-serif);
  white-space: nowrap;
  cursor: pointer;
}
.eon-mode-btn.is-on {
  background: var(--cta);
  color: var(--cta-ink);
  font-weight: 800;
}
.eon-mode-btn:focus-visible { outline: 3px solid var(--cta); outline-offset: 3px; }
.eon-mode-btn:disabled { cursor: default; opacity: 0.65; }
.eon-mode-ico {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  background: currentColor;
  -webkit-mask: var(--ic) center / contain no-repeat;
  mask: var(--ic) center / contain no-repeat;
}
/* A screen, and a pair of headphones. The icon repeats what the label
   says rather than replacing it. */
.eon-mode-btn .eon-mode-ico {
  --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M21 3H3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h7v2H7v2h10v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 12H4V5h16v10z'/%3E%3C/svg%3E");
}
.eon-mode-listen .eon-mode-ico {
  --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 3a8 8 0 0 0-8 8v6a3 3 0 0 0 3 3h1a1 1 0 0 0 1-1v-6a1 1 0 0 0-1-1H6v-1a6 6 0 1 1 12 0v1h-2a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h1a3 3 0 0 0 3-3v-6a8 8 0 0 0-8-8z'/%3E%3C/svg%3E");
}

}

/* Back to video from the dock. */
.eonm-watch {
  --ic: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M21 3H3a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h7v2H7v2h10v-2h-3v-2h7a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1zm-1 12H4V5h16v10z'/%3E%3C/svg%3E");
}
/* At 360px the dock keeps play, the way back to the episode, and close. */
@media (max-width: 420px) {
  .eonm-watch { display: none; }
}

/* ── the toolbar runway (touch viewport-fullscreen only) ─────────────
   Safari retracts its chrome — the URL bar, and in landscape the whole
   tab strip — only when the ROOT scroller moves. The fallback used to
   lock that scroller, which welded the chrome on for the whole film.
   With .eonp-vfull-touch (set by player.js when the fallback engages on
   a touch device) the lock is lifted and the document is sized to the
   LARGE viewport: scrollable by exactly the toolbars' height, so the
   first upward swipe on the picture slides them away, and once they are
   gone the page fits to the pixel and there is nothing left to scroll.
   The stage is sized to the same large viewport — its bottom edge is
   under the toolbar until the swipe, which is why the control bar below
   is position:fixed: pinned to the VISIBLE viewport, on screen before
   the swipe and after it. The overlay chain stays position:fixed
   throughout; only the root's scrollability changes, so no element
   changes its containing block and the iframe keeps its layer. */
html.eonp-vfull-open.eonp-vfull-touch,
html.eonp-vfull-open.eonp-vfull-touch body {
  overflow-y: auto;
  overscroll-behavior: none;      /* scroll yes, rubber-band/refresh no */
  height: 100vh;                  /* vh IS the large viewport on iOS */
  height: 100lvh;
}
html.eonp-vfull-touch .eonp-vfull {
  height: 100vh;
  height: 100lvh;
  max-height: 100vh;
  max-height: 100lvh;
}
html.eonp-vfull-touch .eonp-vfull .eonp-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 6;
  /* Escapes .eonp's overflow clip legitimately: an overflow ancestor
     only clips a fixed descendant if it forms a containing block
     (transform/filter), which .eonp never does. */
}

/* The swipe hint (see _swipeHint). Fixed, so it rides the VISIBLE
   viewport like the bar does, and clear of the bar itself. Pointer
   events off — it is a caption, not a control, and the swipe it
   describes must pass straight through it. */
.eonp-hint {
  position: fixed;
  left: 50%;
  bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  z-index: 7;
  padding: 9px 15px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.74);
  color: #fff;
  font: 600 13px/1 var(--font, Roboto, Arial, sans-serif);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 220ms ease;
}
.eonp-hint.on { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .eonp-hint { transition: none; }
}
