# CASTA — 要件定義書 兼 Claude Code 実装指示書

**版数** 1.0（DBレス構成 / 記事埋め込み型デモ）／ **作成日** 2026-08-17 ／ **発行** ソウゾウ合同会社
**用途** SEO記事「【デモで試せる】Claude Codeで動画配信アプリを開発する方法と費用｜動画つき資料【2026年】」に組み込む実演デモ

---

## このドキュメントの使い方（Claude Code へ）

これ1枚が、規約・仕様・タスクのすべて。リポジトリ直下に `CLAUDE.md` として置き、毎回読み込むこと。

- **実装は「13. 実装タスク」の順に進める。フェーズを飛ばさない。**
- 各タスクの括弧内は要件ID。着手前に該当章を読むこと。
- **仕様がここに書かれていない場合は、推測で実装せず質問する。**
- スコープ外（4章 Won't have）は、思いついても実装しない。
- 「デモだから」を理由に品質を落とす判断はしない。

> **姉妹プロジェクトとの関係**
> HIREBASE（求人サイト）／ RELATE（顧客管理）に続く3本目。**3つ並べて「同じテンプレ」と思われた時点で失敗。**
> HIREBASE＝明朝・余白・静けさ（読ませる）／ RELATE＝ゴシック・高密度・等幅数字（操作させる）／
> **CASTA＝ダーク・映像が主役・UIが消える（見せる）。** 9章のデザイン要件は意図的に第3の方向に振っている。トークンを流用しないこと。

---

## 目次

1. [プロジェクト概要](#1-プロジェクト概要)
2. [記事への組み込み設計](#2-記事への組み込み設計)
3. [アーキテクチャ方針](#3-アーキテクチャ方針)
4. [スコープ定義](#4-スコープ定義)
5. [ロールとデモ切替](#5-ロールとデモ切替)
6. [画面一覧とユーザーフロー](#6-画面一覧とユーザーフロー)
7. [機能要件](#7-機能要件)
8. [データ設計](#8-データ設計)
9. [デザイン要件](#9-デザイン要件)
10. [技術要件・ディレクトリ構成](#10-技術要件ディレクトリ構成)
11. [非機能要件](#11-非機能要件)
12. [費用設計](#12-費用設計)
13. [実装タスク](#13-実装タスク)
14. [受入基準](#14-受入基準)

---

# 1. プロジェクト概要

## 背景と目的

対象記事のキーワード意図は「動画配信アプリを**作りたい**が、**作り方と費用が分からない**」。この検索意図に対し、テキストの説明だけでは以下2点が伝わらない。

1. **どこまでのものが作れるのか** — プレーヤーの完成度、会員制の作り込み、管理画面の実用性
2. **なぜ費用が読みにくいのか** — 動画配信は開発費より**運用費（配信コスト）の設計**が本質。ここを説明できるかが専門性の証明になる

そこで記事内に、実際に動画が再生できるデモを組み込む。**動画配信アプリは「モックでは絶対に伝わらない」領域**であり、実際に再生が始まった瞬間に信頼が生まれる。

| 目的 | 内容 |
|---|---|
| 実装力の証明 | HLS配信・画質切替・レジューム再生・字幕・チャプターまで実装した本物のプレーヤーを見せる |
| ビジネス機能の証明 | 会員制ペイウォール、プラン管理、視聴分析。「収益化まで作れる」ことを示す |
| 費用説明の裏付け | デモ自体の配信コストを実測値として記事に載せる。**自分たちが払っている数字で語る** |
| リード獲得 | デモ内から記事・問い合わせへの導線を常設し、GA4で計測する |

## プロダクト定義

| 項目 | 内容 |
|---|---|
| プロダクト名 | CASTA（キャスタ）※仮称 |
| 一言定義 | 会員制サブスクリプション型の動画配信プラットフォーム |
| 想定業態 | オンラインスクール、社内研修、士業・コンサルの会員制コンテンツ、専門メディア |
| 提供形態 | レスポンシブWebアプリ。**フロントエンド完結（サーバー側の永続化なし）＋ 静的動画配信** |
| 主戦場 | 記事からの流入。**モバイル比率が高い前提で設計する**（SEO記事の読者はスマホが主） |
| 想定利用者 | 記事の読者。登録なしで即座に再生でき、会員機能もロール切替で体験できる |

## プロダクトコンセプト

> **「アプリを見せない。動画を見せる。」**
>
> 動画配信サービスのUIは、うまくいっているとき存在に気づかれない。CASTA は、画面を暗く落とし、装飾を削り、サムネイルと映像だけに光を渡す。操作は身体が覚えているもの（スペースで再生、矢印でシーク）をそのまま採用する。

## デモとしての成功条件

1. **記事を読んでいる人が、3秒以内に再生を開始できる**
2. **プレーヤーが本物である** — 画質切替、速度、字幕、チャプター、レジューム。ここが安っぽいと全部が嘘になる
3. **有料ゲートが体験できる** — 無料会員で有料コンテンツを開くと、プレビュー後にペイウォールが出る
4. **管理側も見られる** — 配信管理と視聴分析まで、ロール切替で確認できる
5. **記事に戻れる／問い合わせできる** — デモが行き止まりにならない

---

# 2. 記事への組み込み設計

**この章が本プロジェクト固有の最重要要件。** 単体で作って後から埋め込むのではなく、埋め込み前提で設計する。

## 配置方針

記事内で**2段構え**にする。

| 段 | 配置 | 中身 | 目的 |
|---|---|---|---|
| ① 記事内インライン | 記事の冒頭〜中盤、`<iframe>` | **プレーヤー単体の軽量版**（1本の動画 + 主要操作） | スクロール中に「動く」ものを見せ、離脱を防ぐ |
| ② 全画面デモ | 「デモを開く」カード → **別タブ** | アプリ全体（カタログ・会員・管理画面） | じっくり触ってもらう |

**アプリ全体を記事内 iframe に入れない。** 理由は3つ。

- 動画を含むアプリ全体は初期ロードが重く、記事本体のCore Web Vitalsを悪化させる
- iframe 内のスクロールと記事のスクロールが競合し、モバイルで操作不能になる
- アプリ内で画面遷移すると、読者が記事のどこにいたか分からなくなる

## 埋め込みモード（`?embed=1`）

同一アプリを、URLパラメータで2つのモードで動作させる。**別リポジトリに分けない。**

| | 通常モード | 埋め込みモード `?embed=1` |
|---|---|---|
| ヘッダー・サイドバー | 表示 | **非表示** |
| ロール切替バー | 表示 | 非表示 |
| 表示内容 | アプリ全体 | 指定された1動画のプレーヤーのみ |
| 動画の自動再生 | しない | **しない（音声ありの自動再生は禁止）** |
| 高さ | 100vh | 親に合わせる（16:9 + 操作バー分） |
| 遷移 | 通常 | **アプリ内遷移を禁止。** 操作は再生関連のみ。「全画面で開く」ボタンは `target="_blank"` |

```html
<!-- 記事側の埋め込みコード -->
<iframe
  src="https://casta.souzoh-demo.com/watch/ep-001?embed=1"
  width="100%" style="aspect-ratio:16/9; border:0"
  loading="lazy"
  allow="fullscreen; picture-in-picture"
  title="CASTA 動画プレーヤーのデモ"></iframe>
```

## 記事本体のパフォーマンスを壊さない

| ID | 要件 |
|---|---|
| EMB-01 | 記事内 iframe は `loading="lazy"` とし、ビューポート接近まで読み込まないこと |
| EMB-02 | 埋め込みモードの初期ロードは、**ポスター画像1枚 + 再生ボタン**のみ。動画のマニフェスト取得は再生ボタン押下後に開始すること |
| EMB-03 | 埋め込みモードのJS初期バンドルは 80KB以下（gzip後）とすること。プレーヤー本体（hls.js）は再生ボタン押下時に動的import すること |
| EMB-04 | 埋め込み iframe が記事の LCP 要素にならないこと。ポスター画像は記事側で `preload` しない |
| EMB-05 | 自動再生は行わない。**音声ありの自動再生は明確に禁止** |

## 「動画つき資料」との一体化（重要な設計判断）

記事タイトルの「動画つき資料」を、別途用意するのではなく**デモの中身そのものにする。**

- デモに投入するシリーズの1本目を「**Claude Codeで動画配信アプリを作る**」の解説動画にする
- そのエピソードに、資料PDF（設計図・費用試算シート）を**添付ファイルとして紐付ける**（FR-208）
- 読者は「デモを触る」と「資料を見る」を1つの体験として行う

**これにより、デモ機能（添付資料ダウンロード）と記事コンテンツ（動画つき資料）が同じ実装で成立する。** 二重に作らない。

## 導線と計測

| ID | 要件 |
|---|---|
| EMB-10 | 全画面デモの上部に、**元の記事に戻るリンク**を常設すること |
| EMB-11 | デモ内の適切な位置（カタログ下部、ペイウォール内、管理画面）に問い合わせ導線を配置すること。**再生中のプレーヤー上には出さない** |
| EMB-12 | GA4 で以下を計測すること：`demo_open`（デモを開く）／`video_play`／`video_progress`（25/50/75/100%）／`paywall_view`／`role_switch`／`form_click`／`line_click` |
| EMB-13 | `form_click` と `line_click` は、既存メディアのキーイベント名と揃えること（媒体横断で集計するため） |

## SEO上の扱い

| ID | 要件 |
|---|---|
| EMB-20 | デモは記事とは別ドメイン／サブドメインで配信し、記事の評価と競合しないよう **固有の title / description** を持つこと |
| EMB-21 | デモ本体は `index` 可とするが、埋め込みモードのURL（`?embed=1`）は `noindex` とすること |
| EMB-22 | デモの各ページから記事へのリンクを持ち、記事側にも `rel="nofollow"` を付けずリンクすること |
| EMB-23 | 記事側に `VideoObject` 構造化データを設置すること（name / description / thumbnailUrl / uploadDate / duration / contentUrl） |

---

# 3. アーキテクチャ方針

## 基本方針

**バックエンドとデータベースを持たない。ただし動画ファイルだけは実在させる。**

| 一般的な構成 | 本プロジェクト |
|---|---|
| PostgreSQL / Supabase | シードデータ（TypeScript）+ ブラウザ内ストア |
| 認証・会員管理 | デモ用ロール切替（ワンクリック） |
| 決済（Stripe） | プラン選択〜完了画面まで再現。課金処理なし |
| 動画アップロード・エンコード | **UIとエンコード進捗を再現。実ファイルは事前に用意した固定素材** |
| 署名付きURL・DRM | 実装しない。ペイウォールはUI上のゲートとして再現 |
| **動画の配信** | **これだけは本物。** 静的オブジェクトストレージ + CDN から HLS で配信する |

## 動画だけは実在させる理由

動画配信アプリのデモで動画が再生できないのは、求人サイトのデモで求人が0件なのと同じ。**再生が始まるかどうかが、このデモの成否そのもの。**

ただし「実在させる」のは配信であって、バックエンドではない。HLS は `.m3u8` プレイリストと `.ts` セグメントという**ただの静的ファイル**なので、オブジェクトストレージに置くだけで配信できる。サーバーは不要。

## 動画素材の方針

| ID | 要件 |
|---|---|
| VID-01 | 動画素材は**ソウゾウが権利を持つもの**を使う。既存YouTubeチャンネルの動画を再エンコードして流用することを第一候補とする |
| VID-02 | 自社素材で埋まらない分は、商用利用可のライセンスが明示された素材のみを使う。**取得元とライセンス種別を `docs/assets-license.md` に記録すること** |
| VID-03 | 本数は 12〜16本。1本あたり 2〜8分。合計 60分以内に収める |
| VID-04 | 各動画に 3段階の画質（1080p / 720p / 360p）を用意し、画質切替を実演できるようにすること |
| VID-05 | 少なくとも3本に**日本語字幕（WebVTT）**を付けること。うち1本は英語字幕も付け、字幕切替を実演できるようにすること |
| VID-06 | 少なくとも5本に**チャプター（WebVTT chapters）**を設定すること |
| VID-07 | 1本目は「Claude Codeで動画配信アプリを作る」の解説動画とし、資料PDFを添付すること（2章参照） |

## エンコード仕様

```
入力: 1080p mp4
出力: HLS（fMP4 / CMAF）
  ├ 1080p  H.264 High  3,500kbps  ─┐
  ├  720p  H.264 Main  1,800kbps   ├ master.m3u8 に3レンディション
  └  360p  H.264 Base    600kbps  ─┘
  セグメント長 4秒 / GOP 2秒 / 音声 AAC 128kbps
  サムネイル: 各動画から3枚（ポスター / ホバープレビュー用スプライト / OGP用）
```

エンコードは ffmpeg のスクリプト（`scripts/encode.sh`）としてリポジトリに含める。**これ自体が記事の「方法」パートの説明材料になる。**

## 配信先

| 候補 | 判断 |
|---|---|
| **Cloudflare R2 + Cloudflare CDN** | **採用。** エグレス（転送量）課金がないため、デモが伸びてもコストが跳ねない |
| Vercel から静的配信 | 不採用。転送量課金が視聴時間に比例して増える |
| Cloudflare Stream / Mux 等の専用サービス | 不採用（本デモでは）。ただし12章で比較対象として扱い、記事の費用パートで解説する |

## 差し替え可能性の担保

**データアクセスは必ず `lib/repo/` のリポジトリ層を経由すること。** コンポーネントからストアを直接触らない。

```
[ コンポーネント ]
        ↓  呼ぶのはこの層だけ
[ lib/repo/*.ts ]   ← 全メソッドを async
        ↓
[ lib/store/*.ts ]  ← Zustand + persist（localStorage）
        ↓
[ lib/seed/*.ts ]   ← 初期データ（動画URLはR2を指す）
```

リポジトリの各メソッドは、中身が同期処理でも `async` で定義する。実案件へ転用する際、リポジトリの実装だけをAPI呼び出しに差し替えれば、UI層は変更不要。

## 永続化の範囲

| 対象 | 挙動 |
|---|---|
| シードデータ（シリーズ・エピソード・会員） | 初期投入。管理画面から編集可能 |
| **視聴進捗・視聴履歴** | localStorage に保存。**リロードしても「続きから」再生できる** |
| マイリスト、プラン契約状態、通知既読 | localStorage に保存 |
| アップロードした動画 | ファイル名とメタデータのみ保持。**再生は既存素材で代替**（FR-403） |
| リセット | ヘッダーの「デモをリセット」で localStorage をクリア |

**擬似ディレイ** は 150〜400ms。ただし**再生開始に関わる操作にはディレイを入れない**（動画配信で待たされるのは致命的な体験のため）。

---

# 4. スコープ定義

## 視聴者向け機能

| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-V01 | 動画プレーヤー | HLS再生、画質切替、再生速度、音量、全画面、PiP、キーボード操作 | Must |
| F-V02 | レジューム再生 | 前回の停止位置から再開。「最初から見る」も選べる | Must |
| F-V03 | 字幕 | WebVTT。日本語／英語／オフの切替、表示サイズ・背景の調整 | Must |
| F-V04 | チャプター | シークバー上のチャプター区切り、チャプター一覧からのジャンプ | Must |
| F-V05 | シークバープレビュー | シークバーのホバー／ドラッグでサムネイルを表示 | Should |
| F-V06 | 次のエピソード | 終了30秒前にカード表示、自動再生（設定でOFF可） | Should |
| F-V07 | トップページ | ヒーロー、続きから見る、カテゴリ別レーン、ランキング、新着 | Must |
| F-V08 | シリーズ詳細 | エピソード一覧、各話の視聴進捗、シリーズ全体の進捗率 | Must |
| F-V09 | 検索・絞り込み | フリーワード、カテゴリ、タグ、再生時間、視聴状態（未視聴／視聴中／視聴済） | Must |
| F-V10 | マイリスト | シリーズ・エピソードの保存 | Should |
| F-V11 | 視聴履歴 | 時系列の履歴、進捗率、履歴の削除 | Should |
| F-V12 | 添付資料 | エピソードに紐づくPDF・スライドの閲覧とダウンロード | Must |
| F-V13 | ペイウォール | 有料コンテンツはプレビュー（既定3分）後にゲート表示 | Must |
| F-V14 | プラン・契約 | プラン比較、選択、決済フォーム、完了画面（課金処理なし） | Must |
| F-V15 | アカウント設定 | 再生設定（既定画質・速度・自動再生・字幕）、通知設定 | Should |
| F-V16 | メモ機能 | 再生中にタイムスタンプ付きメモを残し、クリックでその位置へジャンプ | Could |

## 配信管理（運営）向け機能

| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-A01 | 管理ダッシュボード | 本日の再生数、公開待ち、人気コンテンツ、会員数推移 | Must |
| F-A02 | 動画アップロード | ドロップ、**エンコード進捗の再現**（待機→変換→完了）、失敗ケースの再現 | Must |
| F-A03 | エピソード編集 | タイトル、説明、サムネイル選択、公開設定、公開予約、無料／有料区分 | Must |
| F-A04 | シリーズ管理 | シリーズの作成、エピソードの並び替え、カテゴリ・タグ付与 | Must |
| F-A05 | 字幕編集 | 字幕ファイルのアップロード、タイムライン上での文言修正、プレビュー | Should |
| F-A06 | チャプター編集 | 再生位置を見ながらチャプターを追加・編集 | Should |
| F-A07 | 添付資料管理 | エピソードへのファイル紐付け、公開範囲設定 | Must |
| F-A08 | 会員管理 | 会員一覧、プラン、視聴状況、停止 | Should |
| F-A09 | プラン管理 | プランの作成・編集、価格、視聴可能範囲 | Should |
| F-A10 | 視聴分析 | 再生数・視聴完了率・**離脱ヒートマップ**・デバイス別・流入元 | Must |
| F-A11 | 一括操作 | 複数エピソードの公開／非公開、カテゴリ付与 | Should |

## 共通・基盤

| ID | 機能 | 概要 | 優先度 |
|---|---|---|---|
| F-S01 | ロール切替 | 未ログイン／無料会員／有料会員／運営 をワンクリック切替 | Must |
| F-S02 | 埋め込みモード | `?embed=1` でプレーヤー単体表示（2章） | Must |
| F-S03 | 記事への導線 | 元記事へのリンク、問い合わせ導線 | Must |
| F-S04 | デモリセット | localStorage をクリアして初期状態へ | Must |
| F-S05 | 通知センター | 新着エピソード、シリーズ更新 | Could |
| F-S06 | ガイドツアー | 初回訪問時に「何を試せるか」を3ステップで案内 | Should |

## 対象外（Won't have）

| 項目 | 理由 |
|---|---|
| データベース・バックエンドAPI | 3章の方針に基づく |
| 本物の認証・実際の課金処理 | ロール切替と完了画面で代替 |
| DRM・署名付きURL・視聴制限の実効化 | **デモでは実装しない。** ただし12章で必要性とコストに触れる |
| ライブ配信・リアルタイム配信 | VODに限定。要望が出た場合は別フェーズ |
| 実際のトランスコード処理 | 進捗UIの再現に留める。素材は事前エンコード |
| コメント・レビュー・コミュニティ機能 | 対象外 |
| ダウンロード視聴（オフライン再生） | 対象外 |
| 多言語UI | 日本語のみ（字幕の多言語は対象） |
| ネイティブアプリ・TVアプリ | レスポンシブWebで対応 |

> **スコープリスク：** 動画配信は「ライブ配信も」「DRMも」と広がりやすい。**どちらも単体で数週間〜の領域**。要望が出た場合は12章の費用表を使って別見積として提示し、この文書のスコープは変えない。

---

# 5. ロールとデモ切替

## ロール定義

| ロールID | 名称 | 視聴できる範囲 | 特徴 |
|---|---|---|---|
| `guest` | 未ログイン | 無料コンテンツのみ。有料はプレビュー3分でゲート | 会員登録の導線を体験できる |
| `free` | 無料会員 | 無料コンテンツ全編 + マイリスト・視聴履歴 | 有料コンテンツでペイウォールに当たる |
| `premium` | 有料会員 | **全コンテンツ**。添付資料もすべて取得可 | 制限なしの体験 |
| `admin` | 運営 | 全コンテンツ + 管理画面 | 配信管理・分析 |

## ロールを跨ぐ体験（必ず動くようにする）

1. **未ログインで有料エピソードを再生** → 3分でペイウォール → 有料会員に切替 → **同じ位置から続きが再生される**
2. **無料会員で途中まで視聴して離脱** → リロード → **「続きから見る」に表示され、停止位置から再開する**
3. **運営でエピソードを「非公開」に変更** → 視聴者に切替 → **カタログから消えている**
4. **運営で新規エピソードを公開予約**（1分後） → 待つ → **視聴者側のカタログに出現する**
5. **視聴した動画が、運営の視聴分析にカウントされ、離脱ヒートマップに反映される**

## デモ切替バー（F-S01）

画面上部に常時表示する固定バー。**ただし埋め込みモードでは非表示。**

- 現在のロール表示と4ロールの切替
- 「デモをリセット」ボタン（確認ダイアログ付き）
- **元記事へ戻るリンク**
- 「これはデモです」の明示

**デザイン上の扱い：** アプリ本体がダークテーマなので、切替バーは**逆に明るいグレー**にして区別する。プロダクトのトークンを使わない。

---

# 6. 画面一覧とユーザーフロー

全28画面。画面IDはディレクトリ構成と 1:1 で対応させる。

## 視聴者画面

| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-001 | トップ | `/` | ヒーロー（自動再生なしの背景動画）、続きから見る、カテゴリ別レーン、ランキング、新着 |
| SC-002 | 視聴（プレーヤー） | `/watch/[episodeId]` | プレーヤー、エピソード情報、チャプター一覧、添付資料、次の話、同シリーズ |
| SC-003 | 埋め込みプレーヤー | `/watch/[episodeId]?embed=1` | プレーヤーのみ。全画面デモへのリンク |
| SC-004 | シリーズ詳細 | `/series/[seriesId]` | シリーズ概要、エピソード一覧（進捗付き）、全体進捗率、マイリスト追加 |
| SC-005 | カタログ | `/browse` | カテゴリ絞り込み、並び替え、グリッド表示 |
| SC-006 | 検索結果 | `/search` | フリーワード、絞り込みチップ、シリーズ／エピソードのタブ |
| SC-007 | カテゴリ一覧 | `/browse/[categorySlug]` | カテゴリ別のシリーズ一覧 |
| SC-010 | マイページ | `/my` | 続きから見る、マイリスト、視聴履歴のタブ |
| SC-011 | 視聴履歴 | `/my/history` | 時系列、進捗率、削除 |
| SC-012 | マイリスト | `/my/list` | 保存したシリーズ・エピソード |
| SC-013 | メモ一覧 | `/my/notes` | タイムスタンプ付きメモ。クリックで該当位置へ |
| SC-014 | 再生設定 | `/my/settings` | 既定画質・速度・自動再生・字幕表示設定 |
| SC-020 | プラン比較 | `/plans` | 3プランの比較表、視聴可能範囲、FAQ |
| SC-021 | 申込フォーム | `/plans/[planId]/checkout` | 会員情報、支払い方法（入力UIのみ）、確認 |
| SC-022 | 申込完了 | `/plans/complete` | 完了、視聴開始への導線 |
| SC-023 | 契約状況 | `/my/subscription` | 現在のプラン、変更、解約（デモ上での状態変更） |

## 管理画面

| 画面ID | 画面名 | パス | 主要要素 |
|---|---|---|---|
| SC-100 | 管理ダッシュボード | `/admin` | 本日の再生数、公開待ち、人気コンテンツ、会員数推移 |
| SC-110 | エピソード一覧 | `/admin/episodes` | ステータス別タブ、一括操作、絞り込み |
| SC-111 | アップロード | `/admin/episodes/upload` | ドロップ、**エンコード進捗の再現**、失敗ケース |
| SC-112 | エピソード編集 | `/admin/episodes/[id]` | 基本情報、サムネイル選択、公開設定、公開予約、無料／有料 |
| SC-113 | 字幕編集 | `/admin/episodes/[id]/captions` | タイムライン、行ごとの文言修正、プレビュー |
| SC-114 | チャプター編集 | `/admin/episodes/[id]/chapters` | 再生位置を見ながら追加・編集 |
| SC-115 | 添付資料管理 | `/admin/episodes/[id]/files` | ファイル紐付け、公開範囲 |
| SC-120 | シリーズ管理 | `/admin/series` | シリーズCRUD、エピソードの並び替え |
| SC-130 | 会員管理 | `/admin/members` | 一覧、プラン、視聴状況、停止 |
| SC-131 | プラン管理 | `/admin/plans` | プランCRUD、価格、視聴可能範囲 |
| SC-140 | 視聴分析 | `/admin/analytics` | 再生数推移、視聴完了率、**離脱ヒートマップ**、デバイス別、流入元 |

## 主要フロー

### フローA：記事の読者が動画に触れる（最重要）

```
SEO記事を読んでいる
   ↓
記事内の埋め込みプレーヤー（ポスター画像 + 再生ボタンのみ表示、まだ何も読み込んでいない）
   ↓
再生ボタンを押す ── ここで初めて hls.js とマニフェストを取得
   ↓
再生開始（3秒以内）── 画質切替・速度・字幕を触ってみる
   ↓
「全画面でデモを開く」→ 別タブでアプリ本体へ
   ↓
SC-001 トップ ── ガイドツアーが3ステップで「試せること」を案内
   ↓
SC-004 シリーズ詳細 → SC-002 視聴
   ↓
デモ切替バーで「運営」に切替 → SC-140 視聴分析
   ↓  ★ たった今自分が見た再生が、グラフに反映されている
問い合わせ導線 or 元記事に戻る
```

### フローB：ペイウォールの体験

```
SC-001 トップ（未ログイン）── 有料バッジ付きのエピソードを選ぶ
   ↓
SC-002 視聴 ── 再生開始。プレーヤー上部に「無料プレビュー 残り2:47」
   ↓
3分経過 ── 再生が停止し、ペイウォールがオーバーレイ表示
   「続きを見るには有料プランへの登録が必要です」＋ プラン概要 ＋ CTA
   ↓
SC-020 プラン比較 → SC-021 申込フォーム → SC-022 完了
   ↓  ※ 決済処理は行わない。ロールが premium に変わる
SC-002 に戻る ── ★ 停止した3分00秒の位置から、そのまま続きが再生される
```

### フローC：運営がエピソードを公開する

```
SC-111 アップロード ── ファイルをドロップ
   ↓
エンコード進捗の再現
   待機中 → 変換中（0〜100%、解像度ごとに段階表示）→ サムネイル生成 → 完了
   ※ 一定確率で「変換失敗」を再現し、リトライ導線を見せる
   ↓
SC-112 エピソード編集
   タイトル・説明・サムネイル選択（3候補から）
   シリーズへの割当、無料／有料の区分
   公開設定：下書き／即時公開／公開予約（日時指定）
   ↓
SC-114 チャプター編集 ── 再生しながらチャプターを打つ
   ↓
SC-115 添付資料 ── PDFを紐付け、公開範囲を「有料会員のみ」に
   ↓
公開
   ↓
【ロール切替：無料会員】
SC-001 トップの「新着」に出現。★ 添付資料はロックされている
   ↓
【ロール切替：有料会員】
★ 添付資料がダウンロード可能になっている
```

---

# 7. 機能要件

## 7.1 プレーヤー（**このデモの心臓部**）

| ID | 要件 | 優先 |
|---|---|---|
| FR-101 | HLS（`.m3u8`）を再生できること。ネイティブHLS対応環境（Safari）ではネイティブを使い、非対応環境では hls.js を使うこと | P1 |
| FR-102 | 再生／一時停止／シーク／音量／ミュート／全画面／PiP を操作できること | P1 |
| FR-103 | 画質を「自動／1080p／720p／360p」から選択でき、切替時に**再生位置と再生状態を維持する**こと。現在の実効画質を表示すること | P1 |
| FR-104 | 再生速度を 0.75 / 1.0 / 1.25 / 1.5 / 2.0 倍から選択できること | P1 |
| FR-105 | 字幕を「オフ／日本語／英語」から選択できること。表示サイズ・文字色・背景の不透明度を設定できること | P1 |
| FR-106 | チャプターがある場合、シークバー上に区切りを表示し、現在のチャプター名をバー上部に表示すること | P1 |
| FR-107 | チャプター一覧からクリックで該当位置へジャンプできること | P1 |
| FR-108 | **キーボード操作**：`Space`/`K` 再生停止、`←→` 5秒シーク、`J`/`L` 10秒シーク、`↑↓` 音量、`F` 全画面、`M` ミュート、`C` 字幕、`0-9` 位置ジャンプ、`?` ショートカット一覧 | P1 |
| FR-109 | 視聴位置を5秒ごとに保存し、次回アクセス時に**その位置から再開**できること。「最初から見る」も選べること | P1 |
| FR-110 | 視聴完了（95%以上）した場合、進捗を「視聴済」とし、次回は最初から再生すること | P1 |
| FR-111 | シークバーのホバー／ドラッグ中に、その位置のサムネイルプレビューを表示すること | P2 |
| FR-112 | 終了30秒前に「次のエピソード」カードを表示し、カウントダウン後に自動再生すること。自動再生は設定でOFFにでき、カードからキャンセルできること | P2 |
| FR-113 | 通信状況の悪化時にバッファリング表示を出し、自動で下位画質へ切り替わること | P1 |
| FR-114 | 再生に失敗した場合、原因（ネットワーク／非対応形式／読み込み失敗）を区別したメッセージと再試行ボタンを表示すること。**無言で止まらないこと** | P1 |
| FR-115 | 3秒間操作がない場合、コントロールを自動的に隠すこと。**ただし一時停止中は隠さない** | P1 |
| FR-116 | モバイルではダブルタップで前後10秒シークできること。ピンチで全画面に入れること | P2 |
| FR-117 | 再生中にタイムスタンプ付きメモを残せること。メモ一覧からクリックでその位置へジャンプできること | P3 |

## 7.2 埋め込みモード

| ID | 要件 | 優先 |
|---|---|---|
| FR-151 | `?embed=1` でヘッダー・サイドバー・ロール切替バーを非表示にし、プレーヤーのみを表示すること | P1 |
| FR-152 | 埋め込みモードの初期表示はポスター画像と再生ボタンのみとし、hls.js とマニフェストの取得を再生ボタン押下まで遅延させること | P1 |
| FR-153 | 埋め込みモードでは**アプリ内の画面遷移を発生させないこと**。「全画面で開く」は `target="_blank"` とすること | P1 |
| FR-154 | 埋め込みモードで音声ありの自動再生を行わないこと | P1 |
| FR-155 | 埋め込みモードのプレーヤーは、親要素の幅に追従し、16:9 を維持すること | P1 |

## 7.3 カタログ・視聴体験

| ID | 要件 | 優先 |
|---|---|---|
| FR-201 | トップに「続きから見る」を最上部に配置し、進捗率のバー付きで表示すること。視聴中の作品がない場合はこのレーンを表示しないこと | P1 |
| FR-202 | カテゴリ別レーンを横スクロールで表示し、キーボードの矢印キーでも移動できること | P1 |
| FR-203 | サムネイルのホバー（デスクトップのみ）で、数秒のプレビューを再生すること。**モバイルでは行わないこと** | P2 |
| FR-204 | シリーズ詳細で、各エピソードの視聴状態（未視聴／視聴中N%／視聴済）とシリーズ全体の進捗率を表示すること | P1 |
| FR-205 | 検索はタイトル・説明・シリーズ名・タグを対象とし、表記ゆれ（ひらがな／カタカナ／半角全角）を吸収すること | P1 |
| FR-206 | 絞り込みとして、カテゴリ、タグ、再生時間（〜5分／5〜15分／15分〜）、視聴状態、無料／有料 を提供すること | P1 |
| FR-207 | 絞り込み条件をURLクエリに反映し、リロード・共有で再現できること | P1 |
| FR-208 | エピソードに添付資料（PDF等）を紐付け、視聴画面から閲覧・ダウンロードできること。**公開範囲（全員／会員／有料会員）に応じてロック表示すること** | P1 |
| FR-209 | シリーズ・エピソードをマイリストに保存でき、マイページから確認できること | P2 |
| FR-210 | 視聴履歴を時系列で表示し、個別削除・全削除ができること | P2 |
| FR-211 | 再生設定（既定画質・既定速度・自動再生・字幕の既定表示）を保存し、次回以降の再生に適用すること | P2 |

## 7.4 会員・ペイウォール

| ID | 要件 | 優先 |
|---|---|---|
| FR-301 | エピソードは「無料／有料」の区分を持ち、カタログ上でバッジ表示すること | P1 |
| FR-302 | 未ログイン・無料会員が有料エピソードを再生した場合、**既定3分のプレビュー後に再生を停止し**、ペイウォールをオーバーレイ表示すること | P1 |
| FR-303 | プレビュー中、残り時間をプレーヤー上部に常時表示すること（「無料プレビュー 残り2:47」） | P1 |
| FR-304 | ペイウォールには、プラン概要・視聴できるようになる内容・CTA を含めること。**閉じて他の作品を探す導線も置くこと** | P1 |
| FR-305 | プラン加入後、**プレビューが停止した位置から続きを再生できること** | P1 |
| FR-306 | プラン比較画面で3プラン（無料／スタンダード／プレミアム）の視聴可能範囲を表で示すこと | P1 |
| FR-307 | 申込フォームは会員情報と支払い方法の入力UIを持つこと。**入力値の検証は実装し、送信時に課金処理は行わないこと。デモである旨を画面上に明示すること** | P1 |
| FR-308 | 申込完了でロールが `premium` に変わり、切替バーの表示も更新されること | P1 |
| FR-309 | 契約状況画面からプラン変更・解約ができ、解約すると視聴範囲が即座に制限されること | P2 |
| FR-310 | プレビュー時間はプラン設定・エピソード設定で変更でき、管理画面から編集できること | P3 |

## 7.5 配信管理

| ID | 要件 | 優先 |
|---|---|---|
| FR-401 | 動画ファイルをドラッグ＆ドロップでアップロードできること。対応形式・最大サイズを事前に明示すること | P1 |
| FR-402 | アップロード後、**エンコード進捗を段階表示で再現すること**：アップロード中（%）→ 待機中 → 変換中（解像度ごと）→ サムネイル生成 → 完了 | P1 |
| FR-403 | エンコード完了後の再生は、事前に用意した素材で代替すること。**この挙動をユーザーに誤解させないよう、デモである旨を管理画面上に明示すること** | P1 |
| FR-404 | 一定確率で「変換失敗」を再現し、原因表示とリトライ導線を見せること | P2 |
| FR-405 | エピソード編集で、タイトル・説明・シリーズ割当・カテゴリ・タグ・無料／有料区分 を設定できること | P1 |
| FR-406 | サムネイルを3候補から選択、または任意の画像をアップロードして設定できること | P1 |
| FR-407 | 公開設定として「下書き／即時公開／公開予約（日時指定）／非公開」を選べること | P1 |
| FR-408 | **公開予約した日時が到来すると、視聴者側のカタログに自動的に出現すること**（クライアント側のタイマーで判定） | P1 |
| FR-409 | 非公開にしたエピソードが、視聴者側のカタログ・検索結果から即座に消えること | P1 |
| FR-410 | 字幕ファイル（WebVTT）をアップロードでき、タイムライン上で行ごとに文言を修正できること。修正結果をプレーヤーでプレビューできること | P2 |
| FR-411 | チャプターを、再生位置を確認しながら追加・編集・削除できること。「現在位置で追加」ができること | P2 |
| FR-412 | エピソードに添付資料を紐付け、公開範囲（全員／会員／有料会員）を設定できること | P1 |
| FR-413 | シリーズを作成し、エピソードをドラッグで並び替えられること | P1 |
| FR-414 | 複数エピソードを選択して、公開／非公開・カテゴリ付与を一括実行できること | P2 |
| FR-415 | 会員一覧で、プラン・登録日・最終視聴日・視聴本数を確認でき、停止できること | P2 |
| FR-416 | プランを作成・編集でき、価格と視聴可能範囲を設定できること | P2 |

## 7.6 視聴分析

| ID | 要件 | 優先 |
|---|---|---|
| FR-501 | 再生数・総視聴時間・平均視聴完了率を、期間指定（今日／7日／30日／任意）で表示すること | P1 |
| FR-502 | **離脱ヒートマップ**：エピソードの再生位置ごとの視聴者残存率を折れ線で表示し、離脱が大きい区間を強調すること | P1 |
| FR-503 | 視聴完了率をエピソード別にランキング表示すること | P2 |
| FR-504 | デバイス別（PC／スマホ／タブレット）、画質別の再生比率を表示すること | P2 |
| FR-505 | シリーズごとの継続率（第1話→第2話→…の視聴継続）をファネルで表示すること | P2 |
| FR-506 | **デモ中に視聴した実績が、これらの分析に反映されること**（シードの統計に加算される） | P1 |
| FR-507 | 分析データをCSVでエクスポートできること | P3 |

## 7.7 デモ基盤

| ID | 要件 | 優先 |
|---|---|---|
| FR-601 | 4ロール（未ログイン／無料会員／有料会員／運営）をワンクリックで切り替えられること | P1 |
| FR-602 | 「デモをリセット」で localStorage をクリアし、確認ダイアログを経て初期状態に戻ること | P1 |
| FR-603 | 視聴進捗・マイリスト・契約状態がリロード後も保持されること | P1 |
| FR-604 | 元記事へ戻るリンクを常設すること | P1 |
| FR-605 | 初回訪問時に3ステップのガイドツアーを表示し、「今後表示しない」を選べること | P2 |
| FR-606 | ロール権限外の画面（無料会員で `/admin`）にアクセスした場合、案内画面からロール切替できること。素の404を出さないこと | P1 |
| FR-607 | 権限で不可の操作はボタンを非活性にし、理由をツールチップで示すこと。実行してからエラーを出さないこと | P1 |
| FR-608 | データスコープは `lib/repo/_scope.ts` に集約し、全リポジトリメソッドが通すこと | P1 |

---

# 8. データ設計

## 型定義（`lib/types/`）

```ts
// ---- コンテンツ ----
type Series = {
  id: string
  title: string
  slug: string
  description: string
  categoryId: string
  tagIds: string[]
  coverUrl: string              // 縦長（カタログ用）
  heroUrl: string               // 横長（トップのヒーロー用）
  episodeIds: string[]          // 並び順を保持
  isPublished: boolean
  publishedAt: string
  createdAt: string
}

type Episode = {
  id: string
  seriesId: string
  number: number                // 第N話
  title: string
  description: string
  durationSec: number
  // --- 配信情報（実在するファイルを指す） ---
  hlsUrl: string                // master.m3u8 のURL
  posterUrl: string
  spriteUrl: string             // シークプレビュー用スプライト
  spriteMeta: { cols: number; rows: number; interval: number; w: number; h: number }
  captions: Caption[]
  chapters: Chapter[]
  // --- 公開・課金 ---
  access: 'free' | 'paid'
  previewSec: number            // 有料時のプレビュー秒数（既定180）
  status: 'draft' | 'scheduled' | 'published' | 'unlisted' | 'private'
  scheduledAt?: string          // 公開予約日時（FR-408）
  publishedAt?: string
  // --- 付随 ---
  attachmentIds: string[]
  tagIds: string[]
  createdAt: string
  updatedAt: string
}

type Caption = { id: string; lang: 'ja' | 'en'; label: string; vttUrl: string; isDefault: boolean }
type Chapter = { id: string; startSec: number; title: string }

type Attachment = {
  id: string
  episodeId: string
  name: string
  fileType: 'pdf' | 'slide' | 'zip' | 'other'
  sizeBytes: number
  url: string
  visibility: 'public' | 'member' | 'premium'   // FR-412
}

type Category = { id: string; name: string; slug: string; sortOrder: number }
type Tag = { id: string; name: string }

// ---- 会員・プラン ----
type Plan = {
  id: string
  name: string                  // 無料 / スタンダード / プレミアム
  priceMonthly: number
  description: string
  features: string[]
  accessScope: 'free_only' | 'all'
  sortOrder: number
}

type Member = {
  id: string
  name: string
  email: string
  avatarUrl: string
  role: 'guest' | 'free' | 'premium' | 'admin'
  planId: string
  subscribedAt?: string
  status: 'active' | 'suspended' | 'cancelled'
  lastWatchedAt?: string
  watchedCount: number
  createdAt: string
}

// ---- 視聴 ----
type WatchProgress = {
  episodeId: string
  memberId: string
  positionSec: number           // 5秒ごとに更新（FR-109）
  durationSec: number
  percent: number               // 派生値だが検索性のため保持
  isCompleted: boolean          // 95%以上（FR-110）
  lastWatchedAt: string
}

type WatchEvent = {              // 分析の元データ
  id: string
  episodeId: string
  memberId: string
  type: 'start' | 'progress' | 'complete' | 'abandon' | 'quality_change' | 'seek'
  positionSec: number
  quality?: '1080p' | '720p' | '360p'
  device: 'desktop' | 'mobile' | 'tablet'
  occurredAt: string
}

type Note = { id: string; episodeId: string; memberId: string; positionSec: number; body: string; createdAt: string }
type ListItem = { id: string; memberId: string; targetType: 'series' | 'episode'; targetId: string; addedAt: string }

// ---- 分析（集計値。ストアには保存せず lib/calc で算出） ----
type RetentionPoint = { positionSec: number; retainedRatio: number }   // 離脱ヒートマップ（FR-502）
type EpisodeStats = {
  episodeId: string
  plays: number
  uniqueViewers: number
  totalWatchSec: number
  avgCompletionRate: number
  retention: RetentionPoint[]
  byDevice: Record<'desktop'|'mobile'|'tablet', number>
  byQuality: Record<'1080p'|'720p'|'360p', number>
}

// ---- アップロード（擬似） ----
type UploadJob = {
  id: string
  fileName: string
  sizeBytes: number
  stage: 'uploading' | 'queued' | 'transcoding' | 'thumbnailing' | 'done' | 'failed'
  progress: number              // 0-100
  transcodeProgress: Record<'1080p'|'720p'|'360p', number>
  errorReason?: string
  linkedEpisodeId?: string      // 完了時に既存素材へ紐付け（FR-403）
  createdAt: string
}
```

## シードデータ（`lib/seed/`）

| ファイル | 内容 | 件数 |
|---|---|---|
| `categories.ts` `tags.ts` | カテゴリ6、タグ24 | — |
| `series.ts` | シリーズ | 5本 |
| `episodes.ts` | エピソード（**実在する動画URLを指す**） | 12〜16本 |
| `attachments.ts` | 添付資料（公開範囲を分散） | 10件 |
| `plans.ts` | プラン | 3件 |
| `members.ts` | 会員（無料／有料／停止中を分散、過去90日で登録日を散らす） | 240名 |
| `watchEvents.ts` | 視聴イベント（過去90日分。**離脱ヒートマップが意味を持つ分布**で生成） | 12,000件 |
| `progress.ts` | 操作用ペルソナの視聴進捗（視聴中2本、視聴済3本） | — |

**シード作成のルール（品質を左右する）**

- **シリーズ構成は実在しそうな講座にする。** 例：「Claude Code入門」「業務システムの作り方」「AI活用の基礎」等。タイトルの使い回し禁止
- **エピソードの説明文を書き分ける。** 「第N話の説明です」のようなダミーを残さない
- **視聴イベントは自然な離脱曲線で生成する。** 冒頭30秒で15%落ち、以降なだらかに減衰、終盤で微増（まとめ部分）。均等分布だと分析画面が無意味になる
- **エピソードごとに離脱パターンを変える。** 途中に大きな離脱がある回を意図的に作る（分析の価値が伝わる）
- 日付は現在日時からの相対で生成する。固定日付を埋め込まない
- **「続きから見る」が最初から表示されるよう、ペルソナに視聴途中のエピソードを持たせる。** トップが空だとデモとして成立しない
- 公開予約されたエピソードを1本含める（フローCの実演用）

## ストアとリポジトリ

```
lib/
├── types/          # 上記の型定義
├── seed/
├── store/
│   ├── session.ts  # ロール、操作中の会員
│   ├── content.ts  # series / episodes / attachments / categories
│   ├── watch.ts    # progress / events / notes / list
│   └── ui.ts       # 再生設定、ガイドツアー表示済みフラグ
└── repo/           # ★ コンポーネントが触るのはここだけ
    ├── _delay.ts _scope.ts
    ├── episodes.ts series.ts attachments.ts
    ├── watch.ts members.ts plans.ts uploads.ts analytics.ts
```

**リポジトリ層の規約（厳守）**

- 全メソッドを `async` で定義。中身が同期でも例外なく
- 戻り値は `{ ok: true; data: T } | { ok: false; error: string }` に統一
- **コンポーネントから Zustand ストアを直接参照しない**
- **視聴可否の判定（`canWatch(episode, role)`）は `lib/repo/_scope.ts` に集約する。** コンポーネント側で `role === 'premium'` のような分岐を書かない（FR-608）
- 派生値（進捗率、離脱率、完了率）はストアに保存せず `lib/calc/` で算出する
- **再生開始に関わるメソッドには擬似ディレイを入れない**

```ts
// lib/repo/_scope.ts — 実案件では署名付きURL発行・DRM に置き換わる箇所
export function resolveAccess(ep: Episode, role: Role): AccessResult {
  if (ep.access === 'free') return { kind: 'full' }
  if (role === 'premium' || role === 'admin') return { kind: 'full' }
  return { kind: 'preview', limitSec: ep.previewSec }   // guest / free
}
```

---

# 9. デザイン要件

## アートディレクション

**「画面を暗くして、映像に光を渡す」**

動画配信のUIは、うまくいっているとき存在に気づかれない。CASTA は暗い背景に沈み、サムネイルと再生中の映像だけが明るい。装飾・グラデーション・不要な枠線を持たない。

- **ダークテーマを既定とする。** ライトテーマは実装しない（切替を作らない）
- **サムネイルが唯一の色。** UI自体はほぼ無彩色。彩度はコンテンツ側にある
- **アクセントは1色だけ。** 再生・進捗・現在地にのみ使う
- **コントロールは消える。** 3秒で自動的に隠れ、必要なときだけ現れる

> **HIREBASE / RELATE との差別化（重要）**
> HIREBASE：明朝・白・広い余白（読ませる）
> RELATE：ゴシック・白・高密度・等幅数字（操作させる）
> **CASTA：ダーク・映像優先・UIが消える（見せる）**
> **3つ並べたとき、別の会社が作ったように見えることが理想。トークンをコピーしない。**

## カラートークン

```css
:root {
  /* 背景は3段階の黒で奥行きを作る */
  --bg-base:    #0A0B0D;   /* ページ背景 */
  --bg-raised:  #131519;   /* カード・パネル */
  --bg-overlay: #1C1F25;   /* モーダル・ドロップダウン */

  --fg-high:    #F2F3F5;   /* 見出し・本文 */
  --fg-mid:     #A0A6B0;   /* 補助テキスト */
  --fg-low:     #6B7280;   /* ラベル・非活性 */

  --accent:     #E8503A;   /* 再生・進捗・現在地。★これ以外の色を持ち込まない */
  --accent-dim: rgba(232,80,58,.18);

  --border:     #24272E;   /* 罫線。極めて控えめに */
  --border-hi:  #343943;   /* フォーカス時 */

  /* シグナル（管理画面のみで使用） */
  --ok: #3FA37A; --warn: #C8933A; --danger: #D0544A;
  /* プレーヤー内グラデーション（コントロールの視認性確保） */
  --scrim: linear-gradient(to top, rgba(0,0,0,.85) 0%, rgba(0,0,0,0) 100%);
}
```

**配色ルール**

- **アクセント `--accent` は「再生・進捗・現在地」にのみ使う。** 有料バッジ・エラー・装飾に使わない
- 有料バッジは色ではなく**枠線とアイコン**で表現する。カタログが赤いバッジだらけになると台無しになる
- カード同士は影ではなく**背景の明度差**で分ける（`--bg-base` の上に `--bg-raised`）
- プレーヤーのコントロールは、映像の明暗に負けないよう必ず `--scrim` の上に置く

## タイポグラフィ

| 役割 | 書体 | 用途 |
|---|---|---|
| 見出し | Noto Sans JP 700 | 作品タイトル、セクション見出し。**字間を詰める（`letter-spacing: -.01em`）** |
| 本文 | Noto Sans JP 400 | 説明文、UI |
| 時間表示 | Roboto Mono 500（`tabular-nums`） | 再生時間、経過時間、チャプター時刻 |

| トークン | デスクトップ | モバイル | 用途 |
|---|---|---|---|
| hero | 36px / 700 | 24px | ヒーローの作品タイトル |
| title | 20px / 700 | 18px | 作品タイトル |
| card | 14px / 500 | 13px | カード内タイトル（2行クランプ） |
| body | 14px / 400 | 14px | 説明文 |
| meta | 12px / 400 | 12px | 再生時間、話数、カテゴリ |
| time | 13px / 500 Mono | 12px | プレーヤーの時間表示 |

**ダーク背景では文字が太って見える。** ライト背景と同じウェイトを使わず、本文は 400、見出しは 700 で明確に差をつける（500・600 を多用しない）。

## レイアウトとスペーシング

| 項目 | 定義 |
|---|---|
| スペーシング | 4pxベース：4 / 8 / 12 / 16 / 24 / 32 / 48 / 64 |
| ヘッダー | 64px。**スクロールで背景が透明→不透明に変化**（トップのヒーローを活かすため） |
| コンテンツ幅 | 最大1440px。**レーンは画面端まで伸ばし、見切れを作る**（横スクロールできることを示すため） |
| カード比率 | サムネイル 16:9、シリーズカバー 2:3 |
| レーンのカード幅 | デスクトップ 280px / タブレット 220px / モバイル 160px |
| 角丸 | 6px（カード・サムネイル）／4px（ボタン・バッジ）／999px（アバター・チップ） |
| 影 | **使わない。** 明度差で階層を表現する。例外はモーダルのみ |
| プレーヤー | 16:9 を維持。デスクトップ最大幅1280px、モバイルは画面幅いっぱい |

## プレーヤーUI仕様（**最重要コンポーネント**）

```
┌──────────────────────────────────────────────┐
│  [無料プレビュー 残り2:47]        ← 有料時のみ、上部左  │
│                                              │
│                   映像                        │
│                                              │
│  ┌──── scrim グラデーション ────────────────┐ │
│  │  ● チャプター名（現在地）                  │ │
│  │  ━━━━━━━╋━━━━┃━━━━━━━━━━━━━━  ← シークバー│ │
│  │   ▶  ⏭  🔊━━  1:24 / 8:15   ⚙ CC ⛶ ⧉  │ │
│  └────────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
```

| 要素 | 仕様 |
|---|---|
| シークバー | 高さ通常4px、ホバーで6px。バッファ済みは `--fg-low` の30%、再生済みは `--accent`。チャプター区切りは2pxの隙間で表現 |
| シークプレビュー | ホバー位置の上にサムネイル（160×90）+ 時刻。チャプター名も併記 |
| 再生ボタン | 中央の大型ボタンは初回のみ。以降は下部コントロールに集約 |
| 設定メニュー（⚙） | 画質／速度の2階層。**現在値を親メニューに併記する**（「画質 自動(720p)」） |
| 字幕ボタン（CC） | オン時はアクセント色。長押し／右クリックで字幕設定へ |
| コントロール自動非表示 | 3秒。**一時停止中・設定メニュー展開中は隠さない**（FR-115） |
| バッファリング | 中央に控えめなインジケータ。**映像を暗転させない** |
| エラー | 中央にメッセージ + 再試行ボタン。原因を区別して表示（FR-114） |
| モバイル | コントロールを大きめに。ダブルタップシークは左右に波紋アニメーション |

## 主要コンポーネント仕様

| コンポーネント | 仕様 |
|---|---|
| コンテンツカード | サムネイル16:9／再生済みは右下にチェック／視聴中は下端に進捗バー（`--accent`、高さ3px）／有料は左上に枠線バッジ |
| ホバープレビュー | デスクトップのみ。600ms 停留で拡大 + 無音プレビュー再生。**モバイルでは実装しない**（FR-203） |
| 横スクロールレーン | 端で見切れを作る。左右の矢印はホバーで出現。キーボード矢印でも移動 |
| ヒーロー | 背景に静止画（動画背景は使わない — 帯域とバッテリーを浪費するため）。タイトル・説明2行・再生ボタン・詳細ボタン |
| エピソードリスト行 | サムネイル／話数・タイトル／再生時間／進捗バー／有料バッジ／説明1行 |
| ペイウォール | 映像の上にオーバーレイ。暗転 + ブラー。プラン概要・CTA・**閉じる導線**を必ず持つ |
| 添付資料カード | ファイル名・種別アイコン・サイズ・ダウンロードボタン。ロック時は鍵アイコン + 必要プラン名 |
| アップロード進捗 | 段階を横並びのステップで表示。現在の段階のみ進捗%を表示 |
| 離脱ヒートマップ | 横軸=再生位置、縦軸=残存率の折れ線。**大きな離脱区間を `--danger` で塗り、注釈を出す** |
| 空状態 | 全一覧に専用の空状態。ダーク背景に合う線画イラスト |
| スケルトン | カード形状。ダーク背景では明るいシマー（`--bg-raised` → `--bg-overlay`） |
| デモ切替バー | **アプリ本体がダークなので、切替バーは明るいグレー**にして区別する |

## モーション

| 対象 | duration | 内容 |
|---|---|---|
| コントロールの表示・非表示 | 200ms | フェード。**位置は動かさない** |
| ホバープレビューの拡大 | 300ms（600ms 停留後） | scale 1.0 → 1.12 + z-index 上げ |
| レーンの横スクロール | 400ms | ease-out |
| モーダル・ペイウォール | 240ms | フェード + ブラー |
| 進捗バーの更新 | なし | **アニメーションさせない。**実時間を表すため |
| 次のエピソードのカウントダウン | — | 円形プログレスで残り時間を可視化 |

`prefers-reduced-motion: reduce` 時は全アニメーションを無効化し、**ホバープレビューの自動再生も停止すること。**

## アクセシビリティ（WCAG 2.1 AA）

動画プレーヤーは a11y の要求が特に高い領域。**ここを作り込めることが差別化になる。**

| ID | 要件 |
|---|---|
| A11Y-01 | ダーク背景でのコントラスト比は通常4.5:1以上、18px以上は3:1以上。**`--fg-mid` の使用箇所を実測で検証すること** |
| A11Y-02 | プレーヤーの全操作をキーボードで完遂できること（FR-108） |
| A11Y-03 | 字幕を必ず提供すること。字幕のサイズ・背景不透明度をユーザーが調整できること |
| A11Y-04 | 音声のみでは伝わらない情報がある動画に、説明を本文で補うこと |
| A11Y-05 | **音声ありの自動再生を行わないこと**（FR-154） |
| A11Y-06 | 3回/秒を超える点滅を含むコンテンツを使用しないこと |
| A11Y-07 | フォーカスリングは2px・オフセット2pxで常時可視。**映像の上でも視認できる色にすること** |
| A11Y-08 | シークバーは `role="slider"` とし、`aria-valuenow` / `aria-valuetext`（「1分24秒」）を更新すること |
| A11Y-09 | 再生状態の変化を `aria-live="polite"` で通知すること |
| A11Y-10 | ペイウォール表示時にフォーカスをトラップし、Escで閉じられること |
| A11Y-11 | 情報を色のみで伝えない。視聴済み・有料はアイコンとテキストを併記すること |

## ライティング規約

| 原則 | 例 |
|---|---|
| 再生状態は簡潔に | ○「無料プレビュー 残り2:47」／×「あなたは現在無料プレビューを視聴しています」 |
| ペイウォールは責めない | ○「続きは有料プランでご覧いただけます」／×「視聴権限がありません」 |
| エラーは原因と対処 | ○「通信が不安定なため再生できません。再試行するか、画質を下げてお試しください」／×「エラーが発生しました」 |
| ロックは必要条件を書く | ○「この資料はプレミアムプランでダウンロードできます」／×「ダウンロードできません」 |
| 空状態は誘導する | ○「まだ視聴履歴がありません。おすすめから探してみましょう」+ ボタン |
| システム語を使わない | ○「公開を予約する」／×「statusをscheduledに設定」 |

---

# 10. 技術要件・ディレクトリ構成

## 技術スタック（固定・勝手に変更しない）

| レイヤ | 技術 | 備考 |
|---|---|---|
| フレームワーク | Next.js 15（App Router）／ TypeScript strict | |
| スタイリング | Tailwind CSS + CSS Variables | トークンは CSS 変数で定義 |
| UIコンポーネント | shadcn/ui（Radix UI基盤） | ダークテーマに合わせて上書き |
| 状態管理 | Zustand + persist | localStorage に永続化 |
| **HLS再生** | **hls.js**（動的import） | Safari はネイティブHLSを使う（FR-101） |
| **プレーヤーUI** | **自前実装** | video.js / Plyr を使わない。**既製プレーヤーを貼るとデモの意味がなくなる** |
| 字幕 | ネイティブ `<track>` + WebVTT | |
| D&D | dnd-kit | エピソード並び替え。キーボード対応必須 |
| グラフ | Recharts | 分析画面（動的import） |
| フォーム | React Hook Form + Zod | |
| 日付 | date-fns（ja locale） | |
| アイコン | lucide-react | |
| ホスティング | Vercel | アプリ本体 |
| **動画配信** | **Cloudflare R2 + CDN** | 3章参照。エグレス無料 |
| エンコード | ffmpeg（`scripts/encode.sh`） | ローカル実行。CIには含めない |
| テスト | Vitest / Playwright / axe-core | |

**上記以外のライブラリを入れる前に必ず提案し、承認を得ること。特にプレーヤーライブラリの導入は禁止。**

## ディレクトリ構成

```
/
├── CLAUDE.md
├── app/
│   ├── layout.tsx
│   ├── page.tsx                    # SC-001 トップ
│   ├── watch/[episodeId]/page.tsx  # SC-002 / SC-003（?embed=1）
│   ├── series/[seriesId]/page.tsx  # SC-004
│   ├── browse/                     # SC-005, 007
│   ├── search/                     # SC-006
│   ├── my/                         # SC-010〜014
│   ├── plans/                      # SC-020〜022
│   ├── admin/                      # SC-100〜140
│   └── dev/components/             # コンポーネントカタログ
├── components/
│   ├── player/                     # ★ 最重要
│   │   ├── VideoPlayer.tsx         # ルート。hls.js の生存管理
│   │   ├── ControlBar.tsx
│   │   ├── SeekBar.tsx             # チャプター区切り・プレビュー
│   │   ├── SettingsMenu.tsx        # 画質・速度
│   │   ├── CaptionMenu.tsx
│   │   ├── ChapterList.tsx
│   │   ├── NextEpisodeCard.tsx
│   │   ├── Paywall.tsx
│   │   └── useHls.ts / useKeyboardShortcuts.ts / usePlaybackProgress.ts
│   ├── catalog/                    # ContentCard, Lane, Hero, HoverPreview
│   ├── domain/                     # EpisodeRow, AttachmentCard, UploadStepper, RetentionChart
│   ├── demo/                       # RoleSwitcher, ResetButton, GuideTour, BackToArticle
│   └── layout/
├── lib/
│   ├── types/ seed/ store/ repo/
│   ├── calc/                       # 進捗率、離脱率、完了率、継続率
│   ├── query/                      # 検索・絞り込み
│   ├── validation/
│   └── utils/
├── scripts/
│   └── encode.sh                   # ffmpeg エンコードスクリプト
├── docs/
│   └── assets-license.md           # ★ 動画・画像素材の取得元とライセンス（VID-02）
├── e2e/
└── public/
```

## コーディング規約

- TypeScript strict。`any` 禁止
- ファイル名は kebab-case、コンポーネントは PascalCase
- **コンポーネントから Zustand ストアを直接参照しない。必ず `lib/repo/` 経由**
- **視聴可否の判定を `role === 'premium'` のようにコンポーネントに書かない。** `resolveAccess()` を使う
- 計算ロジックは `lib/calc/` に置き、コンポーネントに埋め込まない
- **hls.js のインスタンスは必ず破棄する。** エピソード遷移・アンマウント時に `hls.destroy()` を呼ぶ。**メモリリークはこのアプリで最も起きやすい不具合**
- `<video>` のイベントリスナーも同様に確実に解除する
- コミットは1タスクごと。メッセージに要件IDを含める
  例：`feat(player): 画質切替時の再生位置維持を実装 (FR-103)`

---

# 11. 非機能要件

| ID | 要件 | 目標値 |
|---|---|---|
| NFR-01 | **再生開始までの時間**（再生ボタン押下→最初のフレーム表示） | **1.5秒以下** |
| NFR-02 | LCP（トップ・モバイル4G相当） | 2.5秒以下 |
| NFR-03 | INP | 200ms以下 |
| NFR-04 | CLS | 0.1以下。**プレーヤー領域は再生前からアスペクト比を確保すること** |
| NFR-05 | 埋め込みモードの初期JSバンドル（gzip後） | 80KB以下（EMB-03） |
| NFR-06 | アプリ本体の初期JSバンドル（gzip後） | 200KB以下（hls.js・Recharts は動的import） |
| NFR-07 | シークバー操作中のフレームレート | 60fps維持 |
| NFR-08 | 5分間の連続再生でメモリ使用量が単調増加しないこと | — |
| NFR-09 | Lighthouse | Performance 90 / Accessibility 95 以上 |
| NFR-10 | 対応ブラウザ | Chrome / Edge / Firefox 最新2バージョン、Safari 16以降、iOS Safari 16以降、Android Chrome |
| NFR-11 | 動画配信のオリジンに適切なCORS・Cache-Controlを設定すること（セグメントは長期キャッシュ、マニフェストは短期） | — |
| NFR-12 | localStorage の容量上限に達した場合、古い視聴イベントから削除し、データを破損させないこと | — |

## セキュリティ・権利（実案件への注記を含む）

| ID | 要件 |
|---|---|
| SEC-01 | 動画・画像・字幕素材の取得元とライセンスを `docs/assets-license.md` に記録すること（VID-02） |
| SEC-02 | 第三者の著作物（音楽・映像・キャラクター）を含む素材を使用しないこと |
| SEC-03 | ユーザー入力（メモ・管理画面の説明文）をサニタイズすること |
| SEC-04 | 申込フォームで入力された情報を**どこにも送信しない。** ブラウザ内で完結し、リセットで消えることを画面上に明示すること |

> **実案件への転用時の注意（デモ本体の要件ではない。商談で聞かれた際に説明できるよう記録）**
> - 本デモの動画URLは公開されており、URLを知る第三者が直接再生できる。**実サービスでは署名付きURL（有効期限付き）またはDRMが必要**
> - 有料コンテンツを扱う場合、特定商取引法に基づく表記、解約条件の明示、資金決済法の観点（前払式支払手段に該当するか）の確認が必要
> - 会員情報は個人情報保護法の対象。利用目的の明示と安全管理措置が必要
> - **本デモは会員情報をサーバーに送信しないため、これらの義務は発生しない。**「デモである」旨は画面上に常時明示する

---

# 12. 費用設計

**記事の主題の半分は「費用」。この章は実装要件であると同時に、記事本文の原稿素材でもある。**

## 費用は3層に分けて説明する

| 層 | 内容 | 特徴 |
|---|---|---|
| ① 初期開発費 | 設計・実装・テスト | 一度きり。機能スコープで決まる |
| ② 固定運用費 | ホスティング、ドメイン、監視 | 視聴量に関係なくかかる。小さい |
| ③ **変動運用費（配信コスト）** | **転送量・ストレージ・エンコード** | **視聴時間に比例して増える。ここが読めないと事業計画が壊れる** |

**動画配信アプリの費用相談で最も多い失敗は、③を計算せずに始めること。** 記事ではここを主軸に据える。

## 変動運用費の計算式（記事に載せる中核）

```
月間転送量(GB) = 平均ビットレート(Mbps) ÷ 8 × 平均視聴秒数 × 月間再生数 ÷ 1024

例：720p（1.8Mbps）／ 平均視聴 6分（360秒）／ 月間 10,000再生
  = 1.8 ÷ 8 × 360 × 10,000 ÷ 1024
  ≈ 791 GB/月
```

この 791GB に、各サービスの単価を掛けたものが月額の配信コストになる。

| 規模 | 月間再生数 | 概算転送量 | 備考 |
|---|---|---|---|
| 小（社内研修・小規模スクール） | 1,000 | 約 79 GB | ほぼ無視できる規模 |
| 中（会員制メディア） | 10,000 | 約 791 GB | ここから配信先の選択が効いてくる |
| 大（成長したサブスク） | 100,000 | 約 7.7 TB | **転送量課金のサービスだと月額が跳ねる** |

## 配信先の選択が費用構造を決める

| 方式 | 課金の考え方 | 向くケース | 注意点 |
|---|---|---|---|
| **オブジェクトストレージ + エグレス無料CDN**（Cloudflare R2 等） | ストレージ容量 + 操作回数。**転送量に課金されない** | 視聴時間が読めない／伸びる可能性がある | エンコードは自前。DRM・視聴制限は別途実装 |
| 汎用クラウドストレージ + CDN（S3+CloudFront 等） | ストレージ + **転送量** | 視聴量が小さく安定している | 伸びるとコストが線形に増える |
| 動画専用サービス（Cloudflare Stream / Mux 等） | **配信分数**または保存分数 + 配信分数 | エンコード・DRM・分析まで任せたい | 単価は高いが、実装工数が大幅に減る |
| 汎用プラットフォームからの静的配信（Vercel 等） | 転送量 | 検証・小規模のみ | **本番の動画配信には向かない** |

**判断基準はシンプル：** 実装工数を払って変動費を抑えるか、変動費を払って実装工数を抑えるか。月間視聴時間が読めない初期は前者、事業として伸びてDRMや詳細分析が必要になったら後者、という移行が現実的。

> **記事に載せる際の注意：** 各サービスの**単価は変動するため、この文書に金額を書き込まない。** 記事執筆時点で公式の料金ページを確認し、確認日を明記して掲載すること。**古い単価を載せた記事は、それだけで信頼を失う。**

## 開発費の目安（機能スコープ別）

| スコープ | 含まれるもの | 目安 |
|---|---|---|
| 最小構成 | 動画一覧・再生（既製プレーヤー）・会員なし | 小 |
| **標準構成（本デモ相当）** | **自前プレーヤー（画質切替・字幕・チャプター・レジューム）／会員制ペイウォール／配信管理CMS／視聴分析** | 中 |
| 拡張構成 | 上記 + DRM・署名付きURL・ライブ配信・アプリ配信 | 大 |

**「動画配信アプリ」という言葉の幅が非常に広いことが、費用が伝わらない最大の原因。** 記事ではこの3段階を明示し、デモは「標準構成」の実物であることを示す。具体的な金額はLPの価格体系に接続する。

## デモ自体の運用コストを実測して記事に載せる

| ID | 要件 |
|---|---|
| COST-01 | デモ公開後、月次で「再生数・総視聴時間・転送量・実際にかかった費用」を記録すること |
| COST-02 | 記録した実測値を記事に掲載し、**確認日を明記すること** |
| COST-03 | 転送量が想定を大きく超えた場合に気づけるよう、上限アラートを設定すること |

**「私たちのデモは、月にこれだけ再生されて、これだけかかっています」**という一文が、どんな料金表よりも説得力を持つ。

---

# 13. 実装タスク

**この順に進める。フェーズを飛ばさない。** 完了時は `[x]` に更新する。

## Phase 0 — 基盤と動画素材（3日）

### 0-1. 初期化
- [ ] Next.js 15 / TypeScript strict / App Router / Tailwind で初期化
- [ ] ESLint / Prettier / husky（pre-commit で lint + typecheck）
- [ ] 10章のディレクトリ構成を作成

### 0-2. 動画素材の準備（**先にこれを終わらせる。素材がないとプレーヤーが作れない**）
- [ ] 使用する動画を確定する（自社YouTube素材を第一候補）(VID-01)
- [ ] `scripts/encode.sh` を作成（HLS 3レンディション、fMP4、4秒セグメント）
- [ ] 12〜16本をエンコードし、サムネイル（ポスター／スプライト／OGP）を生成
- [ ] 字幕（WebVTT）を3本以上、うち1本は日英2言語 (VID-05)
- [ ] チャプター（WebVTT chapters）を5本以上 (VID-06)
- [ ] Cloudflare R2 にアップロードし、CDN経由で配信できることを確認
- [ ] **CORS と Cache-Control を設定**（セグメントは長期、マニフェストは短期）(NFR-11)
- [ ] `docs/assets-license.md` に取得元とライセンスを記録 (VID-02, SEC-01)
- [ ] 1本目に解説動画と資料PDFを配置（2章の「動画つき資料」との一体化）

### 0-3. 型とシード
- [ ] `lib/types/` に8章の型定義をすべて実装
- [ ] `lib/seed/` の全ファイル。**エピソードの説明文を書き分ける。ダミー文言を残さない**
- [ ] `watchEvents.ts` を**自然な離脱曲線**で生成（冒頭落ち→なだらか減衰→終盤微増）。エピソードごとにパターンを変える
- [ ] ペルソナに視聴途中のエピソードを持たせる（「続きから見る」が最初から表示されるように）
- [ ] 公開予約されたエピソードを1本含める

### 0-4. ストアとリポジトリ
- [ ] `lib/store/{session,content,watch,ui}.ts` を Zustand + persist で実装
- [ ] **`lib/repo/_scope.ts` に `resolveAccess()` を実装**（FR-608）
- [ ] `lib/repo/` の全モジュール。全メソッド async、Result型、**再生系にはディレイを入れない**
- [ ] `lib/calc/` に 進捗率・離脱率・完了率・継続率 を実装
- [ ] **`resolveAccess()` と `calc` の単体テストをこの時点で書く**

### 0-5. デザインシステム
- [ ] `app/globals.css` にカラートークンを CSS 変数で定義（**ダークテーマのみ**）
- [ ] `tailwind.config.ts` から CSS 変数を参照するよう theme を拡張
- [ ] フォント（Noto Sans JP / Roboto Mono）。時間表示に `tabular-nums`
- [ ] shadcn/ui を導入し、ダークトークンに合わせて上書き
- [ ] 共通コンポーネント：`ContentCard` / `Lane` / `Hero` / `EmptyState` / `Skeleton`（ダーク用シマー）/ `Badge`
- [ ] `prefers-reduced-motion` の全体対応
- [ ] **`/dev/components` にコンポーネントカタログを作成**

### 0-6. デモ基盤
- [ ] `RoleSwitcher`（4ロール）(FR-601)
- [ ] デモ切替バー。**アプリがダークなので切替バーは明るいグレー**
- [ ] 「デモをリセット」(FR-602)
- [ ] 元記事へ戻るリンク (FR-604)
- [ ] ロール権限外アクセス時の案内画面 (FR-606)

### Phase 0 完了チェック
- [ ] **R2 の動画URLをブラウザで直接開いて再生できる**
- [ ] `/dev/components` で全コンポーネントがトークン通りに表示される
- [ ] `resolveAccess()` のテストが全パターン通る

---

## Phase 1 — プレーヤー（5日・**最重要フェーズ**）

### 1-1. 再生基盤
- [ ] `useHls.ts`：hls.js の初期化・破棄。Safari はネイティブHLS (FR-101)
- [ ] **`hls.destroy()` とイベントリスナー解除を確実に行う**（メモリリーク対策）
- [ ] `VideoPlayer.tsx`：再生／一時停止／シーク／音量／ミュート／全画面／PiP (FR-102)
- [ ] `ControlBar.tsx`：scrim グラデーション上に配置
- [ ] 自動非表示（3秒。**一時停止中・メニュー展開中は隠さない**）(FR-115)
- [ ] バッファリング表示（**映像を暗転させない**）(FR-113)
- [ ] エラー表示（原因を区別 + 再試行）(FR-114)

### 1-2. シークと進捗
- [ ] `SeekBar.tsx`：再生済み／バッファ済み／チャプター区切り
- [ ] `role="slider"` と `aria-valuetext`（「1分24秒」）(A11Y-08)
- [ ] シークプレビュー（スプライトからサムネイル抽出 + 時刻 + チャプター名）(FR-111)
- [ ] `usePlaybackProgress.ts`：5秒ごとに進捗保存 (FR-109)
- [ ] レジューム再生と「最初から見る」(FR-109)
- [ ] 95%以上で「視聴済」判定 (FR-110)

### 1-3. 設定・字幕・チャプター
- [ ] `SettingsMenu.tsx`：画質（自動／1080/720/360）。**切替時に再生位置と再生状態を維持** (FR-103)
- [ ] 現在の実効画質の表示（「画質 自動(720p)」）
- [ ] 再生速度 5段階 (FR-104)
- [ ] `CaptionMenu.tsx`：オフ／日本語／英語、サイズ・背景の調整 (FR-105, A11Y-03)
- [ ] チャプター区切りの表示と現在チャプター名 (FR-106)
- [ ] `ChapterList.tsx`：クリックでジャンプ (FR-107)

### 1-4. キーボードとモバイル
- [ ] `useKeyboardShortcuts.ts`：`Space`/`K`/`←→`/`J`/`L`/`↑↓`/`F`/`M`/`C`/`0-9`/`?` (FR-108)
- [ ] ショートカット一覧のオーバーレイ
- [ ] モバイル：ダブルタップシーク（波紋アニメーション）、大きめコントロール (FR-116)
- [ ] **映像の上でも視認できるフォーカスリング** (A11Y-07)

### 1-5. 視聴画面と埋め込み
- [ ] SC-002 視聴画面（エピソード情報、チャプター、添付資料、次の話、同シリーズ）
- [ ] `NextEpisodeCard.tsx`：終了30秒前に表示、カウントダウン、キャンセル可 (FR-112)
- [ ] SC-003 **埋め込みモード** (FR-151〜155)
- [ ] ポスター + 再生ボタンのみの初期表示、hls.js の動的import (FR-152, EMB-02, EMB-03)
- [ ] 埋め込みモードのバンドルサイズを実測し 80KB以下を確認

### Phase 1 完了チェック
- [ ] **再生開始まで1.5秒以下**（NFR-01）
- [ ] 画質を切り替えても再生位置と再生状態が維持される
- [ ] **キーボードのみでプレーヤーの全操作ができる**
- [ ] エピソードを10回続けて切り替えてもメモリが単調増加しない（NFR-08）
- [ ] 埋め込みモードの初期バンドルが80KB以下

---

## Phase 2 — カタログと会員（4日）

### 2-1. カタログ
- [ ] SC-001 トップ（ヒーロー、続きから見る、カテゴリ別レーン、ランキング、新着）(FR-201)
- [ ] `Lane.tsx`：横スクロール、端で見切れ、矢印、キーボード移動 (FR-202)
- [ ] `HoverPreview`：デスクトップのみ、600ms停留で拡大 + 無音プレビュー (FR-203)
- [ ] SC-004 シリーズ詳細（エピソード一覧、各話の進捗、全体進捗率）(FR-204)
- [ ] SC-005 カタログ / SC-007 カテゴリ一覧
- [ ] SC-006 検索（表記ゆれ吸収）(FR-205)
- [ ] 絞り込み（カテゴリ／タグ／再生時間／視聴状態／無料有料）+ URL同期 (FR-206, FR-207)

### 2-2. マイページ
- [ ] SC-010 マイページ / SC-011 視聴履歴 (FR-210) / SC-012 マイリスト (FR-209)
- [ ] SC-014 再生設定（既定画質・速度・自動再生・字幕）と再生への適用 (FR-211)

### 2-3. ペイウォール（**デモの見せ場**）
- [ ] 有料バッジ表示（**色ではなく枠線とアイコン**）(FR-301)
- [ ] プレビュー制限：既定3分で停止 (FR-302)
- [ ] 残り時間の常時表示 (FR-303)
- [ ] `Paywall.tsx`：オーバーレイ、プラン概要、CTA、**閉じる導線**、フォーカストラップ (FR-304, A11Y-10)
- [ ] SC-020 プラン比較 (FR-306) / SC-021 申込フォーム (FR-307) / SC-022 完了 (FR-308)
- [ ] **加入後、停止位置から続きを再生** (FR-305)
- [ ] SC-023 契約状況、変更・解約 (FR-309)

### 2-4. 添付資料
- [ ] `AttachmentCard`：公開範囲に応じたロック表示 (FR-208, FR-412)
- [ ] ロック時は必要プラン名を明示

### Phase 2 完了チェック
- [ ] **フローBが完走する**（プレビュー → ペイウォール → 加入 → 続きから再生）
- [ ] 5章「ロールを跨ぐ体験」の1が動作する
- [ ] 有料バッジがカタログを赤くしていない（9章の配色ルールを満たす）

---

## Phase 3 — 配信管理（4日）

- [ ] SC-100 管理ダッシュボード
- [ ] SC-111 アップロード：ドロップ、**エンコード進捗の段階再現** (FR-401, FR-402)
- [ ] 完了後は既存素材に紐付け。**デモである旨を明示** (FR-403)
- [ ] 変換失敗の再現とリトライ導線 (FR-404)
- [ ] SC-112 エピソード編集（基本情報、サムネイル3候補、無料／有料）(FR-405, FR-406)
- [ ] 公開設定（下書き／即時／予約／非公開）(FR-407)
- [ ] **公開予約日時の到来でカタログに出現**（クライアントタイマー）(FR-408)
- [ ] 非公開で視聴者側から即座に消える (FR-409)
- [ ] SC-113 字幕編集（アップロード、行ごと修正、プレビュー）(FR-410)
- [ ] SC-114 チャプター編集（再生位置を見ながら、「現在位置で追加」）(FR-411)
- [ ] SC-115 添付資料管理（紐付け、公開範囲）(FR-412)
- [ ] SC-120 シリーズ管理（dnd-kit で並び替え、**キーボード対応**）(FR-413)
- [ ] 一括操作（公開／非公開、カテゴリ付与）(FR-414)
- [ ] SC-130 会員管理 (FR-415) / SC-131 プラン管理 (FR-416)

### Phase 3 完了チェック
- [ ] **フローCが完走する**（アップロード → 編集 → チャプター → 資料 → 公開 → 視聴者側に出現）
- [ ] 5章「ロールを跨ぐ体験」の3と4が動作する

---

## Phase 4 — 視聴分析（2日）

- [ ] SC-140 視聴分析：再生数・総視聴時間・平均完了率、期間指定 (FR-501)
- [ ] **`RetentionChart`：離脱ヒートマップ。大きな離脱区間を強調 + 注釈** (FR-502)
- [ ] 完了率のエピソード別ランキング (FR-503)
- [ ] デバイス別・画質別の比率 (FR-504)
- [ ] シリーズ継続率ファネル (FR-505)
- [ ] **デモ中の視聴が分析に反映されること** (FR-506)
- [ ] CSVエクスポート (FR-507)

### Phase 4 完了チェック
- [ ] **自分が今見た動画の視聴が、離脱ヒートマップに反映される**（5章の体験5）
- [ ] シードの離脱曲線が自然で、分析画面が意味を持って見える

---

## Phase 5 — 記事統合と仕上げ（3日）

### 5-1. 記事統合
- [ ] 記事側の埋め込みコードを作成し、**実際の記事ページで動作確認** (EMB-01〜05)
- [ ] 埋め込みが記事の LCP 要素になっていないことを確認 (EMB-04)
- [ ] 記事の Core Web Vitals を埋め込み前後で比較し、悪化していないことを確認
- [ ] 「デモを開く」カードの設置
- [ ] 元記事へ戻るリンク、問い合わせ導線 (EMB-10, EMB-11)
- [ ] GA4 イベント設定（`demo_open` / `video_play` / `video_progress` / `paywall_view` / `role_switch` / `form_click` / `line_click`）(EMB-12, EMB-13)
- [ ] `?embed=1` を `noindex` に (EMB-21)
- [ ] デモ本体の title / description / OGP (EMB-20)
- [ ] 記事側に `VideoObject` 構造化データ (EMB-23)

### 5-2. 体験の総点検
- [ ] 5章「ロールを跨ぐ体験」の5パターンをすべて手動で確認
- [ ] 全28画面を開き、**空の画面が1つもないこと**を確認
- [ ] 空状態・ローディング・エラー状態を全画面で確認
- [ ] ガイドツアーを実装 (FR-605)
- [ ] localStorage 上限時の処理 (NFR-12)

### 5-3. アクセシビリティ監査
- [ ] axe-core を全画面に実行し、Critical / Serious を0件に
- [ ] **ダーク背景でのコントラスト比を実測**（特に `--fg-mid`）(A11Y-01)
- [ ] キーボードのみで全画面・全プレーヤー操作を完遂
- [ ] スクリーンリーダーでプレーヤーの状態変化が読み上げられることを確認 (A11Y-09)

### 5-4. パフォーマンス
- [ ] hls.js / Recharts を動的import に切り出し、初期バンドルを200KB以下に (NFR-06)
- [ ] 再生開始1.5秒以下を実測 (NFR-01)
- [ ] シークバー操作の60fpsを実測 (NFR-07)
- [ ] **5分連続再生でのメモリ推移を計測** (NFR-08)
- [ ] Lighthouse で Performance 90 / Accessibility 95 (NFR-09)

### 5-5. 公開と記録
- [ ] Playwright で フローA・B・C の E2E
- [ ] Vitest：`_scope` `calc` `query` のカバレッジ80%以上
- [ ] `/dev/components` を本番で非公開に
- [ ] Vercel へデプロイ、R2 の転送量アラートを設定 (COST-03)
- [ ] **転送量・費用の記録を開始** (COST-01)
- [ ] 各フェーズのキャプチャを整理し、記事の「方法」パートの素材にまとめる

---

**合計 21営業日（約4週間）**／1名専任 + レビュー体制。
Phase 1（プレーヤー）に5日を割いているのは、**ここの完成度がデモの成否そのもの**だから。既製プレーヤーを貼れば1日で済むが、それでは「Claude Codeで動画配信アプリが作れる」の証明にならない。

---

# 14. 受入基準

1. 7章の優先度 P1 の要件がすべて実装されていること
2. 6章の主要フローA・B・Cが、エンドツーエンドで完走すること
3. 5章「ロールを跨ぐ体験」の5パターンがすべて動作すること
4. **再生ボタン押下から1.5秒以内に最初のフレームが表示されること**
5. 画質・速度・字幕・チャプターのすべてが動作し、**画質切替で再生位置と再生状態が維持されること**
6. **プレーヤーの全操作がキーボードのみで完遂できること**
7. レジューム再生が動作し、リロード後も停止位置から再開できること
8. ペイウォールが動作し、**加入後に停止位置から続きが再生されること**
9. エピソードを10回連続で切り替えてもメモリが単調増加しないこと
10. 埋め込みモードの初期バンドルが80KB以下で、**記事のCore Web Vitalsを悪化させないこと**
11. デモ中の視聴が視聴分析に反映され、離脱ヒートマップが意味のある形状で表示されること
12. 全28画面のいずれにも空の状態がなく、リアルなコンテンツが表示されていること
13. axe-core で Critical / Serious の指摘が0件であること
14. 動画・画像素材のライセンスが `docs/assets-license.md` にすべて記録されていること
15. データアクセスがすべて `lib/repo/` を経由し、**視聴可否の判定が `resolveAccess()` に集約されていること**
16. **HIREBASE / RELATE と並べたときに、明確に別のプロダクトとして見えること**

---

# 判断に迷ったときのルール

1. **仕様がこの文書にない場合は、実装せずに質問する。** 推測で作らない
2. **プレーヤーの品質と、キーボード操作、空の画面を作らないことは削減対象外**
3. **既製のプレーヤーライブラリ（video.js / Plyr 等）を導入しない。** 自前実装であることがこのデモの価値
4. スコープ外（4章 Won't have）は、思いついても実装しない。特にライブ配信とDRM
5. 「デモだから」を理由に品質を落とす判断はしない
6. hls.js とイベントリスナーの破棄を忘れない。**このアプリで最も起きやすい不具合はメモリリーク**
7. **HIREBASE / RELATE のトークン・コンポーネントをコピーしない**
