# 横に流れる — Style Reference
> 黒い上映館の壁。縦のスクロールが、そのまま横の移動になる

**Theme:** dark  **Series:** PARTS 061  **World:** 架空の映画館「Kinema Nord」の特集上映。6 本の短編を横一列の板に並べる。

## Concept
区画を画面に固定(pin)し、縦のスクロール量をそのまま横の移動距離にする。板の幅の合計から画面幅を引いた長さだけ縦に送れば、端まで流れる。板は上に小さな chip(番号と種別)、下に 100px を超える題名、罫の下に mono の寸法。深紅は chip の番号・進捗の線・終端ボタンの hover だけ。読み込み時に 32% 付近の板の端から始めるので、静止画でも流れている途中が写る。

## Signature Technique
GSAP 3.12.5 + ScrollTrigger の `pin` と `x` のスクラブ。

- `gsap.to(track, { x: () => -(track.scrollWidth − innerWidth), ease: 'none', scrollTrigger: { trigger: pin, pin: true, scrub: .6, anticipatePin: 1, invalidateOnRefresh: true, end: () => '+=' + maxX } })`
- 縦の距離 = 横の距離(`end: '+=' + maxX`)なので、指の動きと板の動きが 1:1 になる
- `onUpdate` で `progress × maxX` を読み出しと上の進捗線に流す
- 初期位置: `load` 後に `ScrollTrigger.refresh()` → `scrollY < 4` かつ hash 無しなら `maxX × .32` に最も近い板の左端へ寄せ、画面幅の 4% 手前で止める(`scrollTo(0, start + offsetLeft − innerWidth × .04)`)。1 枚の題名が欠けずに写り、次の板が右から覗く
- 板の絵は Canvas2D の手続き生成(角からの光漏れの線形グラデーション + 1px の斜線 + 映写光の溜まり + ランダムノイズ ±9)。映写光は半径 `W × .62` の放射グラデーションを `lighter` で重ねる(暖色の板は rgba(255,92,92,.30)、無彩の板は rgba(237,234,227,.16) を芯に、45% で 1/3 に落とす)。画像は使わない
- reduced-motion / GSAP 不在: `body.static` にして区画を `overflow-x: auto` の横スクロールに切り替え、pin を使わない。進捗線は `scroll` で更新
- 上の帯は `mix-blend-mode: difference` で板の上でも読める

```js
const maxX = () => Math.max(0, track.scrollWidth - innerWidth);
gsap.to(track, { x: () => -maxX(), ease: 'none',
  scrollTrigger: { trigger: pin, pin: true, scrub: .6, end: () => '+=' + maxX(),
    invalidateOnRefresh: true, onUpdate: s => readout(s.progress * maxX()) } });
```

## Color Palette
| Token | HEX | Role |
|---|---|---|
| --black | #0B0B0D | 地色、板の地 |
| --panel | #141417 | 予備の面 |
| --bone | #EDEAE3 | 題名、本文 |
| --ash | #7E7E88 | chip と寸法。読み出しにも |
| --hair | rgba(237,234,227,.12) | 板の境、罫、chip の枠 |
| --crimson | #C8102E | chip の番号と進捗線。hover と板の絵の暖色にも |

## Typography
- 題名: Noto Sans JP 700 `clamp(56px, 9vw, 150px)` / line-height 1 / tracking −.02em、nowrap
- 導入の板: 500 `clamp(28px, 3.4vw, 46px)`、終端: 500 `clamp(24px, 3vw, 40px)`
- 見出し(上の区画): 500 `clamp(18px, 1.5vw, 21px)`、本文 `clamp(13px, 1.05vw, 15px)`
- chip・寸法・読み出し・タグ: JetBrains Mono 11〜12px、tracking .06〜.1em

## Spacing & Layout
- 板の幅 `min(72vw, 760px)`(導入 `min(40vw, 440px)`、終端 `min(60vw, 560px)`、640px 以下はすべて 84vw)
- 板の内側 padding: 上 `clamp(72px, 10vh, 110px)`、下 `clamp(84px, 13vh, 128px)`(下の帯に読み出しとタグが入る)
- 板の境は 1px の hairline。影や角丸は使わない。chip だけ pill
- 導入区画は 38vh、終端区画は 50vh

## Motion
- スクラブ `scrub: .6`(指を離した後 0.6 秒で追いつく)。イージングは `none`
- 進捗線は幅 %、読み出しは px と %
- reduced-motion: pin なしの横スクロールに切り替える
- 戻るボタン: `scrollTo(scrollTrigger.start)` を smooth で

## Components
- 上の帯(固定): 館名 / 特集名、右に `scroll ↓ → moves →`、下辺に 2px の進捗線
- 導入の板: chip、見出し、1 文、`program 01 → 06`
- 作品の板: canvas の絵、chip(深紅の番号)、題名、罫の下に時間と種別
- 終端の板: 1 文と pill ボタン
- 左下の読み出し `x / max · %`、右下のタグ `VALE / PARTS 061 / 横に流れる`

## Do / Don't
Do
- 縦の距離と横の距離を 1:1 にする
- 板は hairline で区切り、角丸や影を足さない
- 初期位置を途中にして、静止画でも流れが分かるようにする
- `invalidateOnRefresh` でリサイズ後の距離を取り直す

Don't
- 板の中で独立したアニメーションを回さない(流れだけで見せる)
- 深紅を面で使わない
- 板を 8 枚より増やさない(縦の距離が長くなりすぎる)
- モバイルで pin を外さない限り、板の幅を 100vw にしない(次の板が見えなくなる)

## References
- NEON Rated(cinematic gallery behind crimson neon): 無彩の壁に深紅 1 色、100px を超える題名、薄い枠だけのカード。深紅の使いどころ(番号・進捗・hover)と題名の規模をそのまま引き、書体は Noto Sans JP 700 に置き換えた
- Expo(precision notebook in graphite and signal blue): 丸い chip と hairline の区切り、信号色は「ここを見ろ」の時だけ。chip の形と帯の構造を引き、信号色を青から深紅に変えた

## Usage
1. `<script src="…/gsap.min.js">` と `ScrollTrigger.min.js`(3.12.5)を読み込む
2. HTML: `<section class="pin" id="pin"><div class="track" id="track">` の中に `.card` を横に並べる。読み出し `.read` と `.tag` は任意
3. CSS: `.pin` `.track` `.card` と `body.static` の 3 ブロックを移植する
4. JS: `<script>` ブロックを移植する。板の絵(canvas)が不要なら `document.querySelectorAll('.card canvas')` の部分を消す

| パラメータ | 既定 | 意味 |
|---|---|---|
| scrub | .6 | 追従の遅れ(秒)。0 で即時 |
| end | `'+=' + maxX()` | 縦に送る距離。横の距離と等しい |
| 初期位置 | `.32` | 読み込み時に進めておく割合。最寄りの板の左端へ寄せる。0 で先頭から |
| anticipatePin | 1 | pin 開始時のちらつき防止 |
| 板の幅 | `min(72vw, 760px)` | 1 枚の幅。合計で縦の距離が決まる |

注意: `track` の中に `position: fixed` の要素を置かない(pin と干渉する)。板の中の画像は `loading="eager"` にして `refresh()` 前に高さを確定させる。
