音楽プレイヤー

回転するジャケット写真と曲名・アーティスト表示、シークバーと前後の曲送りを備えた音楽プレイヤー。再生中はイコライザー風のバーが小さく揺れて動きます。

#css#js#media

ライブデモ

コード

HTML
<!-- 音楽プレイヤー: ジャケット+曲情報+シークバー+前後送り。3曲を内部で送り再生する -->
<div class="stage">
  <section class="ma-player" aria-label="音楽プレイヤー">
    <div class="ma-player__art-wrap">
      <img class="ma-player__art" src="https://picsum.photos/id/1062/300/300" alt="アルバムアート: 花明かりの午後" width="150" height="150">
      <span class="ma-player__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
    </div>

    <div class="ma-player__info">
      <p class="ma-player__track">花明かりの午後</p>
      <p class="ma-player__artist">yura</p>
    </div>

    <div class="ma-player__bar" tabindex="0" role="slider" aria-label="再生位置" aria-valuemin="0" aria-valuemax="195" aria-valuenow="0" aria-valuetext="0分00秒 / 3分15秒">
      <div class="ma-player__bar-track">
        <div class="ma-player__bar-fill"></div>
        <div class="ma-player__bar-handle"></div>
      </div>
    </div>
    <div class="ma-player__time">
      <span class="ma-player__current">0:00</span>
      <span class="ma-player__dur">3:15</span>
    </div>

    <div class="ma-player__controls">
      <button type="button" class="ma-player__btn ma-player__prev" aria-label="前の曲">
        <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M6 6h2v12H6zM19 6l-9 6 9 6z" fill="currentColor"/></svg>
      </button>

      <button type="button" class="ma-player__btn ma-player__play" aria-label="再生と一時停止を切り替え" aria-pressed="false">
        <svg class="ma-icon ma-icon--play" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
        <svg class="ma-icon ma-icon--pause" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" hidden><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
      </button>

      <button type="button" class="ma-player__btn ma-player__next" aria-label="次の曲">
        <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M16 6h2v12h-2zM5 6l9 6-9 6z" fill="currentColor"/></svg>
      </button>
    </div>
  </section>
</div>
CSS
/* 音楽プレイヤー: 陽だまりのような温かいクリーム基調 */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  padding: 24px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 14% 12%, #ffe7d1 0%, transparent 46%),
    radial-gradient(circle at 88% 18%, #f4dcec 0%, transparent 48%),
    radial-gradient(circle at 50% 100%, #e7f0e6 0%, transparent 55%),
    #fbf7f2;
  color: #2c2620;
}

.stage { display: grid; place-items: center; width: 100%; }

.ma-player {
  width: min(320px, 90vw);
  padding: 26px 26px 22px;
  border-radius: 26px;
  background: #fffdfb;
  box-shadow: 0 20px 50px rgba(90, 62, 35, .14);
  display: grid;
  justify-items: center;
  gap: 4px;
}

.ma-player__art-wrap {
  position: relative;
  width: 150px;
  height: 150px;
  margin-bottom: 14px;
}
.ma-player__art {
  width: 150px;
  height: 150px;
  border-radius: 999px;
  object-fit: cover;
  display: block;
  box-shadow: 0 14px 34px rgba(90, 62, 35, .28), 0 0 0 6px #fffdfb, 0 0 0 8px rgba(90, 62, 35, .1);
  animation: ma-spin 9s linear infinite;
  animation-play-state: paused;
}
.ma-player.is-playing .ma-player__art { animation-play-state: running; }
@keyframes ma-spin { to { transform: rotate(360deg); } }

.ma-player__eq {
  position: absolute;
  right: -2px;
  bottom: 2px;
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 18px;
  padding: 5px 6px 4px;
  border-radius: 999px;
  background: rgba(44, 38, 32, .82);
  opacity: 0;
  transition: opacity .25s ease;
}
.ma-player.is-playing .ma-player__eq { opacity: 1; }
.ma-player__eq i {
  display: block;
  width: 2.5px;
  border-radius: 2px;
  background: #ffd9a0;
  animation: ma-bounce 1s ease-in-out infinite;
}
.ma-player__eq i:nth-child(1) { height: 5px; animation-delay: -.6s; }
.ma-player__eq i:nth-child(2) { height: 9px; animation-delay: -.3s; }
.ma-player__eq i:nth-child(3) { height: 6px; animation-delay: -.9s; }
.ma-player__eq i:nth-child(4) { height: 8px; animation-delay: -.15s; }
@keyframes ma-bounce { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1.2); } }

.ma-player__info { text-align: center; margin-bottom: 14px; }
.ma-player__track { margin: 0 0 3px; font-size: 16px; font-weight: 800; letter-spacing: .01em; }
.ma-player__artist { margin: 0; font-size: 12.5px; color: #9a8c78; }

.ma-player__bar {
  width: 100%;
  padding: 8px 0;
  cursor: pointer;
  outline-offset: 4px;
}
.ma-player__bar-track {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: #efe6da;
}
.ma-player__bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, #f7b071, #ef8a6b);
}
.ma-player__bar-handle {
  position: absolute;
  top: 50%;
  left: 0%;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 999px;
  background: #fff;
  box-shadow: 0 2px 6px rgba(90, 62, 35, .35), 0 0 0 2px #ef8a6b inset;
  transform: translateY(-50%) scale(0);
  transition: transform .15s ease;
}
.ma-player__bar:hover .ma-player__bar-handle,
.ma-player__bar:focus-visible .ma-player__bar-handle { transform: translateY(-50%) scale(1); }

.ma-player__time {
  width: 100%;
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: #9a8c78;
  margin-bottom: 16px;
}

.ma-player__controls {
  display: flex;
  align-items: center;
  gap: 20px;
}
.ma-player__btn {
  display: grid;
  place-items: center;
  border: none;
  background: transparent;
  color: #6b5c47;
  cursor: pointer;
  border-radius: 999px;
  transition: color .18s ease, transform .12s ease, background .18s ease;
}
.ma-player__prev, .ma-player__next { width: 34px; height: 34px; }
.ma-player__prev:hover, .ma-player__next:hover { color: #ef8a6b; background: rgba(239, 138, 107, .1); }
.ma-player__play {
  width: 54px;
  height: 54px;
  color: #fff;
  background: linear-gradient(135deg, #f7b071, #ef8a6b);
  box-shadow: 0 12px 26px rgba(239, 138, 107, .4);
}
.ma-player__play:hover { transform: translateY(-2px); box-shadow: 0 16px 32px rgba(239, 138, 107, .5); }
.ma-player__btn:active { transform: scale(.92); }

.ma-icon--pause[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .ma-player__art { animation: none; }
  .ma-player__eq i { animation: none; }
  .ma-player__btn, .ma-player__bar-handle { transition: none; }
}
JavaScript
// 音楽プレイヤー: 実音声は使わず、タイマーで進捗を擬似的に進める。3曲を内部で送り再生
(() => {
  const player = document.querySelector('.ma-player');
  if (!player) return; // null安全

  const art = player.querySelector('.ma-player__art');
  const trackEl = player.querySelector('.ma-player__track');
  const artistEl = player.querySelector('.ma-player__artist');
  const bar = player.querySelector('.ma-player__bar');
  const barFill = player.querySelector('.ma-player__bar-fill');
  const barHandle = player.querySelector('.ma-player__bar-handle');
  const curEl = player.querySelector('.ma-player__current');
  const durEl = player.querySelector('.ma-player__dur');
  const playBtn = player.querySelector('.ma-player__play');
  const prevBtn = player.querySelector('.ma-player__prev');
  const nextBtn = player.querySelector('.ma-player__next');
  const iconPlay = playBtn ? playBtn.querySelector('.ma-icon--play') : null;
  const iconPause = playBtn ? playBtn.querySelector('.ma-icon--pause') : null;

  if (!bar || !barFill || !barHandle || !curEl || !durEl) return;

  const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;

  const TRACKS = [
    { title: '花明かりの午後', artist: 'yura', art: 'https://picsum.photos/id/1062/300/300', duration: 195 },
    { title: '夜行バスのブルー', artist: 'Night Owl Parade', art: 'https://picsum.photos/id/1074/300/300', duration: 221 },
    { title: 'そよ風レコード', artist: 'Sunday Milk', art: 'https://picsum.photos/id/1080/300/300', duration: 168 },
  ];

  const TICK_MS = 200;
  const SPEED = 3.6; // デモとして体感しやすい速さで擬似進行させる倍率

  const state = { index: 0, current: 0, playing: false };

  const fmt = (s) => {
    const t = Math.max(0, Math.round(s));
    const m = Math.floor(t / 60);
    const sec = t % 60;
    return `${m}:${String(sec).padStart(2, '0')}`;
  };

  let timer = null;

  function duration() { return TRACKS[state.index].duration; }

  function renderTrack() {
    const t = TRACKS[state.index];
    if (trackEl) trackEl.textContent = t.title;
    if (artistEl) artistEl.textContent = t.artist;
    if (art) { art.src = t.art; art.alt = `アルバムアート: ${t.title}`; }
    durEl.textContent = fmt(t.duration);
    bar.setAttribute('aria-valuemax', String(t.duration));
  }

  function renderProgress() {
    const d = duration();
    const frac = d ? Math.min(1, state.current / d) : 0;
    barFill.style.width = `${frac * 100}%`;
    barHandle.style.left = `${frac * 100}%`;
    bar.setAttribute('aria-valuenow', String(Math.round(state.current)));
    bar.setAttribute('aria-valuetext', `${fmt(state.current)} / ${fmt(d)}`);
    curEl.textContent = fmt(state.current);
  }

  function setPlaying(next) {
    state.playing = next;
    player.classList.toggle('is-playing', next);
    if (playBtn) playBtn.setAttribute('aria-pressed', String(next));
    if (iconPlay) iconPlay.hidden = next;
    if (iconPause) iconPause.hidden = !next;

    clearInterval(timer);
    if (next) {
      timer = setInterval(() => {
        state.current += (TICK_MS / 1000) * SPEED;
        if (state.current >= duration()) { goTo(state.index + 1); return; }
        renderProgress();
      }, TICK_MS);
    }
  }

  function goTo(index) {
    const len = TRACKS.length;
    state.index = ((index % len) + len) % len;
    state.current = 0;
    renderTrack();
    renderProgress();
  }

  function togglePlay() { setPlaying(!state.playing); }

  function seekToFraction(frac) {
    state.current = Math.min(duration(), Math.max(0, frac * duration()));
    renderProgress();
  }

  function fracFromEvent(e) {
    const r = bar.getBoundingClientRect();
    const x = (e.touches && e.touches[0] ? e.touches[0].clientX : e.clientX) - r.left;
    return r.width ? Math.min(1, Math.max(0, x / r.width)) : 0;
  }

  if (playBtn) playBtn.addEventListener('click', togglePlay);
  if (prevBtn) prevBtn.addEventListener('click', () => goTo(state.index - 1));
  if (nextBtn) nextBtn.addEventListener('click', () => goTo(state.index + 1));

  let dragging = false;
  bar.addEventListener('pointerdown', (e) => {
    dragging = true;
    seekToFraction(fracFromEvent(e));
    bar.setPointerCapture && e.pointerId != null && bar.setPointerCapture(e.pointerId);
  });
  bar.addEventListener('pointermove', (e) => { if (dragging) seekToFraction(fracFromEvent(e)); });
  window.addEventListener('pointerup', () => { dragging = false; });
  bar.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowRight') { state.current = Math.min(duration(), state.current + 5); renderProgress(); }
    else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); renderProgress(); }
    else if (e.key === 'Home') { state.current = 0; renderProgress(); }
    else if (e.key === 'End') { state.current = duration(); renderProgress(); }
    else return;
    e.preventDefault();
  });

  renderTrack();
  renderProgress();
  // カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
  if (!reduceMotion) setPlaying(true);
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「音楽プレイヤー」の効果を追加してください。

# 追加してほしい効果
音楽プレイヤー(チャット & メディア)
回転するジャケット写真と曲名・アーティスト表示、シークバーと前後の曲送りを備えた音楽プレイヤー。再生中はイコライザー風のバーが小さく揺れて動きます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 音楽プレイヤー: ジャケット+曲情報+シークバー+前後送り。3曲を内部で送り再生する -->
<div class="stage">
  <section class="ma-player" aria-label="音楽プレイヤー">
    <div class="ma-player__art-wrap">
      <img class="ma-player__art" src="https://picsum.photos/id/1062/300/300" alt="アルバムアート: 花明かりの午後" width="150" height="150">
      <span class="ma-player__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></span>
    </div>

    <div class="ma-player__info">
      <p class="ma-player__track">花明かりの午後</p>
      <p class="ma-player__artist">yura</p>
    </div>

    <div class="ma-player__bar" tabindex="0" role="slider" aria-label="再生位置" aria-valuemin="0" aria-valuemax="195" aria-valuenow="0" aria-valuetext="0分00秒 / 3分15秒">
      <div class="ma-player__bar-track">
        <div class="ma-player__bar-fill"></div>
        <div class="ma-player__bar-handle"></div>
      </div>
    </div>
    <div class="ma-player__time">
      <span class="ma-player__current">0:00</span>
      <span class="ma-player__dur">3:15</span>
    </div>

    <div class="ma-player__controls">
      <button type="button" class="ma-player__btn ma-player__prev" aria-label="前の曲">
        <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M6 6h2v12H6zM19 6l-9 6 9 6z" fill="currentColor"/></svg>
      </button>

      <button type="button" class="ma-player__btn ma-player__play" aria-label="再生と一時停止を切り替え" aria-pressed="false">
        <svg class="ma-icon ma-icon--play" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
        <svg class="ma-icon ma-icon--pause" viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" hidden><path d="M7 5h4v14H7zM13 5h4v14h-4z" fill="currentColor"/></svg>
      </button>

      <button type="button" class="ma-player__btn ma-player__next" aria-label="次の曲">
        <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M16 6h2v12h-2zM5 6l9 6-9 6z" fill="currentColor"/></svg>
      </button>
    </div>
  </section>
</div>

【CSS】
/* 音楽プレイヤー: 陽だまりのような温かいクリーム基調 */
* { box-sizing: border-box; }

body {
  margin: 0;
  min-height: 100vh;
  padding: 24px;
  display: grid;
  place-items: center;
  font-family: "Hiragino Sans", "Segoe UI", system-ui, -apple-system, sans-serif;
  background:
    radial-gradient(circle at 14% 12%, #ffe7d1 0%, transparent 46%),
    radial-gradient(circle at 88% 18%, #f4dcec 0%, transparent 48%),
    radial-gradient(circle at 50% 100%, #e7f0e6 0%, transparent 55%),
    #fbf7f2;
  color: #2c2620;
}

.stage { display: grid; place-items: center; width: 100%; }

.ma-player {
  width: min(320px, 90vw);
  padding: 26px 26px 22px;
  border-radius: 26px;
  background: #fffdfb;
  box-shadow: 0 20px 50px rgba(90, 62, 35, .14);
  display: grid;
  justify-items: center;
  gap: 4px;
}

.ma-player__art-wrap {
  position: relative;
  width: 150px;
  height: 150px;
  margin-bottom: 14px;
}
.ma-player__art {
  width: 150px;
  height: 150px;
  border-radius: 999px;
  object-fit: cover;
  display: block;
  box-shadow: 0 14px 34px rgba(90, 62, 35, .28), 0 0 0 6px #fffdfb, 0 0 0 8px rgba(90, 62, 35, .1);
  animation: ma-spin 9s linear infinite;
  animation-play-state: paused;
}
.ma-player.is-playing .ma-player__art { animation-play-state: running; }
@keyframes ma-spin { to { transform: rotate(360deg); } }

.ma-player__eq {
  position: absolute;
  right: -2px;
  bottom: 2px;
  display: flex;
  align-items: flex-end;
  gap: 3px;
  height: 18px;
  padding: 5px 6px 4px;
  border-radius: 999px;
  background: rgba(44, 38, 32, .82);
  opacity: 0;
  transition: opacity .25s ease;
}
.ma-player.is-playing .ma-player__eq { opacity: 1; }
.ma-player__eq i {
  display: block;
  width: 2.5px;
  border-radius: 2px;
  background: #ffd9a0;
  animation: ma-bounce 1s ease-in-out infinite;
}
.ma-player__eq i:nth-child(1) { height: 5px; animation-delay: -.6s; }
.ma-player__eq i:nth-child(2) { height: 9px; animation-delay: -.3s; }
.ma-player__eq i:nth-child(3) { height: 6px; animation-delay: -.9s; }
.ma-player__eq i:nth-child(4) { height: 8px; animation-delay: -.15s; }
@keyframes ma-bounce { 0%, 100% { transform: scaleY(.4); } 50% { transform: scaleY(1.2); } }

.ma-player__info { text-align: center; margin-bottom: 14px; }
.ma-player__track { margin: 0 0 3px; font-size: 16px; font-weight: 800; letter-spacing: .01em; }
.ma-player__artist { margin: 0; font-size: 12.5px; color: #9a8c78; }

.ma-player__bar {
  width: 100%;
  padding: 8px 0;
  cursor: pointer;
  outline-offset: 4px;
}
.ma-player__bar-track {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: #efe6da;
}
.ma-player__bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, #f7b071, #ef8a6b);
}
.ma-player__bar-handle {
  position: absolute;
  top: 50%;
  left: 0%;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  border-radius: 999px;
  background: #fff;
  box-shadow: 0 2px 6px rgba(90, 62, 35, .35), 0 0 0 2px #ef8a6b inset;
  transform: translateY(-50%) scale(0);
  transition: transform .15s ease;
}
.ma-player__bar:hover .ma-player__bar-handle,
.ma-player__bar:focus-visible .ma-player__bar-handle { transform: translateY(-50%) scale(1); }

.ma-player__time {
  width: 100%;
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: #9a8c78;
  margin-bottom: 16px;
}

.ma-player__controls {
  display: flex;
  align-items: center;
  gap: 20px;
}
.ma-player__btn {
  display: grid;
  place-items: center;
  border: none;
  background: transparent;
  color: #6b5c47;
  cursor: pointer;
  border-radius: 999px;
  transition: color .18s ease, transform .12s ease, background .18s ease;
}
.ma-player__prev, .ma-player__next { width: 34px; height: 34px; }
.ma-player__prev:hover, .ma-player__next:hover { color: #ef8a6b; background: rgba(239, 138, 107, .1); }
.ma-player__play {
  width: 54px;
  height: 54px;
  color: #fff;
  background: linear-gradient(135deg, #f7b071, #ef8a6b);
  box-shadow: 0 12px 26px rgba(239, 138, 107, .4);
}
.ma-player__play:hover { transform: translateY(-2px); box-shadow: 0 16px 32px rgba(239, 138, 107, .5); }
.ma-player__btn:active { transform: scale(.92); }

.ma-icon--pause[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .ma-player__art { animation: none; }
  .ma-player__eq i { animation: none; }
  .ma-player__btn, .ma-player__bar-handle { transition: none; }
}

【JavaScript】
// 音楽プレイヤー: 実音声は使わず、タイマーで進捗を擬似的に進める。3曲を内部で送り再生
(() => {
  const player = document.querySelector('.ma-player');
  if (!player) return; // null安全

  const art = player.querySelector('.ma-player__art');
  const trackEl = player.querySelector('.ma-player__track');
  const artistEl = player.querySelector('.ma-player__artist');
  const bar = player.querySelector('.ma-player__bar');
  const barFill = player.querySelector('.ma-player__bar-fill');
  const barHandle = player.querySelector('.ma-player__bar-handle');
  const curEl = player.querySelector('.ma-player__current');
  const durEl = player.querySelector('.ma-player__dur');
  const playBtn = player.querySelector('.ma-player__play');
  const prevBtn = player.querySelector('.ma-player__prev');
  const nextBtn = player.querySelector('.ma-player__next');
  const iconPlay = playBtn ? playBtn.querySelector('.ma-icon--play') : null;
  const iconPause = playBtn ? playBtn.querySelector('.ma-icon--pause') : null;

  if (!bar || !barFill || !barHandle || !curEl || !durEl) return;

  const reduceMotion = window.matchMedia ? window.matchMedia('(prefers-reduced-motion: reduce)').matches : false;

  const TRACKS = [
    { title: '花明かりの午後', artist: 'yura', art: 'https://picsum.photos/id/1062/300/300', duration: 195 },
    { title: '夜行バスのブルー', artist: 'Night Owl Parade', art: 'https://picsum.photos/id/1074/300/300', duration: 221 },
    { title: 'そよ風レコード', artist: 'Sunday Milk', art: 'https://picsum.photos/id/1080/300/300', duration: 168 },
  ];

  const TICK_MS = 200;
  const SPEED = 3.6; // デモとして体感しやすい速さで擬似進行させる倍率

  const state = { index: 0, current: 0, playing: false };

  const fmt = (s) => {
    const t = Math.max(0, Math.round(s));
    const m = Math.floor(t / 60);
    const sec = t % 60;
    return `${m}:${String(sec).padStart(2, '0')}`;
  };

  let timer = null;

  function duration() { return TRACKS[state.index].duration; }

  function renderTrack() {
    const t = TRACKS[state.index];
    if (trackEl) trackEl.textContent = t.title;
    if (artistEl) artistEl.textContent = t.artist;
    if (art) { art.src = t.art; art.alt = `アルバムアート: ${t.title}`; }
    durEl.textContent = fmt(t.duration);
    bar.setAttribute('aria-valuemax', String(t.duration));
  }

  function renderProgress() {
    const d = duration();
    const frac = d ? Math.min(1, state.current / d) : 0;
    barFill.style.width = `${frac * 100}%`;
    barHandle.style.left = `${frac * 100}%`;
    bar.setAttribute('aria-valuenow', String(Math.round(state.current)));
    bar.setAttribute('aria-valuetext', `${fmt(state.current)} / ${fmt(d)}`);
    curEl.textContent = fmt(state.current);
  }

  function setPlaying(next) {
    state.playing = next;
    player.classList.toggle('is-playing', next);
    if (playBtn) playBtn.setAttribute('aria-pressed', String(next));
    if (iconPlay) iconPlay.hidden = next;
    if (iconPause) iconPause.hidden = !next;

    clearInterval(timer);
    if (next) {
      timer = setInterval(() => {
        state.current += (TICK_MS / 1000) * SPEED;
        if (state.current >= duration()) { goTo(state.index + 1); return; }
        renderProgress();
      }, TICK_MS);
    }
  }

  function goTo(index) {
    const len = TRACKS.length;
    state.index = ((index % len) + len) % len;
    state.current = 0;
    renderTrack();
    renderProgress();
  }

  function togglePlay() { setPlaying(!state.playing); }

  function seekToFraction(frac) {
    state.current = Math.min(duration(), Math.max(0, frac * duration()));
    renderProgress();
  }

  function fracFromEvent(e) {
    const r = bar.getBoundingClientRect();
    const x = (e.touches && e.touches[0] ? e.touches[0].clientX : e.clientX) - r.left;
    return r.width ? Math.min(1, Math.max(0, x / r.width)) : 0;
  }

  if (playBtn) playBtn.addEventListener('click', togglePlay);
  if (prevBtn) prevBtn.addEventListener('click', () => goTo(state.index - 1));
  if (nextBtn) nextBtn.addEventListener('click', () => goTo(state.index + 1));

  let dragging = false;
  bar.addEventListener('pointerdown', (e) => {
    dragging = true;
    seekToFraction(fracFromEvent(e));
    bar.setPointerCapture && e.pointerId != null && bar.setPointerCapture(e.pointerId);
  });
  bar.addEventListener('pointermove', (e) => { if (dragging) seekToFraction(fracFromEvent(e)); });
  window.addEventListener('pointerup', () => { dragging = false; });
  bar.addEventListener('keydown', (e) => {
    if (e.key === 'ArrowRight') { state.current = Math.min(duration(), state.current + 5); renderProgress(); }
    else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); renderProgress(); }
    else if (e.key === 'Home') { state.current = 0; renderProgress(); }
    else if (e.key === 'End') { state.current = duration(); renderProgress(); }
    else return;
    e.preventDefault();
  });

  renderTrack();
  renderProgress();
  // カード上でも「生きている」ことが伝わるよう既定で再生。モーション低減設定時は静止状態から開始
  if (!reduceMotion) setPlaying(true);
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。