動画プレイヤー

ポスター画像にカスタムコントロールを重ねた動画プレイヤー。再生/一時停止・シークバー・音量調整・全画面ボタンを備え、時間経過とともに再生位置が進みます。

#css#js#media

ライブデモ

コード

HTML
<!-- 動画プレイヤー: ポスター+カスタムコントロール。擬似的に再生位置が進み続けるループ再生 -->
<div class="stage">
  <figure class="mv-player" data-duration="222">
    <div class="mv-player__frame">
      <img class="mv-player__poster" src="https://picsum.photos/id/1043/960/540" alt="動画のポスター画像: 朝霧に包まれた湖畔の山並み" width="960" height="540">
      <div class="mv-player__scrim" aria-hidden="true"></div>

      <div class="mv-player__meta">
        <span class="mv-player__badge">HD</span>
        <span class="mv-player__title">静けさの記録 #04 | 朝霧の湖畔</span>
      </div>

      <button type="button" class="mv-player__big-play" aria-label="再生する">
        <svg viewBox="0 0 24 24" width="28" height="28" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
      </button>

      <div class="mv-player__controls">
        <div class="mv-player__bar" tabindex="0" role="slider" aria-label="再生位置" aria-valuemin="0" aria-valuemax="222" aria-valuenow="0" aria-valuetext="0分00秒 / 3分42秒">
          <div class="mv-player__bar-track">
            <div class="mv-player__bar-fill"></div>
            <div class="mv-player__bar-handle"></div>
          </div>
        </div>

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

          <span class="mv-player__time">
            <span class="mv-player__current">0:00</span><span class="mv-player__sep">&nbsp;/&nbsp;</span><span class="mv-player__duration">3:42</span>
          </span>

          <span class="mv-player__spacer"></span>

          <div class="mv-player__volume">
            <button type="button" class="mv-player__btn mv-player__mute" aria-label="ミュートを切り替え" aria-pressed="false">
              <svg class="mv-icon mv-icon--vol" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/><path d="M16.5 8.5a5 5 0 0 1 0 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
              <svg class="mv-icon mv-icon--mute" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" hidden><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/><path d="M17 10l4.2 4.2M21.2 10L17 14.2" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
            </button>
            <input type="range" class="mv-player__volume-range" min="0" max="100" value="70" step="1" aria-label="音量">
          </div>

          <button type="button" class="mv-player__btn mv-player__fullscreen" aria-label="全画面表示を切り替え" aria-pressed="false">
            <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
          </button>
        </div>
      </div>
    </div>
  </figure>
</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 15% 10%, #1c2333 0%, transparent 45%),
    radial-gradient(circle at 90% 85%, #2a1e33 0%, transparent 50%),
    #0b0c12;
  color: #f4f2ef;
}

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

.mv-player {
  width: min(560px, 92vw);
  margin: 0;
  border-radius: 18px;
  overflow: hidden;
  background: #05060a;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55), 0 2px 0 rgba(255, 255, 255, .04) inset;
}

.mv-player__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
  overflow: hidden;
  cursor: pointer;
  background: #000;
}

.mv-player__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(1.02);
}

.mv-player__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(0, 0, 0, .38) 0%, transparent 26%, transparent 55%, rgba(0, 0, 0, .82) 100%);
  pointer-events: none;
}

.mv-player__meta {
  position: absolute;
  top: 14px;
  left: 16px;
  right: 16px;
  display: flex;
  align-items: center;
  gap: 9px;
  pointer-events: none;
}
.mv-player__badge {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .06em;
  color: #05060a;
  background: rgba(244, 242, 239, .92);
  padding: 3px 7px;
  border-radius: 5px;
}
.mv-player__title {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .01em;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .5);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mv-player__big-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(1);
  width: 60px;
  height: 60px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 999px;
  color: #05060a;
  background: rgba(244, 242, 239, .92);
  cursor: pointer;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  transition: opacity .25s ease, transform .2s cubic-bezier(.22, 1, .36, 1), background .2s ease;
}
.mv-player__big-play svg { transform: translateX(2px); }
.mv-player__big-play:hover { background: #fff; transform: translate(-50%, -50%) scale(1.06); }
.mv-player__big-play:active { transform: translate(-50%, -50%) scale(.96); }
.mv-player.is-playing .mv-player__big-play {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(.8);
}

.mv-player__controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0 16px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* シークバー */
.mv-player__bar {
  position: relative;
  padding: 8px 0;
  cursor: pointer;
  outline-offset: 4px;
}
.mv-player__bar-track {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .28);
}
.mv-player__bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, #ffd9a0, #ff9f6b);
}
.mv-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 8px rgba(0, 0, 0, .4);
  transform: translateY(-50%) scale(0);
  transition: transform .15s ease;
}
.mv-player__bar:hover .mv-player__bar-handle,
.mv-player__bar:focus-visible .mv-player__bar-handle { transform: translateY(-50%) scale(1); }
.mv-player__bar:hover .mv-player__bar-track { height: 5px; }

.mv-player__row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.mv-player__btn {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: #f4f2ef;
  cursor: pointer;
  transition: background .18s ease, transform .12s ease, color .18s ease;
}
.mv-player__btn:hover { background: rgba(255, 255, 255, .14); color: #ffd9a0; }
.mv-player__btn:active { transform: scale(.9); }

.mv-player__time {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: rgba(244, 242, 239, .88);
  white-space: nowrap;
}
.mv-player__sep { color: rgba(244, 242, 239, .5); }
.mv-player__spacer { flex: 1; }

.mv-player__volume {
  display: flex;
  align-items: center;
  gap: 6px;
}
.mv-player__volume-range {
  width: 64px;
  accent-color: #ff9f6b;
  cursor: pointer;
}

.mv-icon--pause[hidden],
.mv-icon--mute[hidden] { display: none; }

/* 全画面APIが使えない環境向けの擬似全画面(見た目のみ) */
.mv-player.is-fake-fullscreen {
  position: fixed;
  inset: 24px;
  z-index: 999;
  width: auto;
  height: calc(100vh - 48px);
}
.mv-player.is-fake-fullscreen .mv-player__frame { height: 100%; aspect-ratio: auto; }

/* コントロール全体は常時表示(タッチ端末でも操作可能) */
@media (hover: hover) {
  .mv-player__controls { opacity: .96; transition: opacity .25s ease; }
}

@media (prefers-reduced-motion: reduce) {
  .mv-player__big-play,
  .mv-player__btn,
  .mv-player__bar-handle { transition: none; }
}
JavaScript
// 動画プレイヤー: 実動画は使わず、タイマーで再生位置を擬似的に進める
(() => {
  const player = document.querySelector('.mv-player');
  if (!player) return; // null安全

  const frame = player.querySelector('.mv-player__frame');
  const playBtn = player.querySelector('.mv-player__play');
  const muteBtn = player.querySelector('.mv-player__mute');
  const fsBtn = player.querySelector('.mv-player__fullscreen');
  const bar = player.querySelector('.mv-player__bar');
  const barFill = player.querySelector('.mv-player__bar-fill');
  const barHandle = player.querySelector('.mv-player__bar-handle');
  const curEl = player.querySelector('.mv-player__current');
  const durEl = player.querySelector('.mv-player__duration');
  const volRange = player.querySelector('.mv-player__volume-range');
  const iconPlay = playBtn ? playBtn.querySelector('.mv-icon--play') : null;
  const iconPause = playBtn ? playBtn.querySelector('.mv-icon--pause') : null;
  const iconVol = muteBtn ? muteBtn.querySelector('.mv-icon--vol') : null;
  const iconMute = muteBtn ? muteBtn.querySelector('.mv-icon--mute') : null;

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

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

  const DURATION = parseFloat(player.dataset.duration) || 222;
  const TICK_MS = 200;
  const SPEED = 3.4; // デモとして体感しやすい速さで擬似進行させる倍率

  const state = {
    current: 0,
    playing: false,
    muted: false,
    volume: volRange ? Number(volRange.value) : 70,
    prevVolume: volRange ? Number(volRange.value) : 70,
  };

  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')}`;
  };

  if (durEl) durEl.textContent = fmt(DURATION);

  let timer = null;

  function render() {
    const frac = Math.min(1, state.current / DURATION);
    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(DURATION)}`);
    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) state.current = 0; // カードとして常に生きて見えるようループ再生
        render();
      }, TICK_MS);
    }
  }

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

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

  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;
  }

  // フレームクリックで再生/一時停止(コントロール部分は伝播を止めて二重発火を防ぐ)
  frame.addEventListener('click', togglePlay);
  const controls = player.querySelector('.mv-player__controls');
  if (controls) controls.addEventListener('click', (e) => e.stopPropagation());

  if (playBtn) playBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePlay(); });

  // シークバー: ドラッグで再生位置を変更
  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); render(); }
    else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); render(); }
    else if (e.key === 'Home') { state.current = 0; render(); }
    else if (e.key === 'End') { state.current = DURATION; render(); }
    else return;
    e.preventDefault();
  });

  // 音量とミュート
  function applyVolumeIcons() {
    const isSilent = state.muted || state.volume === 0;
    if (iconVol) iconVol.hidden = isSilent;
    if (iconMute) iconMute.hidden = !isSilent;
    if (muteBtn) muteBtn.setAttribute('aria-pressed', String(isSilent));
  }
  if (volRange) {
    volRange.addEventListener('input', () => {
      state.volume = Number(volRange.value);
      state.muted = state.volume === 0;
      applyVolumeIcons();
    });
  }
  if (muteBtn) {
    muteBtn.addEventListener('click', () => {
      state.muted = !state.muted;
      if (volRange) {
        if (state.muted) { state.prevVolume = state.volume || state.prevVolume; volRange.value = '0'; state.volume = 0; }
        else { state.volume = state.prevVolume || 70; volRange.value = String(state.volume); }
      }
      applyVolumeIcons();
    });
  }
  applyVolumeIcons();

  // 全画面表示(埋め込み環境では許可されないことがあるため、失敗時は見た目だけの擬似全画面に切替)
  if (fsBtn) {
    fsBtn.addEventListener('click', () => {
      const isFake = player.classList.contains('is-fake-fullscreen');
      if (!document.fullscreenElement && !isFake) {
        if (player.requestFullscreen) {
          player.requestFullscreen().catch(() => player.classList.add('is-fake-fullscreen'));
        } else {
          player.classList.add('is-fake-fullscreen');
        }
      } else if (document.fullscreenElement && document.exitFullscreen) {
        document.exitFullscreen().catch(() => {});
      } else {
        player.classList.remove('is-fake-fullscreen');
      }
      fsBtn.setAttribute('aria-pressed', String(!isFake));
    });
  }

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

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

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

# 追加してほしい効果
動画プレイヤー(チャット & メディア)
ポスター画像にカスタムコントロールを重ねた動画プレイヤー。再生/一時停止・シークバー・音量調整・全画面ボタンを備え、時間経過とともに再生位置が進みます。

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

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 動画プレイヤー: ポスター+カスタムコントロール。擬似的に再生位置が進み続けるループ再生 -->
<div class="stage">
  <figure class="mv-player" data-duration="222">
    <div class="mv-player__frame">
      <img class="mv-player__poster" src="https://picsum.photos/id/1043/960/540" alt="動画のポスター画像: 朝霧に包まれた湖畔の山並み" width="960" height="540">
      <div class="mv-player__scrim" aria-hidden="true"></div>

      <div class="mv-player__meta">
        <span class="mv-player__badge">HD</span>
        <span class="mv-player__title">静けさの記録 #04 | 朝霧の湖畔</span>
      </div>

      <button type="button" class="mv-player__big-play" aria-label="再生する">
        <svg viewBox="0 0 24 24" width="28" height="28" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>
      </button>

      <div class="mv-player__controls">
        <div class="mv-player__bar" tabindex="0" role="slider" aria-label="再生位置" aria-valuemin="0" aria-valuemax="222" aria-valuenow="0" aria-valuetext="0分00秒 / 3分42秒">
          <div class="mv-player__bar-track">
            <div class="mv-player__bar-fill"></div>
            <div class="mv-player__bar-handle"></div>
          </div>
        </div>

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

          <span class="mv-player__time">
            <span class="mv-player__current">0:00</span><span class="mv-player__sep">&nbsp;/&nbsp;</span><span class="mv-player__duration">3:42</span>
          </span>

          <span class="mv-player__spacer"></span>

          <div class="mv-player__volume">
            <button type="button" class="mv-player__btn mv-player__mute" aria-label="ミュートを切り替え" aria-pressed="false">
              <svg class="mv-icon mv-icon--vol" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/><path d="M16.5 8.5a5 5 0 0 1 0 7" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
              <svg class="mv-icon mv-icon--mute" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" hidden><path d="M4 9v6h4l5 5V4L8 9H4z" fill="currentColor"/><path d="M17 10l4.2 4.2M21.2 10L17 14.2" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
            </button>
            <input type="range" class="mv-player__volume-range" min="0" max="100" value="70" step="1" aria-label="音量">
          </div>

          <button type="button" class="mv-player__btn mv-player__fullscreen" aria-label="全画面表示を切り替え" aria-pressed="false">
            <svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
          </button>
        </div>
      </div>
    </div>
  </figure>
</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 15% 10%, #1c2333 0%, transparent 45%),
    radial-gradient(circle at 90% 85%, #2a1e33 0%, transparent 50%),
    #0b0c12;
  color: #f4f2ef;
}

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

.mv-player {
  width: min(560px, 92vw);
  margin: 0;
  border-radius: 18px;
  overflow: hidden;
  background: #05060a;
  box-shadow: 0 24px 60px rgba(0, 0, 0, .55), 0 2px 0 rgba(255, 255, 255, .04) inset;
}

.mv-player__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  width: 100%;
  overflow: hidden;
  cursor: pointer;
  background: #000;
}

.mv-player__poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(1.02);
}

.mv-player__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(0, 0, 0, .38) 0%, transparent 26%, transparent 55%, rgba(0, 0, 0, .82) 100%);
  pointer-events: none;
}

.mv-player__meta {
  position: absolute;
  top: 14px;
  left: 16px;
  right: 16px;
  display: flex;
  align-items: center;
  gap: 9px;
  pointer-events: none;
}
.mv-player__badge {
  font-size: 10px;
  font-weight: 800;
  letter-spacing: .06em;
  color: #05060a;
  background: rgba(244, 242, 239, .92);
  padding: 3px 7px;
  border-radius: 5px;
}
.mv-player__title {
  font-size: 13.5px;
  font-weight: 600;
  letter-spacing: .01em;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .5);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.mv-player__big-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%) scale(1);
  width: 60px;
  height: 60px;
  display: grid;
  place-items: center;
  border: none;
  border-radius: 999px;
  color: #05060a;
  background: rgba(244, 242, 239, .92);
  cursor: pointer;
  box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  transition: opacity .25s ease, transform .2s cubic-bezier(.22, 1, .36, 1), background .2s ease;
}
.mv-player__big-play svg { transform: translateX(2px); }
.mv-player__big-play:hover { background: #fff; transform: translate(-50%, -50%) scale(1.06); }
.mv-player__big-play:active { transform: translate(-50%, -50%) scale(.96); }
.mv-player.is-playing .mv-player__big-play {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(.8);
}

.mv-player__controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 0 16px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

/* シークバー */
.mv-player__bar {
  position: relative;
  padding: 8px 0;
  cursor: pointer;
  outline-offset: 4px;
}
.mv-player__bar-track {
  position: relative;
  height: 4px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .28);
}
.mv-player__bar-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0%;
  border-radius: 999px;
  background: linear-gradient(90deg, #ffd9a0, #ff9f6b);
}
.mv-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 8px rgba(0, 0, 0, .4);
  transform: translateY(-50%) scale(0);
  transition: transform .15s ease;
}
.mv-player__bar:hover .mv-player__bar-handle,
.mv-player__bar:focus-visible .mv-player__bar-handle { transform: translateY(-50%) scale(1); }
.mv-player__bar:hover .mv-player__bar-track { height: 5px; }

.mv-player__row {
  display: flex;
  align-items: center;
  gap: 12px;
}

.mv-player__btn {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: #f4f2ef;
  cursor: pointer;
  transition: background .18s ease, transform .12s ease, color .18s ease;
}
.mv-player__btn:hover { background: rgba(255, 255, 255, .14); color: #ffd9a0; }
.mv-player__btn:active { transform: scale(.9); }

.mv-player__time {
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  color: rgba(244, 242, 239, .88);
  white-space: nowrap;
}
.mv-player__sep { color: rgba(244, 242, 239, .5); }
.mv-player__spacer { flex: 1; }

.mv-player__volume {
  display: flex;
  align-items: center;
  gap: 6px;
}
.mv-player__volume-range {
  width: 64px;
  accent-color: #ff9f6b;
  cursor: pointer;
}

.mv-icon--pause[hidden],
.mv-icon--mute[hidden] { display: none; }

/* 全画面APIが使えない環境向けの擬似全画面(見た目のみ) */
.mv-player.is-fake-fullscreen {
  position: fixed;
  inset: 24px;
  z-index: 999;
  width: auto;
  height: calc(100vh - 48px);
}
.mv-player.is-fake-fullscreen .mv-player__frame { height: 100%; aspect-ratio: auto; }

/* コントロール全体は常時表示(タッチ端末でも操作可能) */
@media (hover: hover) {
  .mv-player__controls { opacity: .96; transition: opacity .25s ease; }
}

@media (prefers-reduced-motion: reduce) {
  .mv-player__big-play,
  .mv-player__btn,
  .mv-player__bar-handle { transition: none; }
}

【JavaScript】
// 動画プレイヤー: 実動画は使わず、タイマーで再生位置を擬似的に進める
(() => {
  const player = document.querySelector('.mv-player');
  if (!player) return; // null安全

  const frame = player.querySelector('.mv-player__frame');
  const playBtn = player.querySelector('.mv-player__play');
  const muteBtn = player.querySelector('.mv-player__mute');
  const fsBtn = player.querySelector('.mv-player__fullscreen');
  const bar = player.querySelector('.mv-player__bar');
  const barFill = player.querySelector('.mv-player__bar-fill');
  const barHandle = player.querySelector('.mv-player__bar-handle');
  const curEl = player.querySelector('.mv-player__current');
  const durEl = player.querySelector('.mv-player__duration');
  const volRange = player.querySelector('.mv-player__volume-range');
  const iconPlay = playBtn ? playBtn.querySelector('.mv-icon--play') : null;
  const iconPause = playBtn ? playBtn.querySelector('.mv-icon--pause') : null;
  const iconVol = muteBtn ? muteBtn.querySelector('.mv-icon--vol') : null;
  const iconMute = muteBtn ? muteBtn.querySelector('.mv-icon--mute') : null;

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

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

  const DURATION = parseFloat(player.dataset.duration) || 222;
  const TICK_MS = 200;
  const SPEED = 3.4; // デモとして体感しやすい速さで擬似進行させる倍率

  const state = {
    current: 0,
    playing: false,
    muted: false,
    volume: volRange ? Number(volRange.value) : 70,
    prevVolume: volRange ? Number(volRange.value) : 70,
  };

  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')}`;
  };

  if (durEl) durEl.textContent = fmt(DURATION);

  let timer = null;

  function render() {
    const frac = Math.min(1, state.current / DURATION);
    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(DURATION)}`);
    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) state.current = 0; // カードとして常に生きて見えるようループ再生
        render();
      }, TICK_MS);
    }
  }

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

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

  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;
  }

  // フレームクリックで再生/一時停止(コントロール部分は伝播を止めて二重発火を防ぐ)
  frame.addEventListener('click', togglePlay);
  const controls = player.querySelector('.mv-player__controls');
  if (controls) controls.addEventListener('click', (e) => e.stopPropagation());

  if (playBtn) playBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePlay(); });

  // シークバー: ドラッグで再生位置を変更
  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); render(); }
    else if (e.key === 'ArrowLeft') { state.current = Math.max(0, state.current - 5); render(); }
    else if (e.key === 'Home') { state.current = 0; render(); }
    else if (e.key === 'End') { state.current = DURATION; render(); }
    else return;
    e.preventDefault();
  });

  // 音量とミュート
  function applyVolumeIcons() {
    const isSilent = state.muted || state.volume === 0;
    if (iconVol) iconVol.hidden = isSilent;
    if (iconMute) iconMute.hidden = !isSilent;
    if (muteBtn) muteBtn.setAttribute('aria-pressed', String(isSilent));
  }
  if (volRange) {
    volRange.addEventListener('input', () => {
      state.volume = Number(volRange.value);
      state.muted = state.volume === 0;
      applyVolumeIcons();
    });
  }
  if (muteBtn) {
    muteBtn.addEventListener('click', () => {
      state.muted = !state.muted;
      if (volRange) {
        if (state.muted) { state.prevVolume = state.volume || state.prevVolume; volRange.value = '0'; state.volume = 0; }
        else { state.volume = state.prevVolume || 70; volRange.value = String(state.volume); }
      }
      applyVolumeIcons();
    });
  }
  applyVolumeIcons();

  // 全画面表示(埋め込み環境では許可されないことがあるため、失敗時は見た目だけの擬似全画面に切替)
  if (fsBtn) {
    fsBtn.addEventListener('click', () => {
      const isFake = player.classList.contains('is-fake-fullscreen');
      if (!document.fullscreenElement && !isFake) {
        if (player.requestFullscreen) {
          player.requestFullscreen().catch(() => player.classList.add('is-fake-fullscreen'));
        } else {
          player.classList.add('is-fake-fullscreen');
        }
      } else if (document.fullscreenElement && document.exitFullscreen) {
        document.exitFullscreen().catch(() => {});
      } else {
        player.classList.remove('is-fake-fullscreen');
      }
      fsBtn.setAttribute('aria-pressed', String(!isFake));
    });
  }

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

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

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