beecast-player 0.5.0

The first-party beecast player — a clean-room, dependency-free asciicast (v1/v2/v3) player and VT emulator, exposed as inlinable JS/CSS string constants, with live-follow append for recordings that are still growing.
Documentation
/* beecast-player chrome + terminal palette (see the crate README).
   Clean-room, MIT like the rest of beecast. Every color is a variable so embedding pages can theme it. */
.beecast-player {
  --sp-bg: #121417;
  --sp-fg: #cdd3dc;
  /* ANSI 0–7 */
  --sp-c0: #1c1f24; --sp-c1: #e05f65; --sp-c2: #78bd65; --sp-c3: #d5b45e;
  --sp-c4: #6a9ded; --sp-c5: #bd7bd8; --sp-c6: #55b8c2; --sp-c7: #b8bfc9;
  /* ANSI 8–15 (bright) */
  --sp-c8: #596273; --sp-c9: #ff8188; --sp-c10: #98d989; --sp-c11: #f0d182;
  --sp-c12: #8fb7ff; --sp-c13: #d99df0; --sp-c14: #7fd6df; --sp-c15: #eef1f5;
  display: block;
  background: var(--sp-bg);
  color: var(--sp-fg);
  border-radius: 4px;
  outline: none;
  overflow: hidden;
}
.beecast-player:focus-visible { box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.5); }
/* A fullscreened player centers the terminal block (screen + bar) in the viewport;
   layout() sizes the screen box to the viewport minus the bar. */
.beecast-player:fullscreen {
  width: 100%; height: 100%; border-radius: 0;
  display: flex; flex-direction: column; justify-content: center;
}
/* Some embedding pages reset [hidden]; the player's own chrome must still honor it. */
.beecast-player [hidden] { display: none !important; }
/* Positioning context for the center play overlay and the chapters panel. */
.sp-screen-box { overflow: hidden; position: relative; }
/* The big center play button: shown only while the recording sits at its very start,
   dimming the screen behind it. Block characters — this is a terminal player. */
.sp-overlay {
  position: absolute; inset: 0; z-index: 3;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, 0.45);
  cursor: pointer;
}
.sp-bigplay {
  margin: 0;
  font: 15px/1 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  color: #fff; opacity: 0.85;
  text-shadow: 0 2px 12px rgba(0, 0, 0, 0.8);
  transition: opacity 0.15s, transform 0.15s;
}
.sp-overlay:hover .sp-bigplay { opacity: 1; transform: scale(1.08); }
/* The chapters panel: a fixed, scrollable frame over the screen's right edge. */
.sp-chapters {
  position: absolute; top: 0; right: 0; bottom: 0; z-index: 4;
  width: min(320px, 60%);
  overflow-y: auto;
  background: rgba(9, 11, 15, 0.92);
  border-left: 1px solid rgba(255, 255, 255, 0.14);
  font: 12px/1.45 ui-sans-serif, system-ui, sans-serif;
  padding: 4px 0;
}
.sp-chap {
  display: block; width: 100%; text-align: left;
  font: inherit; color: var(--sp-fg);
  background: transparent; border: none; cursor: pointer;
  padding: 5px 12px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.sp-chap:hover { background: rgba(106, 157, 237, 0.18); color: #fff; }
.sp-chap-t { color: var(--sp-c4); margin-right: 8px; font-variant-numeric: tabular-nums; }
.sp-screen {
  margin: 0;
  padding: 4px 6px;
  font: 13px/1.25 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre;
  transform-origin: top left;
  width: max-content;
}
/* Styled runs render as inline-blocks so a run's background paints the FULL 1.25 line box.
   A plain inline span's background only covers the font's content area (ascent + descent,
   less than the line box), which left unpainted gaps between the rows of a multi-row
   colored panel — visible as dark lines cutting through what a TUI drew as one solid
   block. Runs never span rows (rows are separate lines in the <pre>), so inline-block
   cannot change any line breaking. */
.sp-screen span { display: inline-block; }
.sp-probe { visibility: hidden; position: absolute; }
.sp-b { font-weight: 700; }
.sp-d { opacity: 0.6; }
.sp-i { font-style: italic; }
.sp-u { text-decoration: underline; }
.sp-s { text-decoration: line-through; }
.sp-u.sp-s { text-decoration: underline line-through; }
.sp-cur { outline: 1px solid var(--sp-fg); outline-offset: -1px; }

/* control bar */
.sp-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: rgba(255, 255, 255, 0.04);
  border-top: 1px solid rgba(255, 255, 255, 0.08);
  font: 12px/1 ui-sans-serif, system-ui, sans-serif;
  user-select: none;
}
.sp-bar button {
  font: inherit;
  color: var(--sp-fg);
  background: transparent;
  border: none;
  cursor: pointer;
  padding: 2px 6px;
}
.sp-bar button:hover { color: #fff; }
/* The bar's play/pause is the primary control — a size up from its neighbors. */
.sp-play { font-size: 16px; padding: 1px 7px; }
/* The speed menu grows UP from its bar button. */
.sp-speedwrap { position: relative; display: inline-flex; }
.sp-speedmenu {
  position: absolute; bottom: calc(100% + 6px); right: 0; z-index: 5;
  display: flex; flex-direction: column;
  min-width: 3.6em; padding: 3px;
  background: #161a20;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 4px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
}
.sp-speedopt { text-align: center; border-radius: 3px; }
.sp-speedopt.sp-on { color: var(--sp-c12); font-weight: 700; }
.sp-speedopt:hover { background: rgba(106, 157, 237, 0.18); }
.sp-time, .sp-dur { color: var(--sp-fg); opacity: 0.8; min-width: 3.2em; text-align: center; font-variant-numeric: tabular-nums; }
.sp-seek {
  position: relative;
  flex: 1;
  height: 6px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.12);
  cursor: pointer;
}
.sp-fill {
  position: absolute; left: 0; top: 0; bottom: 0;
  width: 0;
  border-radius: 3px;
  background: var(--sp-c4);
  pointer-events: none;
}
.sp-markers { position: absolute; inset: 0; pointer-events: none; }
.sp-marker {
  position: absolute; top: -2px; bottom: -2px; width: 2px;
  background: var(--sp-c3);
  pointer-events: auto;
}