diff --git a/.agents/TODO.md b/.agents/TODO.md new file mode 100644 index 0000000..bff599e --- /dev/null +++ b/.agents/TODO.md @@ -0,0 +1,79 @@ +# TODO β€” World Cup 2026 Hub + +Checklist of what needs to be done. Organized by build step (approved plan, one approval gate per step). + +Use checkboxes to track progress. Items marked **πŸ”΄ BLOCKER** prevent release; **🟑 IMPORTANT** must be done but don't block; **🟒 OPTIONAL** can be deferred. + +--- + +## 1. Foundation + +### πŸ”΄ BLOCKER +- [x] ~~Step 0 β€” Project memory: `.agents/` + auto-memory pointer + `git init`~~ +- [x] ~~Step 1 β€” File structure + mock data (48 teams, 12 groups, 104 matches UTC, ~30 stadiums, `bracket-config.json` with null third-place slots, mixed results)~~ +- [x] ~~Step 2 β€” Base layout: SPA shell, header + nav, hero (next match + countdown), dashboard cards, `style.css` palette, `app.js` loadData/routing, `storage.js`, `i18n.js` (EN/PT toggle)~~ + +--- + +## 2. Core views + +### πŸ”΄ BLOCKER +- [x] ~~Step 3 β€” Match schedule: list, filters (date/group/phase/team/stadium), search, sort~~ +- [x] ~~Step 4 β€” Group standings computed from results (3/1/0, GD, GF)~~ +- [x] ~~Step 5 β€” Stadiums page (cards + matches held)~~ +- [x] ~~Step 6 β€” Match detail modal (result, penalties, future-stats placeholder, ARIA)~~ + +--- + +## 3. Bracket + +### πŸ”΄ BLOCKER +- [x] ~~Step 7 β€” Static bracket: R32 from config, sequential pairing to FINAL, placeholders, `resolveBracketTeams()`~~ +- [x] ~~Step 8 β€” Interactions: hover path highlight, animations, wheel/pinch zoom, drag/pan~~ +- [x] ~~Step 9 β€” Simulation mode: pick winner + score, propagate rounds, `wc2026_simulation`, reset~~ + +--- + +## 4. Polish & docs + +### πŸ”΄ BLOCKER +- [x] ~~Step 10 β€” Responsiveness (≀767 / 768–1439 / 1440+), accessibility (ARIA, keyboard, focus, contrast), entry animations~~ + +### 🟑 IMPORTANT +- [x] ~~Step 11 β€” README (deploy guide, JSON maintenance, `bracket-config.json` how-to) + spec Β§18 acceptance checklist~~ (done after step 12, at user request) + +--- + +## 5. Extra features (complement spec Β§6–10) + +### 🟑 IMPORTANT +- [x] ~~Step 12a β€” Favorites + "My Matches" filter (`toggleFavorite`, `getFavoriteMatches`, highlights)~~ +- [x] ~~Step 12b β€” Timezone toggle "Local / Stadium time" (`wc2026_prefs.timeMode`)~~ +- [x] ~~Step 12c β€” `.ics` export (`calendar.js`, CRLF, UTC, 2h duration)~~ + +### 🟒 OPTIONAL +- [x] ~~Step 12d β€” Bracket challenge score card (`calculateChallengeScore`)~~ +- [x] ~~Step 12e β€” Share/import prediction via `?prediction=` base64 link~~ + +--- + +## 6. Post-launch (real data) + +### 🟑 IMPORTANT +- [ ] Replace mock `data/*.json` with real World Cup 2026 data +- [ ] Fill `thirdPlaceAssignment` in `bracket-config.json` after group stage +- [ ] Real stadium photos + team flag SVGs in `assets/images/` + +--- + +## Quick final checklist + +``` +[x] All 104 matches load from JSON +[x] Standings + bracket fully derived from results.json +[x] Simulation works and survives reload (localStorage) +[x] GitHub Pages ready (all paths relative β€” verified; actual deploy pending) +[x] Mobile: bracket scroll + zoom + drag +[x] JS < 300KB (74 KB measured) [ ] Lighthouse > 90 (run after deploy) +[x] EN/PT toggle covers every UI string +``` diff --git a/.agents/project-map.md b/.agents/project-map.md new file mode 100644 index 0000000..6bef875 --- /dev/null +++ b/.agents/project-map.md @@ -0,0 +1,154 @@ +# Project Map β€” World Cup 2026 Hub + +Navigation map of the codebase. Use this to find which file owns a concern before reading code. + +> **Status 2026-06-12 (steps 1–10 + 12 done):** everything works β€” all views, bracket interactions, simulation, responsive/a11y pass, favorites, time toggle, challenge, share link, `.ics` export. Remaining: step 11 (full README + acceptance checklist). Spec source of truth: `world-cup-2026-hub-spec-en.md` + `complement-spec-worldcup2026-en.md` (complement **wins on conflict**). + +--- + +## File tree + +``` +worldcup2026/ +β”œβ”€β”€ .agents/ ← Internal documentation for AI agents +β”‚ β”œβ”€β”€ project-map.md This file +β”‚ β”œβ”€β”€ project-memory.md Context, decisions, gotchas +β”‚ └── TODO.md 12-step build checklist +β”‚ +β”œβ”€β”€ index.html β˜… SPA shell β€” header, nav tabs (Home, Matches, +β”‚ Groups, Knockout, Stadiums), hero, dashboard, +β”‚ modal container; loads app.js as ES module +β”‚ +β”œβ”€β”€ assets/ +β”‚ β”œβ”€β”€ css/ +β”‚ β”‚ β”œβ”€β”€ style.css β˜… Palette variables, glassmorphism base, layout, +β”‚ β”‚ β”‚ components β€” mobile-first +β”‚ β”‚ β”œβ”€β”€ bracket.css Bracket columns, connectors, highlight states +β”‚ β”‚ └── animations.css Entry (fade-in, slide-up/left) + interaction +β”‚ β”‚ (hover-scale/glow, pulse, line-draw) +β”‚ β”œβ”€β”€ js/ +β”‚ β”‚ β”œβ”€β”€ app.js β˜… Entry point: loadData() (Promise.all over data/), +β”‚ β”‚ β”‚ tab routing + lastTab, formatMatchTime(), hero, +β”‚ β”‚ β”‚ dashboard, countdown +β”‚ β”‚ β”œβ”€β”€ schedule.js Match list, filters, search, sort, "My Matches" +β”‚ β”‚ β”œβ”€β”€ groups.js Standings computation (3/1/0, GD, GF) + group tables +β”‚ β”‚ β”œβ”€β”€ stadiums.js Stadium cards + "view matches" cross-link +β”‚ β”‚ β”œβ”€β”€ bracket.js β˜… Bracket tree resolution, resolveBracketTeams(), +β”‚ β”‚ β”‚ simulation, challenge score, share prediction +β”‚ β”‚ β”œβ”€β”€ modal.js Match detail modal (ARIA dialog) +β”‚ β”‚ β”œβ”€β”€ storage.js localStorage wrapper β€” wc2026_* keys, auto-JSON +β”‚ β”‚ β”œβ”€β”€ i18n.js EN/PT-BR dicts + t(key), lang toggle +β”‚ β”‚ └── calendar.js .ics export (RFC 5545, CRLF, Blob download) +β”‚ └── images/ Team flag SVGs, stadium placeholders +β”‚ +β”œβ”€β”€ data/ All content β€” the only thing edited for real data +β”‚ β”œβ”€β”€ teams.json 48 teams: { id, name, flag } +β”‚ β”œβ”€β”€ groups.json { "A": [4 team ids], ... } Γ— 12 (A–L) +β”‚ β”œβ”€β”€ matches.json 104 matches; UTC times; knockout uses bracketRef +β”‚ β”œβ”€β”€ results.json { matchId, homeScore, awayScore, penalties?, status } +β”‚ β”œβ”€β”€ stadiums.json ~30 stadiums: { id, name, city, capacity, image, timezone } +β”‚ └── bracket-config.json β˜… 16 R32 slot definitions + thirdPlaceAssignment β€” +β”‚ the ONLY file to edit once real 3rd places are known +β”‚ +β”œβ”€β”€ README.md Setup, GitHub Pages deploy, JSON maintenance guide +β”œβ”€β”€ how-update.md Real-data migration runbook (mock β†’ real WC2026 data/*.json) +β”œβ”€β”€ world-cup-2026-hub-spec-en.md Main spec +└── complement-spec-worldcup2026-en.md Complement spec (precedence on conflict) +``` + +β˜… = critical files. Most changes touch one of them. + +--- + +## Key flows + +### 1. Data load β†’ render + +``` +index.html (type="module") + └─ app.js: loadData() ── Promise.all ──> data/*.json + β”œβ”€ hero + dashboard (app.js) + β”œβ”€ schedule.js ──┐ + β”œβ”€ groups.js β”œβ”€β”€ render into tab panels + β”œβ”€ bracket.js β”€β”€β”˜ + └─ modal.js (on match click, from any view) +``` + +### 2. Bracket resolution + +``` +groups.json + results.json + └─ groups.js: standings (pts 3/1/0 β†’ GD β†’ GF) + └─ bracket.js: R32 from bracket-config.json + β”œβ”€ type:"group" β†’ standings[ref][pos-1] + β”œβ”€ type:"third" β†’ standings[thirdPlaceAssignment[slot]][2] (null β†’ placeholder) + └─ R16…FINAL: sequential pairing of winners (0-1β†’0, 2-3β†’1, …) + └─ wc2026_simulation overlays user picks (never mutates JSON) +``` + +### 3. Time display + +``` +matches.json time (UTC) ── formatMatchTime(match, stadium, mode) + β”œβ”€ mode "local" β†’ Intl.DateTimeFormat() (browser tz) + └─ mode "stadium" β†’ Intl.DateTimeFormat({ timeZone: stadium.timezone }) +``` + +--- + +## Conventions + +### Imports +- ES Modules only (`type="module"`), relative paths, no bundler/CDN. + +### Naming +- Team ids: 3-letter uppercase (`MEX`, `BRA`). Knockout match ids: `R32-1`…`R32-16`, `R16-1`…, `QF-1`…, `SF-1`/`SF-2`, `THIRD-PLACE`, `FINAL`. +- localStorage keys prefixed `wc2026_`, accessed only through `storage.js`. + +### Content / i18n +- All user-facing strings go through `i18n.js` `t(key)` β€” never hardcode UI text in HTML/JS. +- Data values (team names, stadium names, cities) come from JSON and are not translated. + +--- + +## Where is each thing? + +| Question | Answer | +|---|---| +| Where do I update scores / match status? | `data/results.json` | +| Where do I set the 8 best third-place teams? | `data/bracket-config.json` β†’ `thirdPlaceAssignment` | +| Where do I add/translate a UI label? | `assets/js/i18n.js` (both EN and PT dicts) | +| Where is the standings math? | `assets/js/groups.js` | +| Where are knockout teams resolved (placeholders, TBD)? | `assets/js/bracket.js` β†’ `resolveBracketTeams()` | +| Where is simulation state stored/cleared? | `localStorage` key `wc2026_simulation`, via `assets/js/storage.js` | +| Where do I change colors/theme? | CSS variables at the top of `assets/css/style.css` | +| Where do I add a stadium? | `data/stadiums.json` + image in `assets/images/` | +| How do I replace mock data with real WC2026 data? | `how-update.md` (root) β€” schemas, order of operations, integrity checklist | + +--- + +## Main functions + +(Planned signatures from the complement spec β€” confirmed/updated as each step is implemented.) + +| Function | File | Parameters | Returns | Description | +|---|---|---|---|---| +| `loadData` | `assets/js/app.js` | `()` | `Promise` | Fetches all `data/*.json` in parallel, caches in memory | +| `formatMatchTime` | `assets/js/app.js` | `(match, stadium, mode)` | `string` | UTC β†’ display time; `mode` is `"local"` or `"stadium"` | +| `get` / `set` | `assets/js/storage.js` | `(key, fallback)` / `(key, value)` | `any` / `void` | localStorage wrapper, auto JSON parse/stringify | +| `t` | `assets/js/i18n.js` | `(key)` | `string` | Translated UI string for current lang | +| `resolveBracketTeams` | `assets/js/bracket.js` | `(matchOrRef)` | `{ home, away }` of `{ team, label }` | Display slots for any match (group or knockout); reused by schedule/modal/filters | +| `getBracketTree` | `assets/js/bracket.js` | `()` | `{ rounds, third, nodesByRef, champion }` | Cached resolved bracket tree | +| `invalidateBracket` | `assets/js/bracket.js` | `()` | `void` | Drop the cached tree (simulation overlay, step 9) | +| `calculateChallengeScore` | `assets/js/bracket.js` | `(simulation, results, bracketTree)` | `{ correct, total, byPhase }` | Compares user picks vs finished results | +| `getShareableLink` | `assets/js/bracket.js` | `()` | `string` | Current URL + `?prediction=base64(simulation)` | +| `loadPredictionFromURL` | `assets/js/bracket.js` | `()` | `void` | Decodes, validates, confirms before overwriting local simulation | +| `toggleFavorite` | `assets/js/storage.js` | `(teamId)` | `void` | Add/remove team in `wc2026_favorites` | +| `computeStandings` | `assets/js/groups.js` | `()` | `{ [letter]: Row[] }` | Sorted standings per group, finished matches only | +| `isGroupFinished` | `assets/js/groups.js` | `(letter)` | `boolean` | All 6 group matches have status finished | +| `navigateTo` | `assets/js/app.js` | `(tab)` | `void` | Programmatic tab switch (cross-view links) | +| `setStadiumFilter` | `assets/js/schedule.js` | `(stadiumName)` | `void` | Reset filters + show one stadium's matches | +| `getFavoriteMatches` | `assets/js/bracket.js` | `(matches, favorites)` | `Match[]` | Matches involving favorited teams (resolved slots); used by schedule | +| `calculateChallengeScore` | `assets/js/bracket.js` | `(simulation, results, bracketTree)` | `{ correct, total, byPhase }` | Picks vs real finished knockout results | +| `getShareableLink` / `loadPredictionFromURL` | `assets/js/bracket.js` | `()` | `string` / `void` | base64 `?prediction=` export/import with validation + confirm | +| `exportMatchToICS` | `assets/js/calendar.js` | `(match, stadium)` | `void` | RFC 5545 VEVENT (UTC, 2h, CRLF, escaped TEXT), Blob download | diff --git a/.agents/project-memory.md b/.agents/project-memory.md new file mode 100644 index 0000000..928c48e --- /dev/null +++ b/.agents/project-memory.md @@ -0,0 +1,211 @@ +# Project Memory β€” World Cup 2026 Hub + +Persistent memory for this project. Read this before any significant change. + +--- + +## Context + +Static web app showing the FIFA World Cup 2026 (Mexico/USA/Canada, 48 teams) β€” schedule, group standings, interactive knockout bracket with user simulation, stadiums. Hosted on GitHub Pages, all content from `data/*.json`. Started 2026-06-11 from two spec documents; built step-by-step with user approval between steps. + +### What this project is +- A **personal/portfolio** piece β€” visual polish (glassmorphism, animations) is a primary goal, not a nice-to-have. +- A **static SPA**: one `index.html`, ES-module vanilla JS, JSON as the only "database". +- Maintained by editing JSON only β€” code should never need touching to update scores/teams. + +### What this project is **not** +- No backend, no database, no build step, no bundler, no CDN dependencies, no frameworks. +- No automated tests, no linter (explicit spec constraint). +- Not real-data-complete: ships with mock data (fictional teams) to be replaced later. + +--- + +## Priority objectives + +1. **Spec compliance** β€” both spec files define scope; `complement-spec-worldcup2026-en.md` wins on conflict. +2. **Visual quality** β€” FIFA/UCL/Apple-inspired, glassmorphism, smooth animations; portfolio-grade. +3. **Interactive bracket** β€” hover path highlight, zoom, drag, simulation mode; the centerpiece feature. +4. **Easy maintenance** β€” real data drop-in via JSON; `bracket-config.json` is the only file edited after group stage. +5. **Performance/accessibility** β€” Lighthouse > 90, first render < 2s, JS < 300KB, ARIA + keyboard nav. + +--- + +## Technical decisions and rationale + +### Stack +- **Vanilla HTML/CSS/JS ES2022+, ES Modules** β€” spec mandate; GitHub Pages serves static files only. Frameworks/bundlers explicitly forbidden. +- **EN/PT-BR UI toggle via `i18n.js`** (user decision 2026-06-11) β€” not in spec; spec UI examples are EN, user wants both. Tiny dict + `t(key)`, persisted in `wc2026_prefs.lang`. Alternative (EN only) rejected by user. +- **`storage.js` pulled forward to step 2** (spec places it in phase 12) β€” prefs (`lang`, `lastTab`) are needed from the base-layout step; building it late would mean refactoring. + +### Data model +- **All match times in UTC** in `matches.json`; converted at render via `Intl.DateTimeFormat` (`formatMatchTime`). `.ics` export depends on this. +- **Knockout matches carry `bracketRef` instead of teams** β€” teams resolved at runtime from standings + `bracket-config.json`; rounds after R32 have no config, generated by sequential pairing of winners (indices 0-1 β†’ 0, 2-3 β†’ 1, …). +- **Simulation never mutates JSON** β€” overlay stored in `localStorage` `wc2026_simulation`, keyed by bracket match ids (`R32-1`, `QF-2`, `FINAL`, …). + +### Mock data design (2026-06-11, step 1) +- **Real country names, fictional results** β€” generated deterministically (seed 2026), script deleted after run; data is now static JSON. +- **State crafted to test both bracket modes:** matchdays 1-2 all `finished`; matchday 3 `finished` for groups A–F, `scheduled` for G–L (match 61, Group G, is `live`). So R32 slots fed by A–F resolve to real teams; G–L and all `third` slots show placeholders. +- **Knockout results:** `R32-2` (match 74) finished 1-1 + penalties 4-3; `R32-4` (match 76) finished 2-0. Everything else `scheduled` with `null` scores β€” `results.json` has an entry for **all 104 matches**. +- **Image paths:** `flag`/`image` JSON values are relative to `assets/images/` (e.g. `flags/mex.svg`, `stadiums/azteca.svg`). +- **Opener** (match 1) is MEX at Estadio Azteca 2026-06-11; **final** (match 104) at MetLife 2026-07-19. + +### Base layout decisions (2026-06-11, step 2) +- **Hero priority: live > next scheduled** β€” a live match replaces the countdown with the score + pulse badge. Mock data keeps match 61 permanently `live`, so the countdown only shows if that status is changed (verified working by temporarily setting it to `scheduled`). +- **i18n mechanics:** static HTML uses `data-i18n` / `data-i18n-aria` attributes re-applied by `applyI18n()`; dynamic renders call `t()` and listen for the `langchange` event on `document`. Phases translate via `translatePhase()` (PT: R32 = "16 avos de final"). +- **Tab routing:** hash (`#matches`) + `wc2026_prefs.lastTab`, `history.replaceState` to avoid history spam; precedence on load: hash β†’ lastTab β†’ home. +- **Default language:** `navigator.language` startsWith `pt` β†’ PT, else EN; only persisted when the user clicks the toggle. +- **Preview server:** `.claude/launch.json` at `R:\lucas-kalil\Projects\` defines `worldcup2026` (python http.server, port 8126) for the Claude Preview panel. + +### Schedule + performance decisions (2026-06-11, step 3) +- **`schedule.js` ⇄ `app.js` circular import is intentional** β€” `app.js` calls `initSchedule()`, `schedule.js` imports `getData`/`formatMatchTime`/`flagSrc` back. Safe in native ESM because all calls happen after both modules evaluate; keep this pattern for `groups.js`/`bracket.js`/`modal.js`. +- **Filter UX:** toolbar is rebuilt only on init/langchange/clear (state restored programmatically); list re-renders on every filter change. Filter state is in-memory only (not persisted) by design. +- **Knockout cards show "TBD"** until step 7 swaps `teamColumnHTML`'s lookup to `resolveBracketTeams()`. +- **Perf: no `backdrop-filter` on repeated cards** β€” `.match-card` overrides `.glass` blur (huge paint cost Γ— 104 cards, invisible over the smooth gradient). Same rule applies to any future card grid (stadiums, bracket). +- **Perf: fixed gradient lives on `body::before` (position: fixed)**, not `background-attachment: fixed` (the latter repaints the whole background on scroll). + +### Standings decisions (2026-06-11, step 4) +- **Only `status: "finished"` matches count toward standings** β€” live scores are ignored until full-time (keeps standings stable and bracket resolution deterministic). +- **Tiebreak order:** points β†’ goal difference β†’ goals for β†’ team id alphabetical (stable fallback). Verified against an independent Python computation for groups A and G. +- **`computeStandings()` / `isGroupFinished()` exported from `groups.js`** β€” bracket.js (step 7) must import these instead of recomputing. + +### Stadiums decisions (2026-06-11, step 5) +- **`stadiums.js` module added** β€” spec Β§4 has no module for the stadiums view; a dedicated view module keeps the per-view pattern (schedule/groups/bracket) instead of growing `app.js`. +- **Cross-link:** "View matches" on a stadium card calls `setStadiumFilter(name)` (exported by `schedule.js`, resets other filters) + `navigateTo('matches')` (exported by `app.js`). + +### Modal decisions (2026-06-11, step 6) +- **Native `` + `showModal()`** β€” focus trap, Esc-to-close and `::backdrop` come free; no custom trap code. Backdrop click detected via `event.target === dialog` (content is in a padded inner div). +- **Focus restore:** opener element saved in `openMatchModal()` and re-focused on `close`. +- **Card β†’ modal wiring is event delegation on `#schedule-root`** (click + Enter/Space keydown), so it survives list re-renders. Cards got `tabindex="0" role="button" aria-label` already in this step. +- **`openMatchModal(matchId)` is the public API** β€” bracket (step 7) and any future view should call it rather than building their own. + +### Bracket decisions (2026-06-11, step 7) +- **Tree model is language-neutral**: slots are `{ teamId }` or `{ ph: { kind, … } }`; placeholder text is produced at render time by `slotDisplay()` so language switches never invalidate the tree. +- **Tree is cached** in `bracket.js`; `invalidateBracket()` exists for the simulation overlay (step 9). Results are static per page load, so nothing else invalidates it. +- **`resolveBracketTeams(matchOrRef)`** accepts a match object (group or knockout) or a bracketRef string and always returns `{ home, away }` as `{ team: Team|null, label: string }` β€” schedule cards, modal, and search/team filters all consume this, so knockout matches become searchable/filterable once resolved. +- **Connector geometry**: all bracket columns share equal height with `flex: 1` slots, so pair children sit at 25%/75% and the next round's node at 50% β€” pure-CSS connectors (`::before`/`::after` stubs + pair vertical) meet exactly. Column gap 44px = 22px out-stub + 22px in-stub. Breaking the equal-height invariant breaks the lines. +- **Final column** holds champion box (top), FINAL (middle, aligns with SF pair), third-place block (bottom); its champion/third slots suppress the incoming connector stub. + +### Bracket interaction decisions (2026-06-11, step 8) +- **"Full path" on hover/focus** = hovered node + its entire feeder subtree + its winner's route to the FINAL; THIRD-PLACE lights both SFs. Non-path nodes dim via `.has-path` on the canvas. Computed from ref arithmetic (`floor(i/2)` up, `2i/2i+1` down), no tree lookup. +- **Zoom = CSS `transform: scale()` on the canvas + a `#bracket-zoom` box sized to `natural Γ— scale`** (transform doesn't affect layout, so the box gives the scroll container the right scrollable area). Pointer-anchored: scroll adjusted so the point under the cursor stays put. Clamped 0.4–2. +- **Natural canvas size is measured lazily** (`ensureMeasured()`) because the bracket panel can be `hidden` at render time (offsetWidth 0). +- **Pan + pinch via Pointer Events** with `touch-action: none` on the wrap (we own all gestures there; page scroll over the bracket is intentionally captured, like a map widget). +- **Drag–click conflict:** >5px movement sets a flag; a capture-phase click listener on the wrap swallows the click that ends a drag. The flag resets on the next `pointerdown`, so synthetic `el.click()` without pointerdown can be falsely suppressed in tests β€” dispatch pointerdown/up first. +- **Zoom level survives langchange re-renders** (module-level `view.scale`) but intentionally not reloads (not in prefs). + +### Simulation decisions (2026-06-11, step 9) +- **Separation of concerns in the tree:** `decide()` applies only real finished results; `applySimulation()` overlays user picks afterwards and never overrides a real result. Stale entries (winner no longer among the resolved teams) are silently ignored β€” same validation the prediction import (step 12) will use. +- **Sim UX:** "Simulation" toggle in the bracket toolbar; eligible nodes (both teams resolved, real result still `scheduled`) get dashed blue borders; clicking one opens a small native `` picker. An unequal score auto-selects the winner; a draw requires an explicit pick (penalties implied). Empty score defaults to 1-0 for the picked winner. +- **Storage format** is exactly the complement-spec shape: `wc2026_simulation = { "R32-6": { winner: "FRA", score: "2-1" } }` keyed by bracketRef, score oriented home-away. +- **`simchange` custom event** fires after any pick/reset; `schedule.js` listens and re-renders so simulated teams appear on knockout cards too (intentional leak β€” resolved tree is the single source). +- **Simulated nodes** show a blue "SIM" corner chip and blue scores; the modal shows none of this (it reads real results only). + +### Responsive/a11y decisions (2026-06-11, step 10) +- **Breakpoints:** ≀767 (tight spacing, bracket `--node-w: 168px`/gap 36px β€” connector stub offsets must stay at gap/2, overridden in the same media query), 768–1439 (single-row header, centered menu), 1440+ (container widens to 1360px). +- **Tabs follow the WAI-ARIA pattern:** roving tabindex + ArrowLeft/Right/Home/End in `initTabs()`; focus follows activation. +- **Dialogs get `aria-label`** set at open time (match name + phase); schedule count is `aria-live="polite"`; countdown has `role="timer"` + label. +- **Entry animations:** every unhidden `.panel` fades in; card grids (`.match-grid/.groups-grid/.stadiums-grid > *`) slide up with a 45ms stagger on the first 6 children, 260ms for the rest. All killed by `prefers-reduced-motion`. + +### Extra features decisions (2026-06-12, step 12 β€” done before step 11 at user request) +- **Favorites:** single global capture-phase click delegation in `app.js` handles every `.fav-btn` (schedule, groups, modal) and dispatches `favchange`; each view re-renders itself. Stars never trigger the card/modal click (guard via `closest('.fav-btn')`). Bracket shows highlight only (no stars β€” nodes too small). Favorite involvement = gold left border. +- **`getFavoriteMatches(matches, favorites)`** lives in `bracket.js` (needs `resolveBracketTeams`), imported by `schedule.js` for the "My matches" filter. +- **Time mode:** header `#time-toggle` flips `wc2026_prefs.timeMode` and dispatches `timemodechange`; `formatMatchTime()` already defaulted to the pref, so views just re-render. +- **Challenge:** sim entries for real-finished matches can't be created in the UI (locked) but old ones persist in storage β€” that's by design: predictions are made while matches are `scheduled`, then scored when results land. Card renders only when β‰₯1 finished knockout match exists. +- **Share/import:** `?prediction=` is stripped from the URL via `history.replaceState` whether applied or not (prevents re-prompt loops). Declining `confirm()` keeps local picks; unknown refs are rejected wholesale. +- **`.ics`:** RFC 5545 TEXT escaping (`\,` etc.) applied even though the spec template shows raw commas β€” RFC compliance wins; verified output imports with CRLF-only endings. +- **Custom events now in play:** `langchange`, `simchange`, `favchange`, `timemodechange` β€” all on `document`; views own their re-renders. + +### Build complete (2026-06-12, step 11 β€” all 12 steps done) +- README is the user-facing manual (run, deploy, JSON maintenance, localStorage keys, acceptance checklist). Keep it in sync when data formats change. +- **Verified at completion:** spec Β§18 criteria all pass; JS = 74 KB total (budget 300 KB); no root-absolute paths (GitHub Pages safe). **Not yet verified:** Lighthouse > 90 (needs a deployed URL or local Lighthouse run); actual GitHub Pages deploy. +- No commits made yet β€” repo initialized but empty; commit when the user asks. + +### Workflow +- **12-step build plan with approval gates** β€” user approves each step before the next starts; summary after each step. Plan: `C:\Users\Lucas\.claude\plans\read-r-lucas-kalil-projects-web-worldcup-goofy-meerkat.md`. +- **Git repo, commits only when the user asks.** + +--- + +## Known gotchas + +### 1. `fetch()` of JSON fails on `file://` +**Where:** any local testing of `index.html` +**Why:** browsers block `fetch` of local files (CORS/origin rules) +**Symptom if forgotten:** blank app, console CORS errors, wasted debugging +**Solution:** always serve via `python -m http.server` (or any static server) from the project root + +### 2. `.ics` requires CRLF line endings +**Where:** `assets/js/calendar.js` +**Why:** RFC 5545 mandates `\r\n` between lines; some calendar apps reject `\n` +**Symptom if forgotten:** exported event silently fails to import in Outlook/Apple Calendar +**Solution:** join VCALENDAR lines with `\r\n` explicitly + +### 3. Third-place slots are `null` until defined +**Where:** `data/bracket-config.json` β†’ `thirdPlaceAssignment` +**Why:** the 8 best third-place teams are only known after the group stage +**Symptom if forgotten:** crash or "undefined" team names in R32 rendering +**Solution:** `resolveBracketTeams()` must return placeholder labels ("Best 3rd #1", "Group A Winner") whenever a slot is `null` or the group isn't finished + +### 4. Claude Preview screenshots can hang (tooling, not app) +**Where:** Claude Preview panel during verification +**Why:** the preview window's screenshot pipeline occasionally gets stuck; `preview_eval` keeps working +**Symptom if forgotten:** wasted debugging hunting a nonexistent app freeze +**Solution:** `preview_stop` + `preview_start` recovers it; verify state via `preview_eval` first before suspecting the app + +### 5. Stale JS modules in the dev browser +**Where:** any JS edit while previewing via `python -m http.server` +**Why:** the server sends no cache headers, so browsers heuristically cache ES modules; a normal reload can keep serving old code +**Symptom if forgotten:** "module does not provide an export" errors or old behavior despite correct code on disk +**Solution:** `Promise.all(files.map(f => fetch(f, { cache: 'reload' })))` then `location.reload()`, or DevTools hard reload + +### 6. `setPointerCapture` on pointerdown kills element clicks +**Where:** `assets/js/bracket.js` drag/pan handling on `#bracket-wrap` +**Why:** capturing a pointer retargets the eventual `click` event to the capture element, so delegation via `event.target.closest('.bracket-match')` never matches β€” modal and simulation clicks silently die +**Symptom if forgotten:** bracket nodes unclickable with real input while synthetic `el.click()` tests still pass +**Solution:** capture only after the drag threshold (>5px) is exceeded, inside `pointermove`, wrapped in try/catch. **Always verify click flows with `preview_click` (trusted input), not `element.click()`.** + +### 7. GitHub Pages serves under a subpath +**Where:** all asset/data URLs in `index.html` and JS `fetch` calls +**Why:** project pages live at `https://.github.io//`, so root-absolute paths (`/data/...`) break +**Symptom if forgotten:** works locally, 404s on GitHub Pages +**Solution:** use relative paths (`data/matches.json`, `assets/...`) everywhere + +--- + +## Patterns for future changes + +### How to update real-world data (scores, schedule) +1. Edit `data/results.json` (scores/status) or `data/matches.json` (schedule). +2. Once group stage ends: fill `data/bracket-config.json` β†’ `thirdPlaceAssignment` (slot β†’ group letter). Nothing else changes. + +### Real-data migration (2026-06-12) +- `how-update.md` (project root) is the full runbook for replacing mock `data/*.json` with real World Cup 2026 data: file-by-file schemas, order of operations (stadiums β†’ teams β†’ groups β†’ bracket-config.round32 β†’ matches β†’ results β†’ thirdPlaceAssignment), and a cross-file integrity checklist (group membership, id ranges, bracketRef uniqueness, stadium name/city matches). +- Flags one open decision: `stadiums.json` has 30 entries (original bid shortlist) vs. the 16 venues actually used by the real tournament β€” confirm with user whether to trim before/while editing `matches.json`. + +### How to add a UI label +1. Add the key to both `en` and `pt` dicts in `assets/js/i18n.js`. +2. Use `t("key")` at the render site β€” never hardcode the string. + +### How to add a new localStorage preference +1. Extend the `wc2026_prefs` object shape (document the new field here). +2. Read/write only via `storage.js` `get`/`set`. + +### How to add a step summary after finishing a build step +1. Mark the step `[x] ~~...~~` in `.agents/TODO.md`. +2. Append any new decisions/gotchas here (never rewrite existing entries). +3. Rewrite `project-map.md` if structure/functions changed. +4. Stop and wait for user approval before the next step. + +--- + +## Success metrics + +- Lighthouse > 90; first render < 2s; total JS < 300KB. +- Spec Β§18 acceptance criteria all checked (tracked in README checklist, step 11). + +--- + +## Communication + +- User communicates in English/Portuguese mix; docs in English per conventions. +- **Ask before each build step** β€” never chain into the next step without explicit go-ahead.