# World Cup 2026 Hub — Spec Complement (Claude Code Prompt)

> This document complements `World-Cup-2026-Hub-Spec.md` and takes precedence over it in case of conflict. Read both before starting implementation. Stack: HTML5/CSS3/JS ES2022+ (ES Modules), no backend, no frameworks, no bundler, no linting/automated tests.

---

## 1. Data Schemas (`data/`)

### `teams.json`
```json
[
  { "id": "MEX", "name": "Mexico", "flag": "mexico.svg" }
]
```

### `groups.json`
```json
{
  "A": ["MEX", "CAN", "USA", "PAN"]
}
```
12 groups (A–L), 4 teams each — 48 teams total.

### `matches.json`
Keeps the original spec's format. For knockout matches, use `bracketRef` instead of fixed `homeTeam`/`awayTeam`:

```json
{
  "id": 73,
  "phase": "Round of 32",
  "date": "2026-06-29",
  "time": "16:00",
  "stadium": "Azteca",
  "city": "Mexico City",
  "bracketRef": "R32-1"
}
```
`homeTeam`/`awayTeam` for these matches are resolved at runtime via `resolveBracketTeams()`.

### `results.json`
Adds support for penalties (knockout stage):
```json
{
  "matchId": 73,
  "homeScore": 1,
  "awayScore": 1,
  "penalties": { "home": 4, "away": 3 },
  "status": "finished"
}
```
`penalties` is optional — only present if a knockout match ended in a draw.

### `bracket-config.json` (new file)
Defines the generic bracket structure. **The only file to edit once the 8 best third-place teams are known.**

```json
{
  "round32": [
    { "id": "R32-1", "home": { "type": "group", "ref": "A", "pos": 1 }, "away": { "type": "group", "ref": "B", "pos": 2 } },
    { "id": "R32-2", "home": { "type": "group", "ref": "C", "pos": 1 }, "away": { "type": "third", "slot": 1 } }
  ],
  "thirdPlaceAssignment": {
    "1": null,
    "2": null
  }
}
```
- `round32` must have 16 entries, in the actual bracket order (array position defines bracket position).
- `thirdPlaceAssignment` maps slot → group letter (e.g., `"1": "C"`); still `null` until defined.
- Subsequent rounds (R16, QF, SF, 3rd place, Final) **have no config of their own** — they're generated by sequential pairing of winners (indices 0-1 → next 0, 2-3 → next 1, etc.).

### `stadiums.json` (update)
Adds `timezone` (IANA), used by the timezone selector (section 7):
```json
{
  "id": 1,
  "name": "Estadio Azteca",
  "city": "Mexico City",
  "capacity": 87000,
  "image": "azteca.jpg",
  "timezone": "America/Mexico_City"
}
```
**General note**: all times in `matches.json` must be in **UTC** — `formatMatchTime()` (section 7) and the `.ics` export (section 10) depend on this.

### localStorage — keys
Keys prefixed with `wc2026_`, managed via `storage.js` (thin wrapper: `get(key, fallback)` / `set(key, value)`, automatic JSON):

| Key | Content |
|---|---|
| `wc2026_simulation` | `{ "R32-1": { "winner": "MEX", "score": "2-1" }, ... }` |
| `wc2026_favorites` | `["MEX", "BRA"]` |
| `wc2026_prefs` | `{ "timeMode": "local" \| "stadium", "lastTab": "bracket", "theme": "dark" \| "light" }` |

---

## 2. Bracket Logic (`bracket.js`)

1. **Group standings**: from `groups.json` + `results.json`, compute points (3/1/0), goal difference, and goals for; sort each group.
2. **Resolve Round of 32** via `bracket-config.json`:
   - `type: "group"` → `standings[ref][pos - 1]`
   - `type: "third"` → `standings[thirdPlaceAssignment[slot]][2]`
   - If the group stage isn't finished or the slot isn't defined, show a placeholder ("Group A Winner", "Best 3rd #1").
3. **Subsequent rounds**: winner = higher score, or penalty winner on a draw. Match not played yet → "TBD".
4. **`resolveBracketTeams(matchId)`**: function exported from `bracket.js`, reused by `schedule.js` to display the correct teams for knockout matches in the schedule.
5. **Generated ID convention**: `{ROUND}-{N}` (`R16-1`, `QF-1`, `SF-1`, `FINAL`, `THIRD-PLACE`), via sequential pairing — used as keys in `wc2026_simulation` (sections 8 and 9).

---

## 3. Mock Data

Generate `data/*.json` with fictional/placeholder data (to be replaced with real data later):
- 48 fictional teams, 12 groups (A–L)
- 72 group-stage matches + 32 knockout matches = 104 matches (official 2026 format)
- ~30 stadiums (Mexico/USA/Canada)
- Mix of `"finished"`/`"scheduled"` statuses in `results.json` to test both bracket states
- `thirdPlaceAssignment` with all values set to `null`

---

## 4. Execution Plan (sequential phases)

1. File structure + mock data
2. Base layout: header, hero, dashboard
3. Match schedule: listing, filters, search, sorting
4. Group table (computed standings)
5. Stadiums page
6. Match detail modal
7. Bracket: static structure
8. Bracket: hover/highlight, animations, zoom, drag
9. Simulation mode + `localStorage`
10. Responsiveness + accessibility + entry animations
11. README + acceptance criteria checklist
12. Extra features: `storage.js`, favorites, timezone, .ics, bracket challenge, export/import prediction (sections 6–10)

At the end of each phase, summarize what was done before moving on.

---

## 5. Constraints
- Vanilla JS, ES Modules (`type="module"`), no bundler/transpiler/CDN
- No automated tests, no linter
- CSS mobile-first, variables per the original spec's palette

---

## 6. Favorites + "My Matches"

- Star icon next to each team (group table, schedule, modal) → `toggleFavorite(teamId)` in `storage.js`.
- Matches involving favorites get a visual highlight (border/badge) in the schedule, groups, and bracket.
- "My Matches" filter in the schedule (`schedule.js`): shows only matches where `homeTeam` or `awayTeam` is in `wc2026_favorites`.
- `getFavoriteMatches(matches, favorites)` shared between `schedule.js` and `bracket.js`.

---

## 7. Automatic Timezone

- Global toggle "Local time / Stadium time" → saved in `wc2026_prefs.timeMode`.
- `formatMatchTime(match, stadium, mode)` in `app.js`:
  - `mode: "local"` → `Intl.DateTimeFormat` without `timeZone` (uses the browser's timezone)
  - `mode: "stadium"` → `Intl.DateTimeFormat` with `timeZone: stadium.timezone`
- Applied in `schedule.js`, `modal.js`, and `bracket.js` cards.

---

## 8. Bracket Challenge (simulation score)

- `calculateChallengeScore(simulation, results, bracketTree)` in `bracket.js`:
  - For each knockout match with `status: "finished"`, compares the actual winner with `wc2026_simulation[matchId].winner`.
  - Returns `{ correct, total, byPhase: { "Round of 32": "3/8", ... } }` using the IDs/phases from the resolved `bracketTree` (section 2).
- Displayed as a card near the bracket: "X out of Y correct".
- No persistence of its own — recalculated on every load from `wc2026_simulation` + `results.json`.

---

## 9. Export/Import Prediction (shareable link)

- "Share prediction" → `getShareableLink()`: `base64(JSON.stringify(wc2026_simulation))` in `?prediction=...` on the current URL.
- On page load, `loadPredictionFromURL()`:
  - decodes and validates (same keys as the current `bracketTree`)
  - if valid, confirms with the user before overwriting the local `wc2026_simulation`
  - if invalid/incompatible, ignores silently
- Functions in `bracket.js`.

---

## 10. Export Match to Calendar (.ics)

New `calendar.js` module, imported by `modal.js`. "Add to calendar" button in the detail modal.

- iCalendar format (RFC 5545) — `VCALENDAR` with one `VEVENT`.
- `DTSTART`/`DTEND` in UTC (`...Z`) from `match.date` + `match.time`; fixed 2h duration.
- Knockout matches: `home`/`away` via `resolveBracketTeams(match)`.
- `Blob` (`type: "text/calendar"`) + `<a download>`; **CRLF (`\r\n`) required** between lines.

```text
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//WorldCup2026Hub//EN
BEGIN:VEVENT
UID:match-{id}@worldcup2026hub
DTSTAMP:{start}
DTSTART:{start}
DTEND:{end}
SUMMARY:{home} x {away} — {phase}
LOCATION:{stadium.name}, {stadium.city}
END:VEVENT
END:VCALENDAR
```
