Radio show website: Full Astro build with 194 episodes imported

Complete website for The Computer Guru Show (radio.azcomputerguru.com):
- Astro 6.0.4 static site with React islands
- 194 episodes imported from gurushow.com RSS feed
- Dark/light mode HSL design system
- Persistent audio player with session persistence
- Episode archive with search and season filtering
- Home page with animated hero, stats, latest episodes
- All pages: About, Subscribe, Community, Live, Contact, Blog, 404
- Podcast RSS feed with iTunes namespace
- Session log updated

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:44:42 -07:00
parent 1adc2ed3a4
commit ee89727662
236 changed files with 16513 additions and 0 deletions

View File

@@ -0,0 +1,919 @@
import { useState, useEffect, useRef, useCallback } from 'react';
// ─── Types ───────────────────────────────────────────────────────────────────
interface EpisodeInfo {
audioUrl: string;
title: string;
season: number;
episode: number;
}
interface PlayerState extends EpisodeInfo {
currentTime: number;
}
interface PlayEpisodeEvent extends CustomEvent {
detail: EpisodeInfo;
}
// ─── Constants ───────────────────────────────────────────────────────────────
const STORAGE_KEY = 'persistent-player-state';
const PLAYBACK_SPEEDS = [0.75, 1, 1.25, 1.5, 2] as const;
const SKIP_SECONDS = 15;
const SAVE_INTERVAL_MS = 5000;
// ─── Helpers ─────────────────────────────────────────────────────────────────
function formatTime(seconds: number): string {
if (!isFinite(seconds) || seconds < 0) return '0:00';
const hrs = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
const paddedSecs = secs.toString().padStart(2, '0');
if (hrs > 0) {
return `${hrs}:${mins.toString().padStart(2, '0')}:${paddedSecs}`;
}
return `${mins}:${paddedSecs}`;
}
function loadState(): PlayerState | null {
try {
const raw = sessionStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as PlayerState;
if (parsed.audioUrl && parsed.title != null) {
return parsed;
}
return null;
} catch {
return null;
}
}
function saveState(state: PlayerState): void {
try {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
} catch {
// sessionStorage may be unavailable or full; silently ignore
}
}
function clearState(): void {
try {
sessionStorage.removeItem(STORAGE_KEY);
} catch {
// ignore
}
}
function buildEpisodeLabel(season: number, episode: number): string {
if (season === 0) return 'Special';
return `S${season}E${String(episode).padStart(2, '0')}`;
}
// ─── Component ───────────────────────────────────────────────────────────────
export default function PersistentPlayer() {
const audioRef = useRef<HTMLAudioElement | null>(null);
const progressBarRef = useRef<HTMLDivElement | null>(null);
const saveTimerRef = useRef<number | null>(null);
const [episodeInfo, setEpisodeInfo] = useState<EpisodeInfo | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [buffered, setBuffered] = useState(0);
const [volume, setVolume] = useState(1);
const [speedIndex, setSpeedIndex] = useState(1); // default 1x
const [isVisible, setIsVisible] = useState(false);
const [isDraggingProgress, setIsDraggingProgress] = useState(false);
// ── Restore state on mount ──────────────────────────────────────────────
useEffect(() => {
const saved = loadState();
if (saved) {
setEpisodeInfo({
audioUrl: saved.audioUrl,
title: saved.title,
season: saved.season,
episode: saved.episode,
});
setCurrentTime(saved.currentTime);
setIsVisible(true);
}
}, []);
// ── Listen for play-episode custom events ───────────────────────────────
useEffect(() => {
function handlePlayEpisode(e: Event) {
const event = e as PlayEpisodeEvent;
const { audioUrl, title, season, episode } = event.detail;
if (!audioUrl) return;
setEpisodeInfo({ audioUrl, title, season, episode });
setCurrentTime(0);
setDuration(0);
setBuffered(0);
setIsVisible(true);
// Playback will start once the audio element loads the new source
// (handled in the audioUrl effect below)
}
document.addEventListener('play-episode', handlePlayEpisode);
return () => document.removeEventListener('play-episode', handlePlayEpisode);
}, []);
// ── Wire up audio element when episode changes ──────────────────────────
useEffect(() => {
const audio = audioRef.current;
if (!audio || !episodeInfo) return;
// If source changed, load new source
if (audio.src !== episodeInfo.audioUrl) {
audio.src = episodeInfo.audioUrl;
audio.load();
}
// Restore saved position for restored state (non-zero currentTime means restored)
if (currentTime > 0 && audio.readyState >= 1) {
audio.currentTime = currentTime;
}
// Auto-play if this is a fresh play-episode event (currentTime === 0)
// For restored sessions, don't auto-play
const isRestoredSession = currentTime > 0;
if (!isRestoredSession) {
const playOnReady = () => {
audio.play().catch(() => {
// Browser may block autoplay; user will click play manually
});
audio.removeEventListener('canplay', playOnReady);
};
if (audio.readyState >= 3) {
audio.play().catch(() => {});
} else {
audio.addEventListener('canplay', playOnReady);
return () => audio.removeEventListener('canplay', playOnReady);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [episodeInfo?.audioUrl]);
// ── Restore currentTime once audio metadata is loaded ───────────────────
useEffect(() => {
const audio = audioRef.current;
if (!audio || !episodeInfo) return;
const handleLoadedMetadata = () => {
const saved = loadState();
if (saved && saved.audioUrl === episodeInfo.audioUrl && saved.currentTime > 0) {
audio.currentTime = saved.currentTime;
setCurrentTime(saved.currentTime);
}
};
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
return () => audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
}, [episodeInfo]);
// ── Audio event handlers ────────────────────────────────────────────────
useEffect(() => {
const audio = audioRef.current;
if (!audio) return;
const onPlay = () => setIsPlaying(true);
const onPause = () => setIsPlaying(false);
const onEnded = () => {
setIsPlaying(false);
setCurrentTime(0);
};
const onTimeUpdate = () => {
if (!isDraggingProgress) {
setCurrentTime(audio.currentTime);
}
};
const onDurationChange = () => {
if (isFinite(audio.duration)) {
setDuration(audio.duration);
}
};
const onProgress = () => {
if (audio.buffered.length > 0) {
setBuffered(audio.buffered.end(audio.buffered.length - 1));
}
};
audio.addEventListener('play', onPlay);
audio.addEventListener('pause', onPause);
audio.addEventListener('ended', onEnded);
audio.addEventListener('timeupdate', onTimeUpdate);
audio.addEventListener('durationchange', onDurationChange);
audio.addEventListener('progress', onProgress);
return () => {
audio.removeEventListener('play', onPlay);
audio.removeEventListener('pause', onPause);
audio.removeEventListener('ended', onEnded);
audio.removeEventListener('timeupdate', onTimeUpdate);
audio.removeEventListener('durationchange', onDurationChange);
audio.removeEventListener('progress', onProgress);
};
}, [isDraggingProgress]);
// ── Periodic state persistence ──────────────────────────────────────────
useEffect(() => {
if (!episodeInfo || !isPlaying) {
if (saveTimerRef.current != null) {
window.clearInterval(saveTimerRef.current);
saveTimerRef.current = null;
}
return;
}
saveTimerRef.current = window.setInterval(() => {
const audio = audioRef.current;
if (audio && episodeInfo) {
saveState({
...episodeInfo,
currentTime: audio.currentTime,
});
}
}, SAVE_INTERVAL_MS);
return () => {
if (saveTimerRef.current != null) {
window.clearInterval(saveTimerRef.current);
saveTimerRef.current = null;
}
};
}, [episodeInfo, isPlaying]);
// ── Save state on pause / before unload ─────────────────────────────────
useEffect(() => {
function handleBeforeUnload() {
const audio = audioRef.current;
if (audio && episodeInfo) {
saveState({ ...episodeInfo, currentTime: audio.currentTime });
}
}
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [episodeInfo]);
// ── Controls ────────────────────────────────────────────────────────────
const togglePlayPause = useCallback(() => {
const audio = audioRef.current;
if (!audio) return;
if (audio.paused) {
audio.play().catch(() => {});
} else {
audio.pause();
if (episodeInfo) {
saveState({ ...episodeInfo, currentTime: audio.currentTime });
}
}
}, [episodeInfo]);
const skipBack = useCallback(() => {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = Math.max(0, audio.currentTime - SKIP_SECONDS);
}, []);
const skipForward = useCallback(() => {
const audio = audioRef.current;
if (!audio) return;
audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + SKIP_SECONDS);
}, []);
const cycleSpeed = useCallback(() => {
setSpeedIndex((prev) => {
const next = (prev + 1) % PLAYBACK_SPEEDS.length;
const audio = audioRef.current;
if (audio) {
audio.playbackRate = PLAYBACK_SPEEDS[next];
}
return next;
});
}, []);
const handleVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const val = parseFloat(e.target.value);
setVolume(val);
const audio = audioRef.current;
if (audio) {
audio.volume = val;
}
}, []);
const handleProgressClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
const bar = progressBarRef.current;
const audio = audioRef.current;
if (!bar || !audio || !duration) return;
const rect = bar.getBoundingClientRect();
const fraction = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
const newTime = fraction * duration;
audio.currentTime = newTime;
setCurrentTime(newTime);
},
[duration],
);
const handleProgressMouseDown = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
setIsDraggingProgress(true);
handleProgressClick(e);
const handleMouseMove = (ev: MouseEvent) => {
const bar = progressBarRef.current;
const audio = audioRef.current;
if (!bar || !audio || !duration) return;
const rect = bar.getBoundingClientRect();
const fraction = Math.max(0, Math.min(1, (ev.clientX - rect.left) / rect.width));
const newTime = fraction * duration;
audio.currentTime = newTime;
setCurrentTime(newTime);
};
const handleMouseUp = () => {
setIsDraggingProgress(false);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
},
[duration, handleProgressClick],
);
const closePlayer = useCallback(() => {
const audio = audioRef.current;
if (audio) {
audio.pause();
audio.removeAttribute('src');
audio.load();
}
setIsPlaying(false);
setEpisodeInfo(null);
setCurrentTime(0);
setDuration(0);
setBuffered(0);
setIsVisible(false);
clearState();
}, []);
// ── Keyboard shortcuts ──────────────────────────────────────────────────
useEffect(() => {
if (!episodeInfo) return;
function handleKeyDown(e: KeyboardEvent) {
// Only respond when not typing in an input
const target = e.target as HTMLElement;
if (
target.tagName === 'INPUT' ||
target.tagName === 'TEXTAREA' ||
target.tagName === 'SELECT' ||
target.isContentEditable
) {
return;
}
switch (e.key) {
case ' ':
e.preventDefault();
togglePlayPause();
break;
case 'ArrowLeft':
e.preventDefault();
skipBack();
break;
case 'ArrowRight':
e.preventDefault();
skipForward();
break;
}
}
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [episodeInfo, togglePlayPause, skipBack, skipForward]);
// ── Derived values ──────────────────────────────────────────────────────
const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0;
const bufferedPercent = duration > 0 ? (buffered / duration) * 100 : 0;
const currentSpeed = PLAYBACK_SPEEDS[speedIndex];
const episodeLabel = episodeInfo ? buildEpisodeLabel(episodeInfo.season, episodeInfo.episode) : '';
// ── Render ──────────────────────────────────────────────────────────────
return (
<>
{/* Hidden audio element - always mounted so it survives re-renders */}
<audio ref={audioRef} preload="metadata" />
<div
className="persistent-player"
style={{
transform: isVisible ? 'translateY(0)' : 'translateY(100%)',
}}
role="region"
aria-label="Audio player"
>
{/* Progress bar - full width across top of player */}
<div
className="persistent-player__progress-bar"
ref={progressBarRef}
onClick={handleProgressClick}
onMouseDown={handleProgressMouseDown}
role="slider"
aria-label="Playback progress"
aria-valuemin={0}
aria-valuemax={duration}
aria-valuenow={currentTime}
aria-valuetext={`${formatTime(currentTime)} of ${formatTime(duration)}`}
tabIndex={0}
>
<div
className="persistent-player__progress-buffered"
style={{ width: `${bufferedPercent}%` }}
/>
<div
className="persistent-player__progress-fill"
style={{ width: `${progressPercent}%` }}
/>
<div
className="persistent-player__progress-handle"
style={{ left: `${progressPercent}%` }}
/>
</div>
<div className="persistent-player__content">
{/* Left: Episode info */}
<div className="persistent-player__info">
{episodeLabel && (
<span className="persistent-player__badge">{episodeLabel}</span>
)}
<span className="persistent-player__title" title={episodeInfo?.title ?? ''}>
{episodeInfo?.title ?? ''}
</span>
</div>
{/* Center: Playback controls */}
<div className="persistent-player__controls">
<button
className="persistent-player__btn persistent-player__btn--skip"
onClick={skipBack}
aria-label="Skip back 15 seconds"
title="Skip back 15 seconds"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="1 4 1 10 7 10" />
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
<text x="12" y="16" textAnchor="middle" fill="currentColor" stroke="none" fontSize="8" fontWeight="700">15</text>
</svg>
</button>
<button
className="persistent-player__btn persistent-player__btn--play"
onClick={togglePlayPause}
aria-label={isPlaying ? 'Pause' : 'Play'}
title={isPlaying ? 'Pause' : 'Play'}
>
{isPlaying ? (
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<rect x="6" y="4" width="4" height="16" rx="1" />
<rect x="14" y="4" width="4" height="16" rx="1" />
</svg>
) : (
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<polygon points="6 3 20 12 6 21 6 3" />
</svg>
)}
</button>
<button
className="persistent-player__btn persistent-player__btn--skip"
onClick={skipForward}
aria-label="Skip forward 15 seconds"
title="Skip forward 15 seconds"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="23 4 23 10 17 10" />
<path d="M20.49 15a9 9 0 1 1-2.13-9.36L23 10" />
<text x="12" y="16" textAnchor="middle" fill="currentColor" stroke="none" fontSize="8" fontWeight="700">15</text>
</svg>
</button>
</div>
{/* Time display */}
<div className="persistent-player__time">
<span>{formatTime(currentTime)}</span>
<span className="persistent-player__time-separator">/</span>
<span>{formatTime(duration)}</span>
</div>
{/* Right: Volume, speed, close */}
<div className="persistent-player__right">
<div className="persistent-player__volume">
<button
className="persistent-player__btn persistent-player__btn--icon"
onClick={() => {
const audio = audioRef.current;
if (!audio) return;
if (audio.volume > 0) {
audio.volume = 0;
setVolume(0);
} else {
audio.volume = 1;
setVolume(1);
}
}}
aria-label={volume === 0 ? 'Unmute' : 'Mute'}
title={volume === 0 ? 'Unmute' : 'Mute'}
>
{volume === 0 ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
<line x1="23" y1="9" x2="17" y2="15" />
<line x1="17" y1="9" x2="23" y2="15" />
</svg>
) : volume < 0.5 ? (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
</svg>
) : (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
</svg>
)}
</button>
<input
type="range"
className="persistent-player__volume-slider"
min="0"
max="1"
step="0.05"
value={volume}
onChange={handleVolumeChange}
aria-label="Volume"
/>
</div>
<button
className="persistent-player__btn persistent-player__btn--speed"
onClick={cycleSpeed}
aria-label={`Playback speed: ${currentSpeed}x`}
title={`Playback speed: ${currentSpeed}x`}
>
{currentSpeed}x
</button>
<button
className="persistent-player__btn persistent-player__btn--close"
onClick={closePlayer}
aria-label="Close player"
title="Close player"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
</div>
</div>
<style>{`
.persistent-player {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
height: 72px;
background: hsl(220 20% 6% / 0.95);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-top: 1px solid hsl(200 85% 55% / 0.15);
transition: transform 350ms cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
flex-direction: column;
}
[data-theme="light"] .persistent-player {
background: hsl(220 20% 97% / 0.92);
border-top-color: hsl(200 85% 42% / 0.2);
}
/* ── Progress bar ─────────────────────────────────────── */
.persistent-player__progress-bar {
position: relative;
width: 100%;
height: 3px;
background: hsl(220 12% 24% / 0.5);
cursor: pointer;
flex-shrink: 0;
}
.persistent-player__progress-bar:hover {
height: 5px;
}
.persistent-player__progress-bar:hover .persistent-player__progress-handle {
opacity: 1;
}
[data-theme="light"] .persistent-player__progress-bar {
background: hsl(220 12% 85% / 0.5);
}
.persistent-player__progress-buffered {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: hsl(220 8% 48% / 0.3);
pointer-events: none;
}
.persistent-player__progress-fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
background: var(--color-accent, hsl(200 85% 55%));
pointer-events: none;
}
.persistent-player__progress-handle {
position: absolute;
top: 50%;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--color-accent, hsl(200 85% 55%));
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 150ms ease;
pointer-events: none;
}
/* ── Content layout ───────────────────────────────────── */
.persistent-player__content {
display: flex;
align-items: center;
flex: 1;
padding: 0 16px;
gap: 16px;
min-width: 0;
}
/* ── Episode info ─────────────────────────────────────── */
.persistent-player__info {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
flex: 1;
}
.persistent-player__badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 9999px;
font-size: 0.7rem;
font-weight: 600;
background: hsl(200 85% 55% / 0.2);
color: var(--color-accent, hsl(200 85% 55%));
border: 1px solid hsl(200 85% 55% / 0.3);
white-space: nowrap;
flex-shrink: 0;
}
.persistent-player__title {
font-size: 0.875rem;
font-weight: 500;
color: var(--color-text-primary, hsl(220 10% 92%));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
/* ── Controls ─────────────────────────────────────────── */
.persistent-player__controls {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.persistent-player__btn {
display: inline-flex;
align-items: center;
justify-content: center;
background: none;
border: none;
cursor: pointer;
color: var(--color-text-secondary, hsl(220 8% 68%));
padding: 6px;
border-radius: 6px;
transition: color 150ms ease, background 150ms ease;
}
.persistent-player__btn:hover {
color: var(--color-text-primary, hsl(220 10% 92%));
background: hsl(220 16% 16% / 0.6);
}
[data-theme="light"] .persistent-player__btn:hover {
background: hsl(220 16% 92% / 0.6);
}
.persistent-player__btn--play {
width: 40px;
height: 40px;
border-radius: 50%;
color: hsl(220 20% 8%);
background: var(--color-accent, hsl(200 85% 55%));
}
.persistent-player__btn--play:hover {
color: hsl(220 20% 8%);
background: var(--color-accent-hover, hsl(200 85% 65%));
box-shadow: 0 0 16px hsl(200 85% 55% / 0.3);
}
.persistent-player__btn--speed {
font-size: 0.7rem;
font-weight: 700;
min-width: 36px;
font-family: var(--font-mono, monospace);
}
.persistent-player__btn--close {
opacity: 0.5;
}
.persistent-player__btn--close:hover {
opacity: 1;
}
/* ── Time display ─────────────────────────────────────── */
.persistent-player__time {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.75rem;
font-family: var(--font-mono, monospace);
color: var(--color-text-muted, hsl(220 6% 48%));
white-space: nowrap;
flex-shrink: 0;
}
.persistent-player__time-separator {
opacity: 0.5;
}
/* ── Volume ───────────────────────────────────────────── */
.persistent-player__right {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.persistent-player__volume {
display: flex;
align-items: center;
gap: 4px;
}
.persistent-player__volume-slider {
width: 72px;
height: 4px;
-webkit-appearance: none;
appearance: none;
background: hsl(220 12% 24%);
border-radius: 2px;
outline: none;
cursor: pointer;
}
[data-theme="light"] .persistent-player__volume-slider {
background: hsl(220 12% 80%);
}
.persistent-player__volume-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--color-accent, hsl(200 85% 55%));
border: none;
cursor: pointer;
}
.persistent-player__volume-slider::-moz-range-thumb {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--color-accent, hsl(200 85% 55%));
border: none;
cursor: pointer;
}
/* ── Mobile responsive ────────────────────────────────── */
@media (max-width: 768px) {
.persistent-player {
height: 64px;
}
.persistent-player__content {
padding: 0 10px;
gap: 8px;
}
.persistent-player__info {
flex: 1;
min-width: 0;
}
.persistent-player__title {
font-size: 0.8rem;
max-width: 120px;
}
.persistent-player__volume {
display: none;
}
.persistent-player__btn--speed {
display: none;
}
.persistent-player__time {
font-size: 0.65rem;
}
.persistent-player__btn--play {
width: 34px;
height: 34px;
}
.persistent-player__btn--play svg {
width: 18px;
height: 18px;
}
.persistent-player__btn--skip svg {
width: 14px;
height: 14px;
}
}
@media (max-width: 480px) {
.persistent-player__btn--skip {
display: none;
}
.persistent-player__time {
display: none;
}
.persistent-player__title {
max-width: 140px;
}
}
`}</style>
</>
);
}