## Context

The site is a static Astro site (SSG) with no framework islands — all client-side interactivity uses vanilla JS via `<script is:inline>` tags. The existing design system is a dark-theme glassmorphism palette (CSS custom properties in `global.css`). Video and podcast content is rendered through `ContentCard.astro` → `StandardCard.astro`, which currently produce `<a>` tags linking directly to YouTube/Spotify (`target="_blank"`). There are no existing modal/dialog components in the codebase.

Content data comes from a pre-built JSON cache (`ContentItem` type) that contains `id`, `source`, `url`, `title`, `summary`, `publishedAt`, `thumbnailUrl`, and optional `metrics.views`. For podcasts, the cache may also include an `audioUrl` (from RSS enclosure) to enable a first-party audio fallback when a Spotify embed URL is not available.

Umami analytics primarily uses declarative `data-umami-event-*` attributes on HTML elements. For interactions that need runtime-derived properties (e.g., modal close method), the implementation may use `umami.track()`.

## Goals / Non-Goals

**Goals:**
- Keep users on-site when they click video/podcast content cards by opening an embedded player in a modal instead of navigating to YouTube/Spotify
- Stop media playback reliably when the modal is dismissed (close button, Escape key, backdrop click)
- Maintain the existing card visual layout — only the interaction target changes
- Track modal interactions (open, close, CTA clicks) via Umami using the existing `data-umami-event-*` attribute pattern
- Maintain WCAG 2.2 AA accessibility (focus trap, keyboard support, aria attributes)

**Non-Goals:**
- Changing the detail pages (`/videos/[id]`, `/podcast/[id]`) — they remain as-is for SEO/sharing
- Introducing a JS framework (React/Preact/Svelte) — the modal uses vanilla JS consistent with the rest of the project
- Instagram card behavior — Instagram cards are unaffected (they already use a different embed approach)
- Live view count fetching from YouTube API at modal-open time — views come from the pre-built cache. Live counts are a future enhancement
- Picture-in-picture or mini-player functionality
- Autoplay — the user must initiate playback within the embed

## Decisions

### 1. Use native `<dialog>` element for the modal

**Decision:** Use the HTML `<dialog>` element with `showModal()` / `close()`.

**Rationale:** The native `<dialog>` provides built-in backdrop, focus trapping, Escape-to-close, and `aria-modal` semantics for free. No framework dependency needed, consistent with the project's vanilla-JS approach. Browser support is universal in evergreen browsers.

**Alternatives considered:**
- Custom `<div>` with manual focus trap + backdrop: More code, more accessibility bugs, no benefit.
- Astro island with React/Preact: Adds a build dependency and hydration cost for a single component. Overkill for a modal.

### 2. Destroy iframe on close to stop playback

**Decision:** When the modal closes, remove the iframe `src` (set to `about:blank` or remove the iframe entirely) rather than using the YouTube IFrame API or Spotify SDK to pause.

**Rationale:** Both YouTube and Spotify embeds load third-party scripts that are outside our control. The YouTube IFrame API requires loading `https://www.youtube.com/iframe_api` and managing a `YT.Player` instance. Spotify has no public embed API for programmatic pause. Removing/blanking the iframe src is the simplest, most reliable way to guarantee playback stops — no race conditions, no API loading, works for both platforms identically.

**Alternatives considered:**
- YouTube IFrame API `player.stopVideo()`: Requires async API load, player ready callbacks, and doesn't work for Spotify. Two different code paths for two platforms.
- `postMessage` to iframe: Relies on undocumented/unstable message formats per platform. Fragile.

### 3. Cards become `<button>` triggers instead of `<a>` links

**Decision:** For video/podcast content cards, change the root element from `<a href="..." target="_blank">` to a `<button>` (or a clickable `<div>` with `role="button" tabindex="0"`) that opens the modal. The card visual stays the same.

**Rationale:** The card no longer navigates anywhere — it opens a modal. Semantically, this is a button action, not a link. Using a `<button>` (or button role) is correct for accessibility and avoids the confusion of an `<a>` that doesn't navigate.

**Implementation note:** `StandardCard.astro` currently renders an `<a>`. A new prop (e.g., `as="button"`) or a separate `StandardCardButton.astro` wrapper can handle this. The card must carry the `ContentItem` data as `data-*` attributes so the modal script can read them on click.

**Alternatives considered:**
- Keep as `<a>` with `href="#"` and `preventDefault`: Semantically incorrect, accessibility tools may announce it as a link.
- Wrap card in both `<a>` (for SEO/no-JS fallback) and add JS override: Adds complexity. The detail pages already serve the SEO purpose.

### 4. Data flow via `data-*` attributes on card elements

**Decision:** Cards embed the `ContentItem` data they need (id, source, url, title, summary, publishedAt, thumbnailUrl, views) as `data-*` attributes on the card element. The modal script reads these attributes on click to populate the modal.

**Rationale:** The site is SSG — no runtime data store or state management. `data-*` attributes are the standard Astro pattern for passing server-rendered data to inline scripts (same approach used for nav toggle with `data-nav-toggle`, `data-open`). No serialization overhead, no global state, works with any number of cards.

**Alternatives considered:**
- Global JSON blob in a `<script>` tag: Works but couples the modal to a specific data shape and page. `data-*` is more composable.
- Re-fetch from cache at runtime: The site is static. There's no runtime API to call.

### 5. Embed URL construction

**Decision:**
- **YouTube:** Extract video ID from `item.url` (pattern: `youtube.com/watch?v={id}`) → embed URL: `https://www.youtube.com/embed/{id}?rel=0&modestbranding=1`
- **Spotify/Podcast:** When a Spotify episode ID is available, embed using `https://open.spotify.com/embed/episode/{id}` with Spotify's recommended iframe allowlist and sizing (fixed height ~232px). If a Spotify embed URL is not available, fall back to an in-modal HTML5 `<audio>` player when an `audioUrl` is present, otherwise show an outbound "Listen on Spotify" link.

**Rationale:** YouTube embed URLs have a stable, well-documented format. Spotify embed works for episodes hosted on Spotify. For non-Spotify podcast URLs, embedding isn't universally possible, so the modal gracefully degrades to showing metadata + a link.

In practice, many podcast feeds provide a creator/profile URL (or a podcasters.spotify.com URL) plus an MP3 enclosure, but not the canonical `open.spotify.com/episode/{id}` URL needed for a Spotify embed. The `audioUrl` fallback keeps playback on-site in that case.

### 6. Umami event taxonomy for modal interactions

**Decision:** Introduce a new event name `media_preview` for card clicks that open the modal (replacing `outbound_click`). Add distinct events for modal interactions:

| Interaction | Event Name | Key Properties |
|---|---|---|
| Card click → modal opens | `media_preview` | `target_id`, `placement`, `title`, `type`, `source` |
| Modal close (X / Escape / backdrop) | `media_preview_close` | `target_id`, `close_method` (`button` / `escape` / `backdrop`) |
| "View on YouTube" / "Listen on Spotify" CTA | `cta_click` | `target_id`, `placement=media_modal`, `platform`, `target_url` |
| "Subscribe on YouTube" / "Follow on Spotify" CTA | `cta_click` | `target_id`, `placement=media_modal`, `platform`, `target_url` |

**Rationale:** `outbound_click` is no longer accurate — the user isn't leaving the site. `media_preview` describes what actually happens. Close-method tracking (`close_method` property) allows measuring whether users engage with the content or immediately dismiss. CTA clicks inside the modal reuse the existing `cta_click` event name with `placement=media_modal` for consistency with the CTA tracking taxonomy.

### 7. Modal layout mirrors the card but expanded

**Decision:** The modal content area uses the same information architecture as the card (image/embed → title → summary → footer) but expanded:
1. **Header row:** Title (left) + Close button (right)
2. **Embed area:** YouTube iframe or Spotify embed (16:9 aspect ratio for video, fixed height for podcast)
3. **Body:** Full description/summary (not truncated), date, view count
4. **Footer CTAs:** "Subscribe on YouTube" / "Follow on Spotify" + "View on YouTube" / "Listen on Spotify"

**Rationale:** This mirrors the user's request for "an extended version of the card layout." The familiar structure reduces cognitive load.

## Risks / Trade-offs

- **[Spotify embed availability]** → Not all podcast episodes have Spotify episode URLs. Mitigation: use first-party `<audio>` playback from RSS enclosure (`audioUrl`) when available; otherwise show a "Listen on Spotify" outbound link. Optional: maintain a small override map (`content/podcast-spotify-map.json`) to supply episode embed URLs when desired.
- **[Third-party embed loading time]** → YouTube/Spotify iframes add load time. Mitigation: iframes are only loaded when the modal opens (lazy). A loading placeholder is shown until the iframe loads.
- **[No-JS fallback]** → If JavaScript is disabled, the `<button>` cards won't open a modal. Mitigation: Include a `<noscript>` hint or make the card a link as a no-JS fallback (progressive enhancement). The detail pages (`/videos/[id]`, `/podcast/[id]`) always exist as a non-JS path.
- **[Content Security Policy]** → Embedding YouTube/Spotify requires `frame-src` to allow these domains. Mitigation: verify CSP headers (if any) allow `youtube.com` and `spotify.com` iframe sources. Currently no CSP is set, so no issue.
- **[Modal on mobile]** → Full-screen modal on small viewports may feel heavy. Mitigation: on mobile breakpoints, the modal takes full viewport width with reduced padding, and the embed scales responsively via `aspect-ratio` or percentage-based sizing.
