# RC114 · Pass player media-card contract cleanup

## Scope

RC114 fixes the RC113 pass-player image/fallback state shown in screenshots where the premium exercise image escaped the left media column and covered duplicate exercise text behind it.

This release intentionally does **not** refactor the player lifecycle. `app.js` still owns `startWorkout`, `renderStep`, `renderVideo`, `renderRest`, timers, preload, wake lock and video source priority. RC114 changes only the media fallback markup/CSS contract.

## Decision carried forward

The pass player now has a stricter surface contract:

- `.next-pass-media-card` / `#videoWrap` is media-only.
- `.next-pass-info-card` is the only place for exercise name, prescription, coach cues and superset/rest context.
- Video controls remain directly below `#videoWrap` and are visible only for video-capable states.
- Premium S2P1 images remain direct-composite assets, not layered cutouts.

## Code paths reused

Reused existing paths/classes:

- `preview.html` active-pass structure:
  - `.next-pass-media-card`
  - `#videoWrap.next-pass-video-wrap`
  - `#passVideoControls.next-pass-video-controls`
  - `.next-pass-info-card`
  - `.next-pass-progress-below`
- Existing app lifecycle in `assets/js/app.js`:
  - `renderStep()`
  - `renderVideo()`
  - `descriptiveExerciseVisual()`
  - `renderRest()`
  - RC113 `passVideoPlay/Pause/Rewind`
- Existing image service:
  - `window.NEXTExerciseIllustrationService.render(...)`
  - `assets/js/core/exercise-illustrations.js`
  - `assets/img/exercises/premium/s2p1/<exerciseKey>.webp`

No new renderer, no new SVG fallback system, no new background/cutout model.

## Implementation notes

### `assets/js/app.js`

`descriptiveExerciseVisual()` now emits media-only markup:

```html
<div class="no-video exercise-visual-fallback next-pass-media-only has-exercise-illustration">
  <div class="exercise-visual-media next-pass-media-stage">
    <!-- existing NEXTExerciseIllustrationService output -->
  </div>
</div>
```

It no longer duplicates:

- exercise name
- reps/prescription
- benefit copy
- coach-cues
- fallback kicker text

Those remain in `.next-pass-info-card`.

### `assets/css/next-system.css`

Added RC114 active-pass CSS ownership:

- `.next-pass-media-card` and `#videoWrap` are clipped/isolated.
- `.next-pass-media-only` fills the media area.
- `.next-pass-media-stage` contains premium images.
- Premium images use `object-fit: contain` inside the media card.
- Hidden copy selectors are kept as a belt-and-suspenders guard if legacy markup ever leaks through.
- Video controls are forced hidden for image/fallback states such as `projector-blocked`, `missing`, `timer`, and `ota-fallback`.

## QA states rendered

Real-DOM QA rendered the actual `preview.html` with inlined real CSS/JS via Playwright `page.set_content()`. External network/iframes were blocked; local proxy calls were mocked.

States:

1. YouTube/video state
2. Premium image fallback state using `projectorNoYoutube=true` on `Linear Pogos`
3. Rest state

Viewports:

- mobile: `390x844`
- iPad: `820x1180`
- desktop: `1440x1024`

## Real-DOM verification

Stored in `qa_screenshots/rc114_pass_media_contract_dom_metrics.json`.

Key assertions passed:

- Image state uses existing fallback path with `data-video-source="projector-blocked"`.
- Premium image exists in `#videoWrap .next-pass-media-stage`.
- `.exercise-visual-copy` is not present in `#videoWrap`.
- fallback kicker/copy is not present in `#videoWrap`.
- media card does not contain exercise-name text.
- info card does contain exercise-name text.
- premium image bounding box stays within `.next-pass-media-card`.
- premium image does not overlap `.next-pass-info-card`.
- video controls are hidden for image-only/rest states.
- video controls remain visible for YouTube state.
- YouTube controls still issue `playVideo`, `pauseVideo`, and `seekTo`.
- progress remains below the media/info layout, not in topbar.
- no local broken images were detected.

## Screenshots

### YouTube/video state

- `qa_screenshots/rc114_pass_player_youtube_mobile_390x844.png`
- `qa_screenshots/rc114_pass_player_youtube_ipad_820x1180.png`
- `qa_screenshots/rc114_pass_player_youtube_desktop_1440x1024.png`

### Premium image fallback state

- `qa_screenshots/rc114_pass_player_image_mobile_390x844.png`
- `qa_screenshots/rc114_pass_player_image_ipad_820x1180.png`
- `qa_screenshots/rc114_pass_player_image_desktop_1440x1024.png`

### Rest state

- `qa_screenshots/rc114_pass_player_rest_mobile_390x844.png`
- `qa_screenshots/rc114_pass_player_rest_ipad_820x1180.png`
- `qa_screenshots/rc114_pass_player_rest_desktop_1440x1024.png`

## Commands run

```bash
node tests/rc114_pass_media_contract_smoke.js
node tests/rc113_pass_video_controls_smoke.js
node tests/rc112_pass_chrome_reposition_smoke.js
node tests/rc111_pass_player_standardization_smoke.js
node tests/rc110_pass_page_standardization_smoke.js
node tests/rc109_asset_path_cleanup_smoke.js
node tests/rc107_asset_and_library_smoke.js
node tests/rc109_library_compactness_static_smoke.js
node tests/rc104_direct_composite_smoke.js
node tests/rc106_smoke.js assets/js/core/exercise-illustrations.js "$PWD"
find assets/js -name '*.js' -print0 | xargs -0 -n1 node --check
php -l ota_youtube_proxy.php
php -l video_backup_api.php
```

Real-DOM QA was generated state-by-state with:

```bash
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child youtube mobile 390 844
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child image mobile 390 844
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child rest mobile 390 844
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child youtube ipad 820 1180
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child image ipad 820 1180
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child rest ipad 820 1180
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child youtube desktop 1440 1024
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child image desktop 1440 1024
python3 tests/rc114_pass_media_contract_real_dom_qa.py --child rest desktop 1440 1024
```

The combined metrics file was then validated and written to:

```text
qa_screenshots/rc114_pass_media_contract_dom_metrics.json
```

## Caveat

Real deploy host was not tested from this sandbox. YouTube iframe content is blocked by the QA route, so screenshots show the iframe placeholder rather than the external video. DOM/API path is still verified with `data-video-source`, iframe presence, and YouTube command capture.
