## Purpose Define the media modal dialog behavior for in-page video and podcast previews. ## Requirements ### Requirement: Media modal dialog The site MUST provide a modal dialog that displays embedded media (YouTube video or podcast episode) when a user clicks a video or podcast content card on a listing surface (homepage, `/videos`, `/podcast`). The modal MUST render the following elements in order: - A header row with the content title on the left and a close button on the right - An embedded media player (YouTube iframe for videos; Spotify embed for podcast episodes when the URL is a Spotify URL; otherwise an in-modal audio player when an `audioUrl` is available) - The full description/summary text (not truncated) - The publish date and view count (when available) - A "Subscribe on YouTube" / "Follow on Spotify" CTA and a "View on YouTube" / "Listen on Spotify" CTA All modal CTAs that represent navigation MUST be implemented as crawlable anchors: - Each CTA MUST be an `` element with a non-empty `href` attribute. - The UI MUST NOT render placeholder `` elements without `href` in the initial HTML. - If CTA destinations are not known until a user selects an item, the CTA UI MUST be rendered as non-anchor elements until the destinations are known. #### Scenario: User clicks a YouTube video card - **WHEN** a user clicks a video content card on any listing surface - **THEN** a modal dialog opens displaying a YouTube iframe embed, the video title, full description, date, view count (if available), and CTAs for "Subscribe on YouTube" and "View on YouTube" #### Scenario: User clicks a podcast episode card with a Spotify URL - **WHEN** a user clicks a podcast content card whose URL is a Spotify URL - **THEN** a modal dialog opens displaying a Spotify episode embed, the episode title, full description, date, and CTAs for "Follow on Spotify" and "Listen on Spotify" #### Scenario: User clicks a podcast episode card with a non-Spotify URL - **WHEN** a user clicks a podcast content card whose URL is not a Spotify URL - **THEN** the modal dialog opens displaying the episode metadata (title, description, date) and either: - an in-modal audio player when an `audioUrl` is available - otherwise, a "Listen on Spotify" outbound link #### Scenario: Modal renders with missing optional fields - **WHEN** a content item has no view count or no summary - **THEN** the modal MUST still render cleanly with those fields omitted #### Scenario: Modal CTAs are crawlable anchors - **WHEN** the modal is present in the DOM (before any user interaction) - **THEN** the document contains no `` elements in the modal that are missing `href` ### Requirement: Embed fallback is a link only when a destination is available If an embed fallback is presented as a link to an external page, it MUST be an anchor with a valid `href`. If no destination is available, the fallback MUST be hidden or rendered as non-link text. #### Scenario: Embed fallback does not render a non-crawlable anchor - **WHEN** the modal is rendered before any item selection - **THEN** the embed fallback is not rendered as an anchor without `href` ### Requirement: Playback stops on modal close The modal MUST stop all media playback when it is dismissed, regardless of the dismissal method. The modal MUST support three dismissal methods: - Close button click - Pressing the `Escape` key - Clicking the backdrop outside the modal content After dismissal, no audio or video from the embedded player MUST continue playing. #### Scenario: User closes modal via close button - **WHEN** the modal is open with a playing YouTube video and the user clicks the close button - **THEN** the modal closes and the video playback stops immediately #### Scenario: User presses Escape while modal is open - **WHEN** the modal is open with a playing Spotify episode and the user presses the `Escape` key - **THEN** the modal closes and the audio playback stops immediately #### Scenario: User clicks the backdrop - **WHEN** the modal is open and the user clicks outside the modal content area (the backdrop) - **THEN** the modal closes and any active media playback stops immediately ### Requirement: Modal accessibility The modal MUST conform to WCAG 2.2 AA dialog patterns: - The modal MUST use the native `` element opened via `showModal()` - The modal MUST trap keyboard focus within the dialog while open - The modal MUST set `aria-modal="true"` and have an accessible label (via `aria-labelledby` referencing the title element) - Closing the modal MUST return focus to the element that triggered it (the card that was clicked) - The close button MUST have an accessible label (e.g., `aria-label="Close"`) #### Scenario: Focus is trapped within the modal - **WHEN** the modal is open and the user presses `Tab` - **THEN** focus cycles through the focusable elements within the modal and does not move to elements behind the modal #### Scenario: Focus returns to trigger on close - **WHEN** the user closes the modal - **THEN** focus returns to the card element that originally opened the modal ### Requirement: Responsive modal layout The modal MUST be responsive across viewports: - On desktop viewports, the modal MUST be centered with a max-width that leaves visible backdrop on both sides - On mobile viewports (at or below the site's mobile breakpoint), the modal MUST expand to near-full viewport width with reduced padding - Embedded media MUST scale proportionally (16:9 aspect ratio for YouTube video, fixed height for Spotify embed) #### Scenario: Modal on desktop viewport - **WHEN** the modal is opened on a desktop viewport - **THEN** the modal is centered horizontally with backdrop visible and the video embed maintains a 16:9 aspect ratio #### Scenario: Modal on mobile viewport - **WHEN** the modal is opened on a mobile viewport - **THEN** the modal expands to near-full viewport width and the video embed scales to fit ### Requirement: Embed loading state The modal MUST display a loading placeholder while the embedded media iframe is loading. #### Scenario: Iframe loading - **WHEN** the modal opens and the iframe has not yet loaded - **THEN** a placeholder (matching the site's card-placeholder style) is visible in the embed area until the iframe finishes loading